Wire BPHX channel pressure drop on both sides with selectable correlations.
Some checks failed
CI / check (push) Has been cancelled

Replace isobaric 4-port closures with SimplifiedChannel (default) and Martin1996 DP models so z_dp and UI dp_correlation actually affect the Newton solve.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-19 16:52:25 +02:00
parent 5bd180b5b8
commit 44f793a583
19 changed files with 1287 additions and 106 deletions

View File

@@ -33,6 +33,7 @@
"target_subcooling_k": 5.0,
"emergent_pressure": true,
"correlation": "Longo2004",
"dp_correlation": "SimplifiedChannel",
"ua": 2500.0
},
{
@@ -52,6 +53,7 @@
"target_superheat_k": 5.0,
"emergent_pressure": true,
"correlation": "Longo2004",
"dp_correlation": "SimplifiedChannel",
"ua": 2000.0
},
{

View File

@@ -315,7 +315,7 @@ fn process_single_file(
initialization_diagnostics: None,
dof: None,
elapsed_ms: 0,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
}
}
}
@@ -425,7 +425,7 @@ mod tests {
initialization_diagnostics: None,
dof: None,
elapsed_ms: 50,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
},
SimulationResult {
input: "test2.json".to_string(),
@@ -439,7 +439,7 @@ mod tests {
initialization_diagnostics: None,
dof: None,
elapsed_ms: 0,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
},
];
@@ -483,7 +483,7 @@ mod tests {
initialization_diagnostics: None,
dof: None,
elapsed_ms: 50,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
},
SimulationResult {
input: "test2.json".to_string(),
@@ -497,7 +497,7 @@ mod tests {
initialization_diagnostics: None,
dof: None,
elapsed_ms: 0,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
},
];
@@ -529,7 +529,7 @@ mod tests {
initialization_diagnostics: None,
dof: None,
elapsed_ms: 50,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
},
SimulationResult {
input: "test2.json".to_string(),
@@ -543,7 +543,7 @@ mod tests {
initialization_diagnostics: None,
dof: None,
elapsed_ms: 0,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
},
];
@@ -571,7 +571,7 @@ mod tests {
initialization_diagnostics: None,
dof: None,
elapsed_ms: 50,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
},
SimulationResult {
input: "test2.json".to_string(),
@@ -585,7 +585,7 @@ mod tests {
initialization_diagnostics: None,
dof: None,
elapsed_ms: 1000,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
},
];
@@ -643,7 +643,7 @@ mod tests {
initialization_diagnostics: None,
dof: None,
elapsed_ms: 100,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
}],
};
@@ -667,7 +667,7 @@ mod tests {
initialization_diagnostics: None,
dof: None,
elapsed_ms: 50,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
},
SimulationResult {
input: "ok2.json".to_string(),
@@ -681,7 +681,7 @@ mod tests {
initialization_diagnostics: None,
dof: None,
elapsed_ms: 60,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
},
SimulationResult {
input: "fail.json".to_string(),
@@ -695,7 +695,7 @@ mod tests {
initialization_diagnostics: None,
dof: None,
elapsed_ms: 0,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
},
];

View File

@@ -76,6 +76,13 @@ pub struct SimulationResult {
/// solve via the `ENTROPYK_INIT_STATE_JSON` environment variable).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub raw_state_vector: Option<Vec<f64>>,
/// Named solver-computed unknowns (free actuators + calibration factors
/// like `z_ua`, `opening`, `z_dp`) read out of `raw_state_vector` and
/// labeled with their stable id, owning component, and `[min, max]`
/// bounds. Empty for systems with no bounded control variables; skipped
/// on serialization when empty so older consumers keep working.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub solved_variables: Vec<entropyk::SolvedVariable>,
}
/// Compact DoF summary for CLI / web API consumers.
@@ -365,7 +372,7 @@ fn execute_simulation(
initialization_diagnostics: None,
dof: None,
elapsed_ms,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
};
}
@@ -400,7 +407,7 @@ fn execute_simulation(
initialization_diagnostics: None,
dof: None,
elapsed_ms,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
};
}
};
@@ -710,7 +717,7 @@ fn execute_simulation(
initialization_diagnostics: None,
dof: None,
elapsed_ms,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
};
}
},
@@ -730,7 +737,7 @@ fn execute_simulation(
initialization_diagnostics: None,
dof: None,
elapsed_ms,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
};
}
}
@@ -775,7 +782,7 @@ fn execute_simulation(
initialization_diagnostics: None,
dof: None,
elapsed_ms,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
};
}
},
@@ -795,7 +802,7 @@ fn execute_simulation(
initialization_diagnostics: None,
dof: None,
elapsed_ms,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
};
}
None => 1,
@@ -816,7 +823,7 @@ fn execute_simulation(
initialization_diagnostics: None,
dof: None,
elapsed_ms,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
};
}
},
@@ -836,7 +843,7 @@ fn execute_simulation(
initialization_diagnostics: None,
dof: None,
elapsed_ms,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
};
}
None => 0,
@@ -860,7 +867,7 @@ fn execute_simulation(
initialization_diagnostics: None,
dof: None,
elapsed_ms,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
};
}
}
@@ -880,7 +887,7 @@ fn execute_simulation(
initialization_diagnostics: None,
dof: None,
elapsed_ms,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
};
}
}
@@ -950,7 +957,7 @@ fn execute_simulation(
initialization_diagnostics: None,
dof: None,
elapsed_ms,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
};
}
}
@@ -976,7 +983,7 @@ fn execute_simulation(
initialization_diagnostics: None,
dof: None,
elapsed_ms,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
};
let find_component = |name: &str| {
config
@@ -1054,7 +1061,7 @@ fn execute_simulation(
initialization_diagnostics: None,
dof: None,
elapsed_ms,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
};
}
system.add_saturated_controller(controller);
@@ -1072,7 +1079,7 @@ fn execute_simulation(
initialization_diagnostics: None,
dof: None,
elapsed_ms,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
};
}
}
@@ -1246,7 +1253,7 @@ fn execute_simulation(
initialization_diagnostics: None,
dof: None,
elapsed_ms,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
};
}
system.add_free_actuator(var_id);
@@ -1268,7 +1275,7 @@ fn execute_simulation(
initialization_diagnostics: None,
dof: None,
elapsed_ms,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
};
}
}
@@ -1289,7 +1296,7 @@ fn execute_simulation(
initialization_diagnostics: None,
dof: None,
elapsed_ms,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
};
}
@@ -1320,7 +1327,7 @@ fn execute_simulation(
initialization_diagnostics: None,
dof: Some(summary),
elapsed_ms,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
};
}
summary
@@ -1517,7 +1524,7 @@ fn execute_simulation(
initialization_diagnostics: None,
dof: Some(dof_summary),
elapsed_ms,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
};
}
@@ -1746,6 +1753,7 @@ fn execute_simulation(
dof: Some(dof_summary.clone()),
elapsed_ms,
raw_state_vector: Some(converged.state.clone()),
solved_variables: entropyk::extract_solved_variables(&system, &converged.state),
}
}
Err(e) => {
@@ -1791,7 +1799,7 @@ fn execute_simulation(
initialization_diagnostics: Some(initialization_diagnostics),
dof: Some(dof_summary),
elapsed_ms,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
}
}
}
@@ -2939,6 +2947,32 @@ fn bphx_correlation_from_params(
Ok(Some(correlation))
}
/// Parse BPHX frictional `dp_correlation` / `DpCorrelation` (independent of HTC).
///
/// Default when absent: `SimplifiedChannel`. Unknown ids are rejected (no silent isobaric).
fn bphx_dp_correlation_from_params(
params: &std::collections::HashMap<String, serde_json::Value>,
component_name: &str,
) -> CliResult<entropyk_components::heat_exchanger::BphxDpCorrelation> {
use entropyk_components::heat_exchanger::BphxDpCorrelation;
let Some(value) = params
.get("dp_correlation")
.or_else(|| params.get("DpCorrelation"))
.or_else(|| params.get("dpCorrelation"))
else {
return Ok(BphxDpCorrelation::default());
};
let Some(raw) = value.as_str() else {
return Err(CliError::Config(format!(
"{component_name}: dp_correlation must be a string (got {value})"
)));
};
BphxDpCorrelation::parse(raw).map_err(|msg| {
CliError::Config(format!("{component_name}: {msg}"))
})
}
/// Extract calibration Z-factors for BphxEvaporator/BphxCondenser from JSON params.
///
/// Errors if `ua_nominal == 0` and an explicit `ua` override is provided (geometry is
@@ -5095,7 +5129,11 @@ fn create_component(
.with_target_superheat(target_superheat)
.with_refrigerant(refrigerant)
.with_secondary_fluid(secondary_fluid)
.with_fluid_backend(Arc::clone(&backend));
.with_fluid_backend(Arc::clone(&backend))
.with_dp_correlation(bphx_dp_correlation_from_params(
params,
"BphxEvaporator",
)?);
if let Some(correlation) =
bphx_correlation_from_params(params, "BphxEvaporator")?
{
@@ -5151,7 +5189,11 @@ fn create_component(
.with_refrigerant(refrigerant)
.with_secondary_fluid(secondary_fluid)
.with_fluid_backend(Arc::clone(&backend))
.with_target_subcooling(target_subcooling);
.with_target_subcooling(target_subcooling)
.with_dp_correlation(bphx_dp_correlation_from_params(
params,
"BphxCondenser",
)?);
if let Some(correlation) =
bphx_correlation_from_params(params, "BphxCondenser")?
{
@@ -5630,7 +5672,7 @@ mod tests {
initialization_diagnostics: None,
dof: None,
elapsed_ms: 50,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
};
let json = serde_json::to_string_pretty(&result).unwrap();
@@ -5787,6 +5829,56 @@ mod tests {
);
}
#[test]
fn test_bphx_dp_correlation_defaults_and_aliases() {
use entropyk_components::heat_exchanger::BphxDpCorrelation;
use serde_json::json;
assert_eq!(
bphx_dp_correlation_from_params(&std::collections::HashMap::new(), "BphxEvaporator")
.unwrap(),
BphxDpCorrelation::SimplifiedChannel
);
let params = std::collections::HashMap::from([(
"DpCorrelation".to_string(),
json!("Martin1996"),
)]);
assert_eq!(
bphx_dp_correlation_from_params(&params, "BphxCondenser").unwrap(),
BphxDpCorrelation::Martin1996
);
}
#[test]
fn test_bphx_dp_correlation_rejects_unknown() {
use serde_json::json;
let params = std::collections::HashMap::from([(
"dp_correlation".to_string(),
json!("Amalfi2016"),
)]);
let error = bphx_dp_correlation_from_params(&params, "BphxEvaporator").unwrap_err();
assert!(
error.to_string().contains("unsupported dp_correlation"),
"{error}"
);
}
#[test]
fn test_bphx_dp_correlation_rejects_non_string() {
use serde_json::json;
let params = std::collections::HashMap::from([(
"dp_correlation".to_string(),
json!(42),
)]);
let error = bphx_dp_correlation_from_params(&params, "BphxEvaporator").unwrap_err();
assert!(
error.to_string().contains("must be a string"),
"{error}"
);
}
fn comp_from_json(v: serde_json::Value) -> crate::config::ComponentConfig {
serde_json::from_value(v).expect("valid component JSON")
}

View File

@@ -92,7 +92,7 @@ fn test_simulation_result_statuses() {
initialization_diagnostics: None,
dof: None,
elapsed_ms: 50,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
},
SimulationResult {
input: "fail.json".to_string(),
@@ -106,7 +106,7 @@ fn test_simulation_result_statuses() {
initialization_diagnostics: None,
dof: None,
elapsed_ms: 0,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
},
SimulationResult {
input: "timeout.json".to_string(),
@@ -120,7 +120,7 @@ fn test_simulation_result_statuses() {
initialization_diagnostics: None,
dof: None,
elapsed_ms: 1000,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
},
];
@@ -163,7 +163,7 @@ fn test_batch_aggregator_csv_output() {
initialization_diagnostics: None,
dof: None,
elapsed_ms: 150,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
},
SimulationResult {
input: "scenario2.json".to_string(),
@@ -183,7 +183,7 @@ fn test_batch_aggregator_csv_output() {
initialization_diagnostics: None,
dof: None,
elapsed_ms: 200,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
},
SimulationResult {
input: "scenario3.json".to_string(),
@@ -197,7 +197,7 @@ fn test_batch_aggregator_csv_output() {
initialization_diagnostics: None,
dof: None,
elapsed_ms: 0,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
},
];
@@ -229,7 +229,7 @@ fn test_batch_aggregator_json_summary() {
initialization_diagnostics: None,
dof: None,
elapsed_ms: 50,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
},
SimulationResult {
input: "test2.json".to_string(),
@@ -243,7 +243,7 @@ fn test_batch_aggregator_json_summary() {
initialization_diagnostics: None,
dof: None,
elapsed_ms: 75,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
},
SimulationResult {
input: "test3.json".to_string(),
@@ -257,7 +257,7 @@ fn test_batch_aggregator_json_summary() {
initialization_diagnostics: None,
dof: None,
elapsed_ms: 5000,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
},
];
@@ -322,7 +322,7 @@ fn test_batch_summary_csv_with_convergence() {
initialization_diagnostics: None,
dof: None,
elapsed_ms: 300,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
}];
let summary = BatchSummary {

View File

@@ -29,7 +29,7 @@ fn test_simulation_result_serialization() {
initialization_diagnostics: None,
dof: None,
elapsed_ms: 50,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
};
let json = serde_json::to_string_pretty(&result).unwrap();
@@ -70,7 +70,7 @@ fn test_error_result_serialization() {
initialization_diagnostics: None,
dof: None,
elapsed_ms: 0,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
};
let json = serde_json::to_string(&result).unwrap();
@@ -96,7 +96,7 @@ fn test_error_result_serializes_failure_diagnostics() {
initialization_diagnostics: None,
dof: None,
elapsed_ms: 0,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
};
let json = serde_json::to_string(&result).unwrap();
@@ -2303,7 +2303,7 @@ fn test_structural_failure_serializes_without_diagnostics() {
initialization_diagnostics: None,
dof: None,
elapsed_ms: 0,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
};
let json = serde_json::to_string(&result).unwrap();
@@ -2348,7 +2348,7 @@ fn test_success_result_does_not_include_failure_diagnostics() {
initialization_diagnostics: None,
dof: None,
elapsed_ms: 120,
raw_state_vector: None,
raw_state_vector: None, solved_variables: Vec::new(),
};
let json = serde_json::to_string(&result).unwrap();