use std::collections::{BTreeMap, HashMap};
use std::path::{Path, PathBuf};
use std::rc::Rc;
use ndarray::ArrayD;
use serde_json::Value as JsonValue;
use crate::classification::DeclaredRank;
use crate::flatten::FlattenedSystem;
use crate::precision::{self, Precision};
#[cfg_attr(not(feature = "solve"), allow(unused_imports))]
use crate::simulate::Solution;
use crate::simulate::{Flow, Progress, ProgressFn, SimulateError, SolveOptions};
use crate::types::EsmFile;
use crate::simulate_array::{ArrayCompiled, BuildInspection};
#[cfg(target_arch = "wasm32")]
pub type CallbackFn = std::rc::Rc<dyn for<'p> Fn(&Progress<'p>) -> Flow>;
#[cfg(not(target_arch = "wasm32"))]
pub type CallbackFn = std::sync::Arc<dyn for<'p> Fn(&Progress<'p>) -> Flow + Send + Sync>;
#[derive(Clone, Default)]
pub struct CallbackSet {
entries: Vec<(String, CallbackFn)>,
}
impl std::fmt::Debug for CallbackSet {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CallbackSet")
.field("names", &self.names())
.finish()
}
}
impl CallbackSet {
pub fn new() -> Self {
Self::default()
}
pub fn of(name: impl Into<String>, f: CallbackFn) -> Self {
let mut s = Self::new();
s.push(name, f);
s
}
pub fn push(&mut self, name: impl Into<String>, f: CallbackFn) -> &mut Self {
self.entries.push((name.into(), f));
self
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn names(&self) -> Vec<&str> {
self.entries.iter().map(|(n, _)| n.as_str()).collect()
}
pub fn invoke(&self, p: &Progress<'_>) -> Flow {
let mut flow = Flow::Continue;
for (_, f) in &self.entries {
if matches!(f(p), Flow::Cancel) {
flow = Flow::Cancel;
}
}
flow
}
}
pub fn compose(a: &CallbackSet, b: &CallbackSet) -> CallbackSet {
let mut out = a.clone();
out.entries.extend(b.entries.iter().cloned());
out
}
pub enum ProblemInput<'a> {
Path(&'a Path),
Json(&'a JsonValue),
File(&'a EsmFile),
Flattened(&'a FlattenedSystem),
}
impl<'a> From<&'a Path> for ProblemInput<'a> {
fn from(p: &'a Path) -> Self {
ProblemInput::Path(p)
}
}
impl<'a> From<&'a JsonValue> for ProblemInput<'a> {
fn from(v: &'a JsonValue) -> Self {
ProblemInput::Json(v)
}
}
impl<'a> From<&'a EsmFile> for ProblemInput<'a> {
fn from(f: &'a EsmFile) -> Self {
ProblemInput::File(f)
}
}
impl<'a> From<&'a FlattenedSystem> for ProblemInput<'a> {
fn from(f: &'a FlattenedSystem) -> Self {
ProblemInput::Flattened(f)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Rhs {
#[default]
Auto,
Always,
Never,
}
#[deprecated(
since = "0.2.0",
note = "renamed to `Rhs`; the field is now `ProblemOptions::rhs`"
)]
pub type Compile = Rhs;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Compiler {
#[default]
Native,
Interpreter,
Xla,
Mtk,
Sympy,
}
impl Compiler {
pub fn as_str(self) -> &'static str {
match self {
Compiler::Native => "native",
Compiler::Interpreter => "interpreter",
Compiler::Xla => "xla",
Compiler::Mtk => "mtk",
Compiler::Sympy => "sympy",
}
}
pub fn parse(name: &str) -> Option<Compiler> {
match name {
"native" => Some(Compiler::Native),
"interpreter" => Some(Compiler::Interpreter),
"xla" => Some(Compiler::Xla),
"mtk" => Some(Compiler::Mtk),
"sympy" => Some(Compiler::Sympy),
_ => None,
}
}
pub fn parse_named(name: &str) -> Result<Compiler, String> {
Compiler::parse(name).ok_or_else(|| {
format!(
"{}: '{name}' is not a compiler. The vocabulary is closed: {}",
crate::diagnostic::codes::COMPILER_UNKNOWN,
Compiler::vocabulary()
.iter()
.map(|c| c.as_str())
.collect::<Vec<_>>()
.join(", ")
)
})
}
pub fn is_strict(self) -> bool {
matches!(self, Compiler::Native | Compiler::Xla)
}
pub fn vocabulary() -> &'static [Compiler] {
&[
Compiler::Native,
Compiler::Interpreter,
Compiler::Xla,
Compiler::Mtk,
Compiler::Sympy,
]
}
}
impl std::fmt::Display for Compiler {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Debug)]
pub struct CompilerRuleReport {
pub rule: String,
pub kind: &'static str,
pub cadence: &'static str,
pub tier: &'static str,
pub reason: Option<String>,
}
#[derive(Clone, Debug)]
pub struct CompilerReport {
compiler: Compiler,
rules: Vec<CompilerRuleReport>,
fused_groups: usize,
fused_instructions: usize,
}
impl CompilerReport {
fn empty(compiler: Compiler) -> CompilerReport {
CompilerReport {
compiler,
rules: Vec::new(),
fused_groups: 0,
fused_instructions: 0,
}
}
pub fn compiler(&self) -> Compiler {
self.compiler
}
pub fn rules(&self) -> &[CompilerRuleReport] {
&self.rules
}
pub fn n_taped(&self) -> usize {
self.rules.iter().filter(|r| r.tier == "taped").count()
}
pub fn n_xla(&self) -> usize {
self.rules.iter().filter(|r| r.tier == "xla").count()
}
pub fn n_oracle(&self) -> usize {
self.rules
.iter()
.filter(|r| matches!(r.tier, "oracle" | "fallback"))
.count()
}
pub fn n_vectorized(&self) -> usize {
self.rules.iter().filter(|r| r.tier == "vectorized").count()
}
pub fn fused_groups(&self) -> usize {
self.fused_groups
}
pub fn fused_instructions(&self) -> usize {
self.fused_instructions
}
}
impl std::fmt::Display for CompilerReport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"compiler {}: {} rules, {} taped, {} on the per-cell oracle",
self.compiler,
self.rules.len(),
self.n_taped(),
self.n_oracle()
)?;
if self.n_xla() > 0 {
write!(f, ", {} lowered into the XLA program", self.n_xla())?;
}
if self.n_vectorized() > 0 {
write!(f, ", {} on the whole-array overlay", self.n_vectorized())?;
}
let n_relational = self.rules.iter().filter(|r| r.tier == "relational").count();
if n_relational > 0 {
write!(f, ", {n_relational} by the relational engine at setup")?;
}
if self.fused_groups > 0 {
write!(
f,
"; {} fused groups absorbing {} instructions",
self.fused_groups, self.fused_instructions
)?;
}
Ok(())
}
}
#[derive(Default)]
pub struct ProblemOptions {
pub p: HashMap<String, f64>,
pub u0: HashMap<String, f64>,
pub model_name: Option<String>,
pub metaparameters: BTreeMap<String, i64>,
pub base_path: Option<PathBuf>,
pub callbacks: CallbackSet,
pub rhs: Rhs,
pub compiler: Option<Compiler>,
#[cfg(not(target_arch = "wasm32"))]
pub pushdown_rewrite: bool,
#[cfg(not(target_arch = "wasm32"))]
pub providers: HashMap<String, Box<dyn crate::provider::CadenceProvider>>,
#[cfg(not(target_arch = "wasm32"))]
pub build_providers: Vec<(String, Box<dyn crate::prepare::PrepareProvider>)>,
#[cfg(not(target_arch = "wasm32"))]
pub const_arrays: HashMap<String, ArrayD<f64>>,
#[cfg(not(target_arch = "wasm32"))]
pub build_pipeline: bool,
#[cfg(not(target_arch = "wasm32"))]
pub gated_fetch_batch: Option<usize>,
#[cfg(not(target_arch = "wasm32"))]
pub verbose: bool,
#[cfg(not(target_arch = "wasm32"))]
pub progress: Option<crate::prepare::PrepareProgressFn>,
pub inspect: bool,
pub sample_time: Option<f64>,
pub runtime_array_parameters: bool,
}
impl std::fmt::Debug for ProblemOptions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut d = f.debug_struct("ProblemOptions");
d.field("p", &self.p)
.field("u0", &self.u0)
.field("model_name", &self.model_name)
.field("metaparameters", &self.metaparameters)
.field("base_path", &self.base_path)
.field("callbacks", &self.callbacks)
.field("rhs", &self.rhs)
.field("compiler", &self.compiler)
.field("inspect", &self.inspect)
.field("sample_time", &self.sample_time)
.field("runtime_array_parameters", &self.runtime_array_parameters);
#[cfg(not(target_arch = "wasm32"))]
d.field("pushdown_rewrite", &self.pushdown_rewrite)
.field("providers", &self.providers.keys().collect::<Vec<_>>())
.field(
"build_providers",
&self
.build_providers
.iter()
.map(|(k, _)| k)
.collect::<Vec<_>>(),
)
.field(
"const_arrays",
&self.const_arrays.keys().collect::<Vec<_>>(),
)
.field("build_pipeline", &self.build_pipeline)
.field("gated_fetch_batch", &self.gated_fetch_batch)
.field("verbose", &self.verbose)
.field(
"progress",
&self
.progress
.as_ref()
.map(|_| "<observer>")
.unwrap_or("None"),
);
d.finish()
}
}
pub(crate) enum Backend {
Array(Rc<ArrayCompiled>),
Static(String),
}
#[derive(Default)]
pub(crate) struct BuildProducts {
pub fields: HashMap<String, ArrayD<f64>>,
pub members: HashMap<String, Vec<i64>>,
pub extents: HashMap<String, i64>,
pub gated_provider_keys: Vec<String>,
pub baked_parameters: Vec<String>,
pub static_eval_error: Option<String>,
}
pub(crate) struct RawDocument {
text: Option<String>,
value: std::cell::OnceCell<JsonValue>,
}
impl RawDocument {
fn parsed(value: JsonValue) -> Self {
RawDocument {
text: None,
value: std::cell::OnceCell::from(value),
}
}
fn from_text(text: String) -> Self {
RawDocument {
text: Some(text),
value: std::cell::OnceCell::new(),
}
}
fn get(&self) -> &JsonValue {
self.value.get_or_init(|| {
self.text
.as_deref()
.and_then(|t| serde_json::from_str(t).ok())
.unwrap_or(JsonValue::Null)
})
}
}
pub struct EsmProblem {
pub(crate) doc: Rc<RawDocument>,
pub(crate) model_name: Option<String>,
pub(crate) precision: precision::Env,
pub(crate) tspan: (f64, f64),
pub(crate) p: HashMap<String, f64>,
pub(crate) u0: HashMap<String, f64>,
pub(crate) backend: Rc<Backend>,
pub(crate) build: Rc<BuildProducts>,
pub(crate) inspection: std::cell::RefCell<BuildInspection>,
pub(crate) inspect: bool,
pub(crate) compiler: Compiler,
pub(crate) compiler_report: CompilerReport,
pub(crate) callbacks: CallbackSet,
pub(crate) solver: Option<crate::types::Solver>,
#[cfg(not(target_arch = "wasm32"))]
pub(crate) refresh: Option<std::cell::RefCell<crate::provider::RefreshExecutor>>,
#[cfg(not(target_arch = "wasm32"))]
pub(crate) discrete_forcing: std::collections::HashSet<String>,
#[cfg(not(target_arch = "wasm32"))]
pub(crate) refresh_boundaries: Vec<f64>,
pub(crate) program_source: Option<Rc<ProgramSource>>,
pub(crate) runtime_array_parameters: bool,
}
pub(crate) enum ProgramSource {
File(Box<EsmFile>),
Flattened(Box<FlattenedSystem>),
}
impl std::fmt::Debug for EsmProblem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EsmProblem")
.field("model_name", &self.model_name)
.field("tspan", &self.tspan)
.field("p", &self.p.len())
.field("u0", &self.u0.len())
.field("backend", &self.backend_kind())
.field("callbacks", &self.callbacks.names())
.finish()
}
}
impl EsmProblem {
pub fn solver(&self) -> Option<&crate::types::Solver> {
self.solver.as_ref()
}
pub fn tspan(&self) -> (f64, f64) {
self.tspan
}
pub fn p(&self) -> &HashMap<String, f64> {
&self.p
}
pub fn u0(&self) -> &HashMap<String, f64> {
&self.u0
}
pub fn model_name(&self) -> Option<&str> {
self.model_name.as_deref()
}
pub fn document(&self) -> &JsonValue {
self.doc.get()
}
pub fn compiler(&self) -> Compiler {
self.compiler
}
pub fn compiler_report(&self) -> &CompilerReport {
&self.compiler_report
}
pub fn backend_kind(&self) -> &'static str {
match &*self.backend {
Backend::Array(_) => "array",
Backend::Static(_) => "static",
}
}
pub fn precision(&self) -> Precision {
self.precision.document
}
#[doc(hidden)]
pub fn debug_array_compiled(&self) -> Option<Rc<ArrayCompiled>> {
match &*self.backend {
Backend::Array(c) => Some(Rc::clone(c)),
Backend::Static(_) => None,
}
}
pub fn is_dynamic(&self) -> bool {
!matches!(&*self.backend, Backend::Static(_))
}
pub fn state_variable_names(&self) -> Vec<String> {
match &*self.backend {
Backend::Array(c) => c.qualified_state_names().to_vec(),
Backend::Static(_) => Vec::new(),
}
}
pub fn parameter_names(&self) -> Vec<String> {
match &*self.backend {
Backend::Array(c) => qualify_array_names(c, c.parameter_names()),
Backend::Static(_) => Vec::new(),
}
}
pub fn members(&self) -> &HashMap<String, Vec<i64>> {
&self.build.members
}
pub fn extents(&self) -> &HashMap<String, i64> {
&self.build.extents
}
pub fn gated_provider_keys(&self) -> &[String] {
&self.build.gated_provider_keys
}
pub fn take_inspection(&self) -> BuildInspection {
std::mem::take(&mut self.inspection.borrow_mut())
}
pub(crate) fn reset_inspection(&self) {
*self.inspection.borrow_mut() = BuildInspection::default();
}
pub fn observed_fields(&self) -> &HashMap<String, ArrayD<f64>> {
&self.build.fields
}
pub fn observed_variable_names(&self) -> Vec<String> {
match &*self.backend {
Backend::Array(c) => qualify_array_names(c, &c.observed_variable_names()),
Backend::Static(_) => Vec::new(),
}
}
pub fn observed_field_names(&self) -> Vec<String> {
let model = self.model_name.as_deref().unwrap_or("");
let mut names: Vec<String> = self
.build
.fields
.keys()
.chain(self.inspection.borrow().setup_arrays.keys())
.map(|k| qualify(model, k))
.collect();
names.sort();
names.dedup();
names
}
}
pub fn callbacks(prob: &EsmProblem) -> &CallbackSet {
&prob.callbacks
}
pub(crate) fn qualify(model: &str, key: &str) -> String {
let already = key
.strip_prefix(model)
.is_some_and(|rest| rest.starts_with('.'));
if model.is_empty() || already {
key.to_string()
} else {
format!("{model}.{key}")
}
}
fn qualify_array_names(
compiled: &crate::simulate_array::ArrayCompiled,
names: &[String],
) -> Vec<String> {
let Some(ns) = compiled.namespace() else {
return names.to_vec();
};
names.iter().map(|n| qualify(ns, n)).collect()
}
fn component_of(qualified: &str) -> &str {
qualified.rsplit_once('.').map_or("", |(head, _)| head)
}
fn field_components(prob: &EsmProblem) -> std::collections::BTreeSet<String> {
let model = prob.model_name.as_deref().unwrap_or("");
let mut out = std::collections::BTreeSet::new();
for k in prob.build.fields.keys() {
out.insert(component_of(&qualify(model, k)).to_string());
}
for k in prob.inspection.borrow().setup_arrays.keys() {
out.insert(component_of(&qualify(model, k)).to_string());
}
out
}
enum NameResolution {
Key(String),
Ambiguous(Vec<String>),
Miss,
}
fn resolve_observed_key<'a>(
keys: impl Iterator<Item = &'a String> + Clone,
model: &str,
single_component: bool,
name: &str,
) -> NameResolution {
if keys.clone().any(|k| k == name) {
return NameResolution::Key(name.to_string()); }
if let Some(rest) = name.strip_prefix(model).and_then(|r| r.strip_prefix('.'))
&& keys.clone().any(|k| k == rest)
{
return NameResolution::Key(rest.to_string()); }
if name.contains('.') {
return NameResolution::Miss; }
let mut matches: Vec<&String> = keys
.filter(|k| k.rsplit('.').next() == Some(name))
.collect();
matches.sort();
if single_component && matches.len() == 1 {
return NameResolution::Key(matches[0].clone());
}
if matches.is_empty() {
NameResolution::Miss
} else {
NameResolution::Ambiguous(matches.into_iter().cloned().collect())
}
}
pub fn observed_field(prob: &EsmProblem, name: &str) -> Result<ArrayD<f64>, SimulateError> {
let _precision_guard = prob.precision.enter();
let name = problem_merged_renames(prob)
.get(name)
.map(String::as_str)
.unwrap_or(name);
let model = prob.model_name.as_deref().unwrap_or("");
let components = field_components(prob);
let single = components.len() == 1;
if let NameResolution::Key(k) =
resolve_observed_key(prob.build.fields.keys(), model, single, name)
{
return Ok(prob.build.fields[&k].clone());
}
let inspection = prob.inspection.borrow();
if let NameResolution::Key(k) =
resolve_observed_key(inspection.setup_arrays.keys(), model, single, name)
{
return Ok(inspection.setup_arrays[&k].clone());
}
if !single
&& let NameResolution::Ambiguous(matches) = resolve_observed_key(
prob.build
.fields
.keys()
.chain(inspection.setup_arrays.keys()),
model,
single,
name,
)
{
let mut cands: Vec<String> = matches.iter().map(|k| qualify(model, k)).collect();
cands.sort();
cands.dedup();
return Err(SimulateError::Compile(
crate::compile_error::CompileError::build_err(format!(
"observed_field: '{name}' is a bare name and this EsmProblem has {} \
components ({}); qualify it as one of: {}",
components.len(),
components
.iter()
.filter(|c| !c.is_empty())
.cloned()
.collect::<Vec<_>>()
.join(", "),
cands.join(", ")
)),
));
}
let cause = prob
.build
.static_eval_error
.as_deref()
.map(|e| format!("; evaluating the state-free observed graph failed: {e}"))
.unwrap_or_default();
Err(SimulateError::Compile(
crate::compile_error::CompileError::build_err(format!(
"observed_field: '{name}' is not a build-time-evaluable observed of this \
EsmProblem{cause}"
)),
))
}
#[cfg(feature = "solve")]
pub fn observed_trajectory(
prob: &EsmProblem,
sol: &Solution,
name: &str,
) -> Result<Vec<f64>, SimulateError> {
let _precision_guard = prob.precision.enter();
let one = [name.to_string()];
match observed_trajectories(prob, sol, &one)?.pop() {
Some((_, values)) => Ok(values),
None => {
let declared: Vec<String> = match &*prob.backend {
Backend::Array(c) => c.observed_variable_names(),
Backend::Static(_) => Vec::new(),
};
let model = prob.model_name.as_deref().unwrap_or("");
Err(
resolve_observed_name(&declared, model, components_of(&declared, model) == 1, name)
.expect_err("resolution failed, or the bulk form would have answered"),
)
}
}
}
#[cfg(feature = "solve")]
pub fn observed_trajectories(
prob: &EsmProblem,
sol: &Solution,
names: &[String],
) -> Result<Vec<(String, Vec<f64>)>, SimulateError> {
let _precision_guard = prob.precision.enter();
match &*prob.backend {
Backend::Array(c) => array_observed_trajectories(prob, c, sol, names),
Backend::Static(reason) => Err(SimulateError::NotDynamic {
details: format!("{reason}; a static document's results are read with observed_field"),
}),
}
}
#[cfg(feature = "solve")]
fn array_observed_trajectories(
prob: &EsmProblem,
compiled: &crate::simulate_array::ArrayCompiled,
sol: &Solution,
names: &[String],
) -> Result<Vec<(String, Vec<f64>)>, SimulateError> {
let declared = compiled.observed_variable_names();
let model = compiled
.namespace()
.or(prob.model_name.as_deref())
.unwrap_or("");
let single = components_of(&declared, model) == 1;
let mut out = Vec::new();
for name in names {
let Ok(resolved) = resolve_observed_name(&declared, model, single, name) else {
continue;
};
let row_name = match compiled.namespace() {
Some(ns) => qualify(ns, &resolved),
None => resolved.clone(),
};
match sol.state_variable_names.iter().position(|r| *r == row_name) {
Some(i) => out.push((name.clone(), sol.state[i].clone())),
None => {
return Err(SimulateError::Compile(
crate::compile_error::CompileError::build_err(format!(
"observed_trajectory: '{name}' is an observed of this EsmProblem but \
carries no scalar trajectory — it is array-valued, and the array \
runtime appends one row per cell only for an observed the solve was \
asked for. Name it in `SolveOptions::output_observed` and read the \
`{resolved}[i,j]` rows of the solution."
)),
));
}
}
}
Ok(out)
}
#[cfg(feature = "solve")]
fn resolve_observed_name(
declared: &[String],
model: &str,
single_component: bool,
name: &str,
) -> Result<String, SimulateError> {
match resolve_observed_key(declared.iter(), model, single_component, name) {
NameResolution::Key(k) => Ok(k),
NameResolution::Ambiguous(matches) => Err(SimulateError::Compile(
crate::compile_error::CompileError::build_err(format!(
"observed_trajectory: '{name}' is a bare name and this EsmProblem has {} components; qualify it as one of: {}",
components_of(declared, model),
matches
.iter()
.map(|k| qualify(model, k))
.collect::<Vec<_>>()
.join(", ")
)),
)),
NameResolution::Miss => Err(SimulateError::Compile(
crate::compile_error::CompileError::build_err(format!(
"observed_trajectory: '{name}' is not an observed variable of this EsmProblem"
)),
)),
}
}
#[cfg(feature = "solve")]
fn components_of(declared: &[String], model: &str) -> usize {
declared
.iter()
.map(|k| component_of(&qualify(model, k)).to_string())
.collect::<std::collections::BTreeSet<_>>()
.len()
}
#[derive(Debug, Clone, Default)]
pub struct Remake {
pub p: HashMap<String, f64>,
pub u0: HashMap<String, f64>,
pub tspan: Option<(f64, f64)>,
pub callbacks: Option<CallbackSet>,
}
pub fn remake(prob: &EsmProblem, changes: &Remake) -> Result<EsmProblem, SimulateError> {
let _precision_guard = prob.precision.enter();
let known: std::collections::HashSet<String> = prob.parameter_names().into_iter().collect();
let known_bare: HashMap<String, usize> = {
let mut counts: HashMap<String, usize> = HashMap::new();
for n in &known {
*counts
.entry(n.rsplit('.').next().unwrap_or(n).to_string())
.or_insert(0) += 1;
}
counts
};
for name in changes.p.keys() {
if prob.build.baked_parameters.iter().any(|b| b == name) {
return Err(SimulateError::UnsubstitutableBinding {
name: name.clone(),
class: "baked into the build as a load-time constant — build a new EsmProblem"
.to_string(),
});
}
if prob.is_dynamic()
&& !known.contains(name)
&& known_bare.get(name).copied().unwrap_or(0) == 0
{
return Err(SimulateError::UnsubstitutableBinding {
name: name.clone(),
class: "not a parameter of the compiled system".to_string(),
});
}
}
let mut p = prob.p.clone();
p.extend(changes.p.iter().map(|(k, v)| (k.clone(), *v)));
let mut u0 = prob.u0.clone();
u0.extend(changes.u0.iter().map(|(k, v)| (k.clone(), *v)));
let tspan = changes.tspan.unwrap_or(prob.tspan);
#[cfg(not(target_arch = "wasm32"))]
let boundaries: Vec<f64> = prob
.refresh_boundaries
.iter()
.copied()
.filter(|&b| b > tspan.0 && b < tspan.1)
.collect();
Ok(EsmProblem {
doc: Rc::clone(&prob.doc),
solver: prob.solver.clone(),
model_name: prob.model_name.clone(),
precision: prob.precision.clone(),
tspan,
p,
u0,
backend: Rc::clone(&prob.backend),
build: Rc::clone(&prob.build),
inspection: std::cell::RefCell::new(prob.inspection.borrow().clone()),
inspect: prob.inspect,
compiler: prob.compiler,
compiler_report: prob.compiler_report.clone(),
callbacks: changes
.callbacks
.clone()
.unwrap_or_else(|| prob.callbacks.clone()),
#[cfg(not(target_arch = "wasm32"))]
refresh: None,
#[cfg(not(target_arch = "wasm32"))]
discrete_forcing: prob.discrete_forcing.clone(),
#[cfg(not(target_arch = "wasm32"))]
refresh_boundaries: boundaries,
program_source: prob.program_source.clone(),
runtime_array_parameters: prob.runtime_array_parameters,
})
}
pub struct EnsembleProblem<'a> {
prob: &'a EsmProblem,
trajectories: usize,
#[allow(clippy::type_complexity)]
rewrite: Box<dyn Fn(&EsmProblem, usize) -> Result<Remake, SimulateError> + 'a>,
}
impl std::fmt::Debug for EnsembleProblem<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EnsembleProblem")
.field("trajectories", &self.trajectories)
.field("prob", self.prob)
.finish()
}
}
impl<'a> EnsembleProblem<'a> {
pub fn new(
prob: &'a EsmProblem,
trajectories: usize,
rewrite: impl Fn(&EsmProblem, usize) -> Result<Remake, SimulateError> + 'a,
) -> Self {
Self {
prob,
trajectories,
rewrite: Box::new(rewrite),
}
}
pub fn problem(&self) -> &EsmProblem {
self.prob
}
pub fn trajectories(&self) -> usize {
self.trajectories
}
pub fn trajectory(&self, i: usize) -> Result<EsmProblem, SimulateError> {
let changes = (self.rewrite)(self.prob, i)?;
remake(self.prob, &changes)
}
}
#[cfg(feature = "solve")]
pub fn solve_ensemble(
ens: &EnsembleProblem<'_>,
opts: &SolveOptions,
) -> Result<Vec<Solution>, SimulateError> {
let mut out = Vec::with_capacity(ens.trajectories());
for i in 0..ens.trajectories() {
let prob = ens.trajectory(i)?;
out.push(solve(&prob, opts)?);
}
Ok(out)
}
pub fn esm_problem<'a>(
input: impl Into<ProblemInput<'a>>,
tspan: (f64, f64),
opts: ProblemOptions,
) -> Result<EsmProblem, SimulateError> {
let input = input.into();
#[allow(unused_mut)]
let mut opts = opts;
let mut owned_json: Option<JsonValue> = None;
let mut owned_file: Option<EsmFile> = None;
let mut flat_only: Option<&FlattenedSystem> = None;
let mut json_from_text = false;
#[cfg_attr(target_arch = "wasm32", allow(unused_mut))]
let mut rewritten_by_pipeline = false;
let mut source_text: Option<String> = None;
match input {
ProblemInput::Path(path) => {
let text = std::fs::read_to_string(path).map_err(|e| {
SimulateError::Compile(crate::compile_error::CompileError::build_err(format!(
"reading {}: {e}",
path.display()
)))
})?;
let raw: JsonValue = serde_json::from_str(&text).map_err(|e| {
SimulateError::Compile(crate::compile_error::CompileError::build_err(format!(
"parsing {}: {e}",
path.display()
)))
})?;
owned_json = Some(raw);
source_text = Some(text);
json_from_text = true;
if opts.base_path.is_none() {
opts.base_path = Some(match path.parent() {
Some(dir) if !dir.as_os_str().is_empty() => dir.to_path_buf(),
_ => PathBuf::from("."),
});
}
}
ProblemInput::Json(v) => owned_json = Some(v.clone()),
ProblemInput::File(f) => owned_file = Some(f.clone()),
ProblemInput::Flattened(f) => flat_only = Some(f),
}
let prec = element_type_of(owned_json.as_ref(), owned_file.as_ref(), flat_only)?;
let var_precisions = variable_element_types_of(
owned_json.as_ref(),
owned_file.as_ref(),
flat_only,
prec,
opts.model_name.as_deref(),
)?;
let _precision_guard = precision::enter_env(prec, std::sync::Arc::new(var_precisions));
if prec.is_f32() || precision::has_variable_overrides() {
for (k, v) in opts.p.iter_mut() {
*v = precision::of_variable(k).round(*v);
}
for (k, v) in opts.u0.iter_mut() {
*v = precision::of_variable(k).round(*v);
}
#[cfg(not(target_arch = "wasm32"))]
for (k, a) in opts.const_arrays.iter_mut() {
let kp = precision::of_variable(k);
if kp.is_f32() {
a.mapv_inplace(|x| kp.round(x));
}
}
}
let mut inferred = false;
if precision::has_variable_overrides()
&& let Some(f) = owned_file.as_mut()
&& let Some(models) = f.models.as_mut()
{
crate::precision_infer::annotate_models(models, prec).map_err(SimulateError::Compile)?;
inferred = true;
}
if let Some(f) = owned_file.as_mut() {
crate::lower_table_lookup::lower_table_lookups(f).map_err(SimulateError::Compile)?;
}
let lowered_flat;
if let Some(flat) = flat_only
&& let Some(lowered) = crate::lower_table_lookup::lowered_flattened_copy(flat)
.map_err(SimulateError::Compile)?
{
lowered_flat = lowered;
flat_only = Some(&lowered_flat);
}
let mut build = BuildProducts::default();
#[cfg_attr(target_arch = "wasm32", allow(unused_mut))]
let mut model_name = opts.model_name.clone();
let compiler = opts.compiler.unwrap_or_default();
if let Some(details) = unavailable_reason(compiler) {
return Err(SimulateError::CompilerUnavailable {
compiler: compiler.as_str(),
details,
});
}
#[cfg_attr(target_arch = "wasm32", allow(unused_mut))]
let mut route_rows: Vec<CompilerRuleReport> = Vec::new();
let supplied = SuppliedData::capture(&mut opts, owned_json.as_ref(), owned_file.as_ref());
#[cfg(not(target_arch = "wasm32"))]
if owned_json.is_none()
&& let Some(file) = owned_file.as_ref().filter(|_| wants_build_pipeline(&opts))
{
owned_json = Some(serde_json::to_value(file).map_err(|e| {
SimulateError::Compile(crate::compile_error::CompileError::build_err(format!(
"re-serializing the typed document for the build pipeline: {e}"
)))
})?);
owned_file = None;
}
#[cfg(not(target_arch = "wasm32"))]
if let Some(raw) = owned_json.as_mut().filter(|_| wants_build_pipeline(&opts)) {
{
let prepared = crate::prepare::run_build_pipeline(raw, &mut opts)?;
if let Some(r) = &prepared.refused {
let reason = match &r.route {
crate::prepare::Route::PerCell(why) => {
format!("{why}; {}", crate::simulate_array::ONE_CELL_NOTE)
}
_ => String::new(),
};
return Err(SimulateError::Compile(
crate::compile_error::CompileError::CompilerRefusedRule {
compiler: compiler.as_str(),
kind: r.kind,
rule: qualify(&prepared.model_name, &r.name),
tier: "const",
reason,
},
));
}
route_rows.extend(prepared.rules.iter().map(|r| {
let rule = qualify(&prepared.model_name, &r.name);
match &r.route {
crate::prepare::Route::Relational => CompilerRuleReport {
rule,
kind: r.kind,
cadence: "const",
tier: "relational",
reason: None,
},
crate::prepare::Route::Overlay => {
outside_rule_row(compiler, rule, r.kind, None, false)
}
crate::prepare::Route::Tape => CompilerRuleReport {
rule,
kind: r.kind,
cadence: "const",
tier: "taped",
reason: None,
},
crate::prepare::Route::PerCell(why) => {
outside_rule_row(compiler, rule, r.kind, Some(why.clone()), false)
}
}
}));
*raw = prepared.doc;
json_from_text = false;
source_text = None;
rewritten_by_pipeline = true;
model_name = Some(prepared.model_name);
build.fields = prepared.fields;
build.members = prepared.members;
build.extents = prepared.extents;
build.gated_provider_keys = prepared.gated_provider_keys;
build.baked_parameters = opts.p.keys().cloned().collect();
build.baked_parameters.sort();
}
}
let mut doc_solver_of_text: Option<Option<crate::Solver>> = None;
if owned_file.is_none() && owned_json.is_some() {
{
let load_opts = crate::parse::LoadOptions {
base_path: opts.base_path.clone(),
metaparameters: if rewritten_by_pipeline {
BTreeMap::new()
} else {
opts.metaparameters.clone()
},
};
let loaded = if json_from_text && source_text.is_some() {
let raw = owned_json.take().expect("checked above");
doc_solver_of_text = Some(document_solver(&raw));
crate::parse::load_document_owned(raw, &load_opts)
} else if let Some(raw) = owned_json.as_ref()
&& (json_from_text || nesting_within(raw, NESTING_WITHOUT_ROUND_TRIP))
{
crate::parse::load_document_with_options(raw, &load_opts)
} else {
let raw = owned_json.as_ref().expect("checked above");
let text = serde_json::to_string(raw).map_err(|e| {
SimulateError::Compile(crate::compile_error::CompileError::build_err(format!(
"re-serializing the prepared document: {e}"
)))
})?;
crate::parse::load_string_with_options(&text, &load_opts)
};
match loaded {
Ok(f) => owned_file = Some(f),
Err(e) => {
if !rewritten_by_pipeline {
return Err(SimulateError::Compile(
crate::compile_error::CompileError::build_err(format!(
"loading the document: {e}"
)),
));
}
if opts.rhs == Rhs::Always {
return Err(SimulateError::Compile(
crate::compile_error::CompileError::build_err(format!(
"typed parse of the prepared document: {e}"
)),
));
}
}
}
}
}
if !inferred
&& precision::has_variable_overrides()
&& let Some(f) = owned_file.as_mut()
&& let Some(models) = f.models.as_mut()
{
crate::precision_infer::annotate_models(models, prec).map_err(SimulateError::Compile)?;
}
if let Some(f) = owned_file.as_mut() {
crate::lower_table_lookup::lower_table_lookups(f).map_err(SimulateError::Compile)?;
}
#[cfg(not(target_arch = "wasm32"))]
if let Some(f) = owned_file.as_mut() {
supplied.bind_arrays(f, &opts.p, model_name.as_deref());
}
if let Some(f) = owned_file.as_mut() {
for name in bind_value_invention_params(f, &opts.p) {
if !build.baked_parameters.contains(&name) {
build.baked_parameters.push(name);
}
}
for name in bind_join_key_params(f, &opts.p) {
if !build.baked_parameters.contains(&name) {
build.baked_parameters.push(name);
}
}
build.baked_parameters.sort();
}
#[cfg(not(target_arch = "wasm32"))]
if !opts.p.is_empty()
&& let Some(file) = owned_file.as_mut()
{
crate::data_fed::pin_data_fed_parameters(file, opts.p.keys());
}
let backend = compile_backend(
owned_file.as_ref(),
flat_only,
model_name.as_deref(),
opts.rhs,
compiler,
opts.runtime_array_parameters,
)?;
if let Backend::Static(_) = &backend
&& let Some(f) = owned_file.as_ref()
{
supplied.refuse_missing_static(f, &opts.p, model_name.as_deref())?;
}
if build.fields.is_empty()
&& let Backend::Static(_) = &backend
{
let t0 = opts.sample_time.unwrap_or(tspan.0);
let (fields, rows, eval_error) = static_observed_fields(
owned_file.as_ref(),
flat_only,
&opts.p,
t0,
model_name.as_deref(),
compiler,
)?;
build.fields = fields;
build.static_eval_error = eval_error;
route_rows.extend(rows);
}
#[cfg(not(target_arch = "wasm32"))]
let (refresh, discrete_forcing, refresh_boundaries) =
bind_providers(&backend, &mut opts, tspan)?;
if let Backend::Array(c) = &backend {
supplied.refuse_missing_array(c, owned_file.as_ref(), &opts.p)?;
}
#[cfg(not(target_arch = "wasm32"))]
if let Backend::Array(compiled) = &backend {
crate::data_fed::refuse_unbound(
compiled,
&opts.const_arrays,
&discrete_forcing,
opts.p.keys(),
)?;
}
let doc_solver = match (doc_solver_of_text, owned_json.as_ref(), owned_file.as_ref()) {
(Some(solver), _, _) => solver,
(None, Some(raw), _) => document_solver(raw),
(None, None, Some(file)) => crate::solver::normalize_empty(file.solver.clone()),
(None, None, None) => None,
};
let mut compiler_report = build_compiler_report(
&backend,
model_name.as_deref(),
compiler,
#[cfg(not(target_arch = "wasm32"))]
&discrete_forcing,
#[cfg(target_arch = "wasm32")]
&std::collections::HashSet::new(),
&opts.p,
)?;
compiler_report.rules.splice(0..0, route_rows);
#[cfg(feature = "solve")]
if let Backend::Array(c) = &backend {
refuse_missing_initial_values(c, &opts.u0)?;
}
install_xla_program(&backend, model_name.as_deref(), compiler)?;
let program_source = match (&backend, owned_file, flat_only) {
(Backend::Array(_), Some(file), _) => Some(Rc::new(ProgramSource::File(Box::new(file)))),
(Backend::Array(_), None, Some(flat)) => {
Some(Rc::new(ProgramSource::Flattened(Box::new(flat.clone()))))
}
_ => None,
};
let prob = EsmProblem {
doc: Rc::new(match (owned_json, source_text) {
(None, Some(text)) => RawDocument::from_text(text),
(raw, _) => RawDocument::parsed(raw.unwrap_or(JsonValue::Null)),
}),
solver: doc_solver,
model_name,
precision: precision::Env::capture(),
tspan,
p: std::mem::take(&mut opts.p),
u0: std::mem::take(&mut opts.u0),
backend: Rc::new(backend),
build: Rc::new(build),
inspection: std::cell::RefCell::new(BuildInspection::default()),
inspect: opts.inspect,
compiler,
compiler_report,
callbacks: std::mem::take(&mut opts.callbacks),
#[cfg(not(target_arch = "wasm32"))]
refresh,
#[cfg(not(target_arch = "wasm32"))]
discrete_forcing,
#[cfg(not(target_arch = "wasm32"))]
refresh_boundaries,
program_source,
runtime_array_parameters: opts.runtime_array_parameters,
};
Ok(prob)
}
fn element_type_of(
raw: Option<&JsonValue>,
file: Option<&EsmFile>,
flat: Option<&FlattenedSystem>,
) -> Result<Precision, SimulateError> {
let named = raw
.and_then(|v| v.get("domain"))
.and_then(|d| d.get("element_type"))
.and_then(JsonValue::as_str)
.map(str::to_string)
.or_else(|| {
file.and_then(|f| f.domain.as_ref())
.and_then(|d| d.element_type.clone())
})
.or_else(|| {
flat.and_then(|f| f.domain.as_ref())
.and_then(|d| d.element_type.clone())
});
Precision::from_element_type(named.as_deref()).map_err(SimulateError::Compile)
}
fn variable_element_types_of(
raw: Option<&JsonValue>,
file: Option<&EsmFile>,
flat: Option<&FlattenedSystem>,
document: Precision,
selected: Option<&str>,
) -> Result<precision::VarPrecisions, SimulateError> {
if let Some(models) = raw
.and_then(|v| v.get("models"))
.and_then(JsonValue::as_object)
{
let decls: Vec<precision::ModelDeclarations<'_>> = models
.iter()
.map(|(mname, model)| precision::ModelDeclarations {
model: mname,
variables: model
.get("variables")
.and_then(JsonValue::as_object)
.map(|vars| {
vars.iter()
.map(|(v, d)| {
(
v.as_str(),
d.get("element_type").and_then(JsonValue::as_str),
)
})
.collect()
})
.unwrap_or_default(),
subsystem_variables: precision::subsystem_declarations(model.get("subsystems")),
})
.collect();
return precision::VarPrecisions::from_declarations(&decls, document, selected)
.map_err(SimulateError::Compile);
}
if let Some(models) = file.and_then(|f| f.models.as_ref()) {
let decls = crate::precision_infer::model_declarations(models);
return precision::VarPrecisions::from_declarations(&decls, document, selected)
.map_err(SimulateError::Compile);
}
let _ = (flat, document);
Ok(precision::VarPrecisions::default())
}
fn reject_f32_integration(prob: &EsmProblem) -> Result<(), SimulateError> {
let integrates = match &*prob.backend {
Backend::Static(_) => false,
Backend::Array(c) => c.has_differential_equations(),
};
if prob.precision.is_f32() && integrates {
return Err(SimulateError::Compile(
crate::compile_error::CompileError::Float32Unsupported {
construct: "time integration of a dynamic model".to_string(),
reason: "the ODE/DAE solver is instantiated over binary64 (step-size control, error norms and the Newton solve), so the trajectory would be binary64 even though the right-hand side is binary32"
.to_string(),
},
));
}
Ok(())
}
const NESTING_WITHOUT_ROUND_TRIP: usize = 64;
fn nesting_within(v: &JsonValue, limit: usize) -> bool {
match v {
JsonValue::Array(items) => limit > 0 && items.iter().all(|x| nesting_within(x, limit - 1)),
JsonValue::Object(map) => limit > 0 && map.values().all(|x| nesting_within(x, limit - 1)),
_ => true,
}
}
#[cfg(not(target_arch = "wasm32"))]
fn wants_build_pipeline(opts: &ProblemOptions) -> bool {
opts.build_pipeline
|| opts.pushdown_rewrite
|| !opts.build_providers.is_empty()
|| !opts.const_arrays.is_empty()
}
#[cfg(all(not(target_arch = "wasm32"), feature = "solve"))]
pub(crate) fn static_observed_graph(prob: &EsmProblem) -> Option<Rc<ArrayCompiled>> {
if !prob.discrete_forcing.is_empty() {
return None;
}
match &*prob.backend {
Backend::Array(c)
if !c.has_differential_equations()
&& c.state_variable_names().is_empty()
&& c.observeds_are_scalar() =>
{
Some(Rc::clone(c))
}
_ => None,
}
}
#[cfg(all(not(target_arch = "wasm32"), feature = "solve"))]
pub(crate) fn evaluate_static_observeds_over(
prob: &EsmProblem,
graph: &ArrayCompiled,
times: &[f64],
) -> Result<Vec<(String, Vec<f64>)>, SimulateError> {
let _precision_guard = prob.precision.enter();
let rows = graph.evaluate_stateless_observeds(&prob.p, times)?;
let names: Vec<String> = rows.iter().map(|(name, _)| name.clone()).collect();
Ok(qualify_array_names(graph, &names)
.into_iter()
.zip(rows.into_iter().map(|(_, values)| values))
.collect())
}
#[cfg(all(not(target_arch = "wasm32"), feature = "solve"))]
pub(crate) fn stateless_fields(prob: &EsmProblem) -> Option<Vec<(String, ArrayD<f64>)>> {
if !prob.discrete_forcing.is_empty() {
return None;
}
let Backend::Array(c) = &*prob.backend else {
return None;
};
if c.has_differential_equations() || !c.state_variable_names().is_empty() {
return None;
}
let _precision_guard = prob.precision.enter();
c.evaluate_stateless_fields(&prob.p, prob.tspan.0)
.ok()
.filter(|f| !f.is_empty())
}
#[cfg(all(not(target_arch = "wasm32"), feature = "solve"))]
pub(crate) fn has_nothing_to_integrate(prob: &EsmProblem) -> bool {
match &*prob.backend {
Backend::Static(_) => true,
Backend::Array(c) => !c.has_differential_equations(),
}
}
#[allow(clippy::type_complexity)]
fn static_observed_fields(
file: Option<&EsmFile>,
flat_only: Option<&FlattenedSystem>,
p: &HashMap<String, f64>,
t0: f64,
model_name: Option<&str>,
compiler: Compiler,
) -> Result<
(
HashMap<String, ArrayD<f64>>,
Vec<CompilerRuleReport>,
Option<String>,
),
SimulateError,
> {
let nothing = || Ok((HashMap::new(), Vec::new(), None));
let owned_flat;
let flat = match (flat_only, file) {
(Some(f), _) => f,
(None, Some(file)) => {
owned_flat = crate::flatten::flatten(file).map_err(|e| {
SimulateError::Compile(crate::compile_error::CompileError::Flatten(e))
})?;
&owned_flat
}
(None, None) => return nothing(),
};
if let Some(op) = crate::flatten::first_unlowered_operator(flat) {
return Err(SimulateError::Compile(
crate::compile_error::CompileError::UnloweredOperatorError { op },
));
}
if flat
.state_variables
.keys()
.any(|k| !flat.observed_variables.contains_key(k))
{
return nothing();
}
#[cfg(not(target_arch = "wasm32"))]
if let Some(file) = file
&& crate::simulate::whole_document_is_one_model(file)
&& let Some((_, model)) = file.models.as_ref().and_then(|m| m.iter().next())
{
let index_sets: HashMap<String, crate::types::IndexSet> = file
.index_sets
.clone()
.unwrap_or_default()
.into_iter()
.collect();
crate::simulate_array::check_free_variables(model, &index_sets, &[])
.map_err(SimulateError::Compile)?;
}
let mut compiled = ArrayCompiled::from_flattened(flat).map_err(SimulateError::Compile)?;
compiled.runtime_mode = match compiler {
Compiler::Interpreter => crate::simulate_array::RuntimeMode::Interpreter,
_ => crate::simulate_array::RuntimeMode::Native,
};
let compiled = Rc::new(compiled);
let report = rule_report(
&compiled,
model_name,
compiler,
&std::collections::HashSet::new(),
false,
)?;
let values = match compiled.evaluate_stateless_fields(p, t0) {
Ok(values) => values,
Err(e)
if e.to_string()
.contains(crate::simulate_array::GEOMETRY_CLIP_CODE) =>
{
return Err(e);
}
Err(e) => return Ok((HashMap::new(), report.rules, Some(e.to_string()))),
};
let fields = values
.into_iter()
.filter_map(|(name, arr)| {
let declared = flat.observed_variables.get(&name);
if arr.len() == 1 {
let shape = DeclaredRank::of_declaration(declared).scalar_shape();
let v = arr.iter().copied().next().expect("one element");
let arr = ArrayD::from_shape_vec(ndarray::IxDyn(shape), vec![v])
.expect("`scalar_shape` is `[]` or `[1]`, and both hold exactly one element");
return Some((name, arr));
}
declared.map(|_| (name, arr))
})
.collect();
Ok((fields, report.rules, None))
}
fn build_compiler_report(
backend: &Backend,
model_name: Option<&str>,
compiler: Compiler,
discrete_forcing: &std::collections::HashSet<String>,
#[cfg_attr(not(feature = "solve"), allow(unused_variables))] p: &HashMap<String, f64>,
) -> Result<CompilerReport, SimulateError> {
let compiled = match backend {
Backend::Static(_) => return Ok(CompilerReport::empty(compiler)),
Backend::Array(c) => c,
};
#[cfg_attr(not(feature = "solve"), allow(unused_mut))]
let mut report = rule_report(compiled, model_name, compiler, discrete_forcing, true)?;
#[cfg(feature = "solve")]
{
let model = model_name.unwrap_or("");
let mut ic_rows = Vec::new();
for r in compiled.field_ic_records(p, compiler.is_strict()) {
let reason = r.per_cell.then(|| {
"evaluated by the reference evaluator at setup, and a `faq` in it is not one \
the whole-array overlay takes, so it was walked once per cell"
.to_string()
});
if compiler.is_strict()
&& let Some(reason) = reason
{
return Err(SimulateError::Compile(
crate::compile_error::CompileError::CompilerRefusedRule {
compiler: compiler.as_str(),
kind: r.kind,
rule: qualify(model, &r.name),
tier: "const",
reason: format!("{reason}; {}", crate::simulate_array::ONE_CELL_NOTE),
},
));
}
ic_rows.push(outside_rule_row(
compiler,
qualify(model, &r.name),
r.kind,
reason,
compiler == Compiler::Interpreter,
));
}
report.rules.splice(0..0, ic_rows);
}
Ok(report)
}
fn outside_rule_row(
compiler: Compiler,
rule: String,
kind: &'static str,
per_cell: Option<String>,
overlay_off: bool,
) -> CompilerRuleReport {
let tier = if overlay_off || per_cell.is_some() {
"oracle"
} else {
"vectorized"
};
let reason = match compiler {
Compiler::Interpreter => None,
_ => per_cell,
};
CompilerRuleReport {
rule,
kind,
cadence: "const",
tier,
reason,
}
}
fn rule_report(
compiled: &ArrayCompiled,
model_name: Option<&str>,
compiler: Compiler,
discrete_forcing: &std::collections::HashSet<String>,
xla_emitted: bool,
) -> Result<CompilerReport, SimulateError> {
let (records, tape_report) = compiled.tape_rule_records(discrete_forcing);
let model = model_name.unwrap_or("");
let mut rules = Vec::with_capacity(records.len());
for r in records {
let qualified = qualify(model, &r.name);
if compiler.is_strict()
&& let Some(reason) = &r.fallback_reason
{
return Err(SimulateError::Compile(
crate::compile_error::CompileError::CompilerRefusedRule {
compiler: compiler.as_str(),
kind: r.kind,
rule: qualified,
tier: r.cadence,
reason: reason.clone(),
},
));
}
let tier = match (compiler, &r.fallback_reason) {
(Compiler::Interpreter, _) => "oracle",
(Compiler::Xla, _) if xla_emitted => "xla",
(_, None) => "taped",
(_, Some(_)) => "fallback",
};
rules.push(CompilerRuleReport {
rule: qualified,
kind: r.kind,
cadence: r.cadence,
tier,
reason: match compiler {
Compiler::Interpreter => None,
_ => r.fallback_reason,
},
});
}
let (fused_groups, fused_instructions) = if compiler == Compiler::Interpreter {
(0, 0)
} else {
(tape_report.fuse.n_groups, tape_report.fuse.n_member_instrs)
};
Ok(CompilerReport {
compiler,
rules,
fused_groups,
fused_instructions,
})
}
fn unavailable_reason(compiler: Compiler) -> Option<String> {
match compiler {
Compiler::Native | Compiler::Interpreter => None,
Compiler::Xla => xla_unavailable_reason(),
Compiler::Mtk => Some(
"a ModelingToolkit system needs Julia and ModelingToolkit.jl, and this binding has \
no ModelingToolkit runtime to load; `mtk` is provided by the Julia binding"
.to_string(),
),
Compiler::Sympy => Some(
"a lambdified SymPy right-hand side needs Python and SymPy, and this binding has \
neither; `sympy` is provided by the Python binding"
.to_string(),
),
}
}
#[cfg(feature = "xla")]
fn xla_unavailable_reason() -> Option<String> {
crate::xla_runtime::client().err().map(|e| {
format!(
"this build has the `xla` feature, but no usable XLA runtime in this process: {e}. \
Point XLA_EXTENSION_DIR at an unpacked xla_extension release \
(scripts/fetch-xla-extension.sh) and rebuild against it"
)
})
}
#[cfg(not(feature = "xla"))]
fn xla_unavailable_reason() -> Option<String> {
Some(
"this binding's StableHLO emitter over the tape is behind the `xla` Cargo feature, \
which this build does not have: rebuild with `--features xla` and XLA_EXTENSION_DIR \
pointing at an unpacked xla_extension release (scripts/fetch-xla-extension.sh)"
.to_string(),
)
}
fn install_xla_program(
backend: &Backend,
model_name: Option<&str>,
compiler: Compiler,
) -> Result<(), SimulateError> {
let _ = (backend, model_name);
if compiler != Compiler::Xla {
return Ok(());
}
#[cfg(feature = "xla")]
{
use crate::xla_runtime::CompileRhsError;
let Backend::Array(compiled) = backend else {
return Ok(());
};
if !compiled.has_differential_equations() {
return Ok(());
}
match compiled.install_xla_rhs() {
Ok(()) => Ok(()),
Err(CompileRhsError::Refused(e)) => {
Err(SimulateError::Compile(
crate::compile_error::CompileError::CompilerRefusedRule {
compiler: Compiler::Xla.as_str(),
kind: "rule",
rule: qualify(model_name.unwrap_or(""), &e.rule),
tier: "continuous",
reason: e.reason,
},
))
}
Err(CompileRhsError::Runtime(details)) => Err(SimulateError::CompilerUnavailable {
compiler: Compiler::Xla.as_str(),
details,
}),
}
}
#[cfg(not(feature = "xla"))]
{
Ok(())
}
}
fn compile_backend(
file: Option<&EsmFile>,
flat: Option<&FlattenedSystem>,
model_name: Option<&str>,
mode: Rhs,
compiler: Compiler,
runtime_array_parameters: bool,
) -> Result<Backend, SimulateError> {
if let Some(details) = unavailable_reason(compiler) {
return Err(SimulateError::CompilerUnavailable {
compiler: compiler.as_str(),
details,
});
}
refuse_parameter_definitions(file, flat)?;
if mode == Rhs::Never {
return Ok(Backend::Static(
"the caller asked for Rhs::Never".to_string(),
));
}
let runtime_mode = match compiler {
Compiler::Interpreter => crate::simulate_array::RuntimeMode::Interpreter,
Compiler::Xla => crate::simulate_array::RuntimeMode::Xla,
_ => crate::simulate_array::RuntimeMode::Native,
};
if let Some(flat) = flat {
if mode == Rhs::Auto && flat.state_variables.is_empty() {
return Ok(Backend::Static(
"the flattened system declares no state variables".to_string(),
));
}
let mut compiled = crate::simulate_array::ArrayCompiled::from_flattened_with(
flat,
runtime_array_parameters,
)?;
compiled.runtime_mode = runtime_mode;
return Ok(Backend::Array(Rc::new(compiled)));
}
let Some(file) = file else {
if mode == Rhs::Always {
return Err(SimulateError::Compile(
crate::compile_error::CompileError::build_err("no typed document to compile"),
));
}
return Ok(Backend::Static(
"the prepared document has no typed form to compile".to_string(),
));
};
let n_components = file.models.as_ref().map_or(0, |m| m.len())
+ file.reaction_systems.as_ref().map_or(0, |r| r.len());
if n_components == 0 {
return Err(SimulateError::Compile(
crate::compile_error::CompileError::build_err(format!(
"nothing to flatten: '{}' declares no `models` and no `reaction_systems`. A \
file carrying only `expression_templates` is a template LIBRARY (esm-spec \
§9.7); import it from a document that declares components rather than \
building it directly",
file.metadata.name.as_deref().unwrap_or("")
)),
));
}
refuse_structurally_unreachable(file)?;
refuse_unregistered_callback_reads(file)?;
refuse_reference_integrity_errors(file)?;
if mode == Rhs::Auto && !has_differential_equations(file, model_name) {
refuse_static_unsupported_construct(file, model_name)?;
return Ok(Backend::Static(
"the document declares no differential equations".to_string(),
));
}
let mut compiled = crate::simulate::build_array_compiled(file, runtime_array_parameters)?;
compiled.runtime_mode = runtime_mode;
Ok(Backend::Array(Rc::new(compiled)))
}
fn refuse_parameter_definitions(
file: Option<&EsmFile>,
flat: Option<&FlattenedSystem>,
) -> Result<(), SimulateError> {
let refuse = |path: &str, message: &str| {
SimulateError::Compile(crate::compile_error::CompileError::build_err(format!(
"[{}] {path}: {message} (esm-spec §6.3.1)",
crate::diagnostic::codes::EQUATION_DEFINES_PARAMETER
)))
};
if let Some(file) = file
&& let Some(e) = crate::structural::parameter_definition_errors(file).first()
{
return Err(refuse(&e.path, &e.message));
}
if let Some(flat) = flat {
for (k, eq) in flat.equations.iter().enumerate() {
let mut lhs = &eq.lhs;
let name = loop {
match lhs {
crate::types::Expr::Variable(v) => break Some(v.as_str()),
crate::types::Expr::Operator(n) if n.op == "faq" || n.op == "aggregate" => {
match n.expr.as_deref() {
Some(x) => lhs = x,
None => break None,
}
}
crate::types::Expr::Operator(n)
if n.op == "index"
|| (n.op == "D" && matches!(n.wrt.as_deref(), None | Some("t"))) =>
{
match n.args.first() {
Some(x) => lhs = x,
None => break None,
}
}
_ => break None,
}
};
if let Some(name) = name
&& flat.parameters.contains_key(name)
{
return Err(refuse(
&format!("/equations/{k}/lhs"),
&format!(
"Equation {k} defines '{name}', which is a parameter; an equation \
defines unknowns only"
),
));
}
}
}
Ok(())
}
fn refuse_structurally_unreachable(file: &EsmFile) -> Result<(), SimulateError> {
let refuse = |code: &str, path: &str, message: &str, section: &str| {
SimulateError::Compile(crate::compile_error::CompileError::build_err(format!(
"[{code}] {path}: {message} (esm-spec {section})"
)))
};
if let Some(e) = crate::structural::reserved_declaration_errors(file).first() {
return Err(refuse(&e.code.to_string(), &e.path, &e.message, "§4.9.1.1"));
}
if let Some(systems) = &file.reaction_systems {
for (rs_name, rs) in systems {
for (i, eq) in rs.constraint_equations.iter().flatten().enumerate() {
if matches!(&eq.lhs, crate::types::Expr::Operator(n) if n.op == "ic") {
return Err(refuse(
crate::diagnostic::codes::IC_IN_REACTION_SYSTEM,
&format!("/reaction_systems/{rs_name}/constraint_equations/{i}"),
"ic equation not allowed in a reaction system; a reaction system \
hosts no ic equations (a species' initial value is its \
`species.default`, or a scoped-reference ic equation in a model)",
"§11.4.1",
));
}
}
}
}
Ok(())
}
const BUILD_REFUSED_VALIDATION_CODES: &[&str] = &[
"undefined_variable",
"undefined_parameter",
"undefined_species",
"undefined_system",
"undefined_index_set",
"unresolved_scoped_ref",
"event_var_undeclared",
"data_source_undefined",
"missing_required_field",
];
fn refuse_reference_integrity_errors(file: &EsmFile) -> Result<(), SimulateError> {
let result =
crate::validate::validate_scoped(file, crate::validate::ValidationScope::References);
if let Some(e) = result.structural_errors.iter().find(|e| {
BUILD_REFUSED_VALIDATION_CODES.contains(&e.code.to_string().as_str())
&& !e.path.contains("/tests/")
}) {
let named = declared_name_at(&e.path)
.map(|n| format!(" (variable '{n}')"))
.unwrap_or_default();
return Err(SimulateError::Compile(
crate::compile_error::CompileError::build_err(format!(
"[{}] {}: {}{named} (esm-libraries-spec §2.5.2)",
e.code, e.path, e.message
)),
));
}
Ok(())
}
fn declared_name_at(pointer: &str) -> Option<String> {
let segs: Vec<&str> = pointer.trim_start_matches('/').split('/').collect();
let mut parts = Vec::new();
let mut i = 0;
while i + 1 < segs.len() {
match segs[i] {
"models" | "reaction_systems" | "subsystems" => parts.push(segs[i + 1]),
"variables" | "species" | "parameters" => {
parts.push(segs[i + 1]);
return Some(parts.join("."));
}
_ => return None,
}
i += 2;
}
None
}
fn refuse_unregistered_callback_reads(file: &EsmFile) -> Result<(), SimulateError> {
let mut injected: BTreeMap<String, String> = BTreeMap::new();
for entry in file.coupling.iter().flatten() {
let crate::CouplingEntry::Callback {
callback_id,
config,
..
} = entry
else {
continue;
};
let names = config
.as_ref()
.and_then(|c| c.get("callback_variables"))
.and_then(|v| v.as_array())
.into_iter()
.flatten()
.filter_map(|cv| cv.get("name").and_then(|n| n.as_str()));
for name in names {
injected.insert(name.to_string(), callback_id.clone());
}
}
if injected.is_empty() {
return Ok(());
}
let Some(models) = file.models.as_ref() else {
return Ok(());
};
let mut names: Vec<&String> = models.keys().collect();
names.sort();
for name in names {
callback_reads_in(&models[name], name, &injected)?;
}
Ok(())
}
fn callback_reads_in(
model: &crate::types::Model,
path: &str,
injected: &BTreeMap<String, String>,
) -> Result<(), SimulateError> {
for eq in &model.equations {
for side in [&eq.lhs, &eq.rhs] {
let mut vars: Vec<String> = crate::free_variables(side).into_iter().collect();
vars.sort();
for v in vars {
if model.variables.contains_key(&v) {
continue;
}
if let Some(id) = injected.get(&v) {
return Err(SimulateError::Compile(
crate::compile_error::CompileError::build_err(format!(
"[{}] '{path}' reads '{v}', which the `callback` coupling '{id}' \
supplies, but no callback is registered to supply it at \
construction; refusing the build rather than reading a value \
the document does not give (esm-spec §9.6.6)",
crate::diagnostic::codes::CALLBACK_UNREGISTERED
)),
));
}
}
}
}
if let Some(subs) = model.subsystems.as_ref() {
let mut names: Vec<&String> = subs.keys().collect();
names.sort();
for name in names {
if let Ok((sub, _)) = crate::simulate_array::parse_subsystem_model(name, &subs[name]) {
callback_reads_in(&sub, &format!("{path}.{name}"), injected)?;
}
}
}
Ok(())
}
fn refuse_static_unsupported_construct(
file: &EsmFile,
model_name: Option<&str>,
) -> Result<(), SimulateError> {
if model_name.is_none()
&& let Some((construct, event)) = crate::compile_error::first_event_in_file(file)
{
return Err(crate::compile_error::event_refusal(
construct,
crate::compile_error::ARRAY_EVALUATOR,
event.as_deref(),
)
.into());
}
let Some(models) = file.models.as_ref() else {
return Ok(());
};
for (_, model) in models
.iter()
.filter(|(name, _)| model_name.is_none_or(|want| want == name.as_str()))
{
if let Some(refusal) = static_unsupported_construct(model) {
return Err(refusal.into());
}
}
Ok(())
}
fn static_unsupported_construct(
model: &crate::types::Model,
) -> Option<crate::compile_error::CompileError> {
use crate::compile_error::ARRAY_EVALUATOR;
if let Some((construct, event)) = crate::compile_error::first_event(model) {
return Some(crate::compile_error::event_refusal(
construct,
ARRAY_EVALUATOR,
event.as_deref(),
));
}
if let Some(eq) = crate::compile_error::first_implicit_equation(&model.equations) {
return Some(crate::compile_error::implicit_equation_refusal(
ARRAY_EVALUATOR,
eq,
));
}
let class = crate::classification::Classification::of(model);
if let Some(name) = class.brownian_parameters.first() {
return Some(crate::compile_error::CompileError::UnsupportedConstruct {
construct: crate::compile_error::WIENER_NOISE,
evaluator: ARRAY_EVALUATOR,
detail: format!("parameter '{name}'"),
});
}
let subs = model.subsystems.as_ref()?;
let mut names: Vec<&String> = subs.keys().collect();
names.sort();
names.into_iter().find_map(|name| {
crate::simulate_array::parse_subsystem_model(name, &subs[name])
.ok()
.and_then(|(sub, _)| static_unsupported_construct(&sub))
})
}
pub(crate) fn has_differential_equations(file: &EsmFile, model_name: Option<&str>) -> bool {
if let Some(systems) = file.reaction_systems.as_ref()
&& systems
.iter()
.filter(|(name, _)| model_name.is_none_or(|want| want == name.as_str()))
.any(|(_, rs)| !rs.reactions.is_empty())
{
return true;
}
let Some(models) = file.models.as_ref() else {
return false;
};
models
.iter()
.filter(|(name, _)| model_name.is_none_or(|want| want == name.as_str()))
.any(|(_, m)| crate::simulate_array::model_tree_any(m, &model_has_derivative))
}
fn model_has_derivative(model: &crate::types::Model) -> bool {
model
.equations
.iter()
.any(|eq| crate::classification::has_time_derivative(&eq.lhs))
}
fn is_shaped_parameter(var: &crate::types::ModelVariable) -> bool {
var.var_type == crate::types::VariableType::Parameter
&& var.shape.as_ref().is_some_and(|s| !s.is_empty())
}
fn is_externally_fed(var: &crate::types::ModelVariable) -> bool {
var.update.as_ref().is_some_and(|spec| {
spec.rules().iter().all(|rule| {
rule.value()
.is_some_and(|v| v.from.is_some() || v.handler.is_some())
})
})
}
fn names_key<V>(keys: &HashMap<String, V>, name: &str) -> bool {
keys.contains_key(name)
|| name
.split_once('.')
.is_some_and(|(_, tail)| keys.contains_key(tail))
}
fn declared_variable<'f>(
file: Option<&'f EsmFile>,
name: &str,
) -> Option<&'f crate::types::ModelVariable> {
let models = file?.models.as_ref()?;
if let Some((model, var)) = name.split_once('.')
&& let Some(v) = models.get(model).and_then(|m| m.variables.get(var))
{
return Some(v);
}
models.values().find_map(|m| m.variables.get(name))
}
fn missing_data_error(name: &str, var: Option<&crate::types::ModelVariable>) -> SimulateError {
let shaped = var.is_some_and(is_shaped_parameter);
let shape = var
.and_then(|v| v.shape.as_ref())
.filter(|s| !s.is_empty())
.map(|s| format!(" (shape [{}])", s.join(", ")))
.unwrap_or_default();
let mut feeds: Vec<String> = Vec::new();
for rule in var
.and_then(|v| v.update.as_ref())
.map(|u| u.rules())
.unwrap_or_default()
{
let Some(value) = rule.value() else { continue };
if let Some(from) = &value.from {
feeds.push(format!(
"the data source '{}' (file_variable '{}')",
rule.data_source().unwrap_or("?"),
from.file_variable
));
} else if let Some(h) = &value.handler {
feeds.push(format!(
"the registered handler '{}' (update kind '{}'), which writes it only when it fires",
h.handler_id,
rule.kind()
));
}
}
let (fed, how) = if feeds.is_empty() {
(
"Nothing in the document gives it a value.".to_string(),
if shaped {
format!(
"pass its array in `ProblemOptions::const_arrays` under \"{name}\", or declare \
a `default` on it"
)
} else {
format!(
"pass it in `ProblemOptions::p` under \"{name}\", or declare a `default` on it"
)
},
)
} else {
(
format!(
"The document feeds it from {}, and no data for it was supplied at construction.",
feeds.join(" and ")
),
format!(
"pass a provider for it in `ProblemOptions::providers` under \"{name}\", or its \
array in `ProblemOptions::const_arrays` (a constant snapshot), or declare a \
`default` on it"
),
)
};
SimulateError::Compile(crate::compile_error::CompileError::MissingData {
parameter: name.to_string(),
detail: format!(
"no data supplied for the {kind}parameter '{name}'{shape}, which declares no \
`default`. {fed} A parameter with neither a default nor a supplied value is an error \
when a problem is built (esm-spec §10.10). To supply it, {how}.",
kind = if shaped { "shaped " } else { "" }
),
})
}
#[cfg(feature = "solve")]
fn refuse_missing_initial_values(
c: &ArrayCompiled,
u0: &HashMap<String, f64>,
) -> Result<(), SimulateError> {
let unset = c.unset_initial_slots(u0);
let Some(first) = unset.first() else {
return Ok(());
};
let mut shown: Vec<String> = unset.iter().take(5).map(|n| format!("'{n}'")).collect();
if unset.len() > 5 {
shown.push("…".to_string());
}
Err(SimulateError::Compile(
crate::compile_error::CompileError::MissingInitialValue {
slot: first.clone(),
detail: format!(
"no starting value for {} unknown(s) ({}): the unknown declares no `default`, \
and no initial condition, `ic` equation or caller `u0` sets it. An unknown with \
no starting value is an error when a problem is built (esm-spec §11.4). To \
supply it, pass it in `ProblemOptions::u0` under \"{first}\", or declare a \
`default` on the unknown.",
unset.len(),
shown.join(", ")
),
},
))
}
#[cfg(not(target_arch = "wasm32"))]
fn inline_value_of(a: &ArrayD<f64>) -> crate::types::InlineValue {
use crate::types::InlineValue;
fn nest(shape: &[usize], values: &[f64]) -> InlineValue {
let Some((&axis, rest)) = shape.split_first() else {
return InlineValue::Scalar(values[0]);
};
let stride = rest.iter().product::<usize>();
InlineValue::Array(
(0..axis)
.map(|i| nest(rest, &values[i * stride..(i + 1) * stride]))
.collect(),
)
}
let values: Vec<f64> = a.iter().copied().collect();
if a.ndim() == 0 {
return InlineValue::Scalar(values[0]);
}
nest(a.shape(), &values)
}
#[derive(Default)]
struct SuppliedData {
#[cfg(not(target_arch = "wasm32"))]
arrays: HashMap<String, ArrayD<f64>>,
provider_keys: HashMap<String, ()>,
#[cfg(not(target_arch = "wasm32"))]
forcing_arrays: std::cell::RefCell<HashMap<String, ArrayD<f64>>>,
}
impl SuppliedData {
fn capture(opts: &mut ProblemOptions, raw: Option<&JsonValue>, file: Option<&EsmFile>) -> Self {
#[cfg_attr(target_arch = "wasm32", allow(unused_mut))]
let mut out = SuppliedData::default();
#[cfg(not(target_arch = "wasm32"))]
{
out.provider_keys
.extend(opts.providers.keys().map(|k| (k.clone(), ())));
out.provider_keys
.extend(opts.build_providers.iter().map(|(k, _)| (k.clone(), ())));
if !opts.const_arrays.is_empty() {
let shaped = shaped_parameter_keys(raw, file);
let several_models =
opts.model_name.is_none() && model_count(raw, file).is_some_and(|n| n > 1);
let only_trigger = (several_models || raw.is_some_and(declares_derivative))
&& !opts.build_pipeline
&& !opts.pushdown_rewrite
&& opts.build_providers.is_empty()
&& opts.const_arrays.keys().all(|k| shaped.contains(k));
if only_trigger {
out.arrays = std::mem::take(&mut opts.const_arrays);
} else {
for (k, a) in &opts.const_arrays {
if shaped.contains(k) {
out.arrays.insert(k.clone(), a.clone());
}
}
}
}
}
#[cfg(target_arch = "wasm32")]
let _ = (opts, raw, file);
out
}
#[cfg(not(target_arch = "wasm32"))]
fn bind_arrays(&self, file: &mut EsmFile, p: &HashMap<String, f64>, selected: Option<&str>) {
if self.arrays.is_empty() {
return;
}
let Some(models) = file.models.as_mut() else {
return;
};
let in_scope = |mname: &str| selected.is_none_or(|s| s == mname);
let mut owners: HashMap<String, usize> = HashMap::new();
for (_, m) in models.iter().filter(|(n, _)| in_scope(n)) {
for (v, var) in &m.variables {
if is_shaped_parameter(var) {
*owners.entry(v.clone()).or_default() += 1;
}
}
}
for (mname, m) in models.iter_mut() {
if !in_scope(mname) {
continue;
}
for (v, var) in m.variables.iter_mut() {
if !is_shaped_parameter(var) {
continue;
}
let qualified = format!("{mname}.{v}");
let arr = self.arrays.get(&qualified).or_else(|| {
if owners.get(v) == Some(&1) {
self.arrays.get(v)
} else {
None
}
});
let Some(arr) = arr else { continue };
if p.contains_key(&qualified) || p.contains_key(v) {
continue;
}
if is_externally_fed(var) {
if !names_key(&self.provider_keys, &qualified) {
self.forcing_arrays
.borrow_mut()
.insert(qualified, arr.clone());
}
continue;
}
var.default = Some(inline_value_of(arr));
}
}
}
fn refuse_missing_array(
&self,
c: &ArrayCompiled,
file: Option<&EsmFile>,
p: &HashMap<String, f64>,
) -> Result<(), SimulateError> {
let full = |name: &str| match c.namespace() {
Some(ns) if !name.starts_with(&format!("{ns}.")) => format!("{ns}.{name}"),
_ => name.to_string(),
};
let forcing = c.forcing_buffer();
for name in c.forcing_names() {
if forcing.borrow().contains_key(name) || names_key(&self.provider_keys, name) {
continue;
}
#[cfg(not(target_arch = "wasm32"))]
if let Some(arr) = self.forcing_array(name) {
forcing.borrow_mut().insert(name.clone(), arr);
continue;
}
let mut value = match c.forcing_default(name) {
None => {
return Err(missing_data_error(
&full(name),
declared_variable(file, name),
));
}
Some(None) => continue,
Some(Some(value)) => value,
};
let prec = precision::of_variable(name);
if prec.is_f32() {
value.mapv_inplace(|x| prec.round(x));
}
forcing.borrow_mut().insert(name.clone(), value);
}
for name in c.unvalued_shaped_params() {
if !names_key(p, name) {
return Err(missing_data_error(
&full(name),
declared_variable(file, name),
));
}
}
for name in c.unsupplied_params(p) {
let var = declared_variable(file, &name);
if c.unvalued_shaped_params().contains(&name)
|| var.is_some_and(|v| v.distribution.is_some())
{
continue;
}
return Err(missing_data_error(&full(&name), var));
}
Ok(())
}
#[cfg(not(target_arch = "wasm32"))]
fn forcing_array(&self, name: &str) -> Option<ArrayD<f64>> {
let arrays = self.forcing_arrays.borrow();
if let Some(a) = arrays.get(name) {
return Some(a.clone());
}
let mut tails = arrays
.iter()
.filter(|(k, _)| k.split_once('.').is_some_and(|(_, tail)| tail == name));
match (tails.next(), tails.next()) {
(Some((_, a)), None) => Some(a.clone()),
_ => None,
}
}
fn refuse_missing_static(
&self,
file: &EsmFile,
p: &HashMap<String, f64>,
selected: Option<&str>,
) -> Result<(), SimulateError> {
let unvalued = |v: &crate::types::ModelVariable| {
v.var_type == crate::types::VariableType::Parameter
&& v.default.is_none()
&& v.distribution.is_none()
};
let any = file
.models
.as_ref()
.is_some_and(|ms| ms.values().any(|m| m.variables.values().any(unvalued)));
if !any {
return Ok(());
}
let Ok(flat) = crate::flatten::flatten(file) else {
return Ok(());
};
for (name, var) in &flat.parameters {
if !unvalued(var) || names_key(p, name) {
continue;
}
if selected.is_some_and(|m| !name.starts_with(&format!("{m}."))) {
continue;
}
if is_externally_fed(var) && names_key(&self.provider_keys, name) {
continue;
}
return Err(missing_data_error(name, Some(var)));
}
Ok(())
}
}
fn bind_value_invention_params(file: &mut EsmFile, p: &HashMap<String, f64>) -> Vec<String> {
use crate::types::Expr;
use std::collections::HashSet;
fn collect(e: &Expr, inside: bool, out: &mut HashSet<String>) {
match e {
Expr::Variable(v) if inside => {
out.insert(v.clone());
}
Expr::Operator(n) => {
let vi = inside
|| matches!(n.op.as_str(), "skolem" | "rank" | "argmin" | "argmax")
|| n.distinct == Some(true)
|| n.key.is_some();
n.for_each_child(&mut |c| collect(c, vi, out));
}
_ => {}
}
}
let mut used = Vec::new();
if p.is_empty() {
return used;
}
let Some(models) = file.models.as_mut() else {
return used;
};
for (mname, m) in models.iter_mut() {
let mut read: HashSet<String> = HashSet::new();
for eq in &m.equations {
collect(&eq.rhs, false, &mut read);
}
for (v, var) in m.variables.iter_mut() {
if var.var_type != crate::types::VariableType::Parameter
|| var.shape.as_ref().is_some_and(|s| !s.is_empty())
|| !read.contains(v)
{
continue;
}
let qualified = format!("{mname}.{v}");
let key = if p.contains_key(&qualified) {
qualified
} else if p.contains_key(v) {
v.clone()
} else {
continue;
};
var.default = Some(crate::types::InlineValue::Scalar(p[&key]));
used.push(key);
}
}
used
}
fn bind_join_key_params(file: &mut EsmFile, p: &HashMap<String, f64>) -> Vec<String> {
let mut baked = Vec::new();
let Some(models) = file.models.as_mut() else {
return baked;
};
for (mname, m) in models.iter_mut() {
for v in crate::join::join_key_parameters(m) {
let qualified = format!("{mname}.{v}");
if let Some(&x) = p.get(&qualified).or_else(|| p.get(&v))
&& let Some(var) = m.variables.get_mut(&v)
{
var.default = Some(crate::types::InlineValue::Scalar(x));
}
baked.push(qualified);
baked.push(v);
}
}
baked
}
#[cfg(not(target_arch = "wasm32"))]
fn declares_derivative(raw: &JsonValue) -> bool {
fn has_d(v: &JsonValue) -> bool {
match v {
JsonValue::Object(o) => {
o.get("op").and_then(JsonValue::as_str) == Some("D") || o.values().any(has_d)
}
JsonValue::Array(a) => a.iter().any(has_d),
_ => false,
}
}
if raw
.get("reaction_systems")
.and_then(JsonValue::as_object)
.is_some_and(|r| !r.is_empty())
{
return true;
}
raw.get("models")
.and_then(JsonValue::as_object)
.is_some_and(|ms| {
ms.values().any(|m| {
m.get("equations")
.and_then(JsonValue::as_array)
.is_some_and(|eqs| eqs.iter().any(|e| e.get("lhs").is_some_and(has_d)))
})
})
}
#[cfg(not(target_arch = "wasm32"))]
fn model_count(raw: Option<&JsonValue>, file: Option<&EsmFile>) -> Option<usize> {
match raw {
Some(r) => r
.get("models")
.and_then(JsonValue::as_object)
.map(|m| m.len()),
None => file.and_then(|f| f.models.as_ref()).map(|m| m.len()),
}
}
#[cfg(not(target_arch = "wasm32"))]
fn shaped_parameter_keys(
raw: Option<&JsonValue>,
file: Option<&EsmFile>,
) -> std::collections::HashSet<String> {
let mut out = std::collections::HashSet::new();
if let Some(models) = raw
.and_then(|v| v.get("models"))
.and_then(JsonValue::as_object)
{
for (m, model) in models {
let Some(vars) = model.get("variables").and_then(JsonValue::as_object) else {
continue;
};
for (v, var) in vars {
let shaped = var
.get("shape")
.and_then(JsonValue::as_array)
.is_some_and(|s| !s.is_empty());
if shaped && var.get("type").and_then(JsonValue::as_str) == Some("parameter") {
out.insert(format!("{m}.{v}"));
out.insert(v.clone());
}
}
}
} else if let Some(models) = file.and_then(|f| f.models.as_ref()) {
for (m, model) in models {
for (v, var) in &model.variables {
if is_shaped_parameter(var) {
out.insert(format!("{m}.{v}"));
out.insert(v.clone());
}
}
}
}
out
}
#[cfg(not(target_arch = "wasm32"))]
#[allow(clippy::type_complexity)]
fn bind_providers(
backend: &Backend,
opts: &mut ProblemOptions,
tspan: (f64, f64),
) -> Result<
(
Option<std::cell::RefCell<crate::provider::RefreshExecutor>>,
std::collections::HashSet<String>,
Vec<f64>,
),
SimulateError,
> {
if opts.providers.is_empty() {
return Ok((None, Default::default(), Vec::new()));
}
let Backend::Array(compiled) = backend else {
let name = opts.providers.keys().next().cloned().unwrap_or_default();
return Err(SimulateError::ProviderError {
name,
details: "providers require an array/spatial model (this document has none)"
.to_string(),
});
};
let providers = std::mem::take(&mut opts.providers);
let mut exec = crate::provider::RefreshExecutor::from_providers(providers);
let forcing = compiled.forcing_buffer();
exec.materialize_const(&forcing)
.map_err(|e| SimulateError::ProviderError {
name: "<const-loader>".into(),
details: e.to_string(),
})?;
let discrete: std::collections::HashSet<String> = exec
.bindings()
.filter(|b| b.cadence == crate::cadence::Cadence::Discrete)
.flat_map(|b| b.variables.iter().cloned())
.collect();
let sample_time = opts.sample_time.unwrap_or(tspan.0);
let mut boundaries: Vec<f64> = exec.refresh_times();
boundaries.retain(|&b| b > sample_time && b < tspan.1);
Ok((Some(std::cell::RefCell::new(exec)), discrete, boundaries))
}
#[cfg(feature = "solve")]
pub fn solve(prob: &EsmProblem, opts: &SolveOptions) -> Result<Solution, SimulateError> {
let _precision_guard = prob.precision.enter();
reject_f32_integration(prob)?;
let effective = effective_options(prob, opts);
match &*prob.backend {
Backend::Static(reason) => Err(SimulateError::NotDynamic {
details: reason.clone(),
}),
Backend::Array(compiled) => {
let mut insp = BuildInspection::default();
let sink = prob.inspect.then_some(&mut insp);
#[cfg(not(target_arch = "wasm32"))]
let sol = {
match (&prob.refresh, prob.discrete_forcing.is_empty()) {
(Some(exec), false) => {
let forcing = compiled.forcing_buffer();
let mut exec = exec.borrow_mut();
let refresh_fn = |t: f64| -> Result<(), SimulateError> {
exec.refresh_at(t, &forcing).map(|_| ()).map_err(|e| {
SimulateError::ProviderError {
name: "<discrete-loader>".into(),
details: e.to_string(),
}
})
};
compiled.solve_with_refresh_inspect(
prob.tspan,
&prob.p,
&prob.u0,
&effective,
sink,
&prob.discrete_forcing,
&prob.refresh_boundaries,
refresh_fn,
)?
}
_ => compiled.solve_inspect(prob.tspan, &prob.p, &prob.u0, &effective, sink)?,
}
};
#[cfg(target_arch = "wasm32")]
let sol = compiled.solve_inspect(prob.tspan, &prob.p, &prob.u0, &effective, sink)?;
if prob.inspect {
*prob.inspection.borrow_mut() = insp;
}
let mut sol = sol;
if compiled.namespace().is_some() {
let states = compiled.qualified_state_names();
let observed = sol
.state_variable_names
.split_off(states.len().min(sol.state_variable_names.len()));
let mut names = states.to_vec();
names.extend(qualify_array_names(compiled, &observed));
sol.state_variable_names = names;
}
sol.metadata.namespace = None;
Ok(sol)
}
}
}
fn with_resolved_tolerances(prob: &EsmProblem, opts: &SolveOptions) -> SolveOptions {
let (abstol, reltol) =
crate::resolve_tolerances(prob.solver.as_ref(), opts.abstol, opts.reltol);
SolveOptions {
abstol: Some(abstol),
reltol: Some(reltol),
..opts.clone()
}
}
fn effective_options(prob: &EsmProblem, opts: &SolveOptions) -> SolveOptions {
let observed = resolve_output_request(prob, &opts.output_observed);
let resolved = with_resolved_tolerances(prob, opts);
let set = opts
.callback
.clone()
.unwrap_or_else(|| prob.callbacks.clone());
if set.is_empty() {
return match observed {
Some(output_observed) => SolveOptions {
output_observed,
..resolved
},
None => resolved,
};
}
let user = opts.progress.clone();
let observer: ProgressFn = wrap_observer(set, user);
SolveOptions {
progress: Some(observer),
output_observed: observed.unwrap_or_else(|| opts.output_observed.clone()),
..resolved
}
}
fn problem_merged_renames(prob: &EsmProblem) -> &HashMap<String, String> {
static EMPTY: std::sync::OnceLock<HashMap<String, String>> = std::sync::OnceLock::new();
match &*prob.backend {
Backend::Array(c) => c.merged_renames(),
Backend::Static(_) => EMPTY.get_or_init(HashMap::new),
}
}
fn resolve_output_request(prob: &EsmProblem, requested: &[String]) -> Option<Vec<String>> {
if requested.is_empty() {
return None;
}
let renames = problem_merged_renames(prob);
if renames.is_empty() || !requested.iter().any(|n| renames.contains_key(n.as_str())) {
return None;
}
Some(
requested
.iter()
.map(|n| {
renames
.get(n.as_str())
.cloned()
.unwrap_or_else(|| n.clone())
})
.collect(),
)
}
fn document_solver(doc: &JsonValue) -> Option<crate::Solver> {
let block = doc.get("solver")?;
crate::solver::normalize_empty(serde_json::from_value(block.clone()).ok()?)
}
#[cfg(not(target_arch = "wasm32"))]
fn wrap_observer(set: CallbackSet, user: Option<ProgressFn>) -> ProgressFn {
std::sync::Arc::new(move |p: &Progress<'_>| {
let a = set.invoke(p);
let b = user.as_ref().map(|f| f(p)).unwrap_or(Flow::Continue);
if matches!(a, Flow::Cancel) || matches!(b, Flow::Cancel) {
Flow::Cancel
} else {
Flow::Continue
}
})
}
#[cfg(target_arch = "wasm32")]
fn wrap_observer(set: CallbackSet, user: Option<ProgressFn>) -> ProgressFn {
std::sync::Arc::new(move |p: &Progress<'_>| {
let a = set.invoke(p);
let b = user.as_ref().map(|f| f(p)).unwrap_or(Flow::Continue);
if matches!(a, Flow::Cancel) || matches!(b, Flow::Cancel) {
Flow::Cancel
} else {
Flow::Continue
}
})
}
#[cfg(feature = "solve")]
pub struct Integrator<'a> {
prob: &'a EsmProblem,
opts: SolveOptions,
grid: Vec<f64>,
next: usize,
t: f64,
u: HashMap<String, f64>,
time: Vec<f64>,
state: Vec<Vec<f64>>,
names: Vec<String>,
retcode: crate::simulate::ReturnCode,
metadata: crate::simulate::SolutionMetadata,
}
#[cfg(feature = "solve")]
impl std::fmt::Debug for Integrator<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Integrator")
.field("t", &self.t)
.field("remaining", &(self.grid.len().saturating_sub(self.next)))
.field("retcode", &self.retcode)
.finish()
}
}
#[cfg(feature = "solve")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StepStatus {
Advanced,
Done,
}
#[cfg(feature = "solve")]
pub fn init<'a>(
prob: &'a EsmProblem,
opts: &SolveOptions,
) -> Result<Integrator<'a>, SimulateError> {
let _precision_guard = prob.precision.enter();
reject_f32_integration(prob)?;
if let Backend::Static(reason) = &*prob.backend {
return Err(SimulateError::NotDynamic {
details: reason.clone(),
});
}
let (t0, t_end) = prob.tspan;
let grid = match &opts.saveat {
Some(g) if g.len() >= 2 => g.clone(),
_ => {
let n = 100usize;
(0..=n)
.map(|i| t0 + (t_end - t0) * (i as f64) / (n as f64))
.collect()
}
};
Ok(Integrator {
prob,
opts: with_resolved_tolerances(prob, opts),
grid,
next: 0,
t: t0,
u: prob.u0.clone(),
time: Vec::new(),
state: Vec::new(),
names: prob.state_variable_names(),
retcode: crate::simulate::ReturnCode::Success,
metadata: crate::simulate::SolutionMetadata::default(),
})
}
#[cfg(feature = "solve")]
impl Integrator<'_> {
pub fn t(&self) -> f64 {
self.t
}
pub fn retcode(&self) -> crate::simulate::ReturnCode {
self.retcode
}
pub fn u(&self) -> &HashMap<String, f64> {
&self.u
}
pub fn step(&mut self) -> Result<StepStatus, SimulateError> {
let _precision_guard = self.prob.precision.enter();
if !self.retcode.is_success() {
return Ok(StepStatus::Done);
}
while self.next < self.grid.len() && self.grid[self.next] <= self.t {
self.next += 1;
}
if self.next >= self.grid.len() {
return Ok(StepStatus::Done);
}
let t_next = self.grid[self.next];
let seg = remake(
self.prob,
&Remake {
u0: self.u.clone(),
tspan: Some((self.t, t_next)),
..Default::default()
},
)?;
let seg_opts = SolveOptions {
saveat: Some(vec![t_next]),
..self.opts.clone()
};
let sol = solve(&seg, &seg_opts)?;
self.retcode = sol.retcode;
self.metadata.alg = sol.metadata.alg.clone();
self.metadata.n_rhs_calls += sol.metadata.n_rhs_calls;
self.metadata.n_jacobian_calls += sol.metadata.n_jacobian_calls;
self.metadata.n_accepted_steps += sol.metadata.n_accepted_steps;
self.metadata.n_rejected_steps += sol.metadata.n_rejected_steps;
self.metadata.tape_fallbacks = sol.metadata.tape_fallbacks.clone();
if self.state.is_empty() {
self.names = sol.state_variable_names.clone();
self.state = vec![Vec::new(); self.names.len()];
}
if let Some(k) = sol.time.len().checked_sub(1) {
self.time.push(sol.time[k]);
for (r, row) in sol.state.iter().enumerate() {
if let Some(v) = row.get(k) {
if r < self.state.len() {
self.state[r].push(*v);
}
if let Some(name) = self.names.get(r) {
self.u.insert(name.clone(), *v);
}
}
}
self.t = sol.time[k];
}
self.next += 1;
if self.next >= self.grid.len() || !self.retcode.is_success() {
Ok(StepStatus::Done)
} else {
Ok(StepStatus::Advanced)
}
}
pub fn solve_to_completion(&mut self) -> Result<Solution, SimulateError> {
while matches!(self.step()?, StepStatus::Advanced) {}
Ok(Solution {
time: std::mem::take(&mut self.time),
state: std::mem::take(&mut self.state),
state_variable_names: self.names.clone(),
retcode: self.retcode,
metadata: self.metadata.clone(),
})
}
}
#[cfg(feature = "solve")]
pub fn step(integrator: &mut Integrator<'_>) -> Result<StepStatus, SimulateError> {
integrator.step()
}
#[cfg(feature = "solve")]
pub fn solve_to_completion(integrator: &mut Integrator<'_>) -> Result<Solution, SimulateError> {
integrator.solve_to_completion()
}
#[cfg(all(test, feature = "solve", not(target_arch = "wasm32")))]
mod stepping_tolerance_tests {
use super::*;
use crate::simulate::{DEFAULT_ABSTOL, DEFAULT_RELTOL};
fn doc(solver: &str) -> serde_json::Value {
serde_json::from_str(&format!(
r#"{{"esm":"1.1.0","metadata":{{"name":"T"}},{solver}"models":{{"M":{{
"variables":{{"y":{{"type":"unknown","units":"m","default":1.0}}}},
"equations":[{{"lhs":{{"op":"D","args":["y"],"wrt":"t"}},
"rhs":{{"op":"neg","args":["y"]}}}}]}}}}}}"#
))
.expect("fixture parses")
}
fn problem(solver: &str) -> EsmProblem {
let json = doc(solver);
esm_problem(
ProblemInput::Json(&json),
(0.0, 1.0),
ProblemOptions::default(),
)
.expect("builds")
}
#[test]
fn init_resolves_the_documents_tolerances() {
let prob = problem(r#""solver":{"abstol":1e-8,"reltol":1e-6},"#);
let integ = init(&prob, &SolveOptions::default()).expect("init");
assert_eq!(integ.opts.abstol, Some(1e-8));
assert_eq!(integ.opts.reltol, Some(1e-6));
}
#[test]
fn an_explicit_call_site_option_still_beats_the_document() {
let prob = problem(r#""solver":{"abstol":1e-8,"reltol":1e-6},"#);
let opts = SolveOptions {
abstol: Some(1e-12),
reltol: Some(1e-11),
..SolveOptions::default()
};
let integ = init(&prob, &opts).expect("init");
assert_eq!(integ.opts.abstol, Some(1e-12));
assert_eq!(integ.opts.reltol, Some(1e-11));
}
#[test]
fn each_tolerance_falls_through_on_its_own() {
let only_reltol = problem(r#""solver":{"reltol":1e-9},"#);
let integ = init(&only_reltol, &SolveOptions::default()).expect("init");
assert_eq!(integ.opts.reltol, Some(1e-9));
assert_eq!(integ.opts.abstol, Some(DEFAULT_ABSTOL));
let no_block = problem("");
let integ = init(&no_block, &SolveOptions::default()).expect("init");
assert_eq!(integ.opts.abstol, Some(DEFAULT_ABSTOL));
assert_eq!(integ.opts.reltol, Some(DEFAULT_RELTOL));
}
#[test]
fn resolving_twice_is_resolving_once() {
let prob = problem(r#""solver":{"abstol":1e-8,"reltol":1e-6},"#);
let once = with_resolved_tolerances(&prob, &SolveOptions::default());
let twice = with_resolved_tolerances(&prob, &once);
assert_eq!((twice.abstol, twice.reltol), (once.abstol, once.reltol));
assert_eq!((twice.abstol, twice.reltol), (Some(1e-8), Some(1e-6)));
}
}