Skip to content

Commit 6fa170f

Browse files
Merge pull request #698 from cal-adapt/fix/warming-level-years
Fix/warming level years
2 parents 426ef0a + 598fb0e commit 6fa170f

6 files changed

Lines changed: 281 additions & 42 deletions

File tree

.github/workflows/ci-not-main.yml

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,8 @@ jobs:
2828
matrix:
2929
python-version: ["3.12", "3.13"]
3030
fail-fast: false
31-
defaults:
32-
on:
33-
push:
34-
branches-ignore:
35-
- main
36-
python-version: ${{ matrix.python-version }}
31+
steps:
32+
- uses: actions/checkout@v3
3733
- name: Install uv
3834
run: |
3935
python -m pip install --upgrade pip

climakitae/new_core/param_validation/warming_param_validator.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ def validate_warming_level_param(
6161
logger.warning(msg)
6262
return False
6363

64+
# check that catalog is "cadcat"
65+
if not _check_catalog(query):
66+
return False
67+
6468
# validate query
6569
if not _check_query(query):
6670
return False
@@ -71,6 +75,42 @@ def validate_warming_level_param(
7175
return True
7276

7377

78+
def _check_catalog(query: Any) -> bool:
79+
"""
80+
Validates that the catalog is "cadcat" (warming levels only supported for cadcat).
81+
82+
Parameters
83+
----------
84+
query : Any
85+
The query dictionary that may contain a 'catalog' key.
86+
87+
Returns
88+
-------
89+
bool
90+
True if the catalog is "cadcat", False otherwise.
91+
"""
92+
logger.debug("_check_catalog called with query: %s", query)
93+
94+
if not isinstance(query, dict):
95+
return False
96+
97+
catalog = query.get("catalog", UNSET)
98+
if catalog is UNSET:
99+
# If catalog is not specified, assume it will default to cadcat
100+
return True
101+
102+
if catalog != "cadcat":
103+
msg = (
104+
f"Warming level processor is not supported for '{catalog}' catalog. "
105+
"Warming levels are only available for 'cadcat' (Cal-Adapt climate data). "
106+
"Please use time_slice processor instead to specify a time range."
107+
)
108+
logger.warning(msg)
109+
return False
110+
111+
return True
112+
113+
74114
def _check_input_types(
75115
value: dict[str, Any],
76116
) -> bool:

climakitae/new_core/processors/concatenate.py

Lines changed: 66 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import logging
44
from typing import Any, Dict, Iterable, List, Union
55

6+
import numpy as np
67
import xarray as xr
78

89
from climakitae.core.constants import _NEW_ATTRS_KEY, CATALOG_REN_ENERGY_GEN, UNSET
@@ -60,8 +61,6 @@ def __init__(self, value: str = "time"):
6061
self.name = "concat"
6162
self.catalog = None
6263
self.needs_catalog = True
63-
# Log initialization
64-
logger.debug("Concat processor initialized with dim_name=%s", self.dim_name)
6564

6665
def execute(
6766
self,
@@ -91,17 +90,8 @@ def execute(
9190
A single dataset with concatenated data.
9291
9392
"""
94-
logger.debug(
95-
"Concat.execute called with dim_name=%s result_type=%s",
96-
self.dim_name,
97-
type(result).__name__,
98-
)
99-
10093
if isinstance(result, (xr.Dataset, xr.DataArray)):
10194
# If we receive a single dataset, just return it
102-
logger.debug(
103-
"Concat.execute received single dataset/dataarray, returning as-is"
104-
)
10595
return result
10696

10797
# Route to appropriate concat method based on catalog
@@ -138,7 +128,6 @@ def _execute_hdp_concat(
138128
A single dataset with concatenated station data.
139129
140130
"""
141-
logger.debug("Using HDP concatenation logic")
142131
datasets_to_concat = []
143132
station_ids = []
144133

@@ -173,6 +162,11 @@ def _execute_hdp_concat(
173162
join="outer",
174163
)
175164

165+
# Drop "simulation" coordinate if it exists (distinct from "sim" dimension)
166+
if "simulation" in concatenated.coords:
167+
concatenated = concatenated.drop_vars("simulation")
168+
logger.debug("Dropped 'simulation' coordinate from concatenated dataset.")
169+
176170
logger.info("Concatenated HDP datasets along station_id dimension.")
177171
self.update_context(context, station_ids)
178172
return concatenated
@@ -203,15 +197,12 @@ def _execute_gridded_concat(
203197
A single dataset with concatenated data.
204198
205199
"""
206-
logger.debug("Using gridded data concatenation logic")
207-
208200
# Special handling for time dimension concatenation
209201
if self.dim_name == "time" and isinstance(result, dict):
210202
# Handle time domain extension for dictionaries
211203
result = extend_time_domain(result) # type: ignore
212204
# After extending time domain, switch to standard sim concatenation
213205
self.dim_name = "sim"
214-
logger.info("Time-domain extension applied; switching concat dim to 'sim'")
215206

216207
datasets_to_concat = []
217208
concat_attrs = (
@@ -390,6 +381,66 @@ def _execute_gridded_concat(
390381

391382
logger.info("Concatenated datasets along '%s' dimension.", self.dim_name)
392383

384+
# Drop "simulation" coordinate if it exists (distinct from "sim" dimension)
385+
if "simulation" in concatenated.coords:
386+
concatenated = concatenated.drop_vars("simulation")
387+
logger.debug("Dropped 'simulation' coordinate from concatenated dataset.")
388+
389+
# Fix centered_year coordinate if it was assigned per-simulation in warming_level processor
390+
# After concatenating along 'sim', we need to reconstruct it as 2D (sim, warming_level)
391+
if (
392+
"_sim_centered_years" in context
393+
and "sim" in concatenated.dims
394+
and "warming_level" in concatenated.dims
395+
):
396+
sim_centered_years = context.get("_sim_centered_years", {})
397+
398+
if sim_centered_years:
399+
sim_names = list(concatenated.sim.values)
400+
warming_levels = list(concatenated.warming_level.values)
401+
402+
# Build 2D centered_year array indexed by (sim, warming_level)
403+
cy_2d = np.full((len(sim_names), len(warming_levels)), np.nan)
404+
405+
for i, sim_name in enumerate(sim_names):
406+
# Try to find matching key in sim_centered_years
407+
# sim_name might be like 'WRF_UCLA_EC-Earth3_ssp370_day_d03_r1i1p1f1'
408+
# while sim_centered_years keys are like 'WRF.UCLA.EC-Earth3.ssp370.day.d03.r1i1p1f1'
409+
matching_key = None
410+
sim_name_dots = sim_name.replace("_", ".")
411+
412+
for key in sim_centered_years.keys():
413+
if key.replace(".", "_") == sim_name or sim_name_dots == key:
414+
matching_key = key
415+
break
416+
417+
if matching_key and matching_key in sim_centered_years:
418+
cy_values = sim_centered_years[matching_key]
419+
# Fill in the values for this simulation
420+
for j, cy_val in enumerate(cy_values):
421+
if j < len(warming_levels):
422+
cy_2d[i, j] = cy_val
423+
logger.debug(
424+
"Assigned centered_years for sim %s (key=%s): %s",
425+
sim_name,
426+
matching_key,
427+
cy_values,
428+
)
429+
else:
430+
logger.debug(
431+
"Could not find matching key for sim %s in sim_centered_years",
432+
sim_name,
433+
)
434+
435+
# Assign as 2D coordinate
436+
concatenated.coords["centered_year"] = (["sim", "warming_level"], cy_2d)
437+
logger.debug(
438+
"Successfully created 2D centered_year coordinate with shape %s",
439+
cy_2d.shape,
440+
)
441+
else:
442+
logger.debug("sim_centered_years is empty in context")
443+
393444
self.update_context(context, attr_ids)
394445

395446
# Set resolution attribute if available

climakitae/new_core/processors/warming_level.py

Lines changed: 103 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -165,19 +165,33 @@ def execute(
165165
# If not, assume no member_id is present
166166
member_ids.append(None)
167167

168+
logger.debug(
169+
"execute: Extracted member_ids=%s for keys=%s", member_ids, list(ret.keys())
170+
)
171+
168172
# get center years for each key for each warming level
169173
center_years = self.get_center_years(member_ids, ret.keys())
174+
logger.debug(
175+
"execute: Received center_years dict with %d keys",
176+
len(center_years),
177+
)
178+
170179
retkeys = list(ret.keys())
171180

172181
for key in retkeys:
173182
if key not in center_years:
174183
del ret[key]
175184

185+
# Build a mapping of simulation keys to their center years for proper coordinate assignment
186+
sim_centered_years = {}
187+
176188
for key, years in center_years.items():
177189
if not years:
178190
continue
179191

180192
slices = []
193+
valid_wls = [] # Track which warming levels were successfully processed
194+
valid_center_years = [] # Track center years for valid warming levels
181195

182196
for year, wl in zip(years, self.warming_levels):
183197
if year is None or pd.isna(year):
@@ -227,13 +241,20 @@ def execute(
227241
# Expand dimensions to include warming_level as a new dimension
228242
da_slice = da_slice.expand_dims({"warming_level": [wl]})
229243

230-
# Add simulation and centered_year coordinates
231-
da_slice = da_slice.assign_coords(
232-
simulation=key,
233-
centered_year=(["warming_level"], [center_year]),
244+
# Add simulation coordinate (but NOT centered_year yet)
245+
da_slice = da_slice.assign_coords(simulation=key)
246+
247+
logger.debug(
248+
"execute: key=%s, wl=%s -> assigned coord simulation=%s",
249+
key,
250+
wl,
251+
key,
234252
)
235253

236254
slices.append(da_slice)
255+
valid_wls.append(wl)
256+
valid_center_years.append(center_year)
257+
237258
# After processing all warming levels, check if we have any valid slices.
238259
if not slices:
239260
msg = f"No valid slices found for {key}. Ensure the warming level times table is correctly configured."
@@ -247,6 +268,18 @@ def execute(
247268
slices, dim="warming_level", join="outer", fill_value=np.nan
248269
)
249270

271+
# Store center years for this simulation key
272+
# DO NOT assign as coordinate here - it will be broadcast incorrectly
273+
# Store in sim_centered_years dict to be reconstructed in concatenate processor
274+
sim_centered_years[key] = valid_center_years
275+
276+
# Store center years in context for reconstruction after concatenation
277+
context["_sim_centered_years"] = sim_centered_years
278+
logger.debug(
279+
"execute: Stored %d simulation center year mappings in context",
280+
len(sim_centered_years),
281+
)
282+
250283
self.update_context(context)
251284
return ret
252285

@@ -351,9 +384,17 @@ def get_center_years(
351384
"""
352385
center_years = {}
353386

387+
logger.debug(
388+
"get_center_years: Computing center years for %d keys",
389+
len(list(keys)),
390+
)
391+
354392
# cols are key.join("_"), values are warming levels, index is time
355393
for key, member_id in zip(keys, member_ids):
356394
if member_id is None:
395+
logger.debug(
396+
"get_center_years: Skipping key=%s (member_id is None)", key
397+
)
357398
continue # Skip if member_id is None
358399

359400
# build the key for the warming level table
@@ -363,24 +404,54 @@ def get_center_years(
363404
center_years[key] = []
364405

365406
for wl in self.warming_levels:
407+
logger.debug(
408+
"get_center_years: Processing warming_level=%s for key=%s",
409+
wl,
410+
key,
411+
)
412+
366413
if str(wl) not in self.warming_level_times.columns:
414+
logger.debug(
415+
"get_center_years: wl=%s not in warming_level_times.columns, using warming_level_times_idx",
416+
wl,
417+
)
367418
# warming level is not an integer value, so we need to try to find
368419
# the first year that the simulation crosses the given warming level
369420
wl_table_key = f"{key_list[2]}_{member_id}_{key_list[3]}"
421+
logger.debug(
422+
"get_center_years: Looking for wl_table_key=%s in warming_level_times_idx",
423+
wl_table_key,
424+
)
425+
370426
if wl_table_key not in self.warming_level_times_idx.columns:
371427
logger.warning(
372-
f"\n\nWarming level table does not contain data for {wl_table_key}. "
373-
"\nEnsure the warming level times table is correctly configured."
428+
f"\n\nWarning: Warming level table does not contain data for {wl_table_key}. "
429+
"\nEnsure the warming level times table is correctly configured. "
430+
f"Available columns: {list(self.warming_level_times_idx.columns)[:5]}..."
374431
)
375432
center_years[key].append(np.nan)
376433
continue # exit warming levels loop
377434

378435
# get the FIRST index value for this key
379436
mask = (self.warming_level_times_idx[wl_table_key] >= wl).dropna()
437+
logger.debug(
438+
"get_center_years: mask for %s >= %s has %d True values",
439+
wl_table_key,
440+
wl,
441+
mask.sum(),
442+
)
443+
380444
if mask.any():
381-
center_years[key].append(
382-
mask.idxmax() # Get the first occurrence of the warming level
445+
center_year = (
446+
mask.idxmax()
447+
) # Get the first occurrence of the warming level
448+
logger.debug(
449+
"get_center_years: Found center_year=%s for wl_table_key=%s, wl=%s",
450+
center_year,
451+
wl_table_key,
452+
wl,
383453
)
454+
center_years[key].append(center_year)
384455
else:
385456
max_valid_wl = (
386457
self.warming_level_times.loc[
@@ -395,11 +466,34 @@ def get_center_years(
395466
)
396467
center_years[key].append(np.nan)
397468
else:
469+
logger.debug(
470+
"get_center_years: wl=%s found in warming_level_times.columns",
471+
wl,
472+
)
398473
# this is a common warming level, so we can just get the year
399474
tuple_key = (key_list[2], member_id, key_list[3])
475+
logger.debug(
476+
"get_center_years: Looking up tuple_key=%s for wl=%s",
477+
tuple_key,
478+
wl,
479+
)
480+
400481
center_time = pd.to_datetime(
401482
self.warming_level_times.loc[tuple_key, str(wl)]
402483
)
403-
center_years[key].append(center_time.year)
484+
center_year = center_time.year
485+
logger.debug(
486+
"get_center_years: Found center_year=%s from warming_level_times for tuple_key=%s, wl=%s",
487+
center_year,
488+
tuple_key,
489+
wl,
490+
)
491+
center_years[key].append(center_year)
492+
493+
logger.debug(
494+
"get_center_years: Returning center_years with %d keys", len(center_years)
495+
)
496+
for k, v in center_years.items():
497+
logger.debug("get_center_years: key=%s -> center_years=%s", k, v)
404498

405499
return center_years

0 commit comments

Comments
 (0)