Scientific Computing Language

Hello world

To print a string on the screen the print statement is used:

print('Hello world!') 

Statements

The building blocks of the language are the statements. The statements are separated either by semicolons ; or by new lines. A statement can be:

  1. variable

  2. print

  3. view

  4. tuple of variables

  5. import from a Python module

  6. export of arbitrary parameter

  7. function definition

  8. variable update

NOTE: The order of evaluation does not depend on the order of statements. Rather the order of evaluation is determined by the dependecies between variables and their parameters.

NOTE: The print and view statements are evaluated in the same order as they are in the program. See more details here.

Variables

Variables are the most used statements. A variable is initialized immediately with its definition with a variable name on the left hand side and a parameter on the right hand side of the = sign. A variable may not be redefined, i.e. the variable name may not be used to define other parameters. The variable’s parameter can be a string, integer, boolean, a data structure, etc. A parameter can also be a reference to a variable. Parameters are immutable, i.e. they cannot be modified after having been created.

Example:

var_1 = 'Hello world!'
var_2 = var_1
var_3 = 0.5 [meter]

In the first line var_1 is a variable name. However, in the second line, var_1 is a parameter of the variable with name var_2, more specifically a reference to the variable with name var_1. The third statement is a variable with name var_3 and a parameter of numeric floating point type with units meters.

The print statement

The print statement displays the values of one or more parameters on the screen. The syntax is print(par1[, par2[, ...]]) where the optional parameters are displayed in square brackets [].

Example:

print(var_1) # "print the value of variable with name var_1"
var_1 = 'Hello world!' # initialize variable with name var_1 with a string literal

In this example the variable with name var_1 is defined after the print statement. Because the value of variable with name var_1 has to be printed on the screen it has to be evaluated first. Therefore, the variable with name var_1 (i.e. the second statement) is evaluated first and only after that the print statement is evaluated.

NOTE: This is the behavior in the case of instant evaluation and deferred on-demand evaluation. In other forms of deferred evaluation, var_1 may be printed without having been evaluated. In the latter case n.c. (not computed) is printed instead of the value of var_1 that is not available yet.

NOTE: In the case of on-demand evaluation policy, only these variables or parameters are evaluated that are used in print or [export statements]. In this case, the variable var_1 would not be evaluated if there would be no print statement.

Further more detailed explanation of the behavior of input/output operations is provided separately.

The view statement

This statement displays its parameters graphically. The syntax is

view <mode> (parameter_1, parameter_2, ...)

There are currently three modes lineplot, scatterplot and structure that will be explained below. Further modes will be implemented in future upon demand.

Plotting datasets in 2D

This is achieved with the modes lineplot and scatterplot that have the meaning of plot types here. Further modes of 2D plotting will be implemented in future but all these have common parameters patterns.

data shape

parameter 1

parameter 2

parameter 3

parameter 4

long-form data

values, Table

column to use as x axis, String

column to use as y axis, String

optional units for output, Series(String)

wide-form data

values, Series(1D-Array), shape of values:array: (len(index), len(columns)) or (len(columns), len(index))

index, Series(scalar) or 1D-Array

columns, Series(scalar) or 1D-Array or Tuple(scalar)

not used

wide-form data

values, 2D-Array, shape: (len(index), len(columns)) or (len(columns), len(index))

index, Series(scalar) or 1D-Array

columns, Series(scalar) or 1D-Array or Tuple(scalar)

not used

xy data

values, Series(scalar) or 1D-Array

index, Series(scalar) or 1D-Array

not used

not used

