Files
Entropyk/_bmad-output/implementation-artifacts/1-4-compressor-component-ahri-540.md
sepehr 5bd180b5b8
Some checks failed
CI / check (push) Has been cancelled
Snapshot WIP: solver HP epic progress, BPHX/HX physics, BMAD skill refresh.
Capture uncommitted solver robustness work (regularization, domain errors, linear solver lifecycle, tube DP/MSH), web workbench updates, and synced BMAD skills across IDE agent folders before starting BPHX pressure-drop.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-19 16:35:31 +02:00

374 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)**