Files
Entropyk/crates/solver/tests/traceability.rs
sepehr 3358b74342 Add diagram workbench UI with Modelica DoF coaching and ISO glyphs.
Ship the Next.js cycle editor with CAD chrome, technical HX symbols, Fixed/Free boundary guidance, and secondary water/air pressure drop support in the solver stack.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 22:46:46 +02:00

140 lines
4.5 KiB
Rust

use entropyk_components::port::{FluidId, Port};
use entropyk_components::{Component, ComponentError, ConnectedPort, JacobianBuilder, StateSlice};
use entropyk_core::{Enthalpy, Pressure};
use entropyk_solver::solver::{NewtonConfig, Solver};
use entropyk_solver::system::{System, DEFAULT_MASS_FLOW_SEED_KG_S};
struct DummyComponent {
ports: Vec<ConnectedPort>,
/// Fluid label used in signature() so input_hash reflects fluid configuration.
fluid_label: String,
}
impl Component for DummyComponent {
fn compute_residuals(
&self,
_state: &StateSlice,
residuals: &mut entropyk_components::ResidualVector,
) -> Result<(), ComponentError> {
residuals[0] = 0.0;
residuals[1] = 0.0;
Ok(())
}
fn jacobian_entries(
&self,
_state: &StateSlice,
jacobian: &mut JacobianBuilder,
) -> Result<(), ComponentError> {
jacobian.add_entry(0, 0, 1.0);
jacobian.add_entry(1, 1, 1.0);
Ok(())
}
fn n_equations(&self) -> usize {
2
}
fn get_ports(&self) -> &[ConnectedPort] {
&self.ports
}
fn signature(&self) -> String {
format!("DummyComponent({})", self.fluid_label)
}
}
fn make_dummy_component() -> Box<dyn Component> {
make_dummy_component_with_fluid("R134a")
}
fn make_dummy_component_with_fluid(fluid: &str) -> Box<dyn Component> {
let inlet = Port::new(
FluidId::new(fluid),
Pressure::from_pascals(100_000.0),
Enthalpy::from_joules_per_kg(400_000.0),
);
let outlet = Port::new(
FluidId::new(fluid),
Pressure::from_pascals(100_000.0),
Enthalpy::from_joules_per_kg(400_000.0),
);
let (connected_inlet, connected_outlet) = inlet.connect(outlet).unwrap();
let ports = vec![connected_inlet, connected_outlet];
Box::new(DummyComponent {
ports,
fluid_label: fluid.to_string(),
})
}
#[test]
fn test_simulation_metadata_outputs() {
let mut sys = System::new();
let n0 = sys.add_component(make_dummy_component());
let n1 = sys.add_component(make_dummy_component());
sys.add_edge_with_ports(n0, 1, n1, 0).unwrap();
sys.add_edge_with_ports(n1, 1, n0, 0).unwrap();
sys.finalize().unwrap();
let input_hash = sys.input_hash();
// CM1.2: seed each edge's mass-flow slot so the temporary ṁ closures are
// satisfied at the start (DummyComponent residuals are all zero), letting the
// solver recognise convergence without inverting the singular dummy Jacobian.
let mut initial_state = vec![0.0; sys.full_state_vector_len()];
// Refrigerant edges have stride 3 with ṁ first; seed every ṁ slot.
for m in (0..initial_state.len()).step_by(3) {
initial_state[m] = DEFAULT_MASS_FLOW_SEED_KG_S;
}
let mut solver = NewtonConfig {
max_iterations: 5,
initial_state: Some(initial_state),
..Default::default()
};
let result = solver.solve(&mut sys).unwrap();
assert!(result.is_converged());
let metadata = result.metadata;
assert_eq!(metadata.input_hash, input_hash);
assert_eq!(metadata.solver_version, env!("CARGO_PKG_VERSION"));
assert_eq!(metadata.fluid_backend_version, "0.1.0");
}
/// Same topology (two nodes, two edges) but different fluid → different input_hash.
#[test]
fn test_input_hash_different_fluid_same_topology() {
let mut sys_r134a = System::new();
let n0 = sys_r134a.add_component(make_dummy_component_with_fluid("R134a"));
let n1 = sys_r134a.add_component(make_dummy_component_with_fluid("R134a"));
sys_r134a.add_edge_with_ports(n0, 1, n1, 0).unwrap();
sys_r134a.add_edge_with_ports(n1, 1, n0, 0).unwrap();
sys_r134a.finalize().unwrap();
let mut sys_r410a = System::new();
let n0 = sys_r410a.add_component(make_dummy_component_with_fluid("R410A"));
let n1 = sys_r410a.add_component(make_dummy_component_with_fluid("R410A"));
sys_r410a.add_edge_with_ports(n0, 1, n1, 0).unwrap();
sys_r410a.add_edge_with_ports(n1, 1, n0, 0).unwrap();
sys_r410a.finalize().unwrap();
assert_ne!(
sys_r134a.input_hash(),
sys_r410a.input_hash(),
"input_hash must differ when only fluid configuration differs"
);
}
#[test]
fn test_metadata_to_json() {
use entropyk_solver::SimulationMetadata;
let meta = SimulationMetadata::new("abc123".to_string());
let json = meta.to_json().unwrap();
assert!(json.contains("\"solver_version\""));
assert!(json.contains("\"fluid_backend_version\""));
assert!(json.contains("\"input_hash\""));
assert!(json.contains("abc123"));
}