258 lines
13 KiB
Markdown
258 lines
13 KiB
Markdown
# Story 3.2: Port Compatibility Validation
|
||
|
||
Status: done
|
||
|
||
<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
|
||
|
||
## Story
|
||
|
||
As a system designer,
|
||
I want port connection validation at build time,
|
||
so that incompatible connections are caught early.
|
||
|
||
## Acceptance Criteria
|
||
|
||
1. **Incompatible Fluid Rejection** (AC: #1)
|
||
- [x] Given two ports with incompatible fluids (e.g., R134a vs Water)
|
||
- [x] When attempting to connect
|
||
- [x] Then connection fails with clear error (e.g., `ConnectionError::IncompatibleFluid` or `TopologyError`)
|
||
- [x] And error message identifies both fluids
|
||
|
||
2. **Valid Connections Accepted** (AC: #2)
|
||
- [x] Given two ports with same fluid, matching pressure and enthalpy within tolerance
|
||
- [x] When attempting to connect
|
||
- [x] Then connection is accepted
|
||
- [x] And edge is added to system graph
|
||
|
||
3. **Pressure/Enthalpy Continuity Enforced** (AC: #3)
|
||
- [x] Given two ports with same fluid but pressure or enthalpy mismatch beyond tolerance
|
||
- [x] When attempting to connect
|
||
- [x] Then connection fails with clear error
|
||
- [x] And tolerance follows existing port.rs constants (PRESSURE_TOLERANCE_FRACTION, ENTHALPY_TOLERANCE_J_KG)
|
||
|
||
## Tasks / Subtasks
|
||
|
||
- [x] Integrate port validation into System graph build (AC: #1, #2, #3)
|
||
- [x] Extend `add_edge` API to specify port indices: `add_edge_with_ports(source_node, source_port_idx, target_node, target_port_idx)` -> Result<EdgeIndex, ConnectionError>
|
||
- [x] When adding edge: retrieve ports via `component.get_ports()` for source and target nodes
|
||
- [x] Compare `fluid_id()` of outlet port (source) vs inlet port (target) — reject if different
|
||
- [x] Compare pressure and enthalpy within tolerance — reject if mismatch
|
||
- [x] Return `Result<EdgeIndex, ConnectionError>` on failure
|
||
- [x] Error handling and propagation (AC: #1)
|
||
- [x] Reuse `ConnectionError` from port (re-exported in solver)
|
||
- [x] Add `ConnectionError::InvalidPortIndex` for out-of-bounds port indices
|
||
- [x] Ensure error messages are clear and actionable
|
||
- [x] Tests
|
||
- [x] Test: connect R134a outlet to R134a inlet — succeeds
|
||
- [x] Test: connect R134a outlet to Water inlet — fails with IncompatibleFluid
|
||
- [x] Test: connect with pressure mismatch — fails with PressureMismatch
|
||
- [x] Test: connect with enthalpy mismatch — fails with EnthalpyMismatch
|
||
- [x] 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.rs` already has `Port::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 site
|
||
- `crates/solver/src/error.rs` — extend TopologyError if needed
|
||
- `crates/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 in `finalize()` 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()` and `ConnectionError`
|
||
- May need to re-export or map `ConnectionError` from components in solver crate
|
||
|
||
### Developer Context
|
||
|
||
**Existing Implementation:**
|
||
- `port.rs::Port::connect()` already validates: IncompatibleFluid, PressureMismatch, EnthalpyMismatch
|
||
- `port.rs` constants: PRESSURE_TOLERANCE_FRACTION=1e-4, ENTHALPY_TOLERANCE_J_KG=100, MIN_PRESSURE_TOLERANCE_PA=1
|
||
- `system.rs::add_edge()` currently accepts any (source, target) without port validation
|
||
- `system.rs::validate_topology()` checks isolated nodes only; comment says "Story 3.2" for port validation
|
||
- `TopologyError` has `UnconnectedPorts` and `InvalidTopology` (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()` in `finalize()` — 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:**
|
||
1. Fluid compatibility: `source_port.fluid_id() == target_port.fluid_id()`
|
||
2. Pressure continuity: `|P_source - P_target| <= max(P * 1e-4, 1 Pa)`
|
||
3. Enthalpy continuity: `|h_source - h_target| <= 100 J/kg`
|
||
|
||
**Error Types:**
|
||
- Reuse `ConnectionError::IncompatibleFluid` from entropyk_components
|
||
- Reuse `ConnectionError::PressureMismatch`, `EnthalpyMismatch` or add `TopologyError::IncompatiblePorts` with nested cause
|
||
- Solver crate depends on components — can use `ConnectionError` via `entropyk_components::ConnectionError`
|
||
|
||
### Architecture Compliance
|
||
|
||
- **NewType pattern**: Use `Pressure`, `Enthalpy` from 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 finalize
|
||
- `crates/solver/src/error.rs` — possibly add TopologyError variants or use ConnectionError
|
||
- `crates/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/h
|
||
- `test_incompatible_fluid_rejected` — R134a to Water
|
||
- `test_pressure_mismatch_rejected` — same fluid, P differs > tolerance
|
||
- `test_enthalpy_mismatch_rejected` — same fluid, h differs > 100 J/kg
|
||
- `test_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.rs` for 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_jacobian` yields (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
|
||
|
||
1. **Documentation Inaccuracy (MEDIUM)** - Fixed
|
||
- File List incorrectly stated solver files were "modified"
|
||
- Corrected to reflect `crates/solver/` is a **new** crate
|
||
|
||
2. **Error Message Context (LOW)** - Fixed
|
||
- `InvalidPortIndex` error lacked context about which index failed
|
||
- Enhanced to: `Invalid port index {index}: component has {port_count} ports (valid: 0..{max_index})`
|
||
|
||
3. **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
|
||
|
||
### 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
|