Files
Entropyk/docs/rating-and-seasonal-metrics.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

31 KiB
Raw Blame History

Seasonal & Part-Load Ratings — IPLV / NPLV / ESEER / SCOP / SEER

Modular, data-driven standardized ratings for chillers and heat pumps.

Crate: entropyk::rating · CLI: entropyk-cli rate

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
  2. Design philosophy: standards are data, not code
  3. Library API (entropyk::rating)
  4. The rate CLI command
  5. Custom standard JSON schema
  6. How to change a standard when the norm changes
  7. The scop / seer CLI commands (bin method)
  8. Validation & testing
  9. Reference tables
  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.
  • Temperature-bin (SCOP, bin-based SEER): energy summed over a climate's hourly temperature bins, Σ(h·demand) / Σ(h·demand/COP). Implemented by BinClimateStandard + 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::<PartLoadStandard>(...)?.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)

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.

pub struct PartLoadStandard {
    pub name: String,           // e.g. "AHRI 550/590 IPLV"
    pub reference: String,      // citation / provenance (optional in JSON)
    pub load_fractions: Vec<f64>, // e.g. [1.0, 0.75, 0.50, 0.25]
    pub weights: Vec<f64>,      // same length; must sum to 1.0
}

Construction & validation

Method Description
new(name, reference, load_fractions, weights) -> Result<Self, RatingError> 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<f64, RatingError> 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<Self> 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

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

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 %].

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<f64, RatingError> 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).
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).

pub struct TemperatureBin { pub temperature_c: f64, pub hours: f64 }

pub struct BinClimateStandard {
    pub name: String,
    pub reference: String,
    pub bins: Vec<TemperatureBin>,
}

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<f64>;

BinClimateStandard methods:

Method Description
en_14825_average() EN 14825 average heating season (Strasbourg reference, 4910 h).
builtin(id) -> Option<Self> 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

let climate = BinClimateStandard::en_14825_average();
let bins: Vec<BinPerformance> = 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.

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

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.

# 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

{
  "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 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

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<RatePointResult>, // ordered by descending load fraction
    pub integrated_value: Option<f64>,
}

pub struct RatePointResult {
    pub load_fraction: f64,
    pub status: String,          // "converged" | "timeout" | "non_converged" | "error"
    pub eer: Option<f64>,
    pub q_cooling_kw: Option<f64>,
    pub power_kw: Option<f64>,
    pub error: Option<String>,   // present only on failure
}

4.5 Worked examples

Built-in AHRI IPLVrate_chiller_iplv_ahri.json:

entropyk-cli rate -c crates/cli/examples/rate_chiller_iplv_ahri.json
  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 standardrate_chiller_custom_standard.json loads its weights from standard_custom_iplv.json (standard_file). Same points, revised weights [0.02, 0.40, 0.45, 0.13]:

  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):

{
  "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.

// 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:

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.

# 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

{
  "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:

# 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:2018Testing 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