@@ -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