NCL Website header
NCL Home > Documentation > Manuals > Reference

NCL expressions overview

Properties of expressions

The results of expressions are values. Anything like functions, constant values, and variable references are expressions. Expressions can also be nested by using algebraic operators to string together expressions. Expressions are grouped into subsets. There are algebraic expressions that take numeric operands and produce numeric results. There are logical expressions that take any type of operand and produce logical results. There are functions that can produce any type of value and can produce variables. Finally there are arrays that group one or more expressions into a multi-dimensional value.

Multi-dimensional arrays and expressions

NCL's algebra, like Fortran 90, supports operations on entire arrays rather than single scalar values like C and PASCAL. For array operations to work, both operands must have the same number of dimensions and same size dimensions, otherwise an error condition occurs. Furthermore, the data types of each operand must be equivalent, or one operand must be coercible to the type of the other operand. The following is an example of two entire arrays being multiplied together in a single expression; note that a is integer and b is float:
    ;
    ; Define two [2]x[2] arrays
    ;
    	a = (/ (/ 1, 2 /), (/ 3, 4 /) /)
    	b = (/ (/ .1, .01 /), (/ .001, .0001 /) /)
    
    ;
    ; Multiplication of two [2]x[2] arrays
    ;
    	c = a * b
    	print(c)
    
    	Variable: c
    	Type: float
    	Total Size: 16 bytes
                         4 values
    	Number of Dimensions: 2
    	Dimensions and sizes:   [2] x [2]
    	Coordinates: 
    	(0,0)   0.1
    	(0,1)   0.02
    	(1,0)   0.003
    	(1,1)   0.0004
    
The above example illustrates how multi-dimensional arrays are handled in NCL expressions. The above statement c = a * b is equivalent to looping through both dimensions of a and b, multiplying each element and assigning it to the respective element in c. The following code segment is equivalent to the above example. This shows how NCL's syntax saves a lot of typing.
    dims = dimsizes(a)
    c = new(dims,float)
    do i = 0,dims(0) - 1
    	do j = 0, dims(1) - 1
    		c(i,j) = a(i,j) * b(i,j)
    	end do
    end do
    

Scalar values and expressions

Scalar values are special cases when considering array operations. When scalar values appear in an expression with a multi-dimensional value, the scalar value is applied to each element of the multi-dimensional value. For example, each element in an entire array can be doubled by multiplying the array by the scalar value 2. The following example shows this.
    	a = (/ (/ 1, 2 /), (/ 3, 4 /) /)
    ;
    ; Multiplication of an entire array by a single scalar
    ;
    	b = a * 2
    	print(b)
    	
    	Variable: b
    	Type: float
    	Total Size: 16 bytes
                         4 values
    	Number of Dimensions: 2
    	Dimensions and sizes:   [2] x [2]
    	Coordinates: 
    	(0,0)   2
    	(0,1)   4
    	(1,0)   6
    	(1,1)   8
    

Expressions and missing values

Missing values were discussed in the variables section of this document. There it was noted that the "_FillValue" attribute is a special attribute that allows for specific elements of an array to be tagged as missing. When any NCL expression is being evaluated, NCL ignores elements that are equal to the value of the "_FillValue" attribute for each variable. When a missing value is ignored, the result of the expression will contain a missing value at the corresponding array index.

However, missing values can often lead to an error condition. Therefore NCL provides a couple of standard mechanisms to avoid their propagation:

  • The function ismissing returns a logical result True or False depending on whether its input evaluates to a missing value.
  • The logical expressions False .and. Missing and True .or. Missing return the logical results False and True respectively, by lazy evaluation, as described below.

