scikit-complexity
| Category | Tools |
|---|---|
| Development | |
| Testing | |
| Security | |
| Automation | |
| Package | |
| Documentation | |
| Communication |
Overview
scikit-complexity lets you describe a model from any science in a few lines, and takes care of the rest: it works
out the mathematics, picks a solver, runs the simulation, and streams it live to the Rerun viewer.
Flocks, epidemics, markets, fluids, neural networks and cellular automata are usually written in separate tools with separate conventions. Here they share one small vocabulary, in the spirit of scikit-learn: elements carry state, interactions change it through laws, and systems nest and compose. Learn it once, and every field opens up.
![]() Physics Decaying 2D turbulence |
![]() Chemistry Gray-Scott patterns |
![]() Biology Boids flocking |
![]() Economics Sugarscape |
![]() Engineering Nagel-Schreckenberg traffic |
![]() Mathematics Julia sets |
![]() Computer science Game of Life |
![]() Social science Schelling segregation |
A few of the 549 ready-to-run models, each with a documented example in the gallery.
Features
- Every kind of dynamics, one interface. Differential equations, stochastic processes, jump processes, discrete maps, cellular automata, agents, networks and hypergraphs, fields and PDEs, quantum systems, and models that learn by gradient descent.
- The form of your model is inferred. Write the laws; the framework decides whether it is an ODE, an SDE or a jump process, and compiles it for NumPy, Numba, or JAX.
- Symbols when you want them, code when you don't. Laws are Sage symbolic expressions you can inspect and solve exactly, or plain Python functions.
- Watch it run. Stream any simulation to Rerun in real time, and change its parameters while it runs.
- Analysis included. Nearly 50 analyses, from Lyapunov exponents and entropy to network motifs, scaling laws and system identification, plus experiments for parameter sweeps, sensitivity, calibration and basin stability.
- Standing on mature libraries. SciPy, NetworkX, Mesa, LAMMPS, QuTiP, JAX and others do the heavy lifting.
- Extend it, don't fork it. Models are written with the same public classes you use, so each one doubles as an example for your own.
Installation
scikit-complexity supports Python 3.12, 3.13, and 3.14 on Linux and on macOS 13 or later with Apple silicon, and is
available on PyPI. On Python 3.14 the install builds memory-allocator from source, so it needs a C compiler:
pip install scikit-complexity
The installation includes Sage through the passagemath distributions, LAMMPS, Mesa, Numba, JAX, and Rerun. Windows has no passagemath wheels, so on Windows the package runs in WSL.
For development, clone the repository and install the project with PDM:
git clone https://github.com/georgedouzas/scikit-complexity.git
cd scikit-complexity
pdm install
Usage
Build your own model from a few general classes, or start from one of the 549 ready-made models, which are written with the same classes and work as examples for your own.
Build a model
A predator and prey model assembles from ready-made interactions:
from skcomplex.core.system import System
from skcomplex.models.biology.common.populations import (
LogisticGrowthInteraction,
MortalityInteraction,
PopulationElement,
)
from skcomplex.models.biology.ecology import PredationInteraction
system = System(
elements=[PopulationElement('rabbits', n=40), PopulationElement('foxes', n=9)],
interactions=[
LogisticGrowthInteraction('growth', target='rabbits', r=1.1, K=1000),
PredationInteraction('hunt', predator='foxes', prey='rabbits', a=0.4, b=0.1),
MortalityInteraction('death', target='foxes', d=0.4),
],
)
A simulation runs it and records the trajectory. Adding a RerunObserver streams the run to the Rerun viewer, and a
RealTimeClock paces it against wall time:
```python continuation from skcomplex.engine.runtime import RealTimeClock, Simulation from skcomplex.engine.viz import RerunObserver
simulation = Simulation( system, dt=0.05, clock=RealTimeClock(speed=20.0), observers=[RerunObserver('lotka-volterra', spawn=True)], ).check().prepare() trajectory = simulation.run(until=30).trajectory_ print(round(float(trajectory.values('rabbits.n').max())))
A simulation started with `start` runs in the background, and its parameters can change while it runs:
```python continuation
from skcomplex.engine.runtime import RealTimeClock, Simulation
simulation = Simulation(system, dt=0.05, clock=RealTimeClock(speed=20.0)).check().prepare().start(until=30)
simulation.update_parameters({'hunt.a': 0.2})
simulation.wait()
A new interaction takes a few lines. It declares its roles and parameters as annotated arguments of its constructor,
and returns its laws from _get_laws, where the namespace s holds the symbols of its parameters and of the elements
bound to its roles:
from typing import Annotated
from sage.all__sagemath_symbolics import oo
from sage.sets.real_set import RealSet
from skcomplex.analysis import SymbolicAnalysis
from skcomplex.core.interactions import Interaction
from skcomplex.core.laws import RateLaw
from skcomplex.specs import ParameterSpec, RoleSpec
from skcomplex.core.system import System
from skcomplex.models.biology.common.populations import LogisticGrowthInteraction, PopulationElement
class HarvestInteraction(Interaction):
def __init__(
self,
label,
*,
target: Annotated[str, RoleSpec()],
h: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.1,
):
super().__init__(label)
self.target = target
self.h = h
def _get_laws(self, s):
return [RateLaw(s.target.n, -s.h * s.target.n)]
harvested = System(
elements=[PopulationElement('fish', n=500)],
interactions=[
LogisticGrowthInteraction('growth', target='fish', r=0.5, K=1000),
HarvestInteraction('fishing', target='fish', h=0.1),
],
)
print(SymbolicAnalysis().check().prepare(harvested).run().equations_['fish.n'])
Use a ready-made model
Every model comes with a description, tags, parameters and references, so you can browse the catalog from Python:
from skcomplex.models import catalog, describe
for model in catalog(field='biology/ecology'):
print(model.name, '-', model.description)
print([model.name for model in catalog() if 'oscillation' in model.tags])
entry = describe('lotka_volterra')
print(entry.description, entry.formalism, entry.parameters, entry.references)
Run any of them in one line:
from skcomplex.models import run
trajectory = run('sir').trajectory_
print(trajectory.values('I.n').max().round())
Each run can stream to the Rerun viewer:
![]() lotka_volterraPrey and predators cycle |
![]() sirAn epidemic rises and fades |
![]() wolf_sheepWolves, sheep and grass on a grid |
Spec-driven development
The project's engineering constitution lives in openspec/constitution.md and binds all work. The
project uses OpenSpec for spec-driven development, and
openspec/config.yaml names the constitution as binding.










