use super::tape::{TapeCtx, TapeProgram, run_tape_call};
use super::*;
use crate::simulate::CompileError;
use ndarray::ArrayViewD;
use std::collections::HashSet;
pub struct RhsScratch {
state_arrays: ArrMap,
observed_arrays: ArrMap,
pool: Pool,
cse: CseRt,
const_lits: ConstLitMemo,
static_keys: HashSet<String>,
tape: Option<TapeCtx>,
const_arrays: std::sync::Arc<ConstArrayScope>,
}
impl RhsScratch {
pub(super) fn new(var_shapes: &IndexMap<String, VarShape>) -> Self {
let mut state_arrays = ArrMap::with_capacity_and_hasher(var_shapes.len(), FxBuildHasher);
for (name, vs) in var_shapes {
state_arrays.insert(name.clone(), ArrayD::<f64>::zeros(IxDyn(&vs.shape)));
}
RhsScratch {
state_arrays,
observed_arrays: ArrMap::default(),
pool: Pool::default(),
cse: CseRt::default(),
const_lits: ConstLitMemo::default(),
static_keys: HashSet::new(),
tape: None,
const_arrays: std::sync::Arc::new(ConstArrayScope::default()),
}
}
pub(super) fn set_const_arrays(&mut self, scope: std::sync::Arc<ConstArrayScope>) {
self.const_arrays = scope;
}
pub(super) fn const_arrays(&self) -> &std::sync::Arc<ConstArrayScope> {
&self.const_arrays
}
pub(super) fn install_tape(&mut self, prog: std::sync::Arc<TapeProgram>) {
self.tape = Some(TapeCtx::new(prog));
}
pub(super) fn bump_forcing_epoch(&mut self) {
if let Some(tape) = self.tape.as_mut() {
tape.bump_forcing_epoch();
}
}
pub fn has_tape(&self) -> bool {
self.tape.is_some()
}
pub(super) fn set_exports_active(&mut self, on: bool) {
if let Some(tape) = self.tape.as_mut() {
tape.set_exports_active(on);
}
}
pub(super) fn observed_arrays(&self) -> &ArrMap {
&self.observed_arrays
}
pub(super) fn taped_observeds(&self) -> Option<&ArrMap> {
self.tape
.as_ref()
.map(super::tape::TapeCtx::exported_observeds)
}
pub(super) fn set_static(&mut self, static_observeds: ArrMap) {
self.cse.invalidate_consts();
self.static_keys = static_observeds.keys().cloned().collect();
for (name, arr) in static_observeds {
self.observed_arrays.insert(name, arr);
}
}
}
pub(super) fn refill_state_arrays(
state_arrays: &mut ArrMap,
var_shapes: &IndexMap<String, VarShape>,
state: &[f64],
) {
for (name, vs) in var_shapes {
let total = vs.shape.iter().copied().product::<usize>().max(1);
let block = &state[vs.flat_offset..vs.flat_offset + total];
let arr = state_arrays
.get_mut(name)
.expect("scratch has a state array for every variable");
if vs.shape.is_empty() {
arr[IxDyn(&[])] = block[0];
continue;
}
let n = vs.shape.len();
let mut multi = DimU::from_elem(0usize, n);
for _ in 0..total {
let mut cm = 0usize;
let mut stride = 1usize;
for d in 0..n {
cm += multi[d] * stride;
stride *= vs.shape[d];
}
arr[IxDyn(&multi)] = block[cm];
for d in (0..n).rev() {
multi[d] += 1;
if multi[d] < vs.shape[d] {
break;
}
multi[d] = 0;
}
}
}
}
pub(super) fn scatter_col_major_offset(
arr: ArrayViewD<f64>,
dy: &mut [f64],
vs: &VarShape,
dest_lo: &[usize],
) {
let n = arr.ndim();
if n == 0 {
dy[vs.flat_offset] = arr[IxDyn(&[])];
return;
}
let total: usize = arr.shape().iter().product();
let mut multi = DimU::from_elem(0usize, n);
for _ in 0..total {
let mut cm = 0usize;
let mut stride = 1usize;
for d in 0..n {
cm += (dest_lo[d] + multi[d]) * stride;
stride *= vs.shape[d];
}
dy[vs.flat_offset + cm] = arr[IxDyn(&multi)];
for d in (0..n).rev() {
multi[d] += 1;
if multi[d] < arr.shape()[d] {
break;
}
multi[d] = 0;
}
}
}
pub(super) fn observed_rule_var(rule: &AlgebraicRule) -> &String {
match rule {
AlgebraicRule::Scalar { var, .. }
| AlgebraicRule::ArrayLoop { var, .. }
| AlgebraicRule::Recurrence { var, .. } => var,
}
}
pub(super) fn observed_rule_body(rule: &AlgebraicRule) -> &Expr {
match rule {
AlgebraicRule::Scalar { body, .. }
| AlgebraicRule::ArrayLoop { body, .. }
| AlgebraicRule::Recurrence { body, .. } => body,
}
}
pub(super) fn recur_frame(output_ranges: &[(i64, i64)]) -> (DimU, DimI) {
(
output_ranges
.iter()
.map(|(lo, hi)| (hi - lo + 1).max(0) as usize)
.collect(),
output_ranges.iter().map(|(lo, _)| *lo).collect(),
)
}
pub(super) fn sweep_recurrence<'a>(
scope: &'a RecurScope<'a>,
output_idx_names: &[String],
output_ranges: &[(i64, i64)],
body: &Expr,
axis: usize,
ctx: &mut EvalCtx<'a>,
) -> ArrayD<f64> {
let (lo, hi) = output_ranges[axis];
let inner_ranges: Vec<(i64, i64)> = output_ranges
.iter()
.enumerate()
.filter(|(d, _)| *d != axis)
.map(|(_, r)| *r)
.collect();
let inner_axes: Vec<(usize, &String)> = output_idx_names
.iter()
.enumerate()
.filter(|(d, _)| *d != axis)
.collect();
let saved = ctx.recur.take();
ctx.recur = Some(scope);
let mut full = vec![0i64; output_ranges.len()];
'sweep: for i in lo..=hi {
set_bind(&mut ctx.loop_binds, &output_idx_names[axis], i);
full[axis] = i;
let mut inner = CartesianTuples::new(&inner_ranges);
while let Some(tuple) = inner.next() {
for ((d, name), val) in inner_axes.iter().zip(tuple.iter()) {
set_bind(&mut ctx.loop_binds, name, *val);
full[*d] = *val;
}
let v = eval(body, ctx).as_scalar().unwrap_or(f64::NAN);
scope.publish(&full, v);
super::eval::note_per_cell_cell();
if super::eval::stop_after_first_cell() {
break 'sweep;
}
}
}
ctx.recur = saved;
scope.finish()
}
pub(super) fn collect_expr_var_refs(expr: &Expr, out: &mut HashSet<String>) {
match expr {
Expr::Variable(name) => {
out.insert(name.clone());
}
Expr::Operator(node) => {
node.for_each_child(&mut |child| collect_expr_var_refs(child, out));
}
Expr::Number(_) | Expr::Integer(_) => {}
}
}
pub(super) fn dependency_order_observed(
rules: Vec<AlgebraicRule>,
) -> Result<Vec<AlgebraicRule>, CompileError> {
let names: HashSet<String> = rules.iter().map(|r| observed_rule_var(r).clone()).collect();
let deps: Vec<HashSet<String>> = rules
.iter()
.map(|r| {
let mut refs = HashSet::new();
collect_expr_var_refs(observed_rule_body(r), &mut refs);
let self_name = observed_rule_var(r);
refs.retain(|n| names.contains(n) && n != self_name);
refs
})
.collect();
let mut placed: HashSet<String> = HashSet::new();
let mut order: Vec<usize> = Vec::with_capacity(rules.len());
let mut remaining: Vec<usize> = (0..rules.len()).collect();
while !remaining.is_empty() {
let mut progress = false;
let mut still: Vec<usize> = Vec::new();
for i in std::mem::take(&mut remaining) {
if deps[i].iter().all(|d| placed.contains(d)) {
placed.insert(observed_rule_var(&rules[i]).clone());
order.push(i);
progress = true;
} else {
still.push(i);
}
}
remaining = still;
if !progress {
return Err(CompileError::ObservedCycle {
cycle: first_cycle_among(&rules, &deps, &remaining),
});
}
}
let mut slots: Vec<Option<AlgebraicRule>> = rules.into_iter().map(Some).collect();
Ok(order
.into_iter()
.map(|i| slots[i].take().expect("each index visited once"))
.collect())
}
fn first_cycle_among(
rules: &[AlgebraicRule],
deps: &[HashSet<String>],
residue: &[usize],
) -> Vec<String> {
let stuck: std::collections::BTreeMap<String, std::collections::BTreeSet<String>> = residue
.iter()
.map(|&i| {
let name = observed_rule_var(&rules[i]).clone();
let ds: std::collections::BTreeSet<String> = deps[i].iter().cloned().collect();
(name, ds)
})
.collect();
crate::classification::first_observed_cycle(&stuck)
.unwrap_or_else(|| stuck.keys().cloned().collect())
}
pub(super) fn build_state_arrays(var_shapes: &IndexMap<String, VarShape>, state: &[f64]) -> ArrMap {
let mut state_arrays: ArrMap = ArrMap::default();
for (name, vs) in var_shapes {
let total = vs.shape.iter().copied().product::<usize>().max(1);
let block = &state[vs.flat_offset..vs.flat_offset + total];
if vs.shape.is_empty() {
state_arrays.insert(name.clone(), ArrayD::from_elem(IxDyn(&[]), block[0]));
} else {
state_arrays.insert(name.clone(), col_major_to_arrayd(block, &vs.shape));
}
}
state_arrays
}
pub(super) struct ObsPass<'a> {
pub(super) env: EvalEnv<'a>,
pub(super) force_scalar: bool,
}
fn precision_of_rule(rule: &AlgebraicRule) -> Option<crate::precision::PrecisionGuard> {
if !crate::precision::has_variable_overrides() {
return None;
}
let var = match rule {
AlgebraicRule::Scalar { var, .. }
| AlgebraicRule::ArrayLoop { var, .. }
| AlgebraicRule::Recurrence { var, .. } => var,
};
Some(crate::precision::enter(crate::precision::of_variable(var)))
}
pub(super) fn materialize_observeds_pass(
dst: &mut ArrMap,
observed_rules: &[AlgebraicRule],
pass: &ObsPass,
stats: &mut RhsStats,
) {
let ObsPass { env, force_scalar } = pass;
let force_scalar = *force_scalar;
let _overlay = super::vectorized::OverlayGuard::armed(force_scalar);
for rule in observed_rules {
let _rule_precision = precision_of_rule(rule);
match rule {
AlgebraicRule::Scalar {
var,
body,
declared_shape,
} => {
let mut ctx = env.ctx(&*dst);
let arr = match (eval(body, &mut ctx), declared_shape) {
(Value::Array(a), _) => *a,
(Value::Scalar(s), Some(shape)) => ArrayD::from_elem(IxDyn(shape), s),
(Value::Scalar(s), None) => ArrayD::from_elem(IxDyn(&[]), s),
};
dst.insert(var.clone(), arr);
}
AlgebraicRule::Recurrence {
var,
output_idx_names,
output_ranges,
body,
axis,
max_lag: _,
lag_proven: _,
} => {
stats.obs_scalar_rules += 1;
let (shape, origin) = recur_frame(output_ranges);
let scope = RecurScope::new(var.as_str(), shape, origin);
let arr = {
let mut ctx = env.oracle_ctx(&*dst);
sweep_recurrence(
&scope,
output_idx_names,
output_ranges,
body,
*axis,
&mut ctx,
)
};
dst.insert(var.clone(), arr);
}
AlgebraicRule::ArrayLoop {
var,
output_idx_names,
output_ranges,
body,
} => {
let padded_shape: Vec<usize> =
output_ranges.iter().map(|(_, hi)| *hi as usize).collect();
if !force_scalar
&& !output_ranges.is_empty()
&& output_ranges.iter().all(|(lo, _)| *lo == 1)
&& !padded_shape.contains(&0)
{
let materialized = {
let ctx = env.ctx(&*dst);
with_faq_pool(|pool| {
try_eval_faq_vectorized(
output_idx_names,
output_ranges,
body,
&[],
&[],
ReduceKind::Sum,
None,
&ctx,
pool,
)
.map(|(val, _ops)| {
let arr = val
.view()
.expect("vectorized observed value has a view")
.to_owned();
val.release(pool);
arr
})
})
};
if let Some(arr) = materialized {
dst.insert(var.clone(), arr);
stats.obs_vectorized_rules += 1;
continue;
}
}
stats.obs_scalar_rules += 1;
let padded_origin: Vec<i64> = vec![1i64; padded_shape.len()];
let total = padded_shape.iter().copied().product::<usize>().max(1);
let mut buf = vec![0.0f64; total];
{
let mut ctx = env.oracle_ctx(&*dst);
let mut tuples = CartesianTuples::new(output_ranges);
while let Some(tuple) = tuples.next() {
for (name, val) in output_idx_names.iter().zip(tuple.iter()) {
set_bind(&mut ctx.loop_binds, name, *val);
}
let v = eval(body, &mut ctx).as_scalar().unwrap_or(f64::NAN);
let flat = multi_to_flat_col_major(tuple, &padded_shape, &padded_origin);
if flat < buf.len() {
buf[flat] = v;
}
}
}
let arr = col_major_to_arrayd(&buf, &padded_shape);
dst.insert(var.clone(), arr);
}
}
}
}
pub(super) struct RhsCall<'a> {
pub(super) rhs_rules: &'a [RhsRule],
pub(super) observed_rules: &'a [AlgebraicRule],
pub(super) var_shapes: &'a IndexMap<String, VarShape>,
pub(super) param_names: &'a [String],
pub(super) state: &'a [f64],
pub(super) params: &'a [f64],
pub(super) forcing: &'a RefCell<HashMap<String, ArrayD<f64>>>,
pub(super) t: f64,
pub(super) declared: &'a HashSet<String>,
pub(super) rule_set: u64,
}
pub(super) fn next_rule_set_id() -> u64 {
static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
}
fn rule_set_key(call: &RhsCall) -> u64 {
call.rule_set
}
pub(super) fn evaluate_rhs_with_scratch(
call: &RhsCall,
dy: &mut [f64],
force_scalar: bool,
stats: &mut RhsStats,
scratch: &mut RhsScratch,
) {
let _overlay = super::vectorized::OverlayGuard::armed(force_scalar);
if force_scalar || scratch.tape.is_none() {
scratch.const_lits.retarget(rule_set_key(call));
dy.fill(0.0);
evaluate_rhs_legacy(call, dy, force_scalar, stats, scratch);
return;
}
let tape = scratch.tape.as_mut().expect("tape checked Some");
run_tape_call(tape, call, dy, stats);
}
fn evaluate_rhs_legacy(
call: &RhsCall,
dy: &mut [f64],
force_scalar: bool,
stats: &mut RhsStats,
scratch: &mut RhsScratch,
) {
let &RhsCall {
rhs_rules,
observed_rules,
var_shapes,
param_names,
state,
params,
forcing,
t,
declared,
rule_set: _,
} = call;
refill_state_arrays(&mut scratch.state_arrays, var_shapes, state);
let const_scope = std::sync::Arc::clone(scratch.const_arrays());
let const_arrays: &ConstArrayScope = &const_scope;
scratch.cse.retarget(rule_set_key(call));
scratch.cse.bind_params(params);
scratch.cse.set_const_names(|| {
let mut all: rustc_hash::FxHashSet<String> = scratch.static_keys.iter().cloned().collect();
let varying: HashSet<&str> = observed_rules
.iter()
.map(|r| observed_rule_var(r).as_str())
.collect();
for p in param_names {
if !varying.contains(p.as_str()) && !var_shapes.contains_key(p) && !all.contains(p) {
all.insert(p.clone());
}
}
let arrays: rustc_hash::FxHashSet<String> = scratch
.static_keys
.iter()
.filter(|k| {
scratch
.observed_arrays
.get(*k)
.is_some_and(|a| a.ndim() > 0)
})
.cloned()
.collect();
(all, arrays)
});
let derived_rings: RefCell<HashMap<String, ArrayD<f64>>> = RefCell::new(HashMap::new());
{
let RhsScratch {
state_arrays,
observed_arrays,
static_keys,
cse,
const_lits,
..
} = &mut *scratch;
observed_arrays.retain(|k, _| static_keys.contains(k));
let pass = ObsPass {
env: EvalEnv {
state_arrays,
params,
param_names,
t,
derived_rings: &derived_rings,
derived_extents: empty_derived_extents(),
forcing,
cse: Some(&*cse),
const_lits: Some(&*const_lits),
const_arrays,
declared,
},
force_scalar,
};
materialize_observeds_pass(observed_arrays, observed_rules, &pass, stats);
}
let observed_arrays = &scratch.observed_arrays;
let env = EvalEnv {
state_arrays: &scratch.state_arrays,
params,
param_names,
t,
derived_rings: &derived_rings,
derived_extents: empty_derived_extents(),
forcing,
cse: Some(&scratch.cse),
const_lits: Some(&scratch.const_lits),
const_arrays,
declared,
};
let pool = &mut scratch.pool;
for rule in rhs_rules {
match rule {
RhsRule::Scalar { slot, body } => {
let mut ctx = env.ctx(observed_arrays);
let v = eval(body, &mut ctx).as_scalar().unwrap_or(f64::NAN);
dy[*slot] = v;
}
RhsRule::IndexedScalar { slot, body } => {
let mut ctx = env.ctx(observed_arrays);
let v = eval(body, &mut ctx).as_scalar().unwrap_or(f64::NAN);
dy[*slot] = v;
}
RhsRule::ArrayLoop {
var_name,
output_idx_names,
output_ranges,
lhs_idx_exprs,
body,
contract_names,
contract_dims,
reduce,
filter,
} => {
let vs = &var_shapes[var_name];
let filter = filter.as_deref();
let lhs_shifts = lhs_constant_shifts(lhs_idx_exprs, output_idx_names);
if !force_scalar {
if let Some(dest_lo) = lhs_shifts
.as_ref()
.and_then(|shifts| subblock_dest(vs, output_ranges, shifts))
{
let ctx = env.ctx(observed_arrays);
if let Some((val, ops)) = try_eval_faq_vectorized(
output_idx_names,
output_ranges,
body,
contract_names,
contract_dims,
*reduce,
filter,
&ctx,
pool,
) {
let total = vs.shape.iter().copied().product::<usize>().max(1);
if vs.flat_offset + total <= dy.len() {
if let Some(view) = val.view() {
scatter_col_major_offset(view, dy, vs, &dest_lo);
}
val.release(pool);
stats.kernel_ops += ops;
stats.vectorized_rules += 1;
continue;
}
val.release(pool);
}
}
}
stats.scalar_rules += 1;
let static_ranges = static_contract_ranges(contract_dims);
let output_origin: Vec<i64> = output_ranges.iter().map(|(lo, _)| *lo).collect();
let mut ctx = env.oracle_ctx(observed_arrays);
if let Some(scan) = detect_prefix_scan(
output_idx_names,
output_ranges,
contract_names,
static_ranges.as_deref(),
body,
filter,
) {
let (scan_lo, scan_hi) = output_ranges[scan.axis];
let outer_ranges: Vec<(i64, i64)> = output_ranges
.iter()
.enumerate()
.filter(|(d, _)| *d != scan.axis)
.map(|(_, r)| *r)
.collect();
let outer_names: Vec<&String> = output_idx_names
.iter()
.enumerate()
.filter(|(d, _)| *d != scan.axis)
.map(|(_, n)| n)
.collect();
let sweep = ScanSweep {
scan,
i_name: &output_idx_names[scan.axis],
j_name: &contract_names[0],
bounds: (scan_lo, scan_hi),
body,
reduce: *reduce,
};
let mut outer = CartesianTuples::new(&outer_ranges);
while let Some(otuple) = outer.next() {
for (name, val) in outer_names.iter().zip(otuple.iter()) {
set_bind(&mut ctx.loop_binds, name, *val);
}
run_prefix_scan(&sweep, &mut ctx, |_, acc, ctx| {
let actual_multi: Vec<i64> = lhs_idx_exprs
.iter()
.map(|e| eval_simple_index(e, &ctx.loop_binds))
.collect();
let flat =
multi_to_flat_col_major(&actual_multi, &vs.shape, &vs.origin);
dy[vs.flat_offset + flat] = acc;
});
}
continue;
}
let cellbox = CellBox {
names: output_idx_names,
origin: &output_origin,
};
let spec = ReduceSpec {
contract_names,
body,
reduce: *reduce,
filter,
cell: Some(&cellbox),
};
let mut tuples = CartesianTuples::new(output_ranges);
while let Some(tuple) = tuples.next() {
for (name, val) in output_idx_names.iter().zip(tuple.iter()) {
set_bind(&mut ctx.loop_binds, name, *val);
}
let v = reduce_contraction(
&spec,
contract_dims,
static_ranges.as_deref(),
&mut ctx,
);
let actual_multi: Vec<i64> = lhs_idx_exprs
.iter()
.map(|e| eval_simple_index(e, &ctx.loop_binds))
.collect();
let flat = multi_to_flat_col_major(&actual_multi, &vs.shape, &vs.origin);
dy[vs.flat_offset + flat] = v;
}
}
}
}
}
#[cfg(test)]
mod elementwise_array_observed_tests {
use super::*;
use crate::simulate::{Alg, SolveOptions};
use crate::types::EsmFile;
use serde_json::json;
fn typed(doc: serde_json::Value) -> EsmFile {
serde_json::from_value(doc).expect("test document deserializes")
}
fn erk() -> SolveOptions {
SolveOptions {
alg: Alg::Erk,
reltol: Some(1e-10),
abstol: Some(1e-12),
saveat: Some(vec![1.0]),
..Default::default()
}
}
#[test]
fn elementwise_array_observed_chain_drives_state_per_cell() {
let doc = json!({
"esm": "1.0.0",
"metadata": {"name": "ew_obs"},
"index_sets": {"c": {"kind": "interval", "size": 3}},
"models": {"M": {
"variables": {
"psi": {"type": "unknown", "units": "1", "shape": ["c"]},
"k": {"type": "unknown", "shape": ["c"]},
"a": {"type": "unknown", "shape": ["c"]}
},
"equations": [
{"lhs": "k", "rhs": {"op": "const", "value": [1.0, 2.0, 3.0], "args": []}},
{"lhs": "a", "rhs": {"op": "+", "args": ["psi", "k"]}},
{"lhs": {"op": "ic", "args": ["psi"]}, "rhs": 0.0},
{"lhs": {"op": "D", "args": ["psi"], "wrt": "t"}, "rhs": {"op": "-", "args": ["a"]}}
]
}}
});
let file = typed(doc);
let sol = 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,
..Default::default()
},
)
.and_then(|prob| crate::problem::solve(&prob, &erk()))
.expect("simulates");
let ti = sol.time.len() - 1;
let cells = crate::inline_tests::state_cells(&sol.state_variable_names, "psi", "M");
assert_eq!(cells.len(), 3);
let psi: Vec<f64> = cells.iter().map(|(_, row)| sol.state[*row][ti]).collect();
let one_minus_em1 = 1.0 - (-1.0f64).exp();
for (i, k) in [1.0f64, 2.0, 3.0].iter().enumerate() {
let expect = -k * one_minus_em1;
assert!(
(psi[i] - expect).abs() < 1e-6,
"psi[{}](1) = {} != {}",
i + 1,
psi[i],
expect
);
}
}
}