# 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