If more than one term in an expression contains a missing value and the values are not equal, the missing value of the value of the left-most term in the expression containing a missing value is used in the output. The following example illustrates this.

    ;
    ; Assign three variables values and missing values. Each variable's 
    ; missing value is different.
    ;
    	a = (/1, 2, -99/)
    	a@_FillValue = -99
    	b = (/4, -999, 5/)
    	b@_FillValue = -999
    	c = (/-9999, 7, 8/)
    	c@_FillValue = -9999
    
    ;
    ; Assignment to variable d of an expression containing all 
    ; three variables with different missing values.
    ; 
    	d = a * b * c
    
    ;
    ; Assignment to variable of an expression containing two 
    ; variables with different missing values.
    	e = b * c
    
    ;
    ; Results of first assignment. Note resulting missing value.
    ;
    	print(d)
    	
    	Variable: d
    	Type: integer
    	Total Size: 12 bytes
                         3 values
    	Number of Dimensions: 1
    	Dimensions and sizes:   [3]
    	Coordinates: 
    	Number Of Attributes: 1
    	  _FillValue :  -99
    	(0)     -99
    	(1)     -99
    	(2)     -99
    
    ;
    ; Results of second assignment. Note resulting missing value.
    ;
    	print(e)
    
    	Variable: e
    	Type: integer
    	Total Size: 12 bytes
                         3 values
    	Number of Dimensions: 1
    	Dimensions and sizes:   [3]
    	Coordinates: 
    	Number Of Attributes: 1
    	  _FillValue :  -999
    	(0)     -999
    	(1)     -999
    	(2)     40
    

Types of expressions

Algebraic

Algebraic expressions operate on arrays of basic numeric types. There are nine main algebraic operators: multiply, divide, exponent, plus, minus, modulus, less than selection, greater than selection, and negative. All of the operators except negative take two operands. Also, in general, both operands should be the same type. If they are not, NCL attempts to coerce them to identical types. If this fails, a type mismatch error is generated. Additionally, either the operands must have the same number of dimensions and dimension sizes, or one operand can be a scalar value. The operators operate on the entire array of data.

The following is a list of the operator characters. The operators are listed in order of precedence: the first one has the highest precedence. The precedence rules can be circumvented by using parentheses '( )' around expressions that should be computed first.


- Negative
^ Exponent
* Multiply / Divide % Modulus # Matrix multiply
+ Plus - Minus
< Less than selection > Greater than selection
Negative   (-)
The negative operator negates the value of its operand. The operand can be any numeric type. If it is an array, each element of the array is negated.

Note that because this operator has the highest precedence, it will get applied first, and in some cases, in a way you might not expect. For example, the expression:

 
    x = - 3^2
will yield "x = 9" because it is equivalent to:
    x = (-3)^2
This is unlike Fortran, in which:
       x = - 3**2
gives you "x = -9".

Note how the use of parentheses can change the results:

    print(- (3+2)^2)      ----> yields 25
whereas:
    print(- ((3+2)^2))    ---->  yields -25
Finally, note that if "-" appears between two numbers or variables, then it is treated as "minus" rather than "negation". Hence, the following will give you "x = -9":
    x = 0 - 3^2
