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

12 KiB
Raw Permalink Blame History

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

  1. 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
  2. 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)
  3. 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
  4. 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 extend add_component
    • Validate circuit count ≤ 5 when adding
  • 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::CrossCircuitConnection or ConnectionError variant 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_jacobian or new traverse_circuit_for_jacobian(circuit_id) supports per-circuit iteration
  • Backward compatibility
    • Single-circuit case: default circuit_id = 0 for existing add_component calls
    • Existing tests (3.1, 3.2) must pass without modification
  • 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 — 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): 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:

/// 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