Entropyk/1-3-port-and-connection-system.md

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 Disconnected and Connected marker structs
    • Define generic Port<State> struct with PhantomData
    • Implement zero-cost state tracking
  • Task 2: Implement Port Creation (AC: 1)

    • Create Port::new() constructor for Port<Disconnected>
    • Store fluid_id: FluidId, pressure: Pressure, enthalpy: Enthalpy
  • Task 3: Define Connection Errors (AC: 2, 3, 4)

    • Create ConnectionError enum with thiserror
    • Add IncompatibleFluid { from, to } variant
    • Add PressureMismatch { from_pressure, to_pressure } variant
    • Add EnthalpyMismatch { from_enthalpy, to_enthalpy } variant
  • 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
  • Task 5: Implement Connected Port Operations (AC: 6)

    • Add pressure() getter to Port<Connected>
    • Add enthalpy() getter to Port<Connected>
    • Add set_pressure() setter to Port<Connected>
    • Add set_enthalpy() setter to Port<Connected>
    • Add fluid_id() accessor (available in both states)
  • Task 6: Extend Component Trait (AC: 8)

    • Add get_ports() method to Component trait
    • Return appropriate port references for each component type
  • 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.rs matches functionality
  • Trait extension: Component trait extended with get_ports() method
  • Error handling: Uses thiserror crate (standard in Rust ecosystem)
  • Testing: Uses approx for 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

  • 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

  1. Type-State pattern successfully prevents runtime port state errors
  2. All validation rules (fluid, pressure, enthalpy) implemented
  3. Tests cover success and error cases
  4. Component trait extended with get_ports() method
  5. Documentation includes compile-time safety verification example

File List

Created/Modified:

  • crates/components/src/port.rs - Port implementation with Type-State pattern
  • crates/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 macro
  • approx - 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)