Examples:

  • Long-form data:

    tab = (
    (number: 1, 2, 3, 4, 1, 2, 3, 4) [meter],
    (type_: 'square', 'square', 'square', 'square', 'cube', 'cube', 'cube', 'cube'),
    (value: 1, 4, 9, 16, 1, 8, 27, 64),
    (time: 0., 1., 2., 3., 4., 5., 6., 7.) [hour]
    )
    units = (units: 'cm', '', '', 'min')
    view lineplot (tab, 'number', 'value', units)
    
  • Wide-form data:

    ind = (number: 1, 2, 3, 4) [meter]
    val_ser = (values: [1, 4, 9, 16], [1, 8, 27, 64])
    columns = (columns: 'square', 'cube')
    view lineplot (val_ser, ind [cm], columns)
    
  • XY-data:

    ind = (number: 1, 2, 3, 4) [meter]
    sqr = (square: 1, 4, 9, 16)
    view lineplot (sqr, ind [mm])
    view lineplot (sqr:array, ind:array)
    view lineplot (sqr, ind:array)
    view lineplot (sqr:array, ind)
    

Visualize / analyze atomic structures

Parameters of type AMML Structure can be viewed using this simple syntax:

view structure (struct)

Units in print and view parameters

Parameters of numeric types in print and view statements are printed with their units. The parameter’s value can be printed in other than the default units by specifying the units in the print or view parameter. For example, to print a mass in grams we can use this:

mass = 1.5 [kg]
print(mass [g])

The output of this print statement is 1500.0 [gram].

Tuples of variables

Variables can be defined in bulk as tuples. This is only possible if the parameter is of Tuple type with the same length (see Tuple data structure below). This is particularly useful if several variables must be initialized with the elements of a tuple returned by a function.

(a, b, c) = (1, 'abc', true) # a tuple parameter
(d, e) = func(a) # function func returning a tuple

Export statement

The export statement allows exporting the value of a variable to a file or a URL. The common syntax is <var name> to (file <path> | url <url_string>)

a = `Hello world!`
a to file 'hello_world.yaml'

The file extension (the portion of the path after the .) indicates the format in which the value will be exported. Currently, YAML (extensions ‘.yml’ or ‘.yaml’) and JSON (extension ‘.json’) formats are supported for all variable types. Domain formats are supported for some domain-specific types (see the relevant sections).

NOTE: If a file with the same name as specified already exists, the export statement will not work, i.e. export allows no file overwriting.

NOTE: While relative paths, as in the example above, are supported it is strongly recommended to use absolute paths, especially in the workflow evaluation mode.

Other statements

Further statements are imports from external Python modules and function definitions. They are more advanced and are outlined here and here, respectively.

Comments and white space

Comments are ignored and not interpreted. All input after the hash sign # up to the end of the same line is ignored. All input enclosed by a pair of three double quotes """ is ignored. All white space is needed only to separate keyword inputs otherwise white space is ignored.

# this is a comment
a = 'Hello' # this is a comment
b = 1 """
This
   is 
a multi-line comment.   
"""

Type

Variables and parameters have type. The type is fixed with the definition and checked before evaluation begins, i.e. it is static. The type determines in what operations a parameter or a variable can be used. If a statement contains operations on incompatible types a Type error is issued. In most cases, type errors are issued before the evaluation begins, as long as the types of parameters and variables can be evaluated without computing their values.

String type

A string literal is a uncode string enclosed by single or double quotes. Empty strings are allowed.

hello = "Hello world!"
empty = '' # empty string
print(hello == empty) # string match, result: false
print(hello != empty) # string match, result: true

Currently, no operations on strings, except for string match, are available.

Boolean type

Parameters and variables of boolean type have values of either true or false. Unlike in other languages, parameters and variables of other types have no boolean values. Also variables and parameters of boolean type cannot be interpreted as integers. Boolean literals are parameters matching either true or false:

bool_1 = true
bool_2 = false

Boolean expressions

Using the operators and, or and not and any parameters of boolean type, arbitrary boolean expressions can be composed. Expressions with and and or are currently not short circuiting.

a = true and (false or true)
print(not a) # result: false

Boolean expressions always have boolean type.

Numeric types

The parameters of numeric type can be integer (Integer), floating point (Float) or complex (Complex) quantities.

Numeric expressions

Using the operators +, -, *, / and **, and any numeric parameters, arbitrary numeric expressions can be composed.

a = 2
b = (2.0*a + 1)**2 - 1.5

Numeric expressions always have numeric type.

Physical units

All parameters of numeric type have physical units assigned. Here some examples:

