Execution, diagnostics, and arrays
Running a model
Continue with decay.eqi and the environment from Get started.
Run these blocks from the folder containing that file. Submit the model when
you want a handle to inspect or cancel; use eqiora.run when you simply want to
wait for its result:
import eqiora
model = eqiora.compile(path="decay.eqi")field = model.field("x")plan = eqiora.resolve( model, temporal=eqiora.time.Tsitouras45( initial_step_s=0.01, relative_tolerance=1.0e-9, absolute_tolerances={(field, 0, 0, False): 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 each state the run passes through, from creation to a single
final state. progress is the most recent progress snapshot; its contents depend
on the kind of execution, and it is neither a percentage nor an event log. Calling
result() again returns the same immutable object.
Independent time Runs
EvaluationMapPlan.from_time executes an ordered collection of independent time
Runs through the same native worker as run. Each member keeps its own adaptive
controller, event history and physical State. All members use one Plan and the
same requested observation times; their internal steps need not coincide.
Continuing with the Plan above:
states = [eqiora.State.initial(plan) for _ in range(3)]mapped = eqiora.EvaluationMapPlan.from_time( plan, states, until_s=1.0, output_times_s=(0.25, 0.5, 1.0), chunk_size=3, workers=2,)collection = mapped.execute()if isinstance(collection, eqiora.CompleteEvaluationMap): results = [collection[i] for i in range(len(collection))]else: print(collection.stopped_index, collection.statuses, collection.diagnostics)Every accepted member is an ordinary Result. A failed or cancelled collection
keeps original occurrence indices and any accepted in-flight members. It cannot
be used as a complete collection by dropping failed positions. Map cancellation
is checked before dispatch; it lets already running members finish. Individual
Results retain their ordinary persistence and observation interfaces.
For a stochastic Plan, pass a 32-byte master_seed and one SamplingIdentity
per occurrence through sampling=[...]. Repeated IDs remain repeated paths,
not independent replicas. Complete accepted Results can feed
MomentEstimator.from_time_results; its existing sampling and moment checks
still apply. Optional checkpoints=[...] must correspond exactly to the supplied
original States, sample identities, horizon and output schedule. Each checkpoint
resumes the ordinary atomic physical/stochastic Run state.
storage_bytes_limit bounds the native map’s declared storage charge, while
member_bytes_limit bounds each encoded Result or checkpoint. They do not bound
solver scratch or total process memory. The map preserves native interpolation,
event-side sampling and termination semantics; it adds no dense ragged output,
automatic padding, or trajectory differentiation.
Phase-level profiling
Pass profile=True to run or submit to record where the time goes. Profiling
does not change the Model, Plan, numerical result, or saved Result:
result = eqiora.run( plan, state=eqiora.State.initial(plan), until_s=1.0, output_times_s=(1.0,), profile=True,)print(result.profile.summary())for event in result.profile.events: print(event.path, event.fields)For each phase, the summary gives the total inclusive time, the self time, the
number of calls, and the mean inclusive time per call. Self time excludes time
spent in child phases, so adding up the rows does not give the run’s total. Each
ProfilePhase has the same values as inclusive_seconds, self_seconds, calls,
and mean_seconds.
Phases are grouped by their nested path and their identifying fields, available as
ProfilePhase.fields. Per-occurrence values such as step, time, iteration, and
residual are recorded on each ProfileEvent and do not split a phase into
separate rows. For phases without such values, only one representative event is
kept while ProfilePhase holds the full count and timing, so storage does not
grow with the number of cells. Setup is split into backend resolution and
discretization preparation, and assembly into initial linearization and
line-search trials. Transient work is nested under run/solve/time_step, and
Faer SparseLU reports symbolic factorization, numeric factorization, and back
substitution separately. A Result loaded from bytes has no profile, because timing
data is not part of the saved artifact. Leave profiling off for ordinary runs.
A submitted run can also be awaited from asyncio, using the same native runtime:
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 asyncio task or dropping the Run does not stop native work; call
run.cancel(). Cancellation takes effect at the next checkpoint during execution
and never produces a partial result. A run that has already passed its last
checkpoint may still complete.
Native execution releases the GIL once it has copied its inputs, and solver iterations never call back into Python. This applies to standard CPython with the GIL; free-threaded builds and subinterpreter shutdown are not covered.
Stopping and resuming an adaptive run
Explicit Tsitouras45 Runs without forward sensitivities can retain their exact
adaptive history, including supported event resets. RunProgress.accepted_steps
is cumulative even when observations are dropped; model_time_s is an accepted
boundary. maximum_steps is None for adaptive integration. A cancellation
request is checked between native model evaluations and before a trial is
accepted. It cannot interrupt an individual model evaluation already in progress.
run = eqiora.submit( plan, state=eqiora.State.initial(plan), until_s=1000.0, output_times_s=(1.0, 10.0, 1000.0),)run.cancel()try: result = run.result() # A run that already finished may still return normally.except eqiora.CancellationError: stopped = run.cancellation print(stopped.progress.accepted_steps, stopped.state.time_s) plan.write("decay.eqplan") stopped.checkpoint.write("decay.eqcheckpoint")
# These files can also be opened in a fresh Python process. reopened_plan = eqiora.Plan.read("decay.eqplan") checkpoint = eqiora.ExecutionCheckpoint.read( reopened_plan, "decay.eqcheckpoint", ) result = eqiora.run(reopened_plan, checkpoint=checkpoint)Resume preserves the original initial State, horizon, requested output times,
accepted history, controller decision and remaining event budget. Supply only
checkpoint= with its exact Plan; passing a new State or output schedule is an
error. The same provider version and supported numerical profile are required.
Malformed or incomplete checkpoints and changed Plans reject before execution.
The equivalence claim concerns numerical history and outputs; elapsed execution
time may differ.
Restart is a new integration from a physical State: submit stopped.state with a
new explicit horizon and output schedule. It has a fresh controller. Retry starts
the original request again from its original State. Neither operation is an exact
continuation of the interrupted numerical history. A failed Run publishes no new
checkpoint; a previously saved accepted checkpoint can still be reopened.
Other numerical methods and forward sensitivities do not currently support these
adaptive controls. Reference ExecutionSession checkpoints remain in memory and
resume through Model.resume_execution; they cannot be written as adaptive
checkpoint files. Cross-provider/build, distributed and GPU continuation are not
supported.
Structured failures
Every model or execution error raised by Eqiora derives from EqioraError, with
subclasses for validation, compatibility, capability, execution, cancellation, and
internal failures. Each one carries structured diagnostics:
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)Calling a function with the wrong arguments raises an ordinary TypeError. A Rust
panic inside Eqiora is reported as InternalError, but a process abort or running
out of memory cannot be recovered from.
NumPy ownership
An Array holds a dense, native-endian, rank-one float64 buffer in CPU memory.
Reading its shape and type does not import NumPy.
array = result.series(field).valuesview = array.numpy(copy=False) # `None` has the same meaningwritable = array.numpy(copy=True)
assert not view.flags.writeableassert writable.flags.writeableWith copy=False, the buffer is handed to NumPy without copying. The resulting
C-contiguous array is permanently read-only and stays valid after the Result and
Array are gone. copy=True returns a separate, writable copy.
DLPack
DLPack consumers receive a copy of the data on the CPU:
import numpy as np
snapshot = np.from_dlpack(array)The copy never shares memory with the result. Requests for a legacy capsule, a
non-CPU device, a stream other than None, or copy=False raise an error,
because not every DLPack consumer respects the read-only flag.
Inputs to a differentiable program can come from any CPU:0 DLPack producer. Eqiora checks the dtype, rank, length, byte order, alignment, and contiguity, then copies the input before native execution.
Fixed arrays in execution sessions
Model.execution_session accepts fixed-size arrays of real or integer values as
inputs, one complete tuple per tick. This model accumulates two input channels:
sampled = eqiora.compile(source="""public model Accumulator( clock tick: periodic, input drive: array<1, 2> at tick, output total: array<1, 2> at tick) { state memory: array<1, 2> at tick; initial { pre(memory) = [0, 0]; } relation update at tick { next(memory) = [pre(memory)[0] + drive[0], pre(memory)[1] + drive[1]]; total = next(memory); }}""", entry="Accumulator", bindings={"tick": eqiora.ClockDomain(period_s=1)})session = sampled.execution_session( end_time_s=2, max_step_s=0.1, inputs={"drive": ("tick", [(3, 4), (5, 6), (7, 8)])},)session.advance_ticks(1)resumed = sampled.resume_execution(session.checkpoint())resumed.advance_ticks(2)print(resumed.output("total", 2)) # (Fraction(2, 1), (15.0, 18.0))Array values in States and outputs are nested tuples, and integer components stay
Python integers, even above 2**53. A failed tick leaves the State unchanged. A
resumed session continues from the same clock position, with the same outputs
present or absent. The Model fixes every array size. Execution sessions do not
support scalar broadcasting, writing part of an array, spatial tensors, or complex
values. Limits on inputs and stored outputs count individual scalar components.
For ordinary ODE runs, state.value(field) returns the whole declared value: real
scalars as float, complex scalars as complex, and arrays as nested immutable
tuples with the declared axes. derivative_order= selects a stored higher-order
derivative; asking for one that is not stored is an error. Saving and restarting a
State keeps these values intact. result.series(field, component=..., imaginary=...) selects one real coordinate as a time series.