use std::collections::{HashMap, HashSet};
use ndarray::{ArrayD, IxDyn};
use serde_json::{Map, Value};
use crate::broad_phase::OverlapIndex;
use crate::cadence::{self, Cadence};
use crate::faq::{ReduceKind, effective_reduce_kind};
use crate::relational::{self, Key, Num, SemiringOp, group_aggregate};
use crate::types::{Expr, IndexSet, Model};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValueInventionError(pub String);
impl std::fmt::Display for ValueInventionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "ValueInventionError: {}", self.0)
}
}
impl std::error::Error for ValueInventionError {}
fn err<T>(msg: impl Into<String>) -> Result<T, ValueInventionError> {
Err(ValueInventionError(msg.into()))
}
const VI_BODY_OPS: [&str; 3] = ["skolem", "rank", "distinct"];
const VI_ARGWITNESS_OPS: [&str; 2] = ["argmin", "argmax"];
fn is_argwitness(node: &Value) -> bool {
node.get("expr")
.and_then(|b| b.get("op"))
.and_then(|v| v.as_str())
.map(|o| VI_ARGWITNESS_OPS.contains(&o))
.unwrap_or(false)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BoundaryKind {
Periodic,
Clamp,
Error,
}
#[derive(Debug, Clone, Default)]
pub struct ValueInventionResult {
pub extents: HashMap<String, i64>,
pub members: HashMap<String, Vec<Key>>,
pub assignments: HashMap<String, Vec<i64>>,
pub groups: HashMap<String, Vec<f64>>,
pub vi_var_names: HashSet<String>,
pub map_codes: HashMap<String, Vec<f64>>,
}
#[derive(Debug, Clone, PartialEq)]
enum Val {
Bool(bool),
Int(i64),
Float(f64),
Str(String),
Key(Key),
}
impl Val {
fn as_f64(&self) -> Result<f64, ValueInventionError> {
match self {
Val::Int(i) => Ok(*i as f64),
Val::Float(f) => Ok(*f),
Val::Bool(b) => Ok(if *b { 1.0 } else { 0.0 }),
other => err(format!("value {other:?} is not numeric")),
}
}
fn key_int(&self) -> Result<i64, ValueInventionError> {
match self {
Val::Int(i) => Ok(*i),
Val::Bool(b) => Ok(*b as i64),
Val::Float(f) => {
if f.fract() == 0.0 && f.is_finite() {
Ok(*f as i64)
} else {
err(format!(
"value-invention key component {f} is not integer-valued; relational \
keys must be integer / categorical IDs (CONFORMANCE_SPEC.md §5.5.1 rule 1)"
))
}
}
other => err(format!("non-numeric key component {other:?}")),
}
}
}
fn obj(v: &Value) -> Option<&Map<String, Value>> {
v.as_object()
}
fn node_op(node: &Value) -> Option<&str> {
node.get("op").and_then(|v| v.as_str())
}
fn node_args(node: &Value) -> &[Value] {
node.get("args")
.and_then(|v| v.as_array())
.map(|a| a.as_slice())
.unwrap_or(&[])
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum NodeKind {
Producer,
Map,
Exclude,
None,
}
pub fn is_value_invention_assignment(rhs: &Value) -> bool {
vi_node_kind(rhs) != NodeKind::None
}
fn vi_node_kind(node: &Value) -> NodeKind {
let Some(map) = obj(node) else {
return NodeKind::None;
};
if map.get("op").and_then(|v| v.as_str()) != Some("faq") {
return NodeKind::None;
}
if map.get("distinct").and_then(|v| v.as_bool()) == Some(true) {
return NodeKind::Producer;
}
if let Some(bop) = map
.get("expr")
.and_then(|b| b.get("op"))
.and_then(|v| v.as_str())
{
if bop == "skolem" || VI_ARGWITNESS_OPS.contains(&bop) {
return NodeKind::Map;
}
if VI_BODY_OPS.contains(&bop) {
return NodeKind::Exclude;
}
}
if map
.get("key")
.and_then(|k| k.get("op"))
.and_then(|v| v.as_str())
== Some("skolem")
{
return NodeKind::Map;
}
NodeKind::None
}
fn lhs_base_raw(lhs: &Value) -> Option<String> {
if let Some(s) = lhs.as_str() {
return Some(s.to_string());
}
let op = node_op(lhs)?;
if op == "index" || op == "D" {
return node_args(lhs).first().and_then(lhs_base_raw);
}
None
}
fn lhs_base_typed(expr: &Expr) -> Option<String> {
match expr {
Expr::Variable(name) => Some(name.clone()),
Expr::Operator(node) if node.op == "index" || node.op == "D" => {
node.args.first().and_then(lhs_base_typed)
}
_ => None,
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum ChainKind {
Grouped,
Derived,
}
struct Detection {
has_vi: bool,
vi_var_names: HashSet<String>,
maps: Vec<(String, Value)>,
producers: Vec<(String, Value)>,
candidates: Vec<(String, Value)>,
chain: Vec<(String, Value, ChainKind)>,
}
fn vi_index_targets(node: &Value, out: &mut HashSet<String>) {
match node {
Value::Object(map) => {
if node_op(node) == Some("index")
&& let Some(name) = node_args(node).first().and_then(|v| v.as_str())
{
out.insert(name.to_string());
}
for v in map.values() {
vi_index_targets(v, out);
}
}
Value::Array(items) => {
for v in items {
vi_index_targets(v, out);
}
}
_ => {}
}
}
fn vi_join_partner(
join: &[Value],
gsym: &str,
mut is_known: impl FnMut(&str) -> bool,
) -> Option<String> {
for clause in join {
let Some(pairs) = clause.get("on").and_then(|v| v.as_array()) else {
continue;
};
for pair in pairs {
let Some(p) = pair.as_array() else { continue };
if p.len() != 2 {
continue;
}
let (Some(a), Some(b)) = (p[0].as_str(), p[1].as_str()) else {
continue;
};
if a == gsym && is_known(b) {
return Some(b.to_string());
}
if b == gsym && is_known(a) {
return Some(a.to_string());
}
}
}
None
}
fn vi_grouped_key(node: &Value, vi_var_names: &HashSet<String>) -> Option<String> {
if node_op(node) != Some("faq") {
return None;
}
let oi = node.get("output_idx").and_then(|v| v.as_array())?;
if oi.len() != 1 {
return None;
}
let gsym = oi[0].as_str()?;
let join = node.get("join").and_then(|v| v.as_array())?;
vi_join_partner(join, gsym, |n| vi_var_names.contains(n))
}
fn vi_is_derived(node: &Value, vi_var_names: &HashSet<String>) -> bool {
if node_op(node) != Some("faq") {
return false;
}
let Some(oi) = node.get("output_idx").and_then(|v| v.as_array()) else {
return false;
};
if oi.len() != 1 {
return false;
}
let Some(gsym) = oi[0].as_str() else {
return false;
};
if node.get("join").map(|v| !v.is_null()).unwrap_or(false) {
return false;
}
if let Some(ranges) = node.get("ranges").and_then(|v| v.as_object())
&& !ranges.keys().all(|k| k == gsym)
{
return false;
}
let mut refs = HashSet::new();
if let Some(expr) = node.get("expr") {
vi_index_targets(expr, &mut refs);
}
refs.iter().any(|r| vi_var_names.contains(r))
}
fn vi_detect(model_json: &Value) -> Detection {
let mut det = Detection {
has_vi: false,
vi_var_names: HashSet::new(),
maps: Vec::new(),
producers: Vec::new(),
candidates: Vec::new(),
chain: Vec::new(),
};
if let Some(eqs) = model_json.get("equations").and_then(|v| v.as_array()) {
for eq in eqs {
let (Some(lhs), Some(rhs)) = (eq.get("lhs"), eq.get("rhs")) else {
continue;
};
if let Some(base) = lhs_base_raw(lhs) {
classify_assignment(&base, rhs, &mut det);
}
}
}
if let Some(vars) = model_json.get("variables").and_then(|v| v.as_object()) {
for (vname, v) in vars {
if let Some(expr) = v.get("expression") {
classify_assignment(vname, expr, &mut det);
}
}
}
let mut candidates = std::mem::take(&mut det.candidates);
let mut changed = true;
while changed {
changed = false;
let mut rest = Vec::new();
for (base, node) in candidates.drain(..) {
if vi_grouped_key(&node, &det.vi_var_names).is_some() {
det.vi_var_names.insert(base.clone());
det.chain.push((base, node, ChainKind::Grouped));
changed = true;
} else if vi_is_derived(&node, &det.vi_var_names) {
det.vi_var_names.insert(base.clone());
det.chain.push((base, node, ChainKind::Derived));
changed = true;
} else {
rest.push((base, node));
}
}
candidates = rest;
}
det.has_vi = !det.maps.is_empty() || !det.producers.is_empty() || !det.chain.is_empty();
det
}
fn classify_assignment(base: &str, rhs: &Value, det: &mut Detection) {
let kind = vi_node_kind(rhs);
if kind == NodeKind::None {
if node_op(rhs) == Some("faq") {
det.candidates.push((base.to_string(), rhs.clone()));
}
return;
}
det.vi_var_names.insert(base.to_string()); match kind {
NodeKind::Producer => det.producers.push((base.to_string(), rhs.clone())),
NodeKind::Map => det.maps.push((base.to_string(), rhs.clone())),
_ => {}
}
}
struct ViCtx<'a> {
const_arrays: &'a HashMap<String, ArrayD<f64>>,
params: &'a HashMap<String, f64>,
index_sets: &'a Map<String, Value>,
variables: &'a Map<String, Value>,
const_array_boundaries: &'a HashMap<String, Vec<BoundaryKind>>,
maps: HashMap<String, ViBuf>,
}
impl<'a> ViCtx<'a> {
fn boundary(&self, name: &str, d: usize) -> BoundaryKind {
self.const_array_boundaries
.get(name)
.and_then(|dims| dims.get(d))
.copied()
.unwrap_or(BoundaryKind::Error)
}
fn param(&self, name: &str) -> Result<f64, ValueInventionError> {
if let Some(v) = self.params.get(name) {
return Ok(*v);
}
if let Some(d) = self
.variables
.get(name)
.and_then(|v| v.get("default"))
.and_then(|v| v.as_f64())
{
return Ok(d);
}
err(format!(
"value-invention scalar parameter {name:?} has no override or default"
))
}
}
type Bindings = HashMap<String, i64>;
type ViBuf = rustc_hash::FxHashMap<i64, Val>;
fn vi_eval(node: &Value, ctx: &ViCtx, bindings: &Bindings) -> Result<Val, ValueInventionError> {
match node {
Value::Bool(b) => Ok(Val::Bool(*b)),
Value::Number(n) => {
if let Some(i) = n.as_i64() {
Ok(Val::Int(i))
} else {
Ok(Val::Float(crate::precision::round(n.as_f64().unwrap())))
}
}
Value::String(s) => {
if let Some(v) = bindings.get(s) {
return Ok(Val::Int(*v)); }
if ctx.const_arrays.contains_key(s) {
return Ok(Val::Str(s.clone())); }
if ctx
.variables
.get(s)
.and_then(|v| v.get("type"))
.and_then(|v| v.as_str())
== Some("parameter")
{
return Ok(Val::Float(crate::precision::round(ctx.param(s)?)));
}
Ok(Val::Str(s.clone())) }
Value::Object(_) => vi_eval_op(node, ctx, bindings),
other => err(format!("unevaluable value-invention node {other}")),
}
}
fn vi_eval_op(node: &Value, ctx: &ViCtx, bindings: &Bindings) -> Result<Val, ValueInventionError> {
let op = node_op(node)
.ok_or_else(|| ValueInventionError("value-invention node has no `op`".into()))?;
let args = node_args(node);
let arg = |i: usize| -> Result<&Value, ValueInventionError> {
args.get(i).ok_or_else(|| {
ValueInventionError(format!(
"op {op:?} expects at least {} operand(s), found {}",
i + 1,
args.len()
))
})
};
let prec = crate::precision::active();
match op {
crate::precision_infer::MARKER_OP => {
let inner = arg(0)?;
match crate::precision_infer::marker_precision_json(node) {
Some(p) => {
let _guard = crate::precision::enter(p);
vi_eval(inner, ctx, bindings)
}
None => vi_eval(inner, ctx, bindings),
}
}
"index" => vi_index(node, ctx, bindings),
"skolem" => Ok(Val::Key(vi_skolem(node, ctx, bindings)?)),
"true" => Ok(Val::Bool(true)),
"false" => Ok(Val::Bool(false)),
"floor" => Ok(Val::Int(
vi_eval(arg(0)?, ctx, bindings)?.as_f64()?.floor() as i64
)),
"ceil" => Ok(Val::Int(
vi_eval(arg(0)?, ctx, bindings)?.as_f64()?.ceil() as i64
)),
"/" => {
let a = vi_eval(arg(0)?, ctx, bindings)?.as_f64()?;
let b = vi_eval(arg(1)?, ctx, bindings)?.as_f64()?;
Ok(Val::Float(prec.round(a / b)))
}
"*" => {
let mut acc = 1.0;
for a in args {
acc = prec.round(acc * vi_eval(a, ctx, bindings)?.as_f64()?);
}
Ok(Val::Float(acc))
}
"+" => {
let mut acc = 0.0;
for a in args {
acc = prec.round(acc + vi_eval(a, ctx, bindings)?.as_f64()?);
}
Ok(Val::Float(acc))
}
"-" => {
if args.len() == 1 {
Ok(Val::Float(-vi_eval(arg(0)?, ctx, bindings)?.as_f64()?))
} else {
let a = vi_eval(arg(0)?, ctx, bindings)?.as_f64()?;
let b = vi_eval(arg(1)?, ctx, bindings)?.as_f64()?;
Ok(Val::Float(prec.round(a - b)))
}
}
"<" | ">" | "<=" | ">=" | "==" | "!=" => {
let a = prec.round(vi_eval(arg(0)?, ctx, bindings)?.as_f64()?);
let b = prec.round(vi_eval(arg(1)?, ctx, bindings)?.as_f64()?);
Ok(Val::Bool(match op {
"<" => a < b,
">" => a > b,
"<=" => a <= b,
">=" => a >= b,
"==" => a == b,
_ => a != b,
}))
}
other => err(format!(
"value-invention build-time evaluator does not support op {other:?}"
)),
}
}
fn vi_index(node: &Value, ctx: &ViCtx, bindings: &Bindings) -> Result<Val, ValueInventionError> {
let args = node_args(node);
let name = args.first().and_then(|v| v.as_str());
let Some(name) = name else {
return err("value-invention index target must be a const-array factor name");
};
if let Some(buf) = ctx.maps.get(name) {
if args.len() != 2 {
return err(format!(
"materialised value-invention buffer {name:?} is a 1-D buffer; expected one index, \
got {}",
args.len() - 1
));
}
let idx = vi_eval(&args[1], ctx, bindings)?.key_int()?;
return buf.get(&idx).cloned().ok_or_else(|| {
ValueInventionError(format!(
"materialised value-invention buffer {name:?} has no entry at index {idx}"
))
});
}
let Some(arr) = ctx.const_arrays.get(name) else {
return err(format!(
"value-invention index target {name:?} must be a const-array factor \
or an already-materialised value-invention buffer"
));
};
let shape = arr.shape();
let mut idx = Vec::with_capacity(args.len() - 1);
for (d, a) in args[1..].iter().enumerate() {
let one_based = vi_eval(a, ctx, bindings)?.key_int()?;
let n = shape.get(d).copied().unwrap_or(0) as i64;
let resolved = resolve_const_index(ctx, name, d, one_based, n)?;
idx.push((resolved - 1) as usize);
}
if idx.len() != arr.ndim() {
return err(format!(
"value-invention index target {name:?} has rank {}, but {} index(es) were supplied",
arr.ndim(),
idx.len()
));
}
Ok(Val::Float(crate::precision::round(arr[IxDyn(&idx)])))
}
fn resolve_const_index(
ctx: &ViCtx,
name: &str,
d: usize,
one_based: i64,
n: i64,
) -> Result<i64, ValueInventionError> {
if (1..=n).contains(&one_based) {
return Ok(one_based);
}
if n >= 1 {
match ctx.boundary(name, d) {
BoundaryKind::Periodic => return Ok((one_based - 1).rem_euclid(n) + 1),
BoundaryKind::Clamp => return Ok(one_based.clamp(1, n)),
BoundaryKind::Error => {}
}
}
err(format!(
"const array '{name}' index {one_based} out of range 1..{n} in dim {d}"
))
}
fn vi_skolem(node: &Value, ctx: &ViCtx, bindings: &Bindings) -> Result<Key, ValueInventionError> {
let args = node_args(node);
let mut key_comps: Vec<Key> = Vec::with_capacity(args.len());
for a in args {
key_comps.push(Key::Int(vi_eval(a, ctx, bindings)?.key_int()?));
}
if key_comps.len() == 1 {
Ok(key_comps.into_iter().next().unwrap())
} else {
Ok(Key::Tuple(key_comps))
}
}
fn vi_order_syms(ranges: &Map<String, Value>) -> Result<Vec<String>, ValueInventionError> {
let syms: Vec<String> = ranges.keys().cloned().collect();
let mut ordered: Vec<String> = Vec::new();
let mut remaining = syms.clone();
while !remaining.is_empty() {
let mut progressed = false;
let mut i = 0;
while i < remaining.len() {
let s = &remaining[i];
let of: Vec<String> = ranges[s]
.get("of")
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|x| x.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let ready = of.iter().all(|p| ordered.contains(p) || !syms.contains(p));
if ready {
ordered.push(remaining.remove(i));
progressed = true;
} else {
i += 1;
}
}
if !progressed {
return err(format!(
"value-invention ranges have a cyclic `of` dependency: {remaining:?}"
));
}
}
Ok(ordered)
}
fn vi_range_values(
spec: &Value,
ctx: &ViCtx,
bindings: &Bindings,
) -> Result<Vec<i64>, ValueInventionError> {
let from = spec.get("from").and_then(|v| v.as_str());
let Some(from) = from else {
return err("value-invention range spec is missing `from`");
};
let Some(iset) = ctx.index_sets.get(from) else {
return err(format!(
"value-invention range references undeclared index set {from:?}"
));
};
let kind = iset.get("kind").and_then(|v| v.as_str());
match kind {
Some("interval") => {
let size = iset.get("size").and_then(|v| v.as_i64()).unwrap_or(0);
Ok((1..=size).collect())
}
Some("categorical") => {
let n = iset
.get("members")
.and_then(|v| v.as_array())
.map(|a| a.len())
.unwrap_or(0) as i64;
Ok((1..=n).collect())
}
Some("ragged") => {
let of: Vec<String> = spec
.get("of")
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|x| x.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let Some(parent_sym) = of.first() else {
return err(format!(
"ragged value-invention range {from:?} needs an `of` parent"
));
};
let parent = *bindings.get(parent_sym).ok_or_else(|| {
ValueInventionError(format!("ragged parent {parent_sym:?} is not bound"))
})?;
let offsets_name = iset.get("offsets").and_then(|v| v.as_str()).unwrap_or("");
let values_name = iset.get("values").and_then(|v| v.as_str()).unwrap_or("");
let offs = ctx.const_arrays.get(offsets_name).ok_or_else(|| {
ValueInventionError(format!(
"ragged offsets factor {offsets_name:?} not supplied"
))
})?;
let vals = ctx.const_arrays.get(values_name).ok_or_else(|| {
ValueInventionError(format!("ragged values factor {values_name:?} not supplied"))
})?;
if parent < 1 {
return err(format!(
"ragged value-invention range {from:?}: parent index {parent} is out of \
range (1-based parent must be >= 1)"
));
}
let prow = (parent - 1) as usize;
let offs_len = offs.shape().first().copied().unwrap_or(0);
if offs.ndim() != 1 || prow >= offs_len {
return err(format!(
"ragged value-invention range {from:?}: parent {parent} is outside offsets \
factor {offsets_name:?} (shape {:?})",
offs.shape()
));
}
let nmem = offs[IxDyn(&[prow])] as i64;
if nmem < 0 {
return err(format!(
"ragged value-invention range {from:?}: offsets factor {offsets_name:?} gives \
a negative member count {nmem} for parent {parent}"
));
}
let vshape = vals.shape();
if vals.ndim() != 2 || prow >= vshape[0] || nmem as usize > vshape[1] {
return err(format!(
"ragged value-invention range {from:?}: values factor {values_name:?} (shape \
{vshape:?}) is too small for parent {parent} member count {nmem}"
));
}
let mut out = Vec::with_capacity(nmem as usize);
for l in 1..=nmem {
let v = vals[IxDyn(&[prow, (l - 1) as usize])];
out.push(Val::Float(v).key_int()?);
}
Ok(out)
}
other => err(format!(
"value-invention range over index set kind {other:?} is unsupported"
)),
}
}
fn vi_enumerate<F>(
ranges: &Map<String, Value>,
ctx: &ViCtx,
mut visit: F,
) -> Result<(), ValueInventionError>
where
F: FnMut(&Bindings) -> Result<(), ValueInventionError>,
{
let syms = vi_order_syms(ranges)?;
let mut bindings: Bindings = HashMap::new();
vi_enumerate_rec(&syms, 0, ranges, ctx, &mut bindings, &mut visit)
}
fn vi_enumerate_rec<F>(
syms: &[String],
k: usize,
ranges: &Map<String, Value>,
ctx: &ViCtx,
bindings: &mut Bindings,
visit: &mut F,
) -> Result<(), ValueInventionError>
where
F: FnMut(&Bindings) -> Result<(), ValueInventionError>,
{
if k >= syms.len() {
return visit(bindings);
}
let s = &syms[k];
for v in vi_range_values(&ranges[s], ctx, bindings)? {
match bindings.get_mut(s) {
Some(slot) => *slot = v,
None => {
bindings.insert(s.clone(), v);
}
}
vi_enumerate_rec(syms, k + 1, ranges, ctx, bindings, visit)?;
}
bindings.remove(s);
Ok(())
}
fn vi_join_index_sym(
vname: &str,
producer_ranges: &Map<String, Value>,
ctx: &ViCtx,
) -> Result<String, ValueInventionError> {
let v = ctx.variables.get(vname).ok_or_else(|| {
ValueInventionError(format!("join references unknown variable {vname:?}"))
})?;
let shape: Vec<String> = v
.get("shape")
.and_then(|s| s.as_array())
.map(|a| {
a.iter()
.filter_map(|x| x.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
if shape.len() != 1 {
return err(format!(
"value-invention join key {vname:?} must be a 1-D buffer; shape={shape:?}"
));
}
let target = &shape[0];
for (sym, spec) in producer_ranges {
if spec.get("from").and_then(|v| v.as_str()) == Some(target.as_str()) {
return Ok(sym.clone());
}
}
err(format!(
"no producer range binds the index set {target:?} of join key {vname:?}"
))
}
enum ViJoinGate {
Equality {
sym_l: String,
sym_r: String,
lname: String,
rname: String,
},
Overlap {
sym_l: String,
sym_r: String,
index: OverlapIndex,
},
}
fn overlap_candidate_set(
src_env: &[String],
tgt_env: &[String],
ctx: &ViCtx,
eps: f64,
) -> Result<OverlapIndex, ValueInventionError> {
let src_envs = crate::broad_phase::envelope_vectors(src_env, ctx.const_arrays)
.map_err(ValueInventionError)?;
let tgt_envs = crate::broad_phase::envelope_vectors(tgt_env, ctx.const_arrays)
.map_err(ValueInventionError)?;
let pairs = crate::broad_phase::broad_phase_candidates(&src_envs, &tgt_envs, eps);
Ok(OverlapIndex::from_zero_based(&pairs))
}
fn overlap_env_names(ov: &Value, field: &str) -> Result<Vec<String>, ValueInventionError> {
let raw = ov.get(field).and_then(|v| v.as_array()).ok_or_else(|| {
ValueInventionError(format!(
"join `overlap` clause requires both `src_env` and `tgt_env` arrays of envelope \
factor names (Phase 2a spatial overlap gate); missing/invalid `{field}`"
))
})?;
let names: Vec<String> = raw
.iter()
.map(|x| x.as_str().map(String::from).unwrap_or_default())
.collect();
if !matches!(names.len(), 1 | 2 | 4) {
return err(format!(
"join `overlap` `{field}` must name 1 (rings), 2 (point [x,y]), or 4 \
(rect [xmin,ymin,xmax,ymax]) factors; got {}",
names.len()
));
}
Ok(names)
}
fn vi_resolve_join(
join: &[Value],
producer_ranges: &Map<String, Value>,
ctx: &ViCtx,
) -> Result<Vec<ViJoinGate>, ValueInventionError> {
let mut gates: Vec<ViJoinGate> = Vec::new();
for clause in join {
if let Some(ov) = clause.get("overlap") {
let src_env = overlap_env_names(ov, "src_env")?;
let tgt_env = overlap_env_names(ov, "tgt_env")?;
let eps = ov.get("eps").and_then(|v| v.as_f64()).unwrap_or(0.0);
let sym_l = vi_join_index_sym(&src_env[0], producer_ranges, ctx)?;
let sym_r = vi_join_index_sym(&tgt_env[0], producer_ranges, ctx)?;
let index = overlap_candidate_set(&src_env, &tgt_env, ctx, eps)?;
gates.push(ViJoinGate::Overlap {
sym_l,
sym_r,
index,
});
} else if let Some(on) = clause.get("on").and_then(|v| v.as_array()) {
for pair in on {
let cols = pair.as_array().filter(|c| c.len() == 2).ok_or_else(|| {
ValueInventionError("join.on entry must be a [left, right] pair".into())
})?;
let lname = cols[0].as_str().unwrap_or_default().to_string();
let rname = cols[1].as_str().unwrap_or_default().to_string();
let sym_l = vi_join_index_sym(&lname, producer_ranges, ctx)?;
let sym_r = vi_join_index_sym(&rname, producer_ranges, ctx)?;
gates.push(ViJoinGate::Equality {
sym_l,
sym_r,
lname,
rname,
});
}
}
}
Ok(gates)
}
fn vi_join_ok(
gates: &[ViJoinGate],
ctx: &ViCtx,
bindings: &Bindings,
) -> Result<bool, ValueInventionError> {
let unbound =
|sym: &str| ValueInventionError(format!("join index symbol {sym:?} is not bound"));
for g in gates {
match g {
ViJoinGate::Equality {
sym_l,
sym_r,
lname,
rname,
} => {
let missing_map = |name: &str| {
ValueInventionError(format!("join key {name:?} has no materialised map buffer"))
};
let lmap = ctx.maps.get(lname).ok_or_else(|| missing_map(lname))?;
let rmap = ctx.maps.get(rname).ok_or_else(|| missing_map(rname))?;
let lbind = bindings.get(sym_l).ok_or_else(|| unbound(sym_l))?;
let rbind = bindings.get(sym_r).ok_or_else(|| unbound(sym_r))?;
if lmap.get(lbind) != rmap.get(rbind) {
return Ok(false);
}
}
ViJoinGate::Overlap {
sym_l,
sym_r,
index,
} => {
let l = *bindings.get(sym_l).ok_or_else(|| unbound(sym_l))?;
let r = *bindings.get(sym_r).ok_or_else(|| unbound(sym_r))?;
if !index.contains(l, r) {
return Ok(false);
}
}
}
}
Ok(true)
}
fn vi_enumerate_join<F>(
ranges: &Map<String, Value>,
gates: &[ViJoinGate],
ctx: &ViCtx,
mut visit: F,
) -> Result<(), ValueInventionError>
where
F: FnMut(&Bindings) -> Result<(), ValueInventionError>,
{
let driver = gates.iter().find_map(|g| match g {
ViJoinGate::Overlap {
sym_l,
sym_r,
index,
} => Some((sym_l, sym_r, index)),
_ => None,
});
let Some((sym_l, sym_r, index)) = driver else {
return vi_enumerate(ranges, ctx, visit); };
let syms = vi_order_syms(ranges)?;
let free = |s: &String| syms.iter().any(|x| x == s);
if !(free(sym_l) && free(sym_r)) {
return vi_enumerate(ranges, ctx, visit);
}
let plan = crate::broad_phase::overlap_drive_plan(index, None, None, None);
if !matches!(plan, crate::broad_phase::DrivePlan::Pairs) {
return vi_enumerate(ranges, ctx, visit);
}
let pairs: Vec<(i64, i64)> = index.pairs().collect();
let rest: Vec<String> = syms
.into_iter()
.filter(|s| s != sym_l && s != sym_r)
.collect();
let mut bindings: Bindings = HashMap::new();
for (pl, pr) in pairs {
bindings.insert(sym_l.clone(), pl);
bindings.insert(sym_r.clone(), pr);
vi_enumerate_rec(&rest, 0, ranges, ctx, &mut bindings, &mut visit)?;
}
Ok(())
}
fn vi_argreduce(
node: &Value,
ctx: &ViCtx,
outer_bindings: &Bindings,
outer_ranges: &Map<String, Value>,
) -> Result<i64, ValueInventionError> {
let op = node_op(node).unwrap_or("");
let empty = Map::new();
let inner_ranges = node
.get("ranges")
.and_then(|v| v.as_object())
.unwrap_or(&empty);
let arg_sym = node.get("arg").and_then(|v| v.as_str()).ok_or_else(|| {
ValueInventionError(format!(
"arg-witness op {op:?} requires an `arg` naming the witnessing index symbol"
))
})?;
let value_expr = node.get("expr").ok_or_else(|| {
ValueInventionError(format!(
"arg-witness op {op:?} requires an `expr` body (the scalar to optimise)"
))
})?;
if !inner_ranges.contains_key(arg_sym) {
return err(format!(
"arg-witness `arg`={arg_sym:?} must name one of the contracted `ranges` symbols"
));
}
if outer_bindings.contains_key(arg_sym) {
return err(format!(
"arg-witness `arg`={arg_sym:?} shadows an outer index symbol"
));
}
let filt = node.get("filter");
let join: Vec<Value> = node
.get("join")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let mut combined: Map<String, Value> = outer_ranges.clone();
for (k, v) in inner_ranges {
combined.insert(k.clone(), v.clone());
}
let gates = if join.is_empty() {
Vec::new()
} else {
vi_resolve_join(&join, &combined, ctx)?
};
let syms = vi_order_syms(inner_ranges)?;
let mut bindings = outer_bindings.clone();
let mut best: Option<(f64, i64)> = None;
vi_enumerate_rec(&syms, 0, inner_ranges, ctx, &mut bindings, &mut |b| {
if let Some(f) = filt {
let pass = match vi_eval(f, ctx, b)? {
Val::Bool(x) => x,
Val::Int(i) => i > 0,
Val::Float(x) => x > 0.0,
_ => false,
};
if !pass {
return Ok(());
}
}
if !gates.is_empty() && !vi_join_ok(&gates, ctx, b)? {
return Ok(());
}
let v = vi_eval(value_expr, ctx, b)?.as_f64()?;
let a = b[arg_sym];
best = match best {
None => Some((v, a)),
Some((bv, ba)) => {
let better = if op == "argmax" { v > bv } else { v < bv };
if better || (v == bv && a < ba) {
Some((v, a))
} else {
Some((bv, ba))
}
}
};
Ok(())
})?;
match best {
Some((_, a)) => Ok(a),
None => err(format!(
"arg-witness op {op:?} has an empty candidate set; no index witnesses the \
optimum (a point with no candidate generator is undefined)"
)),
}
}
fn vi_materialize_map(
ctx: &mut ViCtx,
vname: &str,
node: &Value,
) -> Result<(), ValueInventionError> {
let output_idx: Vec<String> = node
.get("output_idx")
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|x| x.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
if output_idx.len() != 1 {
return err(format!(
"value-invention map {vname:?} must have a single output index; got {output_idx:?}"
));
}
let body = node.get("expr").ok_or_else(|| {
ValueInventionError(format!("value-invention map {vname:?} has no `expr` body"))
})?;
let empty = Map::new();
let ranges = node
.get("ranges")
.and_then(|v| v.as_object())
.unwrap_or(&empty);
let sym = output_idx[0].clone();
let is_arg = is_argwitness(node);
let mut out = ViBuf::default();
{
let ctx_ref = &*ctx;
let fast = if is_arg {
None
} else {
Fast::compile(body, ctx_ref, ranges)
};
vi_enumerate(ranges, ctx_ref, |bindings| {
let value = if is_arg {
Val::Int(vi_argreduce(body, ctx_ref, bindings, ranges)?)
} else if let Some(Ok(v)) = fast.as_ref().map(|f| f.eval(ctx_ref, bindings)) {
v
} else {
vi_eval(body, ctx_ref, bindings)?
};
out.insert(bindings[&sym], value);
Ok(())
})?;
}
ctx.maps.insert(vname.to_string(), out);
Ok(())
}
enum Fast<'a> {
Bool(bool),
Int(i64),
Float(f64),
Bound(&'a str),
Param(f64),
Index {
name: &'a str,
arr: &'a ArrayD<f64>,
subs: Vec<Fast<'a>>,
},
Skolem(Vec<Fast<'a>>),
Floor(Box<Fast<'a>>),
Ceil(Box<Fast<'a>>),
Div(Box<Fast<'a>>, Box<Fast<'a>>),
Mul(Vec<Fast<'a>>),
Add(Vec<Fast<'a>>),
Neg(Box<Fast<'a>>),
Sub(Box<Fast<'a>>, Box<Fast<'a>>),
}
impl<'a> Fast<'a> {
fn compile(node: &'a Value, ctx: &'a ViCtx, ranges: &Map<String, Value>) -> Option<Self> {
let sub = |n: &'a Value| Self::compile(n, ctx, ranges).map(Box::new);
let all = |ns: &'a [Value]| -> Option<Vec<Self>> {
ns.iter().map(|n| Self::compile(n, ctx, ranges)).collect()
};
match node {
Value::Bool(b) => Some(Fast::Bool(*b)),
Value::Number(n) => Some(match n.as_i64() {
Some(i) => Fast::Int(i),
None => Fast::Float(n.as_f64()?),
}),
Value::String(s) => {
if ranges.contains_key(s) {
return Some(Fast::Bound(s));
}
if ctx.const_arrays.contains_key(s) {
return None;
}
let is_param = ctx
.variables
.get(s)
.and_then(|v| v.get("type"))
.and_then(|v| v.as_str())
== Some("parameter");
if is_param {
ctx.param(s).ok().map(Fast::Param)
} else {
None
}
}
Value::Object(_) => {
let args = node_args(node);
let a = |i: usize| args.get(i);
Some(match node_op(node)? {
"index" => {
let name = args.first()?.as_str()?;
if ctx.maps.contains_key(name) {
return None;
}
let arr = ctx.const_arrays.get(name)?;
if args.len() - 1 != arr.ndim() {
return None;
}
Fast::Index {
name,
arr,
subs: all(&args[1..])?,
}
}
"skolem" => Fast::Skolem(all(args)?),
"true" => Fast::Bool(true),
"false" => Fast::Bool(false),
"floor" => Fast::Floor(sub(a(0)?)?),
"ceil" => Fast::Ceil(sub(a(0)?)?),
"/" => Fast::Div(sub(a(0)?)?, sub(a(1)?)?),
"*" => Fast::Mul(all(args)?),
"+" => Fast::Add(all(args)?),
"-" if args.len() == 1 => Fast::Neg(sub(a(0)?)?),
"-" => Fast::Sub(sub(a(0)?)?, sub(a(1)?)?),
_ => return None,
})
}
_ => None,
}
}
fn eval(&self, ctx: &ViCtx, bindings: &Bindings) -> Result<Val, ValueInventionError> {
let prec = crate::precision::active();
let f = |e: &Self| e.eval(ctx, bindings)?.as_f64();
Ok(match self {
Fast::Bool(b) => Val::Bool(*b),
Fast::Int(i) => Val::Int(*i),
Fast::Float(x) => Val::Float(crate::precision::round(*x)),
Fast::Bound(s) => match bindings.get(*s) {
Some(v) => Val::Int(*v),
None => return err(format!("range symbol {s:?} is not bound")),
},
Fast::Param(x) => Val::Float(crate::precision::round(*x)),
Fast::Index { name, arr, subs } => {
let shape = arr.shape();
let mut idx: smallvec::SmallVec<[usize; 4]> = smallvec::SmallVec::new();
for (d, e) in subs.iter().enumerate() {
let one_based = e.eval(ctx, bindings)?.key_int()?;
let n = shape[d] as i64;
idx.push((resolve_const_index(ctx, name, d, one_based, n)? - 1) as usize);
}
Val::Float(crate::precision::round(arr[IxDyn(&idx)]))
}
Fast::Skolem(args) => {
let mut comps: Vec<Key> = Vec::with_capacity(args.len());
for e in args {
comps.push(Key::Int(e.eval(ctx, bindings)?.key_int()?));
}
Val::Key(if comps.len() == 1 {
comps.pop().expect("one component")
} else {
Key::Tuple(comps)
})
}
Fast::Floor(e) => Val::Int(f(e)?.floor() as i64),
Fast::Ceil(e) => Val::Int(f(e)?.ceil() as i64),
Fast::Div(a, b) => {
let a = f(a)?;
let b = f(b)?;
Val::Float(prec.round(a / b))
}
Fast::Mul(args) => {
let mut acc = 1.0;
for e in args {
acc = prec.round(acc * f(e)?);
}
Val::Float(acc)
}
Fast::Add(args) => {
let mut acc = 0.0;
for e in args {
acc = prec.round(acc + f(e)?);
}
Val::Float(acc)
}
Fast::Neg(e) => Val::Float(-f(e)?),
Fast::Sub(a, b) => {
let a = f(a)?;
let b = f(b)?;
Val::Float(prec.round(a - b))
}
})
}
}
fn vi_materialize_producer(ctx: &ViCtx, node: &Value) -> Result<Vec<Key>, ValueInventionError> {
let key = node.get("key").ok_or_else(|| {
ValueInventionError("value-invention producer aggregate requires a `key` (§5.5)".into())
})?;
let empty = Map::new();
let ranges = node
.get("ranges")
.and_then(|v| v.as_object())
.unwrap_or(&empty);
let filt = node.get("filter");
let join: Vec<Value> = node
.get("join")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let gates = if join.is_empty() {
Vec::new()
} else {
vi_resolve_join(&join, ranges, ctx)?
};
let mut members: Vec<Key> = Vec::new();
vi_enumerate_join(ranges, &gates, ctx, |bindings| {
if let Some(f) = filt {
let fv = vi_eval(f, ctx, bindings)?;
let pass = match fv {
Val::Bool(b) => b,
Val::Int(i) => i > 0,
Val::Float(x) => x > 0.0,
_ => false,
};
if !pass {
return Ok(());
}
}
if !gates.is_empty() && !vi_join_ok(&gates, ctx, bindings)? {
return Ok(());
}
members.push(vi_skolem(key, ctx, bindings)?);
Ok(())
})?;
Ok(relational::distinct(&members))
}
fn vi_oplus(node: &Value) -> Result<(SemiringOp, f64), ValueInventionError> {
let semiring = node.get("semiring").and_then(|v| v.as_str());
let reduce = node.get("reduce").and_then(|v| v.as_str());
let rk = match effective_reduce_kind(semiring, reduce) {
Ok(rk) => rk,
Err(bad) => return err(format!("grouped value-invention reduction: {bad}")),
};
let op = match rk {
ReduceKind::Sum => SemiringOp::Sum,
ReduceKind::Product => SemiringOp::Prod,
ReduceKind::Max => SemiringOp::Max,
ReduceKind::Min => SemiringOp::Min,
ReduceKind::Or | ReduceKind::And => {
return err(
"grouped value-invention reduction ⊕ is index-set-producing (bool_and_or); \
expected a numeric semiring (+, *, max, min)",
);
}
};
Ok((op, rk.identity()))
}
fn vi_assert_buildtime(
ctx: &ViCtx,
vname: &str,
node: &Value,
vi_var_names: &HashSet<String>,
) -> Result<(), ValueInventionError> {
let mut refs = HashSet::new();
vi_index_targets(node, &mut refs);
for r in &refs {
if vi_var_names.contains(r) || ctx.const_arrays.contains_key(r) {
continue;
}
let is_state = ctx
.variables
.get(r)
.and_then(|v| v.get("type"))
.and_then(|v| v.as_str())
== Some("state");
if is_state {
return err(format!(
"grouped/derived value-invention buffer {vname:?} reads live state {r:?} — a \
build-time reduction's inputs must be CONST/DISCRETE factors or materialised \
buffers (RFC §5.7 guard 2)"
));
}
}
Ok(())
}
fn vi_materialize_grouped(
ctx: &mut ViCtx,
vname: &str,
node: &Value,
) -> Result<(), ValueInventionError> {
let oi = node
.get("output_idx")
.and_then(|v| v.as_array())
.filter(|a| a.len() == 1)
.ok_or_else(|| {
ValueInventionError(format!(
"grouped value-invention aggregate {vname:?} must have a single output index"
))
})?;
let gsym = oi[0]
.as_str()
.ok_or_else(|| ValueInventionError("grouped output index must be a string".into()))?
.to_string();
let empty = Map::new();
let ranges = node
.get("ranges")
.and_then(|v| v.as_object())
.unwrap_or(&empty);
if !ranges.contains_key(&gsym) {
return err(format!(
"grouped aggregate {vname:?} output index {gsym:?} is not among its ranges"
));
}
let body = node.get("expr").ok_or_else(|| {
ValueInventionError(format!("grouped aggregate {vname:?} has no expr body"))
})?;
let join = node.get("join").and_then(|v| v.as_array()).ok_or_else(|| {
ValueInventionError(format!(
"grouped aggregate {vname:?} needs a join pairing its group-key buffer with {gsym:?}"
))
})?;
let keyvar = vi_join_partner(join, &gsym, |n| ctx.maps.contains_key(n)).ok_or_else(|| {
ValueInventionError(format!(
"grouped aggregate {vname:?} join.on must pair a materialised value-invention buffer \
with the output index {gsym:?}"
))
})?;
let keysym = vi_join_index_sym(&keyvar, ranges, ctx)?;
let (op, zerobar) = vi_oplus(node)?;
let mut contract = Map::new();
for (s, spec) in ranges {
if s != &gsym {
contract.insert(s.clone(), spec.clone());
}
}
let gspec = ranges[&gsym].clone();
let (rows, out_vals) = {
let ctx_imm: &ViCtx = ctx;
let mut rows: Vec<(Key, Num)> = Vec::new();
vi_enumerate(&contract, ctx_imm, |bindings| {
let p = *bindings.get(&keysym).ok_or_else(|| {
ValueInventionError(format!("grouped key index symbol {keysym:?} is unbound"))
})?;
let kval = ctx_imm
.maps
.get(&keyvar)
.and_then(|m| m.get(&p))
.ok_or_else(|| {
ValueInventionError(format!(
"grouped key buffer {keyvar:?} has no entry at index {p}"
))
})?;
let key = Key::Int(kval.key_int()?);
let v = vi_eval(body, ctx_imm, bindings)?.as_f64()?;
rows.push((key, Num::Float(v)));
Ok(())
})?;
let out_vals = vi_range_values(&gspec, ctx_imm, &HashMap::new())?;
(rows, out_vals)
};
let agg = group_aggregate(&rows, op);
let mut agg_map: HashMap<i64, f64> = HashMap::new();
for (k, n) in agg {
if let Key::Int(i) = k {
agg_map.insert(
i,
match n {
Num::Float(f) => f,
Num::Int(x) => x as f64,
},
);
}
}
let mut out = ViBuf::default();
for g in out_vals {
out.insert(g, Val::Float(*agg_map.get(&g).unwrap_or(&zerobar)));
}
ctx.maps.insert(vname.to_string(), out);
Ok(())
}
fn vi_materialize_derived(
ctx: &mut ViCtx,
vname: &str,
node: &Value,
) -> Result<(), ValueInventionError> {
let oi = node
.get("output_idx")
.and_then(|v| v.as_array())
.filter(|a| a.len() == 1)
.ok_or_else(|| {
ValueInventionError(format!(
"derived value-invention aggregate {vname:?} must have a single output index"
))
})?;
let gsym = oi[0]
.as_str()
.ok_or_else(|| ValueInventionError("derived output index must be a string".into()))?
.to_string();
let empty = Map::new();
let ranges = node
.get("ranges")
.and_then(|v| v.as_object())
.unwrap_or(&empty);
let body = node.get("expr").ok_or_else(|| {
ValueInventionError(format!(
"derived value-invention aggregate {vname:?} has no expr body"
))
})?;
let out = {
let ctx_imm: &ViCtx = ctx;
let mut out = ViBuf::default();
vi_enumerate(ranges, ctx_imm, |bindings| {
let g = *bindings.get(&gsym).ok_or_else(|| {
ValueInventionError(format!("derived output index {gsym:?} is unbound"))
})?;
out.insert(g, Val::Float(vi_eval(body, ctx_imm, bindings)?.as_f64()?));
Ok(())
})?;
out
};
ctx.maps.insert(vname.to_string(), out);
Ok(())
}
fn vi_classification_model(
model_json: &Value,
maps: &[(String, Value)],
) -> Result<Value, ValueInventionError> {
if maps.is_empty() {
return Ok(model_json.clone());
}
let mut out = model_json.clone();
let variables = out.get_mut("variables").and_then(|v| v.as_object_mut());
let Some(variables) = variables else {
return Ok(out);
};
let mut retypes: Vec<(String, String)> = Vec::new();
for (vname, node) in maps {
if !variables.contains_key(vname) {
continue;
}
let Some(body) = node.get("expr") else {
continue;
};
let bcls = cadence::classify(body, model_json)
.map_err(|e| ValueInventionError(format!("cadence classify failed: {e}")))?;
let newtype = match bcls {
Cadence::Const => "parameter",
Cadence::Discrete => "discrete",
Cadence::Continuous => continue,
};
retypes.push((vname.clone(), newtype.to_string()));
}
for (vname, newtype) in retypes {
if let Some(v) = variables.get_mut(&vname).and_then(|v| v.as_object_mut()) {
v.insert("type".to_string(), Value::String(newtype));
}
}
Ok(out)
}
fn vi_variable_precision(vname: &str) -> Option<crate::precision::PrecisionGuard> {
if !crate::precision::has_variable_overrides() {
return None;
}
Some(crate::precision::enter(crate::precision::of_variable(
vname,
)))
}
pub fn materialize_value_invention(
model_json: &Value,
const_arrays: &HashMap<String, ArrayD<f64>>,
params: &HashMap<String, f64>,
const_array_boundaries: &HashMap<String, Vec<BoundaryKind>>,
) -> Result<ValueInventionResult, ValueInventionError> {
let det = vi_detect(model_json);
let mut result = ValueInventionResult {
vi_var_names: det.vi_var_names.clone(),
..Default::default()
};
if !det.has_vi {
return Ok(result);
}
let empty = Map::new();
let index_sets = model_json
.get("index_sets")
.and_then(|v| v.as_object())
.unwrap_or(&empty);
let variables = model_json
.get("variables")
.and_then(|v| v.as_object())
.unwrap_or(&empty);
let mut ctx = ViCtx {
const_arrays,
params,
index_sets,
variables,
const_array_boundaries,
maps: HashMap::new(),
};
let cls_model = vi_classification_model(model_json, &det.maps)?;
for (vname, node) in &det.maps {
let is_arg = is_argwitness(node);
if !is_arg {
continue;
}
let cls = cadence::classify(node, &cls_model)
.map_err(|e| ValueInventionError(format!("cadence classify failed: {e}")))?;
if cls == Cadence::Continuous {
return err(format!(
"arg-witness map {vname:?} classifies CONTINUOUS — a build-time assignment \
buffer's inputs must be CONST/DISCRETE (RFC §5.7 guard 2)"
));
}
}
for (vname, node) in &det.maps {
let _prec = vi_variable_precision(vname);
vi_materialize_map(&mut ctx, vname, node)?;
}
{
let mut per_map: Vec<(&String, Vec<Key>)> = Vec::new();
for (vname, node) in &det.maps {
if is_argwitness(node) {
continue;
}
let m = &ctx.maps[vname];
let mut idx: Vec<i64> = m.keys().copied().collect();
idx.sort_unstable();
let keys: Option<Vec<Key>> = idx
.iter()
.map(|k| match m.get(k) {
Some(Val::Key(key)) => Some(key.clone()),
Some(Val::Int(i)) => Some(Key::Int(*i)),
_ => None,
})
.collect();
if let Some(keys) = keys {
per_map.push((vname, keys));
}
}
let mut all: Vec<&Key> = per_map.iter().flat_map(|(_, ks)| ks.iter()).collect();
all.sort();
all.dedup();
let code: rustc_hash::FxHashMap<&Key, f64> = all
.into_iter()
.enumerate()
.map(|(i, k)| (k, (i + 1) as f64))
.collect();
for (vname, keys) in &per_map {
let buf = keys.iter().map(|k| code[k]).collect();
result.map_codes.insert((*vname).clone(), buf);
}
}
for (vname, node) in &det.maps {
let is_arg = is_argwitness(node);
if !is_arg {
continue;
}
let m = &ctx.maps[vname];
let mut keys: Vec<i64> = m.keys().copied().collect();
keys.sort_unstable();
let buf: Vec<i64> = keys
.iter()
.map(|k| match m.get(k) {
Some(Val::Int(i)) => *i,
_ => 0,
})
.collect();
result.assignments.insert(vname.clone(), buf);
}
for (vname, node, kind) in &det.chain {
vi_assert_buildtime(&ctx, vname, node, &det.vi_var_names)?;
let _prec = vi_variable_precision(vname);
match kind {
ChainKind::Grouped => vi_materialize_grouped(&mut ctx, vname, node)?,
ChainKind::Derived => vi_materialize_derived(&mut ctx, vname, node)?,
}
let m = &ctx.maps[vname];
let mut keys: Vec<i64> = m.keys().copied().collect();
keys.sort_unstable();
let buf: Vec<f64> = keys
.iter()
.map(|k| match m.get(k) {
Some(Val::Float(f)) => *f,
Some(Val::Int(i)) => *i as f64,
_ => 0.0,
})
.collect();
result.groups.insert(vname.clone(), buf);
}
let mut faq_to_set: HashMap<String, String> = HashMap::new();
for (sname, iset) in index_sets {
if iset.get("kind").and_then(|v| v.as_str()) != Some("derived") {
continue;
}
if let Some(faq) = iset.get("from_faq").and_then(|v| v.as_str()) {
faq_to_set.insert(faq.to_string(), sname.clone());
}
}
for (_, node) in &det.producers {
let node_id = node.get("id").and_then(|v| v.as_str()).ok_or_else(|| {
ValueInventionError(
"value-invention producer aggregate requires an `id` naming it for `from_faq`"
.into(),
)
})?;
if !faq_to_set.contains_key(node_id) {
continue; }
let cls = cadence::classify(node, &cls_model)
.map_err(|e| ValueInventionError(format!("cadence classify failed: {e}")))?;
if cls == Cadence::Continuous {
return err(format!(
"relational_node_in_continuous: value-invention producer {node_id:?} classifies \
CONTINUOUS — it may not run per step (RFC §5.7 guard 2); its inputs must be \
CONST/DISCRETE"
));
}
let mem = vi_materialize_producer(&ctx, node)?;
result.extents.insert(node_id.to_string(), mem.len() as i64);
result.members.insert(node_id.to_string(), mem);
}
Ok(result)
}
pub fn rewrite_derived_index_sets(
index_sets: &mut HashMap<String, IndexSet>,
extents: &HashMap<String, i64>,
) {
for iset in index_sets.values_mut() {
if iset.kind != "derived" {
continue;
}
let Some(faq) = &iset.from_faq else { continue };
if let Some(&n) = extents.get(faq) {
iset.kind = "interval".to_string();
iset.size = Some(n);
iset.from_faq = None;
}
}
}
pub fn drop_value_invention_equations(model: &mut Model, vi_var_names: &HashSet<String>) {
if vi_var_names.is_empty() {
return;
}
model.equations.retain(|eq| match lhs_base_typed(&eq.lhs) {
Some(base) => !vi_var_names.contains(&base),
None => true,
});
if let Some(init) = &mut model.initialization_equations {
init.retain(|eq| match lhs_base_typed(&eq.lhs) {
Some(base) => !vi_var_names.contains(&base),
None => true,
});
}
for name in vi_var_names {
model.variables.shift_remove(name);
}
}
pub fn apply_value_invention(
model: &mut Model,
index_sets: &mut HashMap<String, IndexSet>,
result: &ValueInventionResult,
) {
rewrite_derived_index_sets(index_sets, &result.extents);
drop_value_invention_equations(model, &result.vi_var_names);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::relational::canonical_index_set_json;
const EDGE_GOLDEN: &str = "[[1,2],[1,3],[2,3],[2,4],[3,4]]";
const CANDIDATE_GOLDEN: &str = "[[1,1],[2,2],[3,3]]";
const EDGE_FIXTURE: &str =
include_str!("../../../tests/valid/faq/edge_enumeration_area_eff.esm");
const REGRID_FIXTURE: &str =
include_str!("../../../tests/valid/geometry/conservative_regrid_overlap_join.esm");
fn model_json(fixture: &str, model_name: &str) -> Value {
let doc: Value = serde_json::from_str(fixture).expect("fixture parses");
let mut model = doc["models"][model_name].clone();
if let (Value::Object(m), Some(is)) = (&mut model, doc.get("index_sets")) {
m.entry("index_sets".to_string())
.or_insert_with(|| is.clone());
}
model
}
fn arr(shape: &[usize], data: Vec<f64>) -> ArrayD<f64> {
ArrayD::from_shape_vec(IxDyn(shape), data).expect("shape matches data")
}
fn ca(pairs: Vec<(&str, ArrayD<f64>)>) -> HashMap<String, ArrayD<f64>> {
pairs.into_iter().map(|(k, v)| (k.to_string(), v)).collect()
}
fn params(pairs: &[(&str, f64)]) -> HashMap<String, f64> {
pairs.iter().map(|(k, v)| (k.to_string(), *v)).collect()
}
fn no_bounds() -> HashMap<String, Vec<BoundaryKind>> {
HashMap::new()
}
fn bounds(pairs: Vec<(&str, Vec<BoundaryKind>)>) -> HashMap<String, Vec<BoundaryKind>> {
pairs.into_iter().map(|(k, v)| (k.to_string(), v)).collect()
}
fn sorted(set: &HashSet<String>) -> Vec<String> {
let mut v: Vec<String> = set.iter().cloned().collect();
v.sort();
v
}
fn k(i: i64, j: i64) -> Key {
Key::Tuple(vec![Key::Int(i), Key::Int(j)])
}
#[test]
fn skolem_label_is_documentary_and_typo_fails_closed() {
let const_arrays: HashMap<String, ArrayD<f64>> = HashMap::new();
let params: HashMap<String, f64> = HashMap::new();
let empty = Map::new();
let no_bnds = no_bounds();
let ctx = ViCtx {
const_arrays: &const_arrays,
params: ¶ms,
index_sets: &empty,
variables: &empty,
const_array_boundaries: &no_bnds,
maps: HashMap::new(),
};
let bindings: Bindings = [("a".to_string(), 1), ("b".to_string(), 2)]
.into_iter()
.collect();
let tagged = serde_json::json!({
"op": "skolem",
"label": "edge",
"args": ["a", "b"],
});
assert_eq!(vi_skolem(&tagged, &ctx, &bindings).unwrap(), k(1, 2));
let untagged = serde_json::json!({"op": "skolem", "args": ["a", "b"]});
assert_eq!(vi_skolem(&untagged, &ctx, &bindings).unwrap(), k(1, 2));
let typo = serde_json::json!({"op": "skolem", "args": ["aa", "b"]});
let e = vi_skolem(&typo, &ctx, &bindings).unwrap_err();
assert!(
e.0.contains("non-numeric key component"),
"unexpected error message: {}",
e.0
);
}
#[test]
fn edge_enumeration_materializes_to_m3_golden() {
let mj = model_json(EDGE_FIXTURE, "EdgeEnumerationAreaEff");
let const_arrays = ca(vec![
("n_verts_on_face", arr(&[2], vec![3.0, 3.0])),
(
"verts_on_face",
arr(&[2, 3], vec![1.0, 2.0, 3.0, 2.0, 3.0, 4.0]),
),
("n_edges_on_cell", arr(&[2], vec![3.0, 3.0])),
(
"edges_on_cell",
arr(&[2, 3], vec![1.0, 2.0, 3.0, 3.0, 4.0, 5.0]),
),
("dc", arr(&[5], vec![2.0, 3.0, 5.0, 7.0, 11.0])),
("dv", arr(&[5], vec![13.0, 17.0, 19.0, 23.0, 29.0])),
]);
let vi =
materialize_value_invention(&mj, &const_arrays, &HashMap::new(), &no_bounds()).unwrap();
assert_eq!(vi.extents["edge_set"], 5);
let edges = &vi.members["edge_set"];
assert_eq!(*edges, vec![k(1, 2), k(1, 3), k(2, 3), k(2, 4), k(3, 4)]);
assert_eq!(canonical_index_set_json(edges), EDGE_GOLDEN);
assert_eq!(
sorted(&vi.vi_var_names),
vec!["edge_dense_id", "edge_exists"]
);
}
#[test]
fn edge_enumeration_adversarial_inputs_collapse_to_golden() {
let mj = model_json(EDGE_FIXTURE, "EdgeEnumerationAreaEff");
let base = ca(vec![
("n_verts_on_face", arr(&[2], vec![3.0, 3.0])),
(
"verts_on_face",
arr(&[2, 3], vec![1.0, 2.0, 3.0, 2.0, 3.0, 4.0]),
),
]);
let rev = ca(vec![
("n_verts_on_face", arr(&[2], vec![3.0, 3.0])),
(
"verts_on_face",
arr(&[2, 3], vec![3.0, 2.0, 1.0, 4.0, 3.0, 2.0]),
),
]);
for const_arrays in [base, rev] {
let vi = materialize_value_invention(&mj, &const_arrays, &HashMap::new(), &no_bounds())
.unwrap();
assert_eq!(
canonical_index_set_json(&vi.members["edge_set"]),
EDGE_GOLDEN
);
}
}
#[test]
fn regridder_candidate_set_bin_skolem_equijoin() {
let mj = model_json(REGRID_FIXTURE, "ConservativeRegridOverlapJoin");
let p = params(&[("dx", 1.0), ("dy", 1.0), ("atol", 1e-12)]);
let aligned = ca(vec![
("src_lon", arr(&[3], vec![0.2, 1.2, 2.2])),
("src_lat", arr(&[3], vec![0.0, 0.0, 0.0])),
("tgt_lon", arr(&[3], vec![0.2, 1.2, 2.2])),
("tgt_lat", arr(&[3], vec![0.0, 0.0, 0.0])),
]);
let vi = materialize_value_invention(&mj, &aligned, &p, &no_bounds()).unwrap();
assert_eq!(vi.members["candidate_set"], vec![k(1, 1), k(2, 2), k(3, 3)]);
assert_eq!(vi.extents["candidate_set"], 3);
assert_eq!(
canonical_index_set_json(&vi.members["candidate_set"]),
CANDIDATE_GOLDEN
);
assert_eq!(
sorted(&vi.vi_var_names),
vec!["pair_exists", "src_bin", "tgt_bin"]
);
let shifted = ca(vec![
("src_lon", arr(&[3], vec![0.2, 1.2, 2.2])),
("src_lat", arr(&[3], vec![0.0, 0.0, 0.0])),
("tgt_lon", arr(&[3], vec![1.2, 2.2, 9.9])),
("tgt_lat", arr(&[3], vec![0.0, 0.0, 0.0])),
]);
let vi2 = materialize_value_invention(&mj, &shifted, &p, &no_bounds()).unwrap();
assert_eq!(vi2.members["candidate_set"], vec![k(2, 1), k(3, 2)]);
}
#[test]
fn resolver_resolves_derived_set_after_rewrite() {
let mj = model_json(EDGE_FIXTURE, "EdgeEnumerationAreaEff");
let const_arrays = ca(vec![
("n_verts_on_face", arr(&[2], vec![3.0, 3.0])),
(
"verts_on_face",
arr(&[2, 3], vec![1.0, 2.0, 3.0, 2.0, 3.0, 4.0]),
),
]);
let vi =
materialize_value_invention(&mj, &const_arrays, &HashMap::new(), &no_bounds()).unwrap();
let file = crate::parse::load_string(EDGE_FIXTURE).expect("fixture loads");
let mut registry: HashMap<String, IndexSet> = file
.index_sets
.clone()
.expect("fixture declares index_sets")
.into_iter()
.collect();
let model = file
.models
.as_ref()
.unwrap()
.get("EdgeEnumerationAreaEff")
.unwrap();
assert!(crate::faq::resolve_aggregate_ranges(&mut model.clone(), ®istry).is_err());
rewrite_derived_index_sets(&mut registry, &vi.extents);
let edges = registry.get("edges").unwrap();
assert_eq!(edges.kind, "interval");
assert_eq!(edges.size, Some(5));
assert!(crate::faq::resolve_aggregate_ranges(&mut model.clone(), ®istry).is_ok());
}
#[test]
fn continuous_relational_node_is_rejected() {
let model: Value = serde_json::json!({
"index_sets": {
"items": {"kind": "interval", "size": 2},
"tags": {"kind": "derived", "from_faq": "tag_set"}
},
"variables": {
"u": {"type": "unknown", "shape": ["items"]},
"tag": {"type": "unknown", "shape": ["tags"]}
},
"equations": [{
"lhs": {"op": "index", "args": ["tag", "p"]},
"rhs": {
"op": "faq", "id": "tag_set", "semiring": "bool_and_or",
"distinct": true, "output_idx": ["p"],
"ranges": {"i": {"from": "items"}},
"key": {"op": "skolem", "args": ["t", {"op": "index", "args": ["u", "i"]}]},
"expr": {"op": "true", "args": []}
}
}]
});
let const_arrays = ca(vec![("u", arr(&[2], vec![1.0, 2.0]))]);
assert!(
materialize_value_invention(&model, &const_arrays, &HashMap::new(), &no_bounds())
.is_err()
);
}
#[test]
fn no_op_for_plain_model() {
let plain: Value = serde_json::json!({
"variables": {"x": {"type": "unknown", "shape": []}},
"equations": [{"lhs": {"op": "D", "args": ["x"], "wrt": "t"}, "rhs": -1.0}]
});
let vi =
materialize_value_invention(&plain, &HashMap::new(), &HashMap::new(), &no_bounds())
.unwrap();
assert!(vi.extents.is_empty());
assert!(vi.vi_var_names.is_empty());
assert!(vi.assignments.is_empty());
}
const ARGMIN_FIXTURE: &str =
include_str!("../../../tests/valid/faq/nearest_generator_argmin.esm");
#[test]
fn argmin_nearest_generator_smallest_id_tiebreak() {
let mj = model_json(ARGMIN_FIXTURE, "NearestGeneratorArgmin");
let const_arrays = ca(vec![
("gx", arr(&[3], vec![0.0, 1.0, 2.0])),
("gy", arr(&[3], vec![0.0, 0.0, 0.0])),
("px", arr(&[4], vec![0.0, 1.0, 1.5, 2.0])),
("py", arr(&[4], vec![0.0, 0.5, 0.0, 0.0])),
]);
let vi =
materialize_value_invention(&mj, &const_arrays, &HashMap::new(), &no_bounds()).unwrap();
assert_eq!(vi.assignments["assign"], vec![1, 2, 2, 3]);
assert_eq!(sorted(&vi.vi_var_names), vec!["assign"]);
assert!(vi.extents.is_empty());
}
#[test]
fn argmin_binned_same_bin_candidate_join() {
let mj = model_json(ARGMIN_FIXTURE, "NearestGeneratorBinned");
let p = params(&[("binw", 1.0)]);
let const_arrays = ca(vec![
("gx", arr(&[3], vec![0.0, 1.0, 2.0])),
("gy", arr(&[3], vec![0.0, 0.0, 0.0])),
("px", arr(&[4], vec![0.1, 1.1, 2.1, 1.9])),
("py", arr(&[4], vec![0.0, 0.0, 0.0, 0.0])),
]);
let vi = materialize_value_invention(&mj, &const_arrays, &p, &no_bounds()).unwrap();
assert_eq!(vi.assignments["assign_binned"], vec![1, 2, 3, 2]);
assert_eq!(
sorted(&vi.vi_var_names),
vec!["assign_binned", "gen_bin", "point_bin"]
);
}
#[test]
fn argmax_farthest_generator_smallest_id_tiebreak() {
let model: Value = serde_json::json!({
"index_sets": {
"points": {"kind": "interval", "size": 2},
"generators": {"kind": "interval", "size": 3}
},
"variables": {
"gx": {"type": "parameter", "shape": ["generators"]},
"px": {"type": "parameter", "shape": ["points"]},
"far": {"type": "unknown", "shape": ["points"]}
},
"equations": [{
"lhs": {"op": "index", "args": ["far", "i"]},
"rhs": {"op": "faq", "output_idx": ["i"],
"ranges": {"i": {"from": "points"}},
"expr": {"op": "argmax", "arg": "g",
"ranges": {"g": {"from": "generators"}},
"expr": {"op": "*", "args": [
{"op": "-", "args": [{"op": "index", "args": ["px", "i"]}, {"op": "index", "args": ["gx", "g"]}]},
{"op": "-", "args": [{"op": "index", "args": ["px", "i"]}, {"op": "index", "args": ["gx", "g"]}]}
]}
}
}
}]
});
let const_arrays = ca(vec![
("gx", arr(&[3], vec![0.0, 1.0, 2.0])),
("px", arr(&[2], vec![0.0, 1.0])),
]);
let vi = materialize_value_invention(&model, &const_arrays, &HashMap::new(), &no_bounds())
.unwrap();
assert_eq!(vi.assignments["far"], vec![3, 1]);
}
#[test]
fn argmin_empty_candidate_set_is_error() {
let model: Value = serde_json::json!({
"index_sets": {
"points": {"kind": "interval", "size": 1},
"generators": {"kind": "interval", "size": 2}
},
"variables": {
"gx": {"type": "parameter", "shape": ["generators"]},
"px": {"type": "parameter", "shape": ["points"]},
"assign": {"type": "unknown", "shape": ["points"]}
},
"equations": [{
"lhs": {"op": "index", "args": ["assign", "i"]},
"rhs": {"op": "faq", "output_idx": ["i"],
"ranges": {"i": {"from": "points"}},
"expr": {"op": "argmin", "arg": "g",
"ranges": {"g": {"from": "generators"}},
"filter": {"op": "false", "args": []},
"expr": {"op": "*", "args": [
{"op": "index", "args": ["gx", "g"]}, {"op": "index", "args": ["gx", "g"]}]}
}
}
}]
});
let const_arrays = ca(vec![
("gx", arr(&[2], vec![0.0, 1.0])),
("px", arr(&[1], vec![0.5])),
]);
assert!(
materialize_value_invention(&model, &const_arrays, &HashMap::new(), &no_bounds())
.is_err()
);
}
#[test]
fn argmin_continuous_assignment_is_rejected() {
let model: Value = serde_json::json!({
"index_sets": {
"points": {"kind": "interval", "size": 1},
"generators": {"kind": "interval", "size": 2}
},
"variables": {
"gx": {"type": "unknown", "shape": ["generators"]},
"px": {"type": "parameter", "shape": ["points"]},
"assign": {"type": "unknown", "shape": ["points"]}
},
"equations": [{
"lhs": {"op": "index", "args": ["assign", "i"]},
"rhs": {"op": "faq", "output_idx": ["i"],
"ranges": {"i": {"from": "points"}},
"expr": {"op": "argmin", "arg": "g",
"ranges": {"g": {"from": "generators"}},
"expr": {"op": "*", "args": [
{"op": "index", "args": ["gx", "g"]}, {"op": "index", "args": ["gx", "g"]}]}
}
}
}]
});
let const_arrays = ca(vec![
("gx", arr(&[2], vec![0.0, 1.0])),
("px", arr(&[1], vec![0.5])),
]);
assert!(
materialize_value_invention(&model, &const_arrays, &HashMap::new(), &no_bounds())
.is_err()
);
}
fn gather_1d(
name: &str,
data: &ArrayD<f64>,
one_based: i64,
bnds: &HashMap<String, Vec<BoundaryKind>>,
) -> Result<f64, ValueInventionError> {
let const_arrays = ca(vec![(name, data.clone())]);
let params: HashMap<String, f64> = HashMap::new();
let empty = Map::new();
let ctx = ViCtx {
const_arrays: &const_arrays,
params: ¶ms,
index_sets: &empty,
variables: &empty,
const_array_boundaries: bnds,
maps: HashMap::new(),
};
let node = serde_json::json!({"op": "index", "args": [name, one_based]});
match vi_index(&node, &ctx, &HashMap::new())? {
Val::Float(f) => Ok(f),
other => panic!("expected Float gather, got {other:?}"),
}
}
#[test]
fn const_array_in_range_gather_unchanged() {
let m = arr(&[4], vec![10.0, 20.0, 30.0, 40.0]);
let clamp = bounds(vec![("M", vec![BoundaryKind::Clamp])]);
for (i, want) in [(1, 10.0), (2, 20.0), (3, 30.0), (4, 40.0)] {
assert_eq!(gather_1d("M", &m, i, &no_bounds()).unwrap(), want);
assert_eq!(gather_1d("M", &m, i, &clamp).unwrap(), want);
}
}
#[test]
fn const_array_clamp_edge_extends() {
let m = arr(&[4], vec![10.0, 20.0, 30.0, 40.0]);
let clamp = bounds(vec![("M", vec![BoundaryKind::Clamp])]);
assert_eq!(gather_1d("M", &m, 0, &clamp).unwrap(), 10.0); assert_eq!(gather_1d("M", &m, -3, &clamp).unwrap(), 10.0); assert_eq!(gather_1d("M", &m, 5, &clamp).unwrap(), 40.0); assert_eq!(gather_1d("M", &m, 99, &clamp).unwrap(), 40.0); }
#[test]
fn const_array_periodic_wraps() {
let m = arr(&[4], vec![10.0, 20.0, 30.0, 40.0]);
let per = bounds(vec![("M", vec![BoundaryKind::Periodic])]);
assert_eq!(gather_1d("M", &m, 5, &per).unwrap(), 10.0); assert_eq!(gather_1d("M", &m, 0, &per).unwrap(), 40.0); assert_eq!(gather_1d("M", &m, -1, &per).unwrap(), 30.0); assert_eq!(gather_1d("M", &m, 9, &per).unwrap(), 10.0); }
#[test]
fn const_array_no_policy_errors_not_panics() {
let m = arr(&[4], vec![10.0, 20.0, 30.0, 40.0]);
let err_pol = bounds(vec![("M", vec![BoundaryKind::Error])]);
for bnds in [&no_bounds(), &err_pol] {
let e = gather_1d("M", &m, 5, bnds).unwrap_err();
assert!(
e.0.contains("const array 'M' index 5 out of range 1..4 in dim 0"),
"unexpected error message: {}",
e.0
);
assert!(gather_1d("M", &m, 0, bnds).is_err());
}
}
#[test]
fn const_array_empty_dim_errors_under_any_policy() {
let m = ArrayD::from_shape_vec(IxDyn(&[0]), Vec::<f64>::new()).unwrap();
for kind in [
BoundaryKind::Periodic,
BoundaryKind::Clamp,
BoundaryKind::Error,
] {
let bnds = bounds(vec![("M", vec![kind])]);
assert!(gather_1d("M", &m, 1, &bnds).is_err());
}
}
#[test]
fn const_array_2d_mixed_policy_resolves_per_dim() {
let m = arr(&[2, 3], vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
let mixed = bounds(vec![(
"M",
vec![BoundaryKind::Clamp, BoundaryKind::Periodic],
)]);
let const_arrays = ca(vec![("M", m)]);
let params: HashMap<String, f64> = HashMap::new();
let empty = Map::new();
let ctx = ViCtx {
const_arrays: &const_arrays,
params: ¶ms,
index_sets: &empty,
variables: &empty,
const_array_boundaries: &mixed,
maps: HashMap::new(),
};
let gather = |i: i64, j: i64| -> Result<f64, ValueInventionError> {
let node = serde_json::json!({"op": "index", "args": ["M", i, j]});
match vi_index(&node, &ctx, &HashMap::new())? {
Val::Float(f) => Ok(f),
other => panic!("expected Float, got {other:?}"),
}
};
assert_eq!(gather(1, 1).unwrap(), 1.0);
assert_eq!(gather(2, 3).unwrap(), 6.0);
assert_eq!(gather(0, 1).unwrap(), 1.0); assert_eq!(gather(5, 2).unwrap(), 5.0); assert_eq!(gather(1, 4).unwrap(), 1.0); assert_eq!(gather(2, 0).unwrap(), 6.0); assert_eq!(gather(9, 4).unwrap(), 4.0); }
const CENTROID_FIXTURE: &str =
include_str!("../../../tests/valid/faq/nearest_generator_centroid.esm");
#[test]
fn centroid_group_aggregate_over_argmin_key() {
let mj = model_json(CENTROID_FIXTURE, "NearestGeneratorCentroid");
let const_arrays = ca(vec![
("gx", arr(&[3], vec![0.0, 1.0, 2.0])),
("px", arr(&[4], vec![0.0, 0.75, 1.25, 2.0])),
("rho", arr(&[4], vec![1.0, 1.0, 3.0, 4.0])),
]);
let vi =
materialize_value_invention(&mj, &const_arrays, &HashMap::new(), &no_bounds()).unwrap();
assert_eq!(vi.assignments["assign"], vec![1, 2, 2, 3]);
assert_eq!(vi.groups["num"], vec![0.0, 4.5, 8.0]);
assert_eq!(vi.groups["den"], vec![1.0, 4.0, 4.0]);
assert_eq!(vi.groups["centroid"], vec![0.0, 1.125, 2.0]);
assert_eq!(
sorted(&vi.vi_var_names),
vec!["assign", "centroid", "den", "num"]
);
}
#[test]
fn centroid_empty_group_folds_to_zerobar() {
let mj = model_json(CENTROID_FIXTURE, "NearestGeneratorCentroid");
let const_arrays = ca(vec![
("gx", arr(&[3], vec![0.0, 1.0, 2.0])),
("px", arr(&[4], vec![0.0, 0.1, 0.2, 0.3])),
("rho", arr(&[4], vec![2.0, 1.0, 1.0, 1.0])),
]);
let vi =
materialize_value_invention(&mj, &const_arrays, &HashMap::new(), &no_bounds()).unwrap();
assert_eq!(vi.assignments["assign"], vec![1, 1, 1, 1]);
assert_eq!(vi.groups["den"], vec![5.0, 0.0, 0.0]);
assert_eq!(vi.groups["num"][1], 0.0);
assert_eq!(vi.groups["num"][2], 0.0);
assert!(vi.groups["centroid"][1].is_nan());
assert!(vi.groups["centroid"][2].is_nan());
}
#[test]
fn regridder_aggregates_are_not_grouped_value_invention() {
let mj = model_json(REGRID_FIXTURE, "ConservativeRegridOverlapJoin");
let p = params(&[("dx", 1.0), ("dy", 1.0), ("atol", 1e-12)]);
let aligned = ca(vec![
("src_lon", arr(&[3], vec![0.2, 1.2, 2.2])),
("src_lat", arr(&[3], vec![0.0, 0.0, 0.0])),
("tgt_lon", arr(&[3], vec![0.2, 1.2, 2.2])),
("tgt_lat", arr(&[3], vec![0.0, 0.0, 0.0])),
]);
let vi = materialize_value_invention(&mj, &aligned, &p, &no_bounds()).unwrap();
assert!(
vi.groups.is_empty(),
"regridder must mint no grouped buffers"
);
assert!(!vi.vi_var_names.contains("A_j"));
assert!(!vi.vi_var_names.contains("mass_tgt"));
}
#[test]
fn centroid_grouped_reduction_reading_state_is_rejected() {
let model: Value = serde_json::json!({
"index_sets": {
"points": {"kind": "interval", "size": 2},
"generators": {"kind": "interval", "size": 1}
},
"variables": {
"gx": {"type": "parameter", "shape": ["generators"]},
"px": {"type": "parameter", "shape": ["points"]},
"rho": {"type": "unknown", "shape": ["points"]},
"assign": {"type": "unknown", "shape": ["points"]},
"num": {"type": "unknown", "shape": ["generators"]}
},
"equations": [
{"lhs": {"op": "index", "args": ["assign", "i"]},
"rhs": {"op": "faq", "output_idx": ["i"], "ranges": {"i": {"from": "points"}},
"args": ["px", "gx"],
"expr": {"op": "argmin", "args": ["px", "gx"], "arg": "g",
"ranges": {"g": {"from": "generators"}},
"expr": {"op": "*", "args": [
{"op": "-", "args": [{"op": "index", "args": ["px", "i"]}, {"op": "index", "args": ["gx", "g"]}]},
{"op": "-", "args": [{"op": "index", "args": ["px", "i"]}, {"op": "index", "args": ["gx", "g"]}]}]}}}},
{"lhs": {"op": "index", "args": ["num", "g"]},
"rhs": {"op": "faq", "output_idx": ["g"],
"ranges": {"g": {"from": "generators"}, "p": {"from": "points"}},
"semiring": "sum_product", "join": [{"on": [["assign", "g"]]}],
"args": ["assign", "rho"],
"expr": {"op": "index", "args": ["rho", "p"]}}}
]
});
let const_arrays = ca(vec![
("gx", arr(&[1], vec![0.0])),
("px", arr(&[2], vec![0.0, 1.0])),
]);
assert!(
materialize_value_invention(&model, &const_arrays, &HashMap::new(), &no_bounds())
.is_err()
);
}
}