Add model embeddings for Z-factor DoF, separate from SaturatedController.
Some checks failed
CI / check (push) Has been cancelled
Some checks failed
CI / check (push) Has been cancelled
Fixed/Free Probe calibration now emits embeddings[] (unknown + equation) instead of controls[], keeping SaturatedController for physical regulation only. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -17,8 +17,8 @@ use crate::error::{CliError, CliResult};
|
||||
pub const CURRENT_SCHEMA_VERSION: &str = "2";
|
||||
|
||||
/// Schema versions this build can load. `"1"` is the original flat
|
||||
/// `circuits/components/edges` schema; `"2"` adds `controls`, `subsystems`,
|
||||
/// `instances` and `connections`. Both are read by the same loader.
|
||||
/// `circuits/components/edges` schema; `"2"` adds `controls`, `embeddings`,
|
||||
/// `subsystems`, `instances` and `connections`. Both are read by the same loader.
|
||||
pub const SUPPORTED_SCHEMA_VERSIONS: &[&str] = &["1", "2"];
|
||||
|
||||
fn default_schema_version() -> String {
|
||||
@@ -30,7 +30,7 @@ fn default_schema_version() -> String {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ScenarioConfig {
|
||||
/// Model IR schema version (`"1"` legacy flat graph, `"2"` adds
|
||||
/// controls/subsystems/instances/connections). Absent ⇒ `"1"`.
|
||||
/// controls/embeddings/subsystems/instances/connections). Absent ⇒ `"1"`.
|
||||
#[serde(default = "default_schema_version")]
|
||||
pub schema_version: String,
|
||||
/// Scenario name.
|
||||
@@ -54,9 +54,15 @@ pub struct ScenarioConfig {
|
||||
/// hints only; they do not impose thermodynamic boundary conditions.
|
||||
#[serde(default)]
|
||||
pub initialization: Option<InitializationConfig>,
|
||||
/// Steady-state control loops (co-solved saturated-PI controllers).
|
||||
/// Steady-state **system regulation** loops (co-solved saturated-PI).
|
||||
/// For EXV opening, injection, fan speed, etc. — **not** Z-factor calibration.
|
||||
/// Z-factors use [`Self::embeddings`] (Modelica parameter(fixed=false) + equation).
|
||||
#[serde(default)]
|
||||
pub controls: Vec<ControlConfig>,
|
||||
/// Modelica-style Z-factor embeddings: one free unknown + one equation
|
||||
/// (`output = value`). Distinct from [`Self::controls`] (no SaturatedController).
|
||||
#[serde(default)]
|
||||
pub embeddings: Vec<EmbeddingConfig>,
|
||||
/// Reusable subsystem templates (parameterized assemblies of components +
|
||||
/// internal edges, exposing a reduced external port set). Flattened into
|
||||
/// `circuits` at load time — the solver never sees the hierarchy.
|
||||
@@ -146,12 +152,56 @@ pub struct InstanceConfig {
|
||||
pub params: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
/// A steady-state control loop declaration.
|
||||
/// Modelica-style Z-factor embedding: free unknown + binding equation.
|
||||
///
|
||||
/// Currently supports the `SaturatedController` type: a saturated-PI loop with
|
||||
/// anti-windup that is co-solved inside the Newton system. It drives a measured
|
||||
/// plant output (`measure`) to `target` by manipulating an actuator factor
|
||||
/// (`actuator`) within `[min, max]` bounds.
|
||||
/// Equivalent to:
|
||||
/// ```text
|
||||
/// parameter Real z(fixed = false, start = …);
|
||||
/// equation
|
||||
/// component.output = value;
|
||||
/// ```
|
||||
/// Registered as plain `Constraint` + `BoundedVariable` — never a SaturatedController.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct EmbeddingConfig {
|
||||
/// Unique id (also used as the constraint id).
|
||||
pub id: String,
|
||||
/// Free Z-factor unknown on a component.
|
||||
pub unknown: EmbeddingUnknownConfig,
|
||||
/// Binding equation: `component.output = value`.
|
||||
pub equation: EmbeddingEquationConfig,
|
||||
}
|
||||
|
||||
/// Free Z-factor unknown (`parameter ...(fixed=false)`).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct EmbeddingUnknownConfig {
|
||||
/// Component owning the Z-factor.
|
||||
pub component: String,
|
||||
/// Factor name: `z_ua`, `z_dp`, `z_flow`, `z_power`, `z_etav`, `f_w`, …
|
||||
pub factor: String,
|
||||
/// Start / guess value.
|
||||
#[serde(default = "default_actuator_initial")]
|
||||
pub start: f64,
|
||||
/// Lower bound.
|
||||
pub min: f64,
|
||||
/// Upper bound.
|
||||
pub max: f64,
|
||||
}
|
||||
|
||||
/// Binding equation residual: `component.output − value = 0`.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct EmbeddingEquationConfig {
|
||||
/// Component providing the measured output (typically a Probe).
|
||||
pub component: String,
|
||||
/// Output kind: `saturationTemperature`, `temperature`, `pressure`, …
|
||||
pub output: String,
|
||||
/// Right-hand side value (SI).
|
||||
pub value: f64,
|
||||
}
|
||||
|
||||
/// A steady-state **system regulation** loop (EXV, injection, fan, …).
|
||||
///
|
||||
/// Supports `SaturatedController`: saturated-PI with anti-windup, co-solved
|
||||
/// inside Newton. **Not** for Z-factor calibration — use [`EmbeddingConfig`].
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ControlConfig {
|
||||
/// Controller type. Only `"SaturatedController"` is supported today.
|
||||
@@ -217,13 +267,13 @@ pub struct MeasureConfig {
|
||||
pub output: String,
|
||||
}
|
||||
|
||||
/// Reference to a manipulated actuator on a component.
|
||||
/// Reference to a manipulated **physical** actuator on a component.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ActuatorConfig {
|
||||
/// Name of the component carrying the actuator.
|
||||
pub component: String,
|
||||
/// Calibration Z-factor to manipulate: `z_flow`, `z_dp`, `z_ua`, `z_power`, or `z_etav`
|
||||
/// (legacy `f_*` names and BOLT `Z_*` spellings are also accepted).
|
||||
/// Physical actuator factor for regulation: `opening`, `injection`, …
|
||||
/// Z-factors (`z_ua`, …) must use [`EmbeddingConfig`], not controls.
|
||||
pub factor: String,
|
||||
/// Initial actuator value (nominal, e.g. 1.0).
|
||||
#[serde(default = "default_actuator_initial")]
|
||||
@@ -1343,11 +1393,12 @@ mod tests {
|
||||
fn test_json_schema_emits_ir_fields() {
|
||||
let schema = ScenarioConfig::json_schema();
|
||||
// The emitted schema must be the single source of truth covering every IR
|
||||
// pillar: circuits (v1) + controls/subsystems/instances/connections (v2).
|
||||
// pillar: circuits (v1) + controls/embeddings/subsystems/instances/connections (v2).
|
||||
for field in [
|
||||
"schema_version",
|
||||
"circuits",
|
||||
"controls",
|
||||
"embeddings",
|
||||
"subsystems",
|
||||
"instances",
|
||||
"connections",
|
||||
@@ -1361,4 +1412,23 @@ mod tests {
|
||||
let parsed: serde_json::Value = serde_json::from_str(&schema).unwrap();
|
||||
assert!(parsed.get("$schema").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_embeddings() {
|
||||
let json = r#"{
|
||||
"schema_version": "2",
|
||||
"fluid": "R134a",
|
||||
"embeddings": [{
|
||||
"id": "emb_evap_z_ua",
|
||||
"unknown": { "component": "evap", "factor": "z_ua", "start": 0.3, "min": 0.05, "max": 3.0 },
|
||||
"equation": { "component": "SST probe", "output": "saturationTemperature", "value": 278.15 }
|
||||
}]
|
||||
}"#;
|
||||
let config = ScenarioConfig::from_json(json).unwrap();
|
||||
assert_eq!(config.embeddings.len(), 1);
|
||||
assert_eq!(config.embeddings[0].id, "emb_evap_z_ua");
|
||||
assert_eq!(config.embeddings[0].unknown.factor, "z_ua");
|
||||
assert!((config.embeddings[0].equation.value - 278.15).abs() < 1e-9);
|
||||
assert!(config.controls.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -414,6 +414,33 @@ fn execute_simulation(
|
||||
}
|
||||
};
|
||||
|
||||
// Fail fast: Z-factors belong in embeddings[], never controls[].
|
||||
for control in &config.controls {
|
||||
let factor = control.actuator.factor.trim();
|
||||
if entropyk_core::normalize_factor_name(factor).is_some() {
|
||||
return SimulationResult {
|
||||
input: input_name.to_string(),
|
||||
status: SimulationStatus::Error,
|
||||
convergence: None,
|
||||
iterations: None,
|
||||
state: None,
|
||||
performance: None,
|
||||
error: Some(format!(
|
||||
"control '{}': factor '{}' is a Z-factor — use embeddings[] \
|
||||
(Modelica parameter/equation), not controls[]/SaturatedController. \
|
||||
controls[] is for physical regulation (opening, injection, …).",
|
||||
control.id, factor
|
||||
)),
|
||||
failure_diagnostics: None,
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms,
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let mut system = System::new();
|
||||
|
||||
// Track component name -> (node index, component type) mapping per circuit
|
||||
@@ -619,6 +646,27 @@ fn execute_simulation(
|
||||
}
|
||||
}
|
||||
|
||||
// Free z_ua embedding: drop absolute `ua` overrides on the unknown's
|
||||
// component. Otherwise `ua` freezes Calib and ∂Q/∂z_ua → 0 (singular J).
|
||||
for emb in &config.embeddings {
|
||||
if entropyk_core::normalize_factor_name(emb.unknown.factor.trim())
|
||||
== Some(entropyk_core::Z_UA)
|
||||
{
|
||||
if let Some(comp) = expanded_components
|
||||
.iter_mut()
|
||||
.find(|c| c.name == emb.unknown.component)
|
||||
{
|
||||
if comp.params.remove("ua").is_some() {
|
||||
tracing::info!(
|
||||
component = %comp.name,
|
||||
embedding = %emb.id,
|
||||
"Dropped 'ua' override — z_ua embedding owns UA scaling"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fixed EXV orifice opening sets ṁ via the valve — skip compressor
|
||||
// displacement ṁ closure so DoF stays square (ṁ follows the valve).
|
||||
let meter_mass_flow_via_exv = expanded_components.iter().any(|c| {
|
||||
@@ -1050,13 +1098,12 @@ fn execute_simulation(
|
||||
}
|
||||
}
|
||||
|
||||
// Register declared control loops (saturated-PI, co-solved). Must happen
|
||||
// BEFORE finalize() so the actuator is wired to the component's CalibIndices.
|
||||
// Register model embeddings (Z-factor unknowns + equations) and system
|
||||
// regulation controls. Must happen BEFORE finalize() so unknowns wire to
|
||||
// CalibIndices.
|
||||
//
|
||||
// Routing (calibration redesign): controls whose actuator is a z-factor
|
||||
// (z_flow/z_flow_eco/z_dp/z_ua/z_power/z_etav) go through PLAIN inverse
|
||||
// embedding (+1 residual measured−target, +1 unknown z) — not the
|
||||
// SaturatedController (2+2 with integrator, meant for physical actuators).
|
||||
// - embeddings[] → plain Constraint + BoundedVariable (Modelica fixed=false)
|
||||
// - controls[] → SaturatedController only (EXV/injection/…); z-factors forbidden
|
||||
let control_error = |msg: String| SimulationResult {
|
||||
input: input_name.to_string(),
|
||||
status: SimulationStatus::Error,
|
||||
@@ -1072,83 +1119,51 @@ fn execute_simulation(
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
};
|
||||
for control in &config.controls {
|
||||
let factor = control.actuator.factor.trim();
|
||||
// Plain embedding applies to single-point z-factor calibration only.
|
||||
// A control WITH `objectives` is a supervisory override network
|
||||
// (selector semantics) and always keeps the SaturatedController path,
|
||||
// regardless of the actuator factor.
|
||||
if entropyk_core::normalize_factor_name(factor).is_some() && control.objectives.is_empty() {
|
||||
match build_plain_z_embedding(control, factor) {
|
||||
Ok((constraint, bounded_var, actuator_id)) => {
|
||||
if let Err(e) = system.add_constraint(constraint) {
|
||||
return control_error(format!(
|
||||
"Failed to add constraint for control '{}': {:?}",
|
||||
control.id, e
|
||||
));
|
||||
}
|
||||
if let Err(e) = system.add_bounded_variable(bounded_var) {
|
||||
return control_error(format!(
|
||||
"Failed to add actuator for control '{}': {:?}",
|
||||
control.id, e
|
||||
));
|
||||
}
|
||||
if let Err(e) = system.link_constraint_to_control(
|
||||
&entropyk_solver::inverse::ConstraintId::new(control.id.clone()),
|
||||
&actuator_id,
|
||||
) {
|
||||
return control_error(format!(
|
||||
"Failed to link control '{}': {:?}",
|
||||
control.id, e
|
||||
));
|
||||
}
|
||||
|
||||
for emb in &config.embeddings {
|
||||
match build_model_embedding(emb) {
|
||||
Ok((constraint, bounded_var, unknown_id)) => {
|
||||
if let Err(e) = system.add_constraint(constraint) {
|
||||
return control_error(format!(
|
||||
"Failed to add equation for embedding '{}': {:?}",
|
||||
emb.id, e
|
||||
));
|
||||
}
|
||||
Err(msg) => {
|
||||
return control_error(format!("Invalid control '{}': {}", control.id, msg));
|
||||
if let Err(e) = system.add_bounded_variable(bounded_var) {
|
||||
return control_error(format!(
|
||||
"Failed to add unknown for embedding '{}': {:?}",
|
||||
emb.id, e
|
||||
));
|
||||
}
|
||||
if let Err(e) = system.link_constraint_to_control(
|
||||
&entropyk_solver::inverse::ConstraintId::new(emb.id.clone()),
|
||||
&unknown_id,
|
||||
) {
|
||||
return control_error(format!(
|
||||
"Failed to link embedding '{}': {:?}",
|
||||
emb.id, e
|
||||
));
|
||||
}
|
||||
}
|
||||
continue;
|
||||
Err(msg) => {
|
||||
return control_error(format!("Invalid embedding '{}': {}", emb.id, msg));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for control in &config.controls {
|
||||
match build_saturated_control(control) {
|
||||
Ok((bounded_var, controller)) => {
|
||||
if let Err(e) = system.add_bounded_variable(bounded_var) {
|
||||
return SimulationResult {
|
||||
input: input_name.to_string(),
|
||||
status: SimulationStatus::Error,
|
||||
convergence: None,
|
||||
iterations: None,
|
||||
state: None,
|
||||
performance: None,
|
||||
error: Some(format!(
|
||||
"Failed to add actuator for control '{}': {:?}",
|
||||
control.id, e
|
||||
)),
|
||||
failure_diagnostics: None,
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms,
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
};
|
||||
return control_error(format!(
|
||||
"Failed to add actuator for control '{}': {:?}",
|
||||
control.id, e
|
||||
));
|
||||
}
|
||||
system.add_saturated_controller(controller);
|
||||
}
|
||||
Err(msg) => {
|
||||
return SimulationResult {
|
||||
input: input_name.to_string(),
|
||||
status: SimulationStatus::Error,
|
||||
convergence: None,
|
||||
iterations: None,
|
||||
state: None,
|
||||
performance: None,
|
||||
error: Some(format!("Invalid control '{}': {}", control.id, msg)),
|
||||
failure_diagnostics: None,
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms,
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
};
|
||||
return control_error(format!("Invalid control '{}': {}", control.id, msg));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1498,10 +1513,20 @@ fn execute_simulation(
|
||||
state
|
||||
};
|
||||
|
||||
// Seed control-actuator slots at their nominal value (the physical seed only
|
||||
// fills edge/component states, leaving control unknowns at 0.0 which is a poor
|
||||
// start for e.g. an f_m mass-flow factor whose nominal is 1.0).
|
||||
// Seed embedding / control unknowns at their start/initial (physical seed
|
||||
// leaves them at 0.0 — a poor start for Z-factors whose nominal is ~0.3–1).
|
||||
let mut initial_state = initial_state;
|
||||
for emb in &config.embeddings {
|
||||
let unknown_id = entropyk_solver::inverse::BoundedVariableId::new(saturated_actuator_id(
|
||||
&emb.unknown.component,
|
||||
&emb.unknown.factor,
|
||||
));
|
||||
if let Some(u_idx) = system.control_variable_state_index(&unknown_id) {
|
||||
if u_idx < initial_state.len() {
|
||||
initial_state[u_idx] = emb.unknown.start;
|
||||
}
|
||||
}
|
||||
}
|
||||
for control in &config.controls {
|
||||
let actuator_id = entropyk_solver::inverse::BoundedVariableId::new(saturated_actuator_id(
|
||||
&control.actuator.component,
|
||||
@@ -1571,6 +1596,7 @@ fn execute_simulation(
|
||||
let solve_time_budget = (config.solver.timeout_ms > 0)
|
||||
.then(|| std::time::Duration::from_millis(config.solver.timeout_ms));
|
||||
let needs_guarded_newton = !config.controls.is_empty()
|
||||
|| !config.embeddings.is_empty()
|
||||
|| config.circuits.iter().any(|c| {
|
||||
c.enabled
|
||||
&& c.components.iter().any(|comp| {
|
||||
@@ -3085,29 +3111,33 @@ fn bphx_calib_from_params(
|
||||
.or_else(|| params.get("f_ua"))
|
||||
.and_then(|v| v.as_f64());
|
||||
|
||||
// Precedence (DoF-safe): live `z_ua` wins over absolute `ua`.
|
||||
// Baking `ua` into a frozen Calib factor while an embedding frees `z_ua`
|
||||
// zeros ∂Q/∂z_ua → singular Jacobian. Modelica-style: modifier owns the unknown.
|
||||
if config_ua.is_some() && explicit_z_ua.is_some() {
|
||||
tracing::warn!(
|
||||
"BphxExchanger: both 'ua' and 'z_ua' provided — 'ua' takes precedence, 'z_ua' ignored"
|
||||
"BphxExchanger: both 'ua' and 'z_ua' provided — 'z_ua' takes precedence, 'ua' ignored"
|
||||
);
|
||||
}
|
||||
|
||||
let z_ua = match config_ua {
|
||||
Some(u) => {
|
||||
if u < 0.0 {
|
||||
return Err(CliError::Config(format!(
|
||||
"BphxExchanger: ua must be >= 0 (got {:.2} W/K)",
|
||||
u
|
||||
)));
|
||||
}
|
||||
if ua_nominal > 0.0 {
|
||||
u / ua_nominal
|
||||
} else {
|
||||
return Err(CliError::Config(
|
||||
"BphxExchanger: ua_nominal is zero — cannot compute z_ua from explicit 'ua' override. Check geometry parameters.".into(),
|
||||
));
|
||||
}
|
||||
let z_ua = if let Some(z) = explicit_z_ua {
|
||||
z
|
||||
} else if let Some(u) = config_ua {
|
||||
if u < 0.0 {
|
||||
return Err(CliError::Config(format!(
|
||||
"BphxExchanger: ua must be >= 0 (got {:.2} W/K)",
|
||||
u
|
||||
)));
|
||||
}
|
||||
None => explicit_z_ua.unwrap_or(1.0),
|
||||
if ua_nominal > 0.0 {
|
||||
u / ua_nominal
|
||||
} else {
|
||||
return Err(CliError::Config(
|
||||
"BphxExchanger: ua_nominal is zero — cannot compute z_ua from explicit 'ua' override. Check geometry parameters.".into(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
|
||||
if z_ua <= 0.0 {
|
||||
@@ -3137,6 +3167,7 @@ fn bphx_calib_from_params(
|
||||
z_ua,
|
||||
z_power: 1.0,
|
||||
z_etav: 1.0,
|
||||
f_w: 1.0,
|
||||
calibration_source: None,
|
||||
})
|
||||
}
|
||||
@@ -3229,15 +3260,11 @@ fn parse_component_output(
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Builds a bounded actuator + saturated controller from a control config.
|
||||
/// Plain inverse embedding for z-factor calibration controls (calibration
|
||||
/// redesign, WS-2): one `Constraint` (measured − target) + one `BoundedVariable`
|
||||
/// (the z-factor), linked 1:1. The bounded var id reuses the
|
||||
/// `{component}__{z_factor}` convention so `finalize()` wires it to the
|
||||
/// component's matching `CalibIndices` slot (system.rs).
|
||||
fn build_plain_z_embedding(
|
||||
control: &crate::config::ControlConfig,
|
||||
factor: &str,
|
||||
/// Modelica Z-factor embedding: one `Constraint` (output − value) + one
|
||||
/// `BoundedVariable` (the free Z-factor). Bounded var id uses
|
||||
/// `{component}__{factor}` so `finalize()` wires `CalibIndices`.
|
||||
fn build_model_embedding(
|
||||
emb: &crate::config::EmbeddingConfig,
|
||||
) -> Result<
|
||||
(
|
||||
entropyk_solver::inverse::Constraint,
|
||||
@@ -3248,23 +3275,29 @@ fn build_plain_z_embedding(
|
||||
> {
|
||||
use entropyk_solver::inverse::{BoundedVariable, BoundedVariableId, Constraint, ConstraintId};
|
||||
|
||||
let output = parse_component_output(&control.measure.component, &control.measure.output)?;
|
||||
let actuator_id =
|
||||
BoundedVariableId::new(saturated_actuator_id(&control.actuator.component, factor));
|
||||
let factor = emb.unknown.factor.trim();
|
||||
if entropyk_core::normalize_factor_name(factor).is_none() {
|
||||
return Err(format!(
|
||||
"unknown Z-factor '{factor}' (expected z_ua, z_dp, z_flow, z_power, z_etav, f_w, …)"
|
||||
));
|
||||
}
|
||||
let output = parse_component_output(&emb.equation.component, &emb.equation.output)?;
|
||||
let unknown_id =
|
||||
BoundedVariableId::new(saturated_actuator_id(&emb.unknown.component, factor));
|
||||
let bounded_var = BoundedVariable::with_component(
|
||||
actuator_id.clone(),
|
||||
&control.actuator.component,
|
||||
control.actuator.initial,
|
||||
control.actuator.min,
|
||||
control.actuator.max,
|
||||
unknown_id.clone(),
|
||||
&emb.unknown.component,
|
||||
emb.unknown.start,
|
||||
emb.unknown.min,
|
||||
emb.unknown.max,
|
||||
)
|
||||
.map_err(|e| format!("invalid actuator bounds: {e:?}"))?;
|
||||
.map_err(|e| format!("invalid unknown bounds: {e:?}"))?;
|
||||
let constraint = Constraint::new(
|
||||
ConstraintId::new(control.id.clone()),
|
||||
ConstraintId::new(emb.id.clone()),
|
||||
output,
|
||||
control.target,
|
||||
emb.equation.value,
|
||||
);
|
||||
Ok((constraint, bounded_var, actuator_id))
|
||||
Ok((constraint, bounded_var, unknown_id))
|
||||
}
|
||||
|
||||
fn build_saturated_control(
|
||||
@@ -3288,13 +3321,14 @@ fn build_saturated_control(
|
||||
}
|
||||
|
||||
let factor = control.actuator.factor.trim();
|
||||
if entropyk_core::normalize_factor_name(factor).is_none()
|
||||
&& factor != "injection"
|
||||
&& factor != "opening"
|
||||
{
|
||||
if entropyk_core::normalize_factor_name(factor).is_some() {
|
||||
return Err(format!(
|
||||
"unknown actuator factor '{factor}' (expected z_flow, z_dp, z_ua, z_power, z_etav, \
|
||||
injection, opening — legacy f_* and BOLT Z_* names accepted)"
|
||||
"factor '{factor}' is a Z-factor — use embeddings[], not SaturatedController"
|
||||
));
|
||||
}
|
||||
if factor != "injection" && factor != "opening" {
|
||||
return Err(format!(
|
||||
"unknown regulation actuator '{factor}' (expected opening, injection)"
|
||||
));
|
||||
}
|
||||
|
||||
@@ -4060,6 +4094,18 @@ fn create_component(
|
||||
.with_refrigerant(&refrigerant)
|
||||
.with_fluid_backend(Arc::clone(&backend));
|
||||
|
||||
// Energy-retention factor f_w: fraction of shaft work kept in the
|
||||
// refrigerant. h_dis = h_suc + f_w·Δh_is/η_is.
|
||||
// Default 1 = adiabatic; 0.98 ≈ 2% shell loss; 0 = all lost.
|
||||
if let Some(f_w) = params.get("f_w").and_then(|v| v.as_f64()).or_else(|| {
|
||||
params
|
||||
.get("fw")
|
||||
.and_then(|v| v.as_f64())
|
||||
.or_else(|| params.get("f_q").and_then(|v| v.as_f64()))
|
||||
}) {
|
||||
comp = comp.with_f_w(f_w);
|
||||
}
|
||||
|
||||
// Emergent-pressure mode: the compressor no longer pins the discharge
|
||||
// pressure to P_sat(t_cond_k). Normally ṁ is closed by the volumetric
|
||||
// displacement model. When a sibling EXV uses a *fixed* orifice opening,
|
||||
|
||||
Reference in New Issue
Block a user