ODE and DAE API
Symbolic ordinary and differential-algebraic equation systems.
ODE
Symbolic system of first-order ordinary differential equations.
Represents the system d(state_vars)/dt = rhs driven by time_var.
state_varslist[Expr]Symbolic state variables (e.g.
[x, y]).rhslist[Expr]Right-hand side expressions, one per state variable.
time_varExprThe independent time variable.
Example:p = alkahest.ExprPool() t, x = p.symbol("t"), p.symbol("x") ode = alkahest.ODE([x], [-x], t) # dx/dt = -x
Build an explicit first-order system d(state_vars)/dt = rhs.
state_varslist[Expr]Symbolic state variables.
rhslist[Expr]Right-hand sides, one per state variable and in the same order.
time_varExprThe independent variable.
All three arguments are positional; there are no keyword forms.
ODE(state_vars, rhs, time_var) is accepted as a synonym.
For an implicit or constrained system use DAE instead, and for
a scalar higher-order equation use lower_to_first_order().
Example:
# Harmonic oscillator as a first-order system: x' = v, v' = -x
ode = alkahest.ODE.new([x, v], [v, -x], t)Build the first-order system equivalent to equations written with applied functions.
equationslist[Expr]One expression per unknown, each read as
= 0, written withy(t)anddiff(y(t), t)(any order). They must be affine in the highest derivative of each unknown, with an invertible coefficient matrix.functionslist[Expr]The unknowns,
[y(t), z(t), ...](undefined functions oft).time_varExprThe independent variable
t.
The state of a function with highest derivative of order m is
[y, y_dot, ...] (m symbols named like
functions_to_symbols() names them). Refusals: E-DIFF-005/007
for an unknown that is not an undefined function of t; E-ODE-030
when the system does not reduce (wrong equation count, an unknown that
is never differentiated, a nonlinear or singular leading part, i.e. a
DAE). A leading coefficient that needs != 0 is refused here; pass
those equations to dsolve_system(), which reports the condition.
experimental.dsolve_system(ode_or_equations, assumptions=None, *, functions=None, t=None, real_form=True)
Closed-form solution of y' = A y + f(t). Accepts an ODE or a
list of equations in y(t) form (then functions and t are
required, and the result carries "functions": {y(t): expr}). With
real_form the closed form of a real system is written with
cos/sin/cosh/sinh wherever the imaginary part is proved
zero (see to_real_form()).
alkahest.lower_to_first_order(var, rhs, order, time_var) — lower a scalar
higher-order ODE to an equivalent first-order system.
Takes the pieces of the equation, not an ODE object: the scalar
unknown, the right-hand side, the order of the derivative on the left, and
the independent variable. All four are required and positional.
varExprThe scalar unknown, e.g.
x.rhsExprRight-hand side of
d^order(var)/dt^order = rhs.orderintOrder of the equation (
2forx'').time_varExprThe independent variable.
- ODE
A first-order system whose state is
[var, var', …, var^(order-1)].Example:ode = alkahest.lower_to_first_order(x, -4 * x, 2, t) ode.order # 2
DAE
Symbolic differential-algebraic equation system.
A DAE is a system of implicit equations mixing differential and algebraic
constraints: F(t, variables, derivatives) = 0.
equations : list[Expr]
Implicit equations (each equals zero).
variables : list[Expr]
Algebraic and state variables.
derivatives : list[Expr]
Derivative expressions corresponding to variables.
time_var : Expr
The independent time variable.
Use pantelides() to reduce the differential index before simulation.
n_equationsintn_variablesinttime_varExprindexint | NoneDifferentiation index, set by
pantelides()on the DAE it returns;Noneon a DAE that has not been index-reduced.Example:p = alkahest.ExprPool() t, x, dx = p.symbol("t"), p.symbol("x"), p.symbol("dx/dt") dae = alkahest.DAE.new([dx - x], [x], [dx], t) # x' = x dae.equations() # [-x + dx/dt]
Differentiation index, or None if it has not been computed.
pantelides() sets it on the DAE it returns: 0 for an explicit
ODE, 1 for a semi-explicit index-1 system, 3 for the Cartesian
pendulum — the number of differentiations needed to determine every
derivative, including one for the algebraic variables.
Build a DAE from implicit equations.
equationslist[Expr]Implicit equations, each meaning
g = 0. Writex' = fasdx - f, using a separate symbol for the derivative.variableslist[Expr]Dependent variables (state and algebraic).
derivativeslist[Expr]Symbol standing for the time derivative of
variables[i], e.g.pool.symbol("dx/dt"). Alkahest does not parse the name; the positional pairing is what makes it a derivative. It may be shorter thanvariables: the trailing variables are then algebraic (their derivative, if ever needed, is namedd{name}/dt). A variable listed as another's derivative is a jet of that unknown, not a new one.time_varExprThe independent variable.
- DaeError
E-DAE-004whenderivativesis longer thanvariables, an entry is not a symbol, a symbol is repeated, ortime_varis listed among the unknowns.Example:x' = u, y' = w, u' = -lamx, w' = -lamy - 1, x2 + y2 = 1
dae = alkahest.DAE.new( [dx - u, dy - w, du + lam * x, dw + lam * y + one, x2 + y2 - one], [x, y, u, w, lam], [dx, dy, du, dw], t, )
The implicit equations, each meaning g = 0.
After pantelides() or rosenfeld_groebner() this is how you
see which equations were added by differentiation.
alkahest.pantelides(dae) — structural index reduction of a DAE.
Matches equations to unknowns on their highest derivatives (Pryce's
Σ-method, equivalent to Pantelides' algorithm), differentiates each
equation as often as that matching requires, and returns the reduced
DAE — the original equations followed by their derivatives — not a
report object. Read them with DAE.equations() and the new jets with
DAE.derivatives().
DAE.index on the result is the differentiation index: 0 for an
explicit ODE, 1 for a semi-explicit index-1 system, 2 for
x' = y, x = sin t, 3 for the Cartesian pendulum.
Before returning, the Jacobian of the reduced system with respect to the highest derivatives is checked to be non-singular at generic sample points (exact rational arithmetic); a structural answer the coefficients contradict is refused, never returned.
- DaeError
(a
ValueErrorsubclass)E-DAE-002when an equation would need more than 10 differentiations — trydae_index_reduce(), which falls back torosenfeld_groebner();E-DAE-003when there is not one equation per unknown function or no matching exists;E-DAE-004for a malformed DAE;E-DAE-005when the reduced Jacobian is singular.Example:reduced = alkahest.pantelides(dae) reduced.index # 3 for the Cartesian pendulum reduced.equations() # original equations plus the differentiated ones
Sensitivity analysis
alkahest.sensitivity_system(ode, params) — build the forward sensitivity ODE.
alkahest.adjoint_system(ode, objective_grad) — build the adjoint ODE.
Acausal modeling
Acausal component-based modelling system.
An AcausalSystem aggregates components connected through
Port objects (potential/flow pairs). Add components with
add_component(), wire ports together with connect(), and
call flatten() to convert the component network into an equivalent
DAE suitable for simulation or index reduction.
Example (RC circuit):
p = alkahest.ExprPool()
t = p.symbol("t")
src = alkahest.voltage_source("V1", p.symbol("Vs"))
res = alkahest.resistor("R1", p.symbol("R"))
cap = alkahest.capacitor("C1", p.symbol("C"))
sys = alkahest.AcausalSystem(p)
sys.add_component(src["component"])
sys.add_component(res["component"])
sys.add_component(cap["component"])
sys.connect(src["component"].port("V1.p"), res["component"].port("R1.p"))
sys.connect(res["component"].port("R1.n"), cap["component"].port("C1.p"))
sys.connect(cap["component"].port("C1.n"), src["component"].port("V1.n"))
dae = sys.flatten(t)Add a component (e.g. from resistor(), capacitor(),
voltage_source()) to the system.
Connect two ports: equates their potentials and balances their flows
(a.potential == b.potential, a.flow + b.flow == 0).
A physical component (resistor, capacitor, voltage source, …) with named
Port connectors and internal constitutive equations.
Construct components via resistor(), capacitor(), or
voltage_source(), then register them on an AcausalSystem
with AcausalSystem.add_component() and wire them together with
AcausalSystem.connect().
alkahest.resistor(name, resistance) — create a resistor component.
Returns a dict {"name", "n_equations", "n_ports", "component"}; the
"component" entry is a Component usable with
AcausalSystem.add_component and .port(name).
alkahest.capacitor(name, capacitance) — create an ideal capacitor
component (C * dv/dt = i).
Returns a dict {"name", "n_equations", "n_ports", "component"}; the
"component" entry is a Component usable with
AcausalSystem.add_component and .port(name).
alkahest.voltage_source(name, voltage) — create an ideal voltage
source component (v_p - v_n = V).
Returns a dict {"name", "n_equations", "n_ports", "component"}; the
"component" entry is a Component usable with
AcausalSystem.add_component and .port(name).
Hybrid systems
Ordinary differential equation system with discrete events.
A HybridODE wraps a continuous ODE and a list of
Event objects. Each event specifies a guard condition and a
reset map that is applied when the guard crosses zero.
Construction:
hybrid = alkahest.HybridODE.new(ode)
hybrid = hybrid.add_event(event)n_eventsintNumber of discrete events attached to this system.
Differential algebra
No docstring.
rosenfeld_groebner(
dae,
order=None,
max_prolong_rounds=None,
params=None,
eliminate=None,
minimal=False
)
alkahest.rosenfeld_groebner(dae, order=None, max_prolong_rounds=None) —
Rosenfeld–Gröbner-style differential elimination.
Prolongs the system (differentiates each equation, introducing higher jets as new indeterminates) and computes a Gröbner basis after each round, until differentiating adds nothing new to the ideal or the round budget runs out. The basis is the set of algebraic consequences of the differential system — the input–output relations elimination is after.
daeDAEThe system, polynomial in its variables and derivative symbols.
orderstr, optionalMonomial order —
"grevlex"(default),"grlex"or"lex". Use"lex"when you want elimination-ordered generators.max_prolong_roundsint, optionalProlongation budget (default 8). Nonlinear jets often do not saturate in finitely many algebraic steps, so hitting the budget is normal and sets
RosenfeldGroebnerResult.truncated.paramslist[Expr], optionalSymbols to put in the coefficient field
Q(params)rather than in the ring (M9). Without this every free symbol is a ring variable, so a model parameter enlarges the monomial order, the pair schedule and the staircase — which is the difference between eliminating states fromQ[states, jets, params]and fromQ(params)[states, jets]. Withparamsthe return type isParametricRosenfeldGroebnerResultand the basis isParametricGroebnerBasis; without it nothing changes.The parameters must not be DAE variables, derivatives or the time variable.
eliminatelist[Expr], optionalVariables the caller intends to eliminate — typically the unobserved states. Their whole jet chain goes with them: naming
xalso namesdx/dt,d2x/dt2, … as prolongation introduces them. Supplying this makesParametricRosenfeldGroebnerResult.minimal_prolongation_roundsavailable and enables the over-prolongation warning. Requiresparams.minimalbool, optionalStop at the first prolongation round whose elimination ideal with respect to
eliminateis non-empty, instead of prolonging to the budget. One prolongation too many is expensive out of all proportion — on SIR it is 0.002 s and one 4-term relation against 20.9 s and thirteen generators of up to 233 terms — and the over-supplied result is correct, so nothing else signals it. Requireseliminate.Not a minimality guarantee: see
ParametricRosenfeldGroebnerResult.minimal_prolongation_roundsfor the scope, in particular for multi-output models.
- RosenfeldGroebnerResult or ParametricRosenfeldGroebnerResult
Read the relations with
result.final_basis().to_exprs().
- UserWarning
When
eliminateis given, the elimination ideal was already non-empty at an earlier round, andminimalwas not set.Example:t, x, dx = p.symbol("t"), p.symbol("x"), p.symbol("dx/dt") dae = alkahest.DAE.new([dx - x], [x], [dx], t) r = alkahest.rosenfeld_groebner(dae, max_prolong_rounds=1) r.consistent # True r.final_basis().to_exprs() # the eliminated relations, as Expr
dae = alkahest.DAE.new([dx - a*x], [x], [dx], t) r = alkahest.rosenfeld_groebner(dae, params=[a], max_prolong_rounds=1) r.final_basis().conditions()
V2-13 — result of Rosenfeld–Gröbner-style differential elimination.
Returned by rosenfeld_groebner(). The eliminated relations are in
final_basis(); read them with result.final_basis().to_exprs().
consistentboolFalseiff the unit ideal was reached — the system has no common jet solution over ℚ, i.e. the equations are contradictory.truncatedboolTrueif prolongation stopped atmax_prolong_roundsrather than because the differential chain saturated. A truncated basis is a sound set of consequences of the system but need not be complete, so "not in the basis" does not mean "not a consequence".prolongation_roundsintNumber of prolongation rounds that contributed new relations.
The prolonged DAE: the input system plus the derivative jets
introduced while differentiating it.
The jet variables indexing the basis, in exponent-slot order.
These are the symbols the elimination actually ran over — the time
variable, the declared states and derivatives, and every higher jet
(d2x/dt2, …) introduced by prolongation.
The saturated Gröbner basis, or None when the system is
inconsistent.
The returned GroebnerBasis knows its variable ordering, so
final_basis().to_exprs() gives the eliminated relations as
Expr.
Example:
r = alkahest.rosenfeld_groebner(dae, max_prolong_rounds=1)
for eq in r.final_basis().to_exprs():
print(eq, "= 0")