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>
This commit is contained in:
@@ -7,12 +7,11 @@
|
||||
//! - `with_initial_state` builder on FallbackSolver delegates to both sub-solvers
|
||||
|
||||
use approx::assert_relative_eq;
|
||||
use entropyk_components::{
|
||||
Component, ComponentError, JacobianBuilder, ResidualVector, StateSlice,
|
||||
};
|
||||
use entropyk_core::{Enthalpy, Pressure, Temperature};
|
||||
use entropyk_components::{Component, ComponentError, JacobianBuilder, ResidualVector, StateSlice};
|
||||
use entropyk_core::{Enthalpy, Temperature};
|
||||
use entropyk_solver::{
|
||||
solver::{FallbackSolver, NewtonConfig, PicardConfig, Solver},
|
||||
solver::{FallbackSolver, NewtonConfig, PicardConfig, Solver, SolverError},
|
||||
system::DEFAULT_MASS_FLOW_SEED_KG_S,
|
||||
InitializerConfig, SmartInitializer, System,
|
||||
};
|
||||
|
||||
@@ -39,9 +38,13 @@ impl Component for LinearTargetSystem {
|
||||
state: &StateSlice,
|
||||
residuals: &mut ResidualVector,
|
||||
) -> Result<(), ComponentError> {
|
||||
// CM1.3: per-edge state is (ṁ, P, h). Equations i=0..n target state[i+1]
|
||||
// (P and h slots). The last equation pins the mass-flow (state[0]) to the
|
||||
// default seed so the system stays square with 3 unknowns per edge.
|
||||
for (i, &t) in self.targets.iter().enumerate() {
|
||||
residuals[i] = state[i] - t;
|
||||
residuals[i] = state[i + 1] - t;
|
||||
}
|
||||
residuals[self.targets.len()] = state[0] - DEFAULT_MASS_FLOW_SEED_KG_S;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -51,13 +54,15 @@ impl Component for LinearTargetSystem {
|
||||
jacobian: &mut JacobianBuilder,
|
||||
) -> Result<(), ComponentError> {
|
||||
for i in 0..self.targets.len() {
|
||||
jacobian.add_entry(i, i, 1.0);
|
||||
jacobian.add_entry(i, i + 1, 1.0);
|
||||
}
|
||||
// Mass-flow equation: ∂r_ṁ/∂state[0] = 1
|
||||
jacobian.add_entry(self.targets.len(), 0, 1.0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn n_equations(&self) -> usize {
|
||||
self.targets.len()
|
||||
self.targets.len() + 1
|
||||
}
|
||||
|
||||
fn get_ports(&self) -> &[entropyk_components::ConnectedPort] {
|
||||
@@ -89,11 +94,15 @@ fn build_system_with_targets(targets: Vec<f64>) -> System {
|
||||
/// (already converged at initial check).
|
||||
#[test]
|
||||
fn test_newton_with_initial_state_converges_at_target() {
|
||||
// 2-entry state (1 edge × 2 entries: P, h)
|
||||
// 1 edge × (ṁ, P, h); seed ṁ so the placeholder mass-flow closure is satisfied.
|
||||
let targets = vec![300_000.0, 400_000.0];
|
||||
let mut sys = build_system_with_targets(targets.clone());
|
||||
|
||||
let mut solver = NewtonConfig::default().with_initial_state(targets.clone());
|
||||
let mut solver = NewtonConfig::default().with_initial_state(vec![
|
||||
DEFAULT_MASS_FLOW_SEED_KG_S,
|
||||
targets[0],
|
||||
targets[1],
|
||||
]);
|
||||
let result = solver.solve(&mut sys);
|
||||
|
||||
assert!(result.is_ok(), "Should converge: {:?}", result.err());
|
||||
@@ -112,7 +121,11 @@ fn test_picard_with_initial_state_converges_at_target() {
|
||||
let targets = vec![300_000.0, 400_000.0];
|
||||
let mut sys = build_system_with_targets(targets.clone());
|
||||
|
||||
let mut solver = PicardConfig::default().with_initial_state(targets.clone());
|
||||
let mut solver = PicardConfig::default().with_initial_state(vec![
|
||||
DEFAULT_MASS_FLOW_SEED_KG_S,
|
||||
targets[0],
|
||||
targets[1],
|
||||
]);
|
||||
let result = solver.solve(&mut sys);
|
||||
|
||||
assert!(result.is_ok(), "Should converge: {:?}", result.err());
|
||||
@@ -150,7 +163,11 @@ fn test_fallback_solver_with_initial_state_at_solution() {
|
||||
let targets = vec![300_000.0, 400_000.0];
|
||||
let mut sys = build_system_with_targets(targets.clone());
|
||||
|
||||
let mut solver = FallbackSolver::default_solver().with_initial_state(targets.clone());
|
||||
let mut solver = FallbackSolver::default_solver().with_initial_state(vec![
|
||||
DEFAULT_MASS_FLOW_SEED_KG_S,
|
||||
targets[0],
|
||||
targets[1],
|
||||
]);
|
||||
let result = solver.solve(&mut sys);
|
||||
|
||||
assert!(result.is_ok(), "Should converge: {:?}", result.err());
|
||||
@@ -179,8 +196,11 @@ fn test_smart_initializer_reduces_iterations_vs_zero_start() {
|
||||
.expect("zero-start should converge");
|
||||
|
||||
// Run 2: from smart initial state (we directly provide the values as an approximation)
|
||||
// Use 95% of target as "smart" initial — simulating a near-correct heuristic
|
||||
let smart_state: Vec<f64> = targets.iter().map(|&t| t * 0.95).collect();
|
||||
// Use 95% of target as "smart" initial — simulating a near-correct heuristic.
|
||||
// 1 edge × (ṁ, P, h): seed ṁ then the two scaled targets for P, h.
|
||||
let smart_state: Vec<f64> = std::iter::once(DEFAULT_MASS_FLOW_SEED_KG_S)
|
||||
.chain(targets.iter().map(|&t| t * 0.95))
|
||||
.collect();
|
||||
let mut sys_smart = build_system_with_targets(targets.clone());
|
||||
let mut solver_smart = NewtonConfig::default().with_initial_state(smart_state);
|
||||
let result_smart = solver_smart
|
||||
@@ -253,45 +273,38 @@ fn test_cold_start_estimate_then_populate() {
|
||||
init.populate_state(&sys, p_evap, p_cond, h_default, &mut state)
|
||||
.expect("populate_state should succeed");
|
||||
|
||||
assert_eq!(state.len(), 4); // 2 edges × [P, h]
|
||||
// CM1.4: 2-edge linear chain → 1 series branch + 2×2 P,h = 5 state vars.
|
||||
// State layout: [ṁ_branch, P_e0, h_e0, P_e1, h_e1]
|
||||
assert_eq!(state.len(), 5);
|
||||
|
||||
// All edges in single circuit → P_evap used for all
|
||||
assert_relative_eq!(state[0], p_evap.to_pascals(), max_relative = 1e-9);
|
||||
assert_relative_eq!(state[1], h_default.to_joules_per_kg(), max_relative = 1e-9);
|
||||
assert_relative_eq!(state[2], p_evap.to_pascals(), max_relative = 1e-9);
|
||||
assert_relative_eq!(state[3], h_default.to_joules_per_kg(), max_relative = 1e-9);
|
||||
// All edges share 1 ṁ slot (same series branch) → seeded to the mass-flow seed.
|
||||
// All edges in single circuit → P_evap used for all.
|
||||
assert_relative_eq!(state[0], DEFAULT_MASS_FLOW_SEED_KG_S, max_relative = 1e-9); // ṁ branch
|
||||
assert_relative_eq!(state[1], p_evap.to_pascals(), max_relative = 1e-9); // P edge 0
|
||||
assert_relative_eq!(state[2], h_default.to_joules_per_kg(), max_relative = 1e-9); // h edge 0
|
||||
assert_relative_eq!(state[3], p_evap.to_pascals(), max_relative = 1e-9); // P edge 1
|
||||
assert_relative_eq!(state[4], h_default.to_joules_per_kg(), max_relative = 1e-9);
|
||||
// h edge 1
|
||||
}
|
||||
|
||||
/// AC #8 — Verify initial_state length mismatch falls back gracefully (doesn't panic).
|
||||
/// A mismatched `initial_state` length is rejected cleanly (zero-panic).
|
||||
///
|
||||
/// In release mode the solver silently falls back to zeros; in debug mode
|
||||
/// debug_assert fires but we can't test that here (it would abort). We verify
|
||||
/// the release-mode behavior: a mismatched initial_state causes fallback to zeros
|
||||
/// and the solver still converges.
|
||||
/// Previously this aborted via `debug_assert` in debug builds and silently fell
|
||||
/// back to zeros in release builds (solving a different problem). The contract is
|
||||
/// now uniform across build profiles and solvers: a wrong-length initial state
|
||||
/// returns `SolverError::InvalidSystem` rather than panicking or guessing.
|
||||
#[test]
|
||||
fn test_initial_state_length_mismatch_fallback() {
|
||||
// System has 2 state entries (1 edge × 2)
|
||||
fn test_initial_state_length_mismatch_is_rejected() {
|
||||
// System has 3 state entries (1 edge × (ṁ, P, h))
|
||||
let targets = vec![300_000.0, 400_000.0];
|
||||
let mut sys = build_system_with_targets(targets.clone());
|
||||
|
||||
// Provide wrong-length initial state (3 instead of 2)
|
||||
// In release mode: solver falls back to zeros, still converges
|
||||
// In debug mode: debug_assert panics — we skip this test in debug
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
let wrong_state = vec![1.0, 2.0, 3.0]; // length 3, system needs 2
|
||||
let mut solver = NewtonConfig::default().with_initial_state(wrong_state);
|
||||
let result = solver.solve(&mut sys);
|
||||
// Should still converge (fell back to zeros)
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Should converge even with mismatched initial_state in release mode"
|
||||
);
|
||||
}
|
||||
let wrong_state = vec![1.0, 2.0]; // length 2, system needs 3
|
||||
let mut solver = NewtonConfig::default().with_initial_state(wrong_state);
|
||||
let result = solver.solve(&mut sys);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
// In debug mode, skip this test (debug_assert would abort)
|
||||
let _ = (sys, targets); // suppress unused variable warnings
|
||||
}
|
||||
assert!(
|
||||
matches!(result, Err(SolverError::InvalidSystem { .. })),
|
||||
"expected InvalidSystem for a length mismatch, got {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user