Skip to content

Portable IR and bounded symbolic derivatives

export_ir(compiled) returns a PortableIR envelope tied to the existing StructuralModelIdentity. It preserves the normalized typed DAG, ordered states and parameters, axes/coordinates, constants, aliases, observables, known-input metadata and declarations. It does not expand lazy per-cell equations or serialize fitted arrays, executable closures or a Python pickle.

import numpy as np
from op_system import (
    PortableIR,
    compile_spec,
    export_ir,
    export_julia,
    symbolic_derivatives,
)

compiled = compile_spec({
    "kind": "expr",
    "state": ["x", "z"],
    "aliases": {"loss": "a*x", "net": "loss-b*z"},
    "equations": {"x": "-net", "z": "net"},
    "observables": {"measurement": {"expr": "gain*x+z+t"}},
})
interchange = export_ir(compiled)
restored = PortableIR.from_json(
    interchange.to_json(),
    expected=interchange.identity,
)
assert restored == interchange
assert restored.decode()["state_names"] == ("x", "z")

artifact = symbolic_derivatives(compiled)
assert artifact.parameter_names == ("a", "b", "gain")
matrices = artifact.evaluate(0.5, np.array([2.0, 4.0]), a=0.3, b=0.7, gain=3.0)
assert np.allclose(matrices.state, [[-0.3, 0.7], [0.3, -0.7]])
assert np.allclose(matrices.observable_parameters, [[0.0, 0.0, 2.0]])

julia = export_julia(compiled)
assert julia.parameter_names == artifact.parameter_names
assert julia.observable_names == ("measurement",)
assert julia.rational

Supply an independently expected identity when reading a saved export. Import checks the canonical manifest's bytes against that identity and rejects unknown schemas, extra envelope fields, duplicate JSON keys and malformed/cyclic node references. Decoding returns readonly mappings, tuples and IRRecord data; it does not import or instantiate the named record classes. A content identity does not authenticate a source or establish an external tool's mathematical result.

Interchange schema

The outer schema has exactly format="op-system-external-ir", integer schema_version=1, source and manifest. source is the existing schema-1 structural identity. manifest is the canonical op-system-structural-model DAG with nodes and a zero-based root. Each node contains a type tag, scalar or field labels, and references to earlier nodes. This preserves sharing and gives non-Python consumers an explicit data format.

Tag Meaning
none, bool, str Typed scalar values, without coercing booleans into integers.
int Decimal integer text.
float Exact Python hexadecimal floating-point text, including explicit metadata infinities where declared.
sequence Ordered child references.
mapping Named children in canonical key order.
record Compiler record type name and ordered field names, interpreted as data.

Expression records include Literal, Sym, Apply, Subscript, Reduce and HistoryOp; consumers declare which operations they implement. Complete observable definitions and measures remain in the normalized metadata, beside the ordered observable names. t is the known independent time variable; time-varying inputs and unknown scalar parameters have distinct declarations. Initial-condition declarations remain metadata. A runtime consumer must supply its initial vector in the exported state order and retain any initial-condition parameterization separately.

Import has a 32 MiB UTF-8 envelope limit. Full compact-IR export is separate from bounded scalar evaluation/emission: preserving an operator or reduction as data does not claim that the scalar Julia emitter implements it.

Native derivative artifacts

symbolic_derivatives supplies ordered expressions and native evaluation of df/dy, df/dtheta, dg/dy and dg/dtheta. Observable-only parameters are included explicitly. Values are a native named tuple, suitable for JAX JIT; no host snapshot or graph detachment occurs. NumPy, JAX and CPU Torch float32/64 are the qualified array cases. CUDA and other devices need separate qualification.

The initial supported subset is bounded scalar real arithmetic, literal powers, exp/log/sqrt, sin/cos and tanh, including transitive aliases. Evaluate at points where the original expression is defined and differentiable. Shaped parameter buffers, history, numerical operators, gauge-fixed execution, variable powers, nonsmooth operations and reduced/vector observables fail explicitly. General compact IR can still preserve those constructs for another consumer.

Default limits are 128 expanded states and 65,536 dense derivative entries. jvp and vjp apply that bounded dense state Jacobian; they do not claim matrix-free storage. Tangent layouts, namespace and precision must match the states. General sparse and operator-aware derivative exports remain separate work.

state_dependencies is conservative IR dependency metadata, independent of current fitted values. A coefficient becoming zero does not remove its declared state dependency. block_axes repeats the compiler's existing checked block axes; no second separability test is introduced. Neither field proves that a statistical objective factorizes or that parameters are globally identifiable.

A parameter-sensitivity solve additionally needs initial-condition derivatives and the observation map. Its measured-output sensitivity is dg/dy @ S + dg/dtheta; an RHS state Jacobian alone is not that map or a Fisher matrix.

Representative construction and evaluation costs

examples/benchmark_symbolic.py --backend jax --states 32 measures a linear ring with three ordered parameters and one scalar measurement, checking all four matrices against an independent analytic reference. Run each backend/size in a fresh process; the retained raw receipt covers 2, 16 and 32 states on a shared Intel Core Ultra 7 265U, one CPU affinity and native thread limits of one. These are advisory measurements, with one worker per case and the median of seven synchronized warm evaluations.

32-state float64 case Derivative construction First evaluation Warm evaluation
NumPy 42 ms 91 ms 2.31 ms
JAX 113 ms 1.41 s 0.291 ms
CPU Torch 54 ms 13 ms 3.90 ms

The artifact contains 1,155 dense entries. First evaluation includes lazy initialization and, for JAX, JIT compilation; construction follows source model compilation, which is recorded separately. Whole-worker peak RSS includes imports and source compilation. This fixture neither selects a native compiler rewrite nor predicts application performance or the cost of a sensitivity solve.

Optional native state actions provide a separate direct JAX/Torch product boundary, including shaped parameters and array observables. They preserve the dense API above; their transform, storage and performance qualification is documented separately.

Julia expression export

export_julia returns a JuliaExport record with source identity, emitter schema version, ordered state/parameter/observable names, a rational-operation flag and code. The code defines out-of-place op_system_rhs(u, p, t) and op_system_observations(u, p, t). State/parameter names become explicit array indices, not executable identifier interpolation. The caller supplies u0 and p in the retained orders; there is no implicit initialization, prior or forcing conversion. julia_rhs is the code-only convenience.

The same bounded scalar IR supplies the Python derivative artifacts and Julia expressions. Non-rational supported functions retain their actual semantics and set rational=False; no rational-tool compatibility is inferred. This is optional interoperability, with no Julia runtime dependency or full Julia package port.

External structural tools still need their exact versions, rank definitions, known-input assumptions, witness points/tolerances, disagreements and censored timeouts retained. Shared-source export removes a hand-transcription step; it does not settle a disputed rank or make different rank notions equivalent.

Execute the independent Julia check

From a source checkout with a Julia executable already available:

uv run --no-sync python examples/portable_julia.py --julia julia

The standalone example emits RHS and measurement code from one compiled model, then compares both with Python evaluation at three independently specified points. It also compares all four derivative matrices with independent central differences executed in Julia (absolute step 1e-5, tolerance 1e-8). Julia 1.12.6 matched all six value rows exactly and passed every derivative comparison in the local qualification. This is numerical expression conformance, not external structural-rank evidence. The program retains model/emitter/runtime identities and uses a temporary depot when none is explicitly supplied.