13 KiB
Story 3.2: Port Compatibility Validation
Status: done
Story
As a system designer, I want port connection validation at build time, so that incompatible connections are caught early.
Acceptance Criteria
-
Incompatible Fluid Rejection (AC: #1)
- Given two ports with incompatible fluids (e.g., R134a vs Water)
- When attempting to connect
- Then connection fails with clear error (e.g.,
ConnectionError::IncompatibleFluidorTopologyError) - And error message identifies both fluids
-
Valid Connections Accepted (AC: #2)
- Given two ports with same fluid, matching pressure and enthalpy within tolerance
- When attempting to connect
- Then connection is accepted
- And edge is added to system graph
-
Pressure/Enthalpy Continuity Enforced (AC: #3)
- Given two ports with same fluid but pressure or enthalpy mismatch beyond tolerance
- When attempting to connect
- Then connection fails with clear error
- And tolerance follows existing port.rs constants (PRESSURE_TOLERANCE_FRACTION, ENTHALPY_TOLERANCE_J_KG)
Tasks / Subtasks
- Integrate port validation into System graph build (AC: #1, #2, #3)
- Extend
add_edgeAPI to specify port indices:add_edge_with_ports(source_node, source_port_idx, target_node, target_port_idx)-> Result<EdgeIndex, ConnectionError> - When adding edge: retrieve ports via
component.get_ports()for source and target nodes - Compare
fluid_id()of outlet port (source) vs inlet port (target) — reject if different - Compare pressure and enthalpy within tolerance — reject if mismatch
- Return
Result<EdgeIndex, ConnectionError>on failure
- Extend
- Error handling and propagation (AC: #1)
- Reuse
ConnectionErrorfrom port (re-exported in solver) - Add
ConnectionError::InvalidPortIndexfor out-of-bounds port indices - Ensure error messages are clear and actionable
- Reuse
- Tests
- Test: connect R134a outlet to R134a inlet — succeeds
- Test: connect R134a outlet to Water inlet — fails with IncompatibleFluid
- Test: connect with pressure mismatch — fails with PressureMismatch
- Test: connect with enthalpy mismatch — fails with EnthalpyMismatch
- Test: valid connection in 4-node cycle — all edges accepted
Dev Notes
Epic Context
Epic 3: System Topology (Graph) — Enable component assembly via Ports and manage multi-circuits with thermal coupling. FR10 (component connection via Ports) and FR9–FR13 map to crates/solver and crates/components.
Story Dependencies:
- Epic 1 (Component trait, Ports, State machine) — done
- Story 3.1 (System graph structure) — done
port.rsalready hasPort::connect()with fluid, pressure, enthalpy validation — reuse logic or call from solver
Architecture Context (Step 3.2 — CRITICAL EXTRACTION)
Technical Stack:
- Rust, petgraph 0.6.x, thiserror, entropyk-core, entropyk-components
- No new external dependencies required
Code Structure:
crates/solver/src/system.rs— primary modification sitecrates/solver/src/error.rs— extend TopologyError if neededcrates/components/src/port.rs— existing ConnectionError, FluidId, Port validation logic
API Patterns:
- Current:
add_edge(source: NodeIndex, target: NodeIndex) -> EdgeIndex(no port validation) - Target: Either extend to
add_edge(source, source_port, target, target_port) -> Result<EdgeIndex, ConnectionError>or validate infinalize()by traversing edges and checking component ports - Convention: Components typically have
get_ports()returning[inlet, outlet]for 2-port components; multi-port (economizer) need explicit port indices
Relevant Architecture Sections:
- Type-State for Connection Safety (architecture line 239–250): Ports use Disconnected/Connected; connection validation at connect time
- Component Trait (architecture line 231–237):
get_ports() -> &[Port]provides port access - Error Handling (architecture line 276–308): ThermoError, Result<T, E> throughout; zero-panic policy
- System Topology (architecture line 617–619): FR9–FR13 in solver/system.rs
Performance Requirements:
- Validation at build time only (not in solver hot path)
- No dynamic allocation in solver loop — validation happens before finalize()
Testing Standards:
approx::assert_relative_eq!for float comparisons- Tolérance pression: 1e-4 relative ou 1 Pa min (port.rs)
- Tolérance enthalpie: 100 J/kg (port.rs)
Integration Patterns:
- Solver depends on components; components expose
get_ports()andConnectionError - May need to re-export or map
ConnectionErrorfrom components in solver crate
Developer Context
Existing Implementation:
port.rs::Port::connect()already validates: IncompatibleFluid, PressureMismatch, EnthalpyMismatchport.rsconstants: PRESSURE_TOLERANCE_FRACTION=1e-4, ENTHALPY_TOLERANCE_J_KG=100, MIN_PRESSURE_TOLERANCE_PA=1system.rs::add_edge()currently accepts any (source, target) without port validationsystem.rs::validate_topology()checks isolated nodes only; comment says "Story 3.2" for port validationTopologyErrorhasUnconnectedPortsandInvalidTopology(allow dead_code) — ready for use
Design Decision:
- Option A: Extend
add_edge(source, source_port_idx, target, target_port_idx)— explicit, validates at add time - Option B: Add
validate_port_compatibility()infinalize()— traverses edges, infers port mapping from graph (e.g., outgoing edge from node = outlet port, incoming = inlet) - Option B is simpler if graph structure implies port mapping (one outlet per node for simple components); Option A is more flexible for multi-port components
Port Mapping Convention:
- For 2-port components:
get_ports()[0]= inlet,get_ports()[1]= outlet (verify in compressor, condenser, etc.) - For economizer (4 ports): explicit port indices required
Technical Requirements
Validation Rules:
- Fluid compatibility:
source_port.fluid_id() == target_port.fluid_id() - Pressure continuity:
|P_source - P_target| <= max(P * 1e-4, 1 Pa) - Enthalpy continuity:
|h_source - h_target| <= 100 J/kg
Error Types:
- Reuse
ConnectionError::IncompatibleFluidfrom entropyk_components - Reuse
ConnectionError::PressureMismatch,EnthalpyMismatchor addTopologyError::IncompatiblePortswith nested cause - Solver crate depends on components — can use
ConnectionErrorviaentropyk_components::ConnectionError
Architecture Compliance
- NewType pattern: Use
Pressure,Enthalpyfrom core (ports already use them) - No bare f64 in public API
- tracing for validation failures (e.g.,
tracing::warn!("Port validation failed: {}", err)) - Result<T, E> — no unwrap/expect in production
- approx for float assertions in tests
Library/Framework Requirements
- entropyk_components: ConnectionError, FluidId, Port, ConnectedPort, Component::get_ports
- petgraph: Graph, NodeIndex, EdgeIndex — no change
- thiserror: TopologyError extension if needed
File Structure Requirements
Modified files:
crates/solver/src/system.rs— add port validation in add_edge or finalizecrates/solver/src/error.rs— possibly add TopologyError variants or use ConnectionErrorcrates/solver/Cargo.toml— ensure entropyk_components dependency (already present)
No new files required unless extracting validation to a separate module (e.g., validation.rs).
Testing Requirements
Unit tests (system.rs or validation module):
test_valid_connection_same_fluid— R134a to R134a, matching P/htest_incompatible_fluid_rejected— R134a to Watertest_pressure_mismatch_rejected— same fluid, P differs > tolerancetest_enthalpy_mismatch_rejected— same fluid, h differs > 100 J/kgtest_simple_cycle_port_validation— 4 components, 4 edges, all valid
Integration:
- Use real components (e.g., Compressor, Condenser) with ConnectedPort if available, or mock components with get_ports() returning ports with specific FluidId/P/h
Project Structure Notes
- Architecture specifies
crates/solver/src/system.rsfor topology — matches - Story 3.1 created System with add_edge, finalize, validate_topology
- Story 3.2 extends validation to port compatibility
- Story 3.3 (Multi-Circuit) will add circuit tracking — no conflict
Previous Story Intelligence (3.1)
- System uses
Graph<Box<dyn Component>, FlowEdge, Directed> add_edge(source, target)returns EdgeIndex; finalize() assigns state indices- MockComponent in tests has
get_ports() -> &[]— need mock with non-empty ports for 3.2 tests - TopologyError::IsolatedNode already used; UnconnectedPorts/InvalidTopology reserved
traverse_for_jacobianyields (node, component, edge_indices); components get state via edge mapping
References
- Epic 3 Story 3.2: [Source: planning-artifacts/epics.md#Story 3.2]
- Architecture FR10: [Source: planning-artifacts/architecture.md — Component connection via Ports]
- Architecture Type-State: [Source: planning-artifacts/architecture.md — line 239]
- port.rs ConnectionError: [Source: crates/components/src/port.rs]
- port.rs validation constants: [Source: crates/components/src/port.rs — PRESSURE_TOLERANCE_FRACTION, etc.]
- system.rs validate_topology: [Source: crates/solver/src/system.rs — line 114]
- Story 3.1: [Source: implementation-artifacts/3-1-system-graph-structure.md]
Change Log
- 2026-02-15: Story 3.2 implementation complete. Added add_edge_with_ports with port validation, validate_port_continuity in port.rs, ConnectionError::InvalidPortIndex. All ACs satisfied, 5 new port validation tests + 2 port.rs tests.
- 2026-02-17: Code review complete. Fixed File List documentation, enhanced InvalidPortIndex error messages with context, added pressure tolerance boundary test. Status: review → done. All tests passing (43 solver + 297 components).
Dev Agent Record
Agent Model Used
{{agent_model_name_version}}
Debug Log References
Completion Notes List
- Added
validate_port_continuity(outlet, inlet)in port.rs reusing PRESSURE_TOLERANCE_FRACTION, ENTHALPY_TOLERANCE_J_KG - Added
add_edge_with_ports(source, source_port_idx, target, target_port_idx)-> Result<EdgeIndex, ConnectionError> - Convention: port 0 = inlet, port 1 = outlet for 2-port components
- Components with no ports: add_edge (unvalidated) or add_edge_with_ports with empty ports skips validation
- ConnectionError re-exported from solver; InvalidPortIndex for invalid node/port indices
- 5 solver tests + 2 port.rs tests for validate_port_continuity
File List
- crates/components/src/port.rs (modified: validate_port_continuity, ConnectionError::InvalidPortIndex)
- crates/components/src/lib.rs (modified: re-export validate_port_continuity)
- crates/solver/ (new: initial solver crate implementation)
- src/system.rs: System graph with add_edge_with_ports and port validation
- src/lib.rs: Public API exports including ConnectionError re-export
- src/error.rs: TopologyError and AddEdgeError definitions
- src/coupling.rs: ThermalCoupling for multi-circuit support
- src/graph.rs: Graph traversal utilities
- Cargo.toml: Dependencies on entropyk-components, entropyk-core, petgraph, thiserror, tracing
- crates/solver/tests/multi_circuit.rs (new: integration tests for multi-circuit topology)
- _bmad-output/implementation-artifacts/sprint-status.yaml (modified: 3-2 in-progress → review)
Senior Developer Review (AI)
Reviewer: Code Review Agent
Date: 2026-02-17
Outcome: ✅ APPROVED
Issues Found and Fixed
-
Documentation Inaccuracy (MEDIUM) - Fixed
- File List incorrectly stated solver files were "modified"
- Corrected to reflect
crates/solver/is a new crate
-
Error Message Context (LOW) - Fixed
InvalidPortIndexerror lacked context about which index failed- Enhanced to:
Invalid port index {index}: component has {port_count} ports (valid: 0..{max_index})
-
Test Coverage Gap (LOW) - Fixed
- Added
test_pressure_tolerance_boundary()to verify exact tolerance boundary behavior - Tests both at-tolerance (success) and just-outside-tolerance (failure) cases
- Added
Verification Results
- All 43 solver tests passing
- All 297 components tests passing
- All 5 doc-tests passing
- No compiler warnings (solver crate)
Quality Assessment
- Architecture Compliance: ✅ Follows type-state pattern, NewType pattern
- Error Handling: ✅ Result<T,E> throughout, zero unwrap/expect in production
- Test Coverage: ✅ All ACs covered with unit tests
- Documentation: ✅ Clear docstrings, examples in public API
- Security: ✅ No injection risks, input validation at boundaries