ZVODEResult Design Specification¶
Status: draft for the 0.4.0 release.
Goals¶
Add
success,status, andmessageso users never interpret raw ZVODEISTATEvalues.Stay duck-type compatible with
scipy.integrate.OdeResult: code likeif not sol.success: print(sol.message)works verbatim. SciPy — not diffrax or DifferentialEquations.jl — is the compatibility target, since practitioners switch betweensolve_ivpandsolve_complex_ivp.Minimal and stable: every field is plain data; names are reserved (not added) for features that do not exist yet, so future enhancements slot in without changing existing meanings.
Fields¶
ZVODEResult remains a dict subclass with attribute access, always
containing:
Field |
Type |
Origin |
Meaning |
|---|---|---|---|
|
float or |
existing |
Output time(s); scalar in endpoint-only mode |
|
|
existing |
Solution state(s); |
|
bool |
new |
|
|
int |
new |
|
|
str |
new |
Human-readable termination reason |
|
int |
existing |
RHS evaluations |
|
int |
existing |
Jacobian evaluations |
|
int |
existing |
LU decompositions |
|
int |
existing |
Internal solver steps |
|
int |
existing |
Nonlinear iterations |
|
int |
existing |
Nonlinear convergence failures |
|
int |
existing |
Local error test failures |
This is a strict superset of the OdeResult fields that can exist without
dense output and events. The ZVODE-specific counters stay flat alongside
the SciPy ones (no nested stats object). All four (nsteps, nni,
ncfn, netf) are kept in the frozen field set. They are niche — most
users will only read nfev/njev/nlu — but they come free from iwork
and cost nothing to carry, and adding a counter later is non-breaking while
removing one after 1.0 would not be.
All counters are cumulative tallies over the entire solve_complex_ivp
call (ZVODE zeroes them on the initial call only, and the drivers carry
the ISTATE=2 continuation between knots). Future counters must follow
the same rule; per-step diagnostics (HU, NQU, …) do not belong on
the result.
y shape and memory layout¶
Indexing (API, same as SciPy and DifferentialEquations.jl): shape
(n, m);y[i, :]is componentiover time,y[:, k]the state att[k].Layout (guaranteed; SciPy leaves it unspecified): column-major (
y.flags.f_contiguous). This is the natural layout end to end — the knots driver fills one column per knot, the adaptive driver appends a column per accepted step — and makesy[:, k]a contiguous view that can go back into Fortran/LAPACK without a copy.
In endpoint-only mode y is 1-D (n,) and layout does not arise.
status, success, message¶
status uses SciPy semantics, not raw ISTATE. success is defined as
status >= 0, not status == 0 — success is a set of codes (the
lesson behind SciMLBase.successful_retcode), so future event
termination (status == 1) counts as success without callers updating.
status is a plain int; promoting it to an IntEnum later (symbolic
names that still compare equal to the ints) is compatible and out of
scope.
message is for humans — code discriminates on status, never by
parsing message. On success it is SciPy’s generic text (“The solver
successfully reached the end of the integration interval.”). On failure
it gives the failure location plus the ZVODE condition text (the shared
MESSAGES table, also used by the class-based API):
|
|
message detail |
|---|---|---|
|
|
success text |
|
|
Excess work done on this call. |
|
|
Excess accuracy requested. |
|
|
Illegal input detected. |
|
|
Repeated error test failures. |
|
|
Repeated convergence failures. |
|
|
Error weight became zero during problem integration. |
No remedy hints are appended: the condition text plus the raw ISTATE
value is enough, and solver-specific advice (which would name
solve_complex_ivp parameters) does not belong in the message text shared
with the class-based API. Raw ISTATE is not a field; it appears verbatim
inside message, which suffices for bug reports.
Failure behaviour: raise, carrying the failed result¶
Unlike SciPy (which returns with success=False), solve_complex_ivp
keeps raising — code that forgets to check success gets a loud error
instead of silently consuming a truncated trajectory. The bare
RuntimeError becomes ZVODEError(RuntimeError) with a result
attribute: the fully-populated ZVODEResult with success=False,
status=-1, the failure message (which is also the exception text),
the partial t/y trajectory, and all counters.
try:
sol = solve_complex_ivp(fun, tspan, y0)
except ZVODEError as exc:
partial = exc.result # success=False; plot partial.t, partial.y
Existing except RuntimeError handlers keep working. An opt-in flag for
SciPy’s return-instead-of-raise behaviour can be added later without
touching the result object.
Reserved names¶
Reserved for future features, not added as dead None fields now
(zvode has no dense_output/events parameters, so omission matches
SciPy’s “None if not requested” contract):
Reserved |
Future feature |
|---|---|
|
Dense output: callable interpolant, signature |
|
Event support (SciPy layout: lists of ndarrays) |
|
Event termination |
The k in sol(t, k=0) is the derivative order: ZVINDY computes k-th
derivatives natively (the refine path already uses it), so zvode can
offer what scipy.integrate.OdeSolution cannot, at no Fortran-level
cost. Committing to the signature now means it never changes. Making
the result itself callable (DiffEq-style) is possible sugar later.
Anti-goals¶
Deliberate non-features:
Plain data only — no references to
fun/jac/ctxor workspace arrays; picklable as-is. (DifferentialEquations.jl stores the problem on its solution and needsstrip_solutionto serialize — avoid that.)No array interface — slicing lives on
result.y, not on the result.No raw solver state — no
ISTATE/RWORK/IWORK/Nordsieck fields; diffrax-stylesolver_stateis a JAX artifact with no place here.
Pretty-printing¶
The SciPy/MATLAB aligned key: value layout (what solve_ivp and MATLAB
users already see), but with MATLAB-style array placeholders instead of
SciPy’s numpy-formatted array contents — arrays are summarised as
[shape dtype], never dumped. The exception is t: seeing the
integration interval is genuinely useful (SciPy, DifferentialEquations.jl,
and diffrax all print the time values; only MATLAB hides them), and since
t is monotonic its first and last entries convey the interval without a
dump:
message: The solver successfully reached the end of the integration interval.
success: True
status: 0
t: [58 float64] 0.0 to 6.2832
y: [2x58 complex128]
nfev: 131
njev: 0
nlu: 0
nsteps: 57
nni: 0
ncfn: 0
netf: 1
Rules: keys right-justified, verdict block (message/success/status)
first, then t, y, then counters; dict fields outside the canonical
order are appended so nothing goes missing; t carries the
first to last interval suffix; scalars render with repr;
__repr__ and __str__ are identical (the shell shows __repr__). The
printout is not API — never parse it — so the rendering can change in
any release.
Implementation notes¶
success/status/messageare built at the single point insolve_complex_ivpwhere the result dict is constructed; the failure path builds the same dict (truncatedt/y) before raisingZVODEError.The class-based
ZVODEstepper API is unaffected.
Acceptance criteria¶
Successful solve:
success is True,status == 0, generic successmessage.Failing solve raises
ZVODEError;exc.resulthassuccess is False,status == -1, amessagecontaining theISTATEdetail, and the partial trajectory (closes the release-plan item on catchable failures).pickle.loads(pickle.dumps(result))round-trips.A
ZVODEResultpasses for anOdeResultin duck-typed code readingt,y,success,status,message,nfev,njev,nlu.