Exponent   (^)
The left operand is "raised" to the power of the right operand. If the left operand is negative, then the right side must be positive and greater-than-or-equal to 1.0. Currently, NCL doesn't support imaginary numbers; it generates an error in this situation. The result type of this operator is always a floating point number unless double precision values are used.
Multiply   (*)
Multiplies left operand by right operand. No special conditions or restrictions.
Divide   (/)
Divides left operand by right operand. When both operands are integer types, the result is an integer type. Therefore, the decimal remainder is truncated.
Matrix Multiply   (#)
The operands must be one or two dimensional arrays. Higher dimensionality is not supported. This operand computes the dot product of two single dimension arrays. For 2 D arrays the 1st dimension of the left operand must be the same size as the 0th dimension of the right. The output dimensionality will be the [0th dimension of the left] x [1st dimension of the right].
Modulus   (%)
The result is the integer remainder of integer division. Both operands must be integer types.
Plus   (+)
Adds left operand to right operand. No special conditions or restrictions. If the operands are strings, then the strings are concatenated.
Minus   (-)
Subtracts right operand from left operand. No special conditions or restrictions.
Less-than selection   (<)
For arrays, the less-than selection operator selects all values in the left operand that are less than the corresponding value in the right operand. If the value of the left side is greater than or equal to the corresponding value of the right side, then the right side value is placed in the result. For a scalar and an array, the same selection rules apply, but the scalar is compared against each value in the array operand.
Greater-than selection   (>)
For arrays, the greater-than selection operator selects all values in the left operand that are greater than the corresponding value in the right operand. If the value of the left side is less than or equal to the corresponding value of the right side, then the right side value is placed in the result. For a scalar and an array, the same selection rules apply, but the scalar is compared against each value in the array operand.

Logical

Logical expressions are formed by relational operators. There are ten relational operators: less-than-or-equal, greater-than-or-equal, less-than, greater-than, equal, not-equal, and, or, exclusive-or, not. All of these operators yield logical values, in other words scalars or arrays consisting of the values True, False and/or Missing. See the data types overview documentation for more information about the logical data type. And, or, exclusive-or, and not require logical operands, and the rest accept any type.

With the exception of and and or in certain situations (see Lazy evaluation below) all of the logical operators function like the algebraic operators with respect to operations between arrays and arrays, arrays and scalars, and scalars and scalars. For array operands, each corresponding element is compared, and the result is an array of logical values, the size and dimensionality of the operands. A single scalar can be compared against each element in an array. The result is an array of logical values the size of the array operand. Finally, when two scalars are compared, the result is a scalar logical value.

The following is a list of the logical operators ordered by their precedence.

		.le. 	less-than-or-equal
		.lt.	less-than
		.ge.	greater-than-or-equal
		.gt.	greater-than
		.ne.	not-equal
		.eq.	equal
		.and.	and
		.xor.	exclusive-or
		.or.	or
		.not.	not
Less-than-or-equal   (.le.)
Returns True if the left operand is less than or equal to the right operand.
Less-than   (.lt.)
Returns True if the left operand is less than the right operand.
Greater-than-or-equal   (.ge.)
Returns True if the left operand is greater than or equal to the right operand.
Greater-than   (.gt.)
Returns True if the left operand is greater than the right operand.
Not-equal   (.ne.)
Returns True if the left operand is not equal to the right operand.
And   (.and.)
Requires both operands to be logical types. Returns True if and only if both operands are True. (See Lazy evaluation below)
Or   (.or.)
Requires both operands to be logical types. Returns True if either operand is True. (See Lazy evaluation below)
Exclusive-or   (.xor.)
Requires both operands to be logical types. Returns True if one of the operands is True and the other is False.
Not   (.not.)
Takes a single operand which must be logical. Returns True if the operand is False; returns False if the operand is True.

Lazy evaluation

For the and and or logical operators only, NCL supports a feature common to many programming languages known as "lazy evaluation" (also sometimes referred to as "short circuit evaluation"). Basically this means that when the logical value of the whole expression can be determined from the left operand alone, the value of the right operand is not considered in the determination of the expression result value. The fact that NCL logical expressions can evaluate to Missing in addition to True and False makes the situation a bit more complicated. Also there are some special considerations depending on which of the operands are scalars and which are arrays. As of version 5.2.0, the result table for both scalar operands and array operands (on an element-by-element basis) is as follows:

Left OperandOperatorRight OperandResultLazy evaluation
False.and.Any logical valueFalseyes
True.and.FalseFalseno
True.and.TrueTrueno
True.and.MissingMissingno
Missing.and.Any logical valueMissingno
True.or.Any logical valueTrueyes
False.or.TrueTrueno
False.or.FalseFalseno
False.or.MissingMissingno
Missing.or.Any logical valueMissingno

Note that prior to version 5.2.0, lazy evaluation only occurred if the left operand was scalar. Also note that a plausible argument can be made that NCL's implementation is not true "lazy evaluation" because, if the right side is Missing when the short circuit route is taken, the end result (True or False depending on the operation) is different from what it would have been (Missing) if both operands were considered. However, whether or not NCL's implementation can be considered "real" lazy evaluation, its actual behavior is extremely useful in practice for avoiding unwanted propagation of missing values that can easily result in errors when they appear in the wrong context. For example, this code fragment uses lazy evaluation to avoid attempting to reference a variable if it is has a missing value and therefore would cause an error in the if statement evaluation:

  if (.not. ismissing(x_scalar) .and. x_scalar .gt. 0) then
      x_log = log10(x_scalar)
  end if
Even though the result table above is valid for both scalar and array operands as of version 5.2.0 or later, there are some behavioral differences depending on which of the operands are scalar and which are arrays. These differences all stem from the observation that if the left side is scalar and meets the condition for lazy evaulation (True for or or False for and) the result can be determined immediately without further consideration. In this case, NCL has operators that completely bypass evaluation of the right hand side, whether it is array or scalar. In all other cases, both the left and right expressions are evaluated prior to applying the operator, and the logical result table above applies element by element to the left and right expression result values. Here is a table that shows the possible situations.

Lazy evaluation condition met? Left operand typeRight operand typeRight operand evaluated?Result type
yesscalarscalarnoscalar
yesscalararraynoscalar
yesarrayscalaryesarray
yesarrayarrayyesarray
noscalarscalaryesscalar
noscalararrayyesarray
noarrayscalaryesarray
noarrayarrayyesarray

Two points to note about this table:

  1. If the left operand is an array, you cannot depend on a left-hand side test to guard against possible error conditions resulting from evaluating an expression on the right side. For example, you cannot avoid division by 0 and the consequences of an error result with code like this:
    v = (/5,4,0,3,4/)
    res = v .ne. 0 .and. (4.0 / v) .ge. 1
    
  2. In the case of a scalar value on the left side and an array on the right side, the result can be either a scalar or an array depending on whether the lazy evaluation condition is met. This is inconsistent with a general principle of NCL that operations with a scalar and an array operand always yield an array. However, since this has been NCL's behavior since the beginning, and because it would not be easy to change without major code redesign, it is left in place for now. However, it is possible that this behavior might change to conform with NCL's usual conventions in a future release.

Arrays

An array expression combines either scalar values or other arrays into a single array result. An array expression is made up of expressions separated by commas ',' and enclosed in '(/' and '/)' (called array designator characters). Arrays can be made up of any of the basic types as well as graphical objects. Currently, arrays of files are not supported. Each element separated by commas must have the same dimensionality as the rest of the elements in the array expression. If the types are not the same, the coercion rules are used to convert all of the array elements to the same type. If this fails, an error message is generated.

The following are examples of array expressions.

    (/ 1, 2, 3, 4, 5 /)
    (/ (/ 1, 2, 3 /)^2, (/ 4, 5, 6 /)^3,(/ 7, 8, 9 /)^4 /)
    (/ a - b, b + c, c / d /)
    

If a single variable is enclosed in array designator characters '(/' and '/)', only the variable's value is referenced and not its associated attributes, dimension names, and coordinates. The array designator characters are useful in performing value only assignment.

Functions

Functions are expressions because they return a value. Functions do not necessarily have to always return the same type or size array every time they are called. Functions are called by referencing their name and providing an argument list. Arguments can be any type, including files. They can also be constant values, variables, or subsections of variables.

Functions can be defined to restrict parameters to be specific types, to have specific numbers of dimensions, and to have specific dimension sizes. If a parameter must be a certain type, then the coercion rules are applied to it.

One very important thing to note is that all parameters are passed by reference in NCL, meaning a change of a value in a variable within the function changes the value of the parameter.

However, if a parameter is coerced before the function is called, changes within the function will not be reflected in the parameter, because coercion can only be applied in a single direction. A warning message is given in this instance.

Expression results can also be passed to functions as parameters. However, since expression results are not referenced by a variable, there is no way that changes made to the parameter can be passed out from the function, unless it is returned by the function.