Entropyk/_bmad-output/implementation-artifacts/3-3-multi-circuit-machine-definition.md

253 lines
12 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.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 FR9FR13 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): FR9FR13 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