Oscillator Example
The oscillator is the minimal end-to-end Numen example — a damped 1D harmonic oscillator. It demonstrates the complete model-to-result workflow using only the scipy backend.
Source: src/numen/examples/oscillator/
Physics
The equation of motion is:
ẍ + 2ζω·ẋ + ω²x = 0
where ω is the natural frequency [rad/s] and ζ is the damping ratio.
Component
# components.py
from typing import Annotated, Literal
from numen.spec.component import Component
from numen.fields import IntegratedField, ParameterField
class OscillatorComponent(Component):
"""Simple 1D harmonic oscillator: ẍ + 2ζω·ẋ + ω²x = 0."""
kind: Literal["oscillator"] = "oscillator"
position: Annotated[float, IntegratedField()] = 1.0 # initial x = 1 m
velocity: Annotated[float, IntegratedField()] = 0.0 # initial v = 0 m/s
omega: Annotated[float, ParameterField()] = 1.0 # natural frequency [rad/s]
damping: Annotated[float, ParameterField()] = 0.0 # damping ratio ζ
Two IntegratedField slots → state vector x has two elements per entity.
Two ParameterField slots → parameter vector p has two elements.
Dynamics
# dynamics.py
def oscillator_dynamics(dx, x, p, t, spec, system):
"""ẋ = v, v̇ = -ω²x - 2ζωv"""
for (entity_id,) in system.entity_groups:
c = spec.view(entity_id, OscillatorComponent, x, p) # read
dc = spec.dx_view(entity_id, OscillatorComponent, dx) # write
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 = "OscillatorDynamics.oscillator_dynamics!"
component_types = (OscillatorComponent,) tells compile_spec to auto-populate
entity_ids with all entities that have an OscillatorComponent. The entity_groups
tuple on the compiled system will be (("osc",),) for a single-entity world.
World
# world.py
World = GenericWorld[OscillatorComponent, OscillatorSystem, None]
def make_world(x0=1.0, v0=0.0, omega=1.0, damping=0.0, entity_id="osc"):
return World(
components={entity_id: {"oscillator": OscillatorComponent(
position=x0, velocity=v0, omega=omega, damping=damping
)}},
systems={"osc_sys": OscillatorSystem()},
)
Run
# run.py
import math
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(x0=1.0, omega=2 * math.pi, damping=0.05)
spec = compile_spec(world)
# State/param index maps after compilation:
# state_index_map: {"osc.oscillator.position": (0, 1), "osc.oscillator.velocity": (1, 2)}
# param_index_map: {"osc.oscillator.omega": (0, 1), "osc.oscillator.damping": (1, 2)}
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")
Run it
uv run numen run oscillator
Key observations
- The model has exactly 4 scalars: 2 state (
position,velocity) + 2 param (omega,damping). compile_specis a pure function — call it once, reuse the spec for all backends.SnapshotCollector.field_seriesis faster thancollector.at(t)for plotting — it directly slicesresult.xwithout rebuilding world objects.