Field Expressions¶
Fields can be used directly in mathematical expressions without having
to specify indices. Any unindexed Field can be used freely
without having to care about the underlying subgrid on which they are defined
(i.e. their staggeredness):
>>> import sympy as sp
>>> from sofia import Grid, Field, Staggered
>>> G = Grid(ndim=2)
>>> u = Field("u", domain=G)
>>> v = Field("v", domain=G, staggered=Staggered.XFACE)
>>> f = u**2 + sp.sin(v) + sp.exp(u * v)
Differentiation¶
Writing discrete equations from continuous differential equations is tedious and error-prone. The symbolic engine is set up to perform these discretisations automatically for any differential equation at any discretisation accuracy order.
This functionality is provided through the sofia.pde.fdm.diff() function, which extends the standard
sympy.diff to support extra features.
The diff function computes derivatives of expressions with respect to one or more
variables. For example computing a the derivative of expression f
with respect to x:
>>> from sofia.pde import diff
>>> f = u * v
>>> diff(f, G.x)
Derivative(u, global.x)*v + Derivative(v, global.x)*u
Higher-order derivatives can be computed by specifying the order, e.g for second order:
>>> diff(f, G.x, 2)
2*Derivative(u, global.x)*Derivative(v, global.x) + Derivative(u, (global.x, 2))*v + Derivative(v, (global.x, 2))*u
Partial derivatives for multivariable expressions are supported as are mixed partial derivatives:
>>> diff(f, G.x, G.y)
Derivative(u, global.x)*Derivative(v, global.y) + Derivative(u, global.y)*Derivative(v, global.x) + Derivative(u, global.x, global.y)*v + Derivative(v, global.x, global.y)*u
Here diff(f, x, y) computes the mixed partial derivative \(\frac{\partial^2 f}{\partial x \, \partial y}\).
The diff function also supports an allow_expand=False option:
>>> diff(f, G.x, allow_expand=False)
Derivative(u*v, global.x, allow_expand=False)
When used it returns the derivative in a symbolic unevaluated form, meaning it will not automatically expand or apply rules like the product rule.
Discretisation Order and Stencil Direction¶
The derivatives produced by diff are automatically discretised into
finite-difference stencils. The accuracy order is determined by the
discretisation_order parameter, which controls how many grid points are
used in the stencil. Higher orders increase accuracy at the cost of wider
stencils. The order can be specified at three levels (highest precedence
first): on a specific diff call, on the Field, or on the Grid.
The side argument controls the direction of the finite-difference stencil:
"center"(default) — symmetric central difference, highest accuracy for a given stencil width."forward"— points ahead of the evaluation point."backward"— points behind the evaluation point.
For a full description of how derivatives, interpolation, and field indexing are resolved into discrete array expressions, see the Automatic Discretisation section.
Upwinding¶
Upwind derivatives can be computed using the upwind option in diff.
This produces a symbolic Piecewise expression that automatically selects the derivative stencil
based on the sign of the expression:
Forward difference if the expression is negative
Backward difference if the expression is positive
Centered difference otherwise
>>> diff(u, G.x, upwind=True)
Output:
Piecewise(
(Derivative(u, global.x, side='forward', discretisation_order=1), u < -1e-6),
(Derivative(u, global.x, side='backward', discretisation_order=1), u > 1e-6),
(Derivative(u, global.x, discretisation_order=2), True)
)
By default, the forward/backward stencils use order discretisation_order - 1, while the centered
stencil uses order discretisation_order. The stability of finite difference solvers is a complicated topic and differently “sided”
discretisation stencils can show different behaviour. Using a lower-order accurate stencil for the forward/backward derivatives is a common occurence.
For more control, the sofia.pde.fdm.diff_upwind() can be called directly and the upwind_discretisation_order argument can be used to override
the default of discretisation_order - 1. The variable parameter allows the upwind direction to be determined by the sign of an
arbitrary expression instead of the field being differentiated.
>>> from sofia.pde import diff_upwind
>>> diff_upwind(u, G.x, discretisation_order=4, upwind_discretisation_order=2)
Output:
Piecewise(
(Derivative(u, global.x, side='forward', discretisation_order=2), u < -1e-6),
(Derivative(u, global.x, side='backward', discretisation_order=2), u > 1e-6),
(Derivative(u, global.x, discretisation_order=4), True)
)
Example¶
For the 2D convection-diffusion equation in component form:
the right hand sides can be written as:
>>> F_u = diff(u, G.x, 2) + diff(u, G.y, 2) - diff(u**2, G.x) - diff(u*v, G.y)
>>> F_v = diff(v, G.x, 2) + diff(v, G.y, 2) - diff(v**2, G.y) - diff(u*v, G.x)
which is displayed as:
Note
Mathematical expressions are processed by SymPy, which automatically performs certain simplifications (without changing the mathematical meaning), which can lead to unexpected rearrangements. See also the SymPy documentation for more details.
Vector Equations¶
The above example is much simpler to write in vectorized form:
by using the sofia.domain.grid.Grid.ihat and sofia.domain.grid.Grid.jhat unit vectors:
>>> from sofia.pde import laplacian, divergence
# Define vector field
>>> uv = u * G.ihat + v * G.jhat
# (x | y) is shorthand for the outer product of x and y
>>> F_uv = laplacian(uv) - divergence(uv | uv)
After composing the vector expression, the individual components can be
extracted by F_uv.coeff(...):
>>> F_u = F_uv.coeff(G.ihat)
>>> F_v = F_uv.coeff(G.jhat)
Which gives the same result as in the example above:
These expressions are also automatically discretized taking into account the staggeredness of the fields, see the Automatic Discretisation section for more detail.
Field shifts¶
Half-integer (or integer) offsets can be applied to any field using the
Field.s method. This shifts the
logical position at which the field is evaluated, without indexing into
the underlying array directly. This is useful when you need to reference
neighbouring values — for instance in boundary conditions or conditional expressions — without dropping down to the indexed interface.
u
u.s(1, 0)
u.s(1, -1/2)
u.s(j=2)
When the expression is discretised, any half-integer positions that do not coincide with stored data points are resolved by interpolation automatically.