use std::collections::{BTreeMap, HashMap, HashSet};
use std::fmt;
use std::path::PathBuf;
use std::sync::Arc;
use ndarray::{ArrayD, Axis, IxDyn, Slice};
use serde_json::Value as JsonValue;
use crate::classification::DeclaredRank;
use crate::faq::resolve_expr_ranges_with_extents;
use crate::parse::LoadOptions;
use crate::pushdown_rewrite::{
GateAxis, ProviderGate, desugar_pushdown, pushdown_coupling_pairs, pushdown_provider_gates,
};
use crate::simulate_array::{
ArrMap, ConstArrayScope, Value as EvalValue, eval_expression_with_extents_and_consts_shared,
run_value_invention_with_params,
};
use crate::template_imports::resolve_template_machinery;
use crate::types::{Expr, IndexSet, Model, VariableType};
pub use crate::simulate::Flow;
type GatedProvider = (String, Box<dyn PrepareProvider>, ProviderGate);
#[derive(Debug, Clone)]
pub struct PrepareError(pub String);
impl PrepareError {
pub const CANCELLED_PREFIX: &'static str = "cancelled by the caller during";
pub fn is_cancelled(&self) -> bool {
self.0.starts_with(Self::CANCELLED_PREFIX)
}
}
impl fmt::Display for PrepareError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "PrepareError: {}", self.0)
}
}
impl std::error::Error for PrepareError {}
fn err(msg: impl Into<String>) -> PrepareError {
PrepareError(msg.into())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AxisSel {
All,
Indices(Vec<usize>),
Range {
start: usize,
stop: usize,
step: usize,
},
}
impl AxisSel {
pub fn indices(&self, dim_len: usize) -> Vec<usize> {
match self {
AxisSel::All => (0..dim_len).collect(),
AxisSel::Indices(v) => v.clone(),
AxisSel::Range { start, stop, step } => {
(*start..(*stop).min(dim_len)).step_by(*step).collect()
}
}
}
}
pub trait PrepareProvider {
fn sample(&mut self) -> Result<ArrayD<f64>, PrepareError>;
fn supports_selection(&self) -> bool {
false
}
fn sample_with_selection(
&mut self,
_selection: &[AxisSel],
) -> Result<ArrayD<f64>, PrepareError> {
Err(err("provider does not support selection pushdown"))
}
fn is_const(&self) -> bool {
true
}
fn gate_spec(&self) -> Option<ProviderGate> {
None
}
fn extent_metaparameter(&self) -> Option<String> {
None
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum PreparePhase {
Rewrite,
Load,
ConstProviders,
Coordinates,
ValueInvention,
MemberFactors,
GatedFetch,
Observeds,
}
impl PreparePhase {
pub const ALL: [PreparePhase; 8] = [
PreparePhase::Rewrite,
PreparePhase::Load,
PreparePhase::ConstProviders,
PreparePhase::Coordinates,
PreparePhase::ValueInvention,
PreparePhase::MemberFactors,
PreparePhase::GatedFetch,
PreparePhase::Observeds,
];
pub const COUNT: usize = Self::ALL.len();
pub fn index(self) -> usize {
Self::ALL.iter().position(|p| *p == self).unwrap_or(0)
}
pub fn label(self) -> &'static str {
match self {
PreparePhase::Rewrite => "the pushdown rewrite",
PreparePhase::Load => "the typed load",
PreparePhase::ConstProviders => "const provider materialization",
PreparePhase::Coordinates => "build-time coordinate evaluation",
PreparePhase::ValueInvention => "value invention",
PreparePhase::MemberFactors => "member-factor feedback",
PreparePhase::GatedFetch => "the gated fetch",
PreparePhase::Observeds => "observed-graph evaluation",
}
}
}
impl fmt::Display for PreparePhase {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.label())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PrepareProgress<'a> {
pub phase: PreparePhase,
pub done: usize,
pub total: Option<usize>,
pub item: &'a str,
}
impl PrepareProgress<'_> {
pub fn fraction(&self) -> f64 {
match self.total {
Some(t) if t > 0 => (self.done as f64 / t as f64).clamp(0.0, 1.0),
_ => 0.0,
}
}
}
pub type PrepareProgressFn = Arc<dyn Fn(&PrepareProgress<'_>) -> Flow + Send + Sync>;
#[derive(Clone, Default)]
pub(crate) struct PrepareOptions {
pub model_name: Option<String>,
pub metaparameters: BTreeMap<String, i64>,
pub base_path: Option<PathBuf>,
pub pushdown_rewrite: bool,
pub parameters: HashMap<String, f64>,
pub verbose: bool,
pub progress: Option<PrepareProgressFn>,
pub gated_fetch_batch: Option<usize>,
pub refuse_per_cell: bool,
}
impl fmt::Debug for PrepareOptions {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PrepareOptions")
.field("model_name", &self.model_name)
.field("metaparameters", &self.metaparameters)
.field("base_path", &self.base_path)
.field("pushdown_rewrite", &self.pushdown_rewrite)
.field("parameters", &self.parameters)
.field("verbose", &self.verbose)
.field(
"progress",
&self
.progress
.as_ref()
.map(|_| "<observer>")
.unwrap_or("None"),
)
.field("gated_fetch_batch", &self.gated_fetch_batch)
.field("refuse_per_cell", &self.refuse_per_cell)
.finish()
}
}
pub struct PreparedBuild {
pub doc: JsonValue,
pub model_name: String,
pub fields: HashMap<String, ArrayD<f64>>,
pub members: HashMap<String, Vec<i64>>,
pub extents: HashMap<String, i64>,
pub gated_provider_keys: Vec<String>,
pub rules: Vec<PipelineRule>,
pub refused: Option<PipelineRule>,
}
#[derive(Clone, Debug)]
pub(crate) struct PipelineRule {
pub name: String,
pub kind: &'static str,
pub route: Route,
}
#[derive(Clone, Debug)]
pub(crate) enum Route {
Overlay,
Tape,
PerCell(String),
Relational,
}
impl Route {
fn observed(per_cell: Option<String>) -> Route {
per_cell.map_or(Route::Overlay, Route::PerCell)
}
}
const PER_CELL_FAQ: &str = "the build pipeline evaluated it through the reference evaluator, \
and a `faq` in it is not one the whole-array overlay takes, so its body was walked \
once per cell";
const PER_CELL_RECURRENCE: &str = "a causal self-reference (esm-spec §4.3.1.1), which the \
build pipeline sweeps cell by cell through the reference evaluator";
fn declared_args(expr: &Expr, out: &mut HashSet<String>) {
match expr {
Expr::Variable(name) => {
out.insert(name.clone());
}
Expr::Operator(node) => {
for a in &node.args {
declared_args(a, out);
}
node.for_each_child(&mut |c| declared_args(c, out));
}
_ => {}
}
}
fn join_names(expr: &Expr, out: &mut HashSet<String>) {
if let Expr::Operator(node) = expr {
for clause in node.join.iter().flatten() {
out.extend(clause.on.iter().flatten().cloned());
if let Some(ov) = &clause.overlap {
out.extend(ov.src_env.iter().chain(ov.tgt_env.iter()).cloned());
}
}
for a in &node.args {
join_names(a, out);
}
node.for_each_child(&mut |c| join_names(c, out));
}
}
fn state_dependent_observeds(model: &Model, defs: &HashMap<String, Expr>) -> HashSet<String> {
let class = crate::classification::Classification::of(model);
let states: HashSet<&str> = model
.variables
.iter()
.filter(|(name, var)| var.var_type == VariableType::Unknown && !class.is_observed(name))
.map(|(name, _)| name.as_str())
.collect();
let refs: HashMap<&str, HashSet<String>> = defs
.iter()
.map(|(name, e)| {
let mut a = HashSet::new();
declared_args(e, &mut a);
(name.as_str(), a)
})
.collect();
let mut dependent: HashSet<String> = HashSet::new();
loop {
let before = dependent.len();
for (name, reads) in &refs {
if !dependent.contains(*name)
&& reads
.iter()
.any(|r| states.contains(r.as_str()) || dependent.contains(r))
{
dependent.insert((*name).to_string());
}
}
if dependent.len() == before {
return dependent;
}
}
}
fn observed_order(defs: &HashMap<String, Expr>) -> Result<Vec<String>, PrepareError> {
let deps: HashMap<String, HashSet<String>> = defs
.iter()
.map(|(n, e)| {
let mut a = HashSet::new();
declared_args(e, &mut a);
a.retain(|x| defs.contains_key(x) && x != n);
(n.clone(), a)
})
.collect();
let mut ordered = Vec::new();
let mut done: HashSet<String> = HashSet::new();
let mut pending: HashSet<String> = defs.keys().cloned().collect();
while !pending.is_empty() {
let mut ready: Vec<String> = pending
.iter()
.filter(|n| deps[*n].iter().all(|d| done.contains(d)))
.cloned()
.collect();
if ready.is_empty() {
let mut stuck: std::collections::BTreeMap<String, std::collections::BTreeSet<String>> =
std::collections::BTreeMap::new();
for n in &pending {
stuck.insert(n.clone(), deps[n].iter().cloned().collect());
}
let cycle = crate::classification::first_observed_cycle(&stuck)
.unwrap_or_else(|| stuck.keys().cloned().collect());
return Err(err(format!(
"observed_cycle: dependency cycle among observed variables: {}. Each is defined \
in terms of the next, so no evaluation order satisfies every definition \
(esm-spec §4.9.6). `esm validate` reports this cycle before any build.",
cycle.join(" -> ")
)));
}
ready.sort(); for n in ready {
ordered.push(n.clone());
done.insert(n.clone());
pending.remove(&n);
}
}
Ok(ordered)
}
fn observed_defs(model: &Model) -> HashMap<String, Expr> {
crate::classification::observed_definitions(model)
.into_iter()
.filter(|(_, def)| {
serde_json::to_value(def)
.is_ok_and(|raw| !crate::value_invention::is_value_invention_assignment(&raw))
})
.collect()
}
fn scalar_params(
model: &Model,
model_name: &str,
overrides: &HashMap<String, f64>,
) -> (Vec<f64>, Vec<String>) {
let mut names: Vec<String> = model
.variables
.iter()
.filter(|(_, v)| {
v.var_type == VariableType::Parameter
&& v.shape.as_ref().map(|s| s.is_empty()).unwrap_or(true)
&& v.default_scalar().is_some()
})
.map(|(k, _)| k.clone())
.collect();
names.sort();
let vals = names
.iter()
.map(|n| {
overrides
.get(n)
.or_else(|| overrides.get(&format!("{model_name}.{n}")))
.or_else(|| overrides.get(n.rsplit('.').next().unwrap_or(n)))
.copied()
.unwrap_or_else(|| {
model.variables[n]
.default_scalar()
.expect("selected on default_scalar().is_some()")
})
})
.collect();
(vals, names)
}
fn round_external(key: &str, mut a: ArrayD<f64>) -> ArrayD<f64> {
let prec = crate::precision::of_variable(key);
if prec.is_f32() {
a.mapv_inplace(|x| prec.round(x));
}
a
}
fn inject_aliases(arrays: &mut ArrMap, coupling: &[(String, String)]) {
for (frm, to) in coupling {
if !arrays.contains_key(frm) {
continue;
}
if !arrays.contains_key(to) {
let a = arrays[frm].clone();
arrays.insert(to.clone(), a);
}
let tail = to.rsplit('.').next().unwrap_or(to);
if !arrays.contains_key(tail) {
let a = arrays[frm].clone();
arrays.insert(tail.to_string(), a);
}
}
let mut tails: HashMap<String, Vec<String>> = HashMap::new();
for k in arrays.keys() {
if let Some((_, tail)) = k.rsplit_once('.') {
tails.entry(tail.to_string()).or_default().push(k.clone());
}
}
for (tail, mut keys) in tails {
if arrays.contains_key(&tail) {
continue;
}
let mindepth = keys
.iter()
.map(|k| k.matches('.').count())
.min()
.unwrap_or(0);
keys.retain(|k| k.matches('.').count() == mindepth);
keys.sort();
if keys.len() == 1 {
let a = arrays[&keys[0]].clone();
arrays.insert(tail, a);
}
}
}
fn producer_seed_closure(
seeds: &[&Expr],
defs: &HashMap<String, Expr>,
join_free: &HashSet<String>,
) -> HashSet<String> {
let mut needed: HashSet<String> = HashSet::new();
let mut frontier: Vec<String> = Vec::new();
let mut refs = HashSet::new();
for s in seeds {
declared_args(s, &mut refs);
}
for r in refs {
if join_free.contains(&r) && needed.insert(r.clone()) {
frontier.push(r);
}
}
while let Some(cur) = frontier.pop() {
let mut refs = HashSet::new();
declared_args(&defs[&cur], &mut refs);
for r in refs {
if join_free.contains(&r) && needed.insert(r.clone()) {
frontier.push(r);
}
}
}
needed
}
enum Evaluated {
Field(ArrayD<f64>, Route),
Refused(String),
}
#[allow(clippy::too_many_arguments)]
fn compile_on_tape(
name: &str,
expr: &Expr,
arrays: &ArrMap,
param_vals: &[f64],
param_names: &[String],
const_arrays: &ConstArrayScope,
recurrence_only: bool,
) -> Option<Result<Result<ArrayD<f64>, String>, String>> {
use crate::simulate_array::StandaloneError;
match crate::simulate_array::eval_observed_taped(
name,
expr,
arrays,
param_vals,
param_names,
const_arrays,
recurrence_only,
) {
None => None,
Some(Ok(a)) => Some(Ok(Ok(a))),
Some(Err(StandaloneError::Declined(why))) => Some(Ok(Err(why))),
Some(Err(StandaloneError::Fault(details))) => Some(Err(
crate::compile_error::CompileError::InterpreterBuildError { details }.to_string(),
)),
}
}
fn shape_like_interpreter(a: ArrayD<f64>, rank: DeclaredRank) -> ArrayD<f64> {
if a.ndim() == 0 {
ArrayD::from_elem(IxDyn(rank.scalar_shape()), a[IxDyn(&[])])
} else {
a
}
}
#[allow(clippy::too_many_arguments)]
fn eval_observed(
name: &str,
def: &Expr,
arrays: &ArrMap,
param_vals: &[f64],
param_names: &[String],
index_sets: &HashMap<String, IndexSet>,
extents: &HashMap<String, i64>,
const_arrays: &ConstArrayScope,
rank: DeclaredRank,
refuse_per_cell: bool,
) -> Result<Evaluated, PrepareError> {
let _rule_precision = crate::precision::has_variable_overrides()
.then(|| crate::precision::enter(crate::precision::of_variable(name)));
let mut expr = def.clone();
resolve_expr_ranges_with_extents(&mut expr, index_sets, extents)
.map_err(|e| err(format!("resolve ranges for {name}: {e}")))?;
if refuse_per_cell
&& let Some(tape) = compile_on_tape(
name,
&expr,
arrays,
param_vals,
param_names,
const_arrays,
true,
)
{
return tape
.map_err(|why| err(format!("evaluate {name}: {why}")))
.map(|r| match r {
Ok(a) => Evaluated::Field(shape_like_interpreter(a, rank), Route::Tape),
Err(declined) => Evaluated::Refused(format!(
"{PER_CELL_RECURRENCE}, and the tape declined it: {declined}"
)),
});
}
let stop = refuse_per_cell.then(crate::simulate_array::StopAtFirstCell::arm);
let refused = || stop.is_some() && crate::simulate_array::per_cell_walk_refused();
if let Some(res) = crate::simulate_array::eval_observed_recurrence(
name,
&expr,
arrays,
param_vals,
param_names,
0.0,
extents,
const_arrays,
) {
if refused() {
return Ok(Evaluated::Refused(PER_CELL_RECURRENCE.to_string()));
}
return res
.map(|a| Evaluated::Field(a, Route::PerCell(PER_CELL_RECURRENCE.to_string())))
.map_err(|e| err(format!("evaluate {name}: {e}")));
}
let walks_before = crate::simulate_array::per_cell_walks();
crate::simulate_array::take_overlay_decline();
let val = eval_expression_with_extents_and_consts_shared(
&expr,
arrays,
param_vals,
param_names,
0.0,
extents,
const_arrays,
);
if refused() {
let overlay = crate::simulate_array::take_overlay_decline()
.unwrap_or("a construct it has no whole-array form for");
drop(stop);
let tape = compile_on_tape(
name,
&expr,
arrays,
param_vals,
param_names,
const_arrays,
false,
)
.expect("a non-recurrence always reaches the tape");
return match tape {
Err(fault) => Err(err(format!("evaluate {name}: {fault}"))),
Ok(Ok(a)) => Ok(Evaluated::Field(
shape_like_interpreter(a, rank),
Route::Tape,
)),
Ok(Err(declined)) => Ok(Evaluated::Refused(format!(
"the whole-array overlay declined a `faq` in it ({overlay}), and the tape \
declined it: {declined}"
))),
};
}
let val = val.map_err(|e| err(format!("evaluate {name}: {e}")))?;
let per_cell =
(crate::simulate_array::per_cell_walks() != walks_before).then(|| PER_CELL_FAQ.to_string());
let field = match val {
EvalValue::Array(a) => *a,
EvalValue::Scalar(s) => ArrayD::from_elem(IxDyn(rank.scalar_shape()), s),
};
Ok(Evaluated::Field(field, Route::observed(per_cell)))
}
struct GatedFetchPlan {
selection: Vec<AxisSel>,
drop_axes: Vec<usize>,
gated_pos: usize,
gated_pos_out: usize,
gated_extent: usize,
n_requests: usize,
}
fn gated_target(key: &str, gate: &ProviderGate, pd_coupling: &[(String, String)]) -> String {
pd_coupling
.iter()
.find(|(frm, to)| frm == key || to == key)
.map(|(_, to)| to.rsplit('.').next().unwrap_or(to).to_string())
.unwrap_or_else(|| gate.applies_to[0].clone())
}
fn gated_fetch_plan(
key: &str,
gate: &ProviderGate,
index_sets: &HashMap<String, IndexSet>,
members: &HashMap<String, Vec<i64>>,
extents: &HashMap<String, i64>,
) -> Result<GatedFetchPlan, PrepareError> {
let mut selection: Vec<AxisSel> = Vec::with_capacity(gate.axes.len());
let mut drop_axes: Vec<usize> = Vec::new();
let mut gated_pos: Option<usize> = None;
let mut gated_extent = 0usize;
for (ax_i, ax) in gate.axes.iter().enumerate() {
match ax {
GateAxis::All => selection.push(AxisSel::All),
GateAxis::Fixed(fi) => {
selection.push(AxisSel::Indices(vec![*fi]));
drop_axes.push(ax_i);
}
GateAxis::Range { start, stop, step } => selection.push(AxisSel::Range {
start: *start,
stop: *stop,
step: *step,
}),
GateAxis::GatedBy(sname) => {
let faq = index_sets
.get(sname)
.and_then(|is| (is.kind == "derived").then(|| is.from_faq.clone()))
.flatten()
.ok_or_else(|| {
err(format!(
"gated provider '{key}' gates on '{sname}' which is not a \
derived index set with a from_faq"
))
})?;
let mem = members.get(&faq).ok_or_else(|| {
err(format!(
"gated provider '{key}' gates on '{sname}' (faq '{faq}') but its \
value-invention members were not materialised"
))
})?;
let mem0: Vec<usize> = mem
.iter()
.map(|&m| {
usize::try_from(m - 1).map_err(|_| {
err(format!(
"gated provider '{key}': member id {m} is not a 1-based \
cell id"
))
})
})
.collect::<Result<_, _>>()?;
gated_extent = extents.get(&faq).map(|&e| e as usize).unwrap_or(mem0.len());
selection.push(AxisSel::Indices(mem0));
gated_pos = Some(ax_i);
}
}
}
let gated_pos = gated_pos
.ok_or_else(|| err(format!("gated provider '{key}' declares no gated_by axis")))?;
let gated_pos_out = gated_pos - drop_axes.iter().filter(|&&d| d < gated_pos).count();
Ok(GatedFetchPlan {
selection,
drop_axes,
gated_pos,
gated_pos_out,
gated_extent,
n_requests: 1,
})
}
type ReportFn<'a> =
dyn FnMut(PreparePhase, usize, Option<usize>, &str) -> Result<(), PrepareError> + 'a;
#[allow(clippy::too_many_arguments)]
fn run_gated_fetch(
key: &str,
prov: &mut dyn PrepareProvider,
plan: &GatedFetchPlan,
batch: Option<usize>,
issued: &mut usize,
total_requests: usize,
report: &mut ReportFn<'_>,
) -> Result<ArrayD<f64>, PrepareError> {
if !prov.supports_selection() {
report(PreparePhase::GatedFetch, *issued, Some(total_requests), key)?;
*issued += 1;
let full = prov
.sample()
.map_err(|e| err(format!("gated fetch '{key}' (whole): {}", e.0)))?;
return drop_fixed_axes(key, slice_whole(full, &plan.selection), &plan.drop_axes);
}
let AxisSel::Indices(gated_idx) = &plan.selection[plan.gated_pos] else {
return Err(err(format!(
"gated provider '{key}': the gated axis resolved to the full axis, \
which is not a support set"
)));
};
let size = batch.unwrap_or(usize::MAX).max(1);
let n_batches = gated_idx.len().div_ceil(size).max(1);
if n_batches == 1 {
report(PreparePhase::GatedFetch, *issued, Some(total_requests), key)?;
*issued += 1;
let a = prov
.sample_with_selection(&plan.selection)
.map_err(|e| err(format!("gated fetch '{key}': {}", e.0)))?;
return drop_fixed_axes(key, a, &plan.drop_axes);
}
let mut out: Option<ArrayD<f64>> = None;
let mut filled = 0usize;
for (i, run) in gated_idx.chunks(size).enumerate() {
report(PreparePhase::GatedFetch, *issued, Some(total_requests), key)?;
*issued += 1;
let mut sel = plan.selection.clone();
sel[plan.gated_pos] = AxisSel::Indices(run.to_vec());
let part = prov
.sample_with_selection(&sel)
.map_err(|e| err(format!("gated fetch '{key}' (batch {i}): {}", e.0)))?;
let part = drop_fixed_axes(key, part, &plan.drop_axes)?;
let width = part.shape().get(plan.gated_pos_out).copied().unwrap_or(0);
if width != run.len() {
return Err(err(format!(
"gated provider '{key}': batch {i} asked for {} indices and got \
{width} back",
run.len()
)));
}
let dst = out.get_or_insert_with(|| {
let mut shape = part.shape().to_vec();
shape[plan.gated_pos_out] = gated_idx.len();
ArrayD::zeros(IxDyn(&shape))
});
dst.slice_axis_mut(
Axis(plan.gated_pos_out),
Slice::from(filled..filled + width),
)
.assign(&part);
filled += width;
}
out.ok_or_else(|| err(format!("gated provider '{key}': no batches were fetched")))
}
fn drop_fixed_axes(
key: &str,
arr: ArrayD<f64>,
drop_axes: &[usize],
) -> Result<ArrayD<f64>, PrepareError> {
if drop_axes.is_empty() {
return Ok(arr);
}
let shape = arr.shape().to_vec();
for &d in drop_axes {
if shape.get(d).copied() != Some(1) {
return Err(err(format!(
"gated provider '{key}': fixed axis {d} came back with length \
{:?} (expected 1) in shape {shape:?}",
shape.get(d)
)));
}
}
let out: Vec<usize> = shape
.iter()
.enumerate()
.filter(|(i, _)| !drop_axes.contains(i))
.map(|(_, &s)| s)
.collect();
arr.into_shape_with_order(IxDyn(&out)).map_err(|e| {
err(format!(
"gated provider '{key}': reshape after fixed-axis drop: {e}"
))
})
}
pub(crate) fn slice_whole(full: ArrayD<f64>, selection: &[AxisSel]) -> ArrayD<f64> {
let mut arr = full;
for (i, ax) in selection.iter().enumerate() {
if matches!(ax, AxisSel::All) {
continue;
}
let dim_len = arr.shape().get(i).copied().unwrap_or(0);
let idx = ax.indices(dim_len);
arr = arr.select(Axis(i), &idx);
}
arr
}
fn has_template_import_edge(raw: &JsonValue) -> bool {
let Some(obj) = raw.as_object() else {
return false;
};
if obj.contains_key("expression_template_imports") {
return true;
}
["models", "reaction_systems"].iter().any(|kind| {
obj.get(*kind)
.and_then(JsonValue::as_object)
.is_some_and(|comps| {
comps.values().any(|c| {
c.as_object()
.is_some_and(|c| c.contains_key("expression_template_imports"))
})
})
})
}
pub(crate) fn run_prepare(
doc: &JsonValue,
const_arrays: HashMap<String, ArrayD<f64>>,
providers: Vec<(String, Box<dyn PrepareProvider>)>,
opts: &PrepareOptions,
) -> Result<PreparedBuild, PrepareError> {
let mut providers = providers;
let mut metaparameters = opts.metaparameters.clone();
let discovered = discover_loader_extents(&mut providers, opts, &mut metaparameters)?;
let resolved = resolve_pushdown_imports(doc, opts, &metaparameters)?;
let doc: &JsonValue = resolved.as_ref().unwrap_or(doc);
let provider_keys: Vec<String> = providers.iter().map(|(k, _)| k.clone()).collect();
let rw = apply_pushdown_rewrite(doc, opts, &provider_keys)?;
check_discovered_gate_conflict(&discovered, &rw.pd_gates)?;
let mut st = BuildState::load_document(rw.rewritten.as_ref(), opts, metaparameters)?;
let gated = st.inject_providers(const_arrays, providers, discovered, &rw.pd_gates)?;
st.alias_pushdown_names(&rw.pd_coupling);
st.bind_inline_parameters(&gated);
st.seal_const_registry();
st.check_free_names()?;
st.collect_observed_defs()?;
st.eval_buildtime_coordinates()?;
if st.refused.is_some() {
return Ok(st.finish(rw.rewritten.into_owned()));
}
st.invent_values()?;
st.feed_member_factors()?;
st.eval_observeds_before_fetch(&gated, &rw.pd_coupling)?;
if st.refused.is_some() {
return Ok(st.finish(rw.rewritten.into_owned()));
}
st.fetch_gated_providers(gated, &rw.pd_coupling)?;
st.eval_observed_graph()?;
Ok(st.finish(rw.rewritten.into_owned()))
}
fn vlog(opts: &PrepareOptions, msg: &str) {
if opts.verbose {
println!("{msg}");
}
}
fn report_progress(
opts: &PrepareOptions,
phase: PreparePhase,
done: usize,
total: Option<usize>,
item: &str,
) -> Result<(), PrepareError> {
let Some(cb) = &opts.progress else {
return Ok(());
};
let p = PrepareProgress {
phase,
done,
total,
item,
};
match cb(&p) {
Flow::Continue => Ok(()),
Flow::Cancel => Err(err(format!(
"{} {phase}{}",
PrepareError::CANCELLED_PREFIX,
if item.is_empty() {
match total {
Some(t) => format!(" ({done} of {t} done)"),
None => String::new(),
}
} else {
match total {
Some(t) => format!(" at '{item}' ({} of {t})", done + 1),
None => format!(" at '{item}'"),
}
}
))),
}
}
fn discover_loader_extents(
providers: &mut [(String, Box<dyn PrepareProvider>)],
opts: &PrepareOptions,
metaparameters: &mut BTreeMap<String, i64>,
) -> Result<HashMap<String, ArrayD<f64>>, PrepareError> {
let mut discovered: HashMap<String, ArrayD<f64>> = HashMap::new();
let mut discovered_by: BTreeMap<String, (i64, String)> = BTreeMap::new();
for (key, prov) in providers.iter_mut() {
let Some(mp) = prov.extent_metaparameter() else {
continue;
};
if prov.gate_spec().is_some() {
return Err(err(format!(
"provider '{key}' both GATES on a derived index set and declares the \
extent metaparameter '{mp}'; a gated slab's extent is the gating \
set's, not a discovered one"
)));
}
let a = prov
.sample()
.map_err(|e| err(format!("extent discovery for '{key}': {}", e.0)))?;
let n = a.shape().first().copied().unwrap_or(0) as i64;
if let Some((prev, prev_key)) = discovered_by.get(&mp)
&& *prev != n
{
return Err(err(format!(
"loader extent '{mp}' is {prev} from provider '{prev_key}' but {n} from \
'{key}' — the loader's variables are not aligned on one record axis"
)));
}
if let Some(bound) = metaparameters.get(&mp)
&& *bound != n
&& !discovered_by.contains_key(&mp)
{
return Err(err(format!(
"metaparameter '{mp}' was closed at {bound} by the caller but provider \
'{key}' discovers {n} records; drop the binding and let the loader \
declare its own extent"
)));
}
vlog(opts, &format!(" [prepare] extent {mp} <- {key} = {n}"));
discovered_by.insert(mp.clone(), (n, key.clone()));
metaparameters.insert(mp, n);
discovered.insert(key.clone(), a);
}
Ok(discovered)
}
fn resolve_pushdown_imports(
doc: &JsonValue,
opts: &PrepareOptions,
metaparameters: &BTreeMap<String, i64>,
) -> Result<Option<JsonValue>, PrepareError> {
report_progress(opts, PreparePhase::Rewrite, 0, None, "")?;
if opts.pushdown_rewrite && has_template_import_edge(doc) {
resolve_template_machinery(
doc,
opts.base_path
.clone()
.unwrap_or_else(|| PathBuf::from("."))
.as_path(),
metaparameters,
)
.map_err(|e| err(format!("resolve template imports: {}", e.message)))
} else {
Ok(None)
}
}
struct RewriteOutcome<'a> {
rewritten: std::borrow::Cow<'a, JsonValue>,
pd_gates: HashMap<String, ProviderGate>,
pd_coupling: Vec<(String, String)>,
}
fn apply_pushdown_rewrite<'a>(
doc: &'a JsonValue,
opts: &PrepareOptions,
provider_keys: &[String],
) -> Result<RewriteOutcome<'a>, PrepareError> {
let rewritten = if opts.pushdown_rewrite {
desugar_pushdown(doc, opts.model_name.as_deref()).map_err(|e| err(e.0))?
} else {
std::borrow::Cow::Borrowed(doc)
};
let rewrite_fired = matches!(rewritten, std::borrow::Cow::Owned(_));
let pd_gates: HashMap<String, ProviderGate> = if rewrite_fired {
pushdown_provider_gates(&rewritten, provider_keys).map_err(|e| err(e.0))?
} else {
HashMap::new()
};
let pd_coupling = pushdown_coupling_pairs(&rewritten);
vlog(
opts,
&format!(
" [prepare] pushdown rewrite {}",
if rewrite_fired {
"fired"
} else {
"did not fire"
}
),
);
Ok(RewriteOutcome {
rewritten,
pd_gates,
pd_coupling,
})
}
fn check_discovered_gate_conflict(
discovered: &HashMap<String, ArrayD<f64>>,
pd_gates: &HashMap<String, ProviderGate>,
) -> Result<(), PrepareError> {
for key in discovered.keys() {
if pd_gates.contains_key(key) {
return Err(err(format!(
"provider '{key}' both GATES on a derived index set and declares an \
extent metaparameter; a gated slab's extent is the gating set's, not \
a discovered one"
)));
}
}
Ok(())
}
struct BuildState<'o> {
opts: &'o PrepareOptions,
model: Model,
model_name: String,
index_sets: HashMap<String, IndexSet>,
param_vals: Vec<f64>,
param_names: Vec<String>,
arrays: ArrMap,
const_scope: ConstArrayScope,
defs: HashMap<String, Expr>,
join_free: HashSet<String>,
order: Vec<String>,
fields: HashMap<String, ArrayD<f64>>,
members: HashMap<String, Vec<i64>>,
tuple_keyed: HashSet<String>,
extents: HashMap<String, i64>,
gated_keys: Vec<String>,
rules: Vec<PipelineRule>,
refused: Option<PipelineRule>,
evaluated: HashSet<String>,
obs_done: usize,
obs_total: usize,
}
impl<'o> BuildState<'o> {
fn log(&self, msg: &str) {
vlog(self.opts, msg);
}
fn report(
&self,
phase: PreparePhase,
done: usize,
total: Option<usize>,
item: &str,
) -> Result<(), PrepareError> {
report_progress(self.opts, phase, done, total, item)
}
fn load_document(
rewritten: &JsonValue,
opts: &'o PrepareOptions,
metaparameters: BTreeMap<String, i64>,
) -> Result<Self, PrepareError> {
report_progress(opts, PreparePhase::Load, 0, None, "")?;
let text = serde_json::to_string(rewritten)
.map_err(|e| err(format!("serialize rewritten document: {e}")))?;
let metaparameters = if document_has_unresolved_mount(rewritten) {
metaparameters
} else {
let declared = rewritten.get("metaparameters").and_then(|v| v.as_object());
metaparameters
.into_iter()
.filter(|(k, _)| declared.is_some_and(|d| d.contains_key(k)))
.collect()
};
let load_opts = LoadOptions {
base_path: opts.base_path.clone(),
metaparameters,
};
let mut file = crate::parse::load_string_with_options(&text, &load_opts)
.map_err(|e| err(format!("load rewritten document: {e}")))?;
crate::lower_table_lookup::lower_table_lookups(&mut file)
.map_err(|e| err(format!("lower table lookups: {e}")))?;
let mut index_sets: HashMap<String, IndexSet> = file
.index_sets
.clone()
.unwrap_or_default()
.into_iter()
.collect();
let models = file
.models
.as_ref()
.ok_or_else(|| err("document has no models"))?;
let model_name = match &opts.model_name {
Some(n) => n.clone(),
None if models.len() == 1 => models.keys().next().unwrap().clone(),
None => return Err(err("document holds several models; pass model_name")),
};
let mut model = models
.get(&model_name)
.ok_or_else(|| err(format!("model '{model_name}' not found in the document")))?
.clone();
crate::simulate_array::mount_subsystems(&mut model, &mut index_sets)
.map_err(|e| err(format!("model '{model_name}': {e}")))?;
crate::simulate_array::resolve_model_self_references(&mut model, &model_name);
crate::simulate_array::apply_flatten_rewrites(&mut model)
.map_err(|e| err(format!("model '{model_name}': {e}")))?;
let (param_vals, param_names) = scalar_params(&model, &model_name, &opts.parameters);
Ok(BuildState {
opts,
model,
model_name,
index_sets,
param_vals,
param_names,
arrays: ArrMap::default(),
const_scope: ConstArrayScope::default(),
defs: HashMap::new(),
join_free: HashSet::new(),
order: Vec::new(),
fields: HashMap::new(),
members: HashMap::new(),
tuple_keyed: HashSet::new(),
extents: HashMap::new(),
gated_keys: Vec::new(),
rules: Vec::new(),
refused: None,
evaluated: HashSet::new(),
obs_done: 0,
obs_total: 0,
})
}
fn record(&mut self, rule: PipelineRule) -> bool {
let stop = self.opts.refuse_per_cell && matches!(rule.route, Route::PerCell(_));
if stop {
self.refused = Some(rule.clone());
}
self.rules.push(rule);
stop
}
fn inject_providers(
&mut self,
const_arrays: HashMap<String, ArrayD<f64>>,
providers: Vec<(String, Box<dyn PrepareProvider>)>,
mut discovered: HashMap<String, ArrayD<f64>>,
pd_gates: &HashMap<String, ProviderGate>,
) -> Result<Vec<GatedProvider>, PrepareError> {
self.arrays = const_arrays
.into_iter()
.map(|(k, a)| {
let rounded = round_external(&k, a);
(k, rounded)
})
.collect();
let mut gated: Vec<GatedProvider> = Vec::new();
let n_providers = providers.len();
for (i, (k, mut prov)) in providers.into_iter().enumerate() {
self.report(
PreparePhase::ConstProviders,
i,
Some(n_providers),
k.as_str(),
)?;
if let Some(a) = discovered.remove(&k) {
self.arrays.insert(k.clone(), round_external(&k, a));
} else if let Some(gate) = pd_gates.get(&k) {
gated.push((k, prov, gate.clone()));
} else if let Some(gate) = prov.gate_spec() {
gated.push((k, prov, gate));
} else if !prov.is_const() {
return Err(err(format!(
"prepare: provider '{k}' is DISCRETE (non-empty refresh times); \
prepare() is build-time-only"
)));
} else {
let a = prov
.sample()
.map_err(|e| err(format!("materialize const provider '{k}': {}", e.0)))?;
self.log(&format!(
" [prepare] const provider {k} -> {:?}",
a.shape()
));
self.arrays.insert(k.clone(), round_external(&k, a));
}
}
self.report(
PreparePhase::ConstProviders,
n_providers,
Some(n_providers),
"",
)?;
self.gated_keys = gated.iter().map(|(k, _, _)| k.clone()).collect();
self.gated_keys.sort();
Ok(gated)
}
fn alias_pushdown_names(&mut self, pd_coupling: &[(String, String)]) {
inject_aliases(&mut self.arrays, pd_coupling);
}
fn bind_inline_parameters(&mut self, gated: &[GatedProvider]) {
for (name, a) in
crate::simulate_array::inline_parameter_arrays(&self.model, &self.index_sets)
{
let gated_here = gated.iter().any(|(k, _, g)| {
k.rsplit('.').next() == Some(name.as_str()) || g.applies_to.contains(&name)
});
if !gated_here && !self.arrays.contains_key(&name) {
self.arrays.insert(name, a);
}
}
}
fn seal_const_registry(&mut self) {
self.const_scope = ConstArrayScope::from_names(self.arrays.keys().cloned());
}
fn check_free_names(&self) -> Result<(), PrepareError> {
let mut extra: Vec<String> = self.arrays.keys().cloned().collect();
extra.extend(self.param_names.iter().cloned());
extra.extend(
self.index_sets
.values()
.filter_map(|is| is.member_factor.clone()),
);
crate::simulate_array::check_free_variables(&self.model, &self.index_sets, &extra)
.map_err(|e| match e {
crate::compile_error::CompileError::InterpreterBuildError { details } => {
err(details)
}
other => err(other.to_string()),
})
}
fn collect_observed_defs(&mut self) -> Result<(), PrepareError> {
let mut defs = observed_defs(&self.model);
let state_dependent = state_dependent_observeds(&self.model, &defs);
{
let var_shapes = crate::join::declared_var_shapes(&self.model);
for e in defs.values_mut() {
crate::join::resolve_overlap_syms_expr(e, &var_shapes);
crate::join::resolve_expr_joins(e, &self.index_sets, &var_shapes)
.map_err(|e| err(format!("resolve join: {e}")))?;
}
}
self.join_free = defs
.iter()
.filter(|(_, e)| match e {
Expr::Operator(node) => node.join.as_ref().map(|j| j.is_empty()).unwrap_or(true),
_ => true,
})
.map(|(n, _)| n.clone())
.collect();
self.order = observed_order(&defs)?;
self.order
.retain(|name| !state_dependent.contains(name.as_str()));
self.defs = defs;
Ok(())
}
fn eval_buildtime_coordinates(&mut self) -> Result<(), PrepareError> {
let seeds: Vec<&Expr> = self
.model
.equations
.iter()
.filter_map(|eq| match &eq.rhs {
Expr::Operator(node)
if node.op.as_str() == "faq"
&& node.distinct == Some(true)
&& node.join.as_ref().map(|j| !j.is_empty()).unwrap_or(false) =>
{
Some(&eq.rhs)
}
_ => None,
})
.collect();
if seeds.is_empty() {
return Ok(());
}
let needed = producer_seed_closure(&seeds, &self.defs, &self.join_free);
let no_extents: HashMap<String, i64> = HashMap::new();
let coords: Vec<String> = self
.order
.iter()
.filter(|n| needed.contains(*n))
.cloned()
.collect();
let n_coords = coords.len();
for (i, name) in coords.into_iter().enumerate() {
self.report(PreparePhase::Coordinates, i, Some(n_coords), name.as_str())?;
match eval_observed(
&name,
&self.defs[&name],
&self.arrays,
&self.param_vals,
&self.param_names,
&self.index_sets,
&no_extents,
&self.const_scope,
DeclaredRank::of(&self.model, &name),
self.opts.refuse_per_cell,
) {
Ok(Evaluated::Refused(why)) => {
self.record(PipelineRule {
name,
kind: "build-time observed",
route: Route::PerCell(why),
});
return Ok(());
}
Ok(Evaluated::Field(a, route)) => {
self.log(&format!(
" [prepare] build-time coordinate {name} -> {:?}",
a.shape()
));
if self.record(PipelineRule {
name: name.clone(),
kind: "build-time observed",
route,
}) {
return Ok(());
}
self.arrays.insert(name.clone(), a.clone());
self.fields.insert(name, a);
}
Err(e) => {
self.log(&format!(
" [prepare] build-time coordinate {name} skipped ({})",
e.0
));
}
}
}
self.report(PreparePhase::Coordinates, n_coords, Some(n_coords), "")?;
Ok(())
}
fn invent_values(&mut self) -> Result<(), PrepareError> {
self.report(PreparePhase::ValueInvention, 0, None, "")?;
let vi = run_value_invention_with_params(
&self.model,
&self.index_sets,
Some(&self.arrays),
&self.opts.parameters,
)
.map_err(|e| err(format!("value invention: {e}")))?;
let mut members: HashMap<String, Vec<i64>> = HashMap::new();
let mut tuple_keyed: HashSet<String> = HashSet::new();
for (faq, mem) in &vi.members {
let ids: Option<Vec<i64>> = mem
.iter()
.map(|k| match k {
crate::relational::Key::Int(i) => Some(*i),
_ => None,
})
.collect();
match ids {
Some(ids) => {
members.insert(faq.clone(), ids);
}
None => {
tuple_keyed.insert(faq.clone());
}
}
}
let mut producers: Vec<&String> = members.keys().chain(tuple_keyed.iter()).collect();
producers.sort();
for faq in producers {
self.rules.push(PipelineRule {
name: faq.clone(),
kind: "value invention",
route: Route::Relational,
});
}
self.members = members;
self.tuple_keyed = tuple_keyed;
self.extents = vi.extents.clone();
let buffers = vi
.assignments
.iter()
.map(|(n, b)| (n, b.iter().map(|&i| i as f64).collect::<Vec<f64>>()))
.chain(vi.groups.iter().map(|(n, b)| (n, b.clone())));
for (name, values) in buffers {
let n = values.len();
let a = ArrayD::from_shape_vec(ndarray::IxDyn(&[n]), values)
.map_err(|e| err(format!("value-invention buffer '{name}': {e}")))?;
self.arrays.insert(name.clone(), a.clone());
self.fields.insert(name.clone(), a);
}
Ok(())
}
fn feed_member_factors(&mut self) -> Result<(), PrepareError> {
let mut tuple_fed: Vec<&String> = self
.index_sets
.iter()
.filter(|(_, is)| is.kind == "derived" && is.member_factor.is_some())
.filter(|(_, is)| {
is.from_faq
.as_ref()
.is_some_and(|f| self.tuple_keyed.contains(f))
})
.map(|(sname, _)| sname)
.collect();
tuple_fed.sort();
if let Some(sname) = tuple_fed.first() {
return Err(err(format!(
"derived index set '{sname}' names a member_factor, but its producer's members \
are tuples, not scalar member ids"
)));
}
let mut mf_sets: Vec<(&String, &String, &Vec<i64>)> = self
.index_sets
.iter()
.filter(|(_, is)| is.kind == "derived")
.filter_map(|(sname, is)| {
let (mf, faq) = (is.member_factor.as_ref()?, is.from_faq.as_ref()?);
Some((sname, mf, self.members.get(faq)?))
})
.collect();
mf_sets.sort_by(|a, b| a.0.cmp(b.0));
let n_mf = mf_sets.len();
for (i, (sname, mf, mem)) in mf_sets.into_iter().enumerate() {
self.report(PreparePhase::MemberFactors, i, Some(n_mf), mf.as_str())?;
let v: Vec<f64> = mem.iter().map(|&m| m as f64).collect();
self.log(&format!(
" [prepare] member_factor {mf} <- |{sname}| = {}",
v.len()
));
let arr = ArrayD::from_shape_vec(IxDyn(&[v.len()]), v)
.map_err(|e| err(format!("member factor '{mf}': {e}")))?;
self.arrays.insert(mf.clone(), arr);
}
self.report(PreparePhase::MemberFactors, n_mf, Some(n_mf), "")?;
Ok(())
}
fn fetch_gated_providers(
&mut self,
gated: Vec<GatedProvider>,
pd_coupling: &[(String, String)],
) -> Result<(), PrepareError> {
let mut plans: Vec<GatedFetchPlan> = Vec::with_capacity(gated.len());
for (key, prov, gate) in &gated {
if gate.applies_to.len() != 1 {
return Err(err(format!(
"gated provider '{key}': applies_to lists {} variables; bind one \
provider per variable (providers[\"<ModelPath>.<param>\"]) so each gated \
fetch is a single field",
gate.applies_to.len()
)));
}
let mut plan =
gated_fetch_plan(key, gate, &self.index_sets, &self.members, &self.extents)?;
plan.n_requests = if prov.supports_selection() {
match &plan.selection[plan.gated_pos] {
AxisSel::Indices(idx) => idx
.len()
.div_ceil(self.opts.gated_fetch_batch.unwrap_or(usize::MAX).max(1))
.max(1),
AxisSel::All | AxisSel::Range { .. } => 1,
}
} else {
1
};
plans.push(plan);
}
let total_requests: usize = plans.iter().map(|p| p.n_requests).sum();
let mut issued = 0usize;
let opts = self.opts;
let mut report = |phase: PreparePhase, done: usize, total: Option<usize>, item: &str| {
report_progress(opts, phase, done, total, item)
};
for ((key, mut prov, gate), plan) in gated.into_iter().zip(plans) {
let arr = run_gated_fetch(
&key,
prov.as_mut(),
&plan,
opts.gated_fetch_batch,
&mut issued,
total_requests,
&mut report,
)?;
if arr.shape().get(plan.gated_pos_out).copied() != Some(plan.gated_extent) {
return Err(err(format!(
"gated provider '{key}': fetched compact axis is {:?} but the gating \
set extent is {}",
arr.shape().get(plan.gated_pos_out),
plan.gated_extent
)));
}
let target = gated_target(&key, &gate, pd_coupling);
self.log(&format!(
" [prepare] gated fetch {key} -> {target} {:?}",
arr.shape()
));
self.arrays
.insert(target.clone(), round_external(&target, arr));
}
self.report(
PreparePhase::GatedFetch,
total_requests,
Some(total_requests),
"",
)?;
Ok(())
}
fn eval_observeds_before_fetch(
&mut self,
gated: &[GatedProvider],
pd_coupling: &[(String, String)],
) -> Result<(), PrepareError> {
if gated.is_empty() || gated.iter().any(|(_, _, g)| g.applies_to.len() != 1) {
return Ok(());
}
let mut after: HashSet<String> = gated
.iter()
.map(|(key, _, gate)| gated_target(key, gate, pd_coupling))
.collect();
let seeds = after.clone();
for name in &self.order {
let mut reads = HashSet::new();
declared_args(&self.defs[name], &mut reads);
join_names(&self.defs[name], &mut reads);
if reads.iter().any(|r| after.contains(r)) {
after.insert(name.clone());
}
}
for s in &seeds {
after.remove(s);
}
self.eval_observeds(&after, false)
}
fn eval_observed_graph(&mut self) -> Result<(), PrepareError> {
self.eval_observeds(&HashSet::new(), true)
}
fn eval_observeds(&mut self, skip: &HashSet<String>, last: bool) -> Result<(), PrepareError> {
let pending: Vec<String> = self
.order
.iter()
.filter(|n| !self.fields.contains_key(*n))
.cloned()
.collect();
if self.obs_done == 0 {
self.obs_total = pending.len();
}
let n_pending = self.obs_total;
for name in pending.iter() {
if skip.contains(name) || self.evaluated.contains(name) {
continue;
}
self.report(
PreparePhase::Observeds,
self.obs_done,
Some(n_pending),
name.as_str(),
)?;
self.obs_done += 1;
self.evaluated.insert(name.clone());
let t = std::time::Instant::now();
let (a, route) = match eval_observed(
name,
&self.defs[name],
&self.arrays,
&self.param_vals,
&self.param_names,
&self.index_sets,
&self.extents,
&self.const_scope,
DeclaredRank::of(&self.model, name),
self.opts.refuse_per_cell,
)? {
Evaluated::Field(a, route) => (a, route),
Evaluated::Refused(why) => {
self.record(PipelineRule {
name: name.clone(),
kind: "build-time observed",
route: Route::PerCell(why),
});
break;
}
};
self.log(&format!(
" [prepare] {name:<24} shape={:?} {:>7.1} s",
a.shape(),
t.elapsed().as_secs_f64()
));
if self.record(PipelineRule {
name: name.clone(),
kind: "build-time observed",
route,
}) {
break;
}
self.arrays.insert(name.clone(), a);
}
if !last && self.refused.is_none() {
return Ok(());
}
for name in &pending {
if let Some(a) = self.arrays.remove(name) {
self.fields.insert(name.clone(), a);
}
}
if self.refused.is_some() {
return Ok(());
}
self.report(PreparePhase::Observeds, n_pending, Some(n_pending), "")?;
Ok(())
}
fn finish(self, doc: JsonValue) -> PreparedBuild {
PreparedBuild {
doc,
model_name: self.model_name,
fields: self.fields,
members: self.members,
extents: self.extents,
gated_provider_keys: self.gated_keys,
rules: self.rules,
refused: self.refused,
}
}
}
pub(crate) fn run_build_pipeline(
doc: &JsonValue,
opts: &mut crate::problem::ProblemOptions,
) -> Result<PreparedBuild, crate::simulate::SimulateError> {
let internal = PrepareOptions {
model_name: opts.model_name.clone(),
metaparameters: opts.metaparameters.clone(),
base_path: opts.base_path.clone(),
pushdown_rewrite: opts.pushdown_rewrite,
parameters: opts.p.clone(),
verbose: opts.verbose,
progress: opts.progress.clone(),
gated_fetch_batch: opts.gated_fetch_batch,
refuse_per_cell: opts.compiler.unwrap_or_default().is_strict(),
};
let const_arrays = std::mem::take(&mut opts.const_arrays);
let providers = std::mem::take(&mut opts.build_providers);
run_prepare(doc, const_arrays, providers, &internal).map_err(|e| {
if e.is_cancelled() {
crate::simulate::SimulateError::Cancelled { details: e.0 }
} else {
crate::simulate::SimulateError::Compile(
crate::compile_error::CompileError::InterpreterBuildError { details: e.0 },
)
}
})
}
fn document_has_unresolved_mount(doc: &JsonValue) -> bool {
let Some(obj) = doc.as_object() else {
return false;
};
for kind in ["models", "reaction_systems"] {
let Some(comps) = obj.get(kind).and_then(|v| v.as_object()) else {
continue;
};
for comp in comps.values() {
if comp.get("ref").is_some() {
return true;
}
if let Some(subs) = comp.get("subsystems").and_then(|v| v.as_object())
&& subs.values().any(|s| s.get("ref").is_some())
{
return true;
}
}
}
false
}
#[cfg(test)]
mod first_cell_stop_tests {
use crate::compile_error::CompileError;
use crate::problem::{Compiler, ProblemOptions, esm_problem};
use crate::simulate::SimulateError;
use crate::simulate_array::per_cell_cells;
use serde_json::{Value, json};
fn doc(n: usize, variables: Value, equations: Value) -> Value {
json!({
"esm": "1.1.0",
"metadata": {"name": "FirstCellStop"},
"index_sets": {"rows": {"kind": "interval", "size": n}},
"models": {"Rel": {"variables": variables, "equations": equations}},
})
}
fn cumulative(n: usize) -> Value {
doc(
n,
json!({
"left": {"type": "unknown", "units": "1", "shape": ["rows"]},
"cum": {"type": "unknown", "units": "1", "shape": ["rows"]},
}),
json!([
{"lhs": "left",
"rhs": {"op": "faq", "args": [], "output_idx": ["i"],
"ranges": {"i": {"from": "rows"}},
"expr": {"op": "*", "args": [10, "i"]}}},
{"lhs": "cum",
"rhs": {"op": "faq", "args": [], "output_idx": ["i"],
"ranges": {"i": {"from": "rows"}, "j": {"from": "rows"}},
"filter": {"op": "<=", "args": ["j", "i"]},
"expr": {"op": "index", "args": ["left", "j"]}}},
]),
)
}
fn recurrence(n: usize) -> Value {
doc(
n,
json!({"s": {"type": "unknown", "units": "1", "shape": ["rows"]}}),
json!([
{"lhs": "s",
"rhs": {"op": "faq", "args": [], "output_idx": ["k"],
"ranges": {"k": {"from": "rows"}},
"expr": {"op": "ifelse", "args": [
{"op": "<=", "args": ["k", 1]},
1.0,
{"op": "+", "args": [
{"op": "index", "args": ["s", {"op": "-", "args": ["k", 1]}]},
1.0]}]}}},
]),
)
}
fn build(d: &Value, compiler: Compiler) -> Result<crate::problem::EsmProblem, SimulateError> {
esm_problem(
d,
(0.0, 1.0),
ProblemOptions {
compiler: Some(compiler),
build_pipeline: true,
..Default::default()
},
)
}
const LARGE: usize = 200_000;
fn native_cells(d: &Value) -> (crate::problem::EsmProblem, u64) {
let before = per_cell_cells();
let prob = build(d, Compiler::Native).expect("native builds it on the tape");
(prob, per_cell_cells() - before)
}
fn bits(p: &crate::problem::EsmProblem, name: &str) -> Vec<u64> {
crate::problem::observed_field(p, name)
.expect("field")
.iter()
.map(|x| x.to_bits())
.collect()
}
#[test]
fn a_large_prefix_scan_is_taped_after_at_most_one_walked_cell() {
let (native, cells) = native_cells(&cumulative(LARGE));
assert!(
cells <= 1,
"the walk must stop at its first cell, walked {cells}"
);
let before = per_cell_cells();
let prob = build(&cumulative(LARGE), Compiler::Interpreter).expect("interpreter");
assert_eq!(per_cell_cells() - before, LARGE as u64);
let cum = crate::problem::observed_field(&prob, "Rel.cum").expect("cum");
let n = LARGE as f64;
assert_eq!(cum.iter().last().copied(), Some(5.0 * n * (n + 1.0)));
assert_eq!(bits(&native, "Rel.cum"), bits(&prob, "Rel.cum"));
}
#[test]
fn a_large_recurrence_is_taped_without_a_walked_cell() {
let (native, cells) = native_cells(&recurrence(LARGE));
assert_eq!(cells, 0, "a recurrence goes straight to the tape's sweep");
let before = per_cell_cells();
let prob = build(&recurrence(LARGE), Compiler::Interpreter).expect("interpreter");
assert_eq!(per_cell_cells() - before, LARGE as u64);
let s = crate::problem::observed_field(&prob, "Rel.s").expect("s");
assert_eq!(s.iter().last().copied(), Some(LARGE as f64));
assert_eq!(bits(&native, "Rel.s"), bits(&prob, "Rel.s"));
}
fn binning(
n: usize,
) -> (
Value,
std::collections::HashMap<String, ndarray::ArrayD<f64>>,
) {
let d = json!({
"esm": "1.1.0",
"metadata": {"name": "FirstCellStopCoordinates"},
"index_sets": {
"records": {"kind": "interval", "size": n},
"cells": {"kind": "interval", "size": n},
"occupied": {"kind": "derived", "from_faq": "bins"}
},
"models": {"Rel": {
"variables": {
"step": {"type": "parameter", "units": "1", "shape": ["records"]},
"Y": {"type": "parameter", "units": "1", "shape": ["records"]},
"W": {"type": "parameter", "units": "1", "shape": ["cells"]},
"S": {"type": "parameter", "units": "1", "shape": ["cells"]},
"E": {"type": "parameter", "units": "1", "shape": ["cells"]},
"N": {"type": "parameter", "units": "1", "shape": ["cells"]},
"X": {"type": "unknown", "units": "1", "shape": ["records"]},
"present": {"type": "unknown", "shape": ["occupied"]}
},
"equations": [
{"lhs": "X",
"rhs": {"op": "faq", "args": [], "output_idx": ["r"],
"ranges": {"r": {"from": "records"}, "q": {"from": "records"}},
"filter": {"op": "<=", "args": ["q", "r"]},
"expr": {"op": "index", "args": ["step", "q"]}}},
{"lhs": {"op": "index", "args": ["present", "e"]},
"rhs": {"op": "faq", "id": "bins",
"args": ["X", "Y", "W", "S", "E", "N"],
"semiring": "bool_and_or", "distinct": true,
"output_idx": ["e"],
"ranges": {"r": {"from": "records"}, "c": {"from": "cells"}},
"join": [{"overlap": {"src_env": ["X", "Y"],
"tgt_env": ["W", "S", "E", "N"],
"eps": 0.0}}],
"key": {"op": "skolem", "label": "cell", "args": ["c"]},
"expr": {"op": "true", "args": []}}}
]
}}
});
let col = |f: &dyn Fn(usize) -> f64| {
ndarray::ArrayD::from_shape_vec(ndarray::IxDyn(&[n]), (0..n).map(f).collect())
.expect("shape")
};
let arrays = [
("step", col(&|k| if k == 0 { 0.5 } else { 1.0 })),
("Y", col(&|_| 0.5)),
("W", col(&|k| k as f64)),
("S", col(&|_| 0.0)),
("E", col(&|k| k as f64 + 1.0)),
("N", col(&|_| 1.0)),
]
.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect();
(d, arrays)
}
#[test]
fn a_large_per_cell_coordinate_is_taped_after_at_most_one_walked_cell() {
let run = |n: usize, compiler: Compiler| {
let (d, const_arrays) = binning(n);
let before = per_cell_cells();
let r = esm_problem(
&d,
(0.0, 1.0),
ProblemOptions {
compiler: Some(compiler),
build_pipeline: true,
const_arrays,
..Default::default()
},
);
(r, per_cell_cells() - before)
};
let (native, cells) = run(LARGE, Compiler::Native);
let native = native.expect("native builds it on the tape");
assert!(
cells <= 1,
"the coordinate pass must stop at its first cell, walked {cells}"
);
assert_eq!(native.members()["bins"].len(), LARGE, "one record per cell");
let (interp, cells) = run(1000, Compiler::Interpreter);
let prob = interp.expect("the interpreter builds it");
assert_eq!(cells, 1000);
assert_eq!(prob.members()["bins"].len(), 1000, "one record per cell");
let (native_small, _) = run(1000, Compiler::Native);
let native_small = native_small.expect("native");
assert_eq!(native_small.members()["bins"], prob.members()["bins"]);
}
#[test]
fn an_error_in_the_first_cell_is_not_a_refusal() {
let d = doc(
3,
json!({
"table": {"type": "parameter", "units": "1", "shape": ["rows"]},
"cum": {"type": "unknown", "units": "1", "shape": ["rows"]},
}),
json!([
{"lhs": "cum",
"rhs": {"op": "faq", "args": [], "output_idx": ["i"],
"ranges": {"i": {"from": "rows"}, "j": {"from": "rows"}},
"filter": {"op": "<=", "args": ["j", "i"]},
"expr": {"op": "index", "args": ["table", {"op": "+", "args": ["j", 10]}]}}},
]),
);
for compiler in [Compiler::Native, Compiler::Interpreter] {
let table = ndarray::ArrayD::from_shape_vec(ndarray::IxDyn(&[3]), vec![1.0, 2.0, 3.0])
.expect("shape");
let err = esm_problem(
&d,
(0.0, 1.0),
ProblemOptions {
compiler: Some(compiler),
build_pipeline: true,
const_arrays: [("table".to_string(), table)].into_iter().collect(),
..Default::default()
},
)
.expect_err("an out-of-range gather");
assert!(
!matches!(
err,
SimulateError::Compile(CompileError::CompilerRefusedRule { .. })
),
"[{compiler}] {err}"
);
assert!(err.to_string().contains("OOB"), "[{compiler}] {err}");
}
}
#[test]
fn an_error_in_a_later_cell_is_reported_by_the_tape() {
let d = doc(
3,
json!({
"table": {"type": "parameter", "units": "1", "shape": ["rows"]},
"cum": {"type": "unknown", "units": "1", "shape": ["rows"]},
}),
json!([
{"lhs": "cum",
"rhs": {"op": "faq", "args": [], "output_idx": ["i"],
"ranges": {"i": {"from": "rows"}, "j": {"from": "rows"}},
"filter": {"op": "<=", "args": ["j", "i"]},
"expr": {"op": "index", "args": ["table", {"op": "+", "args": ["i", 1]}]}}},
]),
);
let run = |compiler| {
let table = ndarray::ArrayD::from_shape_vec(ndarray::IxDyn(&[3]), vec![1.0, 2.0, 3.0])
.expect("shape");
esm_problem(
&d,
(0.0, 1.0),
ProblemOptions {
compiler: Some(compiler),
build_pipeline: true,
const_arrays: [("table".to_string(), table)].into_iter().collect(),
..Default::default()
},
)
.expect_err("the last cell gathers out of range")
};
let native = run(Compiler::Native);
let interp = run(Compiler::Interpreter);
assert!(interp.to_string().contains("OOB"), "{interp}");
assert_eq!(native.to_string(), interp.to_string());
}
}
#[cfg(test)]
mod mounted_subsystem_tests {
use crate::problem::{Compiler, ProblemOptions, esm_problem, observed_field, solve};
use serde_json::{Value, json};
fn doc(name: &str, units: &str, rhs: Value) -> Value {
json!({
"esm": "1.1.0",
"metadata": {"name": "Mounted"},
"models": {"P": {
"variables": {
"c": {"type": "unknown", "units": "1", "default": 1.0},
name: {"type": "unknown", "units": units}
},
"equations": [
{"lhs": {"op": "D", "args": ["c"], "wrt": "t"},
"rhs": {"op": "-", "args": ["c"]}},
{"lhs": name, "rhs": rhs}
],
"subsystems": {"North": {
"variables": {
"u": {"type": "unknown", "units": "1", "default": 2.0},
"k": {"type": "parameter", "units": "1/s", "default": 0.5},
"theta": {"type": "parameter", "units": "deg", "default": 40.0},
"v": {"type": "unknown", "units": "1"}
},
"equations": [
{"lhs": {"op": "D", "args": ["u"], "wrt": "t"},
"rhs": "k"},
{"lhs": "v", "rhs": {"op": "*", "args": [2.0, "u"]}}
]
}}
}}
})
}
fn built(d: &Value, compiler: Compiler, build_pipeline: bool) -> crate::problem::EsmProblem {
let opts = ProblemOptions {
compiler: Some(compiler),
build_pipeline,
..Default::default()
};
let prob = if build_pipeline {
esm_problem(d, (0.0, 1.0), opts)
} else {
let file = crate::load_string(&d.to_string()).expect("loads");
esm_problem(&file, (0.0, 1.0), opts)
};
prob.unwrap_or_else(|e| panic!("[{compiler}, pipeline {build_pipeline}] {e}"))
}
fn at_t0(prob: &crate::problem::EsmProblem, name: &str) -> f64 {
let sol = solve(
prob,
&crate::SolveOptions {
saveat: Some(vec![0.0]),
output_observed: vec![name.to_string()],
..Default::default()
},
)
.unwrap_or_else(|e| panic!("{e}"));
sol.get(name)
.unwrap_or_else(|| panic!("no {name} row: {:?}", sol.state_variable_names))[0]
}
#[test]
fn a_degree_angle_in_a_subsystem_takes_radians() {
let d = doc("y", "1", json!({"op": "sin", "args": ["North.theta"]}));
let want = 40.0f64.to_radians().sin();
for compiler in [Compiler::Native, Compiler::Interpreter] {
for pipeline in [false, true] {
let prob = built(&d, compiler, pipeline);
assert_eq!(at_t0(&prob, "y"), want, "[{compiler}, pipeline {pipeline}]");
}
let prob = built(&d, compiler, true);
let y = observed_field(&prob, "y").expect("y");
assert_eq!(
y.iter().copied().collect::<Vec<_>>(),
[want],
"[{compiler}]"
);
}
}
#[test]
fn a_time_derivative_of_a_subsystem_state_resolves() {
let d = doc(
"z",
"1/s",
json!({"op": "D", "args": ["North.u"], "wrt": "t"}),
);
for compiler in [Compiler::Native, Compiler::Interpreter] {
for pipeline in [false, true] {
let prob = built(&d, compiler, pipeline);
assert_eq!(at_t0(&prob, "z"), 0.5, "[{compiler}, pipeline {pipeline}]");
}
let prob = built(&d, compiler, true);
let z = observed_field(&prob, "z").expect("z");
assert_eq!(z.iter().copied().collect::<Vec<_>>(), [0.5], "[{compiler}]");
}
}
#[test]
fn a_mounted_state_dependent_observed_is_evaluated_at_run_time() {
let d = doc("y", "1", json!({"op": "sin", "args": ["North.theta"]}));
for compiler in [Compiler::Native, Compiler::Interpreter] {
for pipeline in [false, true] {
let prob = built(&d, compiler, pipeline);
let sol = solve(
&prob,
&crate::SolveOptions {
saveat: Some(vec![0.0]),
output_observed: vec!["North.v".to_string()],
..Default::default()
},
)
.unwrap_or_else(|e| panic!("[{compiler}, pipeline {pipeline}] {e}"));
let v = sol
.get("P.North.v")
.unwrap_or_else(|| panic!("no North.v row: {:?}", sol.state_variable_names));
assert_eq!(v[0], 4.0, "[{compiler}, pipeline {pipeline}]");
}
}
}
#[test]
fn a_top_level_state_dependent_observed_is_evaluated_at_run_time() {
let d = doc("w", "1", json!({"op": "*", "args": [2.0, "c"]}));
for compiler in [Compiler::Native, Compiler::Interpreter] {
for pipeline in [false, true] {
let prob = built(&d, compiler, pipeline);
assert_eq!(at_t0(&prob, "w"), 2.0, "[{compiler}, pipeline {pipeline}]");
}
}
}
}