Entropyk/_bmad-output/implementation-artifacts/3-4-thermal-coupling-between-circuits.md

292 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
1. **Heat Transfer Linking** (AC: #1)
- [x] Given two circuits with a heat exchanger coupling them
- [x] When defining thermal coupling
- [x] Then heat transfer equations link the circuits
- [x] And coupling is represented as additional residuals
2. **Energy Conservation** (AC: #2)
- [x] Given a thermal coupling between two circuits
- [x] When computing heat transfer
- [x] Then Q_hot = -Q_cold (energy conserved)
- [x] And sign convention is documented (positive = heat INTO circuit)
3. **Circular Dependency Detection** (AC: #3)
- [x] Given circuits with mutual thermal coupling (A heats B, B heats A)
- [x] When analyzing coupling topology
- [x] Then circular dependencies are detected
- [x] And solver is informed to solve simultaneously (not sequentially)
4. **Coupling Residuals** (AC: #4)
- [x] Given a defined thermal coupling
- [x] When solver assembles residuals
- [x] Then coupling contributes additional residual equations
- [x] And Jacobian includes coupling derivatives
## Tasks / Subtasks
- [x] Define ThermalCoupling struct (AC: #1, #2)
- [x] Create `ThermalCoupling` with: hot_circuit_id, cold_circuit_id, ua (thermal conductance)
- [x] Add optional efficiency factor (default 1.0)
- [x] Document sign convention: Q > 0 means heat INTO cold_circuit
- [x] Add coupling storage to System (AC: #1)
- [x] Add `thermal_couplings: Vec<ThermalCoupling>` to System
- [x] Add `add_thermal_coupling(coupling: ThermalCoupling) -> Result<usize, TopologyError>`
- [x] Validate circuit_ids exist before adding
- [x] Implement energy conservation (AC: #2)
- [x] Method `compute_coupling_heat(coupling, hot_state, cold_state) -> HeatTransfer`
- [x] Formula: Q = UA * (T_hot - T_cold) where T from respective circuit states
- [x] Returns positive Q for heat into cold circuit
- [x] Implement circular dependency detection (AC: #3)
- [x] Build coupling graph (nodes = circuits, edges = couplings)
- [x] Detect cycles using petgraph::algo::is_cyclic
- [x] Add `has_circular_dependencies() -> bool`
- [x] Add `coupling_groups() -> Vec<Vec<CircuitId>>` returning groups that must solve simultaneously
- [x] Expose coupling residuals for solver (AC: #4)
- [x] Add `coupling_residuals(state: &SystemState) -> Vec<f64>`
- [x] Residual: r = Q_actual - Q_expected (heat balance at coupling point)
- [x] Add `coupling_jacobian_entries()` returning (row, col, partial_derivative) tuples
- [x] Tests
- [x] Test: add_thermal_coupling valid, retrieves correctly
- [x] Test: add_thermal_coupling with invalid circuit_id fails
- [x] Test: compute_coupling_heat positive when T_hot > T_cold
- [x] Test: circular dependency detection (A→B→A)
- [x] Test: no circular dependency (A→B, B→C)
- [x] Test: coupling_groups returns correct groupings
- [x] 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 detection
- `crates/solver/src/error.rs` — add TopologyError::InvalidCircuitForCoupling
**Relevant Architecture Sections:**
- **System Topology** (architecture line 797): FR9FR13 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..=4
- `node_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:**
1. **Coupling Representation:**
- `ThermalCoupling` stored in System, separate from graph edges
- Coupling does NOT create petgraph edges (would confuse flow traversal)
- Coupling indexed by usize for O(1) access
2. **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()`
3. **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)
4. **Residual Convention:**
- Coupling residual: `r = Q_model - Q_coupling` where Q_model is from circuit state
- Jacobian includes ∂r/∂T_hot and ∂r/∂T_cold
- Solver treats coupling residuals like component residuals
### Technical Requirements
**ThermalCoupling Struct:**
```rust
/// 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):**
```rust
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 validation
- `crates/solver/src/error.rs` — add TopologyError::InvalidCircuitForCoupling
- `crates/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.rs` with thermal coupling integration test
### Testing Requirements
**Unit tests (coupling.rs):**
- `test_thermal_coupling_creation` — valid coupling, correct fields
- `test_compute_coupling_heat_positive` — T_hot > T_cold → Q > 0
- `test_compute_coupling_heat_zero` — T_hot == T_cold → Q = 0
- `test_compute_coupling_heat_negative` — T_hot < T_cold Q < 0 (reverse flow)
- `test_circular_dependency_detection` AB, BA cyclic
- `test_no_circular_dependency` AB, BC not cyclic
- `test_coupling_groups_scc` AB, CD 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 stored
- `test_add_thermal_coupling_invalid_circuit` coupling with CircuitId(99) error
- `test_coupling_residuals_basic` 2 circuits with coupling, verify residual equation
### Project Structure Notes
- Architecture specifies `crates/solver/src/system.rs` for FR9FR13 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 components
- `add_component_to_circuit` assigns 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 edgestate mapping, validates topology
- `traverse_for_jacobian` yields (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 `ThermalConductance` NewType in `crates/core/src/types.rs` for type-safe UA values
- Created `crates/solver/src/coupling.rs` with `ThermalCoupling` struct, `compute_coupling_heat()`, `has_circular_dependencies()`, and `coupling_groups()`
- Added `InvalidCircuitForCoupling` error variant to `TopologyError` in `error.rs`
- Extended `System` struct with `thermal_couplings: Vec<ThermalCoupling>` and `add_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 via `kosaraju_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)`. Added `tracing::warn!` in `finalize()` when circular thermal dependencies detected. Added integration test `test_coupling_residuals_basic` in `crates/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.