System classes
Change what a ready-made system builds by subclassing it, and write a system of your own as a class that takes the parameters of its model.
Four kinds of system
Every system derives from BaseSystem, and comes in one of four kinds, from the most general to the most specific:
BaseSystemis the base of every system. It holds the main public methods, derives the tags, and builds the parts of a system from its model parameters.Systemis the generic composite of elements, interactions, and nested systems, on a space and in a time. It plays the role ofPipelinein scikit-learn: it holds parts and adds no model of its own.- General systems of the core, such as
MassActionSystem, name a mathematical structure that several fields share, such as species in compartments that react at mass-action rates. - Field realizations, such as
SIRSystem, give a general system orBaseSystemthe parameters and the words of one field. Every ready-made system of the catalog is one.
The kinds mirror the linear models of scikit-learn, where Ridge and Lasso realize one general structure with
parameters of their own.
from skcomplex.core.components.mass_action import MassActionSystem
from skcomplex.core.components.system import BaseSystem, System
from skcomplex.systems.biology.epidemiology import SIRSystem
issubclass(SIRSystem, MassActionSystem), issubclass(MassActionSystem, BaseSystem), issubclass(System, BaseSystem)
(True, True, True)
How a system class builds its parts
Write a system as a class when you want parameters that read like those of the model, and a system that clones, sweeps,
and calibrates like any estimator. A subclass of BaseSystem does two things:
- its
__init__takes the parameters of the model and stores each under its own name, and nothing else, as in scikit-learn, soget_params,set_params, andclonework on them, - its hook
_build_systemreturns theSystemof its parts, written in terms of these parameters.
from typing import Annotated
from skcomplex.core.components.elements import Element
from skcomplex.core.components.interactions import RateInteraction
from skcomplex.core.components.system import BaseSystem, System
from skcomplex.core.engine.runtime import simulate
from skcomplex.core.specs import StateSpec
class TankElement(Element):
def __init__(self, label, *, level: Annotated[float, StateSpec()] = 1.0):
super().__init__(label)
self.level = level
class DrainingTankSystem(BaseSystem):
until = 10.0
dt = 0.1
def __init__(self, level=1.0, *, k=0.5, label='system'):
super().__init__(label)
self.level = level
self.k = k
def _build_system(self):
drain = RateInteraction('drain', target='tank', state='level', rate=lambda s: -self.k * s.target.level)
return System(self.label, elements=[TankElement('tank', level=self.level)], interactions=[drain])
tank = DrainingTankSystem(k=0.2)
tank.get_params(), sorted(tank.get_tags().laws)
round(float(simulate(tank).values('tank.level')[-1]), 3)
({'k': 0.2, 'label': 'system', 'level': 1.0}, ['rate'])
0.135
prepare builds the parts from the current parameters and keeps them as system_. It builds them again only when a
parameter changed, so set_params(k=0.4) changes the parts of the next prepare and of the next simulate. The tags
come from the parts and are never declared, so they cannot drift from what the system does.
The class attributes until and dt above are tuned run settings. simulate reads them, with solver,
observables, initial_state, invariants, schedule, compiler, and backend, for every argument a call leaves
out. A system whose settings depend on its parameters overrides get_run_settings instead.
General systems of the core
Write a compartment model of any field without writing an element or an interaction. MassActionSystem takes the
initial amount of every species and a table of reactions. Every reaction maps its label to its reactants and products,
as coefficients by species name, and to its rate constant.
from skcomplex.core.components.mass_action import MassActionSystem
from skcomplex.core.engine.runtime import simulate
isomerization = MassActionSystem(
species={'A': 10.0, 'B': 0.0},
reactions={'forth': ({'A': 1}, {'B': 1}, 0.3), 'back': ({'B': 1}, {'A': 1}, 0.1)},
)
round(float(simulate(isomerization, until=50.0, dt=1.0).values('A.n')[-1]), 3)
isomerization.set_params(mode='jump').get_tags().randomness
2.5
stochastic
Every species is a CompartmentElement named after it, whose state n holds its amount, and every reaction is a
ReactionInteraction. The mode writes the reactions as rate equations, 'rate', as random events of single
molecules, 'jump', or as chemical Langevin equations, 'langevin'. The amount of A relaxes to
0.1 / (0.3 + 0.1) of the total.
Field realizations
A field realization takes the parameters of its model under its own names, and gives the general system its parts
through hooks. SIRSystem subclasses MassActionSystem. It takes susceptible, infected, beta, and gamma,
returns its compartments from _get_species, and its infection and recovery from _get_interactions. Epidemics of
susceptible, infected, and recovered individuals and enzyme kinetics are both mass-action systems, so they share the
general system and differ in their realizations.
The hooks of MassActionSystem are these:
_get_speciesreturns the initial amount of every species by name,_get_reactionsreturns the reaction table,_get_modereturns the mode,_get_interactionsreturns the reactions as interactions, for a realization whose reactions have roles of their own names, such as thesusceptibleandinfectedroles of an infection.
Changing a ready-made system by subclassing it
Add a process to a ready-made system, and keep everything else it does. A subclass of SIRSystem that vaccinates
susceptible individuals at the rate nu adds one parameter and one reaction:
from skcomplex.core.components.interactions import ReactionInteraction
from skcomplex.core.engine.runtime import simulate
from skcomplex.systems.biology.epidemiology import SIRSystem
class VaccinatedSIRSystem(SIRSystem):
until = 100.0
def __init__(
self,
susceptible=990.0,
infected=10.0,
recovered=0.0,
*,
beta=0.0003,
gamma=0.1,
nu=0.02,
stochastic=False,
label='system',
):
super().__init__(susceptible, infected, recovered, beta=beta, gamma=gamma, stochastic=stochastic, label=label)
self.nu = nu
def _get_interactions(self):
vaccination = ReactionInteraction(
'vaccination',
reactants={('S', 'n'): 1},
products={('R', 'n'): 1},
k=self.nu,
mode='jump' if self.stochastic else 'rate',
)
return [*super()._get_interactions(), vaccination]
vaccinated = VaccinatedSIRSystem()
[interaction.label for interaction in vaccinated.prepare().system_.interactions]
simulate(SIRSystem()).values('I.n').max().round(), simulate(vaccinated).values('I.n').max().round()
['infection', 'recovery', 'vaccination']
(304.0, 132.0)
The subclass repeats the arguments of its parent in its __init__, since get_params reads the signature of the
class, as in scikit-learn. It overrides one hook and calls the parent for the rest, so the infection, the recovery,
the stochastic mode, and the tuned time step keep working. Vaccination lowers the peak of the epidemic.
The subclass is an estimator like its parent, so every function of the library takes it:
stochastic = vaccinated.set_params(stochastic=True)
stochastic.get_tags().randomness, simulate(stochastic, random_state=0).values('I.n').max()
('stochastic', 110.0)
The contributing guide shows how a system class of your own joins the catalog, with its field, topics, references, tests, and gallery example.