Skip to main content

Python Representation

This page describes how SedaroML is written in Python today. The language's abstractions and semantics are defined in the preceding pages; everything here is representation — the Python spelling of those concepts.

The public surface lives in sedaro.model.v2:

from sedaro.model.v2 import (
Ontology, statemanager, time_step, validator, deriver,
RelInfo, SchemaFieldSpec, OntologyError,
serialize_model, deserialize_model,
)
from sedaro_ts.sv import Type, TypedDatum

Defining an Ontology

An ontology is a class extending Ontology. Fields are class-level type annotations; the string literal under each field is its first-class documentation:

from typing import Annotated

from sedaro.model.v2 import Ontology, statemanager, time_step, validator
from sedaro_ts.sv import TypedDatum


class Pendulum(Ontology):
"""A simple pendulum."""

length: float
"""The length of the pendulum in meters."""

angle: Annotated[TypedDatum, "rad"] = 0.0
"""The angle of the pendulum in radians."""

angleRate: float = 0.0
"""The angular rate of the pendulum in radians per second."""

Do not define __init__; the framework generates it from the fields. For post-construction setup, define __post_init__(self), which runs at the end of the generated constructor.

Fields

  • Plain values use ordinary Python annotations (float, int, str, bool, list[float], tuple[float, float], unions, Optional, named tuples, Enum subclasses) and are type-checked on assignment against their SedaroTS equivalent.
  • Typed datums are declared as Annotated[TypedDatum, "<sedarots-type>"], e.g. "rad", "km", "eci", "#[f64; 3]". Convenience aliases Vector2Vector9, Matrix2Matrix9, and Quaternion are available in sedaro.model.v2.types.
  • Defaults follow the annotation: angle: Annotated[TypedDatum, "rad"] = 0.0. An explicit typed default can be built with Type("deg").td(0.0). A field without a default is required to have its value set upon instantiation or by a deriver.
  • Uncertain values: assign a distribution from sedaro.model.v2.uncertain (e.g. NormalDistribution(mu=120.0, sigma=4.0)) as a default or instance value.

Plain-value annotations map onto SedaroTS types as follows:

Host type (Python)SedaroTS type
floatf64
intint
boolbool
strstr
IDid
list[T][T]
set[T]{T}
tuple[A, B](A, B)
dict[K, V]{K: V} (keys may not be optional)
A | B (union)A + B
T | None (optional)T?
Named tupleTuple of its field types

Working with typed values:

p = Pendulum(length=1.0, angle=Type("deg").td(45.0)) # stored as rad (coerced at rest)
p.angle.convert(Type("deg")).py() # read out in degrees -> 45.0
p.attr_as_td("length") # any field as a TypedDatum
info

Beware mutable defaults (list/dict/set literals): like ordinary Python class attributes, they are shared across instances. The framework warns when it sees one — prefer an immutable default and populate in __post_init__.

Relationships

A field annotated with another Ontology subclass is a relationship; collections express cardinality, and RelInfo attaches metadata such as the reverse field:

class Threat(Ontology):
threatLauncher: Annotated["ThreatLauncher", RelInfo(reverse="threats")]

class ThreatLauncher(Ontology):
threats: list[Annotated[Threat, RelInfo(reverse="threatLauncher")]]
"""List of threats associated with this threat launcher."""

Constraints and Derivation

Validators and derivers are methods marked with decorators:

@validator
def validate_threats_and_launch_times(self):
if len(self.threats) != len(self.launchTimes):
raise ValueError("The number of threats must match the number of launch times.")

@deriver
def order_threats(self):
... # may only assign fields of self

Class-level options are passed as class keywords:

class ThreatLauncher(
Ontology,
requirements={RadarSite}, # required sibling types
schema_fields=SchemaFieldSpec(ignore={"internal"}, order=["name", "threats"]),
):
...

Behaviors

State managers are declared with @statemanager. Note that the implementation takes no self — it is a pure function whose parameters are bound positionally to the elements of the consumed tuple (names need not match the queried fields):

