Kinds of dynamics
Find the laws, elements, spaces, and solvers of the kind of dynamics your system needs, and see how a compiler turns the laws into a problem a solver can run.
from skcomplex.core.engine.compile import BaseCompiler, PassCompiler
from skcomplex.systems import create_system
compiler = PassCompiler().prepare(create_system('sir')).run()
isinstance(compiler, BaseCompiler), compiler.report_.ok, compiler.problem_.form.value
(True, True, 'ordinary differential equations')
Some kinds of dynamics, such as lattice dynamics or operator dynamics, are needed by few systems. None of them is a package of its own. Its laws, elements, and spaces are parts of the core, and the passes that compile it, its solvers, and its visuals are parts of the engine, beside those of every other kind.
Where each kind lives
| Kind | Core | Engine |
|---|---|---|
| agents | RuleLaw, MatchLaw |
the agents passes, MesaSolver |
| lattices | LocalRuleLaw, LocalRuleInteraction, DriveLaw, CascadeLaw, ExtremalLaw, LatticeElement, LatticeSpace, ObjectiveSpec, SamplerSpec |
LatticeSolver, MonteCarloSolver |
| finitely generated groups | CayleySpace |
CayleyVisual |
| rewriting | ProductionLaw, SubstitutionLaw, WordElement, PatchElement |
RewritingCompiler, RewritingSolver, GeometryVisual |
| operator | GeneratorLaw, DissipatorLaw, HilbertSpec |
a system compiler, UnitarySolver, LindbladSolver, JumpTrajectoriesSolver |
| fields | FieldElement, FluxLaw, MeshSpace, laplacian and the other operators |
the fields passes, IMEXEulerSolver, CrankNicolsonSolver, SSPRKSolver |
| kinetics | EncounterLaw, moment |
the encounter passes, KineticSolver |
| fractional | StateSpec(order=...) in (0, 1), FractionalNoiseLaw |
the fractional passes, FractionalABMSolver, GrunwaldLetnikovSolver, FractionalEulerMaruyamaSolver |
| topology | TopologyRequestSpec |
the topology passes |
| particles | PointElement, GradientInteraction, PairCouplingInteraction |
VelocityVerletSolver, Yoshida4Solver, LammpsSolver |
| control | ParameterSpec(adaptive=True, bounds=...) |
PontryaginSolver, CollocationSolver |
Every compiler derives from BaseCompiler and speaks the two verbs of every part.
prepare(system)validates its settings and the system, and setsreport_, the issues it found.run(initial_state)raises the errors of the report, setsproblem_, the problem, and setssolver_, the solver it names for the problem.
Simulation takes the compiler as the compiler setting, and a compiler alone compiles a system with
prepare(system).run().problem_. The default PassCompiler compiles every system but operator ones through its passes,
which lower each law to the problem of its form. For a system that needs another compiler, it names that compiler.
- An operator system takes
compiler=OperatorCompiler(). - A rewriting system takes
compiler=RewritingCompiler(). - A package of your own can bring the compiler of a form of its own, with the solver it names.
Writing a law
A new law derives from BaseLaw. It stores its arguments under their own names, and it implements three methods.
_get_targetsreturns the states it changes, whichpreparesets astargets_._get_requirementsreturns aRequirementSpecof the kinds of time and space it runs in._runsetsvalue_, its value at given values of the symbols it reads, whichpreparesets asreads_.
from typing import Annotated
from sage.all__sagemath_symbolics import var
from skcomplex.core.components.elements import Element
from skcomplex.core.components.laws import BaseLaw
from skcomplex.core.components.spaces import ContinuousTime
from skcomplex.core.components.system import System
from skcomplex.core.engine.compile import PassCompiler
from skcomplex.core.specs import ParameterSpec, RequirementSpec, StateSpec
class DecayLaw(BaseLaw):
def __init__(self, target, rate, *, label=''):
super().__init__(label)
self.target = target
self.rate = rate
def _check(self):
pass
def _get_targets(self):
return (self.target,)
@classmethod
def _get_requirements(cls):
return RequirementSpec(time=(ContinuousTime,))
def _run(self, inputs):
self.value_ = self._evaluate(-self.rate * self.target, inputs)
n, k = var('n k')
law = DecayLaw(n, k).prepare('atom')
law.targets_, sorted(map(str, law.reads_)), law.owner_, law.run({'n': 2.0, 'k': 0.5}).value_
((n,), ['k', 'n'], 'atom', -1.0)
The law checks, prepares, and runs on its own. But the passes and the system compilers are fixed lists of the engine,
so no form takes a law of a new class in a system. The compiler reports it with the code laws-mixed, and a new kind of
dynamics is a change to the engine.
class NucleusElement(Element):
def __init__(self, label, *, n: Annotated[float, StateSpec()] = 1.0, k: Annotated[float, ParameterSpec()] = 1.0):
super().__init__(label)
self.n = n
self.k = k
def _get_laws(self, s):
return [DecayLaw(s.n, s.k)]
report = PassCompiler().prepare(System(elements=[NucleusElement('atom')])).report_
[issue.code for issue in report.errors]
['laws-mixed']