scikit-complexity
| Category | Tools |
|---|---|
| Development | |
| Testing | |
| Security | |
| Automation | |
| Package | |
| Documentation | |
| Communication |
Overview
With scikit-complexity, you can describe a system in any field of science with just a few lines of code, and it takes
care of the rest: it works out the mathematics, picks a solver unless you choose one, runs the simulation, and can
stream it live to the Rerun viewer.
Complex systems are usually modelled in separate tools with separate conventions, often one for each field. Here they all 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 ready-to-run systems, 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 systems that learn by gradient descent.
- The form of your system is inferred. Write the laws, and 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.
- Add your own parts without changing the library. Its systems use the same public classes you do, 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
There are two ways to use the library. Select a prebuilt system from the catalog by its name, or build a custom system
from the general classes of skcomplex.core.components.
Select a prebuilt system
Every prebuilt system has a description, tags, parameters, and references, so you can browse the catalog from Python.
First, list the fields. Each system belongs to one:
from skcomplex.systems import catalog
fields = {system.field for system in catalog()}
Next, list the systems of a field:
bio_eco_systems = {system.name for system in catalog(field='biology/ecology')}
You can also search every field by tag:
oscillation_systems = [system.name for system in catalog() if 'oscillation' in system.tags]
Finally, read the details of the system you chose:
from skcomplex.systems import describe
entry = describe('lotka_volterra')
entry.description, entry.formalism, entry.parameters, entry.references
Simulate any of them with its tuned settings in one line:
from skcomplex.systems import create_simulation
trajectory = create_simulation('sir').prepare().run().trajectory_
round(float(trajectory.values('I.n').max()))
create_system returns a ready-made system with the arguments you choose, so you can simulate it with settings of
your own:
from skcomplex.core.engine.runtime import Simulation
from skcomplex.systems import create_system
system = create_system('lotka_volterra', r=1.5)
trajectory = Simulation(system, dt=0.1).prepare().run(until=50).trajectory_
round(float(trajectory.values('prey.n').max()))
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 |
The catalog lists every system by field and tag, and the page of each system shows the classes it is built from.
Build a custom system
A custom system needs only the core. An element declares its states, and an interaction declares its roles and returns
its laws, where the namespace s holds the symbols of its parameters and of the elements bound to its roles. A predator
and prey system takes one element class, the general RateInteraction, and one new interaction:
from typing import Annotated
from skcomplex.core.components.elements import Element
from skcomplex.core.components.interactions import Interaction, RateInteraction
from skcomplex.core.components.laws import RateLaw
from skcomplex.core.components.system import System
from skcomplex.core.engine.runtime import Simulation
from skcomplex.core.specs import ParameterSpec, RoleSpec, StateSpec
class PopulationElement(Element):
def __init__(self, label, visual=None, *, n: Annotated[float, StateSpec()] = 1.0):
super().__init__(label, visual=visual)
self.n = n
class PredationInteraction(Interaction):
def __init__(
self,
label,
*,
predator: Annotated[str, RoleSpec(states=('n',))],
prey: Annotated[str, RoleSpec(states=('n',))],
a: Annotated[float, ParameterSpec()] = 0.4,
b: Annotated[float, ParameterSpec()] = 0.1,
):
super().__init__(label)
self.predator = predator
self.prey = prey
self.a = a
self.b = b
def _get_laws(self, s):
contacts = s.predator.n * s.prey.n
return [RateLaw(s.prey.n, -s.a * contacts), RateLaw(s.predator.n, s.b * contacts)]
system = System(
elements=[PopulationElement('rabbits', n=40), PopulationElement('foxes', n=9)],
interactions=[
RateInteraction('growth', target='rabbits', state='n', rate=lambda s: 1.1 * s.target.n * (1 - s.target.n / 1000)),
PredationInteraction('hunt', predator='foxes', prey='rabbits'),
RateInteraction('death', target='foxes', state='n', rate=lambda s: -0.4 * s.target.n),
],
)
trajectory = Simulation(system, dt=0.05).prepare().run(until=30).trajectory_
round(float(trajectory.values('rabbits.n').max()))
A RerunObserver records the run for the Rerun viewer, and RerunObserver('predators', spawn=True) opens the viewer
during the run. A RealTimeClock paces the run against wall time:
import tempfile
from pathlib import Path
from skcomplex.core.engine.runtime import RealTimeClock
from skcomplex.core.engine.viz import RerunObserver
recording = Path(tempfile.mkdtemp()) / 'predators.rrd'
simulation = Simulation(
system,
dt=0.05,
clock=RealTimeClock(speed=20.0),
observers=[RerunObserver('predators', save_path=recording)],
).prepare()
float(simulation.run(until=30).trajectory_.times[-1])
A simulation started with start runs in the background, and its parameters can change while it runs:
simulation = Simulation(system, dt=0.05, clock=RealTimeClock(speed=20.0)).prepare().start(until=30)
simulation.update_parameters({'hunt.a': 0.2})
simulation.wait()
Adding a process takes one more interaction, and SymbolicAnalysis shows the equations of the system:
from sage.all__sagemath_symbolics import oo
from sage.sets.real_set import RealSet
from skcomplex.core.analysis import SymbolicAnalysis
class HarvestInteraction(Interaction):
def __init__(
self,
label,
*,
target: Annotated[str, RoleSpec(states=('n',))],
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=[
RateInteraction('growth', target='fish', state='n', rate=lambda s: 0.5 * s.target.n * (1 - s.target.n / 1000)),
HarvestInteraction('fishing', target='fish', h=0.1),
],
)
SymbolicAnalysis().prepare(harvested).run().equations_['fish.n']
Find out which components fit
An interaction states what it needs from each role, and an element states what it carries. Ask them before you build:
hunt = PredationInteraction('hunt', predator='foxes', prey='rabbits')
hunt.get_requirements().roles['prey'].states, list(PopulationElement('rabbits').get_states())
When a role is bound to an element without the state it reads, validation names the role, the state, and the fix:
from skcomplex.core.engine.compile import PassCompiler
class TankElement(Element):
def __init__(self, label, visual=None, *, level: Annotated[float, StateSpec()] = 1.0):
super().__init__(label, visual=visual)
self.level = level
wrong = System(
elements=[TankElement('tank'), PopulationElement('foxes')],
interactions=[PredationInteraction('hunt', predator='foxes', prey='tank')],
)
PassCompiler().prepare(wrong).report_.issues[0].text
The compatibility page lists every element, interaction, law, solver, and backend, and the user guide walks through the choices in order.
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.
Metadata
Release files for scikit-complexity 0.5.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| scikit_complexity-0.5.0.tar.gz | 3.1 MB | With provenance | Details for scikit_complexity-0.5.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| scikit_complexity-0.5.0-py3-none-any.whl | Python 3 | none | any | Details for scikit_complexity-0.5.0-py3-none-any.whl |
Total release size: 6.5 MB
Release files / scikit_complexity-0.5.0.tar.gz
| Download URL | scikit_complexity-0.5.0.tar.gz |
|---|---|
| Size | 3.1 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
00c8dc741886c813a0200bf1c52f58fce1f61c95a6b126926db8f4ad3de54b19
|
|
BLAKE2b-256 checksum How to use checksums |
33cc6742c7b3945f41c1321c8819c504d4aa2069a20258f3ca89cb2fa36d26ff
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 6, 2026.
Transparency logRelease files / scikit_complexity-0.5.0-py3-none-any.whl
| Download URL | scikit_complexity-0.5.0-py3-none-any.whl |
|---|---|
| Size | 3.4 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
354d405a2010c058c707af5c7655ba4634627e75b9d2116926468c703ba0a865
|
|
BLAKE2b-256 checksum How to use checksums |
6a63fe78ba3b1b355ceed15a88581471b2a8b85332e34663aff7f2c398285ad8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 6, 2026.
Transparency logSecurity for scikit-complexity 0.5.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for scikit_complexity-0.5.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 6, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.