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,Enumsubclasses) 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 aliasesVector2–Vector9,Matrix2–Matrix9, andQuaternionare available insedaro.model.v2.types. - Defaults follow the annotation:
angle: Annotated[TypedDatum, "rad"] = 0.0. An explicit typed default can be built withType("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 |
|---|---|
float | f64 |
int | int |
bool | bool |
str | str |
| ID | id |
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 tuple | Tuple 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
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:
| Method | Purpose |
|---|---|
get(id) / query(**fields) / query_type(T) / deepquery(...) | Query the model tree |
root | Walk 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.
| API | Purpose |
|---|---|
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_model | Round-trip lists of models (deserialization needs the ontology classes in scope) |
Debugging
Environment flags useful when developing models:
| Variable | Effect |
|---|---|
SCF_STATEMANAGER_LOG_EXECUTION (_FILTER) | Log each state-manager call (optionally filtered by function-name glob) |
SCF_STATEMANAGER_TIME_EXECUTION | Emit 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_MUTATION | Warn when an implementation mutates its inputs (implementations must be pure) |
SEDARO_DEBUG_DERIVERS | Surface underlying deriver exceptions instead of the aggregate error |
Gotchas
- State-manager implementations take no
self; parameters bind positionally toconsumed. consumedmust be written as a tuple:"(a,)", not"a".TypedDatumannotations must carry a type string, and cannot appear inside aUnion/Optional— use an optional SedaroTS type ("rad?") instead.typeis reserved; don't declare or set it.- A field's default of
Noneis only meaningful forOptionalfields;Nonedoes 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.
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.