253 lines
12 KiB
Markdown
253 lines
12 KiB
Markdown
# Story 3.3: Multi-Circuit Machine Definition
|
||
|
||
Status: done
|
||
|
||
<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
|
||
|
||
## Story
|
||
|
||
As a R&D engineer (Marie),
|
||
I want machines with N independent circuits,
|
||
so that I simulate complex heat pumps.
|
||
|
||
## Acceptance Criteria
|
||
|
||
1. **Circuit Tracking** (AC: #1)
|
||
- [x] Given a machine with 2+ circuits
|
||
- [x] When defining topology
|
||
- [x] Then each circuit is tracked independently
|
||
- [x] And each node (component) and edge belongs to exactly one circuit
|
||
|
||
2. **Circuit Isolation** (AC: #2)
|
||
- [x] Given two circuits (e.g., refrigerant and water)
|
||
- [x] When adding edges
|
||
- [x] Then flow edges connect only nodes within the same circuit
|
||
- [x] And cross-circuit connections are rejected at build time (thermal coupling is Story 3.4)
|
||
|
||
3. **Solver-Ready Structure** (AC: #3)
|
||
- [x] Given a machine with N circuits
|
||
- [x] When the solver queries circuit structure
|
||
- [x] Then circuits can be solved simultaneously or sequentially (strategy deferred to Epic 4)
|
||
- [x] And the solver receives circuit membership for each node/edge
|
||
|
||
4. **Circuit Limit** (AC: #4)
|
||
- [x] Given a machine definition
|
||
- [x] When adding circuits
|
||
- [x] Then supports up to N=5 circuits
|
||
- [x] And returns clear error if limit exceeded
|
||
|
||
## Tasks / Subtasks
|
||
|
||
- [x] Add CircuitId and circuit tracking (AC: #1, #4)
|
||
- [x] Define `CircuitId` (newtype or enum 0..=4, max 5 circuits)
|
||
- [x] Add `node_to_circuit: HashMap<NodeIndex, CircuitId>` to System
|
||
- [x] Add `add_component_to_circuit(component, circuit_id)` or extend `add_component`
|
||
- [x] Validate circuit count ≤ 5 when adding
|
||
- [x] Enforce circuit isolation on edges (AC: #2)
|
||
- [x] When adding edge (add_edge or add_edge_with_ports): validate source and target have same circuit_id
|
||
- [x] Return `TopologyError::CrossCircuitConnection` or `ConnectionError` variant if mismatch
|
||
- [x] Document that thermal coupling (cross-circuit) is Story 3.4
|
||
- [x] Expose circuit structure for solver (AC: #3)
|
||
- [x] Add `circuit_count() -> usize`
|
||
- [x] Add `circuit_nodes(circuit_id: CircuitId) -> impl Iterator<Item = NodeIndex>`
|
||
- [x] Add `circuit_edges(circuit_id: CircuitId) -> impl Iterator<Item = EdgeIndex>`
|
||
- [x] Ensure `traverse_for_jacobian` or new `traverse_circuit_for_jacobian(circuit_id)` supports per-circuit iteration
|
||
- [x] Backward compatibility
|
||
- [x] Single-circuit case: default circuit_id = 0 for existing `add_component` calls
|
||
- [x] Existing tests (3.1, 3.2) must pass without modification
|
||
- [x] Tests
|
||
- [x] Test: 2-circuit machine, each circuit has own components and edges
|
||
- [x] Test: cross-circuit edge rejected
|
||
- [x] Test: circuit_count, circuit_nodes, circuit_edges return correct values
|
||
- [x] Test: N=5 circuits accepted, N=6 rejected
|
||
- [x] Test: single-circuit backward compat (add_component without circuit uses circuit 0)
|
||
|
||
## Dev Notes
|
||
|
||
### Epic Context
|
||
|
||
**Epic 3: System Topology (Graph)** — Enable component assembly via Ports and manage multi-circuits with thermal coupling. FR9 (multi-circuit machine definition), FR10 (ports), FR12 (simultaneous/sequential solving) map to `crates/solver`.
|
||
|
||
**Story Dependencies:**
|
||
- Story 3.1 (System graph structure) — done
|
||
- Story 3.2 (Port compatibility validation) — done
|
||
- Story 3.4 (Thermal coupling) — adds cross-circuit heat transfer; 3.3 provides circuit structure
|
||
- Story 3.5 (Zero-flow) — independent
|
||
|
||
### Architecture Context (Step 3.2 — CRITICAL EXTRACTION)
|
||
|
||
**Technical Stack:**
|
||
- Rust, petgraph 0.6.x, thiserror, entropyk-core, entropyk-components
|
||
- No new external dependencies
|
||
|
||
**Code Structure:**
|
||
- `crates/solver/src/system.rs` — primary modification site (add circuit tracking)
|
||
- `crates/solver/src/error.rs` — add `TopologyError::CrossCircuitConnection`, `TooManyCircuits`
|
||
- Architecture line 797: System Topology FR9–FR13 in `system.rs`
|
||
- Architecture line 702: `tests/integration/multi_circuit.rs` for FR9
|
||
|
||
**API Patterns:**
|
||
- Extend `add_component` to accept optional `CircuitId` (default 0 for backward compat)
|
||
- Or add `add_component_to_circuit(component, circuit_id)` — explicit
|
||
- Edge validation: in `add_edge` and `add_edge_with_ports`, check `node_to_circuit[source] == node_to_circuit[target]`
|
||
|
||
**Relevant Architecture Sections:**
|
||
- **System Topology** (architecture line 797): FR9–FR13 in solver/system.rs
|
||
- **Project Structure** (architecture line 702): `tests/integration/multi_circuit.rs` for FR9
|
||
- **Pre-allocation** (architecture line 239): No dynamic allocation in solver loop — circuit metadata built at finalize/build time
|
||
|
||
**Performance Requirements:**
|
||
- Circuit metadata built at topology build time (not in solver hot path)
|
||
- `circuit_nodes` / `circuit_edges` can be iterators over filtered collections
|
||
|
||
**Testing Standards:**
|
||
- `approx::assert_relative_eq!` for float comparisons
|
||
- Use existing mock components from 3.1/3.2 tests
|
||
|
||
### Developer Context
|
||
|
||
**Existing Implementation:**
|
||
- `System` has `graph`, `edge_to_state`, `finalized`
|
||
- `add_component(component)` adds node, returns NodeIndex
|
||
- `add_edge` and `add_edge_with_ports` add edges with optional port validation
|
||
- `finalize()` builds edge→state mapping, validates topology (isolated nodes)
|
||
- No circuit concept yet — single implicit circuit
|
||
|
||
**Design Decision:**
|
||
- **Single graph with circuit metadata** (not Vec<System>): Simpler for Story 3.4 thermal coupling — cross-circuit heat exchangers will connect nodes in different circuits via coupling equations, not flow edges. Flow edges remain same-circuit only.
|
||
- **CircuitId**: `pub struct CircuitId(pub u8)` with valid range 0..=4, or `NonZeroU8` 1..=5. Use `u8` with validation.
|
||
- **Default circuit**: When `add_component` is called without circuit_id, assign CircuitId(0). Ensures backward compatibility.
|
||
|
||
**Port Mapping Convention (from 3.2):**
|
||
- For 2-port components: `get_ports()[0]` = inlet, `get_ports()[1]` = outlet
|
||
- Port validation unchanged — still validate fluid, P, h when using `add_edge_with_ports`
|
||
|
||
### Technical Requirements
|
||
|
||
**CircuitId:**
|
||
```rust
|
||
/// Circuit identifier. Valid range 0..=4 (max 5 circuits).
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||
pub struct CircuitId(pub u8);
|
||
|
||
impl CircuitId {
|
||
pub const MAX: u8 = 4;
|
||
pub fn new(id: u8) -> Result<Self, TopologyError> {
|
||
if id <= Self::MAX { Ok(CircuitId(id)) } else { Err(TopologyError::TooManyCircuits { requested: id }) }
|
||
}
|
||
}
|
||
```
|
||
|
||
**Edge Validation:**
|
||
- Before adding edge: `node_to_circuit.get(&source) == node_to_circuit.get(&target)` (both Some and equal)
|
||
- If source or target has no circuit (legacy?) — treat as circuit 0 or reject
|
||
|
||
**Error Types:**
|
||
- `TopologyError::CrossCircuitConnection { source_circuit, target_circuit }`
|
||
- `TopologyError::TooManyCircuits { requested: u8 }`
|
||
|
||
### Architecture Compliance
|
||
|
||
- **NewType pattern**: Use `CircuitId` (not bare u8) for circuit identification
|
||
- **tracing** for validation failures (e.g., cross-circuit attempt)
|
||
- **Result<T, E>** — no unwrap/expect in production
|
||
- **#![deny(warnings)]** — all crates
|
||
|
||
### Library/Framework Requirements
|
||
|
||
- **entropyk_components**: Component, get_ports, ConnectionError — unchanged
|
||
- **petgraph**: Graph, NodeIndex, EdgeIndex — unchanged
|
||
- **thiserror**: TopologyError extension
|
||
|
||
### File Structure Requirements
|
||
|
||
**Modified files:**
|
||
- `crates/solver/src/system.rs` — add CircuitId, node_to_circuit, circuit validation, circuit accessors
|
||
- `crates/solver/src/error.rs` — add TopologyError::CrossCircuitConnection, TooManyCircuits
|
||
|
||
**New files (optional):**
|
||
- `crates/solver/src/circuit.rs` — CircuitId definition if preferred over system.rs
|
||
|
||
**Tests:**
|
||
- Add to `crates/solver/src/system.rs` (inline `#[cfg(test)]` module) or `tests/integration/multi_circuit.rs`
|
||
|
||
### Testing Requirements
|
||
|
||
**Unit tests:**
|
||
- `test_two_circuit_machine` — add 2 circuits, add components to each, add edges within each, verify circuit_nodes/circuit_edges
|
||
- `test_cross_circuit_edge_rejected` — add edge from circuit 0 node to circuit 1 node → TopologyError::CrossCircuitConnection
|
||
- `test_circuit_count_and_accessors` — 3 circuits, verify circuit_count()=3, circuit_nodes(0).count() correct
|
||
- `test_max_five_circuits` — add 5 circuits OK, 6th fails with TooManyCircuits
|
||
- `test_single_circuit_backward_compat` — add_component without circuit_id, add_edge — works as before (implicit circuit 0)
|
||
|
||
**Integration:**
|
||
- `tests/integration/multi_circuit.rs` — 2-circuit heat pump topology (refrigerant + water), no thermal coupling yet
|
||
|
||
### Project Structure Notes
|
||
|
||
- Architecture specifies `crates/solver/src/system.rs` for FR9 — matches
|
||
- Story 3.2 added `add_edge_with_ports` — extend validation to check circuit match
|
||
- Story 3.4 will add thermal coupling (cross-circuit heat transfer) — 3.3 provides foundation
|
||
- Story 3.5 (zero-flow) — independent
|
||
|
||
### Previous Story Intelligence (3.2)
|
||
|
||
- `add_edge_with_ports(source, source_port_idx, target, target_port_idx)` validates fluid, P, h via `validate_port_continuity`
|
||
- `add_edge` (no ports) — no validation; use for mock components
|
||
- **Extend both**: before adding edge, check `node_to_circuit[source] == node_to_circuit[target]`
|
||
- `ConnectionError` from components; `TopologyError` from solver — use TopologyError for circuit errors (topology-level)
|
||
- MockComponent in tests has `get_ports() -> &[]` — use for circuit tests; add_component_to_circuit with CircuitId
|
||
|
||
### Previous Story Intelligence (3.1)
|
||
|
||
- System uses `Graph<Box<dyn Component>, FlowEdge, Directed>`
|
||
- State vector layout: `[P_edge0, h_edge0, P_edge1, h_edge1, ...]`
|
||
- `traverse_for_jacobian` yields (node, component, edge_indices)
|
||
- For multi-circuit: solver may iterate per circuit; ensure `circuit_edges(cid)` returns edges in consistent order for state indexing
|
||
|
||
### References
|
||
|
||
- **Epic 3 Story 3.3:** [Source: planning-artifacts/epics.md#Story 3.3]
|
||
- **FR9:** [Source: planning-artifacts/epics.md — Multi-circuit machine definition]
|
||
- **Architecture FR9-FR13:** [Source: planning-artifacts/architecture.md — line 797]
|
||
- **tests/integration/multi_circuit.rs:** [Source: planning-artifacts/architecture.md — line 702]
|
||
- **Story 3.1:** [Source: implementation-artifacts/3-1-system-graph-structure.md]
|
||
- **Story 3.2:** [Source: implementation-artifacts/3-2-port-compatibility-validation.md]
|
||
|
||
## Dev Agent Record
|
||
|
||
### Agent Model Used
|
||
|
||
{{agent_model_name_version}}
|
||
|
||
### Debug Log References
|
||
|
||
### Completion Notes List
|
||
|
||
- Ultimate context engine analysis completed — comprehensive developer guide created
|
||
- Added CircuitId newtype (0..=4), node_to_circuit map, add_component_to_circuit
|
||
- add_edge now returns Result<EdgeIndex, TopologyError>; add_edge_with_ports returns Result<EdgeIndex, AddEdgeError>
|
||
- circuit_count(), circuit_nodes(), circuit_edges() for solver-ready structure
|
||
- 6 new unit tests + 2 integration tests in tests/multi_circuit.rs
|
||
- All 21 solver unit tests pass; backward compat verified
|
||
|
||
### File List
|
||
|
||
- crates/solver/src/system.rs (modified: CircuitId, node_to_circuit, add_component_to_circuit, circuit accessors, add_edge/add_edge_with_ports circuit validation)
|
||
- crates/solver/src/error.rs (modified: TopologyError::CrossCircuitConnection, TooManyCircuits, AddEdgeError)
|
||
- crates/solver/src/lib.rs (modified: re-export CircuitId, AddEdgeError)
|
||
- crates/solver/tests/multi_circuit.rs (new: integration tests for 2-circuit heat pump topology)
|
||
|
||
## Change Log
|
||
|
||
- 2026-02-15: Story 3.3 implementation complete. CircuitId, node_to_circuit, add_component_to_circuit, circuit validation in add_edge/add_edge_with_ports, circuit_count/circuit_nodes/circuit_edges. All ACs satisfied, 6 unit tests + 2 integration tests.
|
||
- 2026-02-17: Code review complete. Fixed 8 issues:
|
||
- Made `node_circuit()` and added `edge_circuit()` public API for circuit membership queries
|
||
- Added edge circuit validation in `finalize()` to ensure edge-circuit consistency
|
||
- Improved `circuit_count()` documentation for edge case behavior
|
||
- Enhanced `test_max_five_circuits()` documentation clarity
|
||
- Enhanced `test_single_circuit_backward_compat()` to verify `edge_circuit()` and `circuit_edges()`
|
||
- Added `test_maximum_five_circuits_integration()` for N=5 circuit coverage
|
||
- Updated module documentation to clarify valid circuit ID range (0-4)
|
||
- All 43 unit tests + 3 integration tests pass
|