Skip to content
Get started

Execution, diagnostics, and arrays

Current-source Python API. Install this source revision, then run and inspect. Canonical guide source.

One run lifecycle

Blocking and awaitable execution use the same native worker, state machine, and once-materialized result:

python
field = model.field(model.field_ids[0])
plan = eqiora.resolve(
model,
temporal=eqiora.time.Tsitouras45(
initial_step_s=0.01,
relative_tolerance=1.0e-9,
absolute_tolerances={field: 1.0e-11},
),
)
state = eqiora.State.initial(plan)
run = eqiora.submit(
plan,
state=state,
until_s=1.0,
output_times_s=(1.0,),
)
print(run.status, run.progress)
result = run.result()
# The blocking convenience uses the same lifecycle.
same_kind_of_result = eqiora.run(
plan,
state=state,
until_s=1.0,
output_times_s=(1.0,),
)

RunStatus records a finite accepted history from creation through one terminal state. progress is an execution-family-specific coalesced snapshot, not a percentage or event log. Repeated result() calls return the same immutable Python result object.

Awaiting does not introduce another native runtime:

python
async def simulate(plan):
run = eqiora.submit(
plan,
state=eqiora.State.initial(plan),
until_s=1.0,
output_times_s=(1.0,),
)
try:
return await run
finally:
if not run.done:
run.cancel()

Cancelling the surrounding asyncio task and dropping a Run do not implicitly cancel native work. Call run.cancel() explicitly. Cancellation is cooperative at accepted execution boundaries, publishes typed cancellation evidence, and never exposes a partial result. A request after the last cancellable boundary may still complete.

Long native waits release the ordinary CPython GIL only after inputs are owned. Solver iterations do not call Python. Free-threaded Python and subinterpreter shutdown remain separate capabilities.

Structured failures

Eqiora model and execution failures derive from EqioraError. Stable subclasses distinguish validation, compatibility, capability, execution, cancellation, and internal failures. Every Eqiora-raised error retains structured diagnostics:

python
try:
result = eqiora.run(
plan,
state=eqiora.State.initial(plan),
until_s=-1.0,
output_times_s=(-1.0,),
)
except eqiora.EqioraError as error:
print(error.category)
for diagnostic in error.diagnostics:
print(diagnostic.code, diagnostic.severity, diagnostic.message)
print(diagnostic.graph_path, diagnostic.source_span)

Python call-shape mistakes remain ordinary TypeError rather than fabricated model diagnostics. Guarded native boundaries sanitize unwinding Rust panics as InternalError; process abort and memory exhaustion are not recoverable claims.

NumPy ownership

An Array owns a dense, native-endian, rank-one CPU float64 allocation. Inspecting descriptors does not import NumPy.

python
array = result["x"].values
view = array.numpy(copy=False) # `None` has the same meaning
writable = array.numpy(copy=True)
assert not view.flags.writeable
assert writable.flags.writeable

The first no-copy projection transfers the native allocation once into an opaque owner. The resulting C-contiguous NumPy array is irreversibly read-only and remains alive independently of the Result and Array handles. copy=True returns an independent writable allocation.

DLPack

Eqiora exports an independent versioned CPU snapshot:

python
import numpy as np
snapshot = np.from_dlpack(array)

The snapshot never aliases immutable result evidence. Legacy capsule requests, non-CPU transfers, non-None streams, and copy=False fail closed because consumer enforcement of DLPack’s advisory read-only flag is not universal.

Differentiable-program inputs may arrive from a complete CPU:0 DLPack producer. Eqiora requests a no-transfer view, validates dtype, rank, length, byte order, alignment, and contiguity, then makes one documented owned staging copy before native execution. This is not a zero-copy execution-input claim. GPU streams, sparse/distributed arrays, and general Run inputs remain separate contracts.

Fixed arrays in sampled sessions

The bounded reference Model.sampled_session path accepts invariant real or exact integer channel arrays through its existing typed input tables. For a Model with drive: array<1, 2> at tick, pass one complete tuple per tick:

python
session = model.sampled_session(
end_time_s=2, max_step_s=0.1,
inputs={"drive": ("tick", [(3, 4), (5, 6), (7, 8)])},
)
session.advance_ticks(1)
resumed = model.resume_sampled(session.checkpoint())

Array State values and accepted output values are complete nested tuples; exact integer components remain Python integers, including values above 2**53. A failed tick commits neither a partial array nor another State update. Restart preserves the accepted clock position and previously absent or present outputs. The immutable Model fixes every extent. Scalar broadcasting, partial indexed writes, spatial tensors and complex execution are not admitted by this path. Input and retained output limits count scalar components, including nested arrays.