chore: remove BMAD framework files and IDE configuration artifacts

Clean up unused BMAD workflow, agent, and command files across all IDE
configurations (.agent, .clinerules, .cursor, .gemini, .github, .kilocode,
.opencode) and internal module files (_bmad/bmb, _bmad/bmm).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Sepehr
2026-04-25 15:01:09 +02:00
parent 891c4ba436
commit ab5dc7e568
3006 changed files with 279068 additions and 59151 deletions

View File

@@ -1,6 +1,10 @@
use std::collections::HashMap;
use thiserror::Error;
use entropyk_core::{CircuitId, ThermalConductance};
use entropyk_solver::inverse::{BoundedVariable, BoundedVariableId, Constraint, ConstraintId};
use entropyk_solver::{AddEdgeError, ThermalCoupling, TopologyError};
use crate::ThermoError;
/// Error type for system builder operations.
@@ -25,6 +29,49 @@ pub enum SystemBuilderError {
reason: String,
},
/// Self-loop edge is not allowed: a component cannot connect to itself.
#[error("Self-loop edge not allowed: component '{component}' cannot connect to itself")]
SelfLoopEdge {
/// Name of the component.
component: String,
},
/// Cross-circuit edge is not allowed: flow edges connect only nodes within the same circuit.
#[error("Cross-circuit edge not allowed: '{from}' and '{to}' are in different circuits")]
CrossCircuitEdge {
/// Name of the source component.
from: String,
/// Name of the target component.
to: String,
},
/// Too many circuits: circuit id exceeds the allowed range (0..=4).
#[error("Too many circuits: requested circuit {requested}, maximum is 5 (0..=4)")]
TooManyCircuits {
/// The requested circuit ID that exceeded the limit.
requested: u16,
},
/// Port name not found on the specified component.
#[error("Port '{port_name}' not found on component '{component}'")]
PortNotFound {
/// The component name.
component: String,
/// The port name that was not found.
port_name: String,
},
/// Port validation failed (fluid, pressure, or enthalpy mismatch).
#[error("Port validation failed from '{from}' to '{to}': {reason}")]
PortValidationFailed {
/// Source component name.
from: String,
/// Target component name.
to: String,
/// Reason for validation failure.
reason: String,
},
/// The system must be finalized before this operation.
#[error("System must be finalized before solving")]
NotFinalized,
@@ -32,6 +79,57 @@ pub enum SystemBuilderError {
/// Cannot build a system with no components.
#[error("Cannot build an empty system")]
EmptySystem,
/// Constraint or inverse control operation failed (duplicate id, invalid reference, etc.).
#[error("Constraint/inverse control: {reason}")]
ConstraintFailed {
/// Reason from the solver layer.
reason: String,
},
/// Bounded variable operation failed (e.g. invalid bounds min >= max).
#[error("Bounded variable: {reason}")]
BoundedVariableFailed {
/// Reason from the solver layer.
reason: String,
},
/// Inverse control DoF or linking failed (constraint/control not found, already linked, over/under-constrained).
#[error("Inverse control DoF/link: {reason}")]
InverseControlDoF {
/// Reason from the solver layer.
reason: String,
},
/// Failed to add a component to the system (internal topology error).
#[error("Failed to add component '{name}': {reason}")]
ComponentAddFailed {
/// Component name.
name: String,
/// Reason from the solver layer.
reason: String,
},
/// Thermal coupling failed (invalid circuit, missing components, or solver rejection).
#[error("Thermal coupling failed: {reason}")]
ThermalCouplingFailed {
/// Reason from the solver layer.
reason: String,
},
/// Attempted to couple a circuit to itself.
#[error("Cannot couple circuit {circuit} to itself — use into_inner() + add_thermal_coupling() for economizer scenarios")]
SameCircuitCoupling {
/// The circuit that was coupled to itself.
circuit: u16,
},
/// Attempted to couple to a circuit with no components.
#[error("Circuit {circuit} has no components — cannot create thermal coupling")]
EmptyCircuitCoupling {
/// The circuit that has no components.
circuit: u16,
},
}
/// A builder for creating thermodynamic systems with a fluent API.
@@ -53,6 +151,7 @@ pub struct SystemBuilder {
system: entropyk_solver::System,
component_names: HashMap<String, petgraph::graph::NodeIndex>,
fluid_name: Option<String>,
thermal_couplings: Vec<ThermalCoupling>,
}
impl SystemBuilder {
@@ -62,6 +161,7 @@ impl SystemBuilder {
system: entropyk_solver::System::new(),
component_names: HashMap::new(),
fluid_name: None,
thermal_couplings: Vec::new(),
}
}
@@ -79,10 +179,11 @@ impl SystemBuilder {
self
}
/// Adds a named component to the system.
/// Adds a named component to the system (circuit 0).
///
/// The name is used for later reference when creating edges.
/// Returns an error if a component with the same name already exists.
/// For multi-circuit systems, use [`component_in_circuit`](Self::component_in_circuit).
///
/// # Arguments
///
@@ -90,16 +191,57 @@ impl SystemBuilder {
/// * `component` - The component to add
#[inline]
pub fn component(
self,
name: &str,
component: Box<dyn entropyk_components::Component>,
) -> Result<Self, SystemBuilderError> {
self.component_in_circuit(name, component, CircuitId::ZERO)
}
/// Adds a named component to a specific circuit.
///
/// The name is used for later reference when creating edges. Circuit id must be in 0..=4 (max 5 circuits).
/// Returns an error if the name already exists or the circuit id is invalid.
///
/// # Arguments
///
/// * `name` - A unique identifier for this component
/// * `component` - The component to add
/// * `circuit_id` - The circuit to add the component to (0..=4)
///
/// # Errors
///
/// Returns `SystemBuilderError::TooManyCircuits` if `circuit_id` is outside 0..=4.
#[inline]
pub fn component_in_circuit(
mut self,
name: &str,
component: Box<dyn entropyk_components::Component>,
circuit_id: CircuitId,
) -> Result<Self, SystemBuilderError> {
if self.component_names.contains_key(name) {
return Err(SystemBuilderError::ComponentExists(name.to_string()));
}
let idx = self.system.add_component(component);
let idx = self
.system
.add_component_to_circuit(component, circuit_id)
.map_err(|e| match e {
TopologyError::TooManyCircuits { requested } => {
SystemBuilderError::TooManyCircuits { requested }
}
other => SystemBuilderError::ComponentAddFailed {
name: name.to_string(),
reason: other.to_string(),
},
})?;
self.component_names.insert(name.to_string(), idx);
if !self.system.register_component_name(name, idx) {
self.component_names.remove(name);
return Err(SystemBuilderError::ComponentExists(format!(
"duplicate component name '{name}' already registered in solver"
)));
}
Ok(self)
}
@@ -116,7 +258,9 @@ impl SystemBuilder {
///
/// # Errors
///
/// Returns an error if either component name is not found.
/// Returns an error if either component name is not found, if the edge
/// would connect two different circuits (cross-circuit edges are not allowed),
/// or if `from` and `to` refer to the same component (self-loops are not allowed).
#[inline]
pub fn edge(mut self, from: &str, to: &str) -> Result<Self, SystemBuilderError> {
let from_idx = self
@@ -129,6 +273,21 @@ impl SystemBuilder {
.get(to)
.ok_or_else(|| SystemBuilderError::ComponentNotFound(to.to_string()))?;
if from_idx == to_idx {
return Err(SystemBuilderError::SelfLoopEdge {
component: from.to_string(),
});
}
let src_circuit = self.system.node_circuit(*from_idx);
let tgt_circuit = self.system.node_circuit(*to_idx);
if src_circuit != tgt_circuit {
return Err(SystemBuilderError::CrossCircuitEdge {
from: from.to_string(),
to: to.to_string(),
});
}
self.system
.add_edge(*from_idx, *to_idx)
.map_err(|e| SystemBuilderError::EdgeFailed {
@@ -140,6 +299,250 @@ impl SystemBuilder {
Ok(self)
}
/// Creates an edge between two named components with port validation.
///
/// Validates port compatibility (fluid, pressure, enthalpy continuity) using
/// the [`Component::resolve_port_name`](entropyk_components::Component::resolve_port_name)
/// method to map port names to indices, supporting both explicit
/// [`port_names()`](entropyk_components::Component::port_names) overrides and
/// convention-based fallback.
///
/// # Arguments
///
/// * `from` - Name of the source component
/// * `from_port` - Port name on source component (e.g., `"discharge"`, `"outlet"`)
/// * `to` - Name of the target component
/// * `to_port` - Port name on target component (e.g., `"inlet"`, `"suction"`)
///
/// # Errors
///
/// Returns an error if:
/// - Either component name is not found
/// - Port name is not recognized for the component
/// - Port validation fails (fluid, pressure, or enthalpy mismatch)
/// - Components are in different circuits
///
/// # Example
///
/// ```ignore
/// let system = SystemBuilder::new()
/// .component("comp", compressor)?
/// .component("cond", condenser)?
/// .edge_with_ports("comp", "discharge", "cond", "refrigerant_in")?
/// .build()?;
/// ```
#[inline]
pub fn edge_with_ports(
mut self,
from: &str,
from_port: &str,
to: &str,
to_port: &str,
) -> Result<Self, SystemBuilderError> {
let from_idx = self
.component_names
.get(from)
.ok_or_else(|| SystemBuilderError::ComponentNotFound(from.to_string()))?;
let to_idx = self
.component_names
.get(to)
.ok_or_else(|| SystemBuilderError::ComponentNotFound(to.to_string()))?;
if from_idx == to_idx {
return Err(SystemBuilderError::SelfLoopEdge {
component: from.to_string(),
});
}
let src_circuit = self.system.node_circuit(*from_idx);
let tgt_circuit = self.system.node_circuit(*to_idx);
if src_circuit != tgt_circuit {
return Err(SystemBuilderError::CrossCircuitEdge {
from: from.to_string(),
to: to.to_string(),
});
}
let from_component = self.system.component(*from_idx);
let from_port_idx = from_component
.resolve_port_name(from_port)
.map_err(|reason| SystemBuilderError::PortNotFound {
component: from.to_string(),
port_name: format!("{from_port}: {reason}"),
})?;
let to_component = self.system.component(*to_idx);
let to_port_idx = to_component.resolve_port_name(to_port).map_err(|reason| {
SystemBuilderError::PortNotFound {
component: to.to_string(),
port_name: format!("{to_port}: {reason}"),
}
})?;
self.system
.add_edge_with_ports(*from_idx, from_port_idx, *to_idx, to_port_idx)
.map_err(|e| match e {
AddEdgeError::Connection(conn_err) => SystemBuilderError::PortValidationFailed {
from: from.to_string(),
to: to.to_string(),
reason: conn_err.to_string(),
},
AddEdgeError::Topology(topo_err) => SystemBuilderError::EdgeFailed {
from: from.to_string(),
to: to.to_string(),
reason: topo_err.to_string(),
},
})?;
Ok(self)
}
/// Adds an output constraint for inverse control (e.g. superheat = 5K at a component).
///
/// The constraint's `component_id` (in [`ComponentOutput`]) should match a component name
/// added via [`component`](Self::component) or [`component_in_circuit`](Self::component_in_circuit).
/// Call [`link_constraint_to_control`](Self::link_constraint_to_control) to link this constraint
/// to a bounded control variable. After [`build`](Self::build), call
/// `system.validate_inverse_control_dof()` before solving to ensure the system is well-posed.
///
/// # Errors
///
/// Returns `SystemBuilderError::ConstraintFailed` if the solver rejects the constraint
/// (e.g. duplicate constraint id).
#[inline]
pub fn with_constraint(mut self, constraint: Constraint) -> Result<Self, SystemBuilderError> {
let component_id = constraint.output().component_id().to_string();
self.system.add_constraint(constraint).map_err(|e| {
let hint = if !self.component_names.contains_key(&component_id) {
format!("{} — component '{}' has not been added to the builder yet (call `component()` first)", e, component_id)
} else {
e.to_string()
};
SystemBuilderError::ConstraintFailed { reason: hint }
})?;
Ok(self)
}
/// Adds a bounded control variable for inverse control (e.g. valve position 0.01.0).
///
/// Link this variable to a constraint via [`link_constraint_to_control`](Self::link_constraint_to_control).
/// After [`build`](Self::build), call `system.validate_inverse_control_dof()` before solving.
///
/// # Errors
///
/// Returns `SystemBuilderError::BoundedVariableFailed` if bounds are invalid (e.g. min >= max).
#[inline]
pub fn with_bounded_variable(
mut self,
variable: BoundedVariable,
) -> Result<Self, SystemBuilderError> {
self.system.add_bounded_variable(variable).map_err(|e| {
SystemBuilderError::BoundedVariableFailed {
reason: e.to_string(),
}
})?;
Ok(self)
}
/// Links a constraint to a bounded control variable for one-shot inverse solving.
///
/// Both the constraint and the bounded variable must have been added previously with
/// [`with_constraint`](Self::with_constraint) and [`with_bounded_variable`](Self::with_bounded_variable).
/// Each constraint should be linked to exactly one control variable (and vice versa) for
/// a well-posed system. Call `system.validate_inverse_control_dof()` after [`build`](Self::build)
/// to check before solving.
///
/// # Errors
///
/// Returns `SystemBuilderError::InverseControlDoF` if the constraint or control is not found,
/// already linked, or the system is over/under-constrained.
#[inline]
pub fn link_constraint_to_control(
mut self,
constraint_id: &ConstraintId,
bounded_variable_id: &BoundedVariableId,
) -> Result<Self, SystemBuilderError> {
self.system
.link_constraint_to_control(constraint_id, bounded_variable_id)
.map_err(|e| SystemBuilderError::InverseControlDoF {
reason: e.to_string(),
})?;
Ok(self)
}
/// Adds a thermal coupling between two circuits (e.g. condenserevaporator HX link).
///
/// Both circuits must already contain at least one component (added via
/// [`component_in_circuit`](Self::component_in_circuit)). The UA value is
/// specified in kW/K and automatically converted to W/K internally.
///
/// The coupling is stored in the builder and applied to the system during
/// [`build`](Self::build).
///
/// # Arguments
///
/// * `circuit_a` - First circuit ID (hot side)
/// * `circuit_b` - Second circuit ID (cold side)
/// * `ua_kw_per_k` - Thermal conductance in kW/K (must be positive)
///
/// # Errors
///
/// - `SystemBuilderError::SameCircuitCoupling` if `circuit_a == circuit_b`.
/// - `SystemBuilderError::EmptyCircuitCoupling` if either circuit has no components.
/// - `SystemBuilderError::ThermalCouplingFailed` if UA is not positive.
pub fn thermal_coupling(
mut self,
circuit_a: u16,
circuit_b: u16,
ua_kw_per_k: f64,
) -> Result<Self, SystemBuilderError> {
// AC3: Reject same-circuit coupling
if circuit_a == circuit_b {
return Err(SystemBuilderError::SameCircuitCoupling {
circuit: circuit_a,
});
}
// AC5: Validate UA > 0
if ua_kw_per_k <= 0.0 {
return Err(SystemBuilderError::ThermalCouplingFailed {
reason: "UA must be positive".to_string(),
});
}
// AC2: Validate both circuits have components
if self
.system
.circuit_nodes(CircuitId(circuit_a))
.next()
.is_none()
{
return Err(SystemBuilderError::EmptyCircuitCoupling {
circuit: circuit_a,
});
}
if self
.system
.circuit_nodes(CircuitId(circuit_b))
.next()
.is_none()
{
return Err(SystemBuilderError::EmptyCircuitCoupling {
circuit: circuit_b,
});
}
// AC1 & AC5: Create coupling with kW→W conversion
let ua = ThermalConductance::from_kilowatts_per_kelvin(ua_kw_per_k);
let coupling =
ThermalCoupling::new(CircuitId(circuit_a), CircuitId(circuit_b), ua);
// Defer to build time
self.thermal_couplings.push(coupling);
Ok(self)
}
/// Gets the underlying system without finalizing.
///
/// This is useful when you need to perform additional operations
@@ -173,6 +576,9 @@ impl SystemBuilder {
/// Returns an error if:
/// - The system is empty (no components)
/// - Finalization fails (e.g., invalid topology)
/// - Constraint/control DoF validation fails — call
/// [`validate_inverse_control_dof()`](entropyk_solver::System::validate_inverse_control_dof)
/// after building to check that constraint and control degrees of freedom are balanced.
pub fn build(self) -> Result<entropyk_solver::System, ThermoError> {
if self.component_names.is_empty() {
return Err(ThermoError::Builder(SystemBuilderError::EmptySystem));
@@ -181,6 +587,15 @@ impl SystemBuilder {
let mut system = self.system;
system.finalize()?;
// Apply deferred thermal couplings
for coupling in self.thermal_couplings {
system.add_thermal_coupling(coupling).map_err(|e| {
ThermoError::Builder(SystemBuilderError::ThermalCouplingFailed {
reason: e.to_string(),
})
})?;
}
Ok(system)
}
@@ -308,4 +723,462 @@ mod tests {
let builder = SystemBuilder::default();
assert_eq!(builder.component_count(), 0);
}
#[test]
fn test_component_in_circuit_two_circuits_build() {
use entropyk_core::CircuitId;
let system = SystemBuilder::new()
.component_in_circuit("a", Box::new(MockComponent { n_eqs: 1 }), CircuitId::ZERO)
.unwrap()
.component_in_circuit("b", Box::new(MockComponent { n_eqs: 1 }), CircuitId::ZERO)
.unwrap()
.component_in_circuit("c", Box::new(MockComponent { n_eqs: 1 }), CircuitId(1))
.unwrap()
.component_in_circuit("d", Box::new(MockComponent { n_eqs: 1 }), CircuitId(1))
.unwrap()
.edge("a", "b")
.unwrap()
.edge("c", "d")
.unwrap()
.build()
.expect("build should succeed");
assert_eq!(system.circuit_count(), 2);
assert_eq!(system.circuit_nodes(CircuitId::ZERO).count(), 2);
assert_eq!(system.circuit_nodes(CircuitId(1)).count(), 2);
assert_eq!(system.node_count(), 4);
assert_eq!(system.edge_count(), 2);
}
#[test]
fn test_edge_cross_circuit_returns_error() {
use entropyk_core::CircuitId;
let result = SystemBuilder::new()
.component_in_circuit("a", Box::new(MockComponent { n_eqs: 1 }), CircuitId::ZERO)
.unwrap()
.component_in_circuit("b", Box::new(MockComponent { n_eqs: 1 }), CircuitId(1))
.unwrap()
.edge("a", "b");
assert!(result.is_err());
if let Err(SystemBuilderError::CrossCircuitEdge { from, to }) = result {
assert_eq!(from, "a");
assert_eq!(to, "b");
} else {
panic!("Expected CrossCircuitEdge error");
}
}
#[test]
fn test_invalid_circuit_id_returns_error() {
use entropyk_core::CircuitId;
let result = SystemBuilder::new().component_in_circuit(
"a",
Box::new(MockComponent { n_eqs: 1 }),
CircuitId(5),
);
assert!(result.is_err());
if let Err(SystemBuilderError::TooManyCircuits { requested }) = result {
assert_eq!(requested, 5);
} else {
panic!("Expected TooManyCircuits error");
}
}
#[test]
fn test_edge_with_ports_missing_component() {
let result = SystemBuilder::new()
.component("a", Box::new(MockComponent { n_eqs: 1 }))
.unwrap()
.edge_with_ports("nonexistent", "outlet", "a", "inlet");
assert!(result.is_err());
if let Err(SystemBuilderError::ComponentNotFound(name)) = result {
assert_eq!(name, "nonexistent");
} else {
panic!("Expected ComponentNotFound error");
}
}
#[test]
fn test_edge_with_ports_missing_target() {
let result = SystemBuilder::new()
.component("a", Box::new(MockComponent { n_eqs: 1 }))
.unwrap()
.edge_with_ports("a", "outlet", "nonexistent", "inlet");
assert!(result.is_err());
if let Err(SystemBuilderError::ComponentNotFound(name)) = result {
assert_eq!(name, "nonexistent");
} else {
panic!("Expected ComponentNotFound error");
}
}
#[test]
fn test_edge_with_ports_cross_circuit_error() {
use entropyk_core::CircuitId;
let result = SystemBuilder::new()
.component_in_circuit("a", Box::new(MockComponent { n_eqs: 1 }), CircuitId::ZERO)
.unwrap()
.component_in_circuit("b", Box::new(MockComponent { n_eqs: 1 }), CircuitId(1))
.unwrap()
.edge_with_ports("a", "outlet", "b", "inlet");
assert!(result.is_err());
if let Err(SystemBuilderError::CrossCircuitEdge { from, to }) = result {
assert_eq!(from, "a");
assert_eq!(to, "b");
} else {
panic!("Expected CrossCircuitEdge error");
}
}
#[test]
fn test_edge_with_ports_convention_based_names() {
let builder = SystemBuilder::new()
.component("a", Box::new(MockComponent { n_eqs: 2 }))
.unwrap()
.component("b", Box::new(MockComponent { n_eqs: 2 }))
.unwrap()
.edge_with_ports("a", "outlet", "b", "inlet")
.expect("edge_with_ports should resolve convention-based port names");
assert_eq!(builder.edge_count(), 1);
}
#[test]
fn test_edge_with_ports_unknown_port_name() {
let result = SystemBuilder::new()
.component("a", Box::new(MockComponent { n_eqs: 2 }))
.unwrap()
.component("b", Box::new(MockComponent { n_eqs: 2 }))
.unwrap()
.edge_with_ports("a", "totally_invalid_port", "b", "inlet");
assert!(result.is_err());
if let Err(SystemBuilderError::PortNotFound {
component,
port_name,
}) = result
{
assert_eq!(component, "a");
assert!(port_name.starts_with("totally_invalid_port"), "port_name should start with the port name, got: {port_name}");
} else {
panic!("Expected PortNotFound error");
}
}
#[test]
fn test_with_constraint_success() {
use entropyk_solver::inverse::{ComponentOutput, Constraint, ConstraintId};
let constraint = Constraint::new(
ConstraintId::new("sh"),
ComponentOutput::Superheat {
component_id: "evap".to_string(),
},
5.0,
);
let builder = SystemBuilder::new()
.component("evap", Box::new(MockComponent { n_eqs: 2 }))
.unwrap()
.with_constraint(constraint)
.expect("with_constraint should succeed");
assert_eq!(builder.component_count(), 1);
}
#[test]
fn test_with_constraint_duplicate_id_error() {
use entropyk_solver::inverse::{ComponentOutput, Constraint, ConstraintId};
let c1 = Constraint::new(
ConstraintId::new("sh"),
ComponentOutput::Superheat {
component_id: "evap".to_string(),
},
5.0,
);
let c2 = Constraint::new(
ConstraintId::new("sh"),
ComponentOutput::Subcooling {
component_id: "cond".to_string(),
},
3.0,
);
let result = SystemBuilder::new()
.component("evap", Box::new(MockComponent { n_eqs: 1 }))
.unwrap()
.with_constraint(c1)
.unwrap()
.with_constraint(c2);
assert!(result.is_err());
if let Err(SystemBuilderError::ConstraintFailed { reason }) = result {
assert!(reason.contains("Duplicate") || reason.contains("sh"));
} else {
panic!("Expected ConstraintFailed error");
}
}
#[test]
fn test_with_bounded_variable_success() {
use entropyk_solver::inverse::{BoundedVariable, BoundedVariableId};
let var = BoundedVariable::new(BoundedVariableId::new("valve"), 0.5, 0.0, 1.0).unwrap();
let builder = SystemBuilder::new()
.component("a", Box::new(MockComponent { n_eqs: 1 }))
.unwrap()
.with_bounded_variable(var)
.expect("with_bounded_variable should succeed");
assert_eq!(builder.component_count(), 1);
}
#[test]
fn test_with_bounded_variable_duplicate_id_error() {
use entropyk_solver::inverse::{BoundedVariable, BoundedVariableId};
let var1 = BoundedVariable::new(BoundedVariableId::new("valve"), 0.5, 0.0, 1.0).unwrap();
let var2 = BoundedVariable::new(BoundedVariableId::new("valve"), 0.6, 0.0, 1.0).unwrap();
let result = SystemBuilder::new()
.component("a", Box::new(MockComponent { n_eqs: 1 }))
.unwrap()
.with_bounded_variable(var1)
.unwrap()
.with_bounded_variable(var2);
assert!(result.is_err());
if let Err(SystemBuilderError::BoundedVariableFailed { reason }) = result {
assert!(reason.contains("Duplicate") || reason.contains("valve"));
} else {
panic!("Expected BoundedVariableFailed error");
}
}
#[test]
fn test_link_constraint_to_control_success() {
use entropyk_solver::inverse::{
BoundedVariable, BoundedVariableId, ComponentOutput, Constraint, ConstraintId,
};
let constraint = Constraint::new(
ConstraintId::new("sh"),
ComponentOutput::Superheat {
component_id: "evap".to_string(),
},
5.0,
);
let var = BoundedVariable::new(BoundedVariableId::new("valve"), 0.5, 0.0, 1.0).unwrap();
let builder = SystemBuilder::new()
.component("evap", Box::new(MockComponent { n_eqs: 2 }))
.unwrap()
.with_constraint(constraint)
.unwrap()
.with_bounded_variable(var)
.unwrap()
.link_constraint_to_control(&ConstraintId::new("sh"), &BoundedVariableId::new("valve"))
.expect("link_constraint_to_control should succeed");
assert_eq!(builder.component_count(), 1);
}
#[test]
fn test_link_constraint_to_control_nonexistent_constraint_error() {
use entropyk_solver::inverse::{BoundedVariable, BoundedVariableId, ConstraintId};
let var = BoundedVariable::new(BoundedVariableId::new("valve"), 0.5, 0.0, 1.0).unwrap();
let result = SystemBuilder::new()
.component("a", Box::new(MockComponent { n_eqs: 1 }))
.unwrap()
.with_bounded_variable(var)
.unwrap()
.link_constraint_to_control(
&ConstraintId::new("nonexistent_constraint"),
&BoundedVariableId::new("valve"),
);
assert!(result.is_err());
if let Err(SystemBuilderError::InverseControlDoF { reason }) = result {
assert!(reason.contains("not found") || reason.contains("Nonexistent"));
} else {
panic!("Expected InverseControlDoF error");
}
}
#[test]
fn test_link_constraint_to_control_nonexistent_bounded_variable_error() {
use entropyk_solver::inverse::{
BoundedVariableId, ComponentOutput, Constraint, ConstraintId,
};
let constraint = Constraint::new(
ConstraintId::new("sh"),
ComponentOutput::Superheat {
component_id: "evap".to_string(),
},
5.0,
);
let result = SystemBuilder::new()
.component("evap", Box::new(MockComponent { n_eqs: 2 }))
.unwrap()
.with_constraint(constraint)
.unwrap()
.link_constraint_to_control(
&ConstraintId::new("sh"),
&BoundedVariableId::new("nonexistent_control"),
);
assert!(result.is_err());
if let Err(SystemBuilderError::InverseControlDoF { reason }) = result {
assert!(reason.contains("not found") || reason.contains("Nonexistent"));
} else {
panic!("Expected InverseControlDoF error");
}
}
#[test]
fn test_thermal_coupling_success() {
// AC1: thermal_coupling(circuit_a, circuit_b, ua_kw_per_k) with valid circuits
let builder = SystemBuilder::new()
.component_in_circuit("a", Box::new(MockComponent { n_eqs: 1 }), CircuitId::ZERO)
.unwrap()
.component_in_circuit("b", Box::new(MockComponent { n_eqs: 1 }), CircuitId(1))
.unwrap()
.thermal_coupling(0, 1, 5.0)
.expect("thermal_coupling should succeed");
assert_eq!(builder.component_count(), 2);
}
#[test]
fn test_thermal_coupling_same_circuit_rejected() {
// AC3: SameCircuitCoupling when circuit_a == circuit_b
let result = SystemBuilder::new()
.component_in_circuit("a", Box::new(MockComponent { n_eqs: 1 }), CircuitId::ZERO)
.unwrap()
.thermal_coupling(0, 0, 5.0);
assert!(result.is_err());
if let Err(SystemBuilderError::SameCircuitCoupling { circuit }) = result {
assert_eq!(circuit, 0);
} else {
panic!("Expected SameCircuitCoupling error");
}
}
#[test]
fn test_thermal_coupling_empty_circuit_rejected() {
// AC2: EmptyCircuitCoupling when circuit has no components
let result = SystemBuilder::new()
.component_in_circuit("a", Box::new(MockComponent { n_eqs: 1 }), CircuitId::ZERO)
.unwrap()
.thermal_coupling(0, 2, 5.0);
assert!(result.is_err());
if let Err(SystemBuilderError::EmptyCircuitCoupling { circuit }) = result {
assert_eq!(circuit, 2);
} else {
panic!("Expected EmptyCircuitCoupling error");
}
}
#[test]
fn test_thermal_coupling_negative_ua_rejected() {
let result = SystemBuilder::new()
.component_in_circuit("a", Box::new(MockComponent { n_eqs: 1 }), CircuitId::ZERO)
.unwrap()
.component_in_circuit("b", Box::new(MockComponent { n_eqs: 1 }), CircuitId(1))
.unwrap()
.thermal_coupling(0, 1, -1.0);
assert!(result.is_err());
if let Err(SystemBuilderError::ThermalCouplingFailed { reason }) = result {
assert!(reason.contains("positive"));
} else {
panic!("Expected ThermalCouplingFailed error for negative UA");
}
}
#[test]
fn test_thermal_coupling_built_system_has_coupling() {
// AC4: Built system contains the coupling
let system = SystemBuilder::new()
.component_in_circuit("a", Box::new(MockComponent { n_eqs: 1 }), CircuitId::ZERO)
.unwrap()
.component_in_circuit("b", Box::new(MockComponent { n_eqs: 1 }), CircuitId::ZERO)
.unwrap()
.component_in_circuit("c", Box::new(MockComponent { n_eqs: 1 }), CircuitId(1))
.unwrap()
.component_in_circuit("d", Box::new(MockComponent { n_eqs: 1 }), CircuitId(1))
.unwrap()
.edge("a", "b")
.unwrap()
.edge("c", "d")
.unwrap()
.thermal_coupling(0, 1, 5.0)
.unwrap()
.build()
.expect("build should succeed");
assert_eq!(system.thermal_coupling_count(), 1);
let coupling = system.get_thermal_coupling(0).expect("coupling should exist");
// AC4 & AC5: 5.0 kW/K = 5000 W/K
approx::assert_relative_eq!(
coupling.ua.to_watts_per_kelvin(),
5000.0,
epsilon = 1e-10
);
}
#[test]
fn test_thermal_coupling_kw_to_w_conversion() {
// AC5: kW→W conversion (2.5 kW/K → 2500 W/K)
let system = SystemBuilder::new()
.component_in_circuit("a", Box::new(MockComponent { n_eqs: 1 }), CircuitId::ZERO)
.unwrap()
.component_in_circuit("b", Box::new(MockComponent { n_eqs: 1 }), CircuitId::ZERO)
.unwrap()
.component_in_circuit("c", Box::new(MockComponent { n_eqs: 1 }), CircuitId(1))
.unwrap()
.component_in_circuit("d", Box::new(MockComponent { n_eqs: 1 }), CircuitId(1))
.unwrap()
.edge("a", "b")
.unwrap()
.edge("c", "d")
.unwrap()
.thermal_coupling(0, 1, 2.5)
.unwrap()
.build()
.expect("build should succeed");
let coupling = system.get_thermal_coupling(0).expect("coupling should exist");
approx::assert_relative_eq!(
coupling.ua.to_watts_per_kelvin(),
2500.0,
epsilon = 1e-10
);
}
#[test]
fn test_thermal_coupling_empty_first_circuit_rejected() {
// AC2: First circuit empty
let result = SystemBuilder::new()
.component_in_circuit("a", Box::new(MockComponent { n_eqs: 1 }), CircuitId(1))
.unwrap()
.thermal_coupling(0, 1, 5.0);
assert!(result.is_err());
if let Err(SystemBuilderError::EmptyCircuitCoupling { circuit }) = result {
assert_eq!(circuit, 0);
} else {
panic!("Expected EmptyCircuitCoupling error for circuit 0");
}
}
}