# Seasonal & Part-Load Ratings — IPLV / NPLV / ESEER / SCOP / SEER > Modular, data-driven standardized ratings for chillers and heat pumps. > > Crate: [`entropyk::rating`](../crates/entropyk/src/rating.rs) · CLI: > [`entropyk-cli rate`](../crates/cli/src/rate.rs) This document is the definitive reference for how Entropyk turns a machine's part-load performance into the **standardized seasonal metrics** used to qualify HVAC/R equipment, and — most importantly — how those standards are kept **modular** so that when a norm is revised (AHRI, Eurovent and EN periodically re-fit their coefficients and climate tables) you can adapt **without recompiling**. --- ## Table of contents 1. [Concepts & scope](#1-concepts--scope) 2. [Design philosophy: standards are data, not code](#2-design-philosophy-standards-are-data-not-code) 3. [Library API (`entropyk::rating`)](#3-library-api-entropykrating) - [`PartLoadStandard`](#31-partloadstandard) - [`PartLoadEfficiencies`](#32-partloadefficiencies) - [`BinClimateStandard`, `BinPerformance`, `scop`](#33-binclimatestandard-binperformance-and-scop) - [`RatingCondition`](#34-ratingcondition) - [`RatingError`](#35-ratingerror) - [Legacy constants](#36-legacy-constants) 4. [The `rate` CLI command](#4-the-rate-cli-command) - [Configuration schema](#41-configuration-schema) - [Selecting the standard (precedence)](#42-selecting-the-standard-precedence) - [How a part-load point is solved](#43-how-a-part-load-point-is-solved) - [Report format](#44-report-format) - [Worked examples](#45-worked-examples) 5. [Custom standard JSON schema](#5-custom-standard-json-schema) 6. [How to change a standard when the norm changes](#6-how-to-change-a-standard-when-the-norm-changes) 7. [The `scop` / `seer` CLI commands (bin method)](#7-the-scop--seer-cli-commands-bin-method) - [Configuration schema](#71-configuration-schema) - [How a bin is solved](#72-how-a-bin-is-solved) - [Report format](#73-report-format) - [Worked example (SCOP)](#74-worked-example-scop) - [Adding another bin-based metric](#75-adding-another-bin-based-metric) 8. [Validation & testing](#8-validation--testing) 9. [Reference tables](#9-reference-tables) 10. [Standards references](#10-standards-references) --- ## 1. Concepts & scope A single full-load efficiency figure (EER or COP at rated conditions) is a poor predictor of real energy use, because equipment spends most of its life at part load. Regulators therefore define **seasonal** metrics that weight several part-load operating points: | Metric | Family | Standard | What it weights | | :--- | :--- | :--- | :--- | | **IPLV** — Integrated Part Load Value | Weighted part-load | AHRI 550/590 | EER at 100/75/50/25 % load | | **NPLV** — Non-standard Part Load Value | Weighted part-load | AHRI 550/590 | Same formula as IPLV, taken at non-standard conditions | | **ESEER** — European SEER | Weighted part-load | Eurovent | EER at 100/75/50/25 % load, different weights | | **SCOP** — Seasonal COP | Temperature-bin | EN 14825 | COP across a climate's hourly temperature bins | | **SEER** — Seasonal EER | Temperature-bin / weighted | EN 14825 | EER across the cooling season | Entropyk models these as **two calculation families**: - **Weighted part-load** (`IPLV`, `NPLV`, `ESEER`, and weighted SEER variants): a weighted average of efficiencies measured at a small set of load fractions. Implemented by [`PartLoadStandard`](#31-partloadstandard). - **Temperature-bin** (`SCOP`, bin-based `SEER`): energy summed over a climate's hourly temperature bins, `Σ(h·demand) / Σ(h·demand/COP)`. Implemented by [`BinClimateStandard`](#33-binclimatestandard-binperformance-and-scop) + [`scop`](#33-binclimatestandard-binperformance-and-scop). > **Purity guarantee.** All the metric arithmetic takes *already-solved* > efficiency values as input. Computing the part-load operating points — by > re-solving the coupled cycle at each rating condition — is the caller's job (the > `rate` CLI command does this). This keeps the metric math deterministic and > trivially unit-testable, and keeps the *physics* honest: nothing is imposed, all > efficiencies emerge from a genuine coupled solve. --- ## 2. Design philosophy: standards are data, not code Rating standards change. AHRI and Eurovent re-fit their part-load weights; EN 14825 revises its climate bins and reference seasons. If those coefficients were hard-coded arithmetic, every revision would mean a code change, a rebuild, and a re-release. Instead, a standard in Entropyk is a **plain data record**: - A weighted part-load standard is just `{ load_fractions, weights }` plus a name and a citation. - A climate standard is just `{ bins }` (temperature + annual hours) plus a name and a citation. Because they are data, a standard can be: - a **built-in preset** (a function returning the record — e.g. `PartLoadStandard::ahri_550_590_iplv()`), - looked up by **name** in a small registry (`PartLoadStandard::builtin("iplv")`), - or **loaded from a JSON file** at run time and validated (`serde_json::from_str::(...)?.validate()?`). The number of load points is **arbitrary** (four for AHRI/Eurovent, but any N is accepted), so a future standard with more or fewer points needs no code change. --- ## 3. Library API (`entropyk::rating`) ```rust use entropyk::rating::{ PartLoadStandard, PartLoadEfficiencies, BinClimateStandard, BinPerformance, TemperatureBin, RatingCondition, RatingError, scop, }; ``` ### 3.1 `PartLoadStandard` The data-driven weighting standard behind IPLV / NPLV / ESEER / weighted SEER. ```rust pub struct PartLoadStandard { pub name: String, // e.g. "AHRI 550/590 IPLV" pub reference: String, // citation / provenance (optional in JSON) pub load_fractions: Vec, // e.g. [1.0, 0.75, 0.50, 0.25] pub weights: Vec, // same length; must sum to 1.0 } ``` **Construction & validation** | Method | Description | | :--- | :--- | | `new(name, reference, load_fractions, weights) -> Result` | Build **and** validate in one step. | | `validate() -> Result<(), RatingError>` | Non-empty, equal-length fractions/weights, weights sum to `1.0` within `1e-6`. | | `len() -> usize` / `is_empty() -> bool` | Number of load points. | **Integration** | Method | Description | | :--- | :--- | | `integrate(&self, efficiencies: &[f64]) -> Result` | Weighted sum `Σ eᵢ·wᵢ`. `efficiencies` must match `weights` in count **and order**. | **Built-in presets & registry** | Method | Returns | | :--- | :--- | | `ahri_550_590_iplv()` | AHRI IPLV/NPLV — weights `[0.01, 0.42, 0.45, 0.12]`. | | `eurovent_eseer()` | Eurovent ESEER — weights `[0.03, 0.33, 0.41, 0.23]`. | | `builtin(id: &str) -> Option` | Case/separator-insensitive lookup (see ids below). | | `builtin_ids() -> &'static [&'static str]` | `["iplv", "nplv", "eseer"]` (for help text). | Recognised `builtin` ids (case-insensitive; spaces, `-` and `/` are normalised to `_`): | Id(s) | Standard | | :--- | :--- | | `iplv`, `nplv`, `ahri`, `ahri_550_590`, `ahri_551_591` | AHRI 550/590 IPLV | | `eseer`, `eurovent` | Eurovent ESEER | **Example — built-in** ```rust let iplv = PartLoadStandard::ahri_550_590_iplv(); let value = iplv.integrate(&[4.0, 5.0, 6.0, 5.5]).unwrap(); // value == 0.01*4.0 + 0.42*5.0 + 0.45*6.0 + 0.12*5.5 ``` **Example — custom, from JSON, no code change** ```rust let json = r#"{ "name": "Custom SEER weighting", "reference": "EN 14825 moderate cooling season (illustrative)", "load_fractions": [1.0, 0.74, 0.47, 0.21], "weights": [0.03, 0.27, 0.41, 0.29] }"#; let std: PartLoadStandard = serde_json::from_str(json)?; std.validate()?; let seer = std.integrate(&[3.0, 4.0, 5.0, 4.5])?; ``` ### 3.2 `PartLoadEfficiencies` A convenience holder for the classic **four-point** case, ordered `[100 %, 75 %, 50 %, 25 %]`. ```rust pub struct PartLoadEfficiencies { pub at_100: f64, pub at_75: f64, pub at_50: f64, pub at_25: f64 } ``` | Method | Description | | :--- | :--- | | `new(at_100, at_75, at_50, at_25)` | Construct. | | `as_array() -> [f64; 4]` | `[100, 75, 50, 25]` order. | | `integrate(&self, std: &PartLoadStandard) -> Result` | **Modular entry point** — apply any four-point standard. | | `iplv() -> f64` | Convenience wrapper (AHRI weights). | | `nplv() -> f64` | Alias of `iplv()` (identical arithmetic). | | `eseer() -> f64` | Convenience wrapper (Eurovent weights). | ```rust let eff = PartLoadEfficiencies::new(4.0, 5.2, 6.1, 5.4); let iplv = eff.iplv(); // legacy helper let iplv2 = eff.integrate(&PartLoadStandard::ahri_550_590_iplv())?; // modular assert!((iplv - iplv2).abs() < 1e-12); ``` ### 3.3 `BinClimateStandard`, `BinPerformance` and `scop` The temperature-bin method behind **SCOP** (and bin-based **SEER**). ```rust pub struct TemperatureBin { pub temperature_c: f64, pub hours: f64 } pub struct BinClimateStandard { pub name: String, pub reference: String, pub bins: Vec, } pub struct BinPerformance { pub bin: TemperatureBin, pub demand_w: f64, // building demand at this bin temperature [W] pub cop: f64, // machine COP at this bin temperature } pub fn scop(bins: &[BinPerformance]) -> Option; ``` `BinClimateStandard` methods: | Method | Description | | :--- | :--- | | `en_14825_average()` | EN 14825 **average** heating season (Strasbourg reference, 4910 h). | | `builtin(id) -> Option` | Ids: `en_14825_average`, `average`, `en14825`. | | `builtin_ids() -> &'static [&'static str]` | For help text. | | `total_hours() -> f64` | Sum of `hours` across all bins. | | `validate() -> Result<(), RatingError>` | At least one bin. | `scop(...)` computes: ``` SCOP = Σ (hoursⱼ · demandⱼ) / Σ (hoursⱼ · demandⱼ / COPⱼ) ``` Bins with non-positive hours, demand or COP are ignored. Returns `None` if no valid bin remains (total electrical energy would be zero). **Example — SCOP over a climate** ```rust let climate = BinClimateStandard::en_14825_average(); let bins: Vec = climate.bins.iter().map(|&bin| BinPerformance { bin, demand_w: building_demand_at(bin.temperature_c), // your building model cop: machine_cop_at(bin.temperature_c), // from a coupled solve }).collect(); let scop = scop(&bins).expect("valid climate"); ``` > **Swapping the climate** is just choosing a different `BinClimateStandard` — > a preset, or a JSON file with a different `bins` table (warmer/colder season, a > revised reference table, or a local TMY-derived climate). The `scop` > arithmetic never changes. ### 3.4 `RatingCondition` Named full-load rating envelopes (secondary-fluid temperatures) used to define where the 100 % point is measured. Refrigerant regimes emerge from the coupled heat-exchanger solve, so only the **secondary** conditions are prescribed. ```rust pub struct RatingCondition { pub name: &'static str, pub evap_secondary_out_c: f64, // chilled-fluid supply pub evap_secondary_in_c: f64, // chilled-fluid return pub cond_secondary_in_c: f64, // condenser/gas-cooler entering } ``` Provided constants: | Constant | Chilled supply/return | Condenser entering | | :--- | :--- | :--- | | `AHRI_550_590_WATER_COOLED` | 6.7 / 12.2 °C | 29.4 °C (water) | | `AHRI_550_590_AIR_COOLED` | 6.7 / 12.2 °C | 35.0 °C (air) | | `EN_14511_WATER_COOLED_A` | 7.0 / 12.0 °C | 30.0 °C (water) | | `EN_14511_AIR_COOLED_A` | 7.0 / 12.0 °C | 35.0 °C (air) | ### 3.5 `RatingError` ```rust pub enum RatingError { Empty, // no load points LengthMismatch { fractions: usize, weights: usize }, WeightsNotNormalized { sum: f64 }, // |Σw − 1| > 1e-6 EfficiencyCountMismatch { expected: usize, got: usize }, } ``` Implements `Display` + `std::error::Error`, so it composes with `?` and `anyhow`. ### 3.6 Legacy constants Kept for backward compatibility (the presets are built from them): | Constant | Value | | :--- | :--- | | `STANDARD_LOAD_FRACTIONS` | `[1.0, 0.75, 0.50, 0.25]` | | `IPLV_WEIGHTS` | `[0.01, 0.42, 0.45, 0.12]` | | `ESEER_WEIGHTS` | `[0.03, 0.33, 0.41, 0.23]` | | `EN_14825_AVERAGE_BINS` | 26 bins, −10…+15 °C, sum 4910 h | --- ## 4. The `rate` CLI command `rate` re-solves a full cycle configuration at each standardized part-load point and aggregates the resulting EERs into the chosen seasonal metric. Every point is a **genuine coupled solve** (condensing/evaporating pressures, capacity and power all emerge from the heat-exchanger ↔ secondary balance), so the integrated value is real simulation output — not imposed design points. ```bash # Pretty table to stdout entropyk-cli rate --config crates/cli/examples/rate_chiller_iplv_ahri.json # Also write a JSON report entropyk-cli rate -c rate_config.json -o rate_report.json # Machine-readable JSON only entropyk-cli --quiet rate -c rate_config.json ``` ### 4.1 Configuration schema ```jsonc { "base_config": "chiller_r134a_emergent_pressure.json", // required; path to a `run` scenario // --- standard selection (see precedence below); all optional --- "metric": "iplv", // built-in enum: "iplv" (default) | "eseer" "standard_name": "eseer", // built-in id (overrides `metric`) "standard_file": "my_standard.json", // custom PartLoadStandard JSON (overrides `standard_name`) "standard": { // inline PartLoadStandard (highest precedence) "name": "…", "reference": "…", "load_fractions": [1.0, 0.75, 0.5, 0.25], "weights": [0.01, 0.42, 0.45, 0.12] }, // --- the part-load points (usually four) --- "points": [ { "load_fraction": 1.00, // required; used to match the standard's fractions "condenser_secondary_inlet_c": 29.4, // optional overrides applied to the base config "evaporator_secondary_inlet_c": 12.0, "condenser_secondary_mass_flow_kg_s": 0.36, "evaporator_secondary_mass_flow_kg_s": 0.48, "compressor_speed_hz": 50.0 } // … 0.75, 0.50, 0.25 … ] } ``` **`base_config`** is resolved relative to the rating file's directory when not absolute. **`standard_file`** is likewise resolved relative to the rating file. **Per-point overrides** are written into the base config's matching components before re-solving. They map to these component params (see the [`run` documentation](./CLI_TUTORIAL.md) for the full param list): | Rating override | Component(s) matched | Param set | | :--- | :--- | :--- | | `condenser_secondary_inlet_c` | `Condenser` | `secondary_inlet_temp_c` | | `condenser_secondary_mass_flow_kg_s` | `Condenser` | `secondary_mass_flow_kg_s` | | `evaporator_secondary_inlet_c` | `Evaporator`, `FloodedEvaporator` | `secondary_inlet_temp_c` | | `evaporator_secondary_mass_flow_kg_s` | `Evaporator`, `FloodedEvaporator` | `secondary_mass_flow_kg_s` | | `compressor_speed_hz` | `IsentropicCompressor`, `Compressor` | `speed_hz` | Overrides apply to **all** matching components (single-circuit chillers, the common case, have exactly one each). Anything not overridden is inherited from the base config, so every point runs the *same machine* through a different operating envelope. ### 4.2 Selecting the standard (precedence) Highest to lowest: 1. **`standard`** — an inline `PartLoadStandard` object. 2. **`standard_file`** — a JSON `PartLoadStandard`, resolved relative to the config. 3. **`standard_name`** — a built-in id (`iplv`, `nplv`, `eseer`, …). 4. **`metric`** — the enum (`iplv` default, or `eseer`). The resolved standard is **validated** before use; an invalid standard (bad lengths, weights not summing to 1, unknown id) fails fast with a clear error. ### 4.3 How a part-load point is solved For each point, in parallel (rayon): 1. **Clone** the base `ScenarioConfig`. 2. **Apply overrides** to matching components' params. 3. **Serialize** and **re-solve** via the standard `run` pipeline (`simulate_from_json`). 4. **Extract** `EER = performance.cop`, plus cooling capacity and power. Aggregation then matches each of the standard's `load_fractions` to the **nearest** part-load point (within a tolerance of `0.02`) and integrates with the standard's weights. If any required load fraction has no converged point within tolerance, the integrated value is reported as `null` while the per-point rows are still shown. ### 4.4 Report format ```rust pub struct RateReport { pub base_config: String, pub metric: String, // the standard's NAME, e.g. "AHRI 550/590 IPLV" pub standard_reference: String, // the standard's citation pub points: Vec, // ordered by descending load fraction pub integrated_value: Option, } pub struct RatePointResult { pub load_fraction: f64, pub status: String, // "converged" | "timeout" | "non_converged" | "error" pub eer: Option, pub q_cooling_kw: Option, pub power_kw: Option, pub error: Option, // present only on failure } ``` ### 4.5 Worked examples **Built-in AHRI IPLV** — [`rate_chiller_iplv_ahri.json`](../crates/cli/examples/rate_chiller_iplv_ahri.json): ```bash entropyk-cli rate -c crates/cli/examples/rate_chiller_iplv_ahri.json ``` ```text Base config: chiller_r134a_emergent_pressure.json Standard: AHRI 550/590 IPLV Reference: AHRI Standard 550/590 — Integrated Part Load Value Load[%] Status Q_cool[kW] Power[kW] EER[-] ────────────────────────────────────────────────────────── 100 converged 7.439 1.819 4.090 75 converged 6.363 1.117 5.697 50 converged 4.938 0.542 9.108 25 converged 2.938 0.223 13.183 ────────────────────────────────────────────────────────── AHRI 550/590 IPLV = 8.114 ``` **External custom standard** — [`rate_chiller_custom_standard.json`](../crates/cli/examples/rate_chiller_custom_standard.json) loads its weights from [`standard_custom_iplv.json`](../crates/cli/examples/standard_custom_iplv.json) (`standard_file`). Same points, revised weights `[0.02, 0.40, 0.45, 0.13]`: ```text Standard: Custom IPLV (revised weights) … Custom IPLV (revised weights) = 8.173 ``` The only difference between the two runs is the weighting file — the physics (per-point EERs) is identical. --- ## 5. Custom standard JSON schema A `PartLoadStandard` file (used by `standard_file`, or inline under `standard`): ```json { "name": "Custom IPLV (revised weights)", "reference": "Illustrative custom weighting — edit to track a revised norm", "load_fractions": [1.0, 0.75, 0.50, 0.25], "weights": [0.02, 0.40, 0.45, 0.13] } ``` Rules enforced by `validate()`: - `load_fractions` and `weights` are **non-empty** and have the **same length**. - `weights` **sum to 1.0** within `1e-6`. - `reference` is optional (defaults to empty). - Any number of points is allowed — the `rate` command matches them to your configured `points` by nearest `load_fraction`. --- ## 6. How to change a standard when the norm changes This is the core requirement the design serves. Three escalating options, none of which require touching the solver or the CLI: **Option A — no rebuild: ship a JSON file.** Edit or add a `PartLoadStandard` JSON file and point `standard_file` at it (or inline it under `standard`). This is the recommended path for tracking a revised norm in the field. ```jsonc // rate_config.json { "base_config": "chiller.json", "standard_file": "ahri_550_590_2026.json", "points": [ … ] } ``` **Option B — a new built-in preset (small code change, for standards you ship).** Add a constructor and register it in the lookup, in [`crates/entropyk/src/rating.rs`](../crates/entropyk/src/rating.rs): ```rust impl PartLoadStandard { pub fn ahri_550_590_2026_iplv() -> Self { Self { name: "AHRI 550/590-2026 IPLV".into(), reference: "AHRI Standard 550/590 (2026 revision)".into(), load_fractions: vec![1.0, 0.75, 0.50, 0.25], weights: vec![/* new coefficients */], } } } // in builtin(): "iplv_2026" | "ahri_550_590_2026" => Some(Self::ahri_550_590_2026_iplv()), ``` Then add the id to `builtin_ids()` and a unit test asserting the weights and that they sum to 1. **Option C — revise an existing preset's coefficients.** If a standard simply re-fits the weights of an existing metric, update the corresponding constant (`IPLV_WEIGHTS` / `ESEER_WEIGHTS`) or the preset body, and update the affected unit tests. The `validate()` invariant (weights sum to 1) guards against typos. > Whichever option you choose, the surrounding cycle solve, the `rate` command and > the report format stay untouched — only the *data* changes. --- ## 7. The `scop` / `seer` CLI commands (bin method) While `rate` integrates a handful of weighted part-load points, **SCOP** (seasonal heating) and **SEER** (seasonal cooling) follow the EN 14825 **bin method**: the machine is re-solved at every outdoor temperature bin of a climate, the building demand is derived from a linear load line, and the season is aggregated by an energy ratio. ```bash # Seasonal heating COP (SCOP) — defaults to the EN 14825 average heating season cargo run -p entropyk-cli -- scop --config crates/cli/examples/scop_heatpump_r134a.json # Seasonal cooling EER (SEER) — requires an explicit cooling climate cargo run -p entropyk-cli -- seer --config my_seer.json --output seer.json # Machine-readable JSON only cargo run -p entropyk-cli -- --quiet scop --config scop_heatpump_r134a.json ``` Both commands share one module (`entropyk_cli::seasonal`) and one config shape; the subcommand only fixes the *mode* (which heat-exchanger side the outdoor temperature drives and which duty is the useful output): | Mode | Subcommand | Useful duty | Outdoor side (default) | Default climate | |--------|------------|--------------------|------------------------|-----------------------| | SCOP | `scop` | heating (condenser)| evaporator | `en_14825_average` | | SEER | `seer` | cooling (evaporator)| condenser | *(explicit required)* | ### 7.1 Configuration schema ```jsonc { "base_config": "heatpump_r134a_air_source.json", // base run scenario (rel. to this file) // Climate selection — precedence: climate > climate_file > climate_name > default "climate_name": "en_14825_average", // built-in BinClimateStandard id // "climate_file": "my_cooling_bins.json", // external BinClimateStandard JSON // "climate": { ...inline BinClimateStandard... }, "outdoor_side": "evaporator", // "evaporator" | "condenser"; defaults by mode // Building load line (linear demand vs outdoor temperature) "design_load_w": 6000.0, // demand [W] at design_outdoor_c "design_outdoor_c": -10.0, // outdoor temp where demand = design_load_w "threshold_outdoor_c": 16.0, // outdoor temp where demand reaches 0 (default 16) "cd": 0.25, // cycling degradation coeff (PLF = 1 - Cd*(1-CR)); default 0.25 "backup_cop": 1.0 // COP of electric backup for heating deficits; default 1.0 } ``` Only `base_config`, `design_load_w` and `design_outdoor_c` are required. The outdoor temperature is applied to the matching heat exchanger's `secondary_inlet_temp_c` — so the base config's secondary stream **flow** and **cp** (e.g. air cp 1006 J/kg·K) are kept and only the inlet temperature is swept. ### 7.2 How a bin is solved For each bin at outdoor temperature `Tj`, with hours `h`: 1. **Demand** from the linear load line — heating: `demand = design_load · (threshold − Tj) / (threshold − design)`; cooling: `demand = design_load · (Tj − threshold) / (design − threshold)`, clamped at ≥ 0. 2. **Full-load solve** — the base cycle is re-solved with the outdoor-side `secondary_inlet_temp_c = Tj` (a genuine coupled solve, *no* injected regime), giving `Q_full`, `W_full`, `COP_full`. 3. **Capacity ratio** `CR = demand / Q_full`. - `CR ≤ 1` (part load): cycling degradation `PLF = 1 − Cd·(1 − CR)`, `COP_bin = COP_full · PLF`. - `CR > 1` (heating deficit): electric backup covers `demand − Q_full`, `COP_bin = demand / (W_full + (demand − Q_full)/backup_cop)`; the reported `backup` fraction is `(demand − Q_full)/demand`. For cooling the capacity simply caps at `Q_full`. 4. **Aggregate** — seasonal useful energy `Σ demand·h`, seasonal electric energy `Σ (demand/COP_bin)·h`, and `SCOP/SEER = useful / electric`. Bins whose demand is 0 (above the threshold for heating) contribute no energy and are skipped in the aggregation. If a *demanded* bin fails to converge, the integrated value is reported as unavailable rather than silently biased. ### 7.3 Report format `SeasonalReport` (also the `--quiet` / `--output` JSON): | Field | Meaning | |--------------------------|-----------------------------------------------------| | `base_config` | base scenario path | | `metric` | `"SCOP"` or `"SEER"` | | `climate` | climate standard name | | `climate_reference` | standard citation | | `total_hours` | Σ bin hours | | `bins[]` | per-bin rows (see below) | | `seasonal_useful_kwh` | Σ demand·hours / 1000 | | `seasonal_electric_kwh` | Σ electric·hours / 1000 | | `integrated_value` | SCOP/SEER, or `null` if a demanded bin failed | Each `bins[]` row: `temperature_c`, `hours`, `demand_w`, `capacity_w`, `full_cop`, `backup_fraction`, `effective_cop`. ### 7.4 Worked example (SCOP) `crates/cli/examples/scop_heatpump_r134a.json` runs the emergent air-source heat pump (`heatpump_r134a_air_source.json`, evaporator = outdoor air) over the EN 14825 average heating season: ``` T[°C] h Demand[kW] Cap[kW] COP_full backup COP_eff -10.0 1 6.000 4.967 3.649 17% 2.506 -7.0 24 5.308 5.419 3.775 0% 3.756 2.0 320 3.231 6.918 4.169 0% 3.613 7.0 326 2.077 7.840 4.392 0% 3.585 15.0 74 0.231 9.445 4.751 0% 3.592 Seasonal useful: 12393.7 kWh Seasonal electric: 3441.1 kWh SCOP = 3.602 ``` Full-load COP climbs monotonically as the outdoor air warms (less lift); at the coldest bins the capacity falls short of demand and electric backup kicks in (non-zero `backup` %), which is exactly where the effective COP is lowest. ### 7.5 Adding another bin-based metric Because the climate is a data-driven `BinClimateStandard`, adding a new season (SEER climate, a regional heating season, a future EN revision) means authoring a bin table — a preset, a built-in id, or an external JSON `climate_file` — with no code change. See §5 for the JSON schema (the same `BinClimateStandard` shape used by `climate_file`/`climate`). --- ## 8. Validation & testing The rating math is pure and exhaustively unit-tested. Run: ```bash # Library: metric math, presets, validation, registries (+ doctests) cargo test -p entropyk --lib rating cargo test -p entropyk --doc rating # CLI: override application, standard resolution & precedence, aggregation cargo test -p entropyk-cli rate # CLI seasonal: bin method demand/degradation/backup + end-to-end SCOP cargo test -p entropyk-cli --lib seasonal cargo test -p entropyk-cli --test single_run test_scop_command ``` Notable guarantees covered by tests: - Presets reproduce the legacy constants bit-for-bit; `integrate()` agrees with `iplv()`/`eseer()` to `1e-12`. - `validate()` rejects length mismatches, non-normalized weights and empty standards with the right `RatingError`. - `builtin()` lookup is case- and separator-insensitive. - Custom standards with a different point count integrate correctly. - CLI standard **precedence** (`standard` > `standard_file` > `standard_name` > `metric`) resolves as specified; unknown ids error clearly. - End-to-end: the emergent chiller re-solves four points and produces a genuine IPLV, with per-point EER increasing monotonically as the pressure lift falls. - Seasonal bin method: demand tracks the linear load line, cycling degradation and electric backup apply only where expected, and the end-to-end SCOP over the 26 EN 14825 bins is a physical energy ratio bounded by the per-bin COPs. > **Build note.** In environments where the Python bindings can't build, restrict > to the relevant crates: `cargo test -p entropyk -p entropyk-cli …` (avoid > `--workspace`). --- ## 9. Reference tables **AHRI 550/590 IPLV weights** (`IPLV_WEIGHTS`) | Load | 100 % | 75 % | 50 % | 25 % | | :--- | :--- | :--- | :--- | :--- | | Weight | 0.01 | 0.42 | 0.45 | 0.12 | **Eurovent ESEER weights** (`ESEER_WEIGHTS`) | Load | 100 % | 75 % | 50 % | 25 % | | :--- | :--- | :--- | :--- | :--- | | Weight | 0.03 | 0.33 | 0.41 | 0.23 | **EN 14825 average heating season** (`EN_14825_AVERAGE_BINS`) — 26 bins from −10 °C to +15 °C, total **4910 h** (Strasbourg reference; EU 813/2013 Annex III Table 5 / EN 14825:2018 Table A.4). **Secondary-fluid cp defaults** (used by `run` when only a mass flow is given): air `1006 J/(kg·K)`, water `4186 J/(kg·K)`. Provide `secondary_cp_j_per_kgk` or `secondary_capacity_rate_w_per_k` to override. --- ## 10. Standards references - **AHRI Standard 550/590 (2023)** — *Performance Rating of Water-Chilling and Heat Pump Water-Heating Packages Using the Vapor Compression Cycle.* - **AHRI Standard 551/591 (SI)** — metric counterpart of 550/590. - **Eurovent** — ESEER definition for liquid chilling packages. - **EN 14511** — rating conditions for air conditioners, liquid chilling packages and heat pumps. - **EN 14825:2018** — *Testing and rating at part load conditions and calculation of seasonal performance* (SCOP/SEER bin method). - **Commission Regulation (EU) No 813/2013**, Annex III — European reference heating seasons (average/warmer/colder). --- ### See also - [`crates/entropyk/src/rating.rs`](../crates/entropyk/src/rating.rs) — the module. - [`crates/cli/src/rate.rs`](../crates/cli/src/rate.rs) — the `rate` command. - [CLI_TUTORIAL.md](./CLI_TUTORIAL.md) — building and running `run` scenarios (the `base_config` for a rating). - [DOCUMENTATION.md](../DOCUMENTATION.md) — the definitive technical manual.