14 KiB
Story 3.4: Thermal Coupling Between Circuits
Status: done
Story
As a systems engineer, I want thermal coupling with circular dependency detection, so that the solver knows whether to solve simultaneously or sequentially.
Acceptance Criteria
-
Heat Transfer Linking (AC: #1)
- Given two circuits with a heat exchanger coupling them
- When defining thermal coupling
- Then heat transfer equations link the circuits
- And coupling is represented as additional residuals
-
Energy Conservation (AC: #2)
- Given a thermal coupling between two circuits
- When computing heat transfer
- Then Q_hot = -Q_cold (energy conserved)
- And sign convention is documented (positive = heat INTO circuit)
-
Circular Dependency Detection (AC: #3)
- Given circuits with mutual thermal coupling (A heats B, B heats A)
- When analyzing coupling topology
- Then circular dependencies are detected
- And solver is informed to solve simultaneously (not sequentially)
-
Coupling Residuals (AC: #4)
- Given a defined thermal coupling
- When solver assembles residuals
- Then coupling contributes additional residual equations
- And Jacobian includes coupling derivatives
Tasks / Subtasks
- Define ThermalCoupling struct (AC: #1, #2)
- Create
ThermalCouplingwith: hot_circuit_id, cold_circuit_id, ua (thermal conductance) - Add optional efficiency factor (default 1.0)
- Document sign convention: Q > 0 means heat INTO cold_circuit
- Create
- Add coupling storage to System (AC: #1)
- Add
thermal_couplings: Vec<ThermalCoupling>to System - Add
add_thermal_coupling(coupling: ThermalCoupling) -> Result<usize, TopologyError> - Validate circuit_ids exist before adding
- Add
- Implement energy conservation (AC: #2)
- Method
compute_coupling_heat(coupling, hot_state, cold_state) -> HeatTransfer - Formula: Q = UA * (T_hot - T_cold) where T from respective circuit states
- Returns positive Q for heat into cold circuit
- Method
- Implement circular dependency detection (AC: #3)
- Build coupling graph (nodes = circuits, edges = couplings)
- Detect cycles using petgraph::algo::is_cyclic
- Add
has_circular_dependencies() -> bool - Add
coupling_groups() -> Vec<Vec<CircuitId>>returning groups that must solve simultaneously
- Expose coupling residuals for solver (AC: #4)
- Add
coupling_residuals(state: &SystemState) -> Vec<f64> - Residual: r = Q_actual - Q_expected (heat balance at coupling point)
- Add
coupling_jacobian_entries()returning (row, col, partial_derivative) tuples
- Add
- Tests
- Test: add_thermal_coupling valid, retrieves correctly
- Test: add_thermal_coupling with invalid circuit_id fails
- Test: compute_coupling_heat positive when T_hot > T_cold
- Test: circular dependency detection (A→B→A)
- Test: no circular dependency (A→B, B→C)
- Test: coupling_groups returns correct groupings
- Test: energy conservation Q_hot = -Q_cold
Dev Notes
Epic Context
Epic 3: System Topology (Graph) — Enable component assembly via Ports and manage multi-circuits with thermal coupling. FR11 (thermal coupling between circuits) maps to crates/solver.
Story Dependencies:
- Story 3.1 (System graph structure) — done
- Story 3.2 (Port compatibility validation) — done
- Story 3.3 (Multi-circuit machine definition) — done; provides CircuitId, node_to_circuit, circuit accessors
- Story 3.5 (Zero-flow) — independent
Architecture Context
Technical Stack:
- Rust, petgraph 0.6.x (already has is_cyclic, cycle detection), thiserror, entropyk-core, entropyk-components
- No new external dependencies
Code Structure:
crates/solver/src/system.rs— primary modification site (add thermal_couplings, add_thermal_coupling)crates/solver/src/coupling.rs— NEW file for ThermalCoupling, coupling graph, dependency detectioncrates/solver/src/error.rs— add TopologyError::InvalidCircuitForCoupling
Relevant Architecture Sections:
- System Topology (architecture line 797): FR9–FR13 in solver/system.rs
- FR11: System supports connections between circuits (thermal coupling)
- Pre-allocation (architecture line 239): Coupling metadata built at finalize time, not in solver hot path
API Patterns:
add_thermal_coupling(coupling: ThermalCoupling) -> Result<usize, TopologyError>— returns coupling index- Coupling does NOT create flow edges (cross-circuit flow prohibited per Story 3.3)
- Coupling represents heat transfer ONLY — separate from fluid flow
Performance Requirements:
- Coupling graph built once at finalize/build time
- Circular dependency detection at finalize time
- coupling_residuals() called in solver loop — must be fast (no allocation)
Developer Context
Existing Implementation (from Story 3.3):
CircuitId(pub u8)with valid range 0..=4node_to_circuit: HashMap<NodeIndex, CircuitId>circuit_count(),circuit_nodes(),circuit_edges()- Cross-circuit flow edges rejected (TopologyError::CrossCircuitConnection)
- Thermal coupling is the ONLY way to connect circuits
Design Decisions:
-
Coupling Representation:
ThermalCouplingstored in System, separate from graph edges- Coupling does NOT create petgraph edges (would confuse flow traversal)
- Coupling indexed by usize for O(1) access
-
Circular Dependency Graph:
- Build temporary petgraph::Graph<CircuitId, ()> for cycle detection
- Nodes = CircuitIds present in any coupling
- Directed edge from hot_circuit → cold_circuit (heat flows hot to cold)
- Use
petgraph::algo::is_cyclic::is_cyclic_directed()
-
Coupling Groups:
- Strongly connected components (SCC) in coupling graph
- Circuits in same SCC must solve simultaneously
- Circuits in different SCCs can solve sequentially (topological order)
-
Residual Convention:
- Coupling residual:
r = Q_model - Q_couplingwhere Q_model is from circuit state - Jacobian includes ∂r/∂T_hot and ∂r/∂T_cold
- Solver treats coupling residuals like component residuals
- Coupling residual:
Technical Requirements
ThermalCoupling Struct:
/// Thermal coupling between two circuits via heat exchanger.
/// Heat flows from hot_circuit to cold_circuit.
#[derive(Debug, Clone)]
pub struct ThermalCoupling {
pub hot_circuit: CircuitId,
pub cold_circuit: CircuitId,
pub ua: ThermalConductance, // W/K
}
/// Sign convention: Q > 0 means heat INTO cold_circuit (out of hot_circuit).
pub fn compute_coupling_heat(
coupling: &ThermalCoupling,
t_hot: Temperature,
t_cold: Temperature,
) -> HeatTransfer {
HeatTransfer(coupling.ua.0 * (t_hot.0 - t_cold.0))
}
Error Types:
TopologyError::InvalidCircuitForCoupling { circuit_id: CircuitId }— circuit doesn't exist
Coupling Graph (internal):
fn build_coupling_graph(couplings: &[ThermalCoupling]) -> petgraph::Graph<CircuitId, ()> {
let mut graph = petgraph::Graph::new();
// Add nodes for each unique circuit in couplings
// Add directed edge: hot_circuit -> cold_circuit
graph
}
Architecture Compliance
- NewType pattern: Use
ThermalConductance(pub f64),HeatTransfer(pub f64)for clarity - tracing for circular dependency warnings:
tracing::warn!("Circular thermal coupling detected, simultaneous solving required") - Result<T, E> — no unwrap/expect in production
- #![deny(warnings)] — all crates
Library/Framework Requirements
- petgraph: Already in dependencies; use
is_cyclic_directed,kosaraju_scc - entropyk_core: Temperature, HeatTransfer (add ThermalConductance if needed)
- thiserror: TopologyError extension
File Structure Requirements
Modified files:
crates/solver/src/system.rs— add thermal_couplings, add_thermal_coupling, finalize validationcrates/solver/src/error.rs— add TopologyError::InvalidCircuitForCouplingcrates/solver/src/lib.rs— re-export ThermalCoupling
New files:
crates/solver/src/coupling.rs— ThermalCoupling, compute_coupling_heat, coupling graph utilities
Tests:
- Add to
crates/solver/src/coupling.rs(inline#[cfg(test)]module) - Extend
tests/multi_circuit.rswith thermal coupling integration test
Testing Requirements
Unit tests (coupling.rs):
test_thermal_coupling_creation— valid coupling, correct fieldstest_compute_coupling_heat_positive— T_hot > T_cold → Q > 0test_compute_coupling_heat_zero— T_hot == T_cold → Q = 0test_compute_coupling_heat_negative— T_hot < T_cold → Q < 0 (reverse flow)test_circular_dependency_detection— A→B, B→A → cyclictest_no_circular_dependency— A→B, B→C → not cyclictest_coupling_groups_scc— A↔B, C→D → groups A,B], [C], [D or similar
Integration tests (system.rs or multi_circuit.rs):
test_add_thermal_coupling_valid— 2 circuits, add coupling, verify storedtest_add_thermal_coupling_invalid_circuit— coupling with CircuitId(99) → errortest_coupling_residuals_basic— 2 circuits with coupling, verify residual equation
Project Structure Notes
- Architecture specifies
crates/solver/src/system.rsfor FR9–FR13 — matches - Story 3.3 provides circuit foundation; 3.4 adds thermal coupling layer
- Story 4.x (Solver) will consume coupling_residuals() and coupling_groups()
- Coupling does NOT modify graph edges — flow edges remain same-circuit only
Previous Story Intelligence (3.3)
- CircuitId range 0..=4 validated at construction
circuit_count()returns number of distinct circuits with componentsadd_component_to_circuitassigns component to circuit- Cross-circuit flow edges rejected with TopologyError::CrossCircuitConnection
- 3.3 explicitly documented: "Story 3.4 will add thermal coupling (cross-circuit heat transfer)"
Previous Story Intelligence (3.1)
- System uses
Graph<Box<dyn Component>, FlowEdge, Directed> finalize()builds edge→state mapping, validates topologytraverse_for_jacobianyields (node, component, edge_indices)- Solver consumes residuals from components; coupling residuals follow same pattern
References
- Epic 3 Story 3.4: [Source: planning-artifacts/epics.md#Story 3.4]
- FR11: [Source: planning-artifacts/epics.md — Thermal coupling between circuits]
- Architecture FR9-FR13: [Source: planning-artifacts/architecture.md — line 797]
- petgraph cycle detection: [Source: https://docs.rs/petgraph/latest/petgraph/algo/fn.is_cyclic_directed.html]
- petgraph SCC: [Source: https://docs.rs/petgraph/latest/petgraph/algo/fn.kosaraju_scc.html]
- Story 3.3: [Source: implementation-artifacts/3-3-multi-circuit-machine-definition.md]
- Story 3.1: [Source: implementation-artifacts/3-1-system-graph-structure.md]
Dev Agent Record
Agent Model Used
claude-sonnet-4-20250514
Debug Log References
N/A
Completion Notes List
- Created
ThermalConductanceNewType incrates/core/src/types.rsfor type-safe UA values - Created
crates/solver/src/coupling.rswithThermalCouplingstruct,compute_coupling_heat(),has_circular_dependencies(), andcoupling_groups() - Added
InvalidCircuitForCouplingerror variant toTopologyErrorinerror.rs - Extended
Systemstruct withthermal_couplings: Vec<ThermalCoupling>andadd_thermal_coupling()method - Circuit validation in
add_thermal_coupling()ensures both hot and cold circuits exist - Circular dependency detection uses petgraph's
is_cyclic_directed()and SCC grouping viakosaraju_scc() - Sign convention: Q > 0 means heat INTO cold_circuit (documented in code)
- 16 unit tests in coupling.rs + 5 integration tests in system.rs (42 solver tests total)
- Fixed pre-existing clippy issues in calib.rs, compressor.rs, and expansion_valve.rs
- All 387 tests pass (297 components + 46 core + 42 solver + 2 integration)
- Code review (AI) 2026-02-17: Implemented missing AC#4 APIs:
coupling_residual_count(),coupling_residuals(temperatures, out),coupling_jacobian_entries(row_offset, t_hot_cols, t_cold_cols). Addedtracing::warn!infinalize()when circular thermal dependencies detected. Added integration testtest_coupling_residuals_basicincrates/solver/tests/multi_circuit.rs. All solver tests pass (43 unit + 4 integration).
File List
- crates/core/src/types.rs (modified: added ThermalConductance NewType with Display, From, conversions)
- crates/core/src/lib.rs (modified: re-export ThermalConductance)
- crates/solver/src/coupling.rs (new: ThermalCoupling, compute_coupling_heat, has_circular_dependencies, coupling_groups, build_coupling_graph)
- crates/solver/src/system.rs (modified: thermal_couplings field, add_thermal_coupling, thermal_coupling_count, thermal_couplings, get_thermal_coupling, circuit_exists)
- crates/solver/src/error.rs (modified: TopologyError::InvalidCircuitForCoupling)
- crates/solver/src/lib.rs (modified: re-export coupling module and types)
- crates/core/src/calib.rs (fixed: clippy manual_range_contains)
- crates/components/src/compressor.rs (fixed: clippy too_many_arguments, unused variable)
- crates/components/src/expansion_valve.rs (fixed: clippy unnecessary_map_or)
- crates/solver/tests/multi_circuit.rs (modified: added test_coupling_residuals_basic)
- _bmad-output/implementation-artifacts/3-4-code-review-findings.md (new: code review report)
Change Log
- 2026-02-17: Story 3.4 implementation complete. ThermalCoupling struct with hot/cold circuit, UA, efficiency. compute_coupling_heat with proper sign convention. Circular dependency detection via petgraph is_cyclic_directed. Coupling groups via kosaraju_scc for SCC analysis. All ACs satisfied, 42 solver tests pass.
- 2026-02-17: Code review (AI): Fixed C1/H1 (coupling_residuals, coupling_jacobian_entries), M1 (tracing::warn in finalize), M3 (test_coupling_residuals_basic), M4 (finalize circular check). Story status → done.