Getting Started
Installation
Install Numen with pip or uv:
pip install numen
# or
uv add numen
Optional extras:
pip install "numen[jax]" # JAX backend
pip install "numen[characterization]" # DOE sweeps, parameter grids
For the Julia backend, install Julia ≥ 1.10 and add it to your PATH. The first solve will automatically install the required Julia packages.
Verify your installation
numen check
Expected output:
Numen backend check
==================================================
scipy ✓ (RK45, oscillator x(1s) = 1.000000)
JAX ✓ (Dopri5, oscillator x(1s) = 1.000000)
Julia ✓ julia version 1.12.0
Start a new project
numen init my_project --model first_model --domain mechanical
cd my_project
This creates:
my_project/
├── CLAUDE.md (AI assistant context)
├── CHARACTERIZATION.md (test campaign reference)
├── DESIGN.md (architecture decisions log)
├── JULIA.md (Julia dynamics conventions)
├── test_plan.schema.json (JSON Schema for IDE autocomplete)
└── first_model/
├── components.py (define state and parameter fields)
├── dynamics.py (write physics — JAX-compatible)
├── dynamics.jl (Julia translation for fast backend)
├── world.py (set initial conditions and topology)
├── test_plan.yaml (characterization campaign)
└── run.py (solve and plot)
Run it immediately:
cd my_project/first_model
python run.py
Recommended: install the Red Hat YAML extension
For live autocomplete and validation in test_plan.yaml, install the
YAML extension by Red Hat
in VS Code or Cursor. The scaffolded test plans already include the
# yaml-language-server: $schema=test_plan.schema.json header — once the
extension is installed, your editor will:
- Autocomplete every test type, plot panel type, and field name
- Catch invalid
sweep_parampaths and unknown options as you type - Show inline docstrings from the Pydantic schema on hover
To regenerate the schema after upgrading Numen:
numen schema -o test_plan.schema.json
Your first model: 1D oscillator
This walkthrough builds the oscillator example from scratch.
Step 1 — Define a component
Components are frozen Pydantic models that declare fields using Annotated type hints.
# components.py
from typing import Annotated, Literal
from numen.spec.component import Component
from numen.fields import IntegratedField, ParameterField
class OscillatorComponent(Component):
kind: Literal["oscillator"] = "oscillator"
position: Annotated[float, IntegratedField()] = 1.0 # initial position = 1 m
velocity: Annotated[float, IntegratedField()] = 0.0 # initial velocity = 0 m/s
omega: Annotated[float, ParameterField()] = 1.0 # natural frequency [rad/s]
damping: Annotated[float, ParameterField()] = 0.05 # damping ratio ζ
IntegratedField— solver integrates this; it's part of the state vectorx.ParameterField— constant during the solve; part of the parameter vectorp.
Step 2 — Define the dynamics
# dynamics.py
import math
from typing import ClassVar, Literal
from numen.spec.system import System, DynamicsFn
from components import OscillatorComponent
def oscillator_dynamics(dx, x, p, t, spec, system):
"""ẋ = v, v̇ = -ω²x - 2ζωv"""
for (eid,) in system.entity_groups:
c = spec.view(eid, OscillatorComponent, x, p) # read state + params
dc = spec.dx_view(eid, OscillatorComponent, dx) # write derivatives
dc.position += c.velocity
dc.velocity += -c.omega**2 * c.position - 2 * c.damping * c.omega * c.velocity
class OscillatorSystem(System):
component_types: ClassVar = (OscillatorComponent,)
python_fn: ClassVar[DynamicsFn] = staticmethod(oscillator_dynamics)
kind: Literal["oscillator_system"] = "oscillator_system"
dynamics_fn: str = "OscDyn.oscillator_dynamics!"
Step 3 — Assemble the world
# world.py
from numen.spec.world import GenericWorld
from components import OscillatorComponent
from dynamics import OscillatorSystem
World = GenericWorld[OscillatorComponent, OscillatorSystem, None]
def make_world(omega=2*math.pi, damping=0.05):
return World(
components={"osc": {"oscillator": OscillatorComponent(omega=omega, damping=damping)}},
systems={"osc_sys": OscillatorSystem()},
)
Step 4 — Compile and solve
# run.py
from numen.compiler.flatten import compile_spec
from numen.bridge.scipy_backend import ScipyBackend
from numen.reconstruction.collector import SnapshotCollector
from world import make_world
world = make_world()
spec = compile_spec(world)
result = ScipyBackend(rtol=1e-9, atol=1e-9).solve(spec, tspan=(0.0, 5.0))
collector = SnapshotCollector(world, spec, result)
t, position = collector.field_series("osc", "oscillator", "position")
print(f"Final position: {position[-1]:.4f} m")
Step 5 — Switch backends
Switch to JAX for repeated solves — no code changes required:
from numen.bridge.jax_backend import JAXBackend
result = JAXBackend(solver="Dopri5").solve(spec, tspan=(0.0, 5.0))
Running built-in examples
numen list # list all examples
numen run oscillator # run the oscillator
numen run coupled_spring # run the spring chain
numen run fluid_poppet # run the poppet valve (scipy only)
CLI reference
numen init [dir] [--model NAME] [--domain DOMAIN]
Bootstrap a new project.
numen check
Smoke-test scipy, JAX, and Julia backends.
numen new NAME [--domain DOMAIN]
Scaffold a new model directory inside an existing project.
numen list
List built-in example models.
numen run EXAMPLE
Run a built-in example.
numen characterize PLAN [--output FILE] [--verbose]
Run a YAML test campaign against a model.