@statemanager(
engine="gnc",
consumed="(length, prev!(angle), prev!(angleRate), prev!(timeStep as s))",
produced="(angle, angleRate)",
outputs="(angle, angleRate)",
)
def propagate_angle(length: float, theta: float, omega: float, step: float) -> tuple[float, float]:
...
return new_angle, new_angle_rate

@time_step(engine="gnc")
def time_step() -> float:
return 0.1

consumed must be a tuple in string form ("(a, b)"), even for a single input ("(a,)"). The full signature:

@statemanager(
engine="gnc", # engine name (default "default")
consumed="(...)", # SedaroQL query for inputs
produced="(...)", # SedaroQL query for state written
outputs="(...)", # SedaroQL query for recorded outputs
lerps=None, # interpolation configuration
rust=None, # "crate.module.fn" to bind a Rust implementation instead
cache=False, # memoize the Python implementation (lru_cache)
cache_size=128,
)

Passing rust="..." binds the behavior to a Rust implementation with the same declared queries.

Building and Running a Model

from sedaro.simulation import Simulation

from .agent import Pendulum

pendulum = Pendulum(length=1.0, angle=0.1, angleRate=0.0)

Simulation(agents=[pendulum], duration=60.0).simulate()

Composition uses add (which parents the instances and checks requirements):

team = Team(name="Engineering")
team.add(alice, bob, task) # clone=True to add copies instead

Finalization (derive → check required fields → validate; see the model lifecycle) runs automatically inside Simulation; call model.finalize() directly to check a model without building.

Useful instance methods:

MethodPurpose
get(id) / query(**fields) / query_type(T) / deepquery(...)Query the model tree
rootWalk up to the root instance
clone() / deepcopy()Copy a subtree (deepcopy round-trips through serialization)
sample(seed)Concrete copy of an uncertain model
validate() / derive() / finalize()Run lifecycle stages explicitly
to_markdown()Human-readable reference doc for the ontology

Serialization APIs

See Serialization (SML) for the format.

APIPurpose
model.serialize(scheme="json"/"yaml")Data document for a tree of instances
model.schema()Schema dict for the root's tree
model.gen_sml(file=...)Full SML document (data + schema) for one root
Ontology.bulk_gen_sml([a, b])SML for multiple roots / graph-shaped models (crawls relationship fields for schema coverage)
serialize_model / deserialize_modelRound-trip lists of models (deserialization needs the ontology classes in scope)

Debugging

Environment flags useful when developing models:

VariableEffect
SCF_STATEMANAGER_LOG_EXECUTION (_FILTER)Log each state-manager call (optionally filtered by function-name glob)
SCF_STATEMANAGER_TIME_EXECUTIONEmit per-call timing
SCF_STATEMANAGER_DUMP_ARGS_ON_ERROR (+SCF_STATEMANAGER_DUMP_REPRO, _REPRO_DIR)On error, log arguments and write a standalone repro script
SCF_STATEMANAGER_DETECT_MUTATIONWarn when an implementation mutates its inputs (implementations must be pure)
SEDARO_DEBUG_DERIVERSSurface underlying deriver exceptions instead of the aggregate error

Gotchas

  • State-manager implementations take no self; parameters bind positionally to consumed.
  • consumed must be written as a tuple: "(a,)", not "a".
  • TypedDatum annotations must carry a type string, and cannot appear inside a Union/Optional — use an optional SedaroTS type ("rad?") instead.
  • type is reserved; don't declare or set it.
  • A field's default of None is only meaningful for Optional fields; None does not otherwise count as "has a default".
  • Model code must live in an importable module (not inline in a notebook or __main__) — behavior implementations are referenced by module path, so the simulator must be able to import them.
info

An older Python representation (Block / Metamodel and the pydantic-based quantity kinds) still exists for legacy content. New models should use Ontology from sedaro.model.v2.