number = 1 # dimensionless integer type quantity
length = 2.0 [meter] # floating point type quantity with units meter
s = number + length; print(s)

Because number is dimensionless it cannot be added to length and the following evaluation error occurs:

Dimensionality error: None:3:5 --> number + length <--
Cannot convert from 'dimensionless' (dimensionless) to 'meter' ([length])

In contrast to type, physical units are checked only during evaluation. This is why this error message will not be issued if we remove the print(s) statement.

NOTE: Dimensionless quantities also have units. This becomes evident in the error message above. These units are [dimensionless]. These can be optionally specified, for example number = 1 [dimensionless].

Complex numbers

Complex numbers have the format real [+-] imag [jJ] where real and imag are the real and the imaginary part of the complex number, respectively. Complex numbers can be used as scalars, as well as in Series and Arrays. The real and the imaginary part of a complex scalar can be retrieved using the built-in functions real() and imag(), respectively. For example, one can define a function to compute the complex conjugate:

conjg(z) = real(z) - imag(z) * (0 + 1 j)

Comparison expressions

Comparison expressions are defined for numeric types. They can include one of the operators ==, !=, >, <, >=, <=. In comparisons with string, boolean and complex operands only the operators == and != are allowed. String matching using the operators == and != can be regarded as a comparison expression. The comparison expressions always have boolean type.

b = 2 < 1
print(b) # result: false

Data structures

In data structures several parameters of different or the same types can be combined to express a certain type of interrelation. The types that are no data structures will be called scalar types.

Tuple

Parameters of any type can be combined in a fixed order using a Tuple. The syntax is like in this example: t = (a, 1.3, 'abc', false); a = 2. Tuples are most useful if used as parameters of tuples of variables but also to pass bundled heterogeneous data.

A tuple containing one parameter should contain a comma before the closing parenthesis, otherwise it may be parsed as an expression. For example, use (1,) or (true,) but not (1) or (true). Empty tuples are not allowed as input.

Series

The Series contains a list of parameters of the same type. The common syntax of Series is: (name: e1[, e2[, e3[...]]]) [units for numeric type]. The Series data structure must have a name. The Series elements are the items between the : and the ) separated by commas. Series literals must have at least one element. Series of numeric types must have elements of the same units. Series literals have syntax that is show in the following example.

a = 3. [s]
s1 = (time: 1. [s], 2. [s], a)
s2 = (lengths: 1., 2., 3.) [nm]
s3 = (booleans: true, bval); bval = false
s4 = (numbers: 0, 3, -2)

The units of Series of numeric type can be specified if all elements are numeric literals, either after every single element, as shown for s1, or after the closing parenthesis ) as shown for s2. If an element is another numeric parameter then units may not be specified as it is shown for the parameter a in s1 (the parameter holds the units itself). If units specification is omitted, as in s4, then the Series still has units but it is dimensionless. One can also specify [dimensionless] but this is optional. Series of non-numeric types may have no units. Empty Series is not allowed as input.

Table

The Table data structure consists of an ordered set of Series parameters of the same length that can be viewed as columns. There are two different syntaxes for Table literals:

t1 = ((numbers: 1, 2, 3), (lengths: 1., 2., 3.) [nm])
t2 = Table ((numbers: 1, 2, 3), s2)
s2 = (lengths: 1., 2., 3.) [nm]

The rows of the Table are Tuples of the Series elements at the same position (see subscripting operations below). Empty tables are not allowed.

Arrays

Arrays are data structures with fixed types. Compared to Series an Array has no name, may be multi-dimensional (whereas Series is one-dimensional) and may not be used as a column in Tables. In addition, an Array may only have numerical, boolean and string type whereas Series may have any type. Array literals have the following syntax:

pbc = [True, True, False] # switching boundary condition
cell = [[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]] [angstrom] # cubic unit cell

Empty arrays are not allowed as input.

Internal functions

An internal function, or simply a function, is a named expression in which some variables are bound. The bound variables (also called dummy variables) are provided comma-separated in a list enclosed by parentheses. The function is called by specifying the parameters to be used for each of the dummy variables:

