Field Expressions#

BOUT++ field algebra now supports lazy expressions for many common operations. Instead of creating a temporary field for every +, -, *, /, pow, sqrt or abs, BOUT++ can keep the expression symbolic and evaluate it only when a concrete field or scalar result is needed.

This keeps ordinary model code readable while reducing temporary allocations and extra loops over the mesh. It is especially helpful for accelerator backends, where launching fewer kernels matters.

What stays lazy#

The following operations can form lazy expressions over Field2D, Field3D, Field3DParallel, and FieldPerp where the combination makes sense:

  • Arithmetic operators: +, -, *, /

  • Binary algebraic helpers such as pow and floor

  • Unary algebraic operators such as sqrt, abs, exp, log, sin, cos, tan, sinh, cosh, tanh, and SQ

  • Simple conditionals with if_else and if_else_zero

  • Reductions such as min, max, and mean

For example:

Field3D n, T;
Field3D result;

result = sqrt(SQ(n) + pow(T + 1.0, 2.0));

The right-hand side can stay lazy until the assignment to result.

When evaluation happens#

An expression is evaluated when BOUT++ needs actual storage or a scalar answer. Common triggers are:

  • assigning to a field

  • constructing a field from an expression

  • assigning a field expression into an Options object

  • calling scalar reductions such as min, max, or mean

Examples:

Field3D result = n + T;
options["rhs"] = n + T;
BoutReal max_value = max(abs(n + T), true);

Region-limited expressions#

Many algebraic operators take a region argument, usually defaulting to RGN_ALL. A lazy expression keeps track of that region.

Only values inside the requested region are guaranteed to be valid after materialization. This is useful for skipping guard-cell work when the result will only be used in a smaller region:

Field3D interior = abs(n, "RGN_NOBNDRY");

As with other region-limited field operations in BOUT++, code that later uses guard cells should communicate or otherwise fill those cells before relying on them.

Metadata propagation#

When an expression is materialized into a field, BOUT++ propagates the field metadata carried by the expression:

  • mesh pointer

  • cell location

  • field directions

  • for FieldPerp, the y-index

This means expressions are intended to behave like ordinary field operations in user code. Compatibility checks still apply: combining fields on different meshes or incompatible staggered locations is an error.

Mixed field types#

Several mixed-type combinations are supported directly:

  • Field2D with Field3D: the 2D quantity is broadcast in z

  • FieldPerp with matching perpendicular data: the operation uses the FieldPerp y-index

  • pow follows the same mixed-rank pattern, so either operand can be Field2D or Field3D and the result is a Field3D

  • expressions involving metric components may return Coordinates::FieldMetric, which is Field2D or Field3D depending on how BOUT++ was built

In practice, this means code such as:

Coordinates::FieldMetric grad = coords->J / coords->g_22;
Field3D rhs = density * temperature + background_2d;

can use the same algebraic style even when metric dimensionality or field rank differs.

Conditionals#

if_else selects between two algebraic branches without forcing the branches to be precomputed:

Field3D rhs = if_else(use_source, source * density, sink * density);

if_else_zero(condition, expr) is a shorthand for selecting either an expression or zero:

Field3D rhs = if_else_zero(include_drive, drive * profile);

This is particularly convenient when optional source terms are enabled or disabled by compile-time or run-time logic.

Field3DParallel and FCI#

Lazy expressions can also carry parallel-slice information. This matters when working with Field3DParallel on FCI meshes:

  • materializing into Field3DParallel preserves yup and ydown slices

  • materializing the same expression into plain Field3D keeps only the main field values

  • on FCI meshes, operands contributing to a Field3DParallel expression must have compatible parallel slices available

For example:

Field3DParallel f_par, g_par;
Field3DParallel result = sqrt(f_par + g_par);

On a non-FCI mesh, assigning a lazy expression to Field3DParallel still evaluates the main field, but no parallel slices are retained.

Reductions on expressions#

Reductions can operate directly on expressions instead of requiring an intermediate field:

BoutReal rms = sqrt(mean(SQ(n - n0), true, "RGN_NOBNDRY"));
BoutReal max_error = max(abs(lhs - rhs), true);

This is often clearer than explicitly constructing a temporary field, and it avoids extra storage.

Relation to GPU execution#

Lazy field expressions are the high-level path to reducing temporary work. They are a good default when ordinary field algebra expresses the operation clearly.

For more control, especially when you want to fuse derivative operators into a single explicit loop, see GPU support.

See also#