Advanced analysis (structural robustness & analytic stochastics)
Beyond the classical Deficiency Zero / Deficiency One theorems, mantis-delta
provides a pipeline that, for a given reaction network, certifies global stability
and robustness from structure alone and emits the exact stochastic stationary
distribution. This page documents those capabilities with the theory and worked
examples; see the API Reference for full signatures.
All features degrade gracefully when their optional dependency is absent
(pip install "mantis-delta[all]" installs them all).
Exhaustive steady states (completeness guarantee)
CRNetwork.steady_states() integrates the ODEs and runs multi-start least-squares —
it can silently miss unstable fixed points that forward integration flows away
from. At mass-action equilibrium the positive steady states are the positive real
roots of a polynomial system, so they can instead be found exhaustively.
all_steady_states() rationalises and non-dimensionalises the rate polynomials,
eliminates the conservation laws, triangularises the system with a lexicographic
Gröbner basis, recovers every complex root by back-substitution (numpy.roots),
polishes each with Newton's method, and returns the real non-negative ones. For a
zero-dimensional ideal this finds all solutions (bounded by the Bézout number).
# Schlögl model: three equilibria, the middle one unstable.
rn = CRNetwork.from_string(
["A + 2 X <-> 3 X", "X <-> B"],
rates={"A + 2X -> 3X": 6.0, "3X -> A + 2X": 1.0, "X -> B": 11.0, "B -> X": 6.0},
chemostatted={"A": 1.0, "B": 1.0},
)
for ss in rn.all_steady_states({"X": 0.5}):
print(ss.concentrations["X"], "stable" if ss.is_stable else "UNSTABLE")
# 1.0 stable / 2.0 UNSTABLE / 3.0 stable
The multi-start solver typically returns only the two stable branches; the
exhaustive engine recovers the unstable middle branch as well. Pass
backend="phcpy" to use polynomial homotopy continuation (PHCpack) for larger or
stiffer systems (pip install "mantis-delta[homotopy]").
Global stability via the Horn–Jackson Lyapunov function
For a complex-balanced network with positive equilibrium $c^$ (the Birch point*), the pseudo-Helmholtz function
$$V(c) = \sum_i \left[ c_i\left(\ln\frac{c_i}{c_i^} - 1\right) + c_i^ \right]$$
is a strict Lyapunov function on every positive compatibility class: $\partial V/\partial c_i = \ln(c_i/c_i^)$, hence $dV/dt = \sum_i \ln(c_i/c_i^)\,\dot c_i \le 0$ with equality only at $c^$. So $c^$ is globally asymptotically stable — a strictly stronger statement than local eigenvalue stability. Every weakly reversible deficiency-zero network is complex-balanced for all rate constants (Horn–Jackson).
rn = CRNetwork.from_string(["A <-> B"], rates={"A -> B": 1.0, "B -> A": 2.0})
rn.is_complex_balanced() # True
rn.complex_balanced_equilibrium({"A": 3.0}) # Birch point {'A': 2.0, 'B': 1.0}
cert = rn.certify_global_stability({"A": 3.0})
print(cert) # readable certificate
cert.globally_stable # True
cert.lyapunov_function, cert.lyapunov_derivative # symbolic V(c) and dV/dt
Absolute Concentration Robustness (ACR)
A network has ACR in species $S$ when $S$'s steady-state concentration is identical in every positive steady state, regardless of the initial totals — the robustness property at the heart of biosensor and homeostatic design. Shinar & Feinberg (Science 2010) give a structural sufficient condition: a deficiency-one network with two non-terminal complexes that differ in a single species $S$ has ACR in $S$.
rn = CRNetwork.from_string(["X + Y -> 2 Y", "Y -> X"],
rates={"X + Y -> 2Y": 3.0, "Y -> X": 6.0})
res = rn.detect_acr({"X": 5.0, "Y": 5.0})
res.species # ['X']
res.acr_values["X"] # 2.0 = k2/k1, independent of the initial totals
Injectivity (ruling out multistationarity beyond D1T)
An injective network admits at most one positive steady state per compatibility
class, so it cannot be multistationary — and unlike the Deficiency One Theorem this
works for higher-deficiency networks. Following Craciun & Feinberg (SIAM J. Appl.
Math. 2005/06), is_injective() forms the square steady-state map (rank-many
independent rows of $N\cdot v(c)$ plus the conservation totals) and tests whether its
Jacobian determinant is sign-definite in the positive concentrations and rate
constants. Sign-definiteness ⇒ the determinant never vanishes ⇒ injective.
CRNetwork.from_string(["A <-> B"], rates={"A -> B": 1, "B -> A": 1}).is_injective().injective # True
schlogl.is_injective().injective # False (multistationary)
A True verdict is rate-independent; False is inconclusive (the test is sufficient,
not necessary).
Pseudo-arclength continuation (fold / Hopf detection)
The log-scan bifurcation() recomputes steady states independently at each parameter
value, so at a saddle-node fold — where the branch turns back on itself — it loses
the branch and never reports the fold. continuation() parameterises the branch by
arclength $s$ along the solution curve $(c(s),\lambda(s))$ and solves the augmented
system
$$ G(c,\lambda)=0,\qquad \langle t_c, c-c_{\text{prev}}\rangle + t_\lambda(\lambda-\lambda_{\text{prev}})-\mathrm{d}s = 0, $$
with a predictor–corrector (Keller 1977). The arclength row keeps the augmented Jacobian regular through the fold, where the steady-state block $G_c$ alone is singular, so folds are traversed smoothly. They are detected from a sign change of $\det G_c$ (refined to $\det = 0$); Hopf points from a change of the unstable-eigenvalue count with no fold (a complex pair crossing the imaginary axis).
res = schlogl.continuation("X -> B", (1.0, 20.0), {"X": 0.5})
res.folds() # both folds bounding the bistable region — (X,k) ≈ (1.35,10.72), (2.53,11.15)
res.species_branch("X") # X along the full S-curve (arclength order, non-monotonic in λ)
On the Brusselator (A, B chemostatted) continuing the rate of B + X -> Y + D
detects the Hopf at the analytic $kB = 1 + A^2$ threshold, with a purely imaginary
critical eigenvalue.
Multistationarity parameter regions (Conradi–Feliu–Mincheva–Wiuf 2017)
Injectivity answers whether multistationarity is possible;
multistationarity_region() maps where in parameter space it occurs (Conradi, Feliu,
Mincheva & Wiuf, PLoS Comput. Biol. 2017). It forms the critical function
$\varphi = (-1)^s \det J$ ($s=\operatorname{rank} N$) and reads it as a polynomial in the
steady-state concentrations with coefficients in the rate constants:
- every coefficient positive for all positive rates ⇒ $\varphi>0$ everywhere ⇒ the network is monostationary for all parameters (an injectivity certificate);
- otherwise each coefficient that can go negative is a face of the multistationarity region — the explicit "where".
res = schlogl.multistationarity_region()
res.monostationary # False — Schlögl is genuinely multistationary
res.region_conditions # coefficient expressions whose negativity ⇒ multistationarity
res.region_boundary # exact discriminant boundary (when reducible to one polynomial)
res.multistationary_at_rates # exact ≥2-root verdict at the supplied rates, when available
Reading the coefficient signs of $\varphi$ over the whole positive orthant is only necessary: $\varphi<0$ might occur at a $c$ that is not a steady state. When the steady states reduce to a univariate polynomial $p(x)$ — directly for $\operatorname{rank}N=1$ networks like Schlögl, or after eliminating the conservation laws — the test is put on the steady-state variety and becomes exact: multistationarity $\iff p$ has $\ge 2$ positive roots, whose boundary is the discriminant $\operatorname{disc}_x(p)=0$. For Schlögl this is
$$ \Delta(k) = -4k^3 + 36k^2 + 648k - 6156, $$
zero at $k = 10.72$ and $11.15$ — exactly the folds the continuation reports — so the
bistable window is $\Delta(k) > 0$. region_boundary returns this discriminant and
multistationary_at_rates the exact positive-root count.
For a fully definitive verdict on a specific compatibility class (any network, no
parametrisation needed), is_multistationary(initial_conditions) enumerates the positive
steady states and checks for two or more:
schlogl.is_multistationary({"X": 0.5}) # True at k = 11 (three steady states)
Exact stochastic stationary distributions (Anderson–Craciun–Kurtz)
For a complex-balanced network the chemical master equation has an exact stationary distribution in closed form (ACK, Bull. Math. Biol. 2010): a product of independent Poissons with means $\lambda_i = N_A V c_i^$, supported on the reachable state space. With conservation laws it is that product conditioned on the conserved totals — a Binomial for a single moiety. You can therefore write down the stationary distribution from structure alone, with no simulation*.
rn = CRNetwork.from_string(["A <-> B"], rates={"A -> B": 2.0, "B -> A": 3.0})
sd = rn.stationary_distribution({"A": 40, "B": 0}, volume_L=1e-15, initial_as="count")
sd.poisson_means() # {'A': λ_A, 'B': λ_B}
sd.expected_counts() # exact stationary means
vals, probs = sd.marginal("A") # exact marginal pmf (matches Binomial to machine precision)
Finite State Projection (CME without noise)
FSP (Munsky–Khammash, J. Chem. Phys. 2006) truncates the count-state space to the reachable set, assembles the sparse CME generator $Q$, and solves the master equation directly — accurate exactly where the SSA is noisiest. It returns the transient distribution $p(t) = e^{Qt}p_0$ with a rigorous leaked-mass error bound, or the stationary distribution (null space of $Q$).
rn = CRNetwork.from_string(["A <-> B"], rates={"A -> B": 2.0, "B -> A": 3.0})
stat = rn.fsp({"A": 30, "B": 0}, volume_L=1e-15, initial_as="count") # stationary
pt = rn.fsp({"A": 30, "B": 0}, volume_L=1e-15, t=0.1, initial_as="count") # transient
pt.expected_counts(); pt.truncation_error
For complex-balanced networks the FSP stationary distribution reproduces the ACK product form to machine precision — an independent cross-check against the closed form and the SSA.
SBML interoperability
With the [sbml] extra, models round-trip through SBML Level 3:
rn.to_sbml("model.xml")
rn2 = CRNetwork.from_sbml("model.xml")