use std::env::temp_dir;
use std::fs;
use std::process;
use oxirs_physics::pinn::loader::{
dump_residual_model_to_json, load_residual_model_from_json, load_residual_model_from_path,
};
use oxirs_physics::pinn::residual_model::DenseLayer;
use oxirs_physics::pinn::{
Activation, PinnCorrector, PinnError, ResidualModel, ResidualModelConfig,
};
fn tiny_identity_model() -> ResidualModel {
let cfg = ResidualModelConfig {
input_dim: 2,
output_dim: 2,
hidden_widths: vec![2],
hidden_activation: Activation::Relu,
output_activation: Activation::Identity,
description: "identity".to_string(),
};
let h = DenseLayer::new(
vec![vec![1.0, 0.0], vec![0.0, 1.0]],
vec![0.0, 0.0],
Activation::Identity,
)
.expect("layer ok");
let o = DenseLayer::new(
vec![vec![1.0, 0.0], vec![0.0, 1.0]],
vec![0.0, 0.0],
Activation::Identity,
)
.expect("layer ok");
ResidualModel::from_layers(cfg, vec![h, o]).expect("model ok")
}
#[test]
fn json_round_trip_through_temp_dir() {
let model = tiny_identity_model();
let bytes = dump_residual_model_to_json(&model).expect("dump ok");
let mut path = temp_dir();
path.push(format!(
"oxirs_physics_pinn_test_{}_{}.json",
process::id(),
chrono::Utc::now().timestamp_nanos_opt().unwrap_or(0)
));
fs::write(&path, &bytes).expect("write ok");
let loaded = load_residual_model_from_path(&path).expect("load ok");
assert_eq!(loaded, model);
let _ = fs::remove_file(&path);
}
#[test]
fn json_bytes_round_trip() {
let model = tiny_identity_model();
let bytes = dump_residual_model_to_json(&model).expect("dump ok");
let loaded = load_residual_model_from_json(&bytes).expect("load ok");
assert_eq!(loaded, model);
}
#[test]
fn parameter_count_under_10k() {
let model = tiny_identity_model();
assert!(model.parameter_count() < 10_000);
}
#[test]
fn corrector_apply_matches_feature() {
let model = tiny_identity_model();
let corrector = PinnCorrector::new(model);
let prev = vec![1.0, 2.0];
let step = vec![3.0, 4.0];
let result = corrector.apply(&prev, &step);
if cfg!(feature = "pinn_correction") {
let v = result.expect("apply ok");
assert_eq!(v, vec![4.0, 6.0]);
assert!(corrector.is_active());
} else {
assert!(matches!(result, Err(PinnError::FeatureDisabled)));
assert!(!corrector.is_active());
}
}
#[test]
fn empty_corrector_always_returns_disabled() {
let c = PinnCorrector::empty();
let r = c.apply(&[1.0], &[1.0]);
assert!(matches!(r, Err(PinnError::FeatureDisabled)));
}
#[test]
fn validate_rejects_inconsistent_layer_count() {
let cfg = ResidualModelConfig {
input_dim: 2,
output_dim: 2,
hidden_widths: vec![2, 2, 2], hidden_activation: Activation::Relu,
output_activation: Activation::Identity,
description: "bad".to_string(),
};
let h = DenseLayer::new(
vec![vec![1.0, 0.0], vec![0.0, 1.0]],
vec![0.0, 0.0],
Activation::Relu,
)
.expect("layer ok");
let o = DenseLayer::new(
vec![vec![1.0, 0.0], vec![0.0, 1.0]],
vec![0.0, 0.0],
Activation::Identity,
)
.expect("layer ok");
let result = ResidualModel::from_layers(cfg, vec![h, o]);
assert!(matches!(result, Err(PinnError::InvalidModel(_))));
}
#[test]
fn synthetic_residual_correction_matches_hand_computation() {
let cfg = ResidualModelConfig {
input_dim: 2,
output_dim: 2,
hidden_widths: vec![2],
hidden_activation: Activation::Identity,
output_activation: Activation::Identity,
description: "synthetic".to_string(),
};
let h = DenseLayer::new(
vec![vec![1.0, 0.0], vec![0.0, 0.0]],
vec![1.0, 0.0],
Activation::Identity,
)
.expect("layer ok");
let o = DenseLayer::new(
vec![vec![1.0, 0.0], vec![0.0, 0.0]],
vec![0.0, 0.0],
Activation::Identity,
)
.expect("layer ok");
let model = ResidualModel::from_layers(cfg, vec![h, o]).expect("model ok");
let raw = model.forward_raw(&[5.0, 7.0]).expect("forward ok");
assert_eq!(raw, vec![6.0, 0.0]);
let out = model.apply_residual(&[5.0, 7.0]).expect("apply ok");
assert_eq!(out, vec![5.0 + 6.0, 7.0 + 0.0]);
}
#[test]
fn loader_rejects_garbage() {
let result = load_residual_model_from_json(b"not a model");
assert!(matches!(result, Err(PinnError::Deserialise(_))));
}