API Reference¶
Functions¶
- class Solverz.sym_algebra.functions.sin(*args)¶
The sine function.
- class Solverz.sym_algebra.functions.cos(*args)¶
The cosine function.
- class Solverz.sym_algebra.functions.exp(*args)¶
The exponential function, \(e^x\).
- class Solverz.sym_algebra.functions.Abs(*args)¶
The element-wise absolute value function:
\[\operatorname{Abs}(x)=|x|\]with derivative
\[\frac{\mathrm{d}}{\mathrm{d}x}{\operatorname{Abs}}(x)=\operatorname{Sign}(x).\]
- class Solverz.sym_algebra.functions.Sign(*args)¶
The element-wise indication of the sign of a number
\[\begin{split}\operatorname{Sign}(x)= \begin{cases} 1&x> 0\\ 0& x==0\\ -1&x< 0 \end{cases}\end{split}\]
- class Solverz.sym_algebra.functions.AntiWindUp(u, umin, umax, e)¶
For PI controller
\[\begin{split}\begin{aligned} \dot{z}(t) & =e(t) \\ u_{d e s}(t) & =K_p e(t)+K_i z(t) \end{aligned},\end{split}\]we limit the integrator output by setting, in the \(K_i>0\) case,
\[\begin{split}\dot{z}(t)= \begin{cases} 0 & \text { if } u_{\text {des }}(t) \geq u_{\max } \text { and } e(t) \geq 0 \\ 0 & \text { if } u_{\text {des }}(t) \leq u_{\min } \text { and } e(t) \leq 0 \\ e(t) & \text { otherwise } \end{cases}.\end{split}\]So the AntiWindUp function reads
\[\dot{z}(t)= \operatorname{AntiWindUp}(u_{\text {des }}(t), u_{\min }, u_{\max }, e(t)).\]
- class Solverz.sym_algebra.functions.Min(x, y)¶
The element-wise minimum of two numbers
\[\begin{split}\begin{split}\min(x,y)= \begin{cases} x&x\leq y\\ y& x>y \end{cases}\end{split}\end{split}\]
- class Solverz.sym_algebra.functions.MatVecMul(*args)¶
Legacy sparse matrix-vector multiply. Use
Mat_Mulinstead.MatVecMuldecomposes the sparse matrix into CSC components and calls a Numba-compatiblecsc_matvec. New code should useMat_Mul(A, x)which usesscipy.sparsedirectly and supports full matrix calculus.
- class Solverz.sym_algebra.functions.Saturation(*args)¶
The element-wise saturation a number
\[\begin{split}\operatorname{Saturation}(v, v_\min, v_\max)= \begin{cases} v_\max&v> v_\max\\ v& v_\min\leq v\leq v_\max\\ v_\min&v< v_\min \end{cases}\end{split}\]
- class Solverz.sym_algebra.functions.heaviside(*args)¶
The heaviside step function
\[\begin{split}\operatorname{Heaviside}(x)= \begin{cases} 1 & x >= 0\\ 0 & x < 0\\ \end{cases}\end{split}\]which should be distinguished from sympy.Heaviside
- class Solverz.sym_algebra.functions.ln(*args)¶
The ln function, \(ln(x)\).
Equations¶
- class Solverz.equation.eqn.Eqn(name: str, eqn)¶
The Equation object
- class Solverz.equation.eqn.Ode(name: str, f, diff_var: iVar | IdxVar | Var)¶
The class for ODE reading
\[\frac{\mathrm{d}y}{\mathrm{d}t}=f(t,y)\]where \(y\) is the state vector.
- class Solverz.equation.eqn.LoopEqn(name: str, outer_index=None, body=None, n_outer: int = None, var_map: Dict[str, object] = None, model=None, outer=None)¶
Equation declared as a parameterised scalar template that prints to a Python
for-loop in the generatedinner_F/inner_J.The motivation: SolMuseum’s network modules (
eps_network,heat_network,gas_network) write per-bus / per-node equations using Python for-loops that emit2 * n_bus(or more) scalarEqnobjects. Each scalar Eqn becomes its owninner_F<N>sub-function, and on the IES benchmark the resulting Numba LLVM compile time dominates the warm-up phase.LoopEqnexpresses the same thing as a SINGLE Eqn whose body is a scalar template parameterised by an outer indexi; the printer then emits ONEinner_F<N>containing afor i in range(n_outer):loop. One sub-function instead ofn_outer.- Parameters:
name : str
Equation name.
outer_index : sympy.Idx
The outer loop index. Each row of the residual is the body evaluated at one value of
outer_index.n_outer : int
Range of the outer loop. The residual block has size
n_outer.body : sympy.Expr
A scalar sympy expression built either from
sympy.IndexedBase(+var_map) or from SolverzIdxVar/IdxPara(+model). May involve theouter_index, innersympy.Sumover auxiliaryIdxsymbols, and arithmetic.model : Model, optional
When the body uses Solverz
IdxVar/IdxPara, pass the containingModelso LoopEqn can resolve eachname0back to a realVar/Param(needed for sparsity detection and CSR pre-computation). Mutually exclusive withvar_map.var_map : Dict[str, sSymBasic], optional
Legacy API. Maps each
sympy.IndexedBasename inbodyto the corresponding SolverzVar/Paraminstance. Mutually exclusive withmodel.
Substrate
Internally, the body is rewritten as a sympy expression built from
sympy.IndexedBase,sympy.Idx,sympy.Sum. Solverz’s ownIdxVar/IdxParaare opaque to sympy’ssubs/Sum.doit()/diff, so we can’t use them verbatim — LoopEqn walks the body once at construction and rewrites any SolverzIdxVar/IdxParainto the correspondingsympy.IndexedBaseform. The user is free to write either:Native Solverz syntax (recommended):
m.G[i, j]andm.ux[j]directly inside the body, and passmodel=mso LoopEqn can look up each name in the Model to resolve sparsity / dim / value info.Explicit sympy syntax (legacy): declare
G_sym = sp.IndexedBase('G')etc., build the body out of those, and pass avar_map={'G': m.G, ...}dict mapping each IndexedBase name to a SolverzVar/Param.
Both styles produce the same internal representation. Native syntax is terser and avoids the bookkeeping trap of forgetting to add an entry to
var_mapwhen adding a new reference.Examples
Native Solverz syntax (preferred):
m.ux = Var('ux', np.ones(nb)) m.uy = Var('uy', np.zeros(nb)) m.G = Param('G', csc_array(G_dense), dim=2, sparse=True) m.B = Param('B', csc_array(B_dense), dim=2, sparse=True) m.ix_pin = Param('ix_pin', ix_pin) i, j = sp.Idx('i'), sp.Idx('j') body = (m.ix_pin[i] - sp.Sum(m.G[i, j] * m.ux[j], (j, 0, nb - 1)) + sp.Sum(m.B[i, j] * m.uy[j], (j, 0, nb - 1))) m.ix_inj = LoopEqn( 'ix_inj', outer_index=i, n_outer=nb, body=body, model=m, )
Legacy
var_mapAPI (still supported):ux_sym = sp.IndexedBase('ux') G_sym = sp.IndexedBase('G') body = sp.Sum(G_sym[i, j] * ux_sym[j], (j, 0, nb - 1)) m.ix_inj = LoopEqn( 'ix_inj', outer_index=i, n_outer=nb, body=body, var_map={'ux': m.ux, 'G': m.G}, )
The generated
inner_Ffor either form is a single function containing one Pythonfor i in range(nb):loop, replacing the2 * nbscalar sub-functions the original loop pattern would emit.
- class Solverz.equation.eqn.LoopOde(name: str, outer_index=None, body=None, diff_var: iVar | IdxVar | Var = None, n_outer: int = None, model=None, var_map=None, outer=None)¶
A LoopEqn whose residual is the time derivative of a contiguous state-Var slice, i.e. an Ode with a scalar-template body.
\[\frac{\mathrm{d}y[i]}{\mathrm{d}t} = f(i,\, y,\, \text{params}) \quad\text{for}\ i = 0, \ldots, n_{\text{outer}} - 1\]where
y(diff_var) is a length-n_outerVar slice and the bodyfis a scalar sympy expression parameterised by the LoopEqnouter_index.Compared to
LoopEqn,LoopOde:Overrides
LHStosp.Derivative(diff_var, t)so the rest of Solverz’s DAE assembly (eqn_dictpartitioning intof_listvsg_list, mass-matrix construction,eval_lhs) sees it as anOdeand routes it to the differential-equation side of the state vector.Participates in the
LoopEqnmodule-printer dispatch via the existingisinstance(eqn, LoopEqn)branch, so it emits exactly ONEinner_F<N>sub-function with a Pythonforloop instead of one per state-cell (or per per-pipeOdeas the legacy heat-PDE pattern does). The residual vector returned by that sub-function is written to_F_[diff_var_state_slice]through the standardprint_eqn_assignment_with_precomputepath.
Multi-inheritance from
LoopEqnandOde(Eqnis the shared base) givesisinstancechecks on both theLoopEqn-specific module-printer path and theOde-specific DAE-split path the right answer. MRO putsLoopEqnbeforeOde, soLoopEqn.__init__is what runs first; we then manually overwriteLHSand attachdiff_var.- Parameters:
name : str
Equation name (same as LoopEqn).
outer_index : sympy.Idx
Scalar-template outer loop index.
body : sympy.Expr
Scalar body expression (same semantics as LoopEqn).
diff_var : iVar / IdxVar / Var
The state-Var slice being differentiated in time. Its runtime length must match
n_outer.model, var_map, n_outer : same as LoopEqn.
- class Solverz.equation.eqn.IndexSet(name: str, values)¶
Named set of integer indices used as the outer iteration range of a
LoopEqn/LoopOde.Pyomo’s
Setprimitive inspired the API. Two construction forms:Identity range —
Set('Nodes', n)wherenis a positiveint. The set is the full range[0, n). The translator emits a plainfor i in range(n):loop; no auxiliary Param is stored.Explicit subset —
Set('PVPQ', values)wherevaluesis a 1-D integer sequence (np.ndarray,list,tuple, orrange— anythingnp.asarray(..., dtype=int)can eat). The values must be non-negative and duplicate-free. An internalParamnamed after the set is created lazily when the set is registered on aModelso the generated kernel can readfor _i in range(len(values)): i = set_param[_i].
The set exposes four attributes the LoopEqn machinery consumes:
size— length of the set.is_identity— True when values are[0, len)so the direct-outer (no Param) fast path can be used.param— the auxiliary 1-D intParamfor a non-identity set;Nonefor an identity set. Lazily created when_register()is called byModel.__setattr__.values— the underlyingnp.ndarrayof indices.
Examples
Identity range (equivalent to
Idx('i', n)+ explicitn_outer):m.Bus = Set('Bus', nb) i = m.Bus.idx('i') m.eqn = LoopEqn('eqn', outer=m.Bus, body=body, model=m)
Explicit subset (replaces the
Param + Idx + indirect-accesspattern):m.PVPQ = Set('PVPQ', pv_pq_array) i = m.PVPQ.idx('i') m.eqn = LoopEqn('eqn', outer=m.PVPQ, body=m.Vm[i], model=m)
- idx(name: str) Idx¶
Produce a bounded
sympy.Idxsized to this set.Body authors use the returned
Idxexactly as they would a bareIdx(name, n)— the body rewriter detects that theIdxcame from a non-identity set and transparently wraps everyVar/Paramaccess with the set’s gather Param.
- property param¶
The auxiliary 1-D int
Paramthat stores the set’s values at runtime. Materialised lazily on first access so body rewriters can reference it without waiting forModel.add.Identity full-range sets (
values == arange(size)) still receive a Param — at the API layeris_identityonly tells the translator “no gather needed when the target is the same size”, but small-identity sets used as subsets of a larger target (e.g.Ref = Set('Ref', [0])indexing a 30-bus Var) still need the Param at runtime to emit the gather.
- Solverz.equation.eqn.Idx(name: str, n: int = None)¶
Create a sympy
sympy.IdxforLoopEqnbody construction — a thin shortcut so users don’t need toimport sympy as spjust to grab one index symbol.- Parameters:
name : str
The index label.
n : int, optional
If given, the index is bounded to
[0, n - 1](sympy stores this as.lower/.upper).Sum()andLoopEqncan then pick up the range automatically:>>> i = Idx('i', 3) >>> Sum(m.G[i, j] * m.ux[j], j) # no explicit range >>> m.eqn = LoopEqn('eqn', outer_index=i, body=body, model=m) # n_outer auto-inferred from i.upper - i.lower + 1 = 3
Without
nthe result is an unboundedsp.IdxandSum()/LoopEqnrequire explicit range / n_outer.
- Solverz.equation.eqn.Sum(expr, dummy, n: int = None)¶
Range-aware shortcut for
sympy.Sumused byLoopEqnbodies. Three call forms:Sum(expr, j, n)→sp.Sum(expr, (j, 0, n - 1))— the most common case.nis an int.Sum(expr, j)wherej = Idx('j', n)is a boundedsp.Idx→sp.Sum(expr, (j, j.lower, j.upper)).Sum(expr, (j, lo, hi))→ passthrough tosp.Sum(legacy sympy tuple form).
Raises
ValueErrorif neithernnor a boundedIdxis available.
Utilities¶
- Solverz.utilities.miscellaneous.derive_incidence_matrix(indices, m, n) coo_array¶
Convert indices to m*n incidence matrix
Solvers¶
AE solver¶
- Solverz.solvers.nlaesolver.nr.nr_method(eqn: nAE, y: ndarray, opt: Opt = None)¶
The Newton-Raphson method.
- Parameters:
eqn : nAE
Numerical AE object.
y : np.ndarray
The initial values of variables
opt : Opt
The solver options, including:
- ite_tol: 1e-5(default)|float
The iteration error tolerance.
- Returns:
sol : aesol
The aesol object.
References
- Solverz.solvers.nlaesolver.cnr.continuous_nr(eqn: nAE, y: ndarray, opt: Opt = None)¶
The Continuous Newton-Raphson method.
- Parameters:
eqn : nAE
Numerical AE object.
y : np.ndarray
The initial values of variables
opt : Opt
The solver options, including:
- ite_tol: 1e-5(default)|float
The iteration error tolerance.
- Returns:
sol : aesol
The aesol object.
References
[R2]F. Milano, ‘Continuous Newton’s Method for Power Flow Analysis’, IEEE Transactions on Power Systems, vol. 24, no. 1, pp. 50–57, Feb. 2009, doi: 10.1109/TPWRS.2008.2004820.
- Solverz.solvers.nlaesolver.lm.lm(eqn: nAE, y: ndarray, opt: Opt = None)¶
The Levenberg-Marquardt method by minpack, wrapped in scipy.
Warning
Note that this function uses only dense Jacobian.
- Parameters:
eqn : nAE
Numerical AE object.
y : np.ndarray
The initial values of variables
opt : Opt
The solver options, including:
- ite_tol: 1e-5(default)|float
The iteration error tolerance.
- Returns:
sol : aesol
The aesol object.
References
[R3]More, Jorge J., Burton S. Garbow, and Kenneth E. Hillstrom. 1980. User Guide for MINPACK-1.
- Solverz.solvers.nlaesolver.sicnm.sicnm(ae: nAE, y0: ndarray, opt: Opt = None)¶
The semi-implicit continuous Newton method [R5]. The philosophy is to rewrite the algebraic equation
\[0=g(y)\]as the differential algebraic equations
\[\begin{split}\begin{aligned} \dot{y}&=z \\ 0&=J(y)z+g(y) \end{aligned}\end{split}\]with \(y_0\) being the initial value guess and \(z_0=-J(y_0)^{-1}g(y_0)\), where \(z\) is an intermediate variable introduced. Then the DAEs are solved by Rodas. SICNM is found to be more robust than the Newton’s method, for which the theoretical proof can be found in my paper [R5]. In addition, the non-iterative nature of Rodas guarantees the efficiency.
One can change the rodas scheme according to the ones implemented in the DAE version of Rodas.
SICNM used the Hessian-vector product (HVP) interface of the algebraic equation models, the make_hvp flag should be set to True when printing numerical modules.
An illustrative example of SICNM can be found in the power flow section of Solverz’ cookbook.
- Parameters:
ae : nAE
Numerical AE object.
y0 : np.ndarray
The initial values of variables
opt : Opt
The solver options, including:
- ite_tol: 1e-5(default)|float
The iteration error tolerance.
- Returns:
sol : aesol
The aesol object.
References
FDAE solver¶
- Solverz.solvers.fdesolver.fdae_solver(fdae: nFDAE, tspan: List | ndarray, y0: ndarray, opt: Opt = None, **kwargs)¶
The general solver of FDAE.
- Parameters:
fdae : nFDAE
Numerical FDAE object.
tspan : List | np.ndarray
An array specifying t0 and tend
y0 : np.ndarray
The initial values of variables
opt : Opt
The solver options, including:
- step_size: 1e-3(default)|float
The step size
- ite_tol: 1e-8(default)|float
The error tolerance of inner Newton iterations.
kwargs : dict
The value of y at previous time nodes. For example, if an FDAE uses \(y_0\), \(y_{-1}\) and \(y_{-2}\), then we can call FDAE solver with
sol = fdae_solver(mdl, [0, 120], y0, Opt(step_size=60), y1=y1, y2=y2)
where y1 denotes \(y_{-1}\) and y2 denotes \(y_{-2}\).
- Returns:
sol : daesol
The daesol object.
DAE solver¶
- Solverz.solvers.daesolver.beuler.backward_euler(dae: nDAE, tspan: List | ndarray, y0: ndarray, opt: Opt = None)¶
The fixed step backward Euler (implicit Euler) method.
- Parameters:
dae : nDAE
Numerical DAE object.
tspan : List | np.ndarray
An array specifying t0 and tend
y0 : np.ndarray
The initial values of variables
opt : Opt
The solver options, including:
- step_size: 1e-3(default)|float
The step size
- Returns:
sol : daesol
The daesol object.
References
- Solverz.solvers.daesolver.trapezoidal.implicit_trapezoid(dae: nDAE, tspan: List | ndarray, y0: ndarray, opt: Opt = None)¶
The fixed step implicit trapezoidal method.
- Parameters:
dae : nDAE
Numerical DAE object.
tspan : List | np.ndarray
An array specifying t0 and tend
y0 : np.ndarray
The initial values of variables
opt : Opt
The solver options, including:
- step_size: 1e-3(default)|float
The step size
- Returns:
sol : daesol
The daesol object.
References
- Solverz.solvers.daesolver.rodas.Rodas(dae: nDAE, tspan: List | ndarray, y0: ndarray, opt: Opt = None)¶
The stiffly accurate Rosenbrock methods including Rodas4 [R8], Rodasp [R9], Rodas5p [R10].
- Parameters:
dae : nDAE
Numerical DAE object.
tspan : List | np.ndarray
Either - a list specifying [t0, tend], or - a np.ndarray specifying the time nodes that you are concerned about
y0 : np.ndarray
The initial values of variables
opt : Opt
The solver options, including:
- scheme: ‘rodas4’(default)|’rodasp’|’rodas5p’
The rodas scheme
- rtol: 1e-3(default)|float
The relative error tolerance
- atol: 1e-6(default)|float
The absolute error tolerance
- event: Callable
The simulation events, with \(t\) and \(y\) being args, and value, is_terminal and direction being outputs
- fixed_h: False(default)|bool
To use fixed step size
- hinit: None(default)|float
Initial step size
- hmax: None(default)|float
Maximum step size.
- pbar: False(default)|bool
To display progress bar
- Returns:
sol : daesol
The daesol object.
References
- Solverz.solvers.daesolver.radau.Radau(dae: nDAE, tspan: List | ndarray, y0: ndarray, opt: Opt = None)¶
Radau IIA(5) DAE solver — 3-stage 5th-order fully-implicit Runge-Kutta method, ported from SciML’s
RadauIIA5.- Parameters:
dae : nDAE
Numerical DAE object.
tspan : List | np.ndarray
Either - a list specifying [t0, tend], or - a np.ndarray specifying the time nodes that you are concerned about
y0 : np.ndarray
The initial values of variables
opt : Opt
The solver options, including:
- rtol: 1e-3(default)|float
The relative error tolerance
- atol: 1e-6(default)|float
The absolute error tolerance
- event: Callable
The simulation events, with \(t\) and \(y\) being args, and value, is_terminal and direction being outputs
- hinit: None(default)|float
Initial step size
- hmax: None(default)|float
Maximum step size
- max_it: 100(default)|int
The simplified Newton maximum iteration count per stage attempt. SciML’s Radau5 default is 10; the lower bound is applied automatically.
- pbar: False(default)|bool
To display progress bar
- Returns:
sol : daesol
The daesol object.
Notes
Stiffly accurate:
y_{n+1} = y_n + Z_3. The Newton update is performed in the W-coordinate system (W = TI @ Z), which decouples the linear solves into one real and one complex sparse LU factorisation per step attempt. Step-size adaptation uses the SciML/Hairer predictive (Gustafsson) controller.References
- Solverz.solvers.daesolver.ode15s.ode15s(dae: nDAE, tspan: List | ndarray, y0: ndarray, opt: Opt = None)¶
The python implementation of MATLAB Ode15s without event detection [R13].
- Parameters:
dae : nDAE
Numerical DAE object.
tspan : List | np.ndarray
Either - a list specifying [t0, tend], or - a np.ndarray specifying the time nodes that you are concerned about
y0 : np.ndarray
The initial values of variables
opt : Opt
The solver options, including:
- rtol: 1e-3(default)|float
The relative error tolerance
- atol: 1e-6(default)|float
The absolute error tolerance
- hinit: None(default)|float
Initial step size
- hmax: None(default)|float
Maximum step size.
- pbar: False(default)|bool
To display progress bar
- Returns:
sol : daesol
The daesol object.
References