use crate::simulate::SimulateError;
use crate::simulate_array::data_feed_source;
use crate::types::{EsmFile, ParameterUpdateSpec};
fn designates(key: &str, full: &str) -> bool {
full == key || full.ends_with(&format!(".{key}"))
}
fn pinned<'a>(p: impl IntoIterator<Item = &'a String>, full: &str) -> bool {
p.into_iter().any(|k| designates(k, full))
}
pub(crate) fn is_data_fed(spec: &ParameterUpdateSpec) -> bool {
data_feed_source(spec).is_some()
}
pub(crate) fn pin_data_fed_parameters<'a>(
file: &mut EsmFile,
p: impl IntoIterator<Item = &'a String> + Clone,
) -> Vec<String> {
let mut pinned_names = Vec::new();
if let Some(models) = file.models.as_mut() {
for (model_name, model) in models.iter_mut() {
for (var_name, var) in model.variables.iter_mut() {
let full = format!("{model_name}.{var_name}");
if var.update.as_ref().is_some_and(is_data_fed) && pinned(p.clone(), &full) {
var.update = None;
pinned_names.push(full);
}
}
if let Some(subs) = model.subsystems.as_mut() {
for (sub_name, sub) in subs.iter_mut() {
pin_in_json(
sub,
&format!("{model_name}.{sub_name}"),
p.clone(),
&mut pinned_names,
);
}
}
}
}
if let Some(systems) = file.reaction_systems.as_mut() {
for (sys_name, sys) in systems.iter_mut() {
for (par_name, par) in sys.parameters.iter_mut() {
let full = format!("{sys_name}.{par_name}");
if par.update.as_ref().is_some_and(is_data_fed) && pinned(p.clone(), &full) {
par.update = None;
pinned_names.push(full);
}
}
if let Some(subs) = sys.subsystems.as_mut() {
for (sub_name, sub) in subs.iter_mut() {
pin_in_json(
sub,
&format!("{sys_name}.{sub_name}"),
p.clone(),
&mut pinned_names,
);
}
}
}
}
pinned_names
}
fn pin_in_json<'a>(
node: &mut serde_json::Value,
prefix: &str,
p: impl IntoIterator<Item = &'a String> + Clone,
out: &mut Vec<String>,
) {
let Some(obj) = node.as_object_mut() else {
return;
};
if let Some(vars) = obj.get_mut("variables").and_then(|v| v.as_object_mut()) {
for (var_name, var) in vars.iter_mut() {
let full = format!("{prefix}.{var_name}");
let Some(var_obj) = var.as_object_mut() else {
continue;
};
if json_update_is_data_fed(var_obj.get("update")) && pinned(p.clone(), &full) {
var_obj.remove("update");
out.push(full);
}
}
}
if let Some(pars) = obj.get_mut("parameters").and_then(|v| v.as_object_mut()) {
for (par_name, par) in pars.iter_mut() {
let full = format!("{prefix}.{par_name}");
let Some(par_obj) = par.as_object_mut() else {
continue;
};
if json_update_is_data_fed(par_obj.get("update")) && pinned(p.clone(), &full) {
par_obj.remove("update");
out.push(full);
}
}
}
if let Some(subs) = obj.get_mut("subsystems").and_then(|v| v.as_object_mut()) {
for (sub_name, sub) in subs.iter_mut() {
pin_in_json(sub, &format!("{prefix}.{sub_name}"), p.clone(), out);
}
}
}
fn json_update_is_data_fed(update: Option<&serde_json::Value>) -> bool {
fn one(rule: &serde_json::Value) -> bool {
rule.get("kind").and_then(|k| k.as_str()) == Some("data") && rule.get("from").is_some()
}
match update {
Some(serde_json::Value::Array(rules)) => rules.iter().any(one),
Some(rule) => one(rule),
None => false,
}
}
pub(crate) fn refuse_unbound<'a>(
compiled: &crate::simulate_array::ArrayCompiled,
const_arrays: &std::collections::HashMap<String, ndarray::ArrayD<f64>>,
discrete_forcing: &std::collections::HashSet<String>,
pins: impl IntoIterator<Item = &'a String> + Clone,
) -> Result<(), SimulateError> {
if compiled.data_fed().is_empty() {
return Ok(());
}
let forcing = compiled.forcing_handle();
let bound = forcing.borrow();
for (name, source) in compiled.data_fed() {
if pinned(pins.clone(), name) {
continue;
}
let bare = compiled
.namespace()
.and_then(|ns| name.strip_prefix(ns))
.and_then(|rest| rest.strip_prefix('.'));
let known = |k: &str| k == name || bare == Some(k);
if bound.keys().any(|k| known(k))
|| discrete_forcing.iter().any(|k| known(k))
|| const_arrays.keys().any(|k| known(k))
{
continue;
}
return Err(SimulateError::DataSourceUnbound {
parameter: name.clone(),
data_source: source.clone(),
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_dotted_suffix_designates_the_parameter_and_a_partial_segment_does_not() {
assert!(designates("Forcing.k", "Forcing.k"));
assert!(designates("k", "Forcing.k"));
assert!(designates("Sub.k", "M.Sub.k"));
assert!(!designates("orcing.k", "Forcing.k"));
assert!(!designates("kk", "Forcing.k"));
assert!(!designates("Forcing", "Forcing.k"));
}
#[test]
fn the_json_reading_accepts_both_update_forms_and_rejects_a_non_data_rule() {
let data =
serde_json::json!({"kind": "data", "source": "S", "from": {"file_variable": "v"}});
assert!(json_update_is_data_fed(Some(&data)));
assert!(json_update_is_data_fed(Some(&serde_json::json!([
data.clone()
]))));
assert!(!json_update_is_data_fed(Some(
&serde_json::json!({"kind": "data", "source": "S"})
)));
assert!(!json_update_is_data_fed(Some(
&serde_json::json!({"kind": "schedule", "interval": 1.0, "expression": "x"})
)));
assert!(!json_update_is_data_fed(None));
}
}