Skip to main content

Constraints & Derivation

Beyond types, SedaroML captures two further classes of model-time logic:

  • Validators — logical constraints a model must satisfy to be realizable
  • Derivers — attributes computed from other parts of the model

Both run when a model is finalized — i.e. at configuration time, before a simulation is ever built. This is a core reliability property of the language: a model that finalizes successfully has already had its constraints checked and its derived state populated, so entire classes of errors can never reach the simulator.

Validators

A validator is a named predicate attached to an ontology. It inspects the instance (and, through relationships, the surrounding model) and raises an error if the model is not physically or logically realizable:

@validator
def validate_length(self):
if self.length <= 0:
raise ValueError("Pendulum length must be positive.")

Validators express the "logical value constraints" of the language — range checks (3 < x <= 12), conditional requirements (if y > 7: z must be undefined), cross-field consistency (e.g. "the number of threats must match the number of launch times"), and so on.

Validation is hierarchical: validating an instance validates its children too, and structural rules (such as duplicate IDs) are checked alongside user-declared validators.

Derivers

A deriver computes fields of its own instance from other information in the model — its own fields, its children, or related instances:

@deriver
def order_threats(self):
pairs = sorted(zip(self.launchTimes, self.threats), key=lambda p: p[0])
self.launchTimes = [t for t, _ in pairs]
self.threats = [threat for _, threat in pairs]

Derivation has deliberately constrained semantics:

  • Fixed-point evaluation. All derivers in the model tree are run repeatedly until every one succeeds. A deriver that depends on another deriver's output simply fails (harmlessly) until its inputs exist and is retried. If a full pass makes no progress, derivation aborts with the collected errors — this is how circular dependencies surface.
  • Single writer. Each field may be written by at most one deriver. Two derivers writing the same field is an error, so derived state has an unambiguous provenance.
  • Ownership. A deriver may only write fields of its own instance. Writes to other instances are rejected. Reads are unrestricted — derivers routinely read from related instances (e.g. an Interceptor deriving its initial position from its interceptorSite's coordinates).
  • Atomicity. A deriver that fails leaves its instance's fields untouched (writes are rolled back before retry).

Together these rules make derivation declarative in spirit: derivers state what a field is in terms of other state, and the framework finds a valid evaluation order — or proves there isn't one.

The Model Lifecycle

Constraints and derivation fit into a defined lifecycle between model authoring and simulation:

  1. Construct — instances are created with field values; values are coerced as they are set.
  2. Compose — instances are added to parents; sibling requirements are checked as the tree is assembled.
  3. Finalize — on the root of the tree:
    • Derive runs the fixed-point derivation described above.
    • Required fields that are still missing are reported as errors; remaining values are coerced.
    • Validate runs all validators, hierarchically.
  4. Build — the finalized model is handed to the Simulation Compiler, which performs its own further checks (type/unit alignment, query resolution, feasibility).

Finalization happens automatically when a model is submitted for simulation, and can also be invoked directly to check a model without building anything.