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

@@ -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")
}