@@ -64,7 +64,7 @@ def convert_calendar(
64
64
The target calendar name.
65
65
dim : str
66
66
Name of the time coordinate in the input DataArray or Dataset.
67
- align_on : {None, 'date', 'year'}
67
+ align_on : {None, 'date', 'year', 'random' }
68
68
Must be specified when either the source or target is a `"360_day"`
69
69
calendar; ignored otherwise. See Notes.
70
70
missing : any, optional
@@ -143,6 +143,16 @@ def convert_calendar(
143
143
will be dropped as there are no equivalent dates in a standard calendar.
144
144
145
145
This option is best used with data on a frequency coarser than daily.
146
+
147
+ "random"
148
+ Similar to "year", each day of year of the source is mapped to another day of year
149
+ of the target. However, instead of having always the same missing days according
150
+ the source and target years, here 5 days are chosen randomly, one for each fifth
151
+ of the year. However, February 29th is always missing when converting to a leap year,
152
+ or its value is dropped when converting from a leap year. This is similar to the method
153
+ used in the LOCA dataset (see Pierce, Cayan, and Thrasher (2014). doi:10.1175/JHM-D-14-0082.1).
154
+
155
+ This option is best used on daily data.
146
156
"""
147
157
from xarray .core .dataarray import DataArray
148
158
@@ -174,14 +184,20 @@ def convert_calendar(
174
184
175
185
out = obj .copy ()
176
186
177
- if align_on == "year" :
187
+ if align_on in [ "year" , "random" ] :
178
188
# Special case for conversion involving 360_day calendar
179
- # Instead of translating dates directly, this tries to keep the position within a year similar.
180
-
181
- new_doy = time .groupby (f"{ dim } .year" ).map (
182
- _interpolate_day_of_year , target_calendar = calendar , use_cftime = use_cftime
183
- )
184
-
189
+ if align_on == "year" :
190
+ # Instead of translating dates directly, this tries to keep the position within a year similar.
191
+ new_doy = time .groupby (f"{ dim } .year" ).map (
192
+ _interpolate_day_of_year ,
193
+ target_calendar = calendar ,
194
+ use_cftime = use_cftime ,
195
+ )
196
+ elif align_on == "random" :
197
+ # The 5 days to remove are randomly chosen, one for each of the five 72-days periods of the year.
198
+ new_doy = time .groupby (f"{ dim } .year" ).map (
199
+ _random_day_of_year , target_calendar = calendar , use_cftime = use_cftime
200
+ )
185
201
# Convert the source datetimes, but override the day of year with our new day of years.
186
202
out [dim ] = DataArray (
187
203
[
@@ -229,6 +245,27 @@ def _interpolate_day_of_year(time, target_calendar, use_cftime):
229
245
).astype (int )
230
246
231
247
248
+ def _random_day_of_year (time , target_calendar , use_cftime ):
249
+ """Return a day of year in the new calendar.
250
+
251
+ Removes Feb 29th and five other days chosen randomly within five sections of 72 days.
252
+ """
253
+ year = int (time .dt .year [0 ])
254
+ source_calendar = time .dt .calendar
255
+ new_doy = np .arange (360 ) + 1
256
+ rm_idx = np .random .default_rng ().integers (0 , 72 , 5 ) + 72 * np .arange (5 )
257
+ if source_calendar == "360_day" :
258
+ for idx in rm_idx :
259
+ new_doy [idx + 1 :] = new_doy [idx + 1 :] + 1
260
+ if _days_in_year (year , target_calendar , use_cftime ) == 366 :
261
+ new_doy [new_doy >= 60 ] = new_doy [new_doy >= 60 ] + 1
262
+ elif target_calendar == "360_day" :
263
+ new_doy = np .insert (new_doy , rm_idx - np .arange (5 ), - 1 )
264
+ if _days_in_year (year , source_calendar , use_cftime ) == 366 :
265
+ new_doy = np .insert (new_doy , 60 , - 1 )
266
+ return new_doy [time .dt .dayofyear - 1 ]
267
+
268
+
232
269
def _convert_to_new_calendar_with_new_day_of_year (
233
270
date , day_of_year , calendar , use_cftime
234
271
):
0 commit comments