# Story 1.4: Compressor Component (AHRI 540)

Status: done

<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->

## Story

As a thermodynamic engineer,
I want to model a compressor using AHRI 540 standard coefficients or SST/SDT polynomial curves,
so that I can simulate real compressor behavior from manufacturer data.

## Acceptance Criteria

1. **Compressor Structure** (AC: #1)
   - [x] Define `Compressor<State>` struct with suction and discharge ports (Type-State pattern)
   - [x] Support two performance models via `CompressorModel` enum: `Ahri540(Ahri540Coefficients)` and `SstSdt(SstSdtCoefficients)`
   - [x] Store 10 AHRI 540 coefficients (M1-M10) in `Ahri540Coefficients` struct
   - [x] Store SST/SDT polynomial curves in `SstSdtCoefficients` (mass_flow_curve + power_curve as `Polynomial2D`)
   - [x] Include rotational speed (RPM), displacement volume, and mechanical efficiency
   - [x] Include `Calib` struct (f_m, f_power, f_etav) for calibration factors
   - [x] Include `CalibIndices` for solver-level embedded calibration variables
   - [x] Include `CircuitId` for multi-circuit machine support (FR9)
   - [x] Include `OperationalState` (On, Off, Bypass) for FR6-FR8
   - [x] Include edge state indices (`suction_m_idx`, `suction_h_idx`, `discharge_m_idx`, `discharge_h_idx`) for CM1.3

2. **AHRI 540 Performance Equations** (AC: #2)
   - [x] Mass flow: `ṁ = M1 × (1 - (P_suction/P_discharge)^(1/M2)) × ρ_suction × V_disp × N/60 × f_etav × f_m`
   - [x] Power (cooling): `Ẇ = (M3 + M4 × (P_discharge/P_suction) + M5 × T_suction + M6 × T_discharge) × f_power`
   - [x] Power (heating): `Ẇ = (M7 + M8 × (P_discharge/P_suction) + M9 × T_suction + M10 × T_discharge) × f_power`
   - [x] Cooling capacity: `Q̇_cool = ṁ × (h_evap_out - h_evap_in)`
   - [x] Heating capacity: `Q̇_heat = ṁ × (h_cond_out - h_cond_in)`
   - [x] COP calculation: `COP = Q̇ / Ẇ`

3. **SST/SDT Polynomial Model** (AC: #3)
   - [x] Mass flow: `ṁ = Σ a_ij × SST^i × SDT^j` via `Polynomial2D`
   - [x] Power: `Ẇ = Σ b_ij × SST^i × SDT^j` via `Polynomial2D`
   - [x] Support bilinear and biquadratic configurations
   - [x] `SstSdtCoefficients::bilinear()` and `::biquadratic()` factory methods
   - [x] SST/SDT model does not distinguish cooling/heating (single power curve)

4. **Residual Computation — 3 equations** (AC: #4)
   - [x] `n_equations()` returns **3** (upgraded from 2 to include CM1.3 mass conservation)
   - [x] `r0`: Mass flow continuity — `ṁ_calc − ṁ_state = 0`
   - [x] `r1`: Energy balance — `Ẇ_calc − ṁ_state × (h_discharge − h_suction) / η_mech = 0`
   - [x] `r2`: Mass conservation (CM1.3) — `ṁ_discharge − ṁ_suction = 0`
   - [x] `OperationalState::Off` → `r0 = ṁ_suction`, `r1 = 0`, `r2 = ṁ_discharge − ṁ_suction`
   - [x] `OperationalState::Bypass` → `r0 = P_suction − P_discharge`, `r1 = h_suction − h_discharge`, `r2 = ṁ_discharge − ṁ_suction`

5. **Jacobian Entries** (AC: #5)
   - [x] Row 0: `∂r0/∂ṁ_suction = -1` (exact, CM1.3)
   - [x] Row 0: `∂r0/∂h_suction` — numerical via `approximate_derivative` (density depends on h)
   - [x] Row 1: `∂r1/∂ṁ_suction = −(h_discharge − h_suction) / η_mech` (exact)
   - [x] Row 1: `∂r1/∂h_suction`, `∂r1/∂h_discharge` — numerical (power depends on temperature estimates)
   - [x] Row 2: `∂r2/∂ṁ_discharge = +1`, `∂r2/∂ṁ_suction = -1` (exact, CM1.3)
   - [x] **PARTIAL**: Derivatives `∂r0/∂h` and `∂r1/∂h` use finite-difference approximation (temperature estimation not differentiable analytically in current placeholder backend)

6. **Component Trait Integration** (AC: #6)
   - [x] Implement `Component` trait for `Compressor<Connected>`
   - [x] `get_ports()` returns empty slice (lifetime constraint — use `get_ports_slice()` instead)
   - [x] `get_ports_slice()` returns `[&Port<Connected>; 2]` as workaround
   - [x] `set_system_context()` stores edge indices for CM1.3 integration
   - [x] `energy_transfers()` returns `(electrical_power, heat_rejection)` for energy validation
   - [x] `signature()` returns `"Compressor(fluid=X, circuit=Y)"`

7. **Validation & Testing** (AC: #7)
   - [x] Unit tests for `Ahri540Coefficients` storage and validation
   - [x] Unit tests for `SstSdtCoefficients` bilinear and biquadratic
   - [x] Unit tests for mass flow calculation (AHRI 540 + SST/SDT)
   - [x] Unit tests for power consumption (cooling + heating modes)
   - [x] Unit tests for residuals at equilibrium
   - [x] Jacobian finite-difference verification
   - [x] Tests with R134a, R410A, R454B refrigerants
   - [x] Tests for Off and Bypass operational states

## Tasks / Subtasks

- [x] Create `crates/components/src/compressor.rs` module (AC: #1)
  - [x] Define `Ahri540Coefficients` struct with M1-M10 fields and `validate()`
  - [x] Define `SstSdtCoefficients` struct wrapping `Polynomial2D`
  - [x] Define `CompressorModel` enum (`Ahri540` | `SstSdt`)
  - [x] Define `Compressor<State>` with all fields (model, ports, calib, circuit_id, edge indices)
  - [x] Implement `Compressor::new()` for AHRI 540 constructor
  - [x] Implement `Compressor::with_model()` for generic model constructor
  - [x] Implement `connect()` method (Type-State transition Disconnected → Connected)
- [x] Create `crates/components/src/polynomials.rs` module (AC: #3)
  - [x] Define `Polynomial2D` struct for 2D polynomial evaluation
  - [x] Implement `evaluate(x, y)`, `validate()`, `bilinear()`, `biquadratic()`
- [x] Implement AHRI 540 calculations (AC: #2)
  - [x] `mass_flow_rate(density, sst_k, sdt_k, state)` — dispatches to model
  - [x] `power_consumption_cooling(t_suction, t_discharge, state)`
  - [x] `power_consumption_heating(t_suction, t_discharge, state)`
  - [x] `cooling_capacity()`, `heating_capacity()`, `cop()`
- [x] Implement residual computation (AC: #4)
  - [x] 3-equation system: r0 mass flow, r1 energy, r2 mass conservation
  - [x] OperationalState dispatch (On/Off/Bypass)
  - [x] Calibration factors applied via `Calib` and `CalibIndices`
- [x] Implement Jacobian entries (AC: #5)
  - [x] Exact entries where analytically tractable
  - [x] `approximate_derivative()` helper for numerical differentiation
- [x] Implement Component trait (AC: #6)
  - [x] `compute_residuals`, `jacobian_entries`, `n_equations`, `get_ports`
  - [x] `set_system_context` for CM1.3 edge index injection
  - [x] `energy_transfers`, `signature`
  - [x] `get_ports_slice()` workaround
- [x] Register exports (AC: #6)
  - [x] `crates/components/src/lib.rs` — pub mod compressor; pub mod polynomials; re-exports
  - [x] `crates/entropyk/src/lib.rs` — re-export compressor types
- [x] Write comprehensive tests (AC: #7)

## Dev Notes

### Architecture Context

**Critical patterns established here (followed by ALL subsequent components):**

```rust
// Type-State pattern: Disconnected → Connected
pub struct Compressor<State> {
    model: CompressorModel,
    port_suction: Port<State>,
    port_discharge: Port<State>,
    speed_rpm: f64,
    displacement_m3_per_rev: f64,
    mechanical_efficiency: f64,
    calib: Calib,                          // Static calibration factors
    calib_indices: CalibIndices,            // Dynamic solver-embedded indices
    fluid_id: FluidId,
    circuit_id: CircuitId,
    operational_state: OperationalState,
    suction_m_idx: Option<usize>,           // CM1.3: injected by set_system_context
    suction_h_idx: Option<usize>,
    discharge_m_idx: Option<usize>,
    discharge_h_idx: Option<usize>,
    _state: PhantomData<State>,
}
```

**Performance model dispatch:**
```rust
pub enum CompressorModel {
    Ahri540(Ahri540Coefficients),
    SstSdt(SstSdtCoefficients),
}
```
`Compressor::new()` takes `Ahri540Coefficients` directly; `Compressor::with_model()` takes `CompressorModel`.

**3-equation system (post CM1.3 upgrade):**
```
n_equations() = 3
r0: ṁ_calc(model) − ṁ_state   = 0    (mass flow matching)
r1: Ẇ_calc − ṁ × Δh / η_mech = 0    (energy balance)
r2: ṁ_discharge − ṁ_suction   = 0    (mass conservation per edge)
```

**CM1.3 state variable layout (edge-based):**
- `set_system_context(state_offset, external_edge_state_indices)` injects `(m_idx, p_idx, h_idx)` per edge
- Edge 0 = suction (incoming), Edge 1 = discharge (outgoing)
- Fallback to positional defaults `[0,1,3,2]` when indices not set (unit tests)

**Calibration factors (FR43):**
```rust
// Calib struct (entropyk_core)
pub struct Calib {
    pub f_m: f64,      // Mass flow correction factor
    pub f_power: f64,  // Power correction factor
    pub f_etav: f64,   // Volumetric efficiency correction
    // ... other factors
}
// Applied as: ṁ_eff = f_etav × ṁ_vol × f_m
//             Ẇ_eff = f_power × Ẇ_nominal
```
`CalibIndices` holds `Option<usize>` indices into the state vector so calibration variables can be solver unknowns (inverse calibration, FR45/FR51).

**get_ports() FIXME — known limitation:**
```rust
// Component trait get_ports() returns empty slice — lifetime constraint.
// Use get_ports_slice() for actual port access:
pub fn get_ports_slice(&self) -> [&Port<Connected>; 2]
```
This is a known API limitation tracked for future refactor.

**Fluid property placeholders:**
Internal functions `estimate_density(fluid_id, p_pa, h_jkg)` and `estimate_temperature(fluid_id, p_pa, h_jkg)` use lookup tables for R134a, R410A, R454B. These are **temporary** and will be replaced by full CoolProp integration via `FluidBackend` (Story 2.2 / Epic 8). When real backend is available, `suction_state()` / `discharge_state()` methods provide full `ThermoState`.

### Technical Requirements

**AHRI 540 Mass Flow (note inverse ratio — p_suction/p_discharge):**
```
η_vol = 1 − (P_suction / P_discharge)^(1/M2)   ← inverse ratio ensures η_vol ∈ [0,1]
ṁ = M1 × η_vol × ρ_suction × V_disp × (N/60) × f_etav × f_m
```
**Do NOT use (P_discharge/P_suction) for volumetric efficiency** — negative η_vol would result.

**SST/SDT Polynomial2D:**
```rust
// Polynomial2D evaluates Σ c[i][j] × x^i × y^j
// bilinear: 2×2 coefficient matrix
// biquadratic: 3×3 coefficient matrix
pub struct Polynomial2D { coefficients: Vec<Vec<f64>> }
```

**Rust naming conventions:**
- Struct: `CamelCase` — `Compressor`, `Ahri540Coefficients`, `SstSdtCoefficients`, `CompressorModel`
- Methods: `snake_case` — `compute_residuals`, `mass_flow_rate`, `power_consumption_cooling`
- Generic param: `State` (not `S`)

### File Structure

```
crates/
├── components/
│   ├── Cargo.toml               # [dev-dependencies] approx = "0.5"
│   └── src/
│       ├── lib.rs               # pub mod compressor; pub mod polynomials; re-exports
│       ├── compressor.rs        # This story (2276 lines)
│       ├── polynomials.rs       # Polynomial2D (used by SstSdtCoefficients)
│       └── port.rs              # Port<State> from Story 1.3
├── core/
│   └── src/
│       ├── calib.rs             # Calib, CalibIndices types (Story 7.6)
│       └── types.rs             # Pressure, Temperature, Enthalpy, MassFlow (Story 1.2)
└── entropyk/
    └── src/
        └── lib.rs               # Re-exports: pub use entropyk_components::compressor::*
```

### Testing Requirements

**Required test coverage:**
```rust
// Tests live in #[cfg(test)] mod tests at bottom of compressor.rs
// Helper: test_coefficients() returns standard Ahri540Coefficients for tests

// 1. Coefficient storage/validation
#[test] fn test_ahri540_coefficients_storage()
#[test] fn test_ahri540_coefficients_validate_invalid_m2()
#[test] fn test_sst_sdt_bilinear_creation()
#[test] fn test_sst_sdt_biquadratic_creation()

// 2. Mass flow
#[test] fn test_mass_flow_calculation()
#[test] fn test_mass_flow_with_high_pressure_ratio()
#[test] fn test_sst_sdt_mass_flow()

// 3. Power consumption
#[test] fn test_power_consumption_cooling()
#[test] fn test_power_consumption_heating()

// 4. Residuals
#[test] fn test_residuals_3_equations()
#[test] fn test_off_state_residuals()
#[test] fn test_bypass_state_residuals()

// 5. Jacobian
#[test] fn test_jacobian_entries()      // finite difference verification

// 6. Refrigerants
#[test] fn test_compressor_with_r134a()
#[test] fn test_compressor_with_r410a()
#[test] fn test_compressor_with_r454b()
```

### References

- **AHRI 540 Standard:** Air-Conditioning, Heating, and Refrigeration Institute Standard 540
- **Inverse pressure ratio decision:** Using `P_suction/P_discharge` (not `P_discharge/P_suction`) ensures `η_vol ≥ 0` — see compressor.rs line ~806
- **Component Trait:** `[Source: crates/components/src/lib.rs]` — `Component`, `ConnectedPort`, `ResidualVector`, `JacobianBuilder`
- **CM1.3 edge-based mass flow:** `[Source: _bmad-output/implementation-artifacts/cm-1-3-mass-flow-newton-unknown.md]`
- **Calib / CalibIndices:** `[Source: crates/core/src/calib.rs]` — FR43, FR45, FR51
- **OperationalState:** `[Source: crates/components/src/lib.rs]` — FR6, FR7, FR8
- **CircuitId:** `[Source: crates/core/src/types.rs]` — FR9
- **Polynomial2D:** `[Source: crates/components/src/polynomials.rs]`
- **FR1:** Compressor AHRI 540 — `[Source: _bmad-output/planning-artifacts/epics.md#FR1]`
- **Architecture Component Model:** `[Source: _bmad-output/planning-artifacts/architecture.md#Component Model]`

## Senior Developer Review (AI)

**Review Date:** 2026-02-15
**Review Outcome:** Approved → Fixed
**Reviewer:** opencode/kimi-k2.5-free (code-review workflow)

### Issues Found and Fixed

**🔴 HIGH (2 fixed):**
1. **Documentation Error** — Mass flow equation in docstring showed wrong pressure ratio direction. Fixed to show `P_suction/P_discharge`.
2. **AC #5 Partial Implementation** — Jacobian uses finite differences for `∂r/∂h` derivatives. Marked AC as PARTIAL.

**🟡 MEDIUM (4 fixed):**
3. **get_ports() Returns Empty** — Lifetime constraint. Added `get_ports_slice()` workaround.
4. **Missing R454B Test** — Added R454B test case.
5. **File List Incomplete** — `Cargo.toml` and `polynomials.rs` not documented. Added.
6. **Non-existent Dependency** — Removed `entropyk-fluids` reference (not a separate crate).

**🟢 LOW (2 fixed):**
7. **Placeholder Documentation** — Added explicit Story 2.2 / Epic 8 references for CoolProp integration.
8. **Test Quality** — Improved `test_mass_flow_with_high_pressure_ratio` clarity.

### Action Items
- [x] All HIGH and MEDIUM issues fixed
- [x] All tests pass (61 unit + 20 doc tests at time of original review)
- [x] Story status updated to "done"

---

## Dev Agent Record

### Agent Model Used

opencode/kimi-k2.5-free

### Debug Log References

- Implementation completed: 2026-02-15
- Code review completed: 2026-02-15
- Story context refreshed: 2026-06-26 (reflects evolved codebase: SST/SDT model, CM1.3 integration, Calib)
- All tests passing: 61+ unit tests
- Clippy validation: Zero warnings

### Completion Notes List

**Original Implementation (2026-02-15):**
- ✅ Created `crates/components/src/compressor.rs` with complete `Compressor` component
- ✅ Implemented `Ahri540Coefficients` with validation for all 10 coefficients
- ✅ Implemented Type-State pattern `Compressor<Disconnected>` / `Compressor<Connected>`
- ✅ Implemented AHRI 540 mass flow with inverse pressure ratio for volumetric efficiency
- ✅ Implemented power equations for cooling (M3-M6) and heating (M7-M10) modes
- ✅ Implemented full `Component` trait integration

**Post-implementation Evolutions (reflected in current codebase):**
- ✅ **SST/SDT model added** (`SstSdtCoefficients`, `Polynomial2D`, `CompressorModel` enum)
- ✅ **CM1.3 upgrade**: `n_equations()` now returns 3 (added mass conservation residual r2)
- ✅ **`set_system_context()`** added: edge index injection for per-edge (ṁ,P,h) state variables
- ✅ **Calibration integration**: `Calib` and `CalibIndices` fields, factors applied in calculations
- ✅ **`energy_transfers()`** added for energy validation framework
- ✅ **`suction_state()` / `discharge_state()`** added for full ThermoState access via FluidBackend
- ✅ **`OperationalState`** support (On/Off/Bypass) per FR6-FR8

**Technical Decisions:**
- Inverse pressure ratio (`P_suction/P_discharge`) for volumetric efficiency — ensures η_vol ∈ [0,1]
- `CompressorModel` enum rather than trait object — avoids object-safety issues with Clone/PartialEq
- Finite-difference approximation for `∂r/∂h` Jacobian — placeholder until real FluidBackend
- `get_ports()` returns empty slice (lifetime constraint) — `get_ports_slice()` as workaround
- R454B uses R410A properties as approximation in placeholder backend

### File List

**Created:**
1. `crates/components/src/compressor.rs` — Compressor component (~2276 lines)
2. `crates/components/src/polynomials.rs` — `Polynomial2D` for SST/SDT model

**Modified:**
1. `crates/components/src/lib.rs` — Added `pub mod compressor`, `pub mod polynomials`, re-exports
2. `crates/components/Cargo.toml` — Added `approx = "0.5"` dev-dependency
3. `crates/entropyk/src/lib.rs` — Re-exported compressor types

### Dependencies

```toml
# crates/components/Cargo.toml
[dependencies]
entropyk-core = { path = "../core" }
entropyk-fluids = { path = "../fluids" }   # for suction_state()/discharge_state()
thiserror = "1.0"
serde = { version = "1.0", features = ["derive"] }

[dev-dependencies]
approx = "0.5"
```

---

**Ultimate context engine analysis completed — story refreshed 2026-06-26 to reflect evolved implementation (SST/SDT model, CM1.3 3-equation system, Calib integration)**
