Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ The changes listed in this file are categorised as follows:

### Changed

- Vectorized ``MeteorInterface._apply_impacts``' per-realization loop. The underlying ``DegreeDaysCalculator.calculate`` is already vectorized over the ``month`` dimension via xarray, so passing the whole ``(realization, month)`` DataArray in one call produces identical results (verified in ``test_degree_days_calculate_batches_realizations`` to ``atol=1e-9``) without the per-realization Python overhead. At N=100 this removes ~20 s from ``gen_impacts``; scales linearly in N.
- Now possibly to send variable length temperature scaling timeseries, fixed noise generator for wrong ordering of base data dimensions

### Fixed
Expand Down
20 changes: 7 additions & 13 deletions src/meteor/meteor_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -2072,7 +2072,7 @@ def _apply_impacts(

# Convert from anomaly (K) to absolute temperature (°C)
# ts_data is anomaly in K, baseline_k is absolute temperature in K
n_realizations, n_months = ts_data.shape
n_months = ts_data.shape[1]

# Create xarray with month dimension (required by calculator)
# Absolute temperature in Celsius = (anomaly_K + baseline_K) - 273.15
Expand All @@ -2082,18 +2082,12 @@ def _apply_impacts(
coords={"month": np.arange(n_months)},
)

# Calculate degree days for each realization
# Both HDD and CDD are calculated in the same call
hdd_results = []
cdd_results = []
for i in range(n_realizations):
result = dd_model.calculate(temp_celsius[i])
hdd_results.append(result.data["annual_hdd"].values)
cdd_results.append(result.data["annual_cdd"].values)

# Stack back into arrays (n_realizations, n_years)
impacts["hdd"][key] = np.array(hdd_results)
impacts["cdd"][key] = np.array(cdd_results)
# DegreeDaysCalculator is internally vectorized over the
# 'month' dimension, so we pass the whole (realization, month)
# array in a single call rather than looping per realization.
result = dd_model.calculate(temp_celsius)
impacts["hdd"][key] = result.data["annual_hdd"].values
impacts["cdd"][key] = result.data["annual_cdd"].values

if verbose: # pragma: no cover
print(f" • HDD for {key}")
Expand Down
38 changes: 38 additions & 0 deletions tests/unit/impacts/test_degree_days.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,44 @@ def test_validate_temperature_input_and_convert():
validate_temperature_input_and_convert("not a data array or float")


def test_degree_days_calculate_batches_realizations():
"""Passing a 2D ``(realization, month)`` DataArray must produce the same
per-realization output as looping ``.calculate()`` over each row.

The MeteorInterface impact path relies on this: it avoids the expensive
per-realization Python loop by batching the whole ensemble into one call.
"""
calc = DegreeDaysCalculator(base_temperature=18.0)

rng = np.random.default_rng(0)
n_real, n_month = 8, 12 * 30 # 30 years
t = np.arange(n_month)
temps = (
15.0
+ 8.0 * np.sin(2 * np.pi * t / 12.0)[None, :]
+ 0.4 * rng.standard_normal((n_real, n_month))
)
ds = xr.DataArray(temps, dims=["realization", "month"], coords={"month": t})

# Per-realization loop (reference)
serial_hdd = np.stack(
[calc.calculate(ds[i]).data["annual_hdd"].values for i in range(n_real)]
)
serial_cdd = np.stack(
[calc.calculate(ds[i]).data["annual_cdd"].values for i in range(n_real)]
)

# Batched
result = calc.calculate(ds)
batched_hdd = result.data["annual_hdd"].values
batched_cdd = result.data["annual_cdd"].values

assert batched_hdd.shape == serial_hdd.shape
assert batched_cdd.shape == serial_cdd.shape
np.testing.assert_allclose(batched_hdd, serial_hdd, rtol=0, atol=1e-9)
np.testing.assert_allclose(batched_cdd, serial_cdd, rtol=0, atol=1e-9)


class TestDegreeDaysCalculator:
"""Test DegreeDaysCalculator functionality."""

Expand Down
Loading