292 lines
14 KiB
Markdown
292 lines
14 KiB
Markdown
# 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): 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..=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` — A→B, B→A → cyclic
|
||
- `test_no_circular_dependency` — A→B, B→C → not cyclic
|
||
- `test_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 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 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 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 edge→state 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.
|