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:
2026-07-17 22:46:46 +02:00
parent 62efea0646
commit 3358b74342
275 changed files with 70187 additions and 5230 deletions

View File

@@ -8,8 +8,6 @@
//! - Bounds enforcement
//! - JSON round-trip of CalibrationResult
use std::collections::HashMap;
use entropyk_components::{
Component, ComponentError, ConnectedPort, JacobianBuilder, ResidualVector, StateSlice,
};
@@ -18,13 +16,14 @@ use entropyk_solver::{
inverse::calibration::{
CalibFactor, CalibRequest, CalibrationMode, CalibrationProblem, CalibrationTarget,
},
NewtonConfig, Solver, System,
NewtonConfig, System,
};
/// Mock component whose capacity scales linearly with f_ua.
/// Capacity = base_capacity * f_ua, where base_capacity = 4000.0 W.
struct MockCalibratedHx {
calib_indices: CalibIndices,
#[allow(dead_code)] // Set by the fixture constructor; documents intended capacity scaling.
base_capacity: f64,
}
@@ -43,9 +42,10 @@ impl Component for MockCalibratedHx {
state: &StateSlice,
residuals: &mut ResidualVector,
) -> Result<(), ComponentError> {
// Fix edge states to known values
residuals[0] = state[0] - 300.0;
residuals[1] = state[1] - 400.0;
// Fix edge states to known values.
// CM1.2: per-edge state is (ṁ, P, h); skip ṁ at index 0.
residuals[0] = state[1] - 300.0;
residuals[1] = state[2] - 400.0;
Ok(())
}
@@ -54,8 +54,8 @@ impl Component for MockCalibratedHx {
_state: &StateSlice,
jacobian: &mut JacobianBuilder,
) -> Result<(), ComponentError> {
jacobian.add_entry(0, 0, 1.0);
jacobian.add_entry(1, 1, 1.0);
jacobian.add_entry(0, 1, 1.0);
jacobian.add_entry(1, 2, 1.0);
Ok(())
}
@@ -91,7 +91,7 @@ fn test_single_factor_calibration_f_ua() {
let problem = CalibrationProblem::new()
.add_request(CalibRequest::new(
CalibFactor::FUa,
CalibFactor::ZUa,
"evaporator",
(0.1, 10.0),
1.0,
@@ -102,7 +102,7 @@ fn test_single_factor_calibration_f_ua() {
let result = problem.calibrate(&mut sys, &config).unwrap();
assert!(result.converged, "Calibration should converge");
let f_ua = result.estimated_factor("evaporator.f_ua").unwrap();
let f_ua = result.estimated_factor("evaporator.z_ua").unwrap();
// The mock capacity is extracted via extract_constraint_values_with_controls,
// which uses the actual solver. Since the mock is simplified, we just verify
// convergence and that a factor was returned.
@@ -119,8 +119,12 @@ fn test_sequential_mode_is_default() {
#[test]
fn test_problem_dof_validation() {
let sys = System::new();
let p = CalibrationProblem::new()
.add_request(CalibRequest::new(CalibFactor::FUa, "evaporator", (0.1, 10.0), 1.0));
let p = CalibrationProblem::new().add_request(CalibRequest::new(
CalibFactor::ZUa,
"evaporator",
(0.1, 10.0),
1.0,
));
// Only 1 request, 0 targets → DoF mismatch
let err = p.validate(&sys).unwrap_err();
assert!(format!("{err}").contains("DoF mismatch"));
@@ -130,7 +134,12 @@ fn test_problem_dof_validation() {
fn test_problem_missing_component() {
let sys = System::new();
let p = CalibrationProblem::new()
.add_request(CalibRequest::new(CalibFactor::FUa, "nonexistent", (0.1, 10.0), 1.0))
.add_request(CalibRequest::new(
CalibFactor::ZUa,
"nonexistent",
(0.1, 10.0),
1.0,
))
.add_target(CalibrationTarget::capacity("nonexistent", 4015.0));
let err = p.validate(&sys).unwrap_err();
assert!(format!("{err}").contains("not registered"));
@@ -142,7 +151,7 @@ fn test_bounds_validation_on_request() {
let problem = CalibrationProblem::new()
.add_request(CalibRequest::new(
CalibFactor::FUa,
CalibFactor::ZUa,
"evaporator",
(0.1, 10.0),
0.05, // initial value below min bound
@@ -159,28 +168,29 @@ fn test_bounds_validation_on_request() {
fn test_calibration_result_json_roundtrip() {
use std::collections::HashMap;
let mut result =
entropyk_solver::inverse::calibration::CalibrationResult {
estimated_factors: HashMap::new(),
residuals: HashMap::new(),
mape: 0.0,
max_abs_error: 0.0,
iterations: 0,
converged: false,
saturated_factors: Vec::new(),
};
let mut result = entropyk_solver::inverse::calibration::CalibrationResult {
estimated_factors: HashMap::new(),
residuals: HashMap::new(),
mape: 0.0,
max_abs_error: 0.0,
iterations: 0,
converged: false,
saturated_factors: Vec::new(),
};
result
.estimated_factors
.insert("evaporator.f_ua".to_string(), 1.15);
.insert("evaporator.z_ua".to_string(), 1.15);
result
.estimated_factors
.insert("compressor.f_m".to_string(), 0.95);
result.residuals.insert("evaporator.f_ua".to_string(), 0.02);
.insert("compressor.z_flow".to_string(), 0.95);
result.residuals.insert("evaporator.z_ua".to_string(), 0.02);
result.mape = 1.5;
result.max_abs_error = 0.05;
result.iterations = 42;
result.converged = true;
result.saturated_factors.push("compressor.f_m".to_string());
result
.saturated_factors
.push("compressor.z_flow".to_string());
let json = serde_json::to_string(&result).unwrap();
let result2: entropyk_solver::inverse::calibration::CalibrationResult =
@@ -191,8 +201,13 @@ fn test_calibration_result_json_roundtrip() {
#[test]
fn test_calib_factor_ordering() {
let order = CalibFactor::calibration_order();
assert_eq!(order[0], CalibFactor::FM, "f_m should come first");
assert_eq!(order[2], CalibFactor::FUa, "f_ua should come third");
assert_eq!(order[0], CalibFactor::ZFlow, "f_m should come first");
assert_eq!(
order[1],
CalibFactor::ZFlowEco,
"economizer flow should follow suction flow"
);
assert_eq!(order[3], CalibFactor::ZUa, "f_ua should come fourth");
}
#[test]