f(x) = x**3 - 2*x**2 + 3*x - 4 # function definition (a statement)
print(f(1)) # function call (a parameter)
b = f(2); print(b) # another call
g(x) = b*x # another function definition where b is unbound

NOTE: The dummy variables are bound to the scope of the relevant functions. This is why the same names can be reused as dummy variables in other functions.

NOTE: The list of dummy variables may not be empty, e.g. f() = 2*a is not valid. In all such cases, simply the expression 2*a should be used instead of the call f(). If the expression has to be used more than once, then a variable b = 2*a can be defined and a reference to b can be used.

The if function and if expression

The value of the if function depends on the value of the first parameter that is always of boolean type: if it is true then the value is equal to the value of the second parameter. If the value of the first parameter is false then the value of the function equals the value of the third parameter. All three parameters are mandatory.

c = if(true, 1, 2); print(c) # result: 1
...
b = f(...) # function call with boolean type
d = if(b, 'b was true', 'b was false'); print(d)

The if expression has a different syntax but the same meaning (semantics) as the if function. The same examples are shown below with using the expression syntax.

c = 1 if true else 2; print(c) # result: 1
...
b = f(...) # function call with boolean type
d = 'b was true' if b else 'b was false'; print(d)

Expression nesting

Expressions of the same type can be nested by using parentheses (). One typical use case is nesting comparison expressions in boolean expressions.

print((3 > 4) or (-1 <= 0)) # result: true

Operations with Series

In the following, operations with parameters of type Series will be outlined.

Slice

A slice of a Series is a new parameter of type Series returning a selection of elements from a parameter of type Series.

Syntax: [start:stop] or [start:stop:step]

In the first syntax the default step is 1.

lens = (length: 1, 2, 3, 4, 5, 6) [m]
print(lens[0:1]) # result: (length: 1) [meter]
print(lens[0:4:2]) # result: (length: 1, 3) [meter]
print(lens[6:0:-1]) # (length: 6, 5, 4, 3, 2) [meter]
print(lens[6::-1]) # invert the order, result: (length: 6, 5, 4, 3, 2, 1) [meter]

Subscripting

Individual Series elements can be retrieved by subscripting. The syntax is [index]. The type and, if appropriate, the units of the returned parameter are the same as these of the Series.

lens = (length: 1, 2, 3, 4, 5, 6) [m]
print(lens[0]) # first element: 1 [meter]
print(lens[1]) # second element: 2 [meter]
print(lens[-1]) # last element: 6 [meter]
print(lens[-2]) # second to last element: 5 [meter]

Retrieve the name

The name of a parameter of Series type can be retrieved using :name:

lens = (length: 1, 2, 3, 4, 5, 6) [m]
print(lens:name) # result: 'length'

The type of Series name is string type.

Retrieve the array

The array of a parameter of Series type can be retrieved using :array:

lens = (length: 1, 2, 3, 4, 5, 6) [m]
print(lens:array) # result: [1, 2, 3, 4, 5, 6] [m]

The returned type is Array type.

Map function

The map() function iterates over the tuples from the elements of the second, third, etc. parameters, which all must be Series of the same length, and calls the function defined as first parameter with the tuple of each iteration.

s = (length: 1, 2, 3) [m]
sqr(x) = x*x
area = map(sqr, s)
print(area) # result (area: 1, 4, 9) [meter ** 2]

The first parameter in map() functions can also be a so-called lambda function. Lambda function is an internal function with no name.

s = (length: 1, 2, 3) [m]
area = map((x: x*x), s)
print(area) # result (area: 1, 4, 9) [meter ** 2]

A typical use case of map() function is to apply operations element-wise to one or more series. In the following example an expression with the elements of two series is computed.

sx = (sx: 0.1, 1.3, -1.2)
sy = (sy: 2.1, -3.7, 4.6)
print(map((x, y: 3*x + 2*y - 1), sx, sy))

The type of the map() function is the same as the type of the first map() parameter. In the example above, this will be the type of the lambda function (x, y: 3*x + 2*y - 1).

