12 KiB
Story 3.3: Multi-Circuit Machine Definition
Status: done
Story
As a R&D engineer (Marie), I want machines with N independent circuits, so that I simulate complex heat pumps.
Acceptance Criteria
-
Circuit Tracking (AC: #1)
- Given a machine with 2+ circuits
- When defining topology
- Then each circuit is tracked independently
- And each node (component) and edge belongs to exactly one circuit
-
Circuit Isolation (AC: #2)
- Given two circuits (e.g., refrigerant and water)
- When adding edges
- Then flow edges connect only nodes within the same circuit
- And cross-circuit connections are rejected at build time (thermal coupling is Story 3.4)
-
Solver-Ready Structure (AC: #3)
- Given a machine with N circuits
- When the solver queries circuit structure
- Then circuits can be solved simultaneously or sequentially (strategy deferred to Epic 4)
- And the solver receives circuit membership for each node/edge
-
Circuit Limit (AC: #4)
- Given a machine definition
- When adding circuits
- Then supports up to N=5 circuits
- And returns clear error if limit exceeded
Tasks / Subtasks
- Add CircuitId and circuit tracking (AC: #1, #4)
- Define
CircuitId(newtype or enum 0..=4, max 5 circuits) - Add
node_to_circuit: HashMap<NodeIndex, CircuitId>to System - Add
add_component_to_circuit(component, circuit_id)or extendadd_component - Validate circuit count ≤ 5 when adding
- Define
- Enforce circuit isolation on edges (AC: #2)
- When adding edge (add_edge or add_edge_with_ports): validate source and target have same circuit_id
- Return
TopologyError::CrossCircuitConnectionorConnectionErrorvariant if mismatch - Document that thermal coupling (cross-circuit) is Story 3.4
- Expose circuit structure for solver (AC: #3)
- Add
circuit_count() -> usize - Add
circuit_nodes(circuit_id: CircuitId) -> impl Iterator<Item = NodeIndex> - Add
circuit_edges(circuit_id: CircuitId) -> impl Iterator<Item = EdgeIndex> - Ensure
traverse_for_jacobianor newtraverse_circuit_for_jacobian(circuit_id)supports per-circuit iteration
- Add
- Backward compatibility
- Single-circuit case: default circuit_id = 0 for existing
add_componentcalls - Existing tests (3.1, 3.2) must pass without modification
- Single-circuit case: default circuit_id = 0 for existing
- Tests
- Test: 2-circuit machine, each circuit has own components and edges
- Test: cross-circuit edge rejected
- Test: circuit_count, circuit_nodes, circuit_edges return correct values
- Test: N=5 circuits accepted, N=6 rejected
- 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— addTopologyError::CrossCircuitConnection,TooManyCircuits- Architecture line 797: System Topology FR9–FR13 in
system.rs - Architecture line 702:
tests/integration/multi_circuit.rsfor FR9
API Patterns:
- Extend
add_componentto accept optionalCircuitId(default 0 for backward compat) - Or add
add_component_to_circuit(component, circuit_id)— explicit - Edge validation: in
add_edgeandadd_edge_with_ports, checknode_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.rsfor 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_edgescan 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:
Systemhasgraph,edge_to_state,finalizedadd_component(component)adds node, returns NodeIndexadd_edgeandadd_edge_with_portsadd edges with optional port validationfinalize()builds edge→state mapping, validates topology (isolated nodes)- No circuit concept yet — single implicit circuit
Design Decision:
- Single graph with circuit metadata (not Vec): 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, orNonZeroU81..=5. Useu8with validation. - Default circuit: When
add_componentis 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:
/// 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 accessorscrates/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) ortests/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_edgestest_cross_circuit_edge_rejected— add edge from circuit 0 node to circuit 1 node → TopologyError::CrossCircuitConnectiontest_circuit_count_and_accessors— 3 circuits, verify circuit_count()=3, circuit_nodes(0).count() correcttest_max_five_circuits— add 5 circuits OK, 6th fails with TooManyCircuitstest_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.rsfor 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 viavalidate_port_continuityadd_edge(no ports) — no validation; use for mock components- Extend both: before adding edge, check
node_to_circuit[source] == node_to_circuit[target] ConnectionErrorfrom components;TopologyErrorfrom 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_jacobianyields (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 addededge_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 verifyedge_circuit()andcircuit_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
- Made