Skip to content

Commit 820f783

Browse files
authored
Merge pull request #775 from cal-adapt/bug/warming-level-months
Fixing warming level months bug to address #774
2 parents beeec0d + 2e4a3c2 commit 820f783

1 file changed

Lines changed: 53 additions & 43 deletions

File tree

climakitae/util/warming_levels.py

Lines changed: 53 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,44 @@ def _get_sliced_data(
128128
# Dropping leap days before slicing time dimension because the window size can affect number of leap days per slice
129129
y = y.loc[~((y.time.dt.month == 2) & (y.time.dt.day == 29))]
130130

131+
# Number of days per month for a non-leap year
132+
days_per_month = {i: calendar.monthrange(2001, i)[1] for i in range(1, 13)}
133+
134+
# --- Build the canonical time axis ONCE, shared by both branches ---
135+
# Steps per year on the full (unfiltered), leap-free calendar
136+
full_per_year = {"monthly": 12, "daily": 365, "hourly": 8760}[y.frequency]
137+
138+
# Month label for each step in ONE clean year
139+
match y.frequency:
140+
case "monthly":
141+
month_of_step = np.arange(1, 13)
142+
case "daily":
143+
month_of_step = np.repeat(
144+
np.arange(1, 13), [days_per_month[m] for m in range(1, 13)]
145+
)
146+
case "hourly":
147+
month_of_step = np.repeat(
148+
np.arange(1, 13), [days_per_month[m] * 24 for m in range(1, 13)]
149+
)
150+
case _:
151+
raise ValueError(
152+
'frequency needs to be either "hourly", "daily", or "monthly"'
153+
)
154+
155+
# Full ±window axis (e.g. daily 30yr -> -5475 ... 5474)
156+
n_full = full_per_year * window * 2
157+
full_axis = np.arange(-n_full // 2, n_full // 2)
158+
159+
# Tag every step in the window with its month, then keep only requested months.
160+
# Works for ANY month subset (contiguous, wrap-around, or full year).
161+
month_full = np.tile(month_of_step, window * 2)
162+
selected_mask = np.isin(month_full, months)
163+
164+
# Canonical time: selected-month deltas that RETAIN their full-window position
165+
# (so e.g. summer ranges -5475...5475 WITH gaps, not a dense -1380...1380 block)
166+
canonical_time = full_axis[selected_mask]
167+
n_expected = len(canonical_time)
168+
131169
# Getting start and end years for slicing if `center_time` is not NaN
132170
if not pd.isna(center_time):
133171
centered_year = pd.to_datetime(center_time).year
@@ -138,59 +176,31 @@ def _get_sliced_data(
138176
if not pd.isna(center_time) and _determine_is_complete_wl(
139177
start_year, end_year, y.simulation.item(), y.downscaling_method, level
140178
):
141-
142179
# Slicing data around the centered year
143180
sliced = y.sel(time=slice(str(start_year), str(end_year)))
144181

145-
# Creating a mask for timestamps that are within the desired months
146-
valid_months_mask = sliced.time.dt.month.isin([months])
147-
148-
# Resetting and renaming time index for each data array so they can overlap and save storage space.
149-
expected_counts = {
150-
"monthly": window * 2 * 12,
151-
"daily": window * 2 * 365,
152-
"hourly": window * 2 * 8760,
153-
}
154-
# There may be missing time for time slices that exceed the 2100 year bound. If that is the case, only return a warming slice for the amount of valid data available AND correctly center `time_from_center` values.
155-
# Otherwise, if no time is missing, then the warming slice will just center the center year.
156-
sliced["time"] = np.arange(
157-
-expected_counts[y.frequency] / 2,
158-
expected_counts[y.frequency] / 2
159-
- (expected_counts[y.frequency] - len(sliced)),
160-
)
182+
# Mask for timestamps within the desired months (computed while time is datetime)
183+
valid_months_mask = sliced.time.dt.month.isin(months)
161184

162-
# Removing data not in the desired months (in this new time dimension)
185+
# Remove data not in the desired months
163186
sliced = sliced.sel(time=valid_months_mask)
164187

188+
# Force onto the canonical axis so every sim shares an identical time coordinate.
189+
# If a slice is short (time window exceeds the 2100 bound), align to the front of
190+
# the canonical axis so the early years stay anchored and the missing tail is dropped.
191+
if len(sliced.time) == n_expected:
192+
sliced["time"] = canonical_time
193+
else:
194+
sliced["time"] = canonical_time[: len(sliced.time)]
195+
165196
# Assigning `centered_year` as a coordinate to the DataArray
166197
sliced = sliced.assign_coords({"centered_year": centered_year})
167198

168199
else:
169-
170-
# This clause creates an empty DataArray with similar shape to real WL slices
171-
# to get dropped after the `.groupby` method is finished.
172-
173-
# Get number of days per month for non-leap year
174-
days_per_month = {i: calendar.monthrange(2001, i)[1] for i in np.arange(1, 13)}
175-
176-
# This creates an approximately appropriately sized DataArray to be dropped later
177-
match y.frequency:
178-
case "monthly":
179-
time_freq = len(months)
180-
case "daily":
181-
time_freq = sum([days_per_month[month] for month in months])
182-
case "hourly":
183-
time_freq = sum([days_per_month[month] for month in months]) * 24
184-
case _:
185-
raise ValueError(
186-
'frequency needs to be either "hourly", "daily", or "monthly"'
187-
)
188-
y = y.isel(
189-
time=slice(0, window * 2 * time_freq)
190-
) # This is to create a dummy slice that conforms with other data structure. Can be re-written to something more elegant.
191-
192-
# Creating attributes
193-
y["time"] = np.arange(-len(y.time) / 2, len(y.time) / 2)
200+
# This clause creates an empty DataArray with the SAME shape/axis as a real WL
201+
# slice, so the groupby recombination aligns cleanly. It gets dropped afterward.
202+
y = y.isel(time=slice(0, n_expected))
203+
y["time"] = canonical_time[: len(y.time)]
194204
y["centered_year"] = np.nan
195205

196206
# Returning DataArray of NaNs to be dropped later.

0 commit comments

Comments
 (0)