use super::*;
use crate::faq::{effective_reduce_kind, is_faq_op, validate_oplus_spellings};
use crate::flatten::FlattenedSystem;
use crate::op_registry::{OpError, is_builtin_function_name};
use crate::simulate::{CompileError, SimulateError};
use crate::types::{
EsmFile, ExpressionNode, JoinClause, Model, ModelVariable, OverlapClause, VariableType,
};
use crate::value_invention::{
ValueInventionResult, materialize_value_invention, rewrite_derived_index_sets,
};
use indexmap::IndexMap;
use serde_json::Value as JsonValue;
use std::collections::{BTreeMap, BTreeSet, HashSet};
pub(super) const ARRAY_OP_NAMES: &[&str] = &[
"faq", "makearray",
"reshape",
"transpose",
"concat",
"broadcast",
];
pub fn file_has_array_ops(file: &EsmFile) -> bool {
let Some(models) = &file.models else {
return false;
};
models
.values()
.any(|model| model_tree_any(model, &model_has_array_ops))
}
pub(crate) fn model_tree_any(model: &Model, pred: &dyn Fn(&Model) -> bool) -> bool {
if pred(model) {
return true;
}
model.subsystems.as_ref().is_some_and(|subs| {
subs.iter().any(|(name, value)| {
parse_subsystem_model(name, value).is_ok_and(|(sub, _)| model_tree_any(&sub, pred))
})
})
}
pub fn file_has_spatial_model(file: &EsmFile) -> bool {
let Some(models) = &file.models else {
return false;
};
let has_shaped_variable = |model: &Model| {
model
.variables
.values()
.any(|var| var.shape.as_ref().is_some_and(|shape| !shape.is_empty()))
};
models
.values()
.any(|model| model_tree_any(model, &has_shaped_variable))
}
pub(super) fn model_has_array_ops(model: &Model) -> bool {
for eq in &model.equations {
if expr_has_array_op(&eq.lhs) || expr_has_array_op(&eq.rhs) {
return true;
}
}
for name in model.variables.keys() {
if name.contains('[') {
return true;
}
}
false
}
pub(super) fn expr_has_array_op(expr: &Expr) -> bool {
match expr {
Expr::Number(_) | Expr::Integer(_) | Expr::Variable(_) => false,
Expr::Operator(node) => {
if ARRAY_OP_NAMES.contains(&node.op.as_str()) {
return true;
}
if node.op == "index" {
return true;
}
node.any_child(&mut expr_has_array_op)
}
}
}
pub(super) fn check_no_spatial_ops(expr: &Expr) -> Result<(), CompileError> {
crate::op_registry::check_expr(expr).map_err(|e| match e {
OpError::Unlowered { op } => CompileError::UnloweredOperatorError { op },
OpError::Arity { op, got, expected } => {
CompileError::InvalidOperatorArity { op, got, expected }
}
OpError::MakearrayRegion { reason } => CompileError::MakearrayRegionInvalid { reason },
OpError::BroadcastFn { reason, .. } => CompileError::InvalidBroadcastFn { reason },
})
}
pub(crate) fn parse_subsystem_model(
sub_name: &str,
value: &serde_json::Value,
) -> Result<(Model, HashMap<String, IndexSet>), CompileError> {
let obj = value.as_object().ok_or_else(|| {
CompileError::build_err(format!("subsystem '{sub_name}' is not a JSON object"))
})?;
if obj.contains_key("models") {
let file: EsmFile = serde_json::from_value(value.clone()).map_err(|e| {
CompileError::build_err(format!(
"subsystem '{sub_name}' does not parse as an ESM file: {e}"
))
})?;
let models = file.models.unwrap_or_default();
if models.len() != 1 {
return Err(CompileError::build_err(format!(
"subsystem '{sub_name}' resolves to a file with {} models; exactly one is \
required to mount it",
models.len()
)));
}
let model = models.into_values().next().expect("len checked above");
Ok((
model,
file.index_sets.unwrap_or_default().into_iter().collect(),
))
} else if obj.contains_key("variables") || obj.contains_key("equations") {
let model: Model = serde_json::from_value(value.clone()).map_err(|e| {
CompileError::build_err(format!(
"subsystem '{sub_name}' does not parse as a model: {e}"
))
})?;
Ok((model, HashMap::new()))
} else if obj.contains_key("ref") {
Err(CompileError::build_err(format!(
"subsystem '{sub_name}' is an unresolved {{\"ref\": โฆ}}; load the document \
through the official loader (crate::parse::load_path) so \
resolve_subsystem_refs_raw inlines it first"
)))
} else {
Err(CompileError::build_err(format!(
"subsystem '{sub_name}' has neither 'models' nor 'variables'/'equations'"
)))
}
}
pub(crate) fn mount_subsystems(
model: &mut Model,
index_sets: &mut HashMap<String, IndexSet>,
) -> Result<(), CompileError> {
let Some(subs) = model.subsystems.take() else {
return Ok(());
};
let mut names: Vec<String> = subs.keys().cloned().collect();
names.sort();
for sub_name in names {
let (mut sub_model, mut sub_sets) = parse_subsystem_model(&sub_name, &subs[&sub_name])?;
mount_subsystems(&mut sub_model, &mut sub_sets)?;
for (k, v) in sub_sets {
index_sets.entry(k).or_insert(v);
}
let siblings: Vec<String> = {
let mut s: Vec<String> = sub_model.variables.keys().cloned().collect();
s.sort();
s
};
let rename_all = |expr: &Expr| -> Expr {
let mut out = expr.clone();
for s in &siblings {
out = rename_free_symbol(&out, s, &format!("{sub_name}.{s}"));
}
out
};
for (vname, var) in &sub_model.variables {
let mounted = format!("{sub_name}.{vname}");
if model.variables.contains_key(&mounted) {
return Err(CompileError::build_err(format!(
"mounting subsystem '{sub_name}' would overwrite existing variable \
'{mounted}'"
)));
}
let mut var = var.clone();
var.for_each_expression_mut(&mut |expr| *expr = rename_all(expr));
model.variables.insert(mounted, var);
}
for eq in &sub_model.equations {
model.equations.push(crate::types::Equation {
lhs: rename_all(&eq.lhs),
rhs: rename_all(&eq.rhs),
comment: eq.comment.clone(),
});
}
}
Ok(())
}
pub(super) fn apply_ragged_factor_scope(
index_sets: &mut HashMap<String, IndexSet>,
variables: &IndexMap<String, ModelVariable>,
) -> Result<(), CompileError> {
let scope_one = |fname: &str, set_name: &str| -> Result<Option<String>, CompileError> {
if variables.contains_key(fname) {
return Ok(None); }
let suffix = format!(".{fname}");
let cands: Vec<&String> = variables.keys().filter(|n| n.ends_with(&suffix)).collect();
if cands.is_empty() {
return Ok(None); }
let mindepth = cands
.iter()
.map(|c| c.matches('.').count())
.min()
.expect("non-empty");
let best: Vec<&&String> = cands
.iter()
.filter(|c| c.matches('.').count() == mindepth)
.collect();
if best.len() > 1 {
let mut names: Vec<String> = best.iter().map(|s| (**s).clone()).collect();
names.sort();
return Err(CompileError::build_err(format!(
"ragged index set '{set_name}': keyed factor '{fname}' is ambiguous in the \
model scope โ {} candidates at namespace depth {mindepth}: {}",
names.len(),
names.join(", ")
)));
}
Ok(Some((*best[0]).clone()))
};
let set_names: Vec<String> = index_sets
.iter()
.filter(|(_, s)| s.kind == "ragged")
.map(|(n, _)| n.clone())
.collect();
for name in set_names {
let (off, vals) = {
let s = &index_sets[&name];
(s.offsets.clone(), s.values.clone())
};
if let Some(f) = off
&& let Some(scoped) = scope_one(&f, &name)?
{
index_sets.get_mut(&name).expect("present").offsets = Some(scoped);
}
if let Some(f) = vals
&& let Some(scoped) = scope_one(&f, &name)?
{
index_sets.get_mut(&name).expect("present").values = Some(scoped);
}
}
Ok(())
}
pub(crate) fn apply_flatten_rewrites(model: &mut Model) -> Result<(), CompileError> {
let time_invariant: std::collections::HashSet<String> = model
.variables
.iter()
.filter(|(_, v)| v.var_type == crate::types::VariableType::Parameter)
.map(|(name, _)| name.clone())
.collect();
let carried =
crate::flatten::resolve_rhs_time_derivatives(&mut model.equations, &time_invariant);
if carried
&& crate::flatten::first_unresolved_rhs_time_derivative_in(&model.equations).is_some()
{
return Err(CompileError::UnloweredOperatorError {
op: "D".to_string(),
});
}
normalize_model_angle_arguments(model);
Ok(())
}
fn normalize_model_angle_arguments(model: &mut Model) {
let mut seen: HashSet<&str> = HashSet::new();
let declares_angle = model
.variables
.values()
.filter_map(|v| v.units.as_deref())
.filter(|u| seen.insert(u))
.any(|u| {
crate::units::parse_unit(u)
.is_ok_and(|unit| crate::units::angle_normalization_factor(&unit).is_some())
});
if !declares_angle {
return;
}
let (env, _) = crate::units::build_unit_env(&model.variables);
for eq in &mut model.equations {
if let Some(next) = crate::units::normalize_angle_arguments(&eq.rhs, &env) {
eq.rhs = next;
}
}
}
impl ArrayCompiled {
pub fn from_file(file: &EsmFile) -> Result<Self, CompileError> {
Self::from_file_with(file, false)
}
pub(crate) fn from_file_with(
file: &EsmFile,
runtime_array_parameters: bool,
) -> Result<Self, CompileError> {
let Some(models) = &file.models else {
return Err(CompileError::build_err("File has no models to simulate"));
};
if models.len() != 1 {
return Err(CompileError::build_err(
"Array-op path currently only supports a single model file (no coupling)",
));
}
let (model_name, model) = models.iter().next().unwrap();
let index_sets: HashMap<String, IndexSet> = file
.index_sets
.clone()
.unwrap_or_default()
.into_iter()
.collect();
let mut compiled = Self::compile_pipeline(
model.clone(),
&index_sets,
None,
Some(model_name),
runtime_array_parameters,
)?;
compiled.namespace = Some(model_name.clone());
compiled.qualify_data_fed(model_name);
Ok(compiled)
}
pub fn from_file_owned(file: EsmFile) -> Result<Self, CompileError> {
let Some(models) = file.models else {
return Err(CompileError::build_err("File has no models to simulate"));
};
if models.len() != 1 {
return Err(CompileError::build_err(
"Array-op path currently only supports a single model file (no coupling)",
));
}
let index_sets: HashMap<String, IndexSet> =
file.index_sets.unwrap_or_default().into_iter().collect();
let (model_name, model) = models.into_iter().next().unwrap();
let mut compiled =
Self::compile_pipeline(model, &index_sets, None, Some(&model_name), false)?;
compiled.qualify_data_fed(&model_name);
compiled.namespace = Some(model_name);
Ok(compiled)
}
pub fn from_flattened(flat: &FlattenedSystem) -> Result<Self, CompileError> {
Self::from_flattened_with(flat, false)
}
pub(crate) fn from_flattened_with(
flat: &FlattenedSystem,
runtime_array_parameters: bool,
) -> Result<Self, CompileError> {
crate::precision_infer::check_flattened(flat)?;
if crate::flatten::first_unresolved_rhs_time_derivative(flat).is_some() {
return Err(CompileError::UnloweredOperatorError {
op: "D".to_string(),
});
}
if flat.independent_variables != ["t"] {
if let Some(op) = crate::flatten::first_unlowered_operator(flat) {
return Err(CompileError::UnloweredOperatorError { op });
}
return Err(CompileError::UnsupportedDimensionalityError {
independent_variables: flat.independent_variables.clone(),
});
}
if let Some(event) = flat.continuous_events.first() {
return Err(crate::compile_error::continuous_event_refusal(
crate::compile_error::ARRAY_EVALUATOR,
event.name.as_deref(),
));
}
if let Some(event) = flat.discrete_events.first() {
return Err(crate::compile_error::discrete_event_refusal(
crate::compile_error::ARRAY_EVALUATOR,
event.name.as_deref(),
));
}
let mut variables: IndexMap<String, ModelVariable> = IndexMap::new();
for (name, var) in &flat.state_variables {
variables.insert(name.clone(), var.clone());
}
for (name, var) in &flat.parameters {
variables.insert(name.clone(), var.clone());
}
for (name, var) in &flat.observed_variables {
variables.insert(name.clone(), var.clone());
}
let model = Model {
variables,
equations: flat.equations.clone(),
..Default::default()
};
let index_sets: HashMap<String, IndexSet> = flat
.index_sets
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
let mut compiled =
Self::compile_pipeline(model, &index_sets, None, None, runtime_array_parameters)?;
for (_, rhs) in &flat.field_ics {
check_evaluable(rhs)?;
}
compiled.field_ics = flat.field_ics.clone();
compiled.merged_renames = flat
.metadata
.merged_variable_renames
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
Ok(compiled)
}
pub fn from_model(
model: &Model,
index_sets: &HashMap<String, IndexSet>,
) -> Result<Self, CompileError> {
Self::from_model_with_arrays(model, index_sets, None)
}
pub fn from_model_with_arrays(
model: &Model,
index_sets: &HashMap<String, IndexSet>,
vi_arrays: Option<&HashMap<String, ArrayD<f64>>>,
) -> Result<Self, CompileError> {
Self::from_model_owned_with_arrays(model.clone(), index_sets, vi_arrays)
}
fn from_model_owned_with_arrays(
model_owned: Model,
index_sets: &HashMap<String, IndexSet>,
vi_arrays: Option<&HashMap<String, ArrayD<f64>>>,
) -> Result<Self, CompileError> {
Self::compile_pipeline(model_owned, index_sets, vi_arrays, None, false)
}
fn compile_pipeline(
mut model_owned: Model,
index_sets: &HashMap<String, IndexSet>,
vi_arrays: Option<&HashMap<String, ArrayD<f64>>>,
model_name: Option<&str>,
runtime_array_parameters: bool,
) -> Result<Self, CompileError> {
if let Some((construct, name)) = crate::compile_error::first_event(&model_owned) {
return Err(crate::compile_error::event_refusal(
construct,
crate::compile_error::ARRAY_EVALUATOR,
name.as_deref(),
));
}
let mut index_sets_owned = index_sets.clone();
mount_subsystems(&mut model_owned, &mut index_sets_owned)?;
if let Some(name) = model_name {
resolve_self_references(&mut model_owned, name);
}
apply_flatten_rewrites(&mut model_owned)?;
let (inline_param_arrays, runtime_params) = lower_inline_array_parameters(
&mut model_owned,
&index_sets_owned,
vi_arrays,
runtime_array_parameters,
)?;
apply_ragged_factor_scope(&mut index_sets_owned, &model_owned.variables)?;
if crate::precision::is_f32() {
let mut names: Vec<&String> = index_sets_owned.keys().collect();
names.sort();
for name in names {
if let Some(size) = index_sets_owned[name].size {
crate::precision::check_index_set_extent(name, size)?;
}
}
}
let mut vi = ViCensus::of(&model_owned);
if vi.arg_witness {
materialize_vi_outputs_to_data(&mut model_owned, &mut index_sets_owned, vi_arrays)?;
vi = ViCensus::of(&model_owned);
}
let derived = materialize_derived_extents(&model_owned, &index_sets_owned, vi_arrays, &vi);
let mut map_names: Vec<&String> = derived.map_codes.keys().collect();
map_names.sort();
if !map_names.is_empty() {
for name in map_names {
rewrite_equation_to_const(&mut model_owned, name, &derived.map_codes[name]);
}
vi = ViCensus::of(&model_owned);
}
let index_sets = &index_sets_owned;
strip_value_invention(&mut model_owned, index_sets, &vi)
.or_else(|e| derived.refuse_on_failure(index_sets).and(Err(e)))?;
let const_scope = std::sync::Arc::new(ConstArrayScope::from_names(
observed_bodies(&model_owned)
.into_iter()
.filter(|(_, body)| matches!(body, Expr::Operator(n) if n.op == "const"))
.map(|(n, _)| n),
));
crate::join::resolve_aggregate_joins(&mut model_owned, index_sets)?;
let census = OpCensus::of(&model_owned);
if census.ranges {
crate::faq::resolve_aggregate_ranges_with_extents(
&mut model_owned,
index_sets,
&derived.extents,
)
.or_else(|e| derived.refuse_on_failure(index_sets).and(Err(e)))?;
refuse_unmaterialized_derived_ranges(&model_owned, index_sets, &derived)?;
}
if census.faq {
validate_oplus_spellings(&model_owned)?;
}
let (
observed_names,
forcing_decls,
eliminated,
held_at_ic,
slots,
param_names,
param_index,
param_defaults,
unvalued_shaped_params,
forcing_defaults,
data_fed,
) = {
let model = &model_owned;
reject_unlowered_spatial_ops(model)?;
if let Some(eq) = crate::compile_error::first_implicit_equation(&model.equations) {
return Err(crate::compile_error::implicit_equation_refusal(
crate::compile_error::ARRAY_EVALUATOR,
eq,
));
}
if let Some((name, diff, alg)) =
crate::compile_error::first_doubly_defined_unknown(&model.equations)
{
return Err(crate::compile_error::doubly_defined_unknown_refusal(
&name, diff, alg,
));
}
check_free_variables(model, index_sets, &[])?;
let (state_vars, param_vars, observed_vars, data_fed) =
classify_variables(model, &runtime_params)?;
let shape_map = infer_state_shapes(model, &state_vars, index_sets, census.index)?;
let (final_states, eliminated, held_at_ic) = partition_states(model, &state_vars);
let slots = build_slot_tables(model, &final_states, &shape_map)?;
let (param_names, param_index, param_defaults) = build_param_tables(model, ¶m_vars);
let unvalued_shaped_params: Vec<String> = param_vars
.iter()
.filter(|name| {
model.variables.get(**name).is_some_and(|v| {
v.shape.as_ref().is_some_and(|s| !s.is_empty())
&& v.default.is_none()
&& v.distribution.is_none()
})
})
.map(|name| (*name).clone())
.collect();
let observed_names: Vec<String> =
observed_vars.iter().map(|(n, _)| (*n).clone()).collect();
let mut forcing_decls: IndexMap<String, Option<Vec<usize>>> = observed_vars
.iter()
.filter(|(_, var)| var.var_type == VariableType::Parameter)
.map(|(name, var)| {
let shape = match var.shape.as_deref() {
None => Some(Vec::new()),
Some(decl) => resolve_declared_shape(decl, index_sets),
};
((*name).clone(), shape)
})
.collect();
infer_unsized_forcing_shapes(&mut forcing_decls, &model.equations);
let forcing_defaults = forcing_default_values(&observed_vars, &forcing_decls)?;
(
observed_names,
forcing_decls,
eliminated,
held_at_ic,
slots,
param_names,
param_index,
param_defaults,
unvalued_shaped_params,
forcing_defaults,
data_fed,
)
};
let ic_scope = capture_ic_scope(
&model_owned,
&observed_names,
&forcing_decls,
&slots.var_shapes,
&eliminated,
);
let observed_rules =
build_observed_rules(&mut model_owned, &observed_names, &eliminated, index_sets)?;
let model = &model_owned;
let field_ics = classify_field_ics(model);
let init_faqs = classify_initialization_faqs(model);
let rhs_rules = build_rhs_rules(model, &slots, &held_at_ic)?;
let held_at_ic_names = held_at_ic.clone();
let SlotTables {
var_shapes,
state_defaults,
n_states,
} = slots;
let declared_names: HashSet<String> = var_shapes
.keys()
.cloned()
.chain(param_names.iter().cloned())
.chain(observed_rules.iter().map(|r| observed_rule_var(r).clone()))
.collect();
Ok(ArrayCompiled {
runtime_mode: crate::simulate_array::RuntimeMode::default(),
#[cfg(feature = "xla")]
xla_rhs: std::cell::OnceCell::new(),
var_shapes,
state_names: std::cell::OnceCell::new(),
qualified_state_names: std::cell::OnceCell::new(),
state_defaults,
param_names,
param_index,
param_defaults,
observed_rules,
rhs_rules,
rule_set: next_rule_set_id(),
n_states,
declared_names,
forcing: Rc::new(RefCell::new(HashMap::new())),
data_fed,
forcing_generation: std::cell::Cell::new(0),
field_ics,
init_faqs,
ic_scope,
index_sets: index_sets.clone(),
namespace: None,
const_scope,
precision: crate::precision::Env::capture(),
merged_renames: HashMap::new(),
#[cfg(feature = "solve")]
field_ic_memo: RefCell::new(None),
inline_param_arrays,
forcing_decls,
forcing_defaults,
unvalued_shaped_params,
held_at_ic: held_at_ic_names,
tape_cache: tape::TapeCache::new(),
})
}
}
fn reject_unlowered_spatial_ops(model: &Model) -> Result<(), CompileError> {
for eq in &model.equations {
check_evaluable_side(&eq.lhs)?;
check_evaluable(&eq.rhs)?;
}
for var in model.variables.values() {
let mut failure = None;
var.for_each_expression(&mut |expr| {
if failure.is_none()
&& let Err(e) = check_evaluable(expr)
{
failure = Some(e);
}
});
if let Some(e) = failure {
return Err(e);
}
}
Ok(())
}
fn check_evaluable_side(expr: &Expr) -> Result<(), CompileError> {
if let Expr::Operator(node) = expr
&& node.op == "ic"
{
for arg in &node.args {
check_evaluable(arg)?;
}
return Ok(());
}
check_evaluable(expr)
}
type NameSet = HashSet<String, rustc_hash::FxBuildHasher>;
pub(crate) fn check_free_variables(
model: &Model,
index_sets: &HashMap<String, IndexSet>,
extra_bound: &[String],
) -> Result<(), CompileError> {
let mut declared: HashSet<&str, rustc_hash::FxBuildHasher> = HashSet::default();
declared.insert("t");
declared.insert("_var");
declared.extend(model.variables.keys().map(String::as_str));
declared.extend(index_sets.keys().map(String::as_str));
declared.extend(extra_bound.iter().map(String::as_str));
let mut bound: NameSet = NameSet::default();
let mut eq_binders: Vec<NameSet> = Vec::with_capacity(model.equations.len());
for eq in &model.equations {
if let Some(v) = equation_defined_var(&eq.lhs) {
bound.insert(v);
}
let mut binders = NameSet::default();
if is_ic_lhs(&eq.lhs) {
collect_free_bare_symbols(&eq.rhs, &mut bound);
collect_dims_and_index_heads(&eq.lhs, &mut bound);
collect_dims_and_index_heads(&eq.rhs, &mut bound);
} else {
collect_dims_heads_and_binders(&eq.lhs, &mut bound, &mut binders, None);
collect_dims_heads_and_binders(&eq.rhs, &mut bound, &mut binders, Some(false));
}
eq_binders.push(binders);
}
for var in model.variables.values() {
var.for_each_expression(&mut |expr| collect_dims_and_index_heads(expr, &mut bound));
}
let mut binders: NameSet = NameSet::default();
for (eq, mut eq_binders) in model.equations.iter().zip(eq_binders) {
if is_ic_lhs(&eq.lhs) {
continue;
}
with_binders(&mut bound, &mut eq_binders, |scope| {
let is_bound = |name: &str| declared.contains(name) || scope.contains(name);
check_expr_free_vars(&eq.lhs, &is_bound)?;
check_expr_free_vars(&eq.rhs, &is_bound)
})?;
}
for var in model.variables.values() {
let mut failure = None;
var.for_each_expression(&mut |expr| {
if failure.is_some() {
return;
}
collect_binders(expr, &mut binders);
if let Err(e) = with_binders(&mut bound, &mut binders, |scope| {
check_expr_free_vars(expr, &|name: &str| {
declared.contains(name) || scope.contains(name)
})
}) {
failure = Some(e);
}
});
if let Some(e) = failure {
return Err(e);
}
}
Ok(())
}
fn with_binders<T>(
bound: &mut NameSet,
binders: &mut NameSet,
check: impl FnOnce(&NameSet) -> T,
) -> T {
let mut added: Vec<String> = Vec::new();
for name in binders.drain() {
if !bound.contains(&name) {
bound.insert(name.clone());
added.push(name);
}
}
let out = check(bound);
for name in &added {
bound.remove(name);
}
out
}
fn is_ic_lhs(lhs: &Expr) -> bool {
matches!(lhs, Expr::Operator(op) if op.op == "ic")
}
fn node_binders(node: &ExpressionNode, out: &mut NameSet) {
node_loop_binders(node, out);
if node.op == "index" {
for arg in node.args.iter().skip(1) {
if let Expr::Variable(name) = arg {
out.insert(name.clone());
}
}
}
}
fn node_loop_binders(node: &ExpressionNode, out: &mut NameSet) {
if let Some(idx) = &node.output_idx {
out.extend(idx.iter().cloned());
}
if let Some(ranges) = &node.ranges {
out.extend(ranges.keys().cloned());
}
if let Some(v) = &node.int_var {
out.insert(v.clone());
}
if let Some(a) = &node.arg {
out.insert(a.clone());
}
if let Some(bindings) = &node.bindings {
out.extend(bindings.keys().cloned());
}
}
fn collect_binders(expr: &Expr, out: &mut NameSet) {
if let Expr::Operator(node) = expr {
node_binders(node, out);
node.for_each_child(&mut |child| collect_binders(child, out));
}
}
fn collect_free_bare_symbols(expr: &Expr, out: &mut NameSet) {
match expr {
Expr::Variable(name) if !name.contains('.') && !is_builtin_function_name(name) => {
out.insert(name.clone());
}
Expr::Operator(node) => {
node.for_each_child(&mut |child| collect_free_bare_symbols(child, out));
}
_ => {}
}
}
fn collect_dims_and_index_heads(expr: &Expr, out: &mut NameSet) {
if let Expr::Operator(node) = expr {
if let Some(dim) = &node.dim {
out.insert(dim.clone());
}
if node.op == "index"
&& let Some(Expr::Variable(name)) = node.args.first()
&& !name.contains('.')
{
out.insert(name.clone());
}
node.for_each_child(&mut |child| collect_dims_and_index_heads(child, out));
}
}
fn collect_dims_heads_and_binders(
expr: &Expr,
credited: &mut NameSet,
binders: &mut NameSet,
rhs: Option<bool>,
) {
let Expr::Operator(node) = expr else {
return;
};
if let Some(dim) = &node.dim {
credited.insert(dim.clone());
}
if node.op == "index"
&& let Some(Expr::Variable(name)) = node.args.first()
&& !name.contains('.')
{
credited.insert(name.clone());
}
let child_rhs = match rhs {
None => {
node_binders(node, binders);
None
}
Some(enclosed) => {
let carries = node.output_idx.is_some()
|| node.ranges.is_some()
|| node.int_var.is_some()
|| node.arg.is_some()
|| node.bindings.is_some();
if enclosed || node.op != "index" {
node_binders(node, binders);
}
Some(enclosed || carries)
}
};
node.for_each_child(&mut |child| {
collect_dims_heads_and_binders(child, credited, binders, child_rhs)
});
}
fn check_expr_free_vars(expr: &Expr, scope: &dyn Fn(&str) -> bool) -> Result<(), CompileError> {
match expr {
Expr::Variable(name) => {
if name.contains('.') || is_builtin_function_name(name) || name.starts_with("d(") {
return Ok(());
}
if scope(name) {
return Ok(());
}
Err(CompileError::build_err(format!(
"Unknown variable '{name}' referenced in expression"
)))
}
Expr::Operator(node) => {
let mut first_err: Option<CompileError> = None;
node.for_each_child(&mut |child| {
if first_err.is_none()
&& let Err(e) = check_expr_free_vars(child, scope)
{
first_err = Some(e);
}
});
match first_err {
Some(e) => Err(e),
None => Ok(()),
}
}
_ => Ok(()),
}
}
#[derive(Clone, Debug)]
pub(super) struct IcScopeDef {
pub(super) name: String,
pub(super) body: Expr,
pub(super) deps: Vec<String>,
pub(super) needs: Vec<String>,
}
#[derive(Clone, Debug, Default)]
pub(super) struct IcScope {
pub(super) defs: Vec<IcScopeDef>,
pub(super) roots: Vec<String>,
pub(super) excluded: Vec<(String, String)>,
}
fn collect_free_names(expr: &Expr, bound: &mut Vec<String>, out: &mut HashSet<String>) {
match expr {
Expr::Variable(name) if !bound.contains(name) => {
out.insert(name.clone());
}
Expr::Operator(node) => {
let mut own = NameSet::default();
node_loop_binders(node, &mut own);
let depth = bound.len();
bound.extend(own);
node.for_each_child(&mut |child| collect_free_names(child, bound, out));
bound.truncate(depth);
}
_ => {}
}
}
pub(super) fn free_names(expr: &Expr) -> HashSet<String> {
let mut out = HashSet::new();
collect_free_names(expr, &mut Vec::new(), &mut out);
out
}
struct StateTaint<'a> {
defs: &'a BTreeMap<String, (Expr, HashSet<String>)>,
is_state: &'a dyn Fn(&str) -> bool,
marks: HashMap<&'a str, Option<Option<String>>>,
}
impl<'a> StateTaint<'a> {
fn cause(&mut self, name: &'a str) -> Option<String> {
match self.marks.get(name) {
Some(Some(cause)) => return cause.clone(),
Some(None) => return None,
None => {}
}
self.marks.insert(name, None);
let defs = self.defs;
let reads = &defs[name].1;
let mut cause = reads
.iter()
.filter(|n| (self.is_state)(n))
.min()
.map(|n| format!("reads '{n}'"));
if cause.is_none() {
let mut deps: Vec<&'a String> = reads
.iter()
.filter(|n| n.as_str() != name && defs.contains_key(n.as_str()))
.collect();
deps.sort();
for dep in deps {
if let Some(inner) = self.cause(dep) {
cause = Some(format!("reads '{dep}', which {inner}"));
break;
}
}
}
self.marks.insert(name, Some(cause.clone()));
cause
}
}
fn capture_ic_scope(
model: &Model,
observed_names: &[String],
forcing_names: &IndexMap<String, Option<Vec<usize>>>,
state_names: &IndexMap<String, VarShape>,
eliminated: &HashSet<String>,
) -> IcScope {
let ic_reads: HashSet<String> = model
.equations
.iter()
.filter(|eq| is_ic_lhs(&eq.lhs))
.flat_map(|eq| free_names(&eq.rhs))
.collect();
if ic_reads.is_empty() {
return IcScope::default();
}
let observed: HashSet<&str> = observed_names.iter().map(String::as_str).collect();
let defs: BTreeMap<String, (Expr, HashSet<String>)> = observed_bodies(model)
.into_iter()
.filter(|(name, _)| observed.contains(name.as_str()))
.map(|(name, body)| {
let reads = free_names(&body);
(name, (body, reads))
})
.collect();
let is_state = |n: &str| n == "t" || state_names.contains_key(n) || eliminated.contains(n);
let mut taint = StateTaint {
defs: &defs,
is_state: &is_state,
marks: HashMap::new(),
};
let mut excluded: Vec<(String, String)> = Vec::new();
let mut roots: Vec<String> = ic_reads
.iter()
.filter(|n| defs.contains_key(n.as_str()))
.cloned()
.collect();
roots.sort();
let mut wanted: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
let mut stack: Vec<&str> = roots.iter().map(String::as_str).collect();
while let Some(name) = stack.pop() {
if wanted.contains_key(name) || excluded.iter().any(|(n, _)| n == name) {
continue;
}
if let Some(cause) = taint.cause(name) {
excluded.push((name.to_string(), format!("is not state-free: it {cause}")));
continue;
}
let deps: Vec<&str> = defs[name]
.1
.iter()
.map(String::as_str)
.filter(|n| *n != name && defs.contains_key(*n))
.collect();
stack.extend(&deps);
wanted.insert(name, deps);
}
for deps in wanted.values_mut() {
deps.retain(|d| !excluded.iter().any(|(n, _)| n == d));
}
let mut pending: BTreeMap<&str, usize> = BTreeMap::new();
let mut readers: HashMap<&str, Vec<&str>> = HashMap::new();
for (name, deps) in &wanted {
pending.insert(name, deps.len());
for dep in deps {
readers.entry(dep).or_default().push(name);
}
}
let mut ready: std::collections::BTreeSet<&str> = pending
.iter()
.filter(|(_, n)| **n == 0)
.map(|(name, _)| *name)
.collect();
let mut order: Vec<&str> = Vec::with_capacity(wanted.len());
while let Some(name) = ready.pop_first() {
pending.remove(name);
order.push(name);
for reader in readers.get(name).into_iter().flatten() {
if let Some(n) = pending.get_mut(reader) {
*n -= 1;
if *n == 0 {
ready.insert(reader);
}
}
}
}
if !pending.is_empty() {
let cycle: Vec<&str> = pending.keys().copied().collect();
let reason = format!(
"is on, or reads, a dependency cycle among the observeds {}, so no evaluation \
order exists",
cycle.join(", ")
);
excluded.extend(cycle.iter().map(|n| (n.to_string(), reason.clone())));
}
let defs = order
.into_iter()
.map(|name| {
let (body, reads) = &defs[name];
let mut deps: Vec<String> = wanted[name].iter().map(|d| d.to_string()).collect();
deps.sort();
let mut needs: Vec<String> = reads
.iter()
.filter(|n| {
n.as_str() != name
&& (defs.contains_key(n.as_str()) || forcing_names.contains_key(n.as_str()))
})
.cloned()
.collect();
needs.sort();
IcScopeDef {
name: name.to_string(),
body: body.clone(),
deps,
needs,
}
})
.collect();
excluded.sort();
IcScope {
defs,
roots,
excluded,
}
}
#[allow(clippy::type_complexity)]
fn lower_inline_array_parameters(
model: &mut Model,
index_sets: &HashMap<String, IndexSet>,
external: Option<&HashMap<String, ArrayD<f64>>>,
keep_at_runtime: bool,
) -> Result<(HashMap<String, (Vec<usize>, Vec<f64>)>, HashSet<String>), CompileError> {
let mut lowered: Vec<(String, JsonValue)> = Vec::new();
let mut dense: HashMap<String, (Vec<usize>, Vec<f64>)> = HashMap::new();
let mut kept: HashSet<String> = HashSet::new();
for (name, var) in &mut model.variables {
if var.var_type != VariableType::Parameter {
continue;
}
let Some(default) = var.default.as_ref() else {
continue;
};
if keep_at_runtime
&& var.shape.as_deref().is_some_and(|s| !s.is_empty())
&& var.update.is_none()
{
kept.insert(name.clone());
continue;
}
if !default.is_array() {
let declared = var.shape.as_deref().unwrap_or(&[]);
if declared.is_empty()
|| var.update.is_some()
|| external.is_some_and(|a| a.contains_key(name))
{
continue;
}
let Some(want) = resolve_declared_shape(declared, index_sets) else {
continue;
};
let Some(scalar) = default.as_scalar() else {
continue;
};
let values = vec![scalar; want.iter().copied().product::<usize>().max(1)];
var.var_type = VariableType::Unknown;
var.default = None;
lowered.push((name.clone(), dense_to_json(&want, &values)));
dense.insert(name.clone(), (want, values));
continue;
}
let (shape, values) = default.to_dense().map_err(|e| {
CompileError::build_err(format!("parameter '{name}': {e} (esm-spec ยง6.3)"))
})?;
let declared = var.shape.as_deref().unwrap_or(&[]);
if declared.is_empty() {
return Err(CompileError::build_err(format!(
"parameter '{name}' carries inline array data but declares no `shape`; only a \
SHAPED variable takes a nested array (esm-spec ยง6.3)"
)));
}
if let Some(want) = resolve_declared_shape(declared, index_sets)
&& want != shape
{
return Err(CompileError::build_err(format!(
"parameter '{name}': inline array data has shape {shape:?}, which does not match \
the declared shape {want:?} (esm-spec ยง6.6.2 โ the array MUST match the \
variable's declared shape after metaparameter folding)"
)));
}
var.var_type = VariableType::Unknown;
var.default = None;
lowered.push((name.clone(), dense_to_json(&shape, &values)));
dense.insert(name.clone(), (shape, values));
}
for (name, value) in lowered {
model.equations.push(crate::types::Equation {
lhs: Expr::Variable(name),
rhs: Expr::operator(ExpressionNode {
op: "const".to_string(),
value: Some(value),
..Default::default()
}),
comment: None,
});
}
Ok((dense, kept))
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn inline_parameter_arrays(
model: &Model,
index_sets: &HashMap<String, IndexSet>,
) -> Vec<(String, ArrayD<f64>)> {
let mut out = Vec::new();
for (name, var) in &model.variables {
if var.var_type != VariableType::Parameter {
continue;
}
let Some(default) = var.default.as_ref() else {
continue;
};
let declared = var.shape.as_deref().unwrap_or(&[]);
if declared.is_empty() {
continue;
}
let want = resolve_declared_shape(declared, index_sets);
let (shape, values) = if default.is_array() {
match default.to_dense() {
Ok((shape, values)) if want.as_ref().is_none_or(|w| *w == shape) => (shape, values),
_ => continue,
}
} else {
if var.update.is_some() {
continue;
}
let (Some(want), Some(scalar)) = (want, default.as_scalar()) else {
continue;
};
let n = want.iter().copied().product::<usize>().max(1);
(want, vec![scalar; n])
};
let prec = crate::precision::active();
let values: Vec<f64> = values.into_iter().map(|v| prec.round(v)).collect();
if let Ok(a) = ArrayD::from_shape_vec(IxDyn(&shape), values) {
out.push((name.clone(), a));
}
}
out
}
fn dense_to_json(shape: &[usize], values: &[f64]) -> JsonValue {
fn build(shape: &[usize], values: &[f64]) -> JsonValue {
let Some((&axis, rest)) = shape.split_first() else {
return serde_json::Number::from_f64(values[0])
.map(JsonValue::Number)
.unwrap_or(JsonValue::Null);
};
let stride = rest.iter().product::<usize>().max(1);
JsonValue::Array(
(0..axis)
.map(|i| build(rest, &values[i * stride..(i + 1) * stride]))
.collect(),
)
}
build(shape, values)
}
#[allow(clippy::type_complexity)]
fn classify_variables<'m>(
model: &'m Model,
runtime_params: &HashSet<String>,
) -> Result<
(
Vec<&'m String>,
Vec<&'m String>,
Vec<(&'m String, &'m ModelVariable)>,
Vec<(String, String)>,
),
CompileError,
> {
let mut state_vars: Vec<&'m String> = Vec::new();
let mut param_vars: Vec<&'m String> = Vec::new();
let mut observed_vars: Vec<(&'m String, &'m ModelVariable)> = Vec::new();
let mut data_fed: Vec<(String, String)> = Vec::new();
let class = crate::classification::VariableRoles::of(model);
let mut var_keys: Vec<&String> = model.variables.keys().collect();
var_keys.sort_unstable();
for name in var_keys {
let var = &model.variables[name];
match var.var_type {
VariableType::Unknown => {
if class.is_observed(name) {
observed_vars.push((name, var));
} else {
state_vars.push(name);
}
}
VariableType::Parameter => {
if runtime_params.contains(name) {
observed_vars.push((name, var));
continue;
}
if class.is_brownian(name) {
return Err(CompileError::UnsupportedConstruct {
construct: crate::compile_error::WIENER_NOISE,
evaluator: crate::compile_error::ARRAY_EVALUATOR,
detail: format!("parameter '{name}'"),
});
}
if class.is_discrete_parameter(name) {
if externally_refreshed(var) {
if let Some(source) = var.update.as_ref().and_then(data_feed_source) {
data_fed.push((name.clone(), source.to_string()));
}
observed_vars.push((name, var));
continue;
}
let kinds: Vec<&str> = var
.update
.iter()
.flat_map(|spec| spec.rules())
.filter(|rule| {
rule.value()
.is_some_and(|v| v.from.is_none() && v.handler.is_none())
})
.map(|rule| rule.kind())
.collect();
return Err(CompileError::UnsupportedFeatureError {
feature: "discrete".to_string(),
message: format!(
"Rust array simulation backend does not yet support a discrete parameter that recomputes itself symbolically; parameter '{name}' carries an `expression` update of kind {} (esm-spec ยง5.4: the event-driven parameter update)",
kinds
.iter()
.map(|k| format!("`{k}`"))
.collect::<Vec<_>>()
.join(", ")
),
});
}
param_vars.push(name);
}
}
}
Ok((state_vars, param_vars, observed_vars, data_fed))
}
fn infer_state_shapes(
model: &Model,
state_vars: &[&String],
index_sets: &HashMap<String, IndexSet>,
any_index: bool,
) -> Result<HashMap<String, Vec<usize>>, CompileError> {
let mut shape_map = if any_index {
infer_shapes(state_vars, &model.equations)?
} else {
state_vars
.iter()
.map(|name| ((*name).clone(), Vec::new()))
.collect()
};
for name in state_vars {
let Some(decl) = model.variables.get(*name).and_then(|v| v.shape.as_ref()) else {
continue;
};
let undeclared: Vec<&str> = decl
.iter()
.filter(|axis| !index_sets.contains_key(axis.as_str()))
.map(String::as_str)
.collect();
if undeclared.is_empty() || shape_map.get(*name).is_some_and(|s| !s.is_empty()) {
continue;
}
return Err(CompileError::build_err(format!(
"state '{name}' declares shape {decl:?}, but index set(s) {undeclared:?} are not \
declared in the document `index_sets` registry and no equation indexes the state, \
so it has no extent; declare them, or inject the grid that does (esm-spec ยง6.3, \
ยง9.7.10: a name still unresolved after injection is an error at the build)"
)));
}
for name in state_vars {
if let Some(decl) = model.variables.get(*name).and_then(|v| v.shape.as_ref()) {
if !decl.is_empty() {
if let Some(resolved) = resolve_declared_shape(decl, index_sets) {
shape_map.insert((*name).clone(), resolved);
}
}
}
}
Ok(shape_map)
}
fn partition_states(
model: &Model,
state_vars: &[&String],
) -> (Vec<String>, HashSet<String>, HashSet<String>) {
let mut scalar_targets: rustc_hash::FxHashSet<&str> = rustc_hash::FxHashSet::default();
let mut faq_targets: HashSet<String> = HashSet::new();
for eq in &model.equations {
if let Some(name) = derivative_scalar_target(&eq.lhs) {
scalar_targets.insert(name);
}
if let Some(DerivArrayop { var: name, .. }) = extract_derivative_faq(&eq.lhs, &eq.rhs) {
faq_targets.insert(name);
}
}
let is_derivative_target =
|name: &str| scalar_targets.contains(name) || faq_targets.contains(name);
let algebraic_defined = collect_algebraic_defined(&model.equations);
let mut final_states: Vec<String> = Vec::new();
let mut eliminated: HashSet<String> = HashSet::new();
let mut held_at_ic: HashSet<String> = HashSet::new();
for name in state_vars {
if is_derivative_target(name) {
final_states.push((*name).clone());
} else if algebraic_defined.contains(*name) {
eliminated.insert((*name).clone());
} else {
final_states.push((*name).clone());
held_at_ic.insert((*name).clone());
}
}
(final_states, eliminated, held_at_ic)
}
struct SlotTables {
var_shapes: IndexMap<String, VarShape>,
state_defaults: Vec<StateDefault>,
n_states: usize,
}
fn build_slot_tables(
model: &Model,
final_states: &[String],
shape_map: &HashMap<String, Vec<usize>>,
) -> Result<SlotTables, CompileError> {
let mut var_shapes: IndexMap<String, VarShape> = IndexMap::new();
let mut state_defaults: Vec<StateDefault> = Vec::with_capacity(final_states.len());
let mut flat_offset: usize = 0;
for name in final_states {
let shape = shape_map.get(name).cloned().unwrap_or_default();
let origin: Vec<i64> = if shape.is_empty() {
Vec::new()
} else {
vec![1i64; shape.len()]
};
let var = model.variables.get(name);
let default = match var.and_then(|v| v.default_array()) {
Some(Ok((dshape, values))) => {
if dshape != shape {
return Err(CompileError::build_err(format!(
"state '{name}': inline array `default` has shape {dshape:?}, which does \
not match the resolved grid shape {shape:?} (esm-spec ยง6.6.2)"
)));
}
if shape.is_empty() {
StateDefault::Scalar(var.and_then(|v| v.default_scalar()))
} else {
StateDefault::Field(values)
}
}
Some(Err(e)) => {
return Err(CompileError::build_err(format!(
"state '{name}': {e} (esm-spec ยง6.3)"
)));
}
_ => StateDefault::Scalar(var.and_then(|v| v.default_scalar())),
};
let total = shape.iter().copied().product::<usize>().max(1);
state_defaults.push(default);
var_shapes.insert(
name.clone(),
VarShape {
shape,
origin,
flat_offset,
},
);
flat_offset += total;
}
Ok(SlotTables {
var_shapes,
state_defaults,
n_states: flat_offset,
})
}
fn build_param_tables(
model: &Model,
param_vars: &[&String],
) -> (Vec<String>, HashMap<String, usize>, Vec<Option<f64>>) {
let param_names: Vec<String> = param_vars.iter().map(|s| (*s).clone()).collect();
let param_index: HashMap<String, usize> = param_names
.iter()
.enumerate()
.map(|(i, n)| (n.clone(), i))
.collect();
let param_defaults: Vec<Option<f64>> = param_vars
.iter()
.map(|n| model.variables.get(*n).and_then(|v| v.default_scalar()))
.collect();
(param_names, param_index, param_defaults)
}
struct SelfRead {
args: Vec<Expr>,
env: HashMap<String, (i64, i64)>,
}
pub(super) struct RecurLowering {
pub(super) idx_names: Vec<String>,
pub(super) ranges: Vec<(i64, i64)>,
pub(super) body: Expr,
pub(super) axis: usize,
pub(super) max_lag: i64,
pub(super) lag_proven: bool,
}
fn recur_err(code: &str, detail: String) -> CompileError {
CompileError::InterpreterBuildError {
details: format!("{code}: {detail}"),
}
}
struct Affine {
coef: i64,
konst: Option<(i64, i64)>,
}
fn affine_in_sym(e: &Expr, sym: &str, env: &HashMap<String, (i64, i64)>) -> Option<Affine> {
let konst = |lo: i64, hi: i64| {
Some(Affine {
coef: 0,
konst: Some((lo, hi)),
})
};
match e {
Expr::Integer(n) => konst(*n, *n),
Expr::Number(f) if f.fract() == 0.0 && f.is_finite() => {
let n = *f as i64;
konst(n, n)
}
Expr::Variable(v) if v == sym => Some(Affine {
coef: 1,
konst: Some((0, 0)),
}),
Expr::Variable(v) => Some(Affine {
coef: 0,
konst: env.get(v).copied(),
}),
Expr::Operator(node) if node.args.len() == 2 => {
let a = affine_in_sym(&node.args[0], sym, env)?;
let b = affine_in_sym(&node.args[1], sym, env)?;
let both = a.konst.zip(b.konst);
match node.op.as_str() {
"+" => Some(Affine {
coef: a.coef + b.coef,
konst: both.map(|((la, ha), (lb, hb))| (la + lb, ha + hb)),
}),
"-" => Some(Affine {
coef: a.coef - b.coef,
konst: both.map(|((la, ha), (lb, hb))| (la - hb, ha - lb)),
}),
"*" => {
let (k, other) = match (a.coef, a.konst, b.coef, b.konst) {
(0, Some((lo, hi)), _, _) if lo == hi => (lo, &b),
(_, _, 0, Some((lo, hi))) if lo == hi => (lo, &a),
_ => return None,
};
Some(Affine {
coef: other.coef * k,
konst: other.konst.map(|(lo, hi)| {
let (p, q) = (lo * k, hi * k);
(p.min(q), p.max(q))
}),
})
}
_ => None,
}
}
_ => None,
}
}
enum SelfIdx {
Identity,
Offset { proven: bool, max_lag: i64 },
Forward,
Bad,
}
fn classify_self_index(arg: &Expr, sym: &str, env: &HashMap<String, (i64, i64)>) -> SelfIdx {
let Some(Affine { coef, konst }) = affine_in_sym(arg, sym, env) else {
return SelfIdx::Bad;
};
if coef != 1 {
return SelfIdx::Bad;
}
let Some((clo, chi)) = konst else {
return SelfIdx::Offset {
proven: false,
max_lag: 0,
};
};
let (lag_lo, lag_hi) = (-chi, -clo);
if lag_lo == 0 && lag_hi == 0 {
return SelfIdx::Identity;
}
if lag_hi <= 0 {
return SelfIdx::Forward;
}
SelfIdx::Offset {
proven: lag_lo >= 1,
max_lag: lag_hi,
}
}
fn aggregate_range_env(node: &ExpressionNode) -> Vec<(String, (i64, i64))> {
node.ranges
.as_ref()
.map(|m| {
m.iter()
.filter_map(|(k, spec)| spec.bounds().map(|b| (k.clone(), (b[0], b[1]))))
.collect()
})
.unwrap_or_default()
}
fn collect_self_reads(
e: &Expr,
var: &str,
env: &mut Vec<(String, (i64, i64))>,
out: &mut Vec<SelfRead>,
bare: &mut bool,
) {
match e {
Expr::Integer(_) | Expr::Number(_) => {}
Expr::Variable(v) => {
if v == var {
*bare = true;
}
}
Expr::Operator(node) => {
let pushed = if crate::faq::is_faq_op(&node.op) {
let add = aggregate_range_env(node);
let n = add.len();
env.extend(add);
n
} else {
0
};
let is_self_index = node.op == "index"
&& matches!(node.args.first(), Some(Expr::Variable(v)) if v == var);
if is_self_index {
let mut snapshot: HashMap<String, (i64, i64)> = HashMap::new();
for (k, v) in env.iter() {
snapshot.insert(k.clone(), *v);
}
out.push(SelfRead {
args: node.args[1..].to_vec(),
env: snapshot,
});
}
let skip = usize::from(is_self_index);
for a in node.args.iter().skip(skip) {
collect_self_reads(a, var, env, out, bare);
}
for side in [
node.expr.as_deref(),
node.filter.as_deref(),
node.key.as_deref(),
node.lower.as_deref(),
node.upper.as_deref(),
]
.into_iter()
.flatten()
{
collect_self_reads(side, var, env, out, bare);
}
if let Some(vs) = node.values.as_ref() {
for v in vs {
collect_self_reads(v, var, env, out, bare);
}
}
env.truncate(env.len() - pushed);
}
}
}
fn lhs_identity_gather(e: &Expr, var: &str, idx_names: &[String]) -> bool {
let Expr::Operator(node) = e else {
return false;
};
if node.op != "index" || node.args.len() != idx_names.len() + 1 {
return false;
}
if !matches!(node.args.first(), Some(Expr::Variable(v)) if v == var) {
return false;
}
node.args[1..]
.iter()
.zip(idx_names)
.all(|(a, want)| matches!(a, Expr::Variable(v) if v == want))
}
fn cell_restrict_aggregate(node: &ExpressionNode, idx_names: &[String]) -> Expr {
let body = node
.expr
.as_deref()
.cloned()
.unwrap_or(Expr::Number(f64::NAN));
let remaining: HashMap<String, crate::types::RangeSpec> = node
.ranges
.as_ref()
.map(|m| {
m.iter()
.filter(|(k, _)| !idx_names.iter().any(|n| n == *k))
.map(|(k, v)| (k.clone(), v.clone()))
.collect()
})
.unwrap_or_default();
let gated = node.filter.is_some()
|| node.join.is_some()
|| node.key.is_some()
|| node.distinct == Some(true);
if remaining.is_empty() && !gated {
return body;
}
let mut restricted = node.clone();
restricted.output_idx = Some(Vec::new());
restricted.ranges = Some(remaining);
Expr::operator(restricted)
}
pub(super) fn lower_recurrence(
var: &str,
lhs: &Expr,
rhs: &Expr,
) -> Result<Option<RecurLowering>, CompileError> {
let mut env: Vec<(String, (i64, i64))> = Vec::new();
let mut reads: Vec<SelfRead> = Vec::new();
let mut bare = false;
collect_self_reads(rhs, var, &mut env, &mut reads, &mut bare);
if reads.is_empty() {
return Ok(None);
}
if bare {
return Err(recur_err(
"recurrence_not_wellfounded",
format!(
"'{var}' is read BARE in its own defining equation as well as through `index`. \
A bare read names the whole array, which does not exist while the recurrence \
sweeps it; read every self-reference through `index` at a strictly earlier \
position (esm-spec ยง4.3.1.1)."
),
));
}
let frame_node: &ExpressionNode = match (lhs, rhs) {
(Expr::Operator(l), _) if crate::faq::is_faq_op(&l.op) => l.as_ref(),
(Expr::Variable(v), Expr::Operator(r)) if v == var && crate::faq::is_faq_op(&r.op) => {
r.as_ref()
}
_ => {
return Err(recur_err(
"recurrence_unsupported_form",
format!(
"the definition of '{var}' reads '{var}' at an earlier position, but its \
shape gives the runtime no cell frame to sweep. Write the recurrence as one \
`faq` over the variable's axes โ either `{var} ~ aggregate{{โฆ}}` or \
`aggregate{{expr: index({var}, kโฆ)}} ~ โฆ` โ with the base case as an \
`ifelse` guard in the body (esm-spec ยง4.3.1.1)."
),
));
}
};
let idx_names: Vec<String> = frame_node.output_idx.clone().unwrap_or_default();
if idx_names.is_empty() || idx_names.iter().any(|n| n.parse::<i64>().is_ok()) {
return Err(recur_err(
"recurrence_unsupported_form",
format!(
"the recurrence definition of '{var}' has no symbolic output index to fold \
along (`output_idx` is {idx_names:?}). A literal singleton dimension cannot be \
a recurrence axis (esm-spec ยง4.3.1.1)."
),
));
}
let ranges_map = frame_node.ranges.clone().unwrap_or_default();
let mut ranges: Vec<(i64, i64)> = Vec::with_capacity(idx_names.len());
for n in &idx_names {
match ranges_map.get(n) {
Some(crate::types::RangeSpec::Interval([lo, hi])) => ranges.push((*lo, *hi)),
other => {
return Err(recur_err(
"recurrence_not_wellfounded",
format!(
"axis '{n}' of the recurrence definition of '{var}' is not a static \
unit-step ascending interval (range: {other:?}). A ragged, derived, \
strided or unresolved axis carries no total order to fold along \
(esm-spec ยง4.3.1.1)."
),
));
}
}
}
let frame_env: HashMap<String, (i64, i64)> = idx_names
.iter()
.cloned()
.zip(ranges.iter().copied())
.collect();
let mut axis: Option<usize> = None;
let mut max_lag: i64 = 0;
let mut lag_proven = true;
for read in &reads {
if read.args.len() != idx_names.len() {
return Err(recur_err(
"recurrence_not_wellfounded",
format!(
"a self-read of '{var}' supplies {} indices but its frame has {} axes \
({idx_names:?}). Every causal self-read indexes every axis (esm-spec \
ยง4.3.1.1).",
read.args.len(),
idx_names.len()
),
));
}
let mut env = frame_env.clone();
for (k, v) in &read.env {
env.insert(k.clone(), *v);
}
let mut lagged: Option<(usize, i64)> = None;
for (d, arg) in read.args.iter().enumerate() {
match classify_self_index(arg, &idx_names[d], &env) {
SelfIdx::Identity => {}
SelfIdx::Forward => {
return Err(recur_err(
"recurrence_not_wellfounded",
format!(
"index {d} of a self-read of '{var}' names the cell being written, \
or a LATER one, on axis '{}'. A causal self-reference reads \
strictly EARLIER positions; no sweep order can satisfy a forward \
or same-cell read (esm-spec ยง4.3.1.1).",
idx_names[d]
),
));
}
SelfIdx::Offset {
proven,
max_lag: hi,
} => {
lag_proven &= proven;
if lagged.is_some() {
return Err(recur_err(
"recurrence_not_wellfounded",
format!(
"a self-read of '{var}' is offset on more than one axis. A \
causal self-reference folds along exactly ONE axis; every \
other index must be the bare frame symbol (esm-spec ยง4.3.1.1)."
),
));
}
lagged = Some((d, hi));
}
SelfIdx::Bad => {
return Err(recur_err(
"recurrence_not_wellfounded",
format!(
"index {d} of a self-read of '{var}' is not an offset of the frame \
symbol '{}'. A causal self-read names a position RELATIVE to the \
cell being written โ `{} - c`, `{} - a`, `{} - a - c` โ so that \
which axis the recurrence folds along, and in which direction, is \
decidable. An index that does not carry '{}' with coefficient 1 \
(a bare constant, `2*{}`, another axis's symbol) is rejected \
rather than guessed at (esm-spec ยง4.3.1.1). {}",
idx_names[d],
idx_names[d],
idx_names[d],
idx_names[d],
idx_names[d],
idx_names[d],
crate::structural::data_lag_guidance(var, &idx_names[d])
),
));
}
}
}
let Some((d, hi)) = lagged else {
return Err(recur_err(
"recurrence_not_wellfounded",
format!(
"a self-read of '{var}' is at the SAME cell on every axis, so it defines \
'{var}' in terms of itself rather than of an earlier position. A causal \
self-reference must be strictly earlier along one axis (esm-spec ยง4.3.1.1)."
),
));
};
match axis {
None => axis = Some(d),
Some(prev) if prev == d => {}
Some(prev) => {
return Err(recur_err(
"recurrence_not_wellfounded",
format!(
"the self-reads of '{var}' disagree on the recurrence axis: one folds \
along '{}' and another along '{}'. A definition folds along exactly one \
axis (esm-spec ยง4.3.1.1).",
idx_names[prev], idx_names[d]
),
));
}
}
max_lag = max_lag.max(hi);
}
let axis = axis.expect("at least one self-read, each of which set the axis");
let body = match rhs {
Expr::Operator(r)
if crate::faq::is_faq_op(&r.op)
&& r.output_idx.as_deref() == Some(idx_names.as_slice()) =>
{
cell_restrict_aggregate(r, &idx_names)
}
other => {
if lhs_identity_gather(
frame_node.expr.as_deref().unwrap_or(&Expr::Integer(0)),
var,
&idx_names,
) {
other.clone()
} else {
return Err(recur_err(
"recurrence_unsupported_form",
format!(
"the recurrence definition of '{var}' cannot be restricted to one cell: \
its RHS is not a `faq` over the frame {idx_names:?}, and its LHS \
is not the identity gather `index({var}, {})`. A self-read reached only \
through a `makearray` region, a `reshape`/`transpose`/`concat` operand, \
or an aggregate over a different frame cannot be sequenced cell by cell \
โ the region order of a `makearray` fixes which write WINS, not the \
order cells are EVALUATED in (esm-spec ยง4.3.1.1, ยง4.3.2).",
idx_names.join(", ")
),
));
}
}
};
Ok(Some(RecurLowering {
idx_names,
ranges,
body,
axis,
max_lag,
lag_proven,
}))
}
fn build_observed_rules(
model: &mut Model,
observed_names: &[String],
eliminated: &HashSet<String>,
index_sets: &HashMap<String, IndexSet>,
) -> Result<Vec<AlgebraicRule>, CompileError> {
let mut observed_rules: Vec<AlgebraicRule> = Vec::new();
let array_axes = declared_axis_names(model);
let declared: std::cell::OnceCell<HashSet<String>> = std::cell::OnceCell::new();
let mut def_eq: HashMap<&str, &crate::types::Equation> = HashMap::new();
for eq in &model.equations {
if let crate::classification::LhsFormRef::Bare(name) =
crate::classification::lhs_form_ref(&eq.lhs)
{
def_eq.entry(name).or_insert(eq);
}
}
for name in observed_names {
let Some(eq) = def_eq.get(name.as_str()) else {
continue;
};
if let Expr::Operator(lhs) = &eq.lhs
&& lhs.op == "index"
{
let declared = declared.get_or_init(|| model.variables.keys().cloned().collect());
check_bare_index_definition(name, lhs, &eq.rhs, &array_axes, declared)?;
}
if let Some(r) = lower_recurrence(name, &eq.lhs, &eq.rhs)? {
observed_rules.push(AlgebraicRule::Recurrence {
var: name.clone(),
output_idx_names: r.idx_names,
output_ranges: r.ranges,
body: std::sync::Arc::new(r.body),
axis: r.axis,
max_lag: r.max_lag,
lag_proven: r.lag_proven,
});
continue;
}
if let Some(a) = extract_algebraic_faq(&eq.lhs, &eq.rhs) {
observed_rules.push(AlgebraicRule::ArrayLoop {
var: a.var,
output_idx_names: a.idx_names,
output_ranges: a.ranges,
body: std::sync::Arc::new(a.body),
});
} else {
observed_rules.push(lower_algebraic_body(
name,
eq.rhs.clone(),
&array_axes,
index_sets,
)?);
}
}
let is_observed: HashSet<&str> = if eliminated.is_empty() {
HashSet::new()
} else {
observed_names.iter().map(String::as_str).collect()
};
for eq in &model.equations {
if let Some(a) = extract_algebraic_faq(&eq.lhs, &eq.rhs) {
if eliminated.contains(&a.var) && !is_observed.contains(a.var.as_str()) {
if let Some(r) = lower_recurrence(&a.var, &eq.lhs, &eq.rhs)? {
observed_rules.push(AlgebraicRule::Recurrence {
var: a.var.clone(),
output_idx_names: r.idx_names,
output_ranges: r.ranges,
body: std::sync::Arc::new(r.body),
axis: r.axis,
max_lag: r.max_lag,
lag_proven: r.lag_proven,
});
continue;
}
observed_rules.push(AlgebraicRule::ArrayLoop {
var: a.var,
output_idx_names: a.idx_names,
output_ranges: a.ranges,
body: std::sync::Arc::new(a.body),
});
}
continue;
}
if let Expr::Variable(name) = &eq.lhs
&& eliminated.contains(name)
&& !is_observed.contains(name.as_str())
{
observed_rules.push(lower_algebraic_body(
name,
eq.rhs.clone(),
&array_axes,
index_sets,
)?);
}
}
dependency_order_observed(observed_rules)
}
fn check_bare_index_definition(
name: &str,
lhs: &ExpressionNode,
rhs: &Expr,
array_axes: &HashMap<String, Vec<String>>,
declared: &HashSet<String>,
) -> Result<(), CompileError> {
let head_is_the_variable = matches!(lhs.args.first(), Some(Expr::Variable(_)));
let subs = lhs.args.get(1..).unwrap_or_default();
let prefix = name.rfind('.').map_or("", |p| &name[..=p]);
let names_binder = |sub: &Expr, binder: &String| match sub {
Expr::Variable(v) => v == binder || v.strip_prefix(prefix) == Some(binder.as_str()),
_ => false,
};
let frame: &[String] = match rhs {
Expr::Operator(node) if is_faq_op(&node.op) => node.output_idx.as_deref().unwrap_or(&[]),
_ => &[],
};
let mut gather = Some(lhs);
while let Some(g) = gather {
for sub in g.args.iter().skip(1) {
if let Expr::Variable(v) = sub
&& !declared.contains(v)
&& !frame.iter().any(|b| names_binder(sub, b))
{
let sym = v.strip_prefix(prefix).unwrap_or(v);
return Err(CompileError::InterpreterBuildError {
details: format!(
"{}: equation defining '{name}' subscripts its left-hand side with \
'{sym}', which no faq binds (esm-spec ยง6.3.1)",
crate::diagnostic::codes::UNBOUND_INDEX_SYMBOL
),
});
}
}
gather = match g.args.first() {
Some(Expr::Operator(inner)) if inner.op == "index" => Some(inner),
_ => None,
};
}
let binds_subscripts = match rhs {
Expr::Operator(node) if is_faq_op(&node.op) => match node.output_idx.as_deref() {
Some(frame) => {
!subs.is_empty()
&& frame.len() == subs.len()
&& subs.iter().zip(frame).all(|(s, b)| names_binder(s, b))
}
None => false,
},
_ => false,
};
let rank_agrees = match array_axes.get(name) {
Some(axes) => axes.len() == subs.len(),
None => true,
};
if head_is_the_variable && binds_subscripts && rank_agrees {
return Ok(());
}
Err(CompileError::InterpreterBuildError {
details: format!(
"indexed_definition_unsupported_form: '{name}' is defined by a bare-index LHS that \
is not runnable; the RHS must be a `faq` whose `output_idx` names the LHS \
subscripts in order (esm-spec ยง6.3.1)"
),
})
}
fn lower_algebraic_body(
name: &str,
body: Expr,
array_axes: &HashMap<String, Vec<String>>,
index_sets: &HashMap<String, IndexSet>,
) -> Result<AlgebraicRule, CompileError> {
let declared_shape = array_axes
.get(name)
.and_then(|axes| resolve_declared_shape(axes, index_sets));
let scalar_rule = |body: Expr| AlgebraicRule::Scalar {
var: name.to_string(),
body: std::sync::Arc::new(body),
declared_shape: declared_shape.clone(),
};
let Some(target_axes) = array_axes.get(name) else {
return Ok(scalar_rule(body));
};
let plan = build_gather_plan(&body, array_axes, name, Some(target_axes.as_slice()), false)?;
if plan_is_identity(&plan, target_axes.len()) {
return Ok(scalar_rule(body));
}
let Some(extents) = resolve_declared_shape(target_axes, index_sets) else {
return Ok(scalar_rule(body));
};
let loops: Vec<String> = (0..extents.len())
.map(|d| format!("_lp{d}_{name}"))
.collect();
let output_ranges: Vec<(i64, i64)> = extents.iter().map(|n| (1i64, *n as i64)).collect();
let looped = index_array_leaves_by_loops(&body, array_axes, Some(&plan), &loops);
Ok(AlgebraicRule::ArrayLoop {
var: name.to_string(),
output_idx_names: loops,
output_ranges,
body: std::sync::Arc::new(looped),
})
}
fn classify_field_ics(model: &Model) -> Vec<(String, Expr)> {
let mut field_ics: Vec<(String, Expr)> = Vec::new();
for eq in &model.equations {
if let Some(target) = crate::flatten::extract_ic_target(&eq.lhs) {
field_ics.push((target, eq.rhs.clone()));
}
}
field_ics
}
fn classify_initialization_faqs(model: &Model) -> Vec<(String, Expr)> {
model
.initialization_equations
.iter()
.flatten()
.filter_map(|eq| match (&eq.lhs, &eq.rhs) {
(Expr::Variable(target), Expr::Operator(node))
if is_faq_op(&node.op)
&& node.output_idx.as_ref().is_some_and(|o| !o.is_empty()) =>
{
Some((target.clone(), eq.rhs.clone()))
}
_ => None,
})
.collect()
}
fn build_rhs_rules(
model: &Model,
slots: &SlotTables,
held_at_ic: &HashSet<String>,
) -> Result<Vec<RhsRule>, CompileError> {
let var_shapes = &slots.var_shapes;
let mut rhs_rules: Vec<RhsRule> = Vec::new();
let mut covered_slots = SlotCoverage::new(slots.n_states);
let array_axes = declared_axis_names(model);
for eq in &model.equations {
if let Some(d) = extract_derivative_faq(&eq.lhs, &eq.rhs) {
lower_faq_derivative(d, var_shapes, &mut covered_slots, &mut rhs_rules)?;
continue;
}
if let Some((var, idx_opt)) = extract_derivative_scalar(&eq.lhs) {
match idx_opt {
Some(indices) => lower_indexed_derivative(
var,
&indices,
&eq.rhs,
var_shapes,
&mut covered_slots,
&mut rhs_rules,
)?,
None => lower_bare_derivative(
var,
&eq.rhs,
model,
&array_axes,
var_shapes,
&mut covered_slots,
&mut rhs_rules,
)?,
}
continue;
}
}
cover_held_at_ic_slots(held_at_ic, var_shapes, &mut covered_slots);
check_state_slots_covered(slots, &covered_slots)?;
Ok(rhs_rules)
}
fn lower_faq_derivative(
d: DerivArrayop,
var_shapes: &IndexMap<String, VarShape>,
covered_slots: &mut SlotCoverage,
rhs_rules: &mut Vec<RhsRule>,
) -> Result<(), CompileError> {
let DerivArrayop {
var,
idx_names,
ranges,
lhs_idx_exprs,
body,
contract_names,
contract_dims,
reduce,
filter,
} = d;
if !var_shapes.contains_key(&var) {
return Err(CompileError::build_err(format!(
"Array-op derivative targets unknown state variable '{var}'"
)));
}
mark_faq_lhs_coverage(
&var_shapes[&var],
&idx_names,
&ranges,
&lhs_idx_exprs,
covered_slots,
);
rhs_rules.push(RhsRule::ArrayLoop {
var_name: var,
output_idx_names: idx_names,
output_ranges: ranges,
lhs_idx_exprs,
body: Box::new(body),
contract_names,
contract_dims,
reduce,
filter,
});
Ok(())
}
fn lower_indexed_derivative(
var: String,
indices: &[i64],
rhs: &Expr,
var_shapes: &IndexMap<String, VarShape>,
covered_slots: &mut SlotCoverage,
rhs_rules: &mut Vec<RhsRule>,
) -> Result<(), CompileError> {
let shape = var_shapes.get(&var).ok_or_else(|| {
CompileError::build_err(format!(
"Scalar derivative targets unknown state variable '{var}'"
))
})?;
let flat = multi_to_flat_col_major(indices, &shape.shape, &shape.origin);
let slot = shape.flat_offset + flat;
covered_slots.insert(slot);
rhs_rules.push(RhsRule::IndexedScalar {
slot,
body: Box::new(rhs.clone()),
});
Ok(())
}
fn lower_bare_derivative(
var: String,
rhs: &Expr,
model: &Model,
array_axes: &HashMap<String, Vec<String>>,
var_shapes: &IndexMap<String, VarShape>,
covered_slots: &mut SlotCoverage,
rhs_rules: &mut Vec<RhsRule>,
) -> Result<(), CompileError> {
let shape = var_shapes
.get(&var)
.ok_or_else(|| {
CompileError::build_err(format!(
"Scalar derivative targets unknown state variable '{var}'"
))
})?
.clone();
let target_axes: Option<&[String]> = model
.variables
.get(&var)
.and_then(|v| v.shape.as_deref())
.filter(|d| d.len() == shape.shape.len());
if shape.shape.is_empty() {
let slot = shape.flat_offset;
covered_slots.insert(slot);
rhs_rules.push(RhsRule::Scalar {
slot,
body: Box::new(rhs.clone()),
});
} else if rhs_has_array_producer(rhs) {
lower_wholearray_producer_lift(
var,
rhs,
&shape,
target_axes,
array_axes,
covered_slots,
rhs_rules,
)?;
} else {
lower_wholearray_percell(
var,
rhs,
&shape,
target_axes,
array_axes,
var_shapes,
covered_slots,
rhs_rules,
)?;
}
Ok(())
}
fn lower_wholearray_producer_lift(
var: String,
rhs: &Expr,
shape: &VarShape,
target_axes: Option<&[String]>,
array_axes: &HashMap<String, Vec<String>>,
covered_slots: &mut SlotCoverage,
rhs_rules: &mut Vec<RhsRule>,
) -> Result<(), CompileError> {
let ndim = shape.shape.len();
let loops: Vec<String> = (0..ndim).map(|d| format!("_lp{d}_{var}")).collect();
let output_ranges: Vec<(i64, i64)> = shape
.shape
.iter()
.zip(shape.origin.iter())
.map(|(sz, o)| (*o, *o + *sz as i64 - 1))
.collect();
let lhs_idx_exprs: Vec<Expr> = loops.iter().map(|l| Expr::Variable(l.clone())).collect();
let plan = build_gather_plan(rhs, array_axes, &var, target_axes, false)?;
let body = index_array_leaves_by_loops(rhs, array_axes, Some(&plan), &loops);
let total = shape.shape.iter().copied().product::<usize>().max(1);
covered_slots.insert_range(shape.flat_offset, total);
rhs_rules.push(RhsRule::ArrayLoop {
var_name: var.clone(),
output_idx_names: loops,
output_ranges,
lhs_idx_exprs,
body: Box::new(body),
contract_names: Vec::new(),
contract_dims: Vec::new(),
reduce: ReduceKind::Sum,
filter: None,
});
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn lower_wholearray_percell(
var: String,
rhs: &Expr,
shape: &VarShape,
target_axes: Option<&[String]>,
array_axes: &HashMap<String, Vec<String>>,
var_shapes: &IndexMap<String, VarShape>,
covered_slots: &mut SlotCoverage,
rhs_rules: &mut Vec<RhsRule>,
) -> Result<(), CompileError> {
let plan = build_gather_plan(rhs, array_axes, &var, target_axes, true)?;
let total = shape.shape.iter().copied().product::<usize>().max(1);
if wholearray_body_loops_as_percell(rhs, array_axes, var_shapes) {
let ndim = shape.shape.len();
let loops: Vec<String> = (0..ndim).map(|d| format!("_lp{d}_{var}")).collect();
let output_ranges: Vec<(i64, i64)> = shape
.shape
.iter()
.zip(shape.origin.iter())
.map(|(sz, o)| (*o, *o + *sz as i64 - 1))
.collect();
let lhs_idx_exprs: Vec<Expr> = loops.iter().map(|l| Expr::Variable(l.clone())).collect();
let body = index_array_leaves(rhs, array_axes, Some(&plan), CellCoords::Loops(&loops));
covered_slots.insert_range(shape.flat_offset, total);
rhs_rules.push(RhsRule::ArrayLoop {
var_name: var,
output_idx_names: loops,
output_ranges,
lhs_idx_exprs,
body: Box::new(body),
contract_names: Vec::new(),
contract_dims: Vec::new(),
reduce: ReduceKind::Sum,
filter: None,
});
return Ok(());
}
for flat in 0..total {
let multi0 = flat_to_multi_col_major(flat, &shape.shape);
let cell: Vec<i64> = multi0
.iter()
.zip(shape.origin.iter())
.map(|(m, o)| *m as i64 + *o)
.collect();
let body = index_array_leaves(rhs, array_axes, Some(&plan), CellCoords::Ints(&cell));
let slot = shape.flat_offset + flat;
covered_slots.insert(slot);
rhs_rules.push(RhsRule::IndexedScalar {
slot,
body: Box::new(body),
});
}
Ok(())
}
fn cover_held_at_ic_slots(
held_at_ic: &HashSet<String>,
var_shapes: &IndexMap<String, VarShape>,
covered_slots: &mut SlotCoverage,
) {
for name in held_at_ic {
if let Some(vs) = var_shapes.get(name) {
let total = vs.shape.iter().copied().product::<usize>().max(1);
covered_slots.insert_range(vs.flat_offset, total);
}
}
}
fn check_state_slots_covered(
slots: &SlotTables,
covered_slots: &SlotCoverage,
) -> Result<(), CompileError> {
match covered_slots.first_uncovered() {
Some(slot) => {
let name = slot_name(&slots.var_shapes, slot).unwrap_or_default();
Err(CompileError::build_err(format!(
"State slot '{name}' has no defining derivative equation."
)))
}
None => Ok(()),
}
}
pub(crate) fn eval_buildtime_field(
expr: &Expr,
index_sets: &HashMap<String, IndexSet>,
params: &HashMap<String, f64>,
) -> Result<Value, CompileError> {
eval_buildtime_field_in_scope(expr, index_sets, params, &HashMap::new())
}
pub(crate) fn eval_buildtime_field_in_scope(
expr: &Expr,
index_sets: &HashMap<String, IndexSet>,
params: &HashMap<String, f64>,
scope: &HashMap<String, ArrayD<f64>>,
) -> Result<Value, CompileError> {
let mut resolved = expr.clone();
crate::faq::resolve_expr_ranges(&mut resolved, index_sets)?;
let param_names: Vec<String> = params.keys().cloned().collect();
let param_vec: Vec<f64> = param_names.iter().map(|n| params[n]).collect();
eval_expression(&resolved, scope, ¶m_vec, ¶m_names, 0.0)
}
pub(super) fn resolve_field_ic_cell(
target: &str,
rhs: &Expr,
cell: &[usize],
forcing: &HashMap<String, ArrayD<f64>>,
index_sets: &HashMap<String, IndexSet>,
params: &HashMap<String, f64>,
cached_field: &mut Option<Value>,
) -> Result<f64, SimulateError> {
if let Expr::Variable(name) = rhs
&& let Some(arr) = forcing.get(name)
{
if arr.ndim() == cell.len() {
return Ok(arr[IxDyn(cell)]);
} else if arr.len() == 1 {
return Ok(arr.iter().copied().next().unwrap());
}
return Err(SimulateError::InvalidFieldInitialCondition {
name: target.to_string(),
details: format!(
"loaded field '{name}' has ndim={} which does not match the {}-D lifted target grid",
arr.ndim(),
cell.len()
),
});
}
if let Some(Value::Scalar(c)) = cached_field.as_ref()
&& c.is_finite()
{
return Ok(*c);
}
if cached_field.is_none()
&& let Ok(c) = crate::expression::evaluate(rhs, params)
&& c.is_finite()
{
*cached_field = Some(Value::Scalar(c));
return Ok(c);
}
if let Expr::Operator(_) = rhs {
if cached_field.is_none() {
if let Ok(v) = eval_buildtime_field(rhs, index_sets, params) {
*cached_field = Some(v);
}
}
match cached_field.as_ref() {
Some(Value::Scalar(s)) if s.is_finite() => return Ok(*s),
Some(Value::Array(arr)) => {
if arr.ndim() != cell.len() {
return Err(SimulateError::InvalidFieldInitialCondition {
name: target.to_string(),
details: format!(
"coordinate expression evaluates to ndim={}, which does not match the {}-D lifted target grid",
arr.ndim(),
cell.len()
),
});
}
let v = arr[IxDyn(cell)];
if v.is_finite() {
return Ok(v);
}
}
_ => {}
}
}
let hint = match rhs {
Expr::Variable(name) => format!(" (no provider field named '{name}')"),
_ => String::new(),
};
Err(SimulateError::InvalidFieldInitialCondition {
name: target.to_string(),
details: format!(
"RHS is neither a provider-served loaded field, a constant, nor a per-cell coordinate expression{hint}"
),
})
}
pub(super) fn equation_defined_var(lhs: &Expr) -> Option<String> {
match lhs {
Expr::Variable(v) => Some(v.clone()),
Expr::Operator(node) => match node.op.as_str() {
"index" => match node.args.first() {
Some(Expr::Variable(v)) => Some(v.clone()),
_ => None,
},
"D" | "ic" => match node.args.first() {
Some(Expr::Variable(v)) => Some(v.clone()),
Some(inner) => equation_defined_var(inner),
None => None,
},
op if is_faq_op(op) => node.expr.as_ref().and_then(|b| equation_defined_var(b)),
_ => None,
},
_ => None,
}
}
pub(super) fn algebraic_defined_var(lhs: &Expr) -> Option<String> {
match lhs {
Expr::Variable(v) => Some(v.clone()),
Expr::Operator(node) => match node.op.as_str() {
"index" => match node.args.first() {
Some(Expr::Variable(v)) => Some(v.clone()),
_ => None,
},
op if is_faq_op(op) => {
let body = node.expr.as_ref()?;
if let Expr::Operator(b) = body.as_ref() {
if b.op == "D" {
return None;
}
}
equation_defined_var(body)
}
_ => None,
},
_ => None,
}
}
pub(super) fn collect_algebraic_defined(equations: &[crate::types::Equation]) -> HashSet<String> {
let mut out = HashSet::new();
for eq in equations {
if let Some(v) = algebraic_defined_var(&eq.lhs) {
out.insert(v);
}
}
out
}
pub(super) fn expr_contains_skolem(expr: &Expr) -> bool {
match expr {
Expr::Number(_) | Expr::Integer(_) | Expr::Variable(_) => false,
Expr::Operator(node) => node.op == "skolem" || node.any_child(&mut expr_contains_skolem),
}
}
fn ranks_a_derived_set(expr: &Expr, index_sets: &HashMap<String, IndexSet>) -> bool {
match expr {
Expr::Operator(node) => {
(node.op == "rank"
&& node.args.iter().any(|a| {
matches!(a, Expr::Variable(v)
if index_sets.get(v).is_some_and(|is| is.kind == "derived"))
}))
|| node.any_child(&mut |c| ranks_a_derived_set(c, index_sets))
}
_ => false,
}
}
pub(super) fn collect_geometry_producer_ids(expr: &Expr, out: &mut HashSet<String>) {
let Expr::Operator(node) = expr else {
return;
};
if matches!(
node.op.as_str(),
"intersect_polygon" | "polygon_intersection_area"
) {
if let Some(id) = &node.id {
out.insert(id.clone());
}
}
node.for_each_child(&mut |child| collect_geometry_producer_ids(child, out));
}
pub(super) fn strip_vi_joins(expr: &mut Expr, vi_cols: &HashSet<String>) {
fn contains_join(e: &Expr) -> bool {
match e {
Expr::Operator(node) => node.join.is_some() || node.any_child(&mut contains_join),
_ => false,
}
}
if !contains_join(expr) {
return;
}
let Some(node) = expr.node_mut() else {
return;
};
if let Some(joins) = &mut node.join {
for clause in joins.iter_mut() {
clause
.on
.retain(|pair| !pair.iter().any(|c| vi_cols.contains(c)));
}
joins.retain(|clause| !clause.on.is_empty() || clause.overlap.is_some());
if joins.is_empty() {
node.join = None;
}
if is_faq_op(&node.op) {
node.args
.retain(|a| !matches!(a, Expr::Variable(v) if vi_cols.contains(v)));
}
}
node.for_each_child_mut(&mut |child| strip_vi_joins(child, vi_cols));
}
pub(super) fn strip_value_invention(
model: &mut Model,
index_sets: &HashMap<String, IndexSet>,
vi: &ViCensus,
) -> Result<(), CompileError> {
let mut vi_vars: HashSet<String> = HashSet::new();
let any_derived = index_sets.values().any(|is| is.kind == "derived");
let mut geom_ids: HashSet<String> = HashSet::new();
if any_derived && vi.geometry {
for eq in &model.equations {
collect_geometry_producer_ids(&eq.rhs, &mut geom_ids);
}
for var in model.variables.values() {
var.for_each_expression(&mut |expr| collect_geometry_producer_ids(expr, &mut geom_ids));
}
}
let const_defined: HashSet<String> = model
.equations
.iter()
.filter(|eq| matches!(&eq.rhs, Expr::Operator(n) if n.op == "const"))
.filter_map(|eq| equation_defined_var(&eq.lhs))
.collect();
for (name, var) in &model.variables {
if var.var_type == VariableType::Parameter || const_defined.contains(name) {
continue;
}
if let Some(shape) = &var.shape {
if shape.iter().any(|s| {
index_sets
.get(s)
.filter(|is| is.kind == "derived")
.map(|is| {
!is.from_faq
.as_deref()
.map(|f| geom_ids.contains(f))
.unwrap_or(false)
})
.unwrap_or(false)
}) {
vi_vars.insert(name.clone());
}
}
}
for eq in &model.equations {
if (vi.skolem && expr_contains_skolem(&eq.rhs))
|| (any_derived && ranks_a_derived_set(&eq.rhs, index_sets))
{
if let Some(v) = equation_defined_var(&eq.lhs) {
vi_vars.insert(v);
}
}
}
if vi_vars.is_empty() {
return Ok(());
}
let stripped_bodies: HashMap<String, Expr> = model
.equations
.iter()
.filter_map(|eq| {
let v = equation_defined_var(&eq.lhs)?;
vi_vars.contains(&v).then(|| (v, eq.rhs.clone()))
})
.collect();
model.variables.retain(|k, _| !vi_vars.contains(k));
model.equations.retain(|eq| {
equation_defined_var(&eq.lhs)
.map(|v| !vi_vars.contains(&v))
.unwrap_or(true)
});
let mut vi_cols: HashSet<String> = vi_vars.clone();
for v in &vi_vars {
if let Some(pos) = v.rfind('.') {
vi_cols.insert(v[pos + 1..].to_string());
}
}
for eq in &mut model.equations {
strip_vi_joins(&mut eq.lhs, &vi_cols);
strip_vi_joins(&mut eq.rhs, &vi_cols);
}
for var in model.variables.values_mut() {
var.for_each_expression_mut(&mut |expr| strip_vi_joins(expr, &vi_cols));
}
for eq in &model.equations {
let mut hit: Option<String> = None;
let mut find = |expr: &Expr| {
if hit.is_none() {
hit = first_variable_reference_in(expr, &vi_vars);
}
};
find(&eq.lhs);
find(&eq.rhs);
if let Some(name) = hit {
let op = stripped_producer_op(&name, &stripped_bodies)
.unwrap_or_else(|| "skolem".to_string());
return Err(CompileError::UnevaluableOperatorError { op });
}
}
Ok(())
}
fn first_variable_reference_in(expr: &Expr, stripped: &HashSet<String>) -> Option<String> {
match expr {
Expr::Variable(v) if stripped.contains(v) => Some(v.clone()),
Expr::Operator(node) => {
let mut out = None;
node.any_child(&mut |child| {
out = first_variable_reference_in(child, stripped);
out.is_some()
});
out
}
_ => None,
}
}
fn stripped_producer_op(name: &str, bodies: &HashMap<String, Expr>) -> Option<String> {
fn head(expr: &Expr) -> Option<String> {
match expr {
Expr::Operator(node) if !crate::simulate_array::is_evaluable_op(&node.op) => {
Some(node.op.clone())
}
Expr::Operator(node) => {
let mut out = None;
node.any_child(&mut |child| {
out = head(child);
out.is_some()
});
out
}
_ => None,
}
}
head(bodies.get(name)?)
}
fn expr_contains_arg_witness(expr: &Expr) -> bool {
match expr {
Expr::Number(_) | Expr::Integer(_) | Expr::Variable(_) => false,
Expr::Operator(node) => {
node.op == "argmin"
|| node.op == "argmax"
|| node.any_child(&mut expr_contains_arg_witness)
}
}
}
pub(super) struct ViCensus {
arg_witness: bool,
skolem: bool,
geometry: bool,
}
impl ViCensus {
fn of(model: &Model) -> Self {
fn walk(expr: &Expr, c: &mut ViCensus) {
let Expr::Operator(node) = expr else {
return;
};
match node.op.as_str() {
"argmin" | "argmax" => c.arg_witness = true,
"skolem" => c.skolem = true,
"intersect_polygon" | "polygon_intersection_area" => c.geometry = true,
_ => {}
}
node.for_each_child(&mut |child| walk(child, c));
}
let mut c = ViCensus {
arg_witness: false,
skolem: false,
geometry: false,
};
for eq in &model.equations {
walk(&eq.lhs, &mut c);
walk(&eq.rhs, &mut c);
}
for var in model.variables.values() {
var.for_each_expression(&mut |expr| walk(expr, &mut c));
}
c
}
}
fn model_contains_arg_witness(model: &Model) -> bool {
model
.equations
.iter()
.any(|eq| expr_contains_arg_witness(&eq.lhs) || expr_contains_arg_witness(&eq.rhs))
|| model.variables.values().any(|v| {
let mut found = false;
v.for_each_expression(&mut |e| found |= expr_contains_arg_witness(e));
found
})
}
#[allow(clippy::type_complexity)]
fn forcing_default_values(
observed_vars: &[(&String, &ModelVariable)],
forcing_decls: &IndexMap<String, Option<Vec<usize>>>,
) -> Result<HashMap<String, Option<(Vec<usize>, Vec<f64>)>>, CompileError> {
let mut out = HashMap::new();
for (name, var) in observed_vars {
let (Some(default), Some(decl)) = (var.default.as_ref(), forcing_decls.get(name.as_str()))
else {
continue;
};
let Some(want) = decl else {
out.insert((*name).clone(), None);
continue;
};
let values = if let Some(scalar) = default.as_scalar() {
vec![scalar; want.iter().product::<usize>()]
} else {
let (shape, values) = default.to_dense().map_err(|e| {
CompileError::build_err(format!("parameter '{name}': {e} (esm-spec ยง6.3)"))
})?;
if &shape != want {
return Err(CompileError::build_err(format!(
"parameter '{name}': inline array data has shape {shape:?}, which does not \
match the declared shape {want:?} (esm-spec ยง6.6.2)"
)));
}
values
};
out.insert((*name).clone(), Some((want.clone(), values)));
}
Ok(out)
}
pub(crate) fn data_feed_source(spec: &crate::types::ParameterUpdateSpec) -> Option<&str> {
spec.rules().iter().find_map(|r| {
r.data_source()
.filter(|_| r.value().is_some_and(|v| v.from.is_some()))
})
}
fn externally_refreshed(var: &ModelVariable) -> bool {
let Some(spec) = &var.update else {
return false;
};
spec.rules().iter().all(|rule| {
rule.value()
.is_some_and(|v| v.from.is_some() || v.handler.is_some())
})
}
fn observed_bodies(model: &Model) -> std::collections::BTreeMap<String, Expr> {
crate::classification::observed_definitions_of_parts(&model.variables, &model.equations)
}
fn collect_const_factor_arrays(model: &Model) -> HashMap<String, ArrayD<f64>> {
let mut out: HashMap<String, ArrayD<f64>> = HashMap::new();
for (name, body) in observed_bodies(model) {
let expr = &body;
let Expr::Operator(node) = expr else { continue };
if node.op != "const" {
continue;
}
match eval_expression(expr, &HashMap::new(), &[], &[], 0.0) {
Ok(Value::Array(a)) => {
out.insert(name.clone(), *a);
}
Ok(Value::Scalar(s)) => {
out.insert(name.clone(), ArrayD::from_elem(IxDyn(&[]), s));
}
Err(_) => {}
}
}
out
}
fn vi_factor_arrays<S: std::hash::BuildHasher>(
model: &Model,
caller_arrays: Option<&HashMap<String, ArrayD<f64>, S>>,
) -> HashMap<String, ArrayD<f64>> {
let mut arrays = collect_const_factor_arrays(model);
if let Some(extra) = caller_arrays {
for (name, arr) in extra {
arrays.insert(name.clone(), arr.clone());
}
}
arrays
}
fn collect_scalar_params(model: &Model, overrides: &HashMap<String, f64>) -> HashMap<String, f64> {
let mut out: HashMap<String, f64> = HashMap::new();
for (name, var) in &model.variables {
if var.var_type != VariableType::Parameter
|| !var.shape.as_ref().map(|s| s.is_empty()).unwrap_or(true)
{
continue;
}
let tail = name.rsplit('.').next().unwrap_or(name);
let value = overrides
.get(name)
.or_else(|| overrides.get(tail))
.copied()
.or_else(|| var.default_scalar());
if let Some(v) = value {
out.insert(name.clone(), v);
}
}
out
}
fn rewrite_equation_to_const(model: &mut Model, name: &str, buf: &[f64]) {
let value = JsonValue::Array(
buf.iter()
.map(|&v| {
serde_json::Number::from_f64(v)
.map(JsonValue::Number)
.unwrap_or(JsonValue::Null)
})
.collect(),
);
let const_node = Expr::operator(ExpressionNode {
op: "const".to_string(),
value: Some(value),
..Default::default()
});
let mut replaced = false;
for eq in &mut model.equations {
if equation_defined_var(&eq.lhs).as_deref() == Some(name) {
eq.lhs = Expr::Variable(name.to_string());
eq.rhs = const_node.clone();
replaced = true;
}
}
if !replaced && model.variables.contains_key(name) {
model.equations.push(crate::types::Equation {
lhs: Expr::Variable(name.to_string()),
rhs: const_node,
comment: None,
});
}
}
pub fn run_value_invention<S: std::hash::BuildHasher>(
model: &Model,
index_sets: &HashMap<String, IndexSet>,
caller_arrays: Option<&HashMap<String, ArrayD<f64>, S>>,
) -> Result<ValueInventionResult, CompileError> {
run_value_invention_with_params(model, index_sets, caller_arrays, &HashMap::new())
}
pub fn run_value_invention_with_params<S: std::hash::BuildHasher>(
model: &Model,
index_sets: &HashMap<String, IndexSet>,
caller_arrays: Option<&HashMap<String, ArrayD<f64>, S>>,
overrides: &HashMap<String, f64>,
) -> Result<ValueInventionResult, CompileError> {
let const_arrays = vi_factor_arrays(model, caller_arrays);
let params = collect_scalar_params(model, overrides);
let mut model_json = serde_json::to_value(model).map_err(|e| {
CompileError::build_err(format!("value-invention: could not serialize model: {e}"))
})?;
if let JsonValue::Object(m) = &mut model_json {
let is_json = serde_json::to_value(index_sets).map_err(|e| {
CompileError::build_err(format!(
"value-invention: could not serialize index_sets: {e}"
))
})?;
m.insert("index_sets".to_string(), is_json);
}
materialize_value_invention(&model_json, &const_arrays, ¶ms, &HashMap::new()).map_err(|e| {
CompileError::build_err(format!("value-invention materialize failed: {}", e.0))
})
}
pub(super) struct DerivedMaterialization {
pub(super) extents: HashMap<String, i64>,
geometry_ids: HashSet<String>,
failure: Option<String>,
map_codes: HashMap<String, Vec<f64>>,
}
fn materialize_derived_extents(
model: &Model,
index_sets: &HashMap<String, IndexSet>,
caller_arrays: Option<&HashMap<String, ArrayD<f64>>>,
vi: &ViCensus,
) -> DerivedMaterialization {
let mut geometry_ids = HashSet::new();
if vi.geometry {
for eq in &model.equations {
collect_geometry_producer_ids(&eq.rhs, &mut geometry_ids);
}
for var in model.variables.values() {
var.for_each_expression(&mut |expr| {
collect_geometry_producer_ids(expr, &mut geometry_ids)
});
}
}
let mut out = DerivedMaterialization {
extents: HashMap::new(),
geometry_ids,
failure: None,
map_codes: HashMap::new(),
};
let needs_value_invention = index_sets.values().any(|is| {
is.kind == "derived"
&& is
.from_faq
.as_deref()
.is_some_and(|f| !out.geometry_ids.contains(f))
}) || (vi.skolem && skolem_map_keys_a_join(model));
if needs_value_invention {
let factors = value_invention_factors(model, index_sets, caller_arrays);
let lean = without_factor_literals(model, &factors);
match run_value_invention(&lean, index_sets, Some(&factors)) {
Ok(result) => {
out.extents = result.extents;
out.map_codes = result.map_codes;
}
Err(e) => out.failure = Some(e.to_string()),
}
}
out
}
fn skolem_map_keys_a_join(model: &Model) -> bool {
let mut maps: HashSet<String> = HashSet::new();
for eq in &model.equations {
if expr_contains_skolem(&eq.rhs)
&& let Some(v) = equation_defined_var(&eq.lhs)
{
if let Some(pos) = v.rfind('.') {
maps.insert(v[pos + 1..].to_string());
}
maps.insert(v);
}
}
if maps.is_empty() {
return false;
}
fn keys_on(e: &Expr, maps: &HashSet<String>) -> bool {
let Expr::Operator(node) = e else {
return false;
};
node.join.as_ref().is_some_and(|j| {
j.iter()
.any(|c| c.on.iter().flatten().any(|col| maps.contains(col)))
}) || node.any_child(&mut |c| keys_on(c, maps))
}
model.equations.iter().any(|eq| keys_on(&eq.rhs, &maps))
}
fn value_invention_factors(
model: &Model,
index_sets: &HashMap<String, IndexSet>,
caller_arrays: Option<&HashMap<String, ArrayD<f64>>>,
) -> HashMap<String, ArrayD<f64>> {
fn index_targets(e: &Expr, out: &mut BTreeSet<String>) {
let Expr::Operator(node) = e else {
return;
};
if node.op == "index"
&& let Some(Expr::Variable(v)) = node.args.first()
{
out.insert(v.clone());
}
node.for_each_child(&mut |c| index_targets(c, out));
}
fn free_names(e: &Expr, bound: &mut Vec<String>, out: &mut BTreeSet<String>) {
match e {
Expr::Variable(v) if !bound.contains(v) => {
out.insert(v.clone());
}
Expr::Operator(node) => {
let mark = bound.len();
bound.extend(node.output_idx.iter().flatten().cloned());
bound.extend(node.ranges.iter().flat_map(|r| r.keys().cloned()));
node.for_each_child(&mut |c| free_names(c, bound, out));
bound.truncate(mark);
}
_ => {}
}
}
struct Cx<'a> {
defs: BTreeMap<String, Expr>,
index_sets: &'a HashMap<String, IndexSet>,
scope: HashMap<String, ArrayD<f64>>,
}
fn derive(cx: &mut Cx<'_>, name: &str, depth: usize) -> bool {
if cx.scope.contains_key(name) {
return true;
}
if depth > 16 {
return false;
}
let Some(body) = cx.defs.get(name).cloned() else {
return false;
};
let mut free = BTreeSet::new();
free_names(&body, &mut Vec::new(), &mut free);
for v in &free {
if !cx.index_sets.contains_key(v) && !derive(cx, v, depth + 1) {
return false;
}
}
match eval_buildtime_field_in_scope(&body, cx.index_sets, &HashMap::new(), &cx.scope) {
Ok(Value::Array(a)) => {
cx.scope.insert(name.to_string(), *a);
true
}
_ => false,
}
}
let mut targets = BTreeSet::new();
for eq in &model.equations {
if expr_contains_skolem(&eq.rhs) {
index_targets(&eq.rhs, &mut targets);
}
}
let mut cx = Cx {
defs: observed_bodies(model),
index_sets,
scope: vi_factor_arrays(model, caller_arrays),
};
for t in &targets {
derive(&mut cx, t, 0);
}
cx.scope
}
fn without_factor_literals(model: &Model, factors: &HashMap<String, ArrayD<f64>>) -> Model {
let mut lean = model.clone();
for eq in &mut lean.equations {
let is_factor = matches!(&eq.rhs, Expr::Operator(n) if n.op == "const")
&& equation_defined_var(&eq.lhs).is_some_and(|v| factors.contains_key(&v));
if is_factor {
eq.rhs = Expr::operator(ExpressionNode {
op: "const".into(),
value: Some(serde_json::Value::from(0)),
..Default::default()
});
}
}
lean
}
pub(super) fn first_unmaterialized_derived_range(
expr: &Expr,
extents: &HashMap<String, i64>,
geometry_ids: &HashSet<String>,
) -> Option<String> {
let Expr::Operator(node) = expr else {
return None;
};
if let Some(ranges) = &node.ranges {
let mut unmaterialized: Vec<&str> = ranges
.values()
.filter_map(|spec| spec.derived())
.filter(|f| !extents.contains_key(*f) && !geometry_ids.contains(*f))
.collect();
unmaterialized.sort_unstable();
if let Some(f) = unmaterialized.first() {
return Some((*f).to_string());
}
}
let mut found = None;
node.any_child(&mut |child| {
found = first_unmaterialized_derived_range(child, extents, geometry_ids);
found.is_some()
});
found
}
pub(super) fn unmaterialized_derived_error(
from_faq: &str,
index_sets: &HashMap<String, IndexSet>,
failure: Option<&str>,
) -> CompileError {
let mut names: Vec<&str> = index_sets
.iter()
.filter(|(_, is)| is.from_faq.as_deref() == Some(from_faq))
.map(|(name, _)| name.as_str())
.collect();
names.sort_unstable();
let subject = match names.first() {
Some(set) => format!("derived index set '{set}' (from_faq '{from_faq}')"),
None => format!("the derived index set produced by '{from_faq}'"),
};
match failure {
Some(reason)
if reason.contains(crate::diagnostic::codes::RELATIONAL_NODE_IN_CONTINUOUS) =>
{
CompileError::ValueInventionRefused {
code: crate::diagnostic::codes::RELATIONAL_NODE_IN_CONTINUOUS,
reason: format!("{subject}: {reason}"),
}
}
_ => CompileError::ValueInventionRefused {
code: crate::diagnostic::codes::DERIVED_INDEX_SET_UNMATERIALIZED,
reason: format!(
"{subject} is not materialized: {}",
failure.unwrap_or("no geometry or value-invention producer supplied its extent")
),
},
}
}
impl DerivedMaterialization {
fn refuse_on_failure(
&self,
index_sets: &HashMap<String, IndexSet>,
) -> Result<(), CompileError> {
let Some(failure) = self.failure.as_deref() else {
return Ok(());
};
let mut sets: Vec<(&String, &str)> = index_sets
.iter()
.filter(|(_, is)| is.kind == "derived")
.filter_map(|(n, is)| Some((n, is.from_faq.as_deref()?)))
.filter(|(_, f)| !self.geometry_ids.contains(*f))
.collect();
sets.sort();
match sets.first() {
Some((_, from_faq)) => Err(unmaterialized_derived_error(
from_faq,
index_sets,
Some(failure),
)),
None => Ok(()),
}
}
}
struct OpCensus {
ranges: bool,
faq: bool,
index: bool,
}
impl OpCensus {
fn of(model: &Model) -> Self {
fn walk(expr: &Expr, c: &mut OpCensus) {
let Expr::Operator(node) = expr else {
return;
};
c.ranges |= node.ranges.is_some();
c.faq |= crate::faq::is_faq_op(&node.op);
c.index |= node.op == "index";
if !(c.ranges && c.faq && c.index) {
node.for_each_child(&mut |child| walk(child, c));
}
}
let mut c = OpCensus {
ranges: false,
faq: false,
index: false,
};
for eq in model
.equations
.iter()
.chain(model.initialization_equations.iter().flatten())
{
walk(&eq.lhs, &mut c);
walk(&eq.rhs, &mut c);
}
for var in model.variables.values() {
var.for_each_expression(&mut |expr| walk(expr, &mut c));
}
c
}
}
fn refuse_unmaterialized_derived_ranges(
model: &Model,
index_sets: &HashMap<String, IndexSet>,
derived: &DerivedMaterialization,
) -> Result<(), CompileError> {
let mut found: Option<String> = None;
let mut visit = |expr: &Expr| {
if found.is_none() {
found =
first_unmaterialized_derived_range(expr, &derived.extents, &derived.geometry_ids);
}
};
for eq in &model.equations {
visit(&eq.lhs);
visit(&eq.rhs);
}
for eq in model.initialization_equations.iter().flatten() {
visit(&eq.lhs);
visit(&eq.rhs);
}
for var in model.variables.values() {
var.for_each_expression(&mut |expr| visit(expr));
}
match found {
Some(from_faq) => Err(unmaterialized_derived_error(
&from_faq,
index_sets,
derived.failure.as_deref(),
)),
None => Ok(()),
}
}
fn materialize_vi_outputs_to_data(
model: &mut Model,
index_sets: &mut HashMap<String, IndexSet>,
caller_arrays: Option<&HashMap<String, ArrayD<f64>>>,
) -> Result<(), CompileError> {
if !model_contains_arg_witness(model) {
return Ok(());
}
let result = run_value_invention(model, index_sets, caller_arrays)?;
rewrite_derived_index_sets(index_sets, &result.extents);
for (name, buf) in &result.assignments {
let as_f64: Vec<f64> = buf.iter().map(|&i| i as f64).collect();
rewrite_equation_to_const(model, name, &as_f64);
}
for (name, buf) in &result.groups {
rewrite_equation_to_const(model, name, buf);
}
Ok(())
}
pub(super) fn resolve_declared_shape(
decl: &[String],
index_sets: &HashMap<String, IndexSet>,
) -> Option<Vec<usize>> {
let mut out = Vec::with_capacity(decl.len());
for s in decl {
let is = index_sets.get(s)?;
let sz = match is.kind.as_str() {
"interval" => is.size? as usize,
"categorical" => is.members.as_ref()?.len(),
_ => return None,
};
out.push(sz);
}
Some(out)
}
pub(super) fn is_array_producer(node: &ExpressionNode) -> bool {
if node.op == "makearray" {
return true;
}
is_faq_op(&node.op)
&& node
.output_idx
.as_ref()
.map(|v| !v.is_empty())
.unwrap_or(false)
}
pub(super) fn rhs_has_array_producer(expr: &Expr) -> bool {
match expr {
Expr::Operator(node) => {
if is_array_producer(node) {
return true;
}
if node.op == "index" || is_faq_op(&node.op) {
return false;
}
node.args.iter().any(rhs_has_array_producer)
}
_ => false,
}
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn resolve_model_self_references(model: &mut Model, model_name: &str) {
resolve_self_references(model, model_name);
}
fn resolve_self_references(model: &mut Model, model_name: &str) {
let hits = self_qualified_references(model, model_name);
if !hits.is_empty() {
resolve_self_qualified_references(model, &hits);
}
}
fn self_qualified_references(model: &Model, model_name: &str) -> Vec<(String, String)> {
let prefix = format!("{model_name}.");
let subsystems: HashSet<&str> = model
.subsystems
.as_ref()
.map(|s| s.keys().map(String::as_str).collect())
.unwrap_or_default();
let mut hits: BTreeMap<String, String> = BTreeMap::new();
fn collect(
expr: &Expr,
prefix: &str,
model: &Model,
subsystems: &HashSet<&str>,
hits: &mut BTreeMap<String, String>,
) {
match expr {
Expr::Variable(name) => {
let Some(rest) = name.strip_prefix(prefix) else {
return;
};
let local = model.variables.contains_key(rest)
|| rest
.split_once('.')
.is_some_and(|(head, _)| subsystems.contains(head));
if local && !hits.contains_key(name) {
hits.insert(name.clone(), rest.to_string());
}
}
Expr::Operator(node) => {
node.for_each_child(&mut |c| collect(c, prefix, model, subsystems, hits));
}
Expr::Number(_) | Expr::Integer(_) => {}
}
}
{
let mut visit = |expr: &Expr| collect(expr, prefix.as_str(), model, &subsystems, &mut hits);
for eq in &model.equations {
visit(&eq.lhs);
visit(&eq.rhs);
}
for var in model.variables.values() {
var.for_each_expression(&mut visit);
}
}
hits.into_iter().collect()
}
fn resolve_self_qualified_references(model: &mut Model, hits: &[(String, String)]) {
let map: HashMap<&str, &str> = hits.iter().map(|(q, l)| (q.as_str(), l.as_str())).collect();
let mut rewrite = |expr: &mut Expr| {
*expr = rewrite_self_qualified(expr, &map);
};
for eq in &mut model.equations {
rewrite(&mut eq.lhs);
rewrite(&mut eq.rhs);
}
for var in model.variables.values_mut() {
var.for_each_expression_mut(&mut rewrite);
}
}
fn rewrite_self_qualified(expr: &Expr, hits: &HashMap<&str, &str>) -> Expr {
if hits.is_empty() {
return expr.clone();
}
match expr {
Expr::Variable(v) => hits
.get(v.as_str())
.map_or_else(|| expr.clone(), |l| Expr::Variable((*l).to_string())),
Expr::Operator(node) => {
let binds = |s: &str| {
node.output_idx
.as_ref()
.is_some_and(|ix| ix.iter().any(|x| x == s))
|| node.ranges.as_ref().is_some_and(|r| r.contains_key(s))
|| node.int_var.as_deref() == Some(s)
};
let narrowed: Option<HashMap<&str, &str>> =
hits.iter().any(|(q, l)| binds(q) || binds(l)).then(|| {
hits.iter()
.filter(|&(q, l)| !binds(q) && !binds(l))
.map(|(q, l)| (*q, *l))
.collect()
});
let sub = narrowed.as_ref().unwrap_or(hits);
let mut out = node.map_children(&mut |a| rewrite_self_qualified(a, sub));
if let Some(join) = &out.join {
let mut renamed = join.clone();
for (q, l) in sub {
renamed = rename_join_names(&renamed, q, l);
}
out.join = Some(renamed);
}
Expr::operator(out)
}
Expr::Number(_) | Expr::Integer(_) => expr.clone(),
}
}
pub(super) fn rename_free_symbol(expr: &Expr, from: &str, to: &str) -> Expr {
match expr {
Expr::Variable(v) if v == from => Expr::Variable(to.to_string()),
Expr::Operator(node) => {
let binds = node
.output_idx
.as_ref()
.map(|ix| ix.iter().any(|s| s == from))
.unwrap_or(false)
|| node
.ranges
.as_ref()
.map(|r| r.contains_key(from))
.unwrap_or(false);
if binds {
return expr.clone();
}
let mut out = node.map_children(&mut |a| rename_free_symbol(a, from, to));
if let Some(join) = &out.join {
out.join = Some(rename_join_names(join, from, to));
}
Expr::operator(out)
}
_ => expr.clone(),
}
}
fn rename_join_names(join: &[JoinClause], from: &str, to: &str) -> Vec<JoinClause> {
let ren = |n: &String| -> String { if n == from { to.to_string() } else { n.clone() } };
join.iter()
.map(|c| JoinClause {
on: c.on.iter().map(|[l, r]| [ren(l), ren(r)]).collect(),
syms: c.syms.clone(),
overlap: c.overlap.as_ref().map(|ov| OverlapClause {
src_env: ov.src_env.iter().map(&ren).collect(),
tgt_env: ov.tgt_env.iter().map(&ren).collect(),
eps: ov.eps,
sym_src: ov.sym_src.clone(),
sym_tgt: ov.sym_tgt.clone(),
}),
on_gate: c.on_gate.as_ref().map(|g| g.map_column_names(ren)),
})
.collect()
}
pub(super) fn inline_region_aggregates(node: &ExpressionNode, loops: &[String]) -> ExpressionNode {
let (Some(regions), Some(values)) = (&node.regions, &node.values) else {
return node.clone();
};
if regions.len() != values.len() {
return node.clone();
}
let mut out = node.clone();
out.values = Some(
regions
.iter()
.zip(values.iter())
.map(|(region, value)| {
let Expr::Operator(v) = value else {
return value.clone();
};
if !is_faq_op(&v.op) || v.filter.is_some() {
return value.clone();
}
let (Some(idx), Some(ranges), Some(body)) = (&v.output_idx, &v.ranges, &v.expr)
else {
return value.clone();
};
if idx.len() != region.len()
|| ranges.len() != idx.len()
|| loops.len() != idx.len()
{
return value.clone();
}
for (d, sym) in idx.iter().enumerate() {
let want = crate::types::region_bounds(®ion[d]);
match (ranges.get(sym).and_then(|r| r.bounds()), want) {
(Some(b), Some(w)) if b == w => {}
_ => return value.clone(),
}
}
let mut inlined = body.as_ref().clone();
for (sym, loop_name) in idx.iter().zip(loops.iter()) {
if sym != loop_name {
inlined = rename_free_symbol(&inlined, sym, loop_name);
}
}
inlined
})
.collect(),
);
out
}
pub(super) fn declared_axis_names(model: &Model) -> HashMap<String, Vec<String>> {
model
.variables
.iter()
.filter_map(|(k, v)| {
v.shape
.as_ref()
.filter(|s| !s.is_empty())
.map(|s| (k.clone(), s.clone()))
})
.collect()
}
type GatherPlan = HashMap<String, Vec<usize>>;
fn collect_wrapped_array_leaves(
expr: &Expr,
array_axes: &HashMap<String, Vec<String>>,
into_binders: bool,
out: &mut Vec<String>,
) {
match expr {
Expr::Variable(v) if array_axes.contains_key(v) => {
out.push(v.clone());
}
Expr::Operator(node) => {
if !crate::op_registry::is_elementwise_node(node) {
return;
}
if !into_binders
&& (is_array_producer(node) || node.op == "index" || is_faq_op(&node.op))
{
return;
}
node.for_each_child(&mut |c| {
collect_wrapped_array_leaves(c, array_axes, into_binders, out)
});
}
_ => {}
}
}
fn build_gather_plan(
rhs: &Expr,
array_axes: &HashMap<String, Vec<String>>,
target: &str,
target_axes: Option<&[String]>,
into_binders: bool,
) -> Result<GatherPlan, CompileError> {
let Some(target_axes) = target_axes else {
return Ok(GatherPlan::new());
};
if (1..target_axes.len()).any(|d| target_axes[..d].contains(&target_axes[d])) {
return Ok(GatherPlan::new());
}
let mut leaves: Vec<String> = Vec::new();
collect_wrapped_array_leaves(rhs, array_axes, into_binders, &mut leaves);
let mut plan = GatherPlan::new();
for leaf in leaves {
let Some(leaf_axes) = array_axes.get(&leaf) else {
continue;
};
let mut positions = Vec::with_capacity(leaf_axes.len());
for axis in leaf_axes {
match target_axes.iter().position(|t| t == axis) {
Some(p) => positions.push(p),
None => {
return Err(CompileError::UnalignableArrayShape {
operand: leaf.clone(),
axis: axis.clone(),
result: target.to_string(),
result_axes: target_axes.to_vec(),
});
}
}
}
plan.insert(leaf, positions);
}
Ok(plan)
}
fn plan_is_identity(plan: &GatherPlan, target_rank: usize) -> bool {
plan.values()
.all(|pos| pos.len() == target_rank && pos.iter().enumerate().all(|(d, &p)| d == p))
}
pub(super) fn index_array_leaves_by_loops(
expr: &Expr,
array_axes: &HashMap<String, Vec<String>>,
plan: Option<&GatherPlan>,
loops: &[String],
) -> Expr {
let wrap = |target: Expr, ix: Vec<&String>| {
let mut args = vec![target];
for l in ix {
args.push(Expr::Variable(l.clone()));
}
Expr::operator(ExpressionNode {
op: "index".to_string(),
args,
..Default::default()
})
};
match expr {
Expr::Variable(v) if array_axes.contains_key(v) => {
let ix: Vec<&String> = match plan.and_then(|p| p.get(v)) {
Some(positions) => positions.iter().map(|&p| &loops[p]).collect(),
None => {
let rank = array_axes[v].len().min(loops.len());
loops[..rank].iter().collect()
}
};
wrap(expr.clone(), ix)
}
Expr::Operator(node) => {
if is_array_producer(node) {
let target = if node.op == "makearray" {
Expr::operator(inline_region_aggregates(node, loops))
} else {
expr.clone()
};
return wrap(target, loops.iter().collect());
}
if node.op == "index" || is_faq_op(&node.op) {
return expr.clone();
}
let child_plan = plan.filter(|_| crate::op_registry::is_elementwise_node(node));
let mut out = ExpressionNode::clone(node);
out.args = node
.args
.iter()
.map(|a| index_array_leaves_by_loops(a, array_axes, child_plan, loops))
.collect();
Expr::operator(out)
}
_ => expr.clone(),
}
}
#[derive(Clone, Copy)]
pub(super) enum CellCoords<'a> {
Ints(&'a [i64]),
Loops(&'a [String]),
}
impl CellCoords<'_> {
fn len(&self) -> usize {
match self {
CellCoords::Ints(c) => c.len(),
CellCoords::Loops(l) => l.len(),
}
}
fn coord(&self, d: usize) -> Expr {
match self {
CellCoords::Ints(c) => Expr::Integer(c[d]),
CellCoords::Loops(l) => Expr::Variable(l[d].clone()),
}
}
}
pub(super) fn index_array_leaves(
expr: &Expr,
array_axes: &HashMap<String, Vec<String>>,
plan: Option<&GatherPlan>,
cell: CellCoords<'_>,
) -> Expr {
match expr {
Expr::Variable(v) => {
if let Some(axes) = array_axes.get(v) {
let mut args = vec![Expr::Variable(v.clone())];
match plan.and_then(|p| p.get(v)) {
Some(positions) => {
args.extend(positions.iter().map(|&p| cell.coord(p)));
}
None => {
let n = axes.len().min(cell.len());
args.extend((0..n).map(|d| cell.coord(d)));
}
}
Expr::operator(ExpressionNode {
op: "index".to_string(),
args,
..Default::default()
})
} else {
expr.clone()
}
}
Expr::Operator(node) => {
let child_plan = plan.filter(|_| crate::op_registry::is_elementwise_node(node));
let mut out =
node.map_children(&mut |a| index_array_leaves(a, array_axes, child_plan, cell));
if node.op == "index"
&& let Some(first) = node.args.first()
{
out.args[0] = first.clone();
}
Expr::operator(out)
}
other => other.clone(),
}
}
fn wholearray_body_loops_as_percell(
expr: &Expr,
array_axes: &HashMap<String, Vec<String>>,
var_shapes: &IndexMap<String, VarShape>,
) -> bool {
fn rewrites_a_leaf(e: &Expr, array_axes: &HashMap<String, Vec<String>>) -> bool {
match e {
Expr::Variable(v) => array_axes.contains_key(v),
Expr::Operator(node) if node.op == "index" => node
.args
.iter()
.skip(1)
.any(|a| rewrites_a_leaf(a, array_axes)),
Expr::Operator(node) => node.any_child(&mut |c| rewrites_a_leaf(c, array_axes)),
_ => false,
}
}
match expr {
Expr::Variable(v) => {
array_axes.contains_key(v) || var_shapes.get(v).is_none_or(|vs| vs.shape.is_empty())
}
Expr::Operator(node) => {
if node.op == "index" {
return !node
.args
.iter()
.skip(1)
.any(|a| rewrites_a_leaf(a, array_axes));
}
if is_faq_op(&node.op) {
return false;
}
!node.any_child(&mut |c| !wholearray_body_loops_as_percell(c, array_axes, var_shapes))
}
_ => true,
}
}
fn derivative_scalar_target(lhs: &Expr) -> Option<&str> {
let Expr::Operator(node) = lhs else {
return None;
};
if node.op != "D" || node.args.len() != 1 {
return None;
}
match &node.args[0] {
Expr::Variable(name) => Some(name),
Expr::Operator(inner) if inner.op == "index" => {
let Expr::Variable(name) = inner.args.first()? else {
return None;
};
inner
.args
.iter()
.skip(1)
.all(|a| matches!(a, Expr::Number(_) | Expr::Integer(_)))
.then_some(name.as_str())
}
_ => None,
}
}
pub(super) fn extract_derivative_scalar(lhs: &Expr) -> Option<(String, Option<Vec<i64>>)> {
let Expr::Operator(node) = lhs else {
return None;
};
if node.op != "D" {
return None;
}
if node.args.len() != 1 {
return None;
}
match &node.args[0] {
Expr::Variable(name) => Some((name.clone(), None)),
Expr::Operator(inner) if inner.op == "index" => {
let name = match inner.args.first()? {
Expr::Variable(v) => v.clone(),
_ => return None,
};
let indices: Vec<i64> = inner
.args
.iter()
.skip(1)
.map(|a| match a {
Expr::Number(n) => Some(*n as i64),
Expr::Integer(n) => Some(*n),
_ => None,
})
.collect::<Option<Vec<_>>>()?;
Some((name, Some(indices)))
}
_ => None,
}
}
pub(super) struct DerivArrayop {
var: String,
idx_names: Vec<String>,
ranges: Vec<(i64, i64)>,
lhs_idx_exprs: Vec<Expr>,
body: Expr,
contract_names: Vec<String>,
contract_dims: Vec<ContractDim>,
reduce: ReduceKind,
filter: Option<Box<Expr>>,
}
pub(super) fn extract_derivative_faq(lhs: &Expr, rhs: &Expr) -> Option<DerivArrayop> {
let Expr::Operator(node) = lhs else {
return None;
};
if !is_faq_op(&node.op) {
return None;
}
let body = node.expr.as_ref()?.as_ref();
let idx_names = node.output_idx.clone()?;
let ranges_map = node.ranges.clone()?;
let Expr::Operator(d_node) = body else {
return None;
};
if d_node.op != "D" {
return None;
}
let Expr::Operator(inner) = d_node.args.first()? else {
return None;
};
if inner.op != "index" {
return None;
}
let var_name = match inner.args.first()? {
Expr::Variable(v) => v.clone(),
_ => return None,
};
let lhs_idx_exprs: Vec<Expr> = inner.args.iter().skip(1).cloned().collect();
let ranges: Vec<(i64, i64)> = idx_names
.iter()
.map(|n| {
let r = ranges_map.get(n).and_then(|s| s.bounds()).unwrap_or([0, 0]);
(r[0], r[1])
})
.collect();
let (rhs_body, contract_names, contract_dims, reduce, filter) = match rhs {
Expr::Operator(rnode) if is_faq_op(&rnode.op) => {
let b = rnode.expr.as_ref().map(|b| b.as_ref().clone())?;
let rop =
effective_reduce_kind(rnode.semiring.as_deref(), rnode.reduce.as_deref()).ok()?;
let mut c_names: Vec<String> = Vec::new();
let mut c_dims: Vec<ContractDim> = Vec::new();
if let Some(rhs_ranges) = &rnode.ranges {
let mut sorted_keys: Vec<&String> = rhs_ranges.keys().collect();
sorted_keys.sort();
for n in sorted_keys {
if !idx_names.contains(n) {
c_names.push(n.clone());
c_dims.push(ContractDim::from_range(&rhs_ranges[n]));
}
}
}
(b, c_names, c_dims, rop, rnode.filter.clone())
}
other => (other.clone(), Vec::new(), Vec::new(), ReduceKind::Sum, None),
};
Some(DerivArrayop {
var: var_name,
idx_names,
ranges,
lhs_idx_exprs,
body: rhs_body,
contract_names,
contract_dims,
reduce,
filter,
})
}
pub(super) struct AlgebraicArrayop {
pub(super) var: String,
pub(super) idx_names: Vec<String>,
pub(super) ranges: Vec<(i64, i64)>,
pub(super) body: Expr,
}
pub(super) fn extract_algebraic_faq(lhs: &Expr, rhs: &Expr) -> Option<AlgebraicArrayop> {
let Expr::Operator(node) = lhs else {
return None;
};
if !is_faq_op(&node.op) {
return None;
}
let body = node.expr.as_ref()?.as_ref();
let idx_names = node.output_idx.clone()?;
let ranges_map = node.ranges.clone()?;
let Expr::Operator(inner) = body else {
return None;
};
if inner.op != "index" {
return None;
}
let var_name = match inner.args.first()? {
Expr::Variable(v) => v.clone(),
_ => return None,
};
let idx_args: Vec<&Expr> = inner.args.iter().skip(1).collect();
if idx_args.len() != idx_names.len() {
return None;
}
for (a, want) in idx_args.iter().zip(idx_names.iter()) {
match a {
Expr::Variable(v) if v == want => {}
_ => return None,
}
}
let ranges: Vec<(i64, i64)> = idx_names
.iter()
.map(|n| {
let r = ranges_map.get(n).and_then(|s| s.bounds()).unwrap_or([0, 0]);
(r[0], r[1])
})
.collect();
let rhs_body = match rhs {
Expr::Operator(rnode) if is_faq_op(&rnode.op) => {
if rnode.filter.is_some() {
return None;
}
rnode.expr.as_ref().map(|b| b.as_ref().clone())?
}
other => other.clone(),
};
Some(AlgebraicArrayop {
var: var_name,
idx_names,
ranges,
body: rhs_body,
})
}
pub(super) fn infer_shapes(
state_vars: &[&String],
equations: &[crate::types::Equation],
) -> Result<HashMap<String, Vec<usize>>, CompileError> {
let state_set: HashSet<&str> = state_vars.iter().map(|s| s.as_str()).collect();
let mut per_var_min: HashMap<String, Vec<i64>> = HashMap::new();
let mut per_var_max: HashMap<String, Vec<i64>> = HashMap::new();
let mut seen_indexed: HashSet<String> = HashSet::new();
let skip_none: HashSet<String> = HashSet::new();
let no_loops: HashMap<String, (i64, i64)> = HashMap::new();
{
let mut walk = ShapeWalk {
states: &state_set,
per_var_min: &mut per_var_min,
per_var_max: &mut per_var_max,
seen_indexed: &mut seen_indexed,
skip_shape_update: &skip_none,
};
for eq in equations {
walk.walk(&eq.lhs, &no_loops);
}
}
let lhs_pinned = seen_indexed.clone();
{
let mut walk = ShapeWalk {
states: &state_set,
per_var_min: &mut per_var_min,
per_var_max: &mut per_var_max,
seen_indexed: &mut seen_indexed,
skip_shape_update: &lhs_pinned,
};
for eq in equations {
walk.walk(&eq.rhs, &no_loops);
}
}
let mut out: HashMap<String, Vec<usize>> = HashMap::new();
for name in state_vars {
let name_s = (*name).clone();
if !seen_indexed.contains(&name_s) {
out.insert(name_s, Vec::new());
continue;
}
let mins = per_var_min.get(&name_s).cloned().unwrap_or_default();
let maxes = per_var_max.get(&name_s).cloned().unwrap_or_default();
if mins.len() != maxes.len() {
return Err(CompileError::build_err(format!(
"Inconsistent index rank for variable '{name_s}'"
)));
}
let shape: Vec<usize> = mins
.iter()
.zip(maxes.iter())
.map(|(lo, hi)| (hi - lo + 1).max(1) as usize)
.collect();
out.insert(name_s, shape);
}
Ok(out)
}
fn infer_unsized_forcing_shapes(
decls: &mut IndexMap<String, Option<Vec<usize>>>,
equations: &[crate::types::Equation],
) {
let unsized_: HashSet<&str> = decls
.iter()
.filter(|(_, s)| s.is_none())
.map(|(n, _)| n.as_str())
.collect();
if unsized_.is_empty() {
return;
}
let mut per_var_min: HashMap<String, Vec<i64>> = HashMap::new();
let mut per_var_max: HashMap<String, Vec<i64>> = HashMap::new();
let mut seen_indexed: HashSet<String> = HashSet::new();
let skip_none: HashSet<String> = HashSet::new();
let no_loops: HashMap<String, (i64, i64)> = HashMap::new();
let mut ranks: HashMap<String, HashSet<usize>> = HashMap::new();
{
let mut walk = ShapeWalk {
states: &unsized_,
per_var_min: &mut per_var_min,
per_var_max: &mut per_var_max,
seen_indexed: &mut seen_indexed,
skip_shape_update: &skip_none,
};
for eq in equations {
walk.walk(&eq.lhs, &no_loops);
walk.walk(&eq.rhs, &no_loops);
}
}
for eq in equations {
for e in [&eq.lhs, &eq.rhs] {
index_ranks(e, &unsized_, &mut ranks);
}
}
for (name, shape) in decls.iter_mut() {
if shape.is_some() || ranks.get(name).is_none_or(|r| r.len() != 1) {
continue;
}
let Some(maxes) = per_var_max.get(name) else {
continue;
};
if maxes.iter().all(|&hi| hi >= 1) {
*shape = Some(maxes.iter().map(|&hi| hi as usize).collect());
}
}
}
fn index_ranks(expr: &Expr, names: &HashSet<&str>, out: &mut HashMap<String, HashSet<usize>>) {
let Expr::Operator(node) = expr else {
return;
};
if node.op == "index"
&& let Some(Expr::Variable(var)) = node.args.first()
&& names.contains(var.as_str())
{
out.entry(var.clone())
.or_default()
.insert(node.args.len() - 1);
}
node.for_each_child(&mut |child| index_ranks(child, names, out));
}
pub(super) struct ShapeWalk<'a> {
states: &'a HashSet<&'a str>,
per_var_min: &'a mut HashMap<String, Vec<i64>>,
per_var_max: &'a mut HashMap<String, Vec<i64>>,
seen_indexed: &'a mut HashSet<String>,
skip_shape_update: &'a HashSet<String>,
}
impl ShapeWalk<'_> {
fn walk(&mut self, expr: &Expr, loop_ranges: &HashMap<String, (i64, i64)>) {
let Expr::Operator(node) = expr else {
return;
};
if node.op == "index"
&& let Some(Expr::Variable(var)) = node.args.first()
&& self.states.contains(var.as_str())
{
self.seen_indexed.insert(var.clone());
if !self.skip_shape_update.contains(var) {
let mut dim_min: Vec<i64> = Vec::new();
let mut dim_max: Vec<i64> = Vec::new();
for idx_expr in node.args.iter().skip(1) {
let (lo, hi) = evaluate_index_range(idx_expr, loop_ranges);
dim_min.push(lo);
dim_max.push(hi);
}
let cur_min = self.per_var_min.entry(var.clone()).or_default();
let cur_max = self.per_var_max.entry(var.clone()).or_default();
if cur_min.len() < dim_min.len() {
cur_min.resize(dim_min.len(), i64::MAX);
}
if cur_max.len() < dim_max.len() {
cur_max.resize(dim_max.len(), i64::MIN);
}
for (d, v) in dim_min.iter().enumerate() {
cur_min[d] = cur_min[d].min(*v);
}
for (d, v) in dim_max.iter().enumerate() {
cur_max[d] = cur_max[d].max(*v);
}
}
}
if is_faq_op(&node.op) {
let mut inner = loop_ranges.clone();
if let Some(ranges) = &node.ranges {
for (k, v) in ranges {
if let Some(b) = v.bounds() {
inner.insert(k.clone(), (b[0], b[1]));
}
}
}
node.for_each_child(&mut |child| self.walk(child, &inner));
return;
}
node.for_each_child(&mut |child| self.walk(child, loop_ranges));
}
}
#[cfg(test)]
mod subsystem_ragged_and_inspection_tests {
use super::*;
use crate::simulate::{Alg, SolveOptions};
use serde_json::json;
fn typed(doc: serde_json::Value) -> EsmFile {
serde_json::from_value(doc).expect("test document deserializes")
}
fn inspecting_problem(file: &EsmFile) -> crate::problem::EsmProblem {
crate::problem::esm_problem(
file,
(0.0, 1.0),
crate::problem::ProblemOptions {
inspect: true,
rhs: crate::problem::Rhs::Always,
compiler: Some(crate::problem::Compiler::Interpreter),
..Default::default()
},
)
.expect("builds")
}
fn model(doc: serde_json::Value) -> Model {
serde_json::from_value(doc).expect("test model deserializes")
}
fn agg_defining<'a>(m: &'a Model, lhs: &str) -> &'a ExpressionNode {
let eq = m
.equations
.iter()
.find(|e| matches!(&e.lhs, Expr::Variable(v) if v == lhs))
.unwrap_or_else(|| panic!("no equation defines {lhs}"));
match &eq.rhs {
Expr::Operator(n) => n,
other => panic!("{lhs} is not defined by an operator node: {other:?}"),
}
}
fn host_mounting_a_join_leaf(join_on: serde_json::Value) -> Model {
model(json!({
"variables": { "doubled": { "type": "unknown" } },
"equations": [
{ "lhs": "doubled", "rhs": { "op": "*", "args": [2.0, "Leaf.matched"] } }
],
"subsystems": { "Leaf": {
"variables": {
"left_key": { "type": "unknown", "shape": ["leaf_left"] },
"right_key": { "type": "unknown", "shape": ["leaf_right"] },
"matched": { "type": "unknown" }
},
"equations": [
{ "lhs": "left_key", "rhs": { "op": "const", "args": [], "value": [7, 9, 4] } },
{ "lhs": "right_key", "rhs": { "op": "const", "args": [], "value": [7, 9] } },
{ "lhs": "matched",
"rhs": { "op": "faq", "args": [], "semiring": "sum_product",
"output_idx": [],
"ranges": { "l": { "from": "leaf_left" },
"r": { "from": "leaf_right" } },
"join": [ { "on": join_on } ],
"expr": 1.0 } }
]
}}
}))
}
#[test]
fn mounting_carries_a_leafs_join_on_key_columns() {
let mut m = host_mounting_a_join_leaf(json!([["left_key", "right_key"]]));
let mut sets: HashMap<String, IndexSet> = HashMap::new();
mount_subsystems(&mut m, &mut sets).expect("mounts");
let node = agg_defining(&m, "Leaf.matched");
let on = &node.join.as_ref().expect("join survives the mount")[0].on;
assert_eq!(
on,
&vec![["Leaf.left_key".to_string(), "Leaf.right_key".to_string()]],
"both key columns follow the variables they name"
);
assert!(
m.variables.contains_key("Leaf.left_key"),
"the variables the join now names are the mounted ones"
);
}
#[test]
fn mounting_leaves_a_shadowed_loop_symbol_alone() {
let mut m = host_mounting_a_join_leaf(json!([["l", "right_key"]]));
m.subsystems.as_mut().expect("subsystems")["Leaf"]["variables"]["l"] =
json!({ "type": "unknown", "shape": ["leaf_left"] });
m.subsystems.as_mut().expect("subsystems")["Leaf"]["equations"]
.as_array_mut()
.expect("equations")
.push(json!({ "lhs": "l", "rhs": { "op": "const", "args": [], "value": [1, 2, 3] } }));
let mut sets: HashMap<String, IndexSet> = HashMap::new();
mount_subsystems(&mut m, &mut sets).expect("mounts");
let node = agg_defining(&m, "Leaf.matched");
let on = &node.join.as_ref().expect("join survives the mount")[0].on;
assert_eq!(
on,
&vec![["l".to_string(), "Leaf.right_key".to_string()]],
"the shadowed loop symbol stays bare; its sibling still follows the registry"
);
}
#[test]
fn mounting_carries_overlap_envelope_factors() {
let mut m = model(json!({
"variables": { "out": { "type": "unknown" } },
"equations": [ { "lhs": "out", "rhs": "Geo.hits" } ],
"subsystems": { "Geo": {
"variables": {
"X": { "type": "unknown", "shape": ["pts"] },
"W": { "type": "unknown", "shape": ["cells"] },
"hits": { "type": "unknown" }
},
"equations": [
{ "lhs": "X", "rhs": { "op": "const", "args": [], "value": [1.0, 3.0] } },
{ "lhs": "W", "rhs": { "op": "const", "args": [], "value": [0.0, 2.0] } },
{ "lhs": "hits",
"rhs": { "op": "faq", "args": [], "semiring": "sum_product",
"output_idx": [],
"ranges": { "p": { "from": "pts" }, "c": { "from": "cells" } },
"join": [ { "overlap": { "src_env": ["X"], "tgt_env": ["W"] } } ],
"expr": 1.0 } }
]
}}
}));
let mut sets: HashMap<String, IndexSet> = HashMap::new();
mount_subsystems(&mut m, &mut sets).expect("mounts");
let node = agg_defining(&m, "Geo.hits");
let ov = node.join.as_ref().expect("join survives")[0]
.overlap
.as_ref()
.expect("overlap survives");
assert_eq!(ov.src_env, vec!["Geo.X".to_string()]);
assert_eq!(ov.tgt_env, vec!["Geo.W".to_string()]);
}
fn erk_opts() -> SolveOptions {
SolveOptions {
alg: Alg::Erk,
reltol: Some(1e-10),
abstol: Some(1e-12),
saveat: Some(vec![1.0]),
..Default::default()
}
}
fn ragged_miniature_doc() -> serde_json::Value {
json!({
"esm": "1.1.0",
"metadata": {"name": "ragged_subsystem_miniature"},
"index_sets": {
"cells": {"kind": "interval", "size": 2},
"edges": {"kind": "interval", "size": 3},
"maxEdges": {"kind": "interval", "size": 3},
"edges_of_cell": {"kind": "ragged", "of": ["cells"],
"offsets": "nEdgesOnCell",
"values": "edgesOnCell"}
},
"models": {"M": {
"subsystems": {"mesh": {
"esm": "1.0.0",
"metadata": {"name": "mini_mesh"},
"models": {"MiniMesh": {
"variables": {
"nEdgesOnCell": {"type": "unknown", "shape": ["cells"]},
"edgesOnCell": {"type": "unknown", "shape": ["cells", "maxEdges"]},
"w": {"type": "unknown", "shape": ["edges"]}
},
"equations": [
{"lhs": "nEdgesOnCell", "rhs": {"op": "const", "value": [2, 3], "args": []}},
{"lhs": "edgesOnCell", "rhs": {"op": "const", "value": [[1, 2, 0], [1, 2, 3]], "args": []}},
{"lhs": "w", "rhs": {"op": "const", "value": [10.0, 20.0, 30.0], "args": []}}]
}}
}},
"variables": {
"u": {"type": "unknown", "units": "1", "shape": ["cells"]},
"nEdgesOnCell": {"type": "unknown", "shape": ["cells"]},
"edgesOnCell": {"type": "unknown", "shape": ["cells", "maxEdges"]},
"s": {"type": "unknown", "shape": ["cells"]}
},
"equations": [
{"lhs": "nEdgesOnCell", "rhs": "mesh.nEdgesOnCell"},
{"lhs": "edgesOnCell", "rhs": "mesh.edgesOnCell"},
{"lhs": "s", "rhs": {
"op": "faq", "args": ["edgesOnCell", "mesh.w"],
"output_idx": ["i"], "semiring": "sum_product",
"ranges": {"i": {"from": "cells"},
"k": {"from": "edges_of_cell", "of": ["i"]}},
"expr": {"op": "index", "args": ["mesh.w",
{"op": "index", "args": ["edgesOnCell", "i", "k"]}]}
}},
{"lhs": {"op": "ic", "args": ["u"]}, "rhs": 0.0},
{"lhs": {"op": "D", "args": ["u"], "wrt": "t"}, "rhs": "s"}
]
}}
})
}
#[test]
fn ragged_csr_miniature_through_subsystem_and_aliases() {
let file = typed(ragged_miniature_doc());
let prob = inspecting_problem(&file);
let sol = crate::problem::solve(&prob, &erk_opts()).expect("solves");
let insp = prob.take_inspection();
let ti = sol.time.len() - 1;
let cells = crate::inline_tests::state_cells(&sol.state_variable_names, "u", "M");
assert_eq!(cells.len(), 2);
let u1: Vec<f64> = cells.iter().map(|(_, row)| sol.state[*row][ti]).collect();
assert!((u1[0] - 30.0).abs() < 1e-8, "u[1](1) = {} != 30", u1[0]);
assert!((u1[1] - 60.0).abs() < 1e-8, "u[2](1) = {} != 60", u1[1]);
let s = insp.setup_arrays.get("s").expect("s captured");
assert_eq!(s.shape(), [2]);
assert_eq!(s[IxDyn(&[0])], 30.0);
assert_eq!(s[IxDyn(&[1])], 60.0);
for name in [
"mesh.nEdgesOnCell",
"mesh.edgesOnCell",
"mesh.w",
"nEdgesOnCell",
"edgesOnCell",
] {
assert!(insp.setup_arrays.contains_key(name), "missing '{name}'");
}
assert_eq!(
insp.setup_arrays["nEdgesOnCell"],
insp.setup_arrays["mesh.nEdgesOnCell"]
);
assert!(insp.observed_exprs.contains_key("s"));
}
#[test]
fn inspection_does_not_change_the_run() {
let file = typed(ragged_miniature_doc());
let plain = crate::problem::esm_problem(
&file,
(0.0, 1.0),
crate::problem::ProblemOptions {
p: HashMap::new().clone(),
u0: HashMap::new().clone(),
rhs: crate::problem::Rhs::Always,
compiler: Some(crate::problem::Compiler::Interpreter),
..Default::default()
},
)
.and_then(|prob| crate::problem::solve(&prob, &erk_opts()))
.expect("simulates");
let prob = inspecting_problem(&file);
let inspected = crate::problem::solve(&prob, &erk_opts()).expect("solves");
let insp = prob.take_inspection();
assert_eq!(plain.time, inspected.time);
assert_eq!(plain.state, inspected.state);
assert_eq!(plain.state_variable_names, inspected.state_variable_names);
assert!(!insp.setup_arrays.is_empty());
}
fn ragged_registry(offsets: &str, values: &str) -> HashMap<String, IndexSet> {
HashMap::from([(
"edges_of_cell".to_string(),
IndexSet {
kind: "ragged".to_string(),
size: None,
members: None,
from_faq: None,
member_factor: None,
of: Some(vec!["cells".to_string()]),
offsets: Some(offsets.to_string()),
values: Some(values.to_string()),
},
)])
}
fn obs_var() -> ModelVariable {
serde_json::from_value(json!({"type": "unknown"})).expect("variable parses")
}
#[test]
fn factor_scope_exact_name_wins_and_shallowest_suffix_resolves() {
let mut reg = ragged_registry("nEdgesOnCell", "edgesOnCell");
let vars: IndexMap<String, ModelVariable> = [
("nEdgesOnCell", obs_var()),
("mesh.nEdgesOnCell", obs_var()),
("edgesOnCell", obs_var()),
("mesh.edgesOnCell", obs_var()),
]
.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect();
apply_ragged_factor_scope(&mut reg, &vars).expect("resolves");
assert_eq!(
reg["edges_of_cell"].offsets.as_deref(),
Some("nEdgesOnCell")
);
assert_eq!(reg["edges_of_cell"].values.as_deref(), Some("edgesOnCell"));
let mut reg = ragged_registry("nEdgesOnCell", "edgesOnCell");
let vars: IndexMap<String, ModelVariable> = [
("Div.nEdgesOnCell", obs_var()),
("Div.mesh.nEdgesOnCell", obs_var()),
("Div.edgesOnCell", obs_var()),
("Div.mesh.edgesOnCell", obs_var()),
]
.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect();
apply_ragged_factor_scope(&mut reg, &vars).expect("resolves");
assert_eq!(
reg["edges_of_cell"].offsets.as_deref(),
Some("Div.nEdgesOnCell")
);
assert_eq!(
reg["edges_of_cell"].values.as_deref(),
Some("Div.edgesOnCell")
);
let mut reg = ragged_registry("nowhere", "edgesOnCell");
let vars: IndexMap<String, ModelVariable> = [("Div.edgesOnCell", obs_var())]
.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect();
apply_ragged_factor_scope(&mut reg, &vars).expect("resolves");
assert_eq!(reg["edges_of_cell"].offsets.as_deref(), Some("nowhere"));
}
#[test]
fn factor_scope_ambiguity_is_a_hard_error() {
let mut reg = ragged_registry("nEdgesOnCell", "edgesOnCell");
let vars: IndexMap<String, ModelVariable> = [
("A.nEdgesOnCell", obs_var()),
("B.nEdgesOnCell", obs_var()),
("A.edgesOnCell", obs_var()),
]
.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect();
let err = apply_ragged_factor_scope(&mut reg, &vars).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("ambiguous"), "unexpected error: {msg}");
assert!(msg.contains("A.nEdgesOnCell") && msg.contains("B.nEdgesOnCell"));
}
#[test]
fn exact_rational_overlap_weights_through_inspection() {
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "inspect_overlap"},
"index_sets": {
"src_cells": {"kind": "interval", "size": 2},
"tgt_cells": {"kind": "interval", "size": 1}
},
"models": {"R": {
"variables": {
"q": {"type": "unknown", "units": "1", "shape": ["tgt_cells"],
"default": 0.0},
"atol": {"type": "parameter", "units": "1", "default": 1e-12},
"src_poly": {"type": "unknown"},
"tgt_poly": {"type": "unknown"},
"A_ij": {"type": "unknown"},
"A_j": {"type": "unknown"},
"W_ij": {"type": "unknown"}
},
"equations": [
{"lhs": "src_poly", "rhs": {"op": "const", "args": [], "value": [
[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]],
[[1.0, 0.0], [2.0, 0.0], [2.0, 1.0], [1.0, 1.0]]]}},
{"lhs": "tgt_poly", "rhs": {"op": "const", "args": [], "value": [
[[0.0, 0.0], [2.0, 0.0], [2.0, 1.0], [0.0, 1.0]]]}},
{"lhs": "A_ij", "rhs": {
"op": "faq", "args": ["src_poly", "tgt_poly"],
"output_idx": ["i", "j"], "semiring": "sum_product",
"ranges": {"i": {"from": "src_cells"}, "j": {"from": "tgt_cells"}},
"expr": {"op": "polygon_intersection_area", "manifold": "planar",
"args": [{"op": "index", "args": ["src_poly", "i"]},
{"op": "index", "args": ["tgt_poly", "j"]}]}}},
{"lhs": "A_j", "rhs": {
"op": "faq", "args": ["A_ij"],
"output_idx": ["j"], "semiring": "sum_product",
"ranges": {"i": {"from": "src_cells"}, "j": {"from": "tgt_cells"}},
"filter": {"op": ">", "args": [
{"op": "index", "args": ["A_ij", "i", "j"]}, "atol"]},
"expr": {"op": "index", "args": ["A_ij", "i", "j"]}}},
{"lhs": "W_ij", "rhs": {
"op": "faq", "args": ["A_ij", "A_j"],
"output_idx": ["i", "j"], "semiring": "sum_product",
"ranges": {"i": {"from": "src_cells"}, "j": {"from": "tgt_cells"}},
"filter": {"op": ">", "args": [
{"op": "index", "args": ["A_ij", "i", "j"]}, "atol"]},
"expr": {"op": "/", "args": [
{"op": "index", "args": ["A_ij", "i", "j"]},
{"op": "index", "args": ["A_j", "j"]}]}}},
{"lhs": {"op": "D", "args": ["q"], "wrt": "t"}, "rhs": 0.0}
]
}}
});
let file = typed(doc);
let prob = inspecting_problem(&file);
crate::problem::solve(&prob, &erk_opts()).expect("solves");
let insp = prob.take_inspection();
let a_ij = insp.setup_arrays.get("A_ij").expect("A_ij captured");
assert_eq!(a_ij.shape(), [2, 1]);
assert_eq!(a_ij[IxDyn(&[0, 0])], 1.0);
assert_eq!(a_ij[IxDyn(&[1, 0])], 1.0);
let a_j = insp.setup_arrays.get("A_j").expect("A_j captured");
assert_eq!(a_j.shape(), [1]);
assert_eq!(a_j[IxDyn(&[0])], 2.0);
let w_ij = insp.setup_arrays.get("W_ij").expect("W_ij captured");
assert_eq!(w_ij.shape(), [2, 1]);
assert_eq!(w_ij[IxDyn(&[0, 0])], 0.5);
assert_eq!(w_ij[IxDyn(&[1, 0])], 0.5);
}
#[test]
fn self_qualified_rewrite_does_not_capture_a_bound_local_name() {
let m = model(json!({
"variables": {
"i": {"type": "parameter", "units": "1", "default": 3.0},
"u": {"type": "unknown", "units": "1", "shape": ["x"]},
"z": {"type": "unknown", "units": "1"}
},
"equations": [
{"lhs": "z", "rhs": "M.i"},
{"lhs": "u", "rhs": {
"op": "faq", "args": [], "output_idx": ["i"],
"ranges": {"i": {"from": "x"}},
"expr": {"op": "*", "args": ["M.i", "i"]}}}
]
}));
let hits = self_qualified_references(&m, "M");
assert_eq!(hits, vec![("M.i".to_string(), "i".to_string())]);
let mut out = m.clone();
resolve_self_qualified_references(&mut out, &hits);
assert!(matches!(&out.equations[0].rhs, Expr::Variable(v) if v == "i"));
let Expr::Operator(node) = &out.equations[1].rhs else {
panic!("expected the aggregate");
};
let body = node.expr.as_deref().expect("aggregate body");
let Expr::Operator(mul) = body else {
panic!("expected the product");
};
assert!(
matches!(&mul.args[0], Expr::Variable(v) if v == "M.i"),
"a node binding `i` must keep the qualified spelling, got {:?}",
mul.args[0]
);
}
#[test]
fn self_qualified_references_inside_subsystem_bodies_resolve() {
let file = typed(json!({
"esm": "1.0.0",
"metadata": {"name": "nested_self_refs"},
"models": {"M": {
"variables": {"k": {"type": "parameter", "units": "1", "default": 2.0}},
"equations": [],
"subsystems": {
"A": {
"variables": {"x": {"type": "unknown", "units": "1", "default": 1.0}},
"equations": [{"lhs": {"op": "D", "args": ["x"], "wrt": "t"},
"rhs": {"op": "*", "args": ["M.k", "M.B.C.z"]}}],
"subsystems": {"C": {
"variables": {"z": {"type": "unknown", "units": "1", "default": 3.0}},
"equations": [{"lhs": {"op": "D", "args": ["z"], "wrt": "t"},
"rhs": {"op": "-", "args": ["M.A.x"]}}]
}}
},
"B": {
"variables": {},
"equations": [],
"subsystems": {"C": {
"variables": {"z": {"type": "unknown", "units": "1", "default": 5.0}},
"equations": [{"lhs": {"op": "D", "args": ["z"], "wrt": "t"},
"rhs": {"op": "*", "args": ["M.k", "M.A.C.z"]}}]
}}
}
}
}}
}));
let compiled = ArrayCompiled::from_file(&file).expect("compiles");
let (_, report) = compiled.build_tape(&HashSet::new());
assert!(
report.fallbacks.is_empty(),
"every self-qualified read must resolve: {:?}",
report.fallbacks
);
let names = compiled.state_variable_names();
let at = |n: &str| names.iter().position(|s| s == n).expect(n);
let mut state = vec![0.0; names.len()];
state[at("A.x")] = 1.0;
state[at("A.C.z")] = 3.0;
state[at("B.C.z")] = 5.0;
let (dy, _) = compiled.debug_eval_rhs(&state, 0.0, &HashMap::new(), true);
assert_eq!(dy[at("A.x")], 10.0, "D(A.x) = k * B.C.z");
assert_eq!(dy[at("A.C.z")], -1.0, "D(A.C.z) = -A.x");
assert_eq!(dy[at("B.C.z")], 6.0, "D(B.C.z) = k * A.C.z");
}
}
#[cfg(test)]
mod event_driven_update_refusal_tests {
use super::*;
#[test]
fn an_expression_update_is_refused_naming_its_kinds() {
let file = crate::parse::load_string(
r#"{
"esm": "1.0.0",
"metadata": { "name": "ExprUpdates", "authors": ["test"] },
"models": { "M": {
"variables": {
"x": { "type": "unknown", "default": 0.0 },
"k": { "type": "parameter", "default": 1.0, "shape": [],
"update": [
{ "kind": "schedule", "interval": 2.0, "expression": 2.0 },
{ "kind": "crossing", "when": { "op": "-", "args": ["x", 1.0] }, "expression": 3.0 }
] }
},
"equations": [ { "lhs": { "op": "D", "args": ["x"], "wrt": "t" }, "rhs": "k" } ]
} }
}"#,
)
.expect("loads");
let model = &file.models.as_ref().unwrap()["M"];
let err = classify_variables(model, &HashSet::new()).expect_err("refused");
let text = err.to_string();
assert!(
text.contains("parameter 'k'") && text.contains("`schedule`, `crossing`"),
"{text}"
);
}
}