use std::collections::HashMap;
use std::fs;
use std::path::Path;
use std::process::ExitCode;
use earthsci_ast::adapter_support::write_report;
use earthsci_ast::flatten;
use earthsci_ast::simulate_array::ArrayCompiled;
use earthsci_ast::{
Alg, Compiler, ProblemOptions, Rhs, SolveOptions, esm_problem, load_string, solve,
};
use ndarray::{ArrayD, IxDyn};
use serde_json::{Map, Value, json};
fn bare(name: &str) -> &str {
name.split_once('.').map(|x| x.1).unwrap_or(name)
}
fn time_key(t: f64) -> String {
format!("{t}")
}
fn nearest_index(times: &[f64], t: f64) -> Option<usize> {
(0..times.len()).min_by(|&a, &b| (times[a] - t).abs().total_cmp(&(times[b] - t).abs()))
}
fn solver_from(name: &str) -> Alg {
match name {
"Bdf" | "bdf" => Alg::Bdf,
"Sdirk" | "sdirk" => Alg::Sdirk,
_ => Alg::Erk,
}
}
fn state_vec(names: &[String], state: &Map<String, Value>) -> Vec<f64> {
names
.iter()
.map(|n| state.get(n).and_then(Value::as_f64).unwrap_or(0.0))
.collect()
}
const TIER_COMPILER: Compiler = Compiler::Native;
fn parse_args() -> Result<(std::path::PathBuf, std::path::PathBuf, Compiler), String> {
let mut manifest: Option<std::path::PathBuf> = None;
let mut output: Option<std::path::PathBuf> = None;
let mut compiler = TIER_COMPILER;
let mut args = std::env::args().skip(1);
while let Some(arg) = args.next() {
match arg.as_str() {
"--manifest" => manifest = args.next().map(std::path::PathBuf::from),
"--output" => output = args.next().map(std::path::PathBuf::from),
"--compiler" => {
let v = args.next().ok_or("--compiler needs a value")?;
compiler = Compiler::parse_named(&v)?;
}
other => return Err(format!("unexpected argument {other:?}")),
}
}
match (manifest, output) {
(Some(m), Some(o)) => Ok((m, o, compiler)),
_ => Err("--manifest and --output are required".to_string()),
}
}
fn run_fixture(
fx: &Value,
base: &Path,
integ: &Value,
compiler: Compiler,
) -> Result<Value, String> {
let rel = fx["path"].as_str().ok_or("fixture.path missing")?;
let json_str = fs::read_to_string(base.join(rel)).map_err(|e| e.to_string())?;
let file = load_string(&json_str).map_err(|e| format!("load: {e:?}"))?;
let params: HashMap<String, f64> = HashMap::new();
let compiled = ArrayCompiled::from_file(&file).map_err(|e| format!("compile: {e:?}"))?;
let names: Vec<String> = compiled.state_variable_names().to_vec();
let mut rhs = Map::new();
for probe in fx["rhs_probes"].as_array().ok_or("rhs_probes not array")? {
let pid = probe["id"].as_str().ok_or("probe.id missing")?;
let state_obj = probe["state"].as_object().ok_or("probe.state missing")?;
let t = probe["t"].as_f64().unwrap_or(0.0);
let sv = state_vec(&names, state_obj);
let (dy, _stats) = compiled.debug_eval_rhs(&sv, t, ¶ms, false);
let mut m = Map::new();
for (i, n) in names.iter().enumerate() {
m.insert(bare(n).to_string(), json!(dy[i]));
}
rhs.insert(pid.to_string(), Value::Object(m));
}
let tr = &fx["trajectory"];
let t0 = tr["time_span"]["start"].as_f64().ok_or("time_span.start")?;
let t1 = tr["time_span"]["end"].as_f64().ok_or("time_span.end")?;
let ics: HashMap<String, f64> = tr["initial_conditions"]
.as_object()
.ok_or("initial_conditions missing")?
.iter()
.map(|(k, v)| (k.clone(), v.as_f64().unwrap_or(0.0)))
.collect();
let out_times: Vec<f64> = tr["output_times"]
.as_array()
.ok_or("output_times missing")?
.iter()
.map(|v| v.as_f64().unwrap_or(0.0))
.collect();
let opts = SolveOptions {
alg: solver_from(integ["solver"].as_str().unwrap_or("Erk")),
abstol: Some(integ["abstol"].as_f64().unwrap_or(1e-12)),
reltol: Some(integ["reltol"].as_f64().unwrap_or(1e-10)),
maxiters: Some(1_000_000),
saveat: Some(out_times.clone()),
..Default::default()
};
let prob = esm_problem(
&file,
(t0, t1),
ProblemOptions {
p: params,
u0: ics,
rhs: Rhs::Always,
compiler: Some(compiler),
..Default::default()
},
)
.map_err(|e| format!("esm_problem: {e:?}"))?;
let sol = solve(&prob, &opts).map_err(|e| format!("solve: {e:?}"))?;
let mut traj = Map::new();
for &t in &out_times {
let idx = nearest_index(&sol.time, t).ok_or("empty solution time grid")?;
let mut m = Map::new();
for (row, n) in sol.state_variable_names.iter().enumerate() {
m.insert(bare(n).to_string(), json!(sol.state[row][idx]));
}
traj.insert(time_key(t), Value::Object(m));
}
Ok(json!({ "rhs": Value::Object(rhs), "trajectory": Value::Object(traj) }))
}
fn state_vec_bare(names: &[String], state: &Map<String, Value>) -> Vec<f64> {
names
.iter()
.map(|n| {
state
.get(n)
.or_else(|| state.get(bare(n)))
.and_then(Value::as_f64)
.unwrap_or(0.0)
})
.collect()
}
fn json_to_field(v: &Value) -> Result<ArrayD<f64>, String> {
let arr = v.as_array().ok_or("input field is not an array")?;
if arr.first().map(Value::is_array).unwrap_or(false) {
let nrow = arr.len();
let ncol = arr[0]
.as_array()
.ok_or("input grid row is not an array")?
.len();
let mut data = Vec::with_capacity(nrow * ncol);
for row in arr {
let r = row.as_array().ok_or("ragged input grid")?;
if r.len() != ncol {
return Err("ragged input grid".to_string());
}
for x in r {
data.push(x.as_f64().ok_or("non-numeric input value")?);
}
}
ArrayD::from_shape_vec(IxDyn(&[nrow, ncol]), data).map_err(|e| e.to_string())
} else {
let data: Vec<f64> = arr
.iter()
.map(|x| x.as_f64().ok_or("non-numeric input value".to_string()))
.collect::<Result<_, _>>()?;
let n = data.len();
ArrayD::from_shape_vec(IxDyn(&[n]), data).map_err(|e| e.to_string())
}
}
fn run_fixture_full(fx: &Value, base: &Path, integ: &Value) -> Result<Value, String> {
let rel = fx["path"].as_str().ok_or("fixture.path missing")?;
let json_str = fs::read_to_string(base.join(rel)).map_err(|e| e.to_string())?;
let file = load_string(&json_str).map_err(|e| format!("load: {e:?}"))?;
let flat = flatten(&file).map_err(|e| format!("flatten: {e:?}"))?;
let compiled = ArrayCompiled::from_flattened(&flat).map_err(|e| format!("compile: {e:?}"))?;
let inputs = fx["inputs"].as_object().ok_or("fixture.inputs missing")?;
{
let forcing = compiled.forcing_handle();
let mut buf = forcing.borrow_mut();
for (k, v) in inputs {
buf.insert(k.clone(), json_to_field(v)?);
}
}
let params: HashMap<String, f64> = HashMap::new();
let names: Vec<String> = compiled.state_variable_names().to_vec();
let mut rhs = Map::new();
for probe in fx["rhs_probes"].as_array().ok_or("rhs_probes not array")? {
let pid = probe["id"].as_str().ok_or("probe.id missing")?;
let state_obj = probe["state"].as_object().ok_or("probe.state missing")?;
let t = probe["t"].as_f64().unwrap_or(0.0);
let sv = state_vec_bare(&names, state_obj);
let (dy, _stats) = compiled.debug_eval_rhs(&sv, t, ¶ms, false);
let mut m = Map::new();
for (i, n) in names.iter().enumerate() {
m.insert(bare(n).to_string(), json!(dy[i]));
}
rhs.insert(pid.to_string(), Value::Object(m));
}
let checkpoints: Vec<f64> = fx["trajectory"]["checkpoints"]
.as_array()
.ok_or("trajectory.checkpoints missing")?
.iter()
.map(|v| v.as_f64().unwrap_or(0.0))
.collect();
let t0 = *checkpoints.first().ok_or("empty checkpoints")?;
let t1 = *checkpoints.last().ok_or("empty checkpoints")?;
let opts = SolveOptions {
alg: solver_from(integ["solver"].as_str().unwrap_or("Erk")),
abstol: Some(integ["abstol"].as_f64().unwrap_or(1e-12)),
reltol: Some(integ["reltol"].as_f64().unwrap_or(1e-10)),
maxiters: Some(10_000_000),
saveat: Some(checkpoints.clone()),
..Default::default()
};
let sol = compiled
.solve((t0, t1), ¶ms, &HashMap::new(), &opts)
.map_err(|e| format!("solve: {e:?}"))?;
let mut traj = Map::new();
for &t in &checkpoints {
let idx = nearest_index(&sol.time, t).ok_or("empty solution time grid")?;
let mut m = Map::new();
for (row, n) in sol.state_variable_names.iter().enumerate() {
m.insert(bare(n).to_string(), json!(sol.state[row][idx]));
}
traj.insert(time_key(t), Value::Object(m));
}
Ok(json!({ "rhs": Value::Object(rhs), "trajectory": Value::Object(traj) }))
}
fn main() -> ExitCode {
let (manifest_path, output_path, compiler) = match parse_args() {
Ok(a) => a,
Err(e) => {
eprintln!("pde-sim-adapter-rust: {e}");
return ExitCode::FAILURE;
}
};
let manifest: Value = match fs::read_to_string(&manifest_path)
.map_err(|e| e.to_string())
.and_then(|s| serde_json::from_str(&s).map_err(|e| e.to_string()))
{
Ok(v) => v,
Err(e) => {
eprintln!("pde-sim-adapter-rust: failed to read manifest: {e}");
return ExitCode::FAILURE;
}
};
let base = manifest_path.parent().unwrap_or_else(|| Path::new("."));
let integ = manifest
.get("integrators")
.and_then(|i| i.get("rust"))
.cloned()
.unwrap_or(Value::Null);
let empty: Vec<Value> = Vec::new();
let mut fixtures = Map::new();
for fx in manifest["fixtures"].as_array().unwrap_or(&empty).iter() {
let id = fx["id"].as_str().unwrap_or("<unknown>").to_string();
let result = if fx.get("pipeline").and_then(Value::as_str) == Some("full") {
run_fixture_full(fx, base, &integ)
} else {
run_fixture(fx, base, &integ, compiler)
};
let entry = match result {
Ok(v) => v,
Err(e) => {
eprintln!("fixture {id}: {e}");
json!({ "error": e })
}
};
fixtures.insert(id, entry);
}
let payload = json!({ "binding": "rust", "fixtures": Value::Object(fixtures) });
if let Err(e) = write_report(&output_path, &payload) {
eprintln!("pde-sim-adapter-rust: failed to write output: {e}");
return ExitCode::FAILURE;
}
ExitCode::SUCCESS
}