1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
use super::*;
// ============================================================================
// Errors
// ============================================================================
// `CompileError` is defined in the non-gated `crate::compile_error` module so
// the WASM-compiled `faq` / `join` passes can name it; re-exported here
// to preserve the native `crate::simulate::CompileError` path.
pub use crate::compile_error::CompileError;
/// Errors raised building an [`crate::problem::EsmProblem`] or running
/// [`crate::problem::solve`].
#[derive(Error, Debug)]
pub enum SimulateError {
/// Wraps a CompileError raised by [`crate::problem::esm_problem`]
/// before solving even starts.
#[error("Compile failed: {0}")]
Compile(#[from] CompileError),
/// diffsol returned a solver-internal error (build failure, step failure,
/// etc.).
#[error("diffsol error: {details}")]
DiffsolError {
/// The underlying diffsol error message.
details: String,
},
/// The integrator could not satisfy the requested tolerances.
#[error("Tolerance not met")]
ToleranceNotMet,
/// [`crate::problem::solve`] was called on a EsmProblem whose document
/// declares no differential equations, so there is nothing to integrate.
/// Its build-time products are still readable with
/// [`crate::problem::observed_field`].
#[error("Nothing to integrate: {details}")]
NotDynamic {
/// Why the EsmProblem carries no integrable right-hand side.
details: String,
},
/// [`crate::problem::ProblemOptions::compiler`] named a value that is in
/// API_SPEC §5.8's closed vocabulary but that this binding, this build or
/// this process cannot provide (esm-spec §9.6.6 `compiler_unavailable`).
///
/// Never answered by building with a different compiler: the whole point
/// of naming one is that the caller knows which ran. `details` names what
/// would have to be loaded or built.
#[error(
"{code}: compiler '{compiler}' is not available here: {details}",
code = crate::diagnostic::codes::COMPILER_UNAVAILABLE
)]
CompilerUnavailable {
/// The vocabulary spelling the caller asked for.
compiler: &'static str,
/// What would have to be loaded or built to provide it.
details: String,
},
/// A range-restricted or operator-restricted right-hand side (API_SPEC
/// §5.8: `select`, `halo`, `tile`, `lanes`) was refused. `code` is one of
/// `range_halo_insufficient`, `range_global_locality`,
/// `range_selection_writes_incomplete` or `range_box_invalid`, and
/// `message` names the rule, variable and axis involved.
#[error("{code}: {message}")]
RangeRestriction {
/// The diagnostic code.
code: &'static str,
/// What was refused, naming the rule, variable and axis.
message: String,
},
/// A DATA-FED parameter — one whose `update` is `kind: "data"` (esm-spec
/// §5.4, §8.5) — reached a build with NOTHING bound to it: no provider,
/// no loaded array, and no caller-supplied `p` value (esm-spec §9.6.6
/// `data_source_unbound`, CONFORMANCE_SPEC §5.46).
///
/// Raised at CONSTRUCTION, before any right-hand side is built, so the
/// answer does not depend on the compiler: this asks whether the
/// DOCUMENT's inputs are bound, not what a compiler can lower. A compiler
/// that additionally cannot lower a read of the forcing channel reports
/// that separately as [`CompileError::CompilerRefusedRule`](crate::CompileError::CompilerRefusedRule).
///
/// Refused rather than evaluated at the parameter's `default`, which is
/// not a fallback value but a placeholder: a forcing at its default
/// integrates to a complete, smooth trajectory reported under the label of
/// a quantity the document says is read from a file.
#[error(
"{code}: parameter '{parameter}' is fed by the data source '{data_source}' (an `update` of kind \"data\"), and nothing bound it: no provider, no loaded array, and no `p` value. Pass a `providers` entry for '{parameter}' to supply the data, or `p` to pin a value. The build will not fall back to the parameter's `default`: a forcing at its default produces a whole trajectory that looks like an answer",
code = crate::diagnostic::codes::DATA_SOURCE_UNBOUND
)]
DataSourceUnbound {
/// The flattened parameter nothing bound.
parameter: String,
/// The `data_sources` key its `update` names. Spelled `data_source`
/// rather than `source`, which `thiserror` reserves for an error's
/// CAUSE — a `String` cannot be one.
data_source: String,
},
/// A build progress observer asked [`crate::problem::esm_problem`] to
/// stop (returned [`Flow::Cancel`]).
///
/// Distinct from every other variant in that nothing went wrong: it is the
/// caller's own decision. It stays an ERROR — unlike a cancelled *solve*,
/// which is [`ReturnCode::Terminated`] with a partial trajectory — because a
/// half-built EsmProblem is not a usable result.
#[error("Cancelled by the caller during the build: {details}")]
Cancelled {
/// The phase and item the build stopped at.
details: String,
},
/// [`crate::problem::remake`] was asked to substitute a binding the
/// EsmProblem cannot honour without redoing part of construction.
///
/// Raised rather than silently rebuilding or silently ignoring the
/// substitution (`esm-libraries-spec.md` §2.5.5): the name and the class
/// that makes it un-substitutable are both reported so the caller knows to
/// build a fresh EsmProblem instead.
#[error("Cannot remake '{name}': {class}")]
UnsubstitutableBinding {
/// The binding the caller tried to substitute.
name: String,
/// The class that makes it un-substitutable.
class: String,
},
/// The caller supplied a time span whose start or end is not a finite
/// number — `NaN`, `+∞` or `−∞`.
///
/// A document cannot spell one: the schema's `TimeSpan` bounds are JSON
/// numbers, and esm-spec §6.6.4 records that "JSON has no infinite
/// literal". So this is a caller-supplied argument of a solve,
/// validated alongside the parameter and initial-condition maps of the same
/// call.
///
/// Refused rather than interpreted. Every ordering test against `NaN` is
/// false, so a `NaN` endpoint reads as a run that cannot advance and would
/// hand back the initial state as though it were a trajectory; an infinite
/// end gives the solver loop a stop time it can never reach.
#[error("Invalid time span ({start}, {end}): start and end must both be finite")]
InvalidTimeSpan {
/// The requested start of the interval.
start: f64,
/// The requested end of the interval.
end: f64,
},
/// The user supplied a parameter name that does not appear in the
/// flattened system.
#[error("Invalid parameter '{name}'")]
InvalidParameter {
/// The unknown parameter name.
name: String,
},
/// The user supplied a key that two or more of the flattened system's
/// parameters carry as a DOTTED SUFFIX — a shared local name (`gain` for
/// `Left.gain` and `Right.gain`) or a shared partial qualification
/// (`sub.g` for `Left.sub.g` and `Right.sub.g`), esm-spec §6.6.2 rule 4.
/// Distinct from [`SimulateError::InvalidParameter`]: the name exists, it
/// just does not identify ONE parameter, and binding it to either
/// candidate would be a wrong answer rather than a missing one.
#[error(
"Ambiguous parameter '{name}': carried as a suffix by {candidates:?} — qualify it further with its owning component"
)]
AmbiguousParameter {
/// The ambiguous local or partially qualified name.
name: String,
/// The qualified parameters that carry it.
candidates: Vec<String>,
},
/// Two or more NON-EXACT `parameter_overrides` keys designate the one
/// flattened parameter (esm-spec §6.6.2): a bare local spelling and a
/// more-qualified one (`solo` and `Doc.Left.solo` both landing on
/// `Left.solo`), or two more-qualified ones (`A.M.g` and `B.M.g` both
/// landing on `M.g`). The caller wrote two overrides and only one can take
/// effect, so picking a winner would be a wrong answer rather than a
/// missing one. An EXACT key is never part of a collision — it identifies
/// its parameter outright and wins over any suffix or bare claim on it.
#[error(
"parameter_overrides: {} keys designate the parameter '{name}' ({}). Supply exactly one override key per name (esm-spec §6.6.2).",
.keys.len(), .keys.join(", ")
)]
CollidingParameterKeys {
/// The flattened parameter every colliding key designates.
name: String,
/// The colliding keys, sorted.
keys: Vec<String>,
},
/// The user supplied an initial condition for a name that is not a state
/// variable, or a state variable has no initial value (no entry in
/// `initial_conditions` and no `default` on the `ModelVariable`).
#[error("Invalid initial condition '{name}'")]
InvalidInitialCondition {
/// The variable name.
name: String,
},
/// The user supplied a key that two or more of the flattened system's
/// states carry as a DOTTED SUFFIX (esm-spec §6.6.2 rule 4). The
/// state-side counterpart of [`SimulateError::AmbiguousParameter`].
#[error(
"Ambiguous initial condition '{name}': carried as a suffix by {candidates:?} — qualify it further with its owning component"
)]
AmbiguousInitialCondition {
/// The ambiguous local or partially qualified name.
name: String,
/// The qualified states that carry it.
candidates: Vec<String>,
},
/// Two or more NON-EXACT `initial_conditions` keys designate the one state
/// element (esm-spec §6.6.2). The state-side counterpart of
/// [`SimulateError::CollidingParameterKeys`].
#[error(
"initial_conditions: {} keys designate the state '{name}' ({}). Supply exactly one override key per name (esm-spec §6.6.2).",
.keys.len(), .keys.join(", ")
)]
CollidingInitialConditionKeys {
/// The state element every colliding key designates.
name: String,
/// The colliding keys, sorted.
keys: Vec<String>,
},
/// An `ic(target)` field initial condition could not be resolved to a
/// per-cell value. Carries the `ic(...)` target state name plus a
/// diagnostic saying why (wrong field rank, unresolvable RHS, ...), so
/// the name field stays a plain identifier.
#[error("Invalid field initial condition for '{name}': {details}")]
InvalidFieldInitialCondition {
/// The `ic(...)` target state name.
name: String,
/// Why the initial condition could not be resolved.
details: String,
},
/// A data provider bound in [`crate::problem::ProblemOptions`] failed to
/// materialize its loader field, or produced the wrong number of fields for
/// its target forcing variable.
#[error("Provider for '{name}': {details}")]
ProviderError {
/// The forcing variable the provider was bound to.
name: String,
/// What went wrong.
details: String,
},
}
impl SimulateError {
/// Whether this is a build progress observer's own [`Flow::Cancel`] rather
/// than something going wrong — the counterpart of reading
/// [`ReturnCode::Terminated`] off a [`Solution`].
pub fn is_cancelled(&self) -> bool {
matches!(self, SimulateError::Cancelled { .. })
}
}