Reduce function

The reduce() function calls the function of two arguments provided as first parameter successively and cumulatively with the elements of the Series provided as second parameter.

Example:

s = (n: 1, 2, 3, 4)
prod(x, y) = x*y
print(reduce(prod, s)) # with internal function, result: 24
print(reduce((x, y: x*y), s)) # with lambda function
# with nested function calls:
print(prod(prod(prod(1, 2), 3), 4)) # equivalent
print(prod(prod(prod(s[0], s[1]), s[2]), s[3])) # equivalent

The type of the reduce() function is the same as the type of the first parameter of reduce(). In the example above, this will be the type of the lambda function (x, y: x*y) or of the function prod(x, y).

By combining map() and reduce() various algorithms can be implemented, for example scalar product of two series:

s1 = (s1: -1., 2., -3.); s2 = (s1: 1., -2., 3.)
print(reduce((x, y: x+y), map((u, v: u*v), s1, s2))

Functions sum, all and any

The sum() function has two syntaxes:

  • If sum() has only one parameter, then it must be of type Series of numeric type and computes the sum of the parameter elements. In this case sum(...) is equivalent to reduce((x, y: x+y), ...).

  • If sum() has more than one parameter then these parameters must be of scalar numeric type and the function computes the sum of all parameters.

The all() function has two syntaxes:

  • If all() has only one parameter, then it must be of type Series of boolean type and has true value when all parameter elements have true values; otherwise it has false value. In this case all(...) is equivalent to reduce((x, y: x and y), ...).

  • If all() has more than one parameter then these parameters must be of scalar boolean type and has true value when all parameters have true values; otherwise it has false value.

The any() function has two syntaxes:

  • If any() has only one parameter, then it must be of type Series of boolean type and has true value if any parameter element has true value; otherwise it has false value. In this case any(...) is equivalent to reduce((x, y: x or y), ...).

  • If any() has more than one parameter then these parameters must be of scalar boolean type and has true value when any parameter has true value; otherwise it has false value.

Filter function and filter expression

The filter function performs a selection of elements from a Series type parameter that satisfy a condition. The condition is provided as a boolean type internal or lambda function.

print(filter((x: x>2), (n: 1, 2, 3, 4))

The filter expression is semantically equivalent to the filter function but has a different syntax (note particularly the variable reference).

s = (n: 1, 2, 3, 4); print(s where n > 2)

More complex conditions are possible. For example:

s = (n: 1, 2, 3, 4)
ffunc = filter((x: (x < 2) or (x > 3)), s)
fexpr = s where n < 2 or n > 3
print(ffunc, fexpr) # (ffunc: 1, 4) (n: 1, 4)

The type of filter functions and expressions is always the same as the type of the input Series.

Operations with Table

In the following, operations including parameters of type Table will be outlined.

Slice

Similarly to Series, a slice from a Table is a Table. The syntax and semantics are the same as with Series.

tab = ((bools: true, false, true), (numbers: 1, 2, 3))
print(tab[0:2]) # result: ((bools: true, false), (numbers: 1, 2))
print(tab[3::-1]) # result: ((bools: true, false, true), (numbers: 3, 2, 1))

Subscripting

Individual Table rows can be retrieved by subscripting. The type of this operation is always a Tuple type. The syntax is [index].

tab = ((bools: true, false, true), (numbers: 1, 2, 3))
print(tab[0]) # result: (true, 1)

Retrieve a column

Individual Table columns can be retrieved using the syntax .<name> where <name> is the column name.

tab = ((bools: true, false, true), (numbers: 1, 2, 3))
print(tab.numbers) # result: (numbers: 1, 2, 3)

The type of this operation is always Series type.

Retrieve the list of column names

The type of this operation is Series type.

tab = ((bools: true, false, true), (numbers: 1, 2, 3))
print(tab:columns) # result: (columns: 'bools', 'numbers')

Filter expressions applied to Table

Filter expressions can be used with parameters of Table type.

tab = ((temp: 100., 200., 300.) [K], (pressure: 1., 2., 3.) [bar])
print(tab where temp > 100 [K])
print(tab select pressure where temp > 100 [K])

After evaluation this result is printed:

((temp: 200.0, 300.0) [kelvin], (pressure: 2.0, 3.0) [bar])
((pressure: 2.0, 3.0) [bar])

Operations with Array

In the following, operations including parameters of type Array will be outlined. The examples are with an array of integer type but the operation can be used with all data types supported in arrays.

Subscripting

The purpose of subscripting is to retrieve individual elements or sub-arrays.

Retrieving individual elements

The following example demonstrates the retrieval of a single array element:

a = [[1, 2], [3, 4]] [m]
print(a[1][0]) # result: 3 [meter]

To retrieve a single array element, the number of subscripts must be the same as the number of dimensions (axes) of the array. The returned type is a scalar type the same as the array data type.

Retrieving sub-arrays

If the number of subscripts is less than the number of array dimensions (axes) then a sub-array is returned:

a = [[1, 2], [3, 4]] [m]
print(a[0]) # result: [1, 2] [meter]

Subscripting errors

If the subscript is larger than the largest index in the relevant axis then an Invalid index is issued:

a = [[1, 2], [3, 4]] [m]
b = a[2] # Index out of range, index: 2, data length: 2
c = a[1][3] # Index out of range, index: 3, data length: 2

If the number of subscripts exceeds the number of axes in the array then a Type error is issued:

a = [[1, 2], [3, 4]] [m]
b = a[0][0][0] # Invalid use of index in type Quantity

Because a[0][0] is a Quantity (a numerical scalar type) it cannot be subscripted with [0].

Slice

The slice returns selected elements within the same axis of an array. The slice syntax is the same as with Series and Table. The slice can be applied only once per statement after all (optional) subscripts. Example:

a = [[1, 2], [3, 4]] [m]
print(a[1][0:1:1]) # result: [3] [meter]
print(a[::]) # result: [[1, 2], [3, 4]] [meter]
print(a[0:1][0]) # Syntax error, intended result [1, 2] [meter]
print(a[0:1][0:1]) # Syntax error, intended result: [[1, 2]] [meter]

To concatenate multiple slices or to combine slices with subscripts in arbitrary ordering, with the current syntax one has to define auxiliary variables:

a = [[1, 2], [3, 4]] [m]
# print(a[0:1][0]) # Syntax error, intended result [1, 2] [meter]
b0 = a[0:1]; print(b0[0]) # result [1, 2] [meter]

# print(a[0:1][0:1]) # Syntax error, intended result: [[1, 2]] [meter]
e0 = a[0:1]; print(e0[0:1]) # result: [[1, 2]] [meter]

# print(a[1:][0:]) # Syntax error, intended result: [[3, 4]] [meter]
f0 = a[1:]; print(f0[0:]) # result: [[3, 4]] [meter]

Operations with Tuple

Currently, no operations are defined for Tuple type.

Range function and range expression

The range function with syntax range(start, stop, step) creates Series in a given numeric range from start to (but not including) stop incrementing by step.

lens = range(1 [m], 6 [m], 1 [m])
print(lens) # result: (lens: 1, 2, 3, 4, 5) [meter]

There is range expression with the same semantics as the range function.

lens = range from 1 [m] to 6 [m] step 1 [m]
print(lens) # result: (lens: 1, 2, 3, 4, 5) [meter]

The parameters start, stop and step must be of the same scalar numeric type, i.e. either integers or floating-point numbers.

Imported objects and functions

External objects and functions from arbitrary Python modules can be imported and used in the language. There are two syntaxes:

  1. use <module>.<name>

  2. use <name1>, <name2> from <module[.submodule]>

The first syntax is shorter while the second allows several imports in the same statement and namespace including modules with arbitrary number of submodules separated by periods.

The imported objects and functions can be used as parameters (references) with the same names as in the imports.

Examples

The example below shows using an imported function len() from the builtins module from the standard Python package using the first syntax:

use builtins.len
s = (numbers: 1, 2, 3)
print(len(s)) # result: 3

The following example demonstrates usage of imported object pi and functions sin() and cos() from the numpy module (package) using the second syntax.

use pi, sin, cos from numpy
print(sin(2.*pi)) # result: -2.4492935982947064e-16
print(cos(2.*pi)) # result: 1.0

Loading parameters from file or URL

Some parameters have the optional syntax allowing to load them from file or download from a URL. The common syntax is

<var name> = <parameter> from (file <path> | url <url_string>) 

Current list of parameters supporting this syntax: Quantity, Bool, String, Series, Table, BoolArray, StrArray, IntArray, FloatArray and ComplexArray. Additionally, there are domain-specific parameters that also support this syntax.

The <path> is a string that must contain the path to the input data file. While relative paths are supported it is strongly recommended to use absolute paths, especially in the workflow evaluation mode. By default the internal serialization format (in JSON) is used and the file type may be JSON (filename extension json) or YAML (file name extensions yml or yaml). Domain-specific parameters may support further domain-specific formats.

Dealing with missing data or default values

Sometimes measurements include data gaps, i.e. some data elements may be missing. Furthemore, in modeling often some parameters have default values that should be used without specifying them. For these two use cases, Series allow the placeholders null and default to specify unknown elements. Note that null and default have no type because they are no parameters. The Series type is inferred from the type of all other elements that must have the same type. If all elements are either null or default then the type of the series is null (unknown type).

numbers = (numbers: 1, 2, null)
sqrs = map((x: x**2), numbers)
print(sqrs) # result: (sqrs: 1, 4, null)
print(sqrs[2]) # result: null

During processing, the null and default elements are skipped. If some quantity or an element in a data structure critically depends on such elements it gets null value. For example, print(any((bools: true, false, null))) yields true and not null because the missing value is not critical for the value of the any() function.

Dealing with failures

As all other parameters in SCL, the variables are immutable objects. This means that they cannot be modified (updated) once they have been defined and initialized.

This behavior can lead to the following situation. Let us have this model that we run in an interactive session:

Input > a = 1
Input > b = a / 0
Input > c = 2
Input > f(x) = b * x
Input > %start
Input > print(f(c))
Arithmetic error: None:2:1 --> b = a / 0 <--
float division by zero

Obviously, we will never be able to use function f because of the run-time error in evaluating b. One work-around is to define a new variable b_correct and a new function f_correct that uses b_correct instead of b:

Input > b_correct = a / 2
Input > f_correct(x) = b_correct * x
Input > f_correct(c)
Output > 1.0

Though this is the recommended approach, there are some cases when this is not desirable or practical. One case is if the model contains a large number of the descendants of variable b. In this case all these statements have to be rewritten. Furthermore, the statements that are descendants of b will never be evaluated but also cannot be removed from the model which is likely to lead to confusions. In another case, the evaluations have not failed but a mistake leading to wrong results is found in a statement. Thus the affected statement and all its descendants have to be invalidated or removed.

Effectively, this can be accomplished by updating the statement with the error / mistake:

Input > b := a / 2
Input > print(f(c))
Output > 1.0

Using this approach, all descendants have been found and re-evaluated.

Currently, there are three restrictions to this approach:

  1. The set of references in the updated variable parameter must be identical with that in the parameter of the original variable. For example, in the example above, the update b := 1 / 2 is not valid because the reference to a is not used. Also, b := c + 1 is not valid because it includes a reference to c that is not in the original version of b.

  2. A variable can be updated only once per model extension. The update becomes ambiguous otherwise. For example, the update b := 1 / a; b := a / 2 is not valid.

  3. The variable may not be part of a parameter variation across several models (a model group). For example, if variable a is in such a variation, that has been added with the statement vary ((a: 1, 2)) then it cannot be updated with a := 3 in further model extensions.

The approach described here is recommended if the evaluation error is caused by the model input. If the error during evaluation is due to failure of computing nodes, network or file system, or other similar failures, then the evaluation can be rerun in an interactive session by using the %rerun magic.