use super::super::{ArrayCompiled, DimU, RhsStats};
use super::ir::*;
use super::refexec::{RefVal, run_reference};
use super::taped;
use crate::types::EsmFile;
use serde_json::json;
use std::collections::{HashMap, HashSet};
fn typed(doc: serde_json::Value) -> EsmFile {
crate::parse::load_string(&doc.to_string()).expect("fixture document loads")
}
pub(super) fn compile(doc: serde_json::Value) -> ArrayCompiled {
ArrayCompiled::from_file(&typed(doc)).expect("fixture compiles")
}
fn default_cfg() -> super::fuse::SuperopCfg {
super::fuse::SuperopCfg {
bin3: false,
ext_pairs: true,
}
}
fn all_superops_cfg() -> super::fuse::SuperopCfg {
super::fuse::SuperopCfg {
bin3: true,
ext_pairs: true,
}
}
pub(super) fn seeded_state(n: usize, seed: u64, lo: f64, hi: f64) -> Vec<f64> {
let mut x = seed.wrapping_mul(0x9E3779B97F4A7C15).wrapping_add(1);
(0..n)
.map(|_| {
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
let u = (x >> 11) as f64 / (1u64 << 53) as f64;
lo + u * (hi - lo)
})
.collect()
}
pub(super) fn declined_check(doc: serde_json::Value) -> DeclinedTape {
let compiled = compile(doc);
let (fused, report) = compiled.build_tape_opts(&HashSet::new(), Some(default_cfg()));
let (unfused, _) = compiled.build_tape_opts(&HashSet::new(), None);
let Err(declined) = fused else {
panic!("the fixture must decline a rule:\n{report}");
};
assert!(unfused.is_err(), "the unfused build must decline too");
assert_refused(&compiled, &declined);
declined
}
fn assert_refused(compiled: &ArrayCompiled, declined: &DeclinedTape) {
let (rule, reason) = declined
.declined_rules()
.into_iter()
.next()
.expect("a declined tape names at least one rule");
match compiled.debug_new_scratch_taped() {
Ok(_) => panic!("a declined build must not hand back a scratch"),
Err(crate::simulate::SimulateError::Compile(
crate::compile_error::CompileError::CompilerRefusedRule {
rule: r,
reason: why,
..
},
)) => {
assert_eq!(r, rule);
assert_eq!(why, reason);
}
Err(e) => panic!("expected compiler_refused_rule, got {e:?}"),
}
}
pub(super) fn ab_check(doc: serde_json::Value, lo: f64, hi: f64) -> TapeProgram {
let compiled = compile(doc);
let (prog, _) = taped(compiled.build_tape_opts(&HashSet::new(), Some(default_cfg())));
let (prog_uf, _) = taped(compiled.build_tape_opts(&HashSet::new(), None));
assert!(
prog_uf.fused.is_empty() && prog_uf.fuse_stats.n_groups == 0,
"the unfused build must not fuse"
);
assert!(
prog.instrs.len() <= prog_uf.instrs.len(),
"fusion must not grow the program"
);
let n = compiled.state_variable_names().len();
let params = HashMap::new();
let param_vec = compiled.debug_resolve_params(¶ms);
let mut fast_scratch = compiled
.debug_new_scratch_taped()
.expect("the tape lowers every rule");
assert!(fast_scratch.has_tape(), "fixture scratch must carry a tape");
let mut fast_uf = super::super::RhsScratch::new(&compiled.var_shapes);
fast_uf.install_tape(std::sync::Arc::new(prog_uf));
let prog_uf = {
taped(compiled.build_tape_opts(&HashSet::new(), None)).0
};
let rm_build =
|| taped(compiled.build_tape_layout(&HashSet::new(), Some(default_cfg()), false)).0;
let prog_rm = rm_build();
assert!(!prog_rm.col_major);
let mut fast_rm = super::super::RhsScratch::new(&compiled.var_shapes);
fast_rm.install_tape(std::sync::Arc::new(rm_build()));
let mut fast_stats = RhsStats::default();
for seed in 0..4u64 {
let state = seeded_state(n, seed, lo, hi);
for &t in &[0.0, 0.37, 2.5] {
let (dy_ref, _) = compiled.debug_eval_rhs(&state, t, ¶ms, false);
for (label, p) in [
("fused", &prog),
("unfused", &prog_uf),
("row-major fused", &prog_rm),
] {
let mut dy = vec![0.0f64; n];
run_reference(p, &compiled, &state, ¶m_vec, t, &mut dy);
for (k, (a, b)) in dy.iter().zip(dy_ref.iter()).enumerate() {
assert_eq!(
a.to_bits(),
b.to_bits(),
"seed {seed} t {t}: dy[{k}] diverged: {label} tape-ref {a:?} \
({:016x}) vs interpreter {b:?} ({:016x})",
a.to_bits(),
b.to_bits()
);
}
}
for (label, scratch) in [
("fused", &mut fast_scratch),
("unfused", &mut fast_uf),
("row-major fused", &mut fast_rm),
] {
let mut dy_fast = vec![0.0f64; n];
compiled.debug_eval_rhs_into(
&state,
t,
¶m_vec,
&mut dy_fast,
scratch,
&mut fast_stats,
);
for (k, (a, b)) in dy_fast.iter().zip(dy_ref.iter()).enumerate() {
assert_eq!(
a.to_bits(),
b.to_bits(),
"seed {seed} t {t}: dy[{k}] diverged: {label} FAST exec {a:?} \
({:016x}) vs interpreter {b:?} ({:016x})",
a.to_bits(),
b.to_bits()
);
}
}
#[cfg(not(target_arch = "wasm32"))]
for (label, scratch) in [
("fused split", &mut fast_scratch),
("row-major fused split", &mut fast_rm),
] {
let mut dy_split = vec![0.0f64; n];
super::exec::force_split(Some(3));
compiled.debug_eval_rhs_into(
&state,
t,
¶m_vec,
&mut dy_split,
scratch,
&mut fast_stats,
);
super::exec::force_split(None);
for (k, (a, b)) in dy_split.iter().zip(dy_ref.iter()).enumerate() {
assert_eq!(
a.to_bits(),
b.to_bits(),
"seed {seed} t {t}: dy[{k}] diverged: {label} exec {a:?} vs \
interpreter {b:?}"
);
}
}
}
}
assert!(
fast_stats.taped_rules > 0,
"the fast executor must have run"
);
prog
}
fn wrap(inner: serde_json::Value, lo: i64, hi: i64) -> serde_json::Value {
let p = hi - lo + 1;
json!({"op": "ifelse", "args": [
{"op": "<", "args": [inner, lo]},
{"op": "+", "args": [inner, p]},
{"op": "ifelse", "args": [
{"op": ">", "args": [inner, hi]},
{"op": "-", "args": [inner, p]},
inner
]}
]})
}
fn d_eq(var: &str, n: i64, rhs: serde_json::Value) -> serde_json::Value {
json!({
"lhs": {"op": "faq", "args": [], "output_idx": ["i"],
"expr": {"op": "D", "args": [{"op": "index", "args": [var, "i"]}], "wrt": "t"},
"ranges": {"i": [1, n]}},
"rhs": rhs
})
}
fn agg(n: i64, body: serde_json::Value) -> serde_json::Value {
json!({"op": "faq", "args": [], "output_idx": ["i"],
"ranges": {"i": [1, n]}, "expr": body})
}
fn idx(var: &str, e: serde_json::Value) -> serde_json::Value {
json!({"op": "index", "args": [var, e]})
}
fn untaped_self_read(var: &str, pos: serde_json::Value) -> serde_json::Value {
json!({"op": "index", "args": [
{"op": "faq", "args": [], "output_idx": ["j"], "ranges": {"j": [1, 1]},
"expr": idx(var, pos)},
1]})
}
fn assert_fast_matches(
compiled: &ArrayCompiled,
scratch: &mut super::super::RhsScratch,
param_vec: &[f64],
state: &[f64],
t: f64,
dy_ref: &[f64],
label: &str,
) {
let mut dy = vec![0.0f64; dy_ref.len()];
let mut stats = RhsStats::default();
compiled.debug_eval_rhs_into(state, t, param_vec, &mut dy, scratch, &mut stats);
for (k, (a, b)) in dy.iter().zip(dy_ref.iter()).enumerate() {
assert_eq!(
a.to_bits(),
b.to_bits(),
"{label}: dy[{k}] diverged: FAST exec {a:?} ({:016x}) vs interpreter {b:?} ({:016x})",
a.to_bits(),
b.to_bits()
);
}
}
#[test]
fn ab_multi_rule_stencil_wrap_and_ghost() {
let n = 8;
let lap_wrap = json!({"op": "+", "args": [
idx("u", wrap(json!({"op": "-", "args": ["i", 1]}), 1, n)),
{"op": "*", "args": [-2.0, idx("u", json!("i"))]},
idx("u", wrap(json!({"op": "+", "args": ["i", 1]}), 1, n))
]});
let lap_ghost = json!({"op": "+", "args": [
idx("v", json!({"op": "-", "args": ["i", 1]})),
{"op": "*", "args": [-2.0, idx("v", json!("i"))]},
idx("v", json!({"op": "+", "args": ["i", 1]}))
]});
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_stencil"},
"models": {"M": {
"variables": {
"u": {"type": "unknown", "shape": ["i"]},
"v": {"type": "unknown", "shape": ["i"]}
},
"equations": [
d_eq("u", n, agg(n, lap_wrap)),
d_eq("v", n, agg(n, lap_ghost))
]
}}
});
let prog = ab_check(doc, -3.0, 3.0);
assert!(
prog.plans
.iter()
.any(|p| p.segs.iter().any(|s| s.len() == 2)),
"expected a rolled (two-segment) gather axis for the periodic wrap"
);
}
#[test]
fn ab_nested_aggregate() {
let n = 8;
let inner = json!({"op": "faq", "args": [], "output_idx": ["j"],
"ranges": {"j": [1, n]},
"expr": {"op": "-", "args": [
idx("u", json!({"op": "+", "args": ["j", 1]})),
idx("u", json!("j"))
]}});
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_nested"},
"models": {"M": {
"variables": {"u": {"type": "unknown", "shape": ["i"]}},
"equations": [
d_eq("u", n, agg(n, json!({"op": "index", "args": [inner, "i"]})))
]
}}
});
ab_check(doc, -2.0, 2.0);
}
#[test]
fn ab_nested_aggregate_shadowing_enclosing_index() {
let n = 8;
let inner = json!({"op": "faq", "args": [], "output_idx": ["i"],
"ranges": {"i": [1, n]},
"expr": {"op": "-", "args": [
idx("u", json!({"op": "+", "args": ["i", 1]})),
idx("u", json!("i"))
]}});
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_nested_shadow"},
"models": {"M": {
"variables": {"u": {"type": "unknown", "shape": ["i"]}},
"equations": [
d_eq("u", n, agg(n, json!({"op": "index", "args": [inner, "i"]})))
]
}}
});
ab_check(doc, -2.0, 2.0);
}
#[test]
fn ab_nested_aggregate_makearray_of_shadowed_aggregates() {
let n = 8;
let region_agg = |lo: i64, hi: i64, body: serde_json::Value| {
json!({"op": "faq", "args": [], "output_idx": ["i"],
"ranges": {"i": [lo, hi]}, "expr": body})
};
let interior = region_agg(
2,
n - 1,
json!({"op": "/", "args": [
{"op": "-", "args": [
idx("u", json!({"op": "+", "args": ["i", 1]})),
idx("u", json!({"op": "-", "args": ["i", 1]}))
]},
2.0
]}),
);
let left = region_agg(
1,
1,
json!({"op": "-", "args": [idx("u", json!(2)), idx("u", json!(1))]}),
);
let right = region_agg(
n,
n,
json!({"op": "-", "args": [idx("u", json!(n)), idx("u", json!(n - 1))]}),
);
let ma = json!({"op": "makearray", "args": [],
"regions": [[[2, n - 1]], [[1, 1]], [[n, n]]],
"values": [interior, left, right]});
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_nested_makearray_agg"},
"models": {"M": {
"variables": {"u": {"type": "unknown", "shape": ["i"]}},
"equations": [
d_eq("u", n, agg(n, json!({"op": "index", "args": [ma, "i"]})))
]
}}
});
let prog = ab_check(doc, -2.0, 2.0);
assert_eq!(prog.regions.len(), 3, "three makearray regions lowered");
}
#[test]
fn ab_nested_aggregate_capturing_enclosing_index_still_declines() {
let n = 6;
let inner = json!({"op": "faq", "args": [], "output_idx": ["j"],
"ranges": {"j": [1, n]},
"expr": {"op": "*", "args": [idx("u", json!("j")), "i"]}});
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_nested_capture"},
"models": {"M": {
"variables": {
"u": {"type": "unknown", "shape": ["i"]},
"v": {"type": "unknown", "shape": ["i"]}
},
"equations": [
d_eq("u", n, agg(n, json!({"op": "index", "args": [inner, "i"]}))),
d_eq("v", n, agg(n, json!({"op": "*", "args": [-0.5, idx("v", json!("i"))]})))
]
}}
});
let (_prog, report) = compile(doc.clone()).build_tape(&HashSet::new());
let (name, reason) = report
.fallbacks
.first()
.expect("the captured enclosing index must decline the rule");
assert!(
reason.contains("enclosing bound index"),
"the decline of `{name}` must name the captured index, got: {reason}"
);
declined_check(doc);
}
#[test]
fn ab_makearray_regions() {
let n = 8;
let d = json!({"op": "-", "args": [idx("u", json!("i")),
idx("u", json!({"op": "-", "args": ["i", 1]}))]});
let interior = json!({"op": "+", "args": [
{"op": "*", "args": [d, d]},
{"op": "*", "args": ["i", d]}
]});
let ma = json!({"op": "makearray", "args": [],
"regions": [[[2, n - 1]], [[1, 1]], [[n, n]]],
"values": [interior, 0.5, {"op": "*", "args": [2.0, idx("u", json!("i"))]}]});
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_makearray"},
"models": {"M": {
"variables": {"u": {"type": "unknown", "shape": ["i"]}},
"equations": [
d_eq("u", n, agg(n, json!({"op": "index", "args": [ma, "i"]})))
]
}}
});
let prog = ab_check(doc, -2.0, 2.0);
assert_eq!(prog.regions.len(), 3, "three makearray regions lowered");
}
#[test]
fn ab_contraction_weights() {
let n = 8;
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_einsum"},
"models": {"M": {
"variables": {"u": {"type": "unknown", "shape": ["i"]}},
"equations": [
d_eq("u", n, json!({"op": "faq", "args": [], "output_idx": ["i"],
"reduce": "+",
"ranges": {"i": [1, n], "k": [-1, 1]},
"expr": {"op": "*", "args": [
25,
{"op": "ifelse", "args": [{"op": "==", "args": ["k", 0]}, -2, 1]},
idx("u", json!({"op": "+", "args": ["i", "k"]}))
]}}))
]
}}
});
ab_check(doc, -2.0, 2.0);
}
#[test]
fn ab_contraction_with_filter_mask() {
let n = 6;
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_filter"},
"models": {"M": {
"variables": {"u": {"type": "unknown", "shape": ["i"]}},
"equations": [
d_eq("u", n, json!({"op": "faq", "args": [], "output_idx": ["i"],
"reduce": "+",
"ranges": {"i": [1, n], "j": [1, n]},
"filter": {"op": "<=", "args": ["j", {"op": "*", "args": [1, "i"]}]},
"expr": idx("u", json!("j"))}))
]
}}
});
let prog = ab_check(doc, -2.0, 2.0);
let has_select = prog
.instrs
.iter()
.any(|i| matches!(i, Instr::Select { .. }))
|| prog
.fused
.iter()
.any(|fs| fs.micro.iter().any(|m| matches!(m, MicroOp::Select { .. })));
assert!(
has_select,
"the filter mask must lower to Select instructions (plain or fused)"
);
}
#[test]
fn ab_scalar_ifelse_short_circuit_traps_untaken_branch() {
let n = 6;
let trap = json!({"op": "/", "args": [
idx("u", json!("i")),
{"op": "-", "args": ["p", "p"]}
]});
let safe = json!({"op": "*", "args": [3.0, idx("u", json!("i"))]});
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_shortcircuit"},
"models": {"M": {
"variables": {
"u": {"type": "unknown", "shape": ["i"]},
"p": {"type": "parameter", "default": 2.0}
},
"equations": [
d_eq("u", n, agg(n, json!({"op": "ifelse", "args": [
{"op": ">", "args": ["p", 1000.0]}, trap, safe]})))
]
}}
});
let compiled = compile(doc);
let (prog, report) = taped(compiled.build_tape(&HashSet::new()));
assert!(report.fallbacks.is_empty(), "{:?}", report.fallbacks);
let (jmp_at, n_true, n_false) = prog
.instrs
.iter()
.enumerate()
.find_map(|(i, ins)| match ins {
Instr::JmpIfZero {
n_true, n_false, ..
} => Some((i, *n_true as usize, *n_false as usize)),
_ => None,
})
.expect("a JmpIfZero was emitted");
let false_defs: Vec<SlotId> = prog.instrs[jmp_at + 1 + n_true..jmp_at + 1 + n_true + n_false]
.iter()
.filter_map(|i| i.out())
.collect();
let true_slots: Vec<SlotId> = prog.instrs[jmp_at + 1..jmp_at + 1 + n_true]
.iter()
.filter_map(|i| i.out())
.filter(|s| !false_defs.contains(s))
.collect();
assert!(
!true_slots.is_empty(),
"the trapping branch has instructions"
);
let nstates = compiled.state_variable_names().len();
let params = HashMap::new();
let param_vec = compiled.debug_resolve_params(¶ms);
let state = seeded_state(nstates, 7, 0.5, 2.0);
let (dy_ref, _) = compiled.debug_eval_rhs(&state, 0.0, ¶ms, false);
let mut dy = vec![0.0f64; nstates];
let run = run_reference(&prog, &compiled, &state, ¶m_vec, 0.0, &mut dy);
for (a, b) in dy.iter().zip(dy_ref.iter()) {
assert_eq!(a.to_bits(), b.to_bits());
assert!(a.is_finite(), "no inf/NaN may leak from the untaken branch");
}
for s in true_slots {
assert!(
run.slots[s as usize].is_none(),
"slot {s} of the untaken branch was executed"
);
}
let mut fast = compiled
.debug_new_scratch_taped()
.expect("the tape lowers every rule");
assert_fast_matches(
&compiled,
&mut fast,
¶m_vec,
&state,
0.0,
&dy_ref,
"short-circuit",
);
}
#[test]
fn ab_select_nan_semantics() {
let n = 8;
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_select_nan"},
"models": {"M": {
"variables": {"u": {"type": "unknown", "shape": ["i"]}},
"equations": [
d_eq("u", n, agg(n, json!({"op": "ifelse", "args": [
{"op": ">=", "args": [idx("u", json!("i")), 0.0]},
{"op": "sqrt", "args": [idx("u", json!("i"))]},
0.25]})))
]
}}
});
let compiled = compile(doc);
let (prog, report) = taped(compiled.build_tape(&HashSet::new()));
assert!(report.fallbacks.is_empty(), "{:?}", report.fallbacks);
let nstates = compiled.state_variable_names().len();
let params = HashMap::new();
let param_vec = compiled.debug_resolve_params(¶ms);
let mut fast = compiled
.debug_new_scratch_taped()
.expect("the tape lowers every rule");
for seed in 0..4u64 {
let state = seeded_state(nstates, seed, -2.0, 2.0);
assert!(state.iter().any(|&x| x < 0.0), "fixture needs negatives");
let (dy_ref, _) = compiled.debug_eval_rhs(&state, 0.0, ¶ms, false);
let mut dy = vec![0.0f64; nstates];
run_reference(&prog, &compiled, &state, ¶m_vec, 0.0, &mut dy);
for (k, (a, b)) in dy.iter().zip(dy_ref.iter()).enumerate() {
assert_eq!(a.to_bits(), b.to_bits(), "dy[{k}]");
assert!(!a.is_nan(), "NaN leaked through the select at cell {k}");
}
assert_fast_matches(
&compiled,
&mut fast,
¶m_vec,
&state,
0.0,
&dy_ref,
"select-nan",
);
}
}
#[test]
fn ab_signed_zero() {
let n = 4;
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_signed_zero"},
"models": {"M": {
"variables": {"u": {"type": "unknown", "shape": ["i"]}},
"equations": [
d_eq("u", n, agg(n, json!({"op": "+", "args": [
{"op": "*", "args": [-0.0, idx("u", json!("i"))]},
{"op": "neg", "args": [0.0]}
]})))
]
}}
});
let prog = ab_check(doc, 0.5, 2.0);
let _ = prog;
}
#[test]
fn ab_nary_fold_order() {
let n = 6;
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_fold_order"},
"models": {"M": {
"variables": {"u": {"type": "unknown", "shape": ["i"]}},
"equations": [
d_eq("u", n, agg(n, json!({"op": "+", "args": [
0.1, 0.2, 0.3,
idx("u", json!("i")),
{"op": "min", "args": [
idx("u", json!("i")),
{"op": "*", "args": [0.1, idx("u", json!("i"))]},
0.7
]}
]})))
]
}}
});
ab_check(doc, -2.0, 2.0);
}
#[test]
fn ab_subblock_dy_scatter() {
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_subblock"},
"models": {"M": {
"variables": {"u": {"type": "unknown", "shape": ["i"]}},
"equations": [
{
"lhs": {"op": "faq", "args": [], "output_idx": ["i"],
"expr": {"op": "D", "args": [
{"op": "index", "args": ["u", {"op": "+", "args": ["i", 1]}]}
], "wrt": "t"},
"ranges": {"i": [1, 7]}},
"rhs": {"op": "faq", "args": [], "output_idx": ["i"],
"ranges": {"i": [1, 7]},
"expr": idx("u", json!("i"))}
},
{"lhs": {"op": "D", "args": [{"op": "index", "args": ["u", 1]}], "wrt": "t"},
"rhs": {"op": "*", "args": [0.5, {"op": "index", "args": ["u", 1]}]}}
]
}}
});
let prog = ab_check(doc, -2.0, 2.0);
assert!(
prog.dy_writes
.iter()
.any(|w| w.dest_lo.iter().any(|&d| d > 0)),
"expected a shifted sub-block dy write"
);
}
#[test]
fn ab_observed_chain_and_broadcast() {
let ni = 5;
let nj = 4;
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_obs"},
"models": {"M": {
"variables": {
"w": {"type": "unknown", "shape": ["i", "j"]},
"x": {"type": "unknown"},
"c": {"type": "unknown", "shape": ["j"]},
"s": {"type": "unknown"}
},
"equations": [
{"lhs": "c", "rhs": {"op": "faq", "args": [], "output_idx": ["j"],
"ranges": {"j": [1, nj]},
"expr": {"op": "cos", "args": [{"op": "*", "args": [0.3, "j"]}]}}},
{"lhs": "s", "rhs": {"op": "*", "args": [2.0, "x"]}},
{"lhs": {"op": "D", "args": ["x"], "wrt": "t"},
"rhs": {"op": "*", "args": [{"op": "neg", "args": ["s"]}, "x"]}},
{
"lhs": {"op": "faq", "args": [], "output_idx": ["i", "j"],
"expr": {"op": "D", "args": [
{"op": "index", "args": ["w", "i", "j"]}], "wrt": "t"},
"ranges": {"i": [1, ni], "j": [1, nj]}},
"rhs": {"op": "faq", "args": [], "output_idx": ["i", "j"],
"ranges": {"i": [1, ni], "j": [1, nj]},
"expr": {"op": "*", "args": [
{"op": "index", "args": ["c", "j"]},
{"op": "index", "args": ["w", "i", "j"]}
]}}
}
]
}}
});
let prog = ab_check(doc, 0.2, 2.0);
assert!(
prog.n_const > 0,
"const-tier observed lowered to CONST section"
);
assert!(
prog.plans.iter().any(|p| p.mapped.iter().any(|m| !m)),
"expected a broadcast axis in the c[j] gather over the [i,j] box"
);
assert!(
prog.exports.iter().any(|(n, _)| n == "s"),
"scalar observed `s` must be exported for the samples pass: {:?}",
prog.exports
);
}
#[test]
fn a_declined_rule_leaves_no_program() {
let n = 3;
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_declined"},
"index_sets": {"c": {"kind": "interval", "size": n}},
"models": {"M": {
"variables": {
"psi": {"type": "unknown", "shape": ["c"]},
"k": {"type": "unknown", "shape": ["c"]},
"a": {"type": "unknown", "shape": ["c"]}
},
"equations": [
{"lhs": "k", "rhs": {"op": "faq", "args": [], "output_idx": ["i"],
"ranges": {"i": [1, n]},
"expr": {"op": "ifelse", "args": [
{"op": "<=", "args": ["i", 1]},
1.0,
{"op": "*", "args": [
untaped_self_read("k", json!({"op": "-", "args": ["i", 1]})), 2.0]}
]}}},
{"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 compiled = compile(doc);
let (prog, report) = compiled.build_tape(&HashSet::new());
let Err(declined) = prog else {
panic!("a declined rule must leave no program:\n{report}");
};
let named: Vec<String> = declined
.declined_rules()
.into_iter()
.map(|(name, _)| name)
.collect();
assert_eq!(named, ["k", "a", "D(psi)"], "{report}");
assert_eq!(declined.rules.len(), declined.reasons.len());
assert_eq!(report.n_rules, declined.rules.len());
assert_eq!(report.n_taped, declined.rules.len() - 3);
assert_eq!(
report.n_instr_const + report.n_instr_segment + report.n_instr_continuous,
0
);
assert_refused(&compiled, &declined);
}
#[test]
fn ab_scalar_rules() {
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_scalar"},
"models": {"M": {
"variables": {
"x": {"type": "unknown"},
"y": {"type": "unknown"},
"r": {"type": "parameter", "default": 0.5}
},
"equations": [
{"lhs": {"op": "D", "args": ["x"], "wrt": "t"},
"rhs": {"op": "*", "args": [{"op": "-", "args": ["r"]}, "x"]}},
{"lhs": {"op": "D", "args": ["y"], "wrt": "t"},
"rhs": {"op": "ifelse", "args": [
{"op": "<", "args": ["t", 1.0]},
{"op": "+", "args": ["x", "y", 0.1]},
{"op": "sin", "args": ["y"]}]}}
]
}}
});
ab_check(doc, -1.5, 1.5);
}
#[test]
fn ab_species_major_lanes_read_and_write_in_place() {
let boxes = 300;
let mut vars = serde_json::Map::new();
let mut eqs: Vec<serde_json::Value> = Vec::new();
vars.insert("k1".into(), json!({"type": "parameter", "default": 0.7}));
for sp in ["a", "c"] {
for b in 0..boxes {
vars.insert(format!("{sp}_{b}"), json!({"type": "unknown"}));
}
}
for b in 0..boxes {
vars.insert(
format!("kb_{b}"),
json!({"type": "parameter", "default": 0.01 * b as f64}),
);
}
for b in 0..boxes {
let (a, c, kb) = (format!("a_{b}"), format!("c_{b}"), format!("kb_{b}"));
let r = json!({"op": "*", "args": ["k1", a, c]});
eqs.push(json!({"lhs": {"op": "D", "args": [a], "wrt": "t"},
"rhs": {"op": "+", "args": [{"op": "-", "args": [r]},
{"op": "*", "args": [kb, {"op": "exp", "args": [{"op": "-", "args": [c]}]}]}]}}));
eqs.push(json!({"lhs": {"op": "D", "args": [c], "wrt": "t"},
"rhs": {"op": "-", "args": [r, {"op": "max", "args": [c, 0.5]}]}}));
}
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_reroll_species_major"},
"models": {"M": {"variables": vars, "equations": eqs}}
});
let prog = ab_check(doc, 0.1, 2.0);
let ls = prog
.lanes
.iter()
.find(|ls| ls.lanes as usize == boxes)
.expect("the boxes run as one lane program");
assert!(
ls.inputs.iter().all(|i| matches!(
(i.kind, &i.ix),
(
LaneKind::State | LaneKind::Param,
LaneIx::Affine { step: 1, .. }
)
)),
"{:?}",
ls.inputs
);
assert_eq!(ls.direct.len(), ls.writes.len(), "{:?}", ls.writes);
}
#[test]
fn ab_rerolled_scalar_boxes() {
let boxes = [1, 2, 3, 5, 8, 13, 21];
let mut vars = serde_json::Map::new();
let mut eqs: Vec<serde_json::Value> = Vec::new();
vars.insert("k1".into(), json!({"type": "parameter", "default": 0.7}));
for b in boxes {
let (a, c, kb) = (format!("a_{b}"), format!("c_{b}"), format!("kb_{b}"));
vars.insert(a.clone(), json!({"type": "unknown"}));
vars.insert(c.clone(), json!({"type": "unknown"}));
vars.insert(
kb.clone(),
json!({"type": "parameter", "default": 0.1 * b as f64}),
);
let r = json!({"op": "*", "args": ["k1", a, c]});
eqs.push(json!({"lhs": {"op": "D", "args": [a], "wrt": "t"},
"rhs": {"op": "+", "args": [
{"op": "-", "args": [r]},
{"op": "*", "args": [kb, {"op": "exp", "args": [{"op": "-", "args": [c]}]}]},
{"op": "*", "args": [0.25, "t"]}]}}));
eqs.push(json!({"lhs": {"op": "D", "args": [c], "wrt": "t"},
"rhs": {"op": "-", "args": [r, {"op": "max", "args": [c, 0.5]}]}}));
}
vars.insert("a_1x".into(), json!({"type": "unknown"}));
eqs.push(json!({"lhs": {"op": "D", "args": ["a_1x"], "wrt": "t"},
"rhs": {"op": "*", "args": ["a_1x", "a_1x", "k1"]}}));
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_reroll"},
"models": {"M": {"variables": vars, "equations": eqs}}
});
let prog = ab_check(doc, 0.1, 2.0);
let lanes: Vec<&LaneSpec> = prog
.instrs
.iter()
.filter_map(|i| match i {
Instr::Lanes { spec } => Some(&prog.lanes[*spec as usize]),
_ => None,
})
.collect();
assert_eq!(lanes.len(), 2, "{:?}", prog.instrs);
let (blocks, multi): (Vec<&LaneSpec>, Vec<&LaneSpec>) =
lanes.into_iter().partition(|ls| ls.lanes == 1);
assert_eq!((blocks.len(), multi.len()), (1, 1));
assert!(blocks[0].inputs.is_empty());
let dsts: Vec<bool> = blocks[0]
.writes
.iter()
.map(|w| matches!(w.dst, LaneDst::Dy(_)))
.collect();
assert_eq!(dsts, vec![true, false], "{:?}", blocks[0].writes);
let ls = multi[0];
assert_eq!(ls.lanes as usize, boxes.len());
assert_eq!(ls.writes.len(), 2);
assert_eq!(ls.micro.len(), 10, "{:?}", ls.micro);
let kinds: Vec<LaneKind> = ls.inputs.iter().map(|i| i.kind).collect();
assert_eq!(
kinds,
vec![LaneKind::State, LaneKind::State, LaneKind::Param],
"{:?}",
ls.inputs
);
assert!(
ls.inputs.iter().any(|i| matches!(i.ix, LaneIx::Table(_))),
"the off-grid state breaks one input's spacing"
);
assert!(
!prog
.instrs
.iter()
.any(|i| matches!(i, Instr::DyWrite { .. })),
"every derivative is written by a lane program"
);
}
#[test]
fn ab_folded_lane_strips() {
let boxes = 101;
let mut vars = serde_json::Map::new();
let mut eqs: Vec<serde_json::Value> = Vec::new();
vars.insert("k1".into(), json!({"type": "parameter", "default": 0.7}));
vars.insert("k2".into(), json!({"type": "parameter", "default": 1.3}));
for b in 0..boxes {
let [a, c, e, f, kb] = ["a", "c", "e", "f", "kb"].map(|v| format!("{v}_{b:03}"));
for v in [&a, &c, &e, &f] {
vars.insert(v.clone(), json!({"type": "unknown"}));
}
vars.insert(
kb.clone(),
json!({"type": "parameter", "default": 0.5 + 0.01 * b as f64}),
);
let r1 = json!({"op": "*", "args": ["k1", a, c]});
let r2 = json!({"op": "/", "args": [kb, c]});
let nr1 = json!({"op": "-", "args": [r1]});
eqs.push(json!({"lhs": {"op": "D", "args": [a], "wrt": "t"},
"rhs": {"op": "+", "args": [nr1, r2, {"op": "exp", "args": [{"op": "-", "args": [c]}]}]}}));
eqs.push(json!({"lhs": {"op": "D", "args": [c], "wrt": "t"},
"rhs": {"op": "-", "args": [{"op": "-", "args": [r1, {"op": "max", "args": [c, 0.5]}]}, e]}}));
eqs.push(json!({"lhs": {"op": "D", "args": [e], "wrt": "t"},
"rhs": {"op": "/", "args": [{"op": "/", "args": [{"op": "-", "args": [{"op": "*", "args": [a, e]}]}, kb]}, "k2"]}}));
eqs.push(json!({"lhs": {"op": "D", "args": [f], "wrt": "t"},
"rhs": {"op": "+", "args": [nr1, {"op": "*", "args": [r2, "k2", f]}]}}));
}
vars.insert("a_050x".into(), json!({"type": "unknown"}));
eqs.push(json!({"lhs": {"op": "D", "args": ["a_050x"], "wrt": "t"},
"rhs": {"op": "*", "args": ["a_050x", "k1"]}}));
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_folded_lanes"},
"models": {"M": {"variables": vars, "equations": eqs}}
});
let prog = ab_check(doc, 0.1, 2.0);
let ls = prog
.lanes
.iter()
.find(|ls| ls.lanes as usize == boxes)
.expect("the boxes run as one lane program");
assert!(
ls.inputs.iter().any(|i| matches!(i.ix, LaneIx::Table(_))),
"{:?}",
ls.inputs
);
assert!(
ls.inputs
.iter()
.any(|i| i.kind == LaneKind::State && matches!(i.ix, LaneIx::Affine { step: 1, .. })),
"{:?}",
ls.inputs
);
assert!(ls.inputs.iter().any(|i| i.kind == LaneKind::Param));
}
#[test]
fn coloring_invariants() {
let n = 8;
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_coloring"},
"models": {"M": {
"variables": {"u": {"type": "unknown", "shape": ["i"]}},
"equations": [
d_eq("u", n, json!({"op": "faq", "args": [], "output_idx": ["i"],
"reduce": "+",
"ranges": {"i": [1, n], "k": [-1, 1]},
"expr": {"op": "*", "args": [
{"op": "ifelse", "args": [{"op": "==", "args": ["k", 0]}, -2, 1]},
{"op": "sin", "args": [idx("u", json!({"op": "+", "args": ["i", "k"]}))]}
]}}))
]
}}
});
let compiled = compile(doc);
let (prog, report) = taped(compiled.build_tape_opts(&HashSet::new(), None));
assert!(report.fallbacks.is_empty());
let mut def = vec![usize::MAX; prog.slots.len()];
let mut last = vec![0usize; prog.slots.len()];
for (i, ins) in prog.instrs.iter().enumerate() {
ins.for_each_def(&prog.tables(), |o| {
if def[o as usize] == usize::MAX {
def[o as usize] = i;
} else {
last[o as usize] = last[o as usize].max(i);
}
});
ins.for_each_read(&prog.tables(), |s| {
last[s as usize] = last[s as usize].max(i);
});
}
let mut by_storage: HashMap<u32, Vec<usize>> = HashMap::new();
for (s, d) in prog.slots.iter().enumerate() {
if def[s] != usize::MAX {
assert_ne!(d.storage, u32::MAX, "defined slot {s} must be colored");
by_storage.entry(d.storage).or_default().push(s);
}
}
let mut recycled_any = false;
for (st, slots) in by_storage {
let sd = &prog.slab.storages[st as usize];
if sd.dedicated {
assert_eq!(slots.len(), 1, "dedicated storage {st} shared");
continue;
}
if slots.len() > 1 {
recycled_any = true;
}
for (ai, &a) in slots.iter().enumerate() {
for &b in &slots[ai + 1..] {
let (a0, a1) = (def[a], last[a].max(def[a]));
let (b0, b1) = (def[b], last[b].max(def[b]));
let overlap = a0.max(b0) < a1.min(b1);
assert!(
!overlap,
"storage {st}: slots {a} [{a0},{a1}] and {b} [{b0},{b1}] overlap"
);
}
}
}
assert!(recycled_any, "the coloring must actually recycle something");
let sum: usize = prog.slab.storages.iter().map(|s| s.elems).sum();
assert_eq!(sum, prog.slab.total_elems);
}
#[test]
fn value_numbering_scope_behaviour() {
let n = 6;
let d = json!({"op": "-", "args": [idx("u", json!("i")),
idx("u", json!({"op": "-", "args": ["i", 1]}))]});
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_vn"},
"models": {"M": {
"variables": {"u": {"type": "unknown", "shape": ["i"]}},
"equations": [
d_eq("u", n, agg(n, json!({"op": "*", "args": [d, d]})))
]
}}
});
let compiled = compile(doc);
let (prog, report) = taped(compiled.build_tape(&HashSet::new()));
assert!(report.fallbacks.is_empty());
assert!(
report.vn_scope_hits + report.vn_hoist_hits >= 1,
"the repeated subtree must hit the value numbering"
);
let subs = prog
.instrs
.iter()
.filter(|i| matches!(i, Instr::Bin { op, .. } if *op == super::super::BinCode::Sub))
.count()
+ prog
.fused
.iter()
.flat_map(|fs| fs.micro.iter())
.filter(|m| matches!(m, MicroOp::Bin { op, .. } if *op == super::super::BinCode::Sub))
.count();
assert_eq!(subs, 1, "repeated subtree must be lowered once");
}
#[test]
fn tape_build_is_side_effect_free() {
let n = 6;
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_pure_build"},
"models": {"M": {
"variables": {"u": {"type": "unknown", "shape": ["i"]}},
"equations": [
d_eq("u", n, agg(n, json!({"op": "*", "args": [2.0, idx("u", json!("i"))]})))
]
}}
});
let compiled = compile(doc);
let state = seeded_state(n as usize, 3, -1.0, 1.0);
let (before, _) = compiled.debug_eval_rhs(&state, 0.0, &HashMap::new(), false);
let _ = compiled.build_tape(&HashSet::new());
let (after, _) = compiled.debug_eval_rhs(&state, 0.0, &HashMap::new(), false);
for (a, b) in before.iter().zip(after.iter()) {
assert_eq!(a.to_bits(), b.to_bits());
}
}
#[test]
fn exports_materialize_as_observed_arrays() {
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_export_shape"},
"models": {"M": {
"variables": {
"x": {"type": "unknown"},
"s": {"type": "unknown"}
},
"equations": [
{"lhs": "s", "rhs": {"op": "*", "args": [2.0, "x"]}},
{"lhs": {"op": "D", "args": ["x"], "wrt": "t"},
"rhs": {"op": "neg", "args": ["s"]}}
]
}}
});
let compiled = compile(doc);
let (prog, report) = taped(compiled.build_tape(&HashSet::new()));
assert!(report.fallbacks.is_empty(), "{:?}", report.fallbacks);
let params = HashMap::new();
let param_vec = compiled.debug_resolve_params(¶ms);
let state = vec![1.5];
let mut dy = vec![0.0f64];
let run = run_reference(&prog, &compiled, &state, ¶m_vec, 0.0, &mut dy);
assert_eq!(dy[0].to_bits(), (-3.0f64).to_bits());
let s = run.obs.get("s").expect("s exported");
assert_eq!(s.ndim(), 0);
assert_eq!(s[ndarray::IxDyn(&[])].to_bits(), 3.0f64.to_bits());
let (_, slot) = prog.exports.iter().find(|(n, _)| n == "s").expect("export");
match &run.slots[*slot as usize] {
Some(RefVal::Scalar(v)) => assert_eq!(v.to_bits(), 3.0f64.to_bits()),
other => panic!("unexpected export slot value {other:?}"),
}
}
#[test]
fn ab_prefix_scan_observeds() {
let (ni, nk) = (4, 5);
let scan_obs = |cmp: &str| {
json!({"op": "faq", "args": [], "output_idx": ["i", "k"],
"reduce": "+",
"ranges": {"i": [1, ni], "k": [1, nk], "m": [1, nk]},
"filter": {"op": cmp, "args": ["m", "k"]},
"expr": {"op": "*", "args": [0.3, {"op": "index", "args": ["u", "i", "m"]}]}})
};
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_scan"},
"models": {"M": {
"variables": {
"u": {"type": "unknown", "shape": ["i", "k"]},
"P": {"type": "unknown", "shape": ["i", "k"]},
"Q": {"type": "unknown", "shape": ["i", "k"]},
"R": {"type": "unknown", "shape": ["i", "k"]},
"S": {"type": "unknown", "shape": ["i", "k"]}
},
"equations": [
{"lhs": "P", "rhs": scan_obs("<=")},
{"lhs": "Q", "rhs": scan_obs("<")},
{"lhs": "R", "rhs": scan_obs(">=")},
{"lhs": "S", "rhs": scan_obs(">")},
{
"lhs": {"op": "faq", "args": [], "output_idx": ["i", "k"],
"expr": {"op": "D", "args": [
{"op": "index", "args": ["u", "i", "k"]}], "wrt": "t"},
"ranges": {"i": [1, ni], "k": [1, nk]}},
"rhs": {"op": "faq", "args": [], "output_idx": ["i", "k"],
"ranges": {"i": [1, ni], "k": [1, nk]},
"expr": {"op": "+", "args": [
{"op": "*", "args": [-0.1,
{"op": "index", "args": ["P", "i", "k"]}]},
{"op": "*", "args": [0.05,
{"op": "index", "args": ["Q", "i", "k"]}]},
{"op": "*", "args": [0.07,
{"op": "index", "args": ["R", "i", "k"]}]},
{"op": "*", "args": [-0.03,
{"op": "index", "args": ["S", "i", "k"]}]}
]}}
}
]
}}
});
let prog = ab_check(doc, -2.0, 2.0);
assert_eq!(
opcount(&prog, "Scan") + scan_micro_ops(&prog),
4,
"one Scan per scan observed"
);
assert_eq!(opcount(&prog, "Region"), 0, "no per-step region writes");
}
fn makearray_scan_doc(n: i64, ni: i64, variant: &str) -> serde_json::Value {
let two_d = ni > 0;
let at = |var: &str, k: serde_json::Value| {
if two_d {
json!({"op": "index", "args": [var, "i", k]})
} else {
json!({"op": "index", "args": [var, k]})
}
};
let term = json!({"op": "*", "args": [at("u", json!("j")), at("dz", json!("j"))]});
let (out_idx, ranges, total_out, total_ranges) = if two_d {
(
json!(["i", "k"]),
json!({"i": [1, ni], "k": [1, n], "j": [1, n]}),
json!(["i"]),
json!({"i": [1, ni], "j": [1, n]}),
)
} else {
(
json!(["k"]),
json!({"k": [1, n], "j": [1, n]}),
json!([]),
json!({"j": [1, n]}),
)
};
let scan = |cmp: &str| {
json!({"op": "faq", "args": [], "output_idx": out_idx, "reduce": "+",
"ranges": ranges, "filter": {"op": cmp, "args": ["j", "k"]}, "expr": term})
};
let total = json!({"op": "faq", "args": [], "output_idx": total_out, "reduce": "+",
"ranges": total_ranges, "expr": term});
let region = |lo: i64, hi: i64| {
if two_d {
json!([[1, ni], [lo, hi]])
} else {
json!([[lo, hi]])
}
};
let f = match variant {
"from_bottom" => json!({"op": "makearray", "args": [],
"regions": [region(1, n), region(n + 1, n + 1)],
"values": [scan("<"), total]}),
"to_top" => json!({"op": "makearray", "args": [],
"regions": [region(1, n), region(n + 1, n + 1)],
"values": [scan(">="), 0.0]}),
"shifted" => json!({"op": "makearray", "args": [],
"regions": [region(1, 1), region(2, n + 1)],
"values": [0.0, scan("<=")]}),
_ => unreachable!(),
};
let shape = |k: &str| if two_d { json!(["i", k]) } else { json!([k]) };
let d_lhs = if two_d {
json!({"op": "faq", "args": [], "output_idx": ["i", "k"],
"expr": {"op": "D", "args": [at("u", json!("k"))], "wrt": "t"},
"ranges": {"i": [1, ni], "k": [1, n]}})
} else {
json!({"op": "faq", "args": [], "output_idx": ["k"],
"expr": {"op": "D", "args": [at("u", json!("k"))], "wrt": "t"},
"ranges": {"k": [1, n]}})
};
let mut rhs_ranges = json!({"k": [1, n]});
if two_d {
rhs_ranges["i"] = json!([1, ni]);
}
json!({
"esm": "1.1.0",
"metadata": {"name": "tape_makearray_scan"},
"index_sets": {
"i": {"kind": "interval", "size": ni.max(1)},
"k": {"kind": "interval", "size": n},
"kn": {"kind": "interval", "size": n + 1}
},
"models": {"M": {
"variables": {
"u": {"type": "unknown", "shape": shape("k")},
"dz": {"type": "unknown", "shape": shape("k")},
"F": {"type": "unknown", "shape": shape("kn")}
},
"equations": [
{"lhs": "dz", "rhs": {"op": "faq", "args": [], "output_idx": out_idx.clone(),
"ranges": rhs_ranges.clone(),
"expr": {"op": "+", "args": [0.5, {"op": "*", "args": [0.01, "k"]}]}}},
{"lhs": "F", "rhs": f},
{"lhs": d_lhs,
"rhs": {"op": "faq", "args": [], "output_idx": out_idx, "ranges": rhs_ranges,
"expr": {"op": "*", "args": [-0.1, {"op": "-", "args": [
at("F", json!({"op": "+", "args": ["k", 1]})),
at("F", json!("k"))]}]}}}
]
}}
})
}
#[test]
fn ab_makearray_prefix_scan_region() {
for variant in ["from_bottom", "to_top", "shifted"] {
for ni in [0, 3] {
let n = 7;
let prog = ab_check(makearray_scan_doc(n, ni, variant), -2.0, 2.0);
assert_eq!(
opcount(&prog, "Scan") + scan_micro_ops(&prog),
scans_of(variant),
"[{variant}, ni={ni}] the scan region is one running fold"
);
let cont = prog.section_range(Cadence::Continuous);
let rhs = &prog.instrs[cont.start..cont.start + prog.n_rhs as usize];
let assembles = rhs
.iter()
.filter(|i| matches!(i, Instr::Assemble { .. }))
.count();
assert_eq!(
assembles,
usize::from(variant == "shifted"),
"[{variant}, ni={ni}]"
);
}
}
}
#[test]
fn ab_reverse_scans_fuse_over_many_columns() {
for variant in ["to_top", "from_bottom"] {
let prog = ab_check(makearray_scan_doc(6, 40, variant), -2.0, 2.0);
assert_eq!(
opcount(&prog, "Scan"),
0,
"[{variant}] every scan is absorbed"
);
assert_eq!(scan_micro_ops(&prog), scans_of(variant), "[{variant}]");
let reversed = prog
.fused
.iter()
.filter(|f| f.rows_reversed().is_some())
.count();
assert_eq!(reversed, usize::from(variant == "to_top"), "[{variant}]");
}
}
#[test]
fn makearray_prefix_scan_region_is_linear_in_the_column() {
let n = 3000;
for variant in ["from_bottom", "to_top", "shifted"] {
let compiled = compile(makearray_scan_doc(n, 0, variant));
let (prog, _) = taped(compiled.build_tape_opts(&HashSet::new(), Some(default_cfg())));
let widest = prog.slots.iter().map(|s| s.elems()).max().unwrap_or(0);
assert!(
widest <= n as usize + 1,
"[{variant}] a slot of {widest} cells: the scan was lowered as a box"
);
assert_eq!(
opcount(&prog, "Scan") + scan_micro_ops(&prog),
scans_of(variant)
);
}
}
fn scans_of(_variant: &str) -> usize {
1
}
fn broadcast_const_doc(reader: &str) -> serde_json::Value {
let wk = json!({"op": "index", "args": ["w", "k"]});
let rhs = match reader {
"product" => json!({"op": "*", "args": [wk, {"op": "index", "args": ["u", "i", "k"]}]}),
"dy" => wk,
_ => unreachable!(),
};
json!({
"esm": "1.1.0",
"metadata": {"name": "tape_broadcast_const"},
"index_sets": {
"i": {"kind": "interval", "size": 20},
"k": {"kind": "interval", "size": 5}
},
"models": {"M": {
"variables": {
"u": {"type": "unknown", "shape": ["i", "k"]},
"w": {"type": "unknown", "shape": ["k"]}
},
"equations": [
{"lhs": "w", "rhs": {"op": "faq", "args": [], "output_idx": ["k"],
"ranges": {"k": [1, 5]},
"expr": {"op": "+", "args": [0.5, {"op": "*", "args": [0.01, "k"]}]}}},
{"lhs": {"op": "faq", "args": [], "output_idx": ["i", "k"],
"expr": {"op": "D", "args": [{"op": "index", "args": ["u", "i", "k"]}],
"wrt": "t"},
"ranges": {"i": [1, 20], "k": [1, 5]}},
"rhs": {"op": "faq", "args": [], "output_idx": ["i", "k"],
"ranges": {"i": [1, 20], "k": [1, 5]}, "expr": rhs}}
]
}}
})
}
#[test]
fn ab_const_broadcast_sinks_only_into_a_fusing_reader() {
for reader in ["product", "dy"] {
let prog = ab_check(broadcast_const_doc(reader), -2.0, 2.0);
let gathers = |c: Cadence| {
prog.instrs[prog.section_range(c)]
.iter()
.filter(|i| matches!(i, Instr::Gather { .. }))
.count()
};
let (build, call) = (gathers(Cadence::Const), gathers(Cadence::Continuous));
if reader == "product" {
assert_eq!((build, call), (0, 0), "[{reader}] the broadcast folds");
let folded = prog
.fused
.iter()
.flat_map(|f| &f.inputs)
.any(|inp| inp.shifted_ix.is_some() || inp.gather.is_some());
assert!(folded, "[{reader}] read in the reader's group");
} else {
assert_eq!((build, call), (1, 0), "[{reader}] the broadcast stays");
}
}
}
#[test]
fn a_reverse_window_that_is_not_a_scan_folds_from_the_top() {
let n = 9;
let body_box = json!({"op": "*", "args": [{"op": "index", "args": ["u", "m"]}, "i"]});
let body_mixed = json!({"op": "*", "args": [
{"op": "index", "args": ["u", {"op": "+", "args": [{"op": "-", "args": ["m", "i"]}, 1]}]},
{"op": "+", "args": ["i", 0.25]}]});
for (cmp, body) in [(">=", &body_box), (">", &body_box), (">=", &body_mixed)] {
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_reverse_window"},
"models": {"M": {
"variables": {
"u": {"type": "unknown", "shape": ["i"]},
"R": {"type": "unknown", "shape": ["i"]}
},
"equations": [
{"lhs": "R", "rhs": {"op": "faq", "args": [], "output_idx": ["i"],
"reduce": "+", "ranges": {"i": [1, n], "m": [1, n]},
"filter": {"op": cmp, "args": ["m", "i"]},
"expr": body}},
d_eq("u", n, agg(n, json!({"op": "neg", "args": [
{"op": "index", "args": ["R", "i"]}]})))
]
}}
});
let prog = ab_check(doc, -3.0, 3.0);
let reverse_reduces = prog
.instrs
.iter()
.filter(|i| matches!(i, Instr::Reduce { reverse: true, .. }))
.count();
assert!(reverse_reduces > 0, "{cmp} {body}");
}
}
fn scan_micro_ops(prog: &TapeProgram) -> usize {
prog.fused
.iter()
.flat_map(|f| &f.micro)
.filter(|m| matches!(m, MicroOp::Scan { .. }))
.count()
}
#[test]
fn ab_scan_absorbed_into_its_group() {
let (ni, nk) = (3, 1100);
let scan_obs = |cmp: &str| {
json!({"op": "faq", "args": [], "output_idx": ["i", "k"],
"reduce": "+",
"ranges": {"i": [1, ni], "k": [1, nk], "m": [1, nk]},
"filter": {"op": cmp, "args": ["m", "k"]},
"expr": {"op": "*", "args": [
{"op": "index", "args": ["u", "i", "m"]},
{"op": "index", "args": ["w", "i", "m"]}]}})
};
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_scan_fused"},
"models": {"M": {
"variables": {
"u": {"type": "unknown", "shape": ["i", "k"]},
"w": {"type": "unknown", "shape": ["i", "k"]},
"P": {"type": "unknown", "shape": ["i", "k"]},
"Q": {"type": "unknown", "shape": ["i", "k"]}
},
"equations": [
{"lhs": "w", "rhs": {"op": "faq", "args": [], "output_idx": ["i", "k"],
"ranges": {"i": [1, ni], "k": [1, nk]},
"expr": {"op": "+", "args": [1.0, {"op": "*", "args": [0.5,
{"op": "sin", "args": [{"op": "*", "args": ["i", "k"]}]}]}]}}},
{"lhs": "P", "rhs": scan_obs("<=")},
{"lhs": "Q", "rhs": scan_obs("<")},
{
"lhs": {"op": "faq", "args": [], "output_idx": ["i", "k"],
"expr": {"op": "D", "args": [
{"op": "index", "args": ["u", "i", "k"]}], "wrt": "t"},
"ranges": {"i": [1, ni], "k": [1, nk]}},
"rhs": {"op": "faq", "args": [], "output_idx": ["i", "k"],
"ranges": {"i": [1, ni], "k": [1, nk]},
"expr": {"op": "+", "args": [
{"op": "*", "args": [-0.001,
{"op": "index", "args": ["P", "i", "k"]}]},
{"op": "*", "args": [0.002,
{"op": "index", "args": ["Q", "i", "k"]}]}
]}}
}
]
}}
});
let prog = ab_check(doc, -2.0, 2.0);
assert_eq!(opcount(&prog, "Scan"), 0, "both scans are absorbed");
assert_eq!(scan_micro_ops(&prog), 2);
let cont = prog.section_range(Cadence::Continuous);
let fused_cont = prog.instrs[cont]
.iter()
.filter(|i| matches!(i, Instr::Fused { .. }))
.count();
assert_eq!(fused_cont, 1, "the scans and their reader run as one group");
}
#[test]
fn ab_scan_fused_with_its_neighbours() {
let n = 2500;
let ranges = json!({"k": [1, n], "m": [1, n]});
let scan = |cmp: &str, red: &str, term: serde_json::Value| {
json!({"op": "faq", "args": [], "output_idx": ["k"], "reduce": red,
"ranges": ranges, "filter": {"op": cmp, "args": ["m", "k"]}, "expr": term})
};
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_scan_neighbours"},
"models": {"M": {
"variables": {
"u": {"type": "unknown", "shape": ["k"]},
"v": {"type": "unknown", "shape": ["k"]},
"w": {"type": "unknown", "shape": ["k"]},
"P": {"type": "unknown", "shape": ["k"]},
"Q": {"type": "unknown", "shape": ["k"]},
"R": {"type": "unknown", "shape": ["k"]},
"z": {"type": "unknown", "shape": ["k"]},
"Z1": {"type": "unknown", "shape": ["k"]},
"Z2": {"type": "unknown", "shape": ["k"]}
},
"equations": [
{"lhs": "w", "rhs": {"op": "faq", "args": [], "output_idx": ["k"],
"ranges": {"k": [1, n]},
"expr": {"op": "+", "args": [1.0, {"op": "*", "args": [0.5,
{"op": "sin", "args": ["k"]}]}]}}},
{"lhs": "P", "rhs": scan("<=", "+", json!({"op": "*", "args": [
{"op": "index", "args": ["u", "m"]}, {"op": "index", "args": ["w", "m"]}]}))},
{"lhs": "Q", "rhs": scan("<", "+", json!({"op": "-", "args": [
{"op": "index", "args": ["v", "m"]}, 0.25]}))},
{"lhs": "R", "rhs": scan("<=", "*", json!({"op": "+", "args": [1.0,
{"op": "*", "args": [1e-4, {"op": "index", "args": ["v", "m"]}]}]}))},
{"lhs": "Z1", "rhs": scan("<=", "+", json!({"op": "*", "args": [
{"op": "index", "args": ["z", "m"]}, 2.0]}))},
{"lhs": "Z2", "rhs": scan("<=", "+", json!({"op": "*", "args": [
{"op": "index", "args": ["Z1", "m"]}, 0.5]}))},
{"lhs": {"op": "faq", "args": [], "output_idx": ["k"],
"expr": {"op": "D", "args": [{"op": "index", "args": ["z", "k"]}], "wrt": "t"},
"ranges": {"k": [1, n]}},
"rhs": {"op": "faq", "args": [], "output_idx": ["k"], "ranges": {"k": [1, n]},
"expr": {"op": "*", "args": [{"op": "index", "args": ["Z2", "k"]}, 0.01]}}},
{"lhs": {"op": "faq", "args": [], "output_idx": ["k"],
"expr": {"op": "D", "args": [{"op": "index", "args": ["u", "k"]}], "wrt": "t"},
"ranges": {"k": [1, n]}},
"rhs": {"op": "faq", "args": [], "output_idx": ["k"], "ranges": {"k": [1, n]},
"expr": {"op": "*", "args": [-0.001, {"op": "index", "args": ["P", "k"]}]}}},
{"lhs": {"op": "faq", "args": [], "output_idx": ["k"],
"expr": {"op": "D", "args": [{"op": "index", "args": ["v", "k"]}], "wrt": "t"},
"ranges": {"k": [1, n]}},
"rhs": {"op": "faq", "args": [], "output_idx": ["k"], "ranges": {"k": [1, n]},
"expr": {"op": "+", "args": [
{"op": "/", "args": [{"op": "index", "args": ["Q", "k"]}, 3.0]},
{"op": "index", "args": ["R", "k"]}]}}}
]
}}
});
let prog = ab_check(doc.clone(), -2.0, 2.0);
let fusions: Vec<ScanFuse> = prog
.fused
.iter()
.flat_map(|f| f.scan_fuse.iter().copied())
.collect();
assert!(scan_micro_ops(&prog) >= 4, "{fusions:?}");
assert!(
fusions.iter().filter(|f| f.pre && f.post).count() >= 2,
"the weighted sum and the scan of the scaled scan run with both \
neighbours: {fusions:?}"
);
let mut compiled = compile(doc);
compiled.runtime_mode = super::super::RuntimeMode::Native;
let (prog, _) = taped(compiled.build_tape(&HashSet::new()));
assert!(
prog.exports.iter().any(|(e, _)| e == "P"),
"fixture exports P"
);
assert!(
prog.fused.iter().flat_map(|f| &f.scan_fuse).any(|f| f.keep),
"{:?}",
prog.fused.iter().map(|f| &f.scan_fuse).collect::<Vec<_>>()
);
let params = HashMap::new();
let param_vec = compiled.debug_resolve_params(¶ms);
let state = seeded_state(3 * n, 7, -2.0, 2.0);
let (dy_ref, _) = compiled.debug_eval_rhs(&state, 0.0, ¶ms, false);
let mut ctx = super::exec::TapeCtx::new(std::sync::Arc::new(prog));
for publish in [false, true, false] {
ctx.set_exports_active(publish);
let mut dy = vec![f64::NAN; 3 * n];
let mut stats = RhsStats::default();
let call = super::super::RhsCall {
rhs_rules: &compiled.rhs_rules,
observed_rules: &compiled.observed_rules,
var_shapes: &compiled.var_shapes,
param_names: &compiled.param_names,
state: &state,
params: ¶m_vec,
forcing: &compiled.forcing,
t: 0.0,
declared: &compiled.declared_names,
rule_set: compiled.rule_set,
};
super::exec::run_tape_call(&mut ctx, &call, &mut dy, &mut stats);
for (k, (a, b)) in dy.iter().zip(&dy_ref).enumerate() {
assert_eq!(a.to_bits(), b.to_bits(), "publish {publish}: dy[{k}]");
}
if publish {
let p = ctx.exec.obs.get("P").expect("P published");
let mut acc = 0.0f64;
for k in 0..n {
let w = 1.0 + 0.5 * ((k + 1) as f64).sin();
acc += state[k] * w;
assert_eq!(p[ndarray::IxDyn(&[k])].to_bits(), acc.to_bits(), "P[{k}]");
}
}
}
}
#[test]
fn ab_chunk_gathers_on_short_rows() {
let build = |ni: i64, nk: i64| {
let nj = 4i64;
let ix3 = |i: serde_json::Value, k: serde_json::Value| json!({"op": "index", "args": ["u", i, "j", k]});
let lap = json!({"op": "+", "args": [
ix3(json!("i"), json!({"op": "-", "args": ["k", 1]})),
{"op": "*", "args": [-2.0, ix3(json!("i"), json!("k"))]},
ix3(json!("i"), json!({"op": "+", "args": ["k", 1]})),
{"op": "*", "args": [0.5, ix3(wrap(json!({"op": "+", "args": ["i", 1]}), 1, ni), json!("k"))]}
]});
let ranges = json!({"i": [1, ni], "j": [1, nj], "k": [1, nk]});
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_chunk_gather"},
"models": {"M": {
"variables": {"u": {"type": "unknown", "shape": ["i", "j", "k"]}},
"equations": [{
"lhs": {"op": "faq", "args": [], "output_idx": ["i", "j", "k"],
"expr": {"op": "D", "args": [
{"op": "index", "args": ["u", "i", "j", "k"]}], "wrt": "t"},
"ranges": ranges},
"rhs": {"op": "faq", "args": [], "output_idx": ["i", "j", "k"],
"ranges": ranges, "expr": {"op": "*", "args": [0.25, lap]}}
}]
}}
});
ab_check(doc, -2.0, 2.0)
};
let progs: Vec<TapeProgram> = [(40, 40), (8, 6), (40, 6), (8, 40)]
.into_iter()
.map(|(ni, nk)| build(ni, nk))
.collect();
for prog in &progs {
assert!(
prog.fused
.iter()
.flat_map(|f| &f.inputs)
.any(|i| i.gather.is_some()),
"an innermost-axis shift is read through its plan"
);
assert_eq!(opcount(prog, "Gather"), 0, "no gather is materialized");
assert_eq!(
prog.instrs.len(),
progs[0].instrs.len(),
"the program is flat in the box"
);
}
}
#[test]
fn ab_pre_reduction_absorbed_only_for_own_column_reads() {
let (nc, nr) = (40i64, 3i64);
let perm: Vec<f64> = (0..nc).map(|c| ((c * 7 + 3) % nc + 1) as f64).collect();
let doc = |read: serde_json::Value| {
json!({
"esm": "1.1.0",
"metadata": {"name": "tape_pre_reduce_reads"},
"models": {"M": {
"variables": {
"u": {"type": "unknown", "shape": ["c", "r"]},
"H": {"type": "unknown", "shape": ["c"]},
"w": {"type": "unknown", "shape": ["c"]}
},
"equations": [
{"lhs": "w", "rhs": {"op": "const", "args": [], "value": perm}},
{"lhs": "H", "rhs": {"op": "faq", "args": [], "output_idx": ["c"],
"ranges": {"c": [1, nc], "l": [1, nr]},
"expr": {"op": "index", "args": ["u", "c", "l"]}}},
{
"lhs": {"op": "faq", "args": [], "output_idx": ["c", "r"],
"expr": {"op": "D", "args": [
{"op": "index", "args": ["u", "c", "r"]}], "wrt": "t"},
"ranges": {"c": [1, nc], "r": [1, nr]}},
"rhs": {"op": "faq", "args": [], "output_idx": ["c", "r"],
"ranges": {"c": [1, nc], "r": [1, nr]},
"expr": {"op": "+", "args": [
{"op": "*", "args": [-0.1,
{"op": "index", "args": ["u", "c", "r"]}]},
{"op": "*", "args": [0.25, read]}
]}}
}
]
}}
})
};
let reads = [
(
"own column",
json!({"op": "index", "args": ["H", "c"]}),
true,
),
(
"next column",
json!({"op": "index", "args": ["H", {"op": "+", "args": ["c", 1]}]}),
false,
),
(
"data subscript",
json!({"op": "index", "args": ["H", {"op": "index", "args": ["w", "c"]}]}),
false,
),
];
for (what, read, absorbs) in reads {
let prog = ab_check(doc(read), -2.0, 2.0);
assert!(prog.col_major);
assert_eq!(
prog.fused.iter().any(|f| f.pre.is_some()),
absorbs,
"{what}: absorbed"
);
}
let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../tests/conformance/scaling/fixtures/column_sum/column_sum_N1000.esm");
let file = crate::parse::load_path(&path).expect("scaling fixture loads");
let compiled = ArrayCompiled::from_file(&file).expect("fixture compiles");
let prog = taped(compiled.build_tape_opts(&HashSet::new(), Some(default_cfg()))).0;
assert!(
prog.fused.iter().any(|f| f.pre.is_some()),
"column_sum: the column sum is absorbed"
);
}
#[test]
fn ab_index_gather_reduction_interleaved() {
let n = 1500usize;
let nbr: Vec<Vec<f64>> = (0..n)
.map(|i| {
(0..4)
.map(|k| ((i * 7 + k * 389 + 1) % n + 1) as f64)
.collect()
})
.collect();
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_mesh_gather"},
"models": {"M": {
"variables": {
"u": {"type": "unknown", "shape": ["cells"]},
"nbr": {"type": "unknown", "shape": ["cells", "nb"]},
"kappa": {"type": "parameter", "default": 0.1}
},
"equations": [
{"lhs": "nbr", "rhs": {"op": "const", "args": [], "value": nbr}},
{
"lhs": {"op": "faq", "args": [], "output_idx": ["i"],
"expr": {"op": "D", "args": [
{"op": "index", "args": ["u", "i"]}], "wrt": "t"},
"ranges": {"i": [1, n]}},
"rhs": {"op": "faq", "args": [], "output_idx": ["i"],
"ranges": {"i": [1, n], "k": [1, 4]},
"expr": {"op": "*", "args": ["kappa", {"op": "-", "args": [
{"op": "index", "args": ["u",
{"op": "index", "args": ["nbr", "i", "k"]}]},
{"op": "index", "args": ["u", "i"]}]}]}}
}
]
}}
});
let prog = ab_check(doc, -2.0, 2.0);
assert!(
prog.fused
.iter()
.any(|f| f.interleave.is_some() && f.inputs.iter().any(|i| i.index.is_some())),
"the neighbour reduction runs interleaved over its folded gather"
);
assert!(
prog.fused
.iter()
.filter(|f| f.reduce.is_some())
.all(|f| super::exec::fold_fused_op(f).is_some()),
"the scaled difference folds straight into the accumulator"
);
}
#[test]
fn ab_dense_contraction_folds_its_product() {
let n = 40i64;
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_source_receptor"},
"models": {"M": {
"variables": {
"c": {"type": "unknown", "shape": ["i"]},
"e": {"type": "unknown", "shape": ["i"]},
"K": {"type": "unknown", "shape": ["i", "j"]}
},
"equations": [
{"lhs": "K", "rhs": {"op": "faq", "args": [], "output_idx": ["i", "j"],
"ranges": {"i": [1, n], "j": [1, n]},
"expr": {"op": "*", "args": [0.001, {"op": "+", "args": [1,
{"op": "sin", "args": [{"op": "*", "args": ["i", "j"]}]}]}]}}},
d_eq("c", n, json!({"op": "faq", "args": [], "output_idx": ["i"],
"reduce": "+",
"ranges": {"i": [1, n], "j": [1, n]},
"expr": {"op": "*", "args": [
{"op": "index", "args": ["K", "i", "j"]}, idx("e", json!("j"))]}})),
d_eq("e", n, json!({"op": "*", "args": [-0.1, idx("e", json!("i"))]}))
]
}}
});
let prog = ab_check(doc, -2.0, 2.0);
let red = prog
.fused
.iter()
.find(|f| f.reduce.is_some())
.expect("the contraction fuses with its reduction");
assert!(red.interleave.is_none());
assert!(
super::exec::fold_fused_op(red).is_some(),
"the product folds straight into the accumulator"
);
}
#[test]
fn ab_wholesale_makearray_and_elementwise_observeds() {
let n = 8;
let interior = json!({"op": "faq", "args": [], "output_idx": ["i"],
"ranges": {"i": [2, n - 1]},
"expr": {"op": "-", "args": [
idx("u", json!({"op": "+", "args": ["i", 1]})),
idx("u", json!({"op": "-", "args": ["i", 1]}))
]}});
let top = json!({"op": "faq", "args": [], "output_idx": ["i"],
"ranges": {"i": [n, n]},
"expr": {"op": "*", "args": [2.0, idx("u", json!("i"))]}});
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_wholesale"},
"models": {"M": {
"variables": {
"u": {"type": "unknown", "shape": ["i"]},
"g": {"type": "unknown", "shape": ["i"]},
"q": {"type": "unknown", "shape": ["i"]},
"h": {"type": "unknown", "shape": ["i"]}
},
"equations": [
{"lhs": "g", "rhs": {"op": "faq", "args": [], "output_idx": ["i"],
"ranges": {"i": [1, n]},
"expr": {"op": "sin", "args": [{"op": "*", "args": [0.5, "i"]}]}}},
{"lhs": "q", "rhs": {"op": "makearray", "args": [],
"regions": [[[2, n - 1]], [[1, 1]], [[n, n]]],
"values": [interior, 1.5, top]}},
{"lhs": "h", "rhs": {"op": "+", "args": [
{"op": "*", "args": [2.0, "u"]}, "g", "q"]}},
d_eq("u", n, agg(n, json!({"op": "neg", "args": [
{"op": "index", "args": ["h", "i"]}]})))
]
}}
});
let prog = ab_check(doc, -2.0, 2.0);
assert!(prog.n_const > 0);
}
fn fallback_ifelse_array_condition_doc(reader: serde_json::Value) -> serde_json::Value {
let n = 3;
json!({
"esm": "1.1.0",
"metadata": {"name": "tape_fallback_ifelse"},
"index_sets": {"c": {"kind": "interval", "size": n}},
"models": {"M": {
"variables": {
"psi": {"type": "unknown", "shape": ["c"]},
"m": {"type": "unknown", "shape": ["c"]},
"s": {"type": "unknown"},
"a": {"type": "unknown", "shape": ["c"]},
"p": {"type": "parameter", "default": 2.0}
},
"equations": [
{"lhs": "m", "rhs": {"op": "ifelse", "args": [
{"op": ">", "args": ["psi", {"op": "index", "args": ["psi", "p", 1]}]},
1.0,
0.0
]}},
{"lhs": "s", "rhs": reader},
{"lhs": "a", "rhs": {"op": "+", "args": ["psi", "s"]}},
{"lhs": {"op": "ic", "args": ["psi"]}, "rhs": 0.0},
{"lhs": {"op": "D", "args": ["psi"], "wrt": "t"},
"rhs": {"op": "-", "args": ["a"]}}
]
}}
})
}
fn check_fallback_ifelse_array_condition(reader: serde_json::Value) {
let doc = fallback_ifelse_array_condition_doc(reader);
let (_prog, report) = compile(doc.clone()).build_tape(&HashSet::new());
assert!(
report
.fallbacks
.iter()
.any(|(name, reason)| name == "m" && reason.contains("computed subscripts")),
"`m` must be the declined rule this test is about: {:?}",
report.fallbacks
);
declined_check(doc);
}
#[test]
fn ab_fallback_ifelse_array_condition_elementwise_reader() {
check_fallback_ifelse_array_condition(json!({"op": "*", "args": ["m", 2.0]}));
}
#[test]
fn ab_fallback_ifelse_array_condition_indexed_reader() {
check_fallback_ifelse_array_condition(json!({"op": "index", "args": ["m", 2]}));
}
#[test]
fn ab_model_file_if_available() {
let Ok(path) = std::env::var("TAPE_AB_MODEL") else {
return;
};
let mut mp: std::collections::BTreeMap<String, i64> = Default::default();
if let Ok(spec) = std::env::var("TAPE_AB_MP") {
for kv in spec.split(',') {
let (k, v) = kv.split_once('=').expect("KEY=VALUE");
mp.insert(k.to_string(), v.parse().expect("integer"));
}
}
let file =
crate::load_path_with_options(std::path::Path::new(&path), &mp).expect("model loads");
let seed_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,
&crate::simulate::SolveOptions {
alg: crate::simulate::Alg::Erk,
abstol: Some(1e-8),
reltol: Some(1e-6),
saveat: Some(vec![0.0]),
..Default::default()
},
)
})
.expect("u0 seed solve");
let compiled = ArrayCompiled::from_file(&file).expect("model compiles");
let n = compiled.state_variable_names().len();
let u0: Vec<f64> = seed_sol.state.iter().take(n).map(|row| row[0]).collect();
let (prog, report) = taped(compiled.build_tape(&HashSet::new()));
eprintln!("{report}");
assert!(report.fallbacks.is_empty(), "{:?}", report.fallbacks);
let params = HashMap::new();
let param_vec = compiled.debug_resolve_params(¶ms);
let mut fast = compiled
.debug_new_scratch_taped()
.expect("the tape lowers every rule");
let mut fast_stats = RhsStats::default();
for seed in 0..3u64 {
let noise = seeded_state(n, seed, -0.01, 0.01);
let state: Vec<f64> = u0.iter().zip(&noise).map(|(u, e)| u * (1.0 + e)).collect();
for &t in &[0.0, 1234.5] {
let (dy_ref, _) = compiled.debug_eval_rhs(&state, t, ¶ms, false);
let mut dy = vec![0.0f64; n];
run_reference(&prog, &compiled, &state, ¶m_vec, t, &mut dy);
let mut diverged = 0usize;
for (k, (a, b)) in dy.iter().zip(dy_ref.iter()).enumerate() {
if a.to_bits() != b.to_bits() {
if diverged < 8 {
eprintln!("dy[{k}]: tape {a:e} vs interpreter {b:e}");
}
diverged += 1;
}
}
assert_eq!(diverged, 0, "seed {seed} t {t}: {diverged} slots diverged");
let mut dy_fast = vec![0.0f64; n];
compiled.debug_eval_rhs_into(
&state,
t,
¶m_vec,
&mut dy_fast,
&mut fast,
&mut fast_stats,
);
let mut diverged = 0usize;
for (k, (a, b)) in dy_fast.iter().zip(dy_ref.iter()).enumerate() {
if a.to_bits() != b.to_bits() {
if diverged < 8 {
eprintln!("dy[{k}] (FAST): tape {a:e} vs interpreter {b:e}");
}
diverged += 1;
}
}
assert_eq!(
diverged, 0,
"seed {seed} t {t}: {diverged} slots diverged (FAST exec)"
);
}
}
}
#[test]
fn ab_shifted_read_folding_wrap_ghost_linear() {
let (ni, nj) = (8, 6);
let idx2 = |var: &str, i: serde_json::Value, j: serde_json::Value| json!({"op": "index", "args": [var, i, j]});
let lap_wrap = json!({"op": "+", "args": [
idx2("u", wrap(json!({"op": "-", "args": ["i", 1]}), 1, ni), json!("j")),
{"op": "*", "args": [-2.0, idx2("u", json!("i"), json!("j"))]},
idx2("u", wrap(json!({"op": "+", "args": ["i", 1]}), 1, ni), json!("j")),
idx2("u", json!("i"), wrap(json!({"op": "-", "args": ["j", 1]}), 1, nj)),
idx2("u", json!("i"), wrap(json!({"op": "+", "args": ["j", 1]}), 1, nj))
]});
let lap_ghost = json!({"op": "+", "args": [
idx2("v", json!({"op": "-", "args": ["i", 1]}), json!("j")),
{"op": "*", "args": [-2.0, idx2("v", json!("i"), json!("j"))]},
idx2("v", json!({"op": "+", "args": ["i", 1]}), json!("j")),
idx2("v", json!("i"), json!({"op": "-", "args": ["j", 1]})),
idx2("v", json!("i"), json!({"op": "+", "args": ["j", 1]}))
]});
let d2 = |var: &str, rhs: serde_json::Value| {
json!({
"lhs": {"op": "faq", "args": [], "output_idx": ["i", "j"],
"expr": {"op": "D", "args": [
{"op": "index", "args": [var, "i", "j"]}], "wrt": "t"},
"ranges": {"i": [1, ni], "j": [1, nj]}},
"rhs": rhs
})
};
let agg2 = |body: serde_json::Value| {
json!({"op": "faq", "args": [], "output_idx": ["i", "j"],
"ranges": {"i": [1, ni], "j": [1, nj]}, "expr": body})
};
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_fold"},
"models": {"M": {
"variables": {
"u": {"type": "unknown", "shape": ["i", "j"]},
"v": {"type": "unknown", "shape": ["i", "j"]},
"w": {"type": "unknown", "shape": ["i", "j"]},
"s": {"type": "unknown", "shape": ["i"]},
"r": {"type": "unknown", "shape": ["j"]}
},
"equations": [
{"lhs": "s", "rhs": {"op": "faq", "args": [], "output_idx": ["i"],
"ranges": {"i": [1, ni]},
"expr": {"op": "*", "args": [0.5,
{"op": "index", "args": ["w", "i", 3]}]}}},
{"lhs": "r", "rhs": {"op": "faq", "args": [], "output_idx": ["j"],
"ranges": {"j": [1, nj]},
"expr": {"op": "*", "args": [0.5,
{"op": "index", "args": ["w", 2, "j"]}]}}},
d2("u", agg2(json!({"op": "*", "args": [0.25, lap_wrap]}))),
d2("v", agg2(json!({"op": "*", "args": [0.25, lap_ghost]}))),
d2("w", agg2(json!({"op": "*", "args": [
{"op": "+", "args": [
{"op": "index", "args": ["s", "i"]},
{"op": "index", "args": ["r", "j"]}]},
{"op": "index", "args": ["w", "i", "j"]}
]})))
]
}}
});
let prog = ab_check(doc, -2.0, 2.0);
assert!(
prog.fuse_stats.n_gathers_folded >= 4,
"expected the stencil gathers to fold: {:?}",
prog.fuse_stats
);
let any_multi_run = prog.fused.iter().any(|f| f.schedule.n_runs > 1);
let any_ghost = prog
.fused
.iter()
.flat_map(|f| f.schedule.expanded())
.any(|r| r.in_off.contains(&GHOST_OFF));
let any_strided = prog
.fused
.iter()
.flat_map(|f| f.inputs.iter())
.any(|i| i.shifted_ix.is_some() && i.elem_stride > 1);
assert!(any_multi_run, "wrap fold must produce a multi-run schedule");
assert!(any_ghost, "ghost-edge fold must produce a ghost run");
assert!(any_strided, "level-slice fold must produce a strided input");
}
#[test]
fn output_only_observeds_leave_the_rhs() {
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_output_only"},
"models": {"M": {
"variables": {
"x": {"type": "unknown"},
"s": {"type": "unknown"},
"o": {"type": "unknown"}
},
"equations": [
{"lhs": "o", "rhs": {"op": "*", "args": [{"op": "sin", "args": ["x"]}, 3.0]}},
{"lhs": "s", "rhs": {"op": "*", "args": [2.0, "x"]}},
{"lhs": {"op": "D", "args": ["x"], "wrt": "t"},
"rhs": {"op": "neg", "args": ["s"]}}
]
}}
});
ab_check(doc.clone(), -1.0, 1.0);
let compiled = compile(doc);
let (prog, _) = taped(compiled.build_tape(&HashSet::new()));
let cont = prog.section_range(Cadence::Continuous).len() as u32;
assert!(
prog.n_rhs < cont,
"o's instructions leave the call: {:?}",
prog.instrs
);
let exported = prog.exports.iter().any(|(n, _)| n == "o");
let params = HashMap::new();
let param_vec = compiled.debug_resolve_params(¶ms);
let state = vec![1.5f64];
let run_call = |ctx: &mut super::exec::TapeCtx, dy: &mut [f64]| {
let mut stats = RhsStats::default();
let call = super::super::RhsCall {
rhs_rules: &compiled.rhs_rules,
observed_rules: &compiled.observed_rules,
var_shapes: &compiled.var_shapes,
param_names: &compiled.param_names,
state: &state,
params: ¶m_vec,
forcing: &compiled.forcing,
t: 0.0,
declared: &compiled.declared_names,
rule_set: compiled.rule_set,
};
super::exec::run_tape_call(ctx, &call, dy, &mut stats);
};
let mut ctx = super::exec::TapeCtx::new(std::sync::Arc::new(prog));
let mut dy = vec![0.0f64; 1];
run_call(&mut ctx, &mut dy);
assert_eq!(dy[0].to_bits(), (-3.0f64).to_bits());
ctx.set_exports_active(true);
let mut dy2 = vec![0.0f64; 1];
run_call(&mut ctx, &mut dy2);
assert_eq!(dy2[0].to_bits(), dy[0].to_bits());
if exported {
let o = ctx.exec.obs.get("o").expect("o published");
assert_eq!(
o[ndarray::IxDyn(&[])].to_bits(),
(1.5f64.sin() * 3.0).to_bits()
);
}
}
#[test]
fn export_demotion_skips_unread_publishes() {
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_export_demote"},
"models": {"M": {
"variables": {
"x": {"type": "unknown"},
"s": {"type": "unknown"}
},
"equations": [
{"lhs": "s", "rhs": {"op": "*", "args": [2.0, "x"]}},
{"lhs": {"op": "D", "args": ["x"], "wrt": "t"},
"rhs": {"op": "neg", "args": ["s"]}}
]
}}
});
let compiled = compile(doc);
let (prog, report) = taped(compiled.build_tape(&HashSet::new()));
assert!(report.fallbacks.is_empty(), "{:?}", report.fallbacks);
assert!(
prog.exports.iter().any(|(n, _)| n == "s"),
"fixture must export `s`"
);
let params = HashMap::new();
let param_vec = compiled.debug_resolve_params(¶ms);
let state = vec![1.5f64];
let run_call = |ctx: &mut super::exec::TapeCtx, dy: &mut [f64]| {
let mut stats = RhsStats::default();
let call = super::super::RhsCall {
rhs_rules: &compiled.rhs_rules,
observed_rules: &compiled.observed_rules,
var_shapes: &compiled.var_shapes,
param_names: &compiled.param_names,
state: &state,
params: ¶m_vec,
forcing: &compiled.forcing,
t: 0.0,
declared: &compiled.declared_names,
rule_set: compiled.rule_set,
};
super::exec::run_tape_call(ctx, &call, dy, &mut stats);
};
let mut ctx = super::exec::TapeCtx::new(std::sync::Arc::new(prog));
let mut dy = vec![0.0f64; 1];
run_call(&mut ctx, &mut dy);
assert_eq!(dy[0].to_bits(), (-3.0f64).to_bits());
assert!(
!ctx.exec.obs.contains_key("s"),
"demoted export must not publish"
);
ctx.set_exports_active(true);
let mut dy2 = vec![0.0f64; 1];
run_call(&mut ctx, &mut dy2);
assert_eq!(dy2[0].to_bits(), dy[0].to_bits());
let s = ctx.exec.obs.get("s").expect("export array");
assert_eq!(
s[ndarray::IxDyn(&[])].to_bits(),
3.0f64.to_bits(),
"active export must publish the slot value"
);
}
#[test]
fn ab_superop_bin3_and_extended_pairs() {
let n = 9;
let u = idx("u", json!("i"));
let v = idx("v", json!("i"));
let chain = json!({"op": "/", "args": [
{"op": "-", "args": [
{"op": "*", "args": [
{"op": "+", "args": [{"op": "*", "args": [u.clone(), 2.5]}, v.clone()]},
u.clone()
]},
1.5
]},
{"op": "+", "args": [v.clone(), 3.0]}
]});
let limiter = json!({"op": "ifelse", "args": [
{"op": ">", "args": [{"op": "*", "args": [u.clone(), v.clone()]}, 1.0]},
{"op": "max", "args": [{"op": "min", "args": [u.clone(), v.clone()]}, 0.1]},
u.clone()
]});
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_superops"},
"models": {"M": {
"variables": {
"u": {"type": "unknown", "shape": ["i"]},
"v": {"type": "unknown", "shape": ["i"]}
},
"equations": [
d_eq("u", n, agg(n, chain)),
d_eq("v", n, agg(n, limiter))
]
}}
});
let prog = ab_check(doc.clone(), -3.0, 3.0);
let has_bin3 = |p: &TapeProgram| {
p.fused
.iter()
.any(|f| f.micro.iter().any(|m| matches!(m, MicroOp::Bin3 { .. })))
};
let has_ext_bin2 = prog.fused.iter().any(|f| {
f.micro.iter().any(|m| {
matches!(m, MicroOp::Bin2 { op1, op2, .. }
if matches!((op1, op2),
(crate::simulate_array::BinCode::Mul, crate::simulate_array::BinCode::Gt)
| (crate::simulate_array::BinCode::Min, crate::simulate_array::BinCode::Max)))
})
});
assert!(
has_ext_bin2,
"expected an extended-pair Bin2 ((Mul,Gt) or (Min,Max)) in the fused program"
);
assert!(
!has_bin3(&prog),
"Bin3 must stay off in the default configuration"
);
let compiled = compile(doc);
let (prog3, _) = taped(compiled.build_tape_opts(&HashSet::new(), Some(all_superops_cfg())));
assert!(
has_bin3(&prog3),
"expected a Bin3 superop with bin3 enabled"
);
for f in &prog3.fused {
let has3 = f.micro.iter().any(|m| matches!(m, MicroOp::Bin3 { .. }));
if has3 {
assert_eq!(f.n_splat_regs as usize, f.scalars.len() + 1);
} else {
assert_eq!(f.n_splat_regs, 0);
}
}
let n_state = compiled.state_variable_names().len();
let params = HashMap::new();
let param_vec = compiled.debug_resolve_params(¶ms);
let mut fast3 = super::super::RhsScratch::new(&compiled.var_shapes);
fast3.install_tape(std::sync::Arc::new(
taped(compiled.build_tape_opts(&HashSet::new(), Some(all_superops_cfg()))).0,
));
let mut stats = RhsStats::default();
for seed in 0..4u64 {
let state = seeded_state(n_state, seed, -3.0, 3.0);
for &t in &[0.0, 0.37, 2.5] {
let (dy_ref, _) = compiled.debug_eval_rhs(&state, t, ¶ms, false);
let mut dy = vec![0.0f64; n_state];
run_reference(&prog3, &compiled, &state, ¶m_vec, t, &mut dy);
for (k, (a, b)) in dy.iter().zip(dy_ref.iter()).enumerate() {
assert_eq!(
a.to_bits(),
b.to_bits(),
"seed {seed} t {t}: dy[{k}] diverged: bin3 tape-ref vs interpreter"
);
}
let mut dy_fast = vec![0.0f64; n_state];
compiled.debug_eval_rhs_into(
&state,
t,
¶m_vec,
&mut dy_fast,
&mut fast3,
&mut stats,
);
for (k, (a, b)) in dy_fast.iter().zip(dy_ref.iter()).enumerate() {
assert_eq!(
a.to_bits(),
b.to_bits(),
"seed {seed} t {t}: dy[{k}] diverged: bin3 FAST exec vs interpreter"
);
}
}
}
}
fn bcast_doc(name: &str, n: i64, rhs: serde_json::Value) -> serde_json::Value {
json!({
"esm": "1.1.0",
"metadata": {"name": name},
"models": {"M": {
"variables": {
"u": {"type": "unknown", "shape": ["i"]},
"s": {"type": "unknown", "shape": ["i"]}
},
"equations": [
{"lhs": "s",
"rhs": agg(n, json!({"op": "*", "args": [0.5, idx("u", json!("i"))]}))},
d_eq("u", n, agg(n, rhs))
]
}}
})
}
fn dy_three_ways(doc: serde_json::Value, state: &[f64]) -> [Vec<f64>; 3] {
let compiled = compile(doc);
let params = HashMap::new();
let param_vec = compiled.debug_resolve_params(¶ms);
let (dy_oracle, _) = compiled.debug_eval_rhs(state, 0.0, ¶ms, true);
let (dy_vec, _) = compiled.debug_eval_rhs(state, 0.0, ¶ms, false);
let (prog, report) = taped(compiled.build_tape(&HashSet::new()));
assert!(
report.fallbacks.is_empty(),
"the tape must lower this model with no fallback, else the tape path is untested: {:?}",
report.fallbacks
);
let mut dy_tape = vec![0.0f64; state.len()];
run_reference(&prog, &compiled, state, ¶m_vec, 0.0, &mut dy_tape);
[dy_oracle, dy_vec, dy_tape]
}
fn assert_bits_eq(a: &[f64], b: &[f64], what: &str) {
assert_eq!(a.len(), b.len(), "{what}: length");
for (k, (x, y)) in a.iter().zip(b.iter()).enumerate() {
assert_eq!(
x.to_bits(),
y.to_bits(),
"{what}: dy[{k}] diverged: {x:?} ({:016x}) vs {y:?} ({:016x})",
x.to_bits(),
y.to_bits()
);
}
}
#[test]
fn unary_broadcast_matches_the_bare_node_on_all_three_paths() {
let n = 6i64;
let state = seeded_state(n as usize, 11, 0.25, 3.0);
for f in [
"-", "neg", "log", "exp", "sqrt", "sin", "cos", "tanh", "floor", "not",
] {
let operand = idx("s", json!("i"));
let bare = json!({"op": f, "args": [operand]});
let bcast = json!({"op": "broadcast", "fn": f, "args": [operand]});
let [o_bare, v_bare, t_bare] = dy_three_ways(bcast_doc("bare", n, bare), &state);
let [o_bc, v_bc, t_bc] = dy_three_ways(bcast_doc("bcast", n, bcast), &state);
assert_bits_eq(&o_bc, &o_bare, &format!("fn `{f}`: oracle"));
assert_bits_eq(&v_bc, &v_bare, &format!("fn `{f}`: vectorized overlay"));
assert_bits_eq(&t_bc, &t_bare, &format!("fn `{f}`: tape"));
assert_bits_eq(&v_bc, &o_bc, &format!("fn `{f}`: overlay vs oracle"));
assert_bits_eq(&t_bc, &o_bc, &format!("fn `{f}`: tape vs oracle"));
let [o_id, _, _] = dy_three_ways(bcast_doc("id", n, idx("s", json!("i"))), &state);
assert!(
o_bare
.iter()
.zip(o_id.iter())
.any(|(a, b)| a.to_bits() != b.to_bits()),
"fn `{f}` is the identity on this state โ the test would pass vacuously"
);
}
}
#[test]
fn n_ary_broadcast_folds_are_unchanged() {
let n = 6i64;
let state = seeded_state(n as usize, 7, 0.25, 3.0);
let s = || idx("s", json!("i"));
let cases: Vec<(&str, serde_json::Value, serde_json::Value)> = vec![
(
"unary +",
json!({"op": "broadcast", "fn": "+", "args": [s()]}),
json!({"op": "+", "args": [s()]}),
),
(
"binary -",
json!({"op": "broadcast", "fn": "-", "args": [s(), 0.25]}),
json!({"op": "-", "args": [s(), 0.25]}),
),
(
"ternary *",
json!({"op": "broadcast", "fn": "*", "args": [s(), 2.0, s()]}),
json!({"op": "*", "args": [s(), 2.0, s()]}),
),
(
"binary min",
json!({"op": "broadcast", "fn": "min", "args": [s(), 1.0]}),
json!({"op": "min", "args": [s(), 1.0]}),
),
(
"binary atan2",
json!({"op": "broadcast", "fn": "atan2", "args": [s(), 2.0]}),
json!({"op": "atan2", "args": [s(), 2.0]}),
),
(
"ternary ifelse",
json!({"op": "broadcast", "fn": "ifelse",
"args": [{"op": ">", "args": [s(), 0.5]}, s(), 0.125]}),
json!({"op": "ifelse",
"args": [{"op": ">", "args": [s(), 0.5]}, s(), 0.125]}),
),
];
for (label, bcast, bare) in cases {
let [o_bare, v_bare, t_bare] = dy_three_ways(bcast_doc("bare", n, bare), &state);
let [o_bc, v_bc, t_bc] = dy_three_ways(bcast_doc("bcast", n, bcast), &state);
assert_bits_eq(&o_bc, &o_bare, &format!("{label}: oracle"));
assert_bits_eq(&v_bc, &v_bare, &format!("{label}: vectorized overlay"));
assert_bits_eq(&t_bc, &t_bare, &format!("{label}: tape"));
}
}
#[test]
fn unary_broadcast_minus_negates() {
let n = 3i64;
let state = vec![1.0, 2.0, 4.0];
let rhs = json!({"op": "broadcast", "fn": "-", "args": [idx("s", json!("i"))]});
for dy in dy_three_ways(bcast_doc("neg_check", n, rhs), &state) {
assert_bits_eq(&dy, &[-0.5, -1.0, -2.0], "broadcast(fn=\"-\") must negate");
}
}
fn opcount(prog: &TapeProgram, opcode: &str) -> usize {
prog.instrs.iter().filter(|i| i.opcode() == opcode).count()
}
fn reductions(prog: &TapeProgram) -> Vec<(DimU, usize, SlotId)> {
let mut out = Vec::new();
for i in &prog.instrs {
match i {
Instr::Reduce {
axes,
src_shape,
out: o,
..
} => {
assert!(
axes.iter().enumerate().all(|(k, &a)| a as usize == k),
"the tape folds leading axes"
);
out.push((src_shape.clone(), axes.len(), *o));
}
Instr::Fused { spec } => {
let fs = &prog.fused[*spec as usize];
if let Some(r) = &fs.reduce {
let mut inner = 1usize;
let mut kept = 0usize;
while inner < r.n_inner {
kept += 1;
inner *= fs.shape[fs.shape.len() - kept];
}
assert_eq!(inner, r.n_inner, "n_inner is a trailing sub-box");
out.push((fs.shape.clone(), fs.shape.len() - kept, r.out));
}
}
_ => {}
}
}
out
}
#[test]
fn ab_const_array_observed() {
let n = 4;
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_const_array"},
"models": {"M": {
"variables": {
"u": {"type": "unknown", "shape": ["i"]},
"zc": {"type": "unknown", "shape": ["i"]},
"f": {"type": "unknown", "shape": ["i"]}
},
"equations": [
{"lhs": "zc", "rhs": {"op": "const", "args": [],
"value": [0.25, -1.5, 2.0, 3.75]}},
{"lhs": "f", "rhs": {"op": "+", "args": [
1, {"op": "cos", "args": [{"op": "*", "args": [3.5, "zc"]}]}]}},
d_eq("u", n, agg(n, json!({"op": "*", "args": [
idx("f", json!("i")),
idx("zc", wrap(json!({"op": "+", "args": ["i", 1]}), 1, n))
]})))
]
}}
});
let prog = ab_check(doc, -2.0, 2.0);
assert_eq!(
opcount(&prog, "ConstArray"),
1,
"the literal is stored once"
);
assert_eq!(prog.const_data.len(), 1);
assert_eq!(&prog.const_data[0].shape[..], &[4]);
assert_eq!(&prog.const_data[0].values, &[0.25, -1.5, 2.0, 3.75]);
let store = prog
.instrs
.iter()
.position(|i| matches!(i, Instr::ConstArray { .. }))
.expect("ConstArray emitted");
assert_eq!(prog.section_of(store), Cadence::Const);
}
#[test]
fn ab_rank0_scalar_reductions() {
let n = 5;
let red = |kind: &str| {
json!({"op": "faq", "args": [], "output_idx": [], "reduce": kind,
"ranges": {"j": [1, n]},
"expr": {"op": "*", "args": [idx("u", json!("j")), 1.5]}})
};
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_rank0_reduce"},
"models": {"M": {
"variables": {
"u": {"type": "unknown", "shape": ["i"]},
"s": {"type": "unknown", "default": 0.0},
"tot": {"type": "unknown"},
"prod": {"type": "unknown"},
"hi": {"type": "unknown"},
"lo": {"type": "unknown"}
},
"equations": [
{"lhs": "tot", "rhs": red("+")},
{"lhs": "prod", "rhs": red("*")},
{"lhs": "hi", "rhs": red("max")},
{"lhs": "lo", "rhs": red("min")},
{"lhs": {"op": "D", "args": ["s"], "wrt": "t"},
"rhs": {"op": "+", "args": ["tot", "prod", "hi", "lo"]}},
d_eq("u", n, agg(n, json!({"op": "*", "args": ["tot", idx("u", json!("i"))]})))
]
}}
});
let prog = ab_check(doc, -2.0, 2.0);
let folds = reductions(&prog);
assert_eq!(folds.len(), 4, "one fold per reduction rule");
for (src_shape, n_axes, out) in folds {
assert_eq!(n_axes, 1, "the single contracted axis");
assert_eq!(&src_shape[..], &[n as usize]);
assert!(prog.slots[out as usize].scalar, "rank-0 output");
}
}
#[test]
fn ab_rank0_reduction_two_contracted_axes() {
let n = 4;
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_rank0_reduce_2d"},
"models": {"M": {
"variables": {
"u": {"type": "unknown", "shape": ["i"]},
"s": {"type": "unknown", "default": 0.0},
"tot": {"type": "unknown"}
},
"equations": [
{"lhs": "tot", "rhs": {"op": "faq", "args": [], "output_idx": [],
"ranges": {"j": [1, n], "k": [1, n]},
"expr": {"op": "*", "args": [
{"op": "^", "args": [10.0, "k"]},
idx("u", json!("j"))]}}},
{"lhs": {"op": "D", "args": ["s"], "wrt": "t"}, "rhs": "tot"},
d_eq("u", n, agg(n, json!({"op": "*", "args": ["tot", idx("u", json!("i"))]})))
]
}}
});
let prog = ab_check(doc, -3.0, 3.0);
let folds = reductions(&prog);
let (src_shape, n_axes, _) = folds.first().expect("a fold emitted");
assert_eq!(*n_axes, 2);
assert_eq!(&src_shape[..], &[n as usize, n as usize]);
}
#[test]
fn ab_rank0_reduction_with_a_filter() {
let n = 4;
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_rank0_filter"},
"models": {"M": {
"variables": {
"u": {"type": "unknown", "shape": ["i"]},
"s": {"type": "unknown", "default": 0.0},
"tot": {"type": "unknown"}
},
"equations": [
{"lhs": "tot", "rhs": {"op": "faq", "args": [], "output_idx": [],
"ranges": {"j": [1, n]},
"filter": {"op": "<=", "args": ["j", 2]},
"expr": idx("u", json!("j"))}},
{"lhs": {"op": "D", "args": ["s"], "wrt": "t"}, "rhs": "tot"},
d_eq("u", n, agg(n, idx("u", json!("i"))))
]
}}
});
let prog = ab_check(doc, -2.0, 2.0);
assert_eq!(reductions(&prog).len(), 1, "one fold");
}
#[test]
fn a_reader_of_a_declined_observed_declines_at_the_read() {
let n = 6;
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_declined_producer_shape"},
"models": {"M": {
"variables": {
"u": {"type": "unknown", "shape": ["i"]},
"r": {"type": "unknown", "shape": ["i"]},
"g": {"type": "unknown", "shape": ["i"]}
},
"equations": [
{"lhs": "r", "rhs": {"op": "faq", "args": [], "output_idx": ["i"],
"ranges": {"i": [1, n]},
"expr": {"op": "ifelse", "args": [
{"op": "<=", "args": ["i", 1]},
idx("u", json!("i")),
{"op": "+", "args": [
idx("u", json!("i")),
{"op": "*", "args": [
0.5,
untaped_self_read("r", json!({"op": "-", "args": ["i", 1]}))]}
]}
]}}},
{"lhs": "g", "rhs": {"op": "*", "args": [2.0, "r"]}},
d_eq("u", n, agg(n, json!({"op": "-", "args": [
idx("g", json!("i")),
idx("r", wrap(json!({"op": "+", "args": ["i", 1]}), 1, n))
]})))
]
}}
});
let declined = declined_check(doc);
let rules = declined.declined_rules();
let names: Vec<&str> = rules.iter().map(|(name, _)| name.as_str()).collect();
assert_eq!(names, ["r", "g", "D(u)"]);
for (name, reason) in &rules[1..] {
assert!(
reason.contains("observed `r` is produced by a declined rule")
|| reason.contains("observed `g` is produced by a declined rule"),
"{name}: {reason}"
);
}
}
#[test]
fn ab_elementwise_observed_gather_fixture() {
let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"../../tests/conformance/elementwise_observed_gather/fixtures/elementwise_gather.esm",
);
let file = crate::parse::load_path(&path).expect("corpus fixture loads");
let compiled = ArrayCompiled::from_file(&file).expect("fixture compiles");
for (label, fuse) in [("fused", Some(default_cfg())), ("unfused", None)] {
let (prog, report) = taped(compiled.build_tape_opts(&HashSet::new(), fuse));
assert!(
report.fallbacks.is_empty(),
"{label}: {:?}",
report.fallbacks
);
assert_eq!(report.n_taped, 6, "{label}: all six rules");
assert_eq!(opcount(&prog, "ConstArray"), 1, "{label}");
assert_eq!(opcount(&prog, "Reduce"), 1, "{label}");
let n = compiled.state_variable_names().len();
let params = HashMap::new();
let param_vec = compiled.debug_resolve_params(¶ms);
for seed in 0..3u64 {
let state = seeded_state(n, seed, -2.0, 2.0);
for &t in &[0.0, 1.0] {
let (dy_ref, _) = compiled.debug_eval_rhs(&state, t, ¶ms, false);
let mut dy = vec![0.0f64; n];
run_reference(&prog, &compiled, &state, ¶m_vec, t, &mut dy);
for (k, (a, b)) in dy.iter().zip(dy_ref.iter()).enumerate() {
assert_eq!(
a.to_bits(),
b.to_bits(),
"{label} seed {seed} t {t}: dy[{k}] {a:?} vs {b:?}"
);
}
}
}
}
let n = compiled.state_variable_names().len();
let params = HashMap::new();
let param_vec = compiled.debug_resolve_params(¶ms);
let mut fast = compiled
.debug_new_scratch_taped()
.expect("the tape lowers every rule");
for seed in 0..3u64 {
let state = seeded_state(n, seed, -2.0, 2.0);
for &t in &[0.0, 1.0] {
let (dy_ref, _) = compiled.debug_eval_rhs(&state, t, ¶ms, false);
assert_fast_matches(
&compiled,
&mut fast,
¶m_vec,
&state,
t,
&dy_ref,
"elementwise_gather fixture",
);
}
}
}
const DATETIME_TIMES: &[f64] = &[
0.0,
-1.0,
-0.5,
86_399.999,
86_400.0,
946_684_800.0, 951_782_400.0, 951_868_799.0, 1_709_164_800.0, 1_735_689_599.0, 1_735_689_600.0, -2_208_988_800.0, -2_203_891_201.0, 4_102_444_800.0, -62_135_596_800.0, 1_500_000_000.25,
-1_500_000_000.25,
-49_539_254_400.0, -36_916_473_600.0, -11_670_912_000.0, 951_868_800.0, ];
const DATETIME_NAMES: &[&str] = &[
"datetime.year",
"datetime.month",
"datetime.day",
"datetime.hour",
"datetime.minute",
"datetime.second",
"datetime.day_of_year",
"datetime.julian_day",
"datetime.is_leap_year",
];
fn datetime_doc() -> serde_json::Value {
let n = 3i64;
let mut vars = serde_json::Map::new();
let mut eqs: Vec<serde_json::Value> = Vec::new();
for (k, name) in DATETIME_NAMES.iter().enumerate() {
let var = format!("f{k}");
vars.insert(var.clone(), json!({"type": "unknown"}));
eqs.push(json!({
"lhs": {"op": "D", "args": [var], "wrt": "t"},
"rhs": {"op": "fn", "name": name, "args": ["t"]}
}));
}
vars.insert("u".to_string(), json!({"type": "unknown", "shape": ["i"]}));
eqs.push(d_eq(
"u",
n,
agg(
n,
json!({"op": "*", "args": [
"i",
{"op": "fn", "name": "datetime.hour", "args": ["t"]}
]}),
),
));
json!({
"esm": "1.1.0",
"metadata": {"name": "tape_datetime"},
"models": {"M": {"variables": vars, "equations": eqs}}
})
}
fn datetime_ab_check(doc: serde_json::Value, times: &[f64]) {
let compiled = compile(doc);
let (prog, report) = taped(compiled.build_tape_opts(&HashSet::new(), Some(default_cfg())));
assert!(
report.fallbacks.is_empty(),
"the datetime family must lower with no fallback, else the tape path is \
untested: {:?}",
report.fallbacks
);
let (prog_uf, _) = taped(compiled.build_tape_opts(&HashSet::new(), None));
let n = compiled.state_variable_names().len();
let params = HashMap::new();
let param_vec = compiled.debug_resolve_params(¶ms);
let mut scratch = compiled
.debug_new_scratch_taped()
.expect("the tape lowers every rule");
let state = vec![0.0f64; n];
for &t in times {
let (dy_ref, _) = compiled.debug_eval_rhs(&state, t, ¶ms, false);
for (label, p) in [("fused", &prog), ("unfused", &prog_uf)] {
let mut dy = vec![0.0f64; n];
run_reference(p, &compiled, &state, ¶m_vec, t, &mut dy);
assert_bits_eq(&dy, &dy_ref, &format!("t={t} tape-ref {label}"));
}
assert_fast_matches(
&compiled,
&mut scratch,
¶m_vec,
&state,
t,
&dy_ref,
&format!("t={t} fast"),
);
}
}
#[test]
fn ab_datetime_family() {
datetime_ab_check(datetime_doc(), DATETIME_TIMES);
}
#[test]
fn ab_datetime_nan_argument() {
let nan = json!({"op": "/", "args": ["x", "x"]});
let mut vars = serde_json::Map::new();
vars.insert("x".to_string(), json!({"type": "unknown"}));
let mut eqs = vec![json!({"lhs": {"op": "D", "args": ["x"], "wrt": "t"}, "rhs": 0.0})];
for (k, name) in DATETIME_NAMES.iter().enumerate() {
let var = format!("f{k}");
vars.insert(var.clone(), json!({"type": "unknown"}));
eqs.push(json!({
"lhs": {"op": "D", "args": [var], "wrt": "t"},
"rhs": {"op": "fn", "name": name, "args": [nan]}
}));
}
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_datetime_nan"},
"models": {"M": {"variables": vars, "equations": eqs}}
});
datetime_ab_check(doc, &[0.0, 1.0]);
}
#[test]
fn datetime_on_a_literal_time_folds_at_build_time() {
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_datetime_const"},
"models": {"M": {
"variables": {"x": {"type": "unknown"}},
"equations": [
{"lhs": {"op": "D", "args": ["x"], "wrt": "t"},
"rhs": {"op": "fn", "name": "datetime.year", "args": [946684800.0]}}
]
}}
});
let compiled = compile(doc);
let (prog, report) = taped(compiled.build_tape(&HashSet::new()));
assert!(report.fallbacks.is_empty(), "{:?}", report.fallbacks);
for op in ["Bin", "Un", "Select"] {
assert_eq!(
opcount(&prog, op),
0,
"a constant `datetime.year` left a `{op}` instruction behind:\n{report}"
);
}
let params = HashMap::new();
let param_vec = compiled.debug_resolve_params(¶ms);
let mut dy = vec![0.0f64; 1];
run_reference(&prog, &compiled, &[0.0], ¶m_vec, 0.0, &mut dy);
assert_eq!(dy[0], 2000.0, "datetime.year(946684800) is 2000");
}
#[test]
fn interp_closed_functions_lower_to_one_instruction() {
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_interp"},
"models": {"M": {
"variables": {"x": {"type": "unknown"}},
"equations": [
{"lhs": {"op": "D", "args": ["x"], "wrt": "t"},
"rhs": {"op": "fn", "name": "interp.searchsorted", "args": [
"t", {"op": "const", "args": [], "value": [0.0, 1.0, 2.0]}]}}
]
}}
});
let compiled = compile(doc);
let (prog, report) = taped(compiled.build_tape(&HashSet::new()));
assert!(report.fallbacks.is_empty(), "{:?}", report.fallbacks);
assert_eq!(opcount(&prog, "Interp"), 1, "{report}");
assert_eq!(prog.interp_tables.len(), 1);
assert_eq!(opcount(&prog, "ConstArray"), 0, "{report}");
let params = HashMap::new();
let param_vec = compiled.debug_resolve_params(¶ms);
let mut dy = vec![0.0f64; 1];
run_reference(&prog, &compiled, &[0.0], ¶m_vec, 1.5, &mut dy);
assert_eq!(dy[0], 3.0);
run_reference(&prog, &compiled, &[0.0], ¶m_vec, 9.0, &mut dy);
assert_eq!(dy[0], 4.0);
}
const PAST_U16: usize = 70_000;
#[test]
fn ab_state_and_parameter_indices_past_u16() {
let n = PAST_U16;
let mut vars = serde_json::Map::new();
for k in 1..=n {
vars.insert(format!("u{k}"), json!({"type": "unknown", "default": 1.0}));
vars.insert(
format!("c{k}"),
json!({"type": "parameter", "default": 1.0 + k as f64 * 1e-3}),
);
}
let coeff = |k: usize| {
if k <= 3 || k > n - 3 {
json!(format!("c{}", n + 1 - k))
} else {
json!(0.1)
}
};
let eqs: Vec<serde_json::Value> = (1..=n)
.map(|k| {
let mut nb = Vec::new();
if k > 1 {
nb.push(json!(format!("u{}", k - 1)));
}
if k < n {
nb.push(json!(format!("u{}", k + 1)));
}
let sum = if nb.len() == 1 {
nb.pop().unwrap()
} else {
json!({"op": "+", "args": nb})
};
json!({
"lhs": {"op": "D", "args": [format!("u{k}")], "wrt": "t"},
"rhs": {"op": "*", "args": [coeff(k), {"op": "-", "args": [
sum, {"op": "*", "args": [2, format!("u{k}")]}]}]}
})
})
.collect();
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_wide_state_indices"},
"models": {"M": {"variables": vars, "equations": eqs}}
});
let prog = ab_check(doc, -1.0, 1.0);
assert_eq!(prog.state_vars.len(), n);
}
#[test]
fn ab_fused_box_past_u16_splits_into_groups() {
let n = 8;
let terms: Vec<serde_json::Value> = (1..=PAST_U16)
.map(|j| json!({"op": "*", "args": [j as f64 * 0.5, idx("u", json!("i"))]}))
.collect();
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_wide_fused_box"},
"models": {"M": {
"variables": {"u": {"type": "unknown", "shape": ["i"]}},
"equations": [d_eq("u", n, agg(n, json!({"op": "+", "args": terms})))]
}}
});
let prog = ab_check(doc, -1.0, 1.0);
assert!(
prog.fused.len() > 1,
"the box must be split across groups, got {}",
prog.fused.len()
);
let scalars: usize = prog.fused.iter().map(|f| f.scalars.len()).sum();
assert!(
scalars > GroupIx::MAX as usize,
"the fixture must put more scalar operands on the box than one group can index ({scalars})"
);
for f in &prog.fused {
let regs = f.n_regs as usize + f.n_load_regs as usize + f.n_splat_regs as usize;
assert!(
f.micro.len() < GroupIx::MAX as usize
&& f.scalars.len() < GroupIx::MAX as usize
&& regs < GroupIx::MAX as usize,
"a group outgrew its local indices"
);
}
}
#[test]
fn a_shaped_parameter_default_is_taped_from_its_data() {
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "shaped_param_data", "authors": ["tape tests"]},
"index_sets": {"x": {"kind": "interval", "size": 3}, "y": {"kind": "interval", "size": 2}},
"models": {"M": {
"variables": {
"u": {"type": "unknown", "units": "1", "default": 1.0, "shape": ["x", "y"]},
"w": {"type": "parameter", "units": "1", "shape": ["x", "y"],
"default": [[0.5, -1.25], [2.0, 3.5], [-0.75, 1e-3]]}
},
"equations": [{
"lhs": {"op": "faq", "args": [], "output_idx": ["i", "j"],
"expr": {"op": "D", "args": [{"op": "index", "args": ["u", "i", "j"]}], "wrt": "t"},
"ranges": {"i": [1, 3], "j": [1, 2]}},
"rhs": {"op": "faq", "args": [], "output_idx": ["i", "j"],
"ranges": {"i": [1, 3], "j": [1, 2]},
"expr": {"op": "*", "args": [
{"op": "index", "args": ["w", "i", "j"]},
{"op": "index", "args": ["u", "i", "j"]}]}}
}]
}}
});
let prog = ab_check(doc.clone(), -2.0, 2.0);
let payloads: Vec<&ConstArrayData> = prog.const_data.iter().collect();
let (shape, values): ([usize; 2], [f64; 6]) = if prog.col_major {
([2, 3], [0.5, 2.0, -0.75, -1.25, 3.5, 1e-3])
} else {
([3, 2], [0.5, -1.25, 2.0, 3.5, -0.75, 1e-3])
};
assert!(
payloads
.iter()
.any(|d| d.shape[..] == shape && d.values == values),
"the default's row-major data is a ConstArray payload: {payloads:?}"
);
let compiled = compile(doc);
let n = compiled.state_variable_names().len();
let params = HashMap::new();
let param_vec = compiled.debug_resolve_params(¶ms);
let state = seeded_state(n, 7, -2.0, 2.0);
let (dy_oracle, _) = compiled.debug_eval_rhs(&state, 0.0, ¶ms, true);
let mut scratch = compiled
.debug_new_scratch_taped()
.expect("the tape lowers every rule");
let mut dy = vec![0.0f64; n];
compiled.debug_eval_rhs_into(
&state,
0.0,
¶m_vec,
&mut dy,
&mut scratch,
&mut RhsStats::default(),
);
let bits = |v: &[f64]| v.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
assert_eq!(bits(&dy), bits(&dy_oracle));
}
fn quad(x0: f64, y0: f64, w: f64, h: f64) -> serde_json::Value {
json!([[x0, y0], [x0 + w, y0], [x0 + w, y0 + h], [x0, y0 + h]])
}
fn regrid_doc(
manifold: &str,
src: Vec<serde_json::Value>,
tgt: Vec<serde_json::Value>,
) -> serde_json::Value {
let (ns, nt) = (src.len() as i64, tgt.len() as i64);
json!({
"esm": "1.1.0",
"metadata": {"name": "tape_poly_area"},
"index_sets": {
"s": {"kind": "interval", "size": ns},
"t": {"kind": "interval", "size": nt},
"v": {"kind": "interval", "size": 4},
"c": {"kind": "interval", "size": 2}
},
"models": {"M": {
"variables": {
"src": {"type": "unknown", "shape": ["s", "v", "c"]},
"tgt": {"type": "unknown", "shape": ["t", "v", "c"]},
"A": {"type": "unknown", "shape": ["s", "t"]},
"G": {"type": "unknown", "shape": ["s"]},
"F": {"type": "unknown", "shape": ["t"]}
},
"equations": [
{"lhs": "src", "rhs": {"op": "const", "args": [], "value": src}},
{"lhs": "tgt", "rhs": {"op": "const", "args": [], "value": tgt}},
{"lhs": "A", "rhs": {"op": "faq", "args": [], "output_idx": ["i", "j"],
"ranges": {"i": {"from": "s"}, "j": {"from": "t"}},
"expr": {"op": "polygon_intersection_area", "manifold": manifold,
"args": [idx("src", json!("i")), idx("tgt", json!("j"))]}}},
{"lhs": {"op": "faq", "args": [], "output_idx": ["i"],
"expr": {"op": "D", "args": [idx("G", json!("i"))], "wrt": "t"},
"ranges": {"i": {"from": "s"}}},
"rhs": {"op": "faq", "args": [], "output_idx": ["i"],
"ranges": {"i": {"from": "s"}},
"expr": {"op": "-", "args": [idx("G", json!("i"))]}}},
{"lhs": {"op": "faq", "args": [], "output_idx": ["j"],
"expr": {"op": "D", "args": [idx("F", json!("j"))], "wrt": "t"},
"ranges": {"j": {"from": "t"}}},
"rhs": {"op": "faq", "args": [], "output_idx": ["j"],
"ranges": {"i": {"from": "s"}, "j": {"from": "t"}},
"expr": {"op": "-", "args": [
{"op": "*", "args": [
{"op": "index", "args": ["A", "i", "j"]},
idx("G", json!("i"))]},
{"op": "/", "args": [idx("F", json!("j")), ns]}]}}}
]
}}
})
}
fn only_poly_area(prog: &TapeProgram) -> &GeomSpec {
assert_eq!(opcount(prog, "PolyArea"), 1, "one geometry instruction");
assert_eq!(prog.geoms.len(), 1);
let at = prog
.instrs
.iter()
.position(|i| matches!(i, Instr::PolyArea { .. }))
.expect("PolyArea emitted");
assert_eq!(
prog.section_of(at),
Cadence::Const,
"clipped once per solve"
);
&prog.geoms[0]
}
#[test]
fn ab_poly_area_planar_broad_phase() {
let mut src: Vec<serde_json::Value> = (0..6).map(|k| quad(k as f64, 0.0, 1.0, 1.0)).collect();
src.push(json!([[6.0, 0.0], [7.0, 0.0], [7.0, 1.0], [7.0, 1.0]]));
src.push(json!([[8.0, 0.0], [8.0, 1.0], [9.0, 1.0], [9.0, 0.0]]));
src.push(json!([[10.0, 0.0], [11.0, 0.0], [null, 1.0], [10.0, 1.0]]));
let mut tgt: Vec<serde_json::Value> = (0..6)
.map(|k| quad(k as f64 + 0.5, 0.25, 1.0, 1.0))
.collect();
tgt.push(quad(3.0, 1.0, 1.0, 1.0)); tgt.push(quad(7.0, 1.0, 1.0, 1.0)); tgt.push(quad(40.0, 40.0, 1.0, 1.0)); let prog = ab_check(regrid_doc("planar", src, tgt), 0.5, 2.0);
let spec = only_poly_area(&prog);
assert_eq!(
spec.pairs,
Some((0, 1)),
"the pair axes drive the broad phase"
);
}
#[test]
fn ab_poly_area_planar_regular_grids() {
let src: Vec<serde_json::Value> = (0..5)
.flat_map(|i| (0..4).map(move |j| quad(i as f64, j as f64, 1.0, 1.0)))
.collect();
let tgt: Vec<serde_json::Value> = (0..3)
.flat_map(|i| (0..3).map(move |j| quad(1.7 * i as f64 - 0.3, 1.3 * j as f64, 1.7, 1.3)))
.collect();
let prog = ab_check(regrid_doc("planar", src, tgt), 0.5, 2.0);
assert!(only_poly_area(&prog).pairs.is_some());
}
#[test]
fn ab_poly_area_spherical_dense() {
let src: Vec<serde_json::Value> = (0..3)
.map(|k| quad(10.0 * k as f64, 0.0, 10.0, 10.0))
.collect();
let tgt: Vec<serde_json::Value> = (0..2)
.map(|k| quad(10.0 * k as f64 + 5.0, 5.0, 10.0, 10.0))
.collect();
let prog = ab_check(regrid_doc("spherical", src, tgt), 0.5, 2.0);
assert_eq!(only_poly_area(&prog).pairs, None);
}
#[test]
fn ab_regrid_bin_skolem_gate_drives_the_apply() {
let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../tests/conformance/scaling/fixtures/regrid/regrid_N100.esm");
let text = std::fs::read_to_string(&path).expect("scaling fixture reads");
let doc: serde_json::Value = serde_json::from_str(&text).expect("scaling fixture parses");
let prog = ab_check(doc, 0.5, 2.0);
let n = 100usize;
let cont = (prog.n_const + prog.n_segment) as usize;
let mut seg = 0;
for ins in &prog.instrs[cont..] {
let elems = match ins {
Instr::Fused { spec } => prog.fused[*spec as usize].shape.iter().product::<usize>(),
_ => ins.out().map_or(0, |o| {
prog.slots[o as usize].shape.iter().product::<usize>()
}),
};
assert!(
elems <= 2 * n,
"a per-call instruction spans {elems} elements: {ins:?}"
);
seg += usize::from(matches!(ins, Instr::SegReduce { .. }));
}
assert_eq!(seg, 1, "the apply is one gated segmented sum");
}
#[test]
fn ab_poly_area_wholesale_and_same_axis() {
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_poly_area_scalar"},
"index_sets": {"c": {"kind": "interval", "size": 3},
"v": {"kind": "interval", "size": 4},
"x": {"kind": "interval", "size": 2}},
"models": {"M": {
"variables": {
"a": {"type": "unknown", "shape": ["v", "x"]},
"b": {"type": "unknown", "shape": ["v", "x"]},
"poly": {"type": "unknown", "shape": ["c", "v", "x"]},
"whole": {"type": "unknown"},
"picked": {"type": "unknown"},
"self_area": {"type": "unknown", "shape": ["c"]},
"y": {"type": "unknown", "default": 1.0},
"u": {"type": "unknown", "shape": ["c"]}
},
"equations": [
{"lhs": "a", "rhs": {"op": "const", "args": [], "value": quad(0.0, 0.0, 2.0, 2.0)}},
{"lhs": "b", "rhs": {"op": "const", "args": [], "value": quad(1.0, 1.0, 2.0, 2.0)}},
{"lhs": "poly", "rhs": {"op": "const", "args": [], "value": [
quad(0.0, 0.0, 1.0, 1.0), quad(0.0, 0.0, 2.0, 3.0), quad(5.0, 5.0, 0.5, 0.25)]}},
{"lhs": "whole", "rhs": {"op": "polygon_intersection_area", "manifold": "planar",
"args": ["a", "b"]}},
{"lhs": "picked", "rhs": {"op": "polygon_intersection_area", "manifold": "planar",
"args": [{"op": "index", "args": ["poly", 2]}, "b"]}},
{"lhs": "self_area", "rhs": {"op": "faq", "args": [], "output_idx": ["i"],
"ranges": {"i": {"from": "c"}},
"expr": {"op": "polygon_intersection_area", "manifold": "planar",
"args": [idx("poly", json!("i")), idx("poly", json!("i"))]}}},
{"lhs": {"op": "D", "args": ["y"], "wrt": "t"},
"rhs": {"op": "*", "args": [-1.0, "whole", "picked", "y"]}},
d_eq("u", 3, agg(3, json!({"op": "*", "args": [
idx("self_area", json!("i")), idx("u", json!("i"))]})))
]
}}
});
let prog = ab_check(doc, 0.5, 2.0);
assert_eq!(opcount(&prog, "PolyArea"), 3);
assert!(prog.geoms.iter().all(|g| g.pairs.is_none()));
}
#[test]
fn ab_intersect_polygon_build_time_ring() {
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_intersect_polygon"},
"index_sets": {
"sv": {"kind": "interval", "size": 4},
"tv": {"kind": "interval", "size": 4},
"coord": {"kind": "interval", "size": 2},
"clip_ring": {"kind": "derived", "from_faq": "ov"}
},
"models": {"M": {
"variables": {
"src": {"type": "unknown", "shape": ["sv", "coord"]},
"tgt": {"type": "unknown", "shape": ["tv", "coord"]},
"clip": {"type": "unknown", "shape": ["clip_ring", "coord"]},
"area": {"type": "unknown"},
"y": {"type": "unknown", "default": 1.0}
},
"equations": [
{"lhs": "src", "rhs": {"op": "const", "args": [], "value": quad(0.0, 0.0, 2.0, 2.0)}},
{"lhs": "tgt", "rhs": {"op": "const", "args": [],
"value": [[1.0, 1.0], [3.0, 1.5], [2.5, 3.0], [0.5, 2.5]]}},
{"lhs": "clip", "rhs": {"op": "intersect_polygon", "id": "ov", "manifold": "planar",
"args": ["src", "tgt"]}},
{"lhs": "area", "rhs": {"op": "faq", "args": [], "output_idx": [],
"ranges": {"k": {"from": "clip_ring"}},
"expr": {"op": "*", "args": [0.5, {"op": "-", "args": [
{"op": "*", "args": [
{"op": "index", "args": ["clip", "k", 1]},
{"op": "index", "args": ["clip", {"op": "+", "args": ["k", 1]}, 2]}]},
{"op": "*", "args": [
{"op": "index", "args": ["clip", {"op": "+", "args": ["k", 1]}, 1]},
{"op": "index", "args": ["clip", "k", 2]}]}]}]}}},
{"lhs": {"op": "D", "args": ["y"], "wrt": "t"},
"rhs": {"op": "*", "args": [-1.0, "area", "y"]}}
]
}}
});
let prog = ab_check(doc, 0.5, 2.0);
assert_eq!(opcount(&prog, "PolyArea"), 0);
assert!(
prog.const_data
.iter()
.any(|d| d.shape[1] == 2 && d.shape[0] >= 4),
"the closed ring is a literal"
);
}
#[test]
fn ab_index_gather_unstructured() {
let n = 6;
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_index_gather"},
"index_sets": {"cells": {"kind": "interval", "size": n},
"nb": {"kind": "interval", "size": 3}},
"models": {"M": {
"variables": {
"u": {"type": "unknown", "shape": ["cells"], "default": 1.0},
"nbr": {"type": "unknown", "shape": ["cells", "nb"]},
"kappa": {"type": "parameter", "default": 0.3}
},
"equations": [
{"lhs": "nbr", "rhs": {"op": "const", "args": [], "value": [
[2, 6, 3], [1, 3, 0], [2.5, 4, 7], [3, 5, -1], [2.49, 6, 1], [5, 1, 4]]}},
{"lhs": {"op": "faq", "args": [], "output_idx": ["i"],
"expr": {"op": "D", "args": [idx("u", json!("i"))], "wrt": "t"},
"ranges": {"i": {"from": "cells"}}},
"rhs": {"op": "faq", "args": [], "output_idx": ["i"],
"ranges": {"i": {"from": "cells"}, "k": {"from": "nb"}},
"expr": {"op": "*", "args": ["kappa", {"op": "-", "args": [
{"op": "index", "args": ["u",
{"op": "index", "args": ["nbr", "i", "k"]}]},
idx("u", json!("i"))]}]}}}
]
}}
});
let prog = ab_check(doc, -1.0, 2.0);
assert_eq!(prog.index_gathers.len(), 1);
assert_eq!(opcount(&prog, "IndexGather"), 0, "folded away");
assert!(
prog.fused.iter().any(|f| f
.inputs
.iter()
.any(|i| i.index.is_some_and(|fi| fi.n == n as usize))),
"a fused group reads the source through the subscript"
);
let spec = &prog.index_gathers[0];
assert_eq!(spec.axes[..], [GatherAxis::Data]);
assert_eq!(&spec.shape[..], &[3, n as usize], "the promoted (k, i) box");
}
#[test]
fn ab_index_gather_mixed_axes() {
let doc = json!({
"esm": "1.1.0",
"metadata": {"name": "tape_index_gather_mixed"},
"index_sets": {"cells": {"kind": "interval", "size": 4},
"lev": {"kind": "interval", "size": 3}},
"models": {"M": {
"variables": {
"w": {"type": "unknown", "shape": ["cells", "lev"], "default": 1.0},
"nbr": {"type": "unknown", "shape": ["cells"]},
"pick": {"type": "parameter", "default": 3.0}
},
"equations": [
{"lhs": "nbr", "rhs": {"op": "const", "args": [], "value": [4, 1, 0, 2]}},
{"lhs": {"op": "faq", "args": [], "output_idx": ["i", "j"],
"expr": {"op": "D", "args": [{"op": "index", "args": ["w", "i", "j"]}],
"wrt": "t"},
"ranges": {"i": {"from": "cells"}, "j": {"from": "lev"}}},
"rhs": {"op": "faq", "args": [], "output_idx": ["i", "j"],
"ranges": {"i": {"from": "cells"}, "j": {"from": "lev"}},
"expr": {"op": "+", "args": [
{"op": "index", "args": ["w",
{"op": "index", "args": ["nbr", "i"]}, "j"]},
{"op": "index", "args": ["w", "pick", "j"]}]}}}
]
}}
});
let prog = ab_check(doc, -1.0, 2.0);
assert_eq!(opcount(&prog, "IndexGather"), 2);
}
#[test]
fn ab_float32_variables_beside_float64_neighbours() {
use crate::precision::Precision;
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../tests/fixtures/element_type/float32_state_float64_neighbour.esm");
let text = std::fs::read_to_string(&path).expect("fixture reads");
let doc: serde_json::Value = serde_json::from_str(&text).expect("fixture is JSON");
let file = typed(doc);
let env = crate::precision_infer::env_of_file(&file).expect("precision environment");
let annotated = crate::precision_infer::annotated(&file)
.expect("precision inference")
.expect("the fixture declares element types");
let _env = env.enter();
let compiled = ArrayCompiled::from_file(&annotated).expect("fixture compiles");
let (prog, report) = taped(compiled.build_tape_opts(&HashSet::new(), Some(default_cfg())));
assert!(report.fallbacks.is_empty(), "{:?}", report.fallbacks);
assert_eq!(prog.precision.len(), prog.instrs.len());
assert!(prog.precision.contains(&Precision::Float32));
assert!(prog.precision.contains(&Precision::Float64));
let n = compiled.state_variable_names().len();
let params = HashMap::new();
let param_vec = compiled.debug_resolve_params(¶ms);
let prog_uf = taped(compiled.build_tape_opts(&HashSet::new(), None)).0;
let mut fast = compiled
.debug_new_scratch_taped()
.expect("the tape lowers every rule");
assert!(fast.has_tape());
for seed in 0..4u64 {
let state = seeded_state(n, seed, -1.0, 1.0);
let (want, _) = compiled.debug_eval_rhs(&state, 0.0, ¶ms, false);
let (oracle, _) = compiled.debug_eval_rhs(&state, 0.0, ¶ms, true);
let bits = |v: &[f64]| v.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
assert_eq!(bits(&want), bits(&oracle), "seed {seed}: overlay vs oracle");
for (label, p) in [("fused", &prog), ("unfused", &prog_uf)] {
let mut dy = vec![0.0f64; n];
run_reference(p, &compiled, &state, ¶m_vec, 0.0, &mut dy);
assert_eq!(
bits(&dy),
bits(&want),
"seed {seed}: {label} reference executor"
);
}
let mut dy = vec![0.0f64; n];
compiled.debug_eval_rhs_into(
&state,
0.0,
¶m_vec,
&mut dy,
&mut fast,
&mut RhsStats::default(),
);
assert_eq!(bits(&dy), bits(&want), "seed {seed}: fast executor");
}
}
fn recurrence_doc(n: i64) -> serde_json::Value {
let w = json!({"op": "const", "args": [], "value": [0.0, 0.5, -0.25, 0.125]});
let body = json!({"op": "ifelse", "args": [
{"op": "==", "args": ["a", 0]},
{"op": "+", "args": [
{"op": "index", "args": ["u", "i"]},
{"op": "*", "args": [0.1, "j"]}
]},
{"op": "*", "args": [
{"op": "max", "args": [
{"op": "index", "args": ["r", "i", {"op": "-", "args": ["j", "a"]}]}, -1.0]},
{"op": "index", "args": [w, {"op": "+", "args": ["a", 1]}]}
]}
]});
let lagged = json!({"op": "ifelse", "args": [
{"op": "<=", "args": ["j", "L"]},
0.0,
{"op": "index", "args": ["r", "i", {"op": "-", "args": ["j", "L"]}]}
]});
json!({
"esm": "1.1.0",
"metadata": {"name": "tape_recurrence"},
"models": {"M": {
"variables": {
"L": {"type": "parameter", "default": 2},
"u": {"type": "unknown", "shape": ["i"]},
"r": {"type": "unknown", "shape": ["i", "j"]},
"q": {"type": "unknown", "shape": ["i", "j"]}
},
"equations": [
{"lhs": "r", "rhs": {"op": "faq", "args": [], "output_idx": ["i", "j"],
"reduce": "+",
"ranges": {"i": [1, 3], "j": [1, n], "a": [0, 3]},
"filter": {"op": "<=", "args": ["a", {"op": "-", "args": ["j", 1]}]},
"expr": body}},
{"lhs": "q", "rhs": {"op": "faq", "args": [], "output_idx": ["i", "j"],
"ranges": {"i": [1, 3], "j": [1, n]},
"expr": {"op": "+", "args": [
{"op": "index", "args": ["r", "i", "j"]}, lagged]}}},
{
"lhs": {"op": "faq", "args": [], "output_idx": ["i"],
"expr": {"op": "D", "args": [
{"op": "index", "args": ["u", "i"]}], "wrt": "t"},
"ranges": {"i": [1, 3]}},
"rhs": {"op": "faq", "args": [], "output_idx": ["i"],
"ranges": {"i": [1, 3]},
"expr": {"op": "*", "args": [-0.01,
{"op": "index", "args": ["q", "i", n]}]}}
}
]
}}
})
}
#[test]
fn ab_recurrence_sweep() {
let prog = ab_check(recurrence_doc(6), -2.0, 2.0);
assert_eq!(opcount(&prog, "Sweep"), 1);
assert!(opcount(&prog, "ScalarRead") > 0);
let body_len = |p: &TapeProgram| p.sweeps[0].body_len;
let small = taped(compile(recurrence_doc(6)).build_tape_opts(&HashSet::new(), None)).0;
let large = taped(compile(recurrence_doc(600)).build_tape_opts(&HashSet::new(), None)).0;
assert_eq!(body_len(&small), body_len(&large));
assert_eq!(small.instrs.len(), large.instrs.len());
}
fn doc_2d(vars: serde_json::Value, rhs_expr: serde_json::Value) -> serde_json::Value {
let mut variables = json!({"u": {"type": "unknown", "units": "1", "default": 0.5,
"shape": ["x", "y"]}});
for (k, v) in vars.as_object().expect("variables") {
variables[k] = v.clone();
}
json!({
"esm": "1.2.0",
"metadata": {"name": "lazy_shapes", "authors": ["tape tests"]},
"index_sets": {"x": {"kind": "interval", "size": 3}, "y": {"kind": "interval", "size": 2}},
"models": {"M": {
"variables": variables,
"equations": [{
"lhs": {"op": "faq", "args": [], "output_idx": ["i", "j"],
"expr": {"op": "D", "args": [{"op": "index", "args": ["u", "i", "j"]}],
"wrt": "t"},
"ranges": {"i": [1, 3], "j": [1, 2]}},
"rhs": {"op": "faq", "args": [], "output_idx": ["i", "j"],
"ranges": {"i": [1, 3], "j": [1, 2]}, "expr": rhs_expr}
}]
}}
})
}
fn u_at(i: serde_json::Value, j: serde_json::Value) -> serde_json::Value {
json!({"op": "index", "args": ["u", i, j]})
}
#[test]
fn a_rank_0_faq_nested_in_a_box_is_taped_per_cell_and_once() {
let per_cell = json!({"op": "faq", "args": [], "output_idx": [],
"ranges": {"k": [1, 2]},
"filter": {"op": "!=", "args": ["k", "j"]},
"expr": {"op": "*", "args": [u_at(json!("i"), json!("k")),
{"op": "index", "args": ["w", "k"]}]}});
let per_row = json!({"op": "faq", "args": [], "output_idx": [],
"ranges": {"k": [1, 2]}, "expr": u_at(json!("i"), json!("k"))});
let once = json!({"op": "faq", "args": [], "output_idx": [],
"ranges": {"a": [1, 3], "b": [1, 2]}, "expr": u_at(json!("a"), json!("b"))});
let doc = doc_2d(
json!({"w": {"type": "parameter", "units": "1", "shape": ["y"], "default": [0.25, -3.0]}}),
json!({"op": "+", "args": [
{"op": "*", "args": [per_cell, u_at(json!("i"), json!("j"))]},
{"op": "*", "args": [once, 1e-3]},
{"op": "*", "args": [per_row, u_at(json!("i"), json!("j")), 0.5]}]}),
);
let prog = ab_check(doc, -2.0, 2.0);
assert!(
prog.instrs
.iter()
.any(|i| matches!(i, Instr::Reduce { .. }))
|| prog.fused.iter().any(|f| f.reduce.is_some()),
"the reductions are folds, not unrolled terms"
);
}
#[test]
fn a_scalar_condition_joins_a_scalar_arm_and_an_array_arm() {
for flag in [0.0, 1.0] {
let doc = doc_2d(
json!({"flag": {"type": "parameter", "units": "1", "default": flag}}),
json!({"op": "ifelse", "args": [
{"op": ">", "args": ["flag", 0.5]}, -1.5, u_at(json!("i"), json!("j"))]}),
);
ab_check(doc, -2.0, 2.0);
}
}
fn guarded_doc() -> serde_json::Value {
let c = json!({"op": "const", "args": [], "value": [[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]]});
let read = |di: i64, dj: i64| {
json!({"op": "index", "args": [c.clone(),
{"op": "+", "args": ["i", di]}, {"op": "+", "args": ["j", dj]}]})
};
doc_2d(
json!({}),
json!({"op": "+", "args": [
{"op": "ifelse", "args": [{"op": ">", "args": [u_at(json!("i"), json!("j")), 0.0]},
read(0, 1), 0.0]},
{"op": "ifelse", "args": [{"op": "<", "args": [u_at(json!("i"), json!("j")), -1.0]},
read(-1, 0), 0.0]}]}),
)
}
#[test]
fn a_fault_behind_a_state_condition_fires_where_the_oracle_reaches_it() {
let compiled = compile(guarded_doc());
let n = compiled.state_variable_names().len();
let params = HashMap::new();
let param_vec = compiled.debug_resolve_params(¶ms);
let progs = [
taped(compiled.build_tape_opts(&HashSet::new(), Some(default_cfg()))).0,
taped(compiled.build_tape_opts(&HashSet::new(), None)).0,
taped(compiled.build_tape_layout(&HashSet::new(), Some(default_cfg()), false)).0,
];
assert!(
progs[0]
.instrs
.iter()
.any(|i| matches!(i, Instr::FaultScan { .. })),
"the guarded reads lower to a fault scan"
);
let mut states: Vec<Vec<f64>> = vec![
vec![-0.5; n],
vec![-0.5, -0.5, -0.5, -0.5, 0.5, -0.5],
vec![-2.0, -0.5, -0.5, -0.5, -0.5, -0.5],
vec![-2.0, -0.5, -0.5, 0.5, 0.5, 0.5],
vec![0.5, -0.5, -0.5, 0.5, -0.5, -0.5],
];
states.extend((0..6).map(|s| seeded_state(n, s, -2.0, 2.0)));
let mut fired = [false; 2];
for state in &states {
crate::simulate_array::take_const_array_oob();
let (dy_ref, _) = compiled.debug_eval_rhs(state, 0.0, ¶ms, true);
let want = crate::simulate_array::take_const_array_oob();
fired[usize::from(want.is_some())] = true;
for (k, prog) in progs.iter().enumerate() {
let mut dy = vec![0.0f64; n];
run_reference(prog, &compiled, state, ¶m_vec, 0.0, &mut dy);
assert_eq!(
crate::simulate_array::take_const_array_oob(),
want,
"program {k} ref"
);
let mut scratch = super::super::RhsScratch::new(&compiled.var_shapes);
scratch.install_tape(std::sync::Arc::new(match k {
0 => taped(compiled.build_tape_opts(&HashSet::new(), Some(default_cfg()))).0,
1 => taped(compiled.build_tape_opts(&HashSet::new(), None)).0,
_ => {
taped(compiled.build_tape_layout(&HashSet::new(), Some(default_cfg()), false)).0
}
}));
let mut dy_fast = vec![0.0f64; n];
compiled.debug_eval_rhs_into(
state,
0.0,
¶m_vec,
&mut dy_fast,
&mut scratch,
&mut RhsStats::default(),
);
assert_eq!(
crate::simulate_array::take_const_array_oob(),
want,
"program {k} fast"
);
let bits = |v: &[f64]| v.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
assert_eq!(bits(&dy_fast), bits(&dy_ref), "program {k}: dy");
assert_eq!(bits(&dy), bits(&dy_ref), "program {k}: reference dy");
}
}
assert_eq!(
fired,
[true, true],
"the states cover both a fault and none"
);
}
fn two_rule_doc(rhs_u: serde_json::Value, rhs_v: serde_json::Value) -> serde_json::Value {
let eq = |var: &str, rhs: serde_json::Value| {
json!({
"lhs": {"op": "faq", "args": [], "output_idx": ["i", "j"],
"expr": {"op": "D", "args": [{"op": "index", "args": [var, "i", "j"]}],
"wrt": "t"},
"ranges": {"i": [1, 3], "j": [1, 2]}},
"rhs": {"op": "faq", "args": [], "output_idx": ["i", "j"],
"ranges": {"i": [1, 3], "j": [1, 2]}, "expr": rhs}
})
};
let state = json!({"type": "unknown", "units": "1", "default": 0.5, "shape": ["x", "y"]});
let param = json!({"type": "parameter", "units": "1", "default": 2.0});
json!({
"esm": "1.2.0",
"metadata": {"name": "rule_faults", "authors": ["tape tests"]},
"index_sets": {"x": {"kind": "interval", "size": 3}, "y": {"kind": "interval", "size": 2}},
"models": {"M": {
"variables": {"u": state.clone(), "v": state,
"p": param.clone(), "q": param.clone(), "r": param},
"equations": [eq("u", rhs_u), eq("v", rhs_v)]
}}
})
}
fn on_scalar(name: &str) -> serde_json::Value {
json!({"op": "index", "args": [name, 1]})
}
fn after_half(arm: serde_json::Value) -> serde_json::Value {
json!({"op": "ifelse", "args": [{"op": ">", "args": ["t", 0.5]}, arm, 0.0]})
}
fn assert_fault_parity(doc: serde_json::Value, times: &[f64]) -> TapeProgram {
let compiled = compile(doc);
let n = compiled.state_variable_names().len();
let param_vec = compiled.debug_resolve_params(&HashMap::new());
let build = |k: usize| match k {
0 => taped(compiled.build_tape_opts(&HashSet::new(), Some(default_cfg()))).0,
1 => taped(compiled.build_tape_opts(&HashSet::new(), None)).0,
_ => taped(compiled.build_tape_layout(&HashSet::new(), Some(default_cfg()), false)).0,
};
let state = seeded_state(n, 7, -1.0, 1.0);
for &t in times {
crate::simulate_array::take_const_array_oob();
compiled.debug_eval_rhs(&state, t, &HashMap::new(), true);
let want = crate::simulate_array::take_const_array_oob();
assert!(want.is_some(), "the oracle faults at t = {t}");
for k in 0..3 {
let mut dy = vec![0.0f64; n];
run_reference(&build(k), &compiled, &state, ¶m_vec, t, &mut dy);
assert_eq!(
crate::simulate_array::take_const_array_oob(),
want,
"program {k} ref, t = {t}"
);
let mut scratch = super::super::RhsScratch::new(&compiled.var_shapes);
scratch.install_tape(std::sync::Arc::new(build(k)));
compiled.debug_eval_rhs_into(
&state,
t,
¶m_vec,
&mut dy,
&mut scratch,
&mut RhsStats::default(),
);
assert_eq!(
crate::simulate_array::take_const_array_oob(),
want,
"program {k} fast, t = {t}"
);
}
}
build(0)
}
#[test]
fn a_branch_fault_keeps_the_fault_tables_parallel() {
let prog = assert_fault_parity(
two_rule_doc(
json!({"op": "+", "args": [after_half(on_scalar("q")), u_at(json!("i"), json!("j"))]}),
on_scalar("r"),
),
&[0.0],
);
assert_eq!(prog.faults.len(), prog.fault_order.len());
assert!(
prog.instrs
.iter()
.any(|i| matches!(i, Instr::FaultScan { .. }))
);
}
fn c_row(row: serde_json::Value) -> serde_json::Value {
let c = json!({"op": "const", "args": [], "value": [[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]]});
json!({"op": "index", "args": [c, row, "j"]})
}
#[test]
fn an_earlier_rules_fault_scan_latches_before_a_later_rules_certain_fault() {
assert_fault_parity(
two_rule_doc(
json!({"op": "+", "args": [after_half(on_scalar("q")), on_scalar("p")]}),
on_scalar("r"),
),
&[0.0, 1.0],
);
}
#[test]
fn run_time_gather_faults_keep_the_rule_order() {
let past_end = json!({"op": "+", "args": [3,
{"op": ">", "args": [u_at(json!("i"), json!("j")), -5.0]}]});
assert_fault_parity(two_rule_doc(c_row(past_end), on_scalar("r")), &[0.0]);
assert_fault_parity(
two_rule_doc(
json!({"op": "+", "args": [after_half(on_scalar("q")), u_at(json!("i"), json!("j"))]}),
c_row(json!({"op": "+", "args": ["p", 2]})),
),
&[0.0, 1.0],
);
}
fn neg_zero_sum_doc(variant: &str) -> serde_json::Value {
let n = 5;
let u = |k: serde_json::Value| json!({"op": "index", "args": ["u", k]});
let term = json!({"op": "-", "args": [
{"op": "*", "args": [0.0, {"op": "exp", "args": [u(json!("j"))]}]}]});
let sum = |out: serde_json::Value, ranges: serde_json::Value, filter: Option<&str>| {
let mut f = json!({"op": "faq", "args": [], "output_idx": out, "reduce": "+",
"ranges": ranges, "expr": term});
if let Some(cmp) = filter {
f["filter"] = json!({"op": cmp, "args": ["j", "k"]});
}
f
};
let column = |filter| sum(json!(["k"]), json!({"k": [1, n], "j": [1, n]}), filter);
let mut variables = json!({"u": {"type": "unknown", "shape": ["k"]}});
let mut equations = Vec::new();
let rhs = match variant {
"contraction" => column(None),
"single" => sum(json!(["k"]), json!({"k": [1, n], "j": [2, 2]}), None),
"weighted" => {
let mut f = column(None);
f["expr"] = json!({"op": "*", "args": [f["expr"].clone(), "k"]});
f
}
"inclusive_scan" => column(Some("<=")),
"reverse_scan" => column(Some(">=")),
"observed" => {
variables["s"] = json!({"type": "unknown", "shape": ["k"]});
equations.push(json!({"lhs": "s", "rhs": column(None)}));
json!({"op": "index", "args": ["s", "k"]})
}
"rank0" => {
variables["s"] = json!({"type": "unknown"});
equations.push(json!({"lhs": "s",
"rhs": sum(json!([]), json!({"j": [1, n]}), None)}));
json!("s")
}
"makearray_scan_total" => {
variables["F"] = json!({"type": "unknown", "shape": ["kn"]});
let f = json!({"op": "makearray", "args": [],
"regions": [[[1, n]], [[n + 1, n + 1]]],
"values": [column(Some("<")), sum(json!([]), json!({"j": [1, n]}), None)]});
equations.push(json!({"lhs": "F", "rhs": f}));
json!({"op": "index", "args": ["F", {"op": "+", "args": ["k", 1]}]})
}
_ => unreachable!(),
};
let rhs = if rhs["op"] == "faq" {
rhs
} else {
json!({"op": "faq", "args": [], "output_idx": ["k"], "ranges": {"k": [1, n]},
"expr": rhs})
};
equations.push(json!({
"lhs": {"op": "faq", "args": [], "output_idx": ["k"],
"expr": {"op": "D", "args": [u(json!("k"))], "wrt": "t"},
"ranges": {"k": [1, n]}},
"rhs": rhs}));
json!({
"esm": "1.1.0",
"metadata": {"name": "tape_neg_zero_sum"},
"index_sets": {
"k": {"kind": "interval", "size": n},
"kn": {"kind": "interval", "size": n + 1}
},
"models": {"M": {"variables": variables, "equations": equations}}
})
}
#[test]
fn ab_an_all_negative_zero_sum_is_positive_zero() {
for variant in [
"contraction",
"single",
"weighted",
"observed",
"rank0",
"inclusive_scan",
"reverse_scan",
"makearray_scan_total",
] {
let doc = neg_zero_sum_doc(variant);
ab_check(doc.clone(), -2.0, 2.0);
let compiled = compile(doc);
let n = compiled.state_variable_names().len();
let state = seeded_state(n, 1, -2.0, 2.0);
let (dy, _) = compiled.debug_eval_rhs(&state, 0.0, &HashMap::new(), false);
for (k, v) in dy.iter().enumerate() {
assert_eq!(v.to_bits(), 0, "[{variant}] dy[{k}] = {v:?}, not +0.0");
}
}
}