Equations¶
A model is written as on paper: unknowns, parameters, known fields and equations in text, with terms on whichever side you like.
import numpy as np
import conatus as cn
m = cn.Model(cn.Box(x=cn.periodic(2.0), z=cn.walls(-0.5, 0.5)),
name="Rayleigh-Benard 2D", Ra=2500, Pr=1) # parameters by keyword
u, p, T = m.velocity("u"), m.pressure("p"), m.scalar("T")
m.equations("""
momentum: dt(u) + grad(p) - Pr*lap(u) = Pr*Ra*T*ez - u·grad(u)
continuity: div(u) = 0
heat: dt(T) - lap(T) = -u·grad(T)
""")
m.at("bottom", u=0, T=+0.5)
m.at("top", u=0, T=-0.5)
print(m.explain())
Declarations¶
Call |
Declares |
|---|---|
|
the model and its parameters (numbers, by keyword); later |
|
an unknown |
|
a vector and a scalar unknown with a role that Conatus checks (incompressibility, Boundary conditions) |
|
a known field; the argument names say which coordinates it depends on |
|
a named operator, substituted where it is used |
|
a pointwise function of a scalar field, used as |
|
the model is an eigenvalue problem in |
|
equations, one per line, optionally labelled |
|
boundary conditions (Boundary conditions) |
Every unknown is declared before the equations. Names are checked: a name already used, a
coordinate name, an operator name or a name starting with tau_ is refused.
A function of your own enters the equations by its name. It is evaluated on the dealiased grid
(not alias-free, as explain() says), and takes and returns arrays (CuPy arrays on the GPU):
import numpy as np
import conatus as cn
m = cn.Model(cn.Box(x=cn.periodic(2 * np.pi)), name="damped by tanh", eps=0.1)
m.scalar("u")
m.function("g", np.tanh, derivative=lambda u: 1 - np.tanh(u)**2)
m.equations("dt(u) - eps*lap(u) = -g(u)")
sim = m.simulate(N=32, scheme="RK443", dt=1e-2).initial(u=lambda x: np.sin(x))
sim.run(steps=20)
The kind of problem follows from the equations¶
The equations contain |
Problem |
Solve with |
|---|---|---|
|
evolution (IVP) |
|
a declared eigenvalue |
eigenvalue problem |
|
neither, linear |
boundary-value problem |
|
An evolution model also gives its linear stability problem: m.linearize(about={u: 0, T: lambda z: -z})
returns normal modes \(\propto e^{st}\) about that base state.
Which terms are implicit¶
In an evolution problem you do not split the terms into a left and a right side. Conatus does it,
term by term, with one rule: a term linear in the unknowns (constant coefficients,
parameters, given fields) is treated implicitly; a nonlinear term or a source is explicit.
A term changes side with its sign, and explain() prints where each one went:
Term placement (IMEX split, automatic; override with implicit(...) / explicit(...)):
[0] momentum
implicit grad(p) (linear, constant coefficients)
implicit dt(u) (linear, constant coefficients)
implicit -Pr*lap(u) (linear, constant coefficients)
implicit -T*Ra*Pr*ez (linear, constant coefficients; moved from the right side)
explicit -dot(u, grad(u)) (nonlinear, degree(s) [2] in the unknowns)
To override the rule, wrap a whole term: explicit(-u) treats a linear reaction explicitly,
implicit(...) forces a term implicit (refused on a nonlinear term).
Syntax¶
Written |
Meaning |
|---|---|
|
time derivative and vector calculus in the geometry’s coordinates |
|
partial derivative along a coordinate ( |
|
Cartesian unit vectors ( |
|
contraction (dot product); |
|
trace at a point; integral over the domain |
|
pointwise functions (reported as not alias-free) |
|
a component; numbers |
Precedence is Python’s: -u·grad(u) is the product of -u and grad(u). A line ending with an
operator or an open parenthesis continues on the next line; # starts a comment. The string is
never passed to eval: a small parser reads it, and each name is looked up among your
declarations.
Errors point at the column¶
UndeclaredVariableError: name 'lapp' is not declared
where: dt(T) - lapp(T) = 0
^ (column 9)
fix: ... Did you mean 'lap' or 'kappa'?
EquationSyntaxError: unexpected 'lap': an operator is missing
where: dt(T) - kappa lap(T) = 0
^ (column 15)
fix: write '*' for a product (Pr*lap(T), 2*u), '+' or '-' between terms
The suggestion is never applied for you. A product of two vectors written u*u is refused
(write u·u or outer(u, u)), and Dedalus spellings that Conatus does not have, such as
trace(grad(u)), are refused with the equivalent form (div(u)).
Behind the text¶
The same strings are accepted by the expert layer (problem.add_equation("...")), where each one is
parsed into exactly the expression the Python operators would build. conatus.ir.parse.GRAMMAR
is the full grammar, and to_string(eq) writes an equation back as text that parses to the same
expression (IR and operators). The parser is also available on its own: cn.parse_equation
reads "lhs = rhs" and cn.parse_expr one expression, against the names you pass (a mapping, a
problem, or a list of fields and parameters):
import conatus as cn
from conatus.expert import CartesianCoordinates, Chebyshev, Domain, Field, Parameter
cs = CartesianCoordinates("x"); x = cs["x"]
u = Field("u", Domain(cs, [Chebyshev(x, 16, (-1, 1), dealias=3/2)]))
nu = Parameter("nu", 0.1)
eq = cn.parse_equation("dt(u) - nu*lap(u) = -u*dx(u)", dict(u=u, nu=nu))
print(eq) # dt(u) - nu*lap(u) = -u*dx(u)
print(cn.parse_expr("u*dx(u)", [u])) # an IR expression: u*dx(u)