11 KiB
Story 1.3: Port and Connection System
Status: ready-for-dev
Story
As a thermodynamic systems developer,
I want a type-safe port and connection system with compile-time state validation,
so that I can prevent runtime connection errors and ensure fluid compatibility between components.
Acceptance Criteria
AC 1: Port Creation
Given a fluid type and thermodynamic state
When I create a new port
Then it initializes in Disconnected state with fluid_id, pressure, and enthalpy
AC 2: Fluid Compatibility Validation
Given two ports with different fluid_id values
When I attempt to connect them
Then the connection fails with ConnectionError::IncompatibleFluid
AC 3: Pressure Continuity Validation
Given two ports with significantly different pressure values
When I attempt to connect them
Then the connection fails with ConnectionError::PressureMismatch
AC 4: Enthalpy Continuity Validation
Given two ports with significantly different enthalpy values
When I attempt to connect them
Then the connection fails with ConnectionError::EnthalpyMismatch
AC 5: Successful Connection
Given two compatible ports (same fluid, matching pressure/enthalpy within tolerance)
When I connect them
Then I receive two Port<Connected> instances with averaged thermodynamic values
AC 6: Connected Port Operations
Given a Port<Connected> instance
When I access its properties
Then I can read pressure(), enthalpy(), and modify them via set_pressure(), set_enthalpy()
AC 7: Compile-Time Type Safety
Given a Port<Disconnected> instance
When code attempts to call pressure() or set_pressure() on it
Then the compilation fails (Type-State pattern enforcement)
AC 8: Component Trait Integration
Given the Component trait
When I implement it for a component
Then I can provide get_ports() to expose the component's connection points
Tasks / Subtasks
-
Task 1: Define Type-State Pattern Foundation (AC: 1, 7)
- Create
DisconnectedandConnectedmarker structs - Define generic
Port<State>struct withPhantomData - Implement zero-cost state tracking
- Create
-
Task 2: Implement Port Creation (AC: 1)
- Create
Port::new()constructor forPort<Disconnected> - Store
fluid_id: FluidId,pressure: Pressure,enthalpy: Enthalpy
- Create
-
Task 3: Define Connection Errors (AC: 2, 3, 4)
- Create
ConnectionErrorenum withthiserror - Add
IncompatibleFluid { from, to }variant - Add
PressureMismatch { from_pressure, to_pressure }variant - Add
EnthalpyMismatch { from_enthalpy, to_enthalpy }variant
- Create
-
Task 4: Implement Connection Logic (AC: 2, 3, 4, 5)
- Add
Port<Disconnected>::connect()method - Validate fluid compatibility
- Validate pressure continuity (tolerance: 1e-6 Pa)
- Validate enthalpy continuity (tolerance: 1e-6 J/kg)
- Return averaged values on success
- Add
-
Task 5: Implement Connected Port Operations (AC: 6)
- Add
pressure()getter toPort<Connected> - Add
enthalpy()getter toPort<Connected> - Add
set_pressure()setter toPort<Connected> - Add
set_enthalpy()setter toPort<Connected> - Add
fluid_id()accessor (available in both states)
- Add
-
Task 6: Extend Component Trait (AC: 8)
- Add
get_ports()method toComponenttrait - Return appropriate port references for each component type
- Add
-
Task 7: Write Tests
- Test port creation with valid parameters
- Test connection with compatible ports
- Test error handling for incompatible fluids
- Test error handling for pressure mismatches
- Test error handling for enthalpy mismatches
- Test value modification on connected ports
- Add compile-time safety verification test
Dev Notes
Type-State Pattern Implementation
The Type-State pattern uses Rust's type system to encode state machines at compile time:
// Zero-cost marker types
pub struct Disconnected;
pub struct Connected;
// Generic Port with state parameter
pub struct Port<State> {
fluid_id: FluidId,
pressure: Pressure,
enthalpy: Enthalpy,
_state: PhantomData<State>, // Zero-cost at runtime
}
// Only Disconnected ports can be connected
impl Port<Disconnected> {
pub fn connect(self, other: Port<Disconnected>)
-> Result<(Port<Connected>, Port<Connected>), ConnectionError> {
// Validation logic...
}
}
// Only Connected ports expose mutable operations
impl Port<Connected> {
pub fn pressure(&self) -> Pressure { self.pressure }
pub fn set_pressure(&mut self, pressure: Pressure) { self.pressure = pressure }
}
Connection Validation Logic
pub fn connect(self, other: Port<Disconnected>)
-> Result<(Port<Connected>, Port<Connected>), ConnectionError>
{
// 1. Fluid compatibility check
if self.fluid_id != other.fluid_id {
return Err(ConnectionError::IncompatibleFluid {
from: self.fluid_id.to_string(),
to: other.fluid_id.to_string()
});
}
// 2. Pressure continuity (1e-6 Pa tolerance)
let pressure_diff = (self.pressure.to_pascals() - other.pressure.to_pascals()).abs();
if pressure_diff > 1e-6 {
return Err(ConnectionError::PressureMismatch {
from_pressure: self.pressure.to_pascals(),
to_pressure: other.pressure.to_pascals()
});
}
// 3. Enthalpy continuity (1e-6 J/kg tolerance)
let enthalpy_diff = (self.enthalpy.to_joules_per_kg() - other.enthalpy.to_joules_per_kg()).abs();
if enthalpy_diff > 1e-6 {
return Err(ConnectionError::EnthalpyMismatch {
from_enthalpy: self.enthalpy.to_joules_per_kg(),
to_enthalpy: other.enthalpy.to_joules_per_kg()
});
}
// 4. Create connected ports with averaged values
let avg_pressure = Pressure::from_pascals(
(self.pressure.to_pascals() + other.pressure.to_pascals()) / 2.0
);
let avg_enthalpy = Enthalpy::from_joules_per_kg(
(self.enthalpy.to_joules_per_kg() + other.enthalpy.to_joules_per_kg()) / 2.0
);
Ok((
Port {
fluid_id: self.fluid_id,
pressure: avg_pressure,
enthalpy: avg_enthalpy,
_state: PhantomData,
},
Port {
fluid_id: other.fluid_id,
pressure: avg_pressure,
enthalpy: avg_enthalpy,
_state: PhantomData,
}
))
}
Testing Approach
Use the approx crate for floating-point assertions:
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_port_creation() {
let port = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(1.0),
Enthalpy::from_joules_per_kg(400_000.0)
);
assert_eq!(port.fluid_id().to_string(), "R134a");
}
#[test]
fn test_incompatible_fluid_error() {
let port1 = Port::new(FluidId::new("R134a"), p1, h1);
let port2 = Port::new(FluidId::new("Water"), p1, h1);
match port1.connect(port2) {
Err(ConnectionError::IncompatibleFluid { .. }) => (), // Expected
_ => panic!("Expected IncompatibleFluid error"),
}
}
}
Compile-Time Safety Verification
Create a test that intentionally fails to compile:
#[test]
fn test_compile_time_safety() {
let port: Port<Disconnected> = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(1.0),
Enthalpy::from_joules_per_kg(400_000.0)
);
// This line should NOT compile - uncomment to verify:
// let _p = port.pressure(); // ERROR: no method `pressure` on Port<Disconnected>
// This should work after connection:
let port2 = Port::new(FluidId::new("R134a"), Pressure::from_bar(1.0), Enthalpy::from_joules_per_kg(400_000.0));
let (mut connected, _) = port.connect(port2).unwrap();
let _p = connected.pressure(); // OK: Port<Connected> has pressure()
}
Project Structure Notes
File Locations
crates/
├── components/
│ ├── Cargo.toml # Add: entropyk-core dependency, approx dev-dependency
│ └── src/
│ ├── lib.rs # Add: pub mod port; extend Component trait
│ └── port.rs # NEW: Port implementation
│
└── core/
└── src/
└── types.rs # Pressure, Enthalpy, FluidId definitions
Dependencies to Add
In crates/components/Cargo.toml:
[dependencies]
entropyk-core = { path = "../core" }
thiserror = "1.0"
[dev-dependencies]
approx = "0.5"
Alignment with Project Structure
- Path convention: All new code in
crates/components/src/ - Naming: Module name
port.rsmatches functionality - Trait extension:
Componenttrait extended withget_ports()method - Error handling: Uses
thiserrorcrate (standard in Rust ecosystem) - Testing: Uses
approxfor floating-point comparisons (recommended for thermodynamic calculations)
References
Technical Documentation
- [Source: README_STORY_1_3.md] - Original implementation documentation
- [Source: demo/README.md] - Feature status tracking (Story 1.3: Ports & Connexions)
- [Source: docs/TUTORIAL.md#5] - Port system usage in tutorial
External Resources
- Rust Type-State Pattern - Official Rust patterns documentation
- thiserror Documentation - Error handling derive macro
- approx Documentation - Floating-point assertion macros
Related Stories
- Story 1.1: Component Trait (foundation trait extended with get_ports)
- Story 1.2: Physical Types (Pressure, Enthalpy, Temperature)
- Story 1.4: Compressor (first component using port system)
Dev Agent Record
Agent Model Used
N/A - Story documentation created retroactively for existing implementation.
Debug Log References
N/A - Implementation completed prior to story documentation.
Completion Notes List
- ✅ Type-State pattern successfully prevents runtime port state errors
- ✅ All validation rules (fluid, pressure, enthalpy) implemented
- ✅ Tests cover success and error cases
- ✅ Component trait extended with
get_ports()method - ✅ Documentation includes compile-time safety verification example
File List
Created/Modified:
crates/components/src/port.rs- Port implementation with Type-State patterncrates/components/src/lib.rs- Extended Component trait with get_ports()crates/components/Cargo.toml- Added entropyk-core dependency, thiserror, approx
Dependencies:
entropyk-core- Physical types (Pressure, Enthalpy, FluidId)thiserror- Error derive macroapprox- Floating-point test assertions (dev dependency)
Story Completion Status: ready-for-dev
Ultimate context engine analysis completed - comprehensive developer guide created
Date: 2026-02-22
Created by: create-story workflow (BMAD v6.0.1)