cavapy 0.1.4__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of cavapy might be problematic. Click here for more details.
cavapy.py
CHANGED
|
@@ -1,689 +1,788 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import multiprocessing as mp
|
|
3
|
-
from concurrent.futures import ThreadPoolExecutor
|
|
4
|
-
from functools import partial
|
|
5
|
-
import logging
|
|
6
|
-
import warnings
|
|
7
|
-
|
|
8
|
-
warnings.filterwarnings(
|
|
9
|
-
"ignore",
|
|
10
|
-
category=FutureWarning,
|
|
11
|
-
message=".*geopandas.dataset module is deprecated.*",
|
|
12
|
-
)
|
|
13
|
-
import geopandas as gpd # noqa: E402
|
|
14
|
-
import pandas as pd # noqa: E402
|
|
15
|
-
import xarray as xr # noqa: E402
|
|
16
|
-
import numpy as np # noqa: E402
|
|
17
|
-
from xclim import sdba # noqa: E402
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
logger = logging.getLogger("climate")
|
|
21
|
-
logger.handlers = [] # Remove any existing handlers
|
|
22
|
-
handler = logging.StreamHandler()
|
|
23
|
-
formatter = logging.Formatter(
|
|
24
|
-
"%(asctime)s | %(name)s | %(process)d:%(thread)d [%(levelname)s]: %(message)s"
|
|
25
|
-
)
|
|
26
|
-
handler.setFormatter(formatter)
|
|
27
|
-
for hdlr in logger.handlers[:]: # remove all old handlers
|
|
28
|
-
logger.removeHandler(hdlr)
|
|
29
|
-
logger.addHandler(handler)
|
|
30
|
-
logger.setLevel(logging.DEBUG)
|
|
31
|
-
|
|
32
|
-
VARIABLES_MAP = {
|
|
33
|
-
"pr": "tp",
|
|
34
|
-
"tasmax": "t2mx",
|
|
35
|
-
"tasmin": "t2mn",
|
|
36
|
-
"hurs": "hurs",
|
|
37
|
-
"sfcWind": "sfcwind",
|
|
38
|
-
"rsds": "ssrd",
|
|
39
|
-
}
|
|
40
|
-
VALID_VARIABLES = list(VARIABLES_MAP)
|
|
41
|
-
# TODO: Throw an error if the selected country is not in the selected domain
|
|
42
|
-
VALID_DOMAINS = [
|
|
43
|
-
"NAM-22",
|
|
44
|
-
"EUR-22",
|
|
45
|
-
"AFR-22",
|
|
46
|
-
"EAS-22",
|
|
47
|
-
"SEA-22",
|
|
48
|
-
"WAS-22",
|
|
49
|
-
"AUS-22",
|
|
50
|
-
"SAM-22",
|
|
51
|
-
"CAM-22",
|
|
52
|
-
]
|
|
53
|
-
VALID_RCPS = ["rcp26", "rcp85"]
|
|
54
|
-
VALID_GCM = ["MOHC", "MPI", "NCC"]
|
|
55
|
-
VALID_RCM = ["REMO", "Reg"]
|
|
56
|
-
|
|
57
|
-
INVENTORY_DATA_REMOTE_URL = (
|
|
58
|
-
"https://hub.ipcc.ifca.es/thredds/fileServer/inventories/cava.csv"
|
|
59
|
-
)
|
|
60
|
-
INVENTORY_DATA_LOCAL_PATH = os.path.join(
|
|
61
|
-
os.path.expanduser("~"), "shared/inventories/cava/inventory.csv"
|
|
62
|
-
)
|
|
63
|
-
ERA5_DATA_REMOTE_URL = (
|
|
64
|
-
"https://hub.ipcc.ifca.es/thredds/dodsC/fao/observations/ERA5/0.25/ERA5_025.ncml"
|
|
65
|
-
)
|
|
66
|
-
ERA5_DATA_LOCAL_PATH = os.path.join(
|
|
67
|
-
os.path.expanduser("~"), "shared/data/observations/ERA5/0.25/ERA5_025.ncml"
|
|
68
|
-
)
|
|
69
|
-
DEFAULT_YEARS_OBS = range(1980, 2006)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
gcm (str): GCM name. One of {VALID_GCM}.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
#
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
)
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
"
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
],
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
)
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
#
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
)
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
)
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
1
|
+
import os
|
|
2
|
+
import multiprocessing as mp
|
|
3
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
4
|
+
from functools import partial
|
|
5
|
+
import logging
|
|
6
|
+
import warnings
|
|
7
|
+
|
|
8
|
+
warnings.filterwarnings(
|
|
9
|
+
"ignore",
|
|
10
|
+
category=FutureWarning,
|
|
11
|
+
message=".*geopandas.dataset module is deprecated.*",
|
|
12
|
+
)
|
|
13
|
+
import geopandas as gpd # noqa: E402
|
|
14
|
+
import pandas as pd # noqa: E402
|
|
15
|
+
import xarray as xr # noqa: E402
|
|
16
|
+
import numpy as np # noqa: E402
|
|
17
|
+
from xclim import sdba # noqa: E402
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger("climate")
|
|
21
|
+
logger.handlers = [] # Remove any existing handlers
|
|
22
|
+
handler = logging.StreamHandler()
|
|
23
|
+
formatter = logging.Formatter(
|
|
24
|
+
"%(asctime)s | %(name)s | %(process)d:%(thread)d [%(levelname)s]: %(message)s"
|
|
25
|
+
)
|
|
26
|
+
handler.setFormatter(formatter)
|
|
27
|
+
for hdlr in logger.handlers[:]: # remove all old handlers
|
|
28
|
+
logger.removeHandler(hdlr)
|
|
29
|
+
logger.addHandler(handler)
|
|
30
|
+
logger.setLevel(logging.DEBUG)
|
|
31
|
+
|
|
32
|
+
VARIABLES_MAP = {
|
|
33
|
+
"pr": "tp",
|
|
34
|
+
"tasmax": "t2mx",
|
|
35
|
+
"tasmin": "t2mn",
|
|
36
|
+
"hurs": "hurs",
|
|
37
|
+
"sfcWind": "sfcwind",
|
|
38
|
+
"rsds": "ssrd",
|
|
39
|
+
}
|
|
40
|
+
VALID_VARIABLES = list(VARIABLES_MAP)
|
|
41
|
+
# TODO: Throw an error if the selected country is not in the selected domain
|
|
42
|
+
VALID_DOMAINS = [
|
|
43
|
+
"NAM-22",
|
|
44
|
+
"EUR-22",
|
|
45
|
+
"AFR-22",
|
|
46
|
+
"EAS-22",
|
|
47
|
+
"SEA-22",
|
|
48
|
+
"WAS-22",
|
|
49
|
+
"AUS-22",
|
|
50
|
+
"SAM-22",
|
|
51
|
+
"CAM-22",
|
|
52
|
+
]
|
|
53
|
+
VALID_RCPS = ["rcp26", "rcp85"]
|
|
54
|
+
VALID_GCM = ["MOHC", "MPI", "NCC"]
|
|
55
|
+
VALID_RCM = ["REMO", "Reg"]
|
|
56
|
+
|
|
57
|
+
INVENTORY_DATA_REMOTE_URL = (
|
|
58
|
+
"https://hub.ipcc.ifca.es/thredds/fileServer/inventories/cava.csv"
|
|
59
|
+
)
|
|
60
|
+
INVENTORY_DATA_LOCAL_PATH = os.path.join(
|
|
61
|
+
os.path.expanduser("~"), "shared/inventories/cava/inventory.csv"
|
|
62
|
+
)
|
|
63
|
+
ERA5_DATA_REMOTE_URL = (
|
|
64
|
+
"https://hub.ipcc.ifca.es/thredds/dodsC/fao/observations/ERA5/0.25/ERA5_025.ncml"
|
|
65
|
+
)
|
|
66
|
+
ERA5_DATA_LOCAL_PATH = os.path.join(
|
|
67
|
+
os.path.expanduser("~"), "shared/data/observations/ERA5/0.25/ERA5_025.ncml"
|
|
68
|
+
)
|
|
69
|
+
DEFAULT_YEARS_OBS = range(1980, 2006)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_climate_data(
|
|
73
|
+
*,
|
|
74
|
+
country: str | None,
|
|
75
|
+
years_obs: range | None = None,
|
|
76
|
+
obs: bool = False,
|
|
77
|
+
cordex_domain: str | None = None,
|
|
78
|
+
rcp: str | None = None,
|
|
79
|
+
gcm: str | None = None,
|
|
80
|
+
rcm: str | None = None,
|
|
81
|
+
years_up_to: int | None = None,
|
|
82
|
+
bias_correction: bool = False,
|
|
83
|
+
historical: bool = False,
|
|
84
|
+
buffer: int = 0,
|
|
85
|
+
xlim: tuple[float, float] | None = None,
|
|
86
|
+
ylim: tuple[float, float] | None = None,
|
|
87
|
+
remote: bool = True,
|
|
88
|
+
variables: list[str] | None = None,
|
|
89
|
+
num_processes: int = len(VALID_VARIABLES),
|
|
90
|
+
max_threads_per_process: int = 8,
|
|
91
|
+
) -> dict[str, xr.DataArray]:
|
|
92
|
+
f"""
|
|
93
|
+
Process climate data required by pyAEZ climate module.
|
|
94
|
+
The function automatically access CORDEX-CORE models at 0.25° and the ERA5 datasets.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
country (str): Name of the country for which data is to be processed.
|
|
98
|
+
Use None if specifying a region using xlim and ylim.
|
|
99
|
+
years_obs (range): Range of years for observational data (ERA5 only). Required when obs is True. (default: None).
|
|
100
|
+
obs (bool): Flag to indicate if processing observational data (default: False).
|
|
101
|
+
When True, only years_obs is required. CORDEX parameters are optional.
|
|
102
|
+
cordex_domain (str): CORDEX domain of the climate data. One of {VALID_DOMAINS}.
|
|
103
|
+
Required when obs is False. (default: None).
|
|
104
|
+
rcp (str): Representative Concentration Pathway. One of {VALID_RCPS}.
|
|
105
|
+
Required when obs is False. (default: None).
|
|
106
|
+
gcm (str): GCM name. One of {VALID_GCM}.
|
|
107
|
+
Required when obs is False. (default: None).
|
|
108
|
+
rcm (str): RCM name. One of {VALID_RCM}.
|
|
109
|
+
Required when obs is False. (default: None).
|
|
110
|
+
years_up_to (int): The ending year for the projected data. Projections start in 2006 and ends in 2100.
|
|
111
|
+
Hence, if years_up_to is set to 2030, data will be downloaded for the 2006-2030 period.
|
|
112
|
+
Required when obs is False. (default: None).
|
|
113
|
+
bias_correction (bool): Whether to apply bias correction (default: False).
|
|
114
|
+
historical (bool): Flag to indicate if processing historical data (default: False).
|
|
115
|
+
If True, historical data is provided together with projections.
|
|
116
|
+
Historical simulation runs for CORDEX-CORE initiative are provided for the 1980-2005 time period.
|
|
117
|
+
buffer (int): Buffer distance to expand the region of interest (default: 0).
|
|
118
|
+
xlim (tuple or None): Longitudinal bounds of the region of interest. Use only when country is None (default: None).
|
|
119
|
+
ylim (tuple or None): Latitudinal bounds of the region of interest. Use only when country is None (default: None).
|
|
120
|
+
remote (bool): Flag to work with remote data or not (default: True).
|
|
121
|
+
variables (list[str] or None): List of variables to process. Must be a subset of {VALID_VARIABLES}. If None, all variables are processed. (default: None).
|
|
122
|
+
num_processes (int): Number of processes to use, one per variable.
|
|
123
|
+
By default equals to the number of all possible variables. (default: {len(VALID_VARIABLES)}).
|
|
124
|
+
max_threads_per_process (int): Max number of threads within each process. (default: 8).
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
dict: A dictionary containing processed climate data for each variable as an xarray object.
|
|
128
|
+
|
|
129
|
+
Examples:
|
|
130
|
+
# For observations only:
|
|
131
|
+
data = get_climate_data(country="Togo", obs=True, years_obs=range(1990, 2011))
|
|
132
|
+
|
|
133
|
+
# For CORDEX projections:
|
|
134
|
+
data = get_climate_data(country="Togo", cordex_domain="AFR-22", rcp="rcp26",
|
|
135
|
+
gcm="MPI", rcm="Reg", years_up_to=2030)
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
# Validation for basic parameters
|
|
139
|
+
if xlim is None and ylim is not None or xlim is not None and ylim is None:
|
|
140
|
+
raise ValueError(
|
|
141
|
+
"xlim and ylim mismatch: they must be both specified or both unspecified"
|
|
142
|
+
)
|
|
143
|
+
if country is None and xlim is None:
|
|
144
|
+
raise ValueError("You must specify a country or (xlim, ylim)")
|
|
145
|
+
if country is not None and xlim is not None:
|
|
146
|
+
raise ValueError("You must specify either country or (xlim, ylim), not both")
|
|
147
|
+
|
|
148
|
+
# Conditional validation based on obs flag
|
|
149
|
+
if obs:
|
|
150
|
+
# When obs=True, only years_obs is required
|
|
151
|
+
if years_obs is None:
|
|
152
|
+
raise ValueError("years_obs must be provided when obs is True")
|
|
153
|
+
if not (1980 <= min(years_obs) <= max(years_obs) <= 2020):
|
|
154
|
+
raise ValueError("Years in years_obs must be within the range 1980 to 2020")
|
|
155
|
+
|
|
156
|
+
# Set default values for CORDEX parameters (not used but needed for function calls)
|
|
157
|
+
cordex_domain = cordex_domain or "AFR-22" # dummy value
|
|
158
|
+
rcp = rcp or "rcp26" # dummy value
|
|
159
|
+
gcm = gcm or "MPI" # dummy value
|
|
160
|
+
rcm = rcm or "Reg" # dummy value
|
|
161
|
+
years_up_to = years_up_to or 2030 # dummy value
|
|
162
|
+
else:
|
|
163
|
+
# When obs=False, CORDEX parameters are required
|
|
164
|
+
required_params = {
|
|
165
|
+
"cordex_domain": VALID_DOMAINS,
|
|
166
|
+
"rcp": VALID_RCPS,
|
|
167
|
+
"gcm": VALID_GCM,
|
|
168
|
+
"rcm": VALID_RCM,
|
|
169
|
+
}
|
|
170
|
+
for param_name, valid_values in required_params.items():
|
|
171
|
+
param_value = locals()[param_name]
|
|
172
|
+
if param_value is None:
|
|
173
|
+
raise ValueError(f"{param_name} is required when obs is False")
|
|
174
|
+
if param_value not in valid_values:
|
|
175
|
+
raise ValueError(
|
|
176
|
+
f"Invalid {param_name}={param_value}. Must be one of {valid_values}"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
if years_up_to is None:
|
|
180
|
+
raise ValueError("years_up_to is required when obs is False")
|
|
181
|
+
if years_up_to <= 2006:
|
|
182
|
+
raise ValueError("years_up_to must be greater than 2006")
|
|
183
|
+
|
|
184
|
+
# Set default years_obs when not processing observations
|
|
185
|
+
if years_obs is None:
|
|
186
|
+
years_obs = DEFAULT_YEARS_OBS
|
|
187
|
+
|
|
188
|
+
# Validate variables if provided
|
|
189
|
+
if variables is not None:
|
|
190
|
+
invalid_vars = [var for var in variables if var not in VALID_VARIABLES]
|
|
191
|
+
if invalid_vars:
|
|
192
|
+
raise ValueError(
|
|
193
|
+
f"Invalid variables: {invalid_vars}. Must be a subset of {VALID_VARIABLES}"
|
|
194
|
+
)
|
|
195
|
+
else:
|
|
196
|
+
variables = VALID_VARIABLES
|
|
197
|
+
|
|
198
|
+
_validate_urls(gcm, rcm, rcp, remote, cordex_domain, obs, historical, bias_correction)
|
|
199
|
+
|
|
200
|
+
bbox = _geo_localize(country, xlim, ylim, buffer, cordex_domain, obs)
|
|
201
|
+
|
|
202
|
+
with mp.Pool(processes=min(num_processes, len(variables))) as pool:
|
|
203
|
+
futures = []
|
|
204
|
+
for variable in variables:
|
|
205
|
+
futures.append(
|
|
206
|
+
pool.apply_async(
|
|
207
|
+
process_worker,
|
|
208
|
+
args=(max_threads_per_process,),
|
|
209
|
+
kwds={
|
|
210
|
+
"variable": variable,
|
|
211
|
+
"bbox": bbox,
|
|
212
|
+
"cordex_domain": cordex_domain,
|
|
213
|
+
"rcp": rcp,
|
|
214
|
+
"gcm": gcm,
|
|
215
|
+
"rcm": rcm,
|
|
216
|
+
"years_up_to": years_up_to,
|
|
217
|
+
"years_obs": years_obs,
|
|
218
|
+
"obs": obs,
|
|
219
|
+
"bias_correction": bias_correction,
|
|
220
|
+
"historical": historical,
|
|
221
|
+
"remote": remote,
|
|
222
|
+
},
|
|
223
|
+
)
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
results = {
|
|
227
|
+
variable: futures[i].get() for i, variable in enumerate(variables)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
pool.close() # Prevent any more tasks from being submitted to the pool
|
|
231
|
+
pool.join() # Wait for all worker processes to finish
|
|
232
|
+
|
|
233
|
+
return results
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _validate_urls(
|
|
237
|
+
gcm: str = None,
|
|
238
|
+
rcm: str = None,
|
|
239
|
+
rcp: str = None,
|
|
240
|
+
remote: bool = True,
|
|
241
|
+
cordex_domain: str = None,
|
|
242
|
+
obs: bool = False,
|
|
243
|
+
historical: bool = False,
|
|
244
|
+
bias_correction: bool = False,
|
|
245
|
+
):
|
|
246
|
+
# Load the data
|
|
247
|
+
log = logger.getChild("URL-validation")
|
|
248
|
+
|
|
249
|
+
if obs is False:
|
|
250
|
+
inventory_csv_url = (
|
|
251
|
+
INVENTORY_DATA_REMOTE_URL if remote else INVENTORY_DATA_LOCAL_PATH
|
|
252
|
+
)
|
|
253
|
+
data = pd.read_csv(inventory_csv_url)
|
|
254
|
+
|
|
255
|
+
# Set the column to use based on whether the data is remote or local
|
|
256
|
+
column_to_use = "location" if remote else "hub"
|
|
257
|
+
|
|
258
|
+
# Define which experiments we need
|
|
259
|
+
experiments = [rcp]
|
|
260
|
+
if historical or bias_correction:
|
|
261
|
+
experiments.append("historical")
|
|
262
|
+
|
|
263
|
+
# Filter the data based on the conditions
|
|
264
|
+
filtered_data = data[
|
|
265
|
+
lambda x: (
|
|
266
|
+
x["activity"].str.contains("FAO", na=False)
|
|
267
|
+
& (x["domain"] == cordex_domain)
|
|
268
|
+
& (x["model"].str.contains(gcm, na=False))
|
|
269
|
+
& (x["rcm"].str.contains(rcm, na=False))
|
|
270
|
+
& (x["experiment"].isin(experiments))
|
|
271
|
+
)
|
|
272
|
+
][["experiment", column_to_use]]
|
|
273
|
+
|
|
274
|
+
# Extract the column values as a list
|
|
275
|
+
for _, row in filtered_data.iterrows():
|
|
276
|
+
if row["experiment"] == "historical":
|
|
277
|
+
log_hist = logger.getChild("URL-validation-historical")
|
|
278
|
+
log_hist.info(f"{row[column_to_use]}")
|
|
279
|
+
else:
|
|
280
|
+
log_proj = logger.getChild("URL-validation-projections")
|
|
281
|
+
log_proj.info(f"{row[column_to_use]}")
|
|
282
|
+
|
|
283
|
+
else: # when obs is True
|
|
284
|
+
log_obs = logger.getChild("URL-validation-observations")
|
|
285
|
+
log_obs.info(f"{ERA5_DATA_REMOTE_URL}")
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _geo_localize(
|
|
289
|
+
country: str = None,
|
|
290
|
+
xlim: tuple[float, float] = None,
|
|
291
|
+
ylim: tuple[float, float] = None,
|
|
292
|
+
buffer: int = 0,
|
|
293
|
+
cordex_domain: str = None,
|
|
294
|
+
obs: bool = False,
|
|
295
|
+
) -> dict[str, tuple[float, float]]:
|
|
296
|
+
if country:
|
|
297
|
+
if xlim or ylim:
|
|
298
|
+
raise ValueError(
|
|
299
|
+
"Specify either a country or bounding box limits (xlim, ylim), but not both."
|
|
300
|
+
)
|
|
301
|
+
# Load country shapefile and extract bounds
|
|
302
|
+
world = gpd.read_file(gpd.datasets.get_path("naturalearth_lowres"))
|
|
303
|
+
country_shp = world[world.name == country]
|
|
304
|
+
if country_shp.empty:
|
|
305
|
+
# Check if it's a capitalization issue
|
|
306
|
+
if country and country[0].islower():
|
|
307
|
+
capitalized = country.capitalize()
|
|
308
|
+
raise ValueError(f"Country '{country}' not found. Try capitalizing the first letter: '{capitalized}'")
|
|
309
|
+
else:
|
|
310
|
+
raise ValueError(f"Country '{country}' is unknown.")
|
|
311
|
+
bounds = country_shp.total_bounds # [minx, miny, maxx, maxy]
|
|
312
|
+
xlim, ylim = (bounds[0], bounds[2]), (bounds[1], bounds[3])
|
|
313
|
+
elif not (xlim and ylim):
|
|
314
|
+
raise ValueError(
|
|
315
|
+
"Either a country or bounding box limits (xlim, ylim) must be specified."
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
# Apply buffer
|
|
319
|
+
xlim = (xlim[0] - buffer, xlim[1] + buffer)
|
|
320
|
+
ylim = (ylim[0] - buffer, ylim[1] + buffer)
|
|
321
|
+
|
|
322
|
+
# Only validate CORDEX domain when processing non-observational data
|
|
323
|
+
# Skip validation for observations or when using dummy values
|
|
324
|
+
if not obs and cordex_domain:
|
|
325
|
+
_validate_cordex_domain(xlim, ylim, cordex_domain)
|
|
326
|
+
|
|
327
|
+
return {"xlim": xlim, "ylim": ylim}
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _validate_cordex_domain(xlim, ylim, cordex_domain):
|
|
331
|
+
|
|
332
|
+
# CORDEX domains data
|
|
333
|
+
cordex_domains_df = pd.DataFrame(
|
|
334
|
+
{
|
|
335
|
+
"min_lon": [
|
|
336
|
+
-33,
|
|
337
|
+
-28.3,
|
|
338
|
+
89.25,
|
|
339
|
+
86.75,
|
|
340
|
+
19.25,
|
|
341
|
+
44.0,
|
|
342
|
+
-106.25,
|
|
343
|
+
-115.0,
|
|
344
|
+
-24.25,
|
|
345
|
+
10.75,
|
|
346
|
+
],
|
|
347
|
+
"min_lat": [
|
|
348
|
+
-28,
|
|
349
|
+
-23,
|
|
350
|
+
-15.25,
|
|
351
|
+
-54.25,
|
|
352
|
+
-15.75,
|
|
353
|
+
-4.0,
|
|
354
|
+
-58.25,
|
|
355
|
+
-14.5,
|
|
356
|
+
-46.25,
|
|
357
|
+
17.75,
|
|
358
|
+
],
|
|
359
|
+
"max_lon": [
|
|
360
|
+
20,
|
|
361
|
+
18,
|
|
362
|
+
147.0,
|
|
363
|
+
-152.75,
|
|
364
|
+
116.25,
|
|
365
|
+
-172.0,
|
|
366
|
+
-16.25,
|
|
367
|
+
-30.5,
|
|
368
|
+
59.75,
|
|
369
|
+
140.25,
|
|
370
|
+
],
|
|
371
|
+
"max_lat": [28, 21.7, 26.5, 13.75, 45.75, 65.0, 18.75, 28.5, 42.75, 69.75],
|
|
372
|
+
"cordex_domain": [
|
|
373
|
+
"NAM-22",
|
|
374
|
+
"EUR-22",
|
|
375
|
+
"SEA-22",
|
|
376
|
+
"AUS-22",
|
|
377
|
+
"WAS-22",
|
|
378
|
+
"EAS-22",
|
|
379
|
+
"SAM-22",
|
|
380
|
+
"CAM-22",
|
|
381
|
+
"AFR-22",
|
|
382
|
+
"CAS-22",
|
|
383
|
+
],
|
|
384
|
+
}
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
def is_bbox_contained(bbox, domain):
|
|
388
|
+
"""Check if bbox is contained within the domain bounding box."""
|
|
389
|
+
return (
|
|
390
|
+
bbox[0] >= domain["min_lon"]
|
|
391
|
+
and bbox[1] >= domain["min_lat"]
|
|
392
|
+
and bbox[2] <= domain["max_lon"]
|
|
393
|
+
and bbox[3] <= domain["max_lat"]
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
user_bbox = [xlim[0], ylim[0], xlim[1], ylim[1]]
|
|
397
|
+
domain_row = cordex_domains_df[cordex_domains_df["cordex_domain"] == cordex_domain]
|
|
398
|
+
|
|
399
|
+
if domain_row.empty:
|
|
400
|
+
raise ValueError(f"CORDEX domain '{cordex_domain}' is not recognized.")
|
|
401
|
+
|
|
402
|
+
domain_bbox = domain_row.iloc[0]
|
|
403
|
+
|
|
404
|
+
if not is_bbox_contained(user_bbox, domain_bbox):
|
|
405
|
+
suggested_domains = cordex_domains_df[
|
|
406
|
+
cordex_domains_df.apply(
|
|
407
|
+
lambda row: is_bbox_contained(user_bbox, row), axis=1
|
|
408
|
+
)
|
|
409
|
+
]
|
|
410
|
+
|
|
411
|
+
if suggested_domains.empty:
|
|
412
|
+
raise ValueError(
|
|
413
|
+
f"The bounding box {user_bbox} is outside of all available CORDEX domains."
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
suggested_domain = suggested_domains.iloc[0]["cordex_domain"]
|
|
417
|
+
|
|
418
|
+
raise ValueError(
|
|
419
|
+
f"Bounding box {user_bbox} is not within '{cordex_domain}'. Suggested domain: '{suggested_domain}'."
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _leave_one_out_bias_correction(ref, hist, variable, log):
|
|
424
|
+
"""
|
|
425
|
+
Perform leave-one-out cross-validation for bias correction to avoid overfitting.
|
|
426
|
+
|
|
427
|
+
Args:
|
|
428
|
+
ref: Reference (observational) data
|
|
429
|
+
hist: Historical model data
|
|
430
|
+
variable: Variable name for determining correction method
|
|
431
|
+
log: Logger instance
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
xr.DataArray: Bias-corrected historical data
|
|
435
|
+
"""
|
|
436
|
+
log.info("Starting leave-one-out cross-validation for bias correction")
|
|
437
|
+
|
|
438
|
+
# Get unique years from historical data
|
|
439
|
+
hist_years = hist.time.dt.year.values
|
|
440
|
+
unique_years = np.unique(hist_years)
|
|
441
|
+
|
|
442
|
+
# Initialize list to store corrected data for each year
|
|
443
|
+
corrected_years = []
|
|
444
|
+
|
|
445
|
+
for leave_out_year in unique_years:
|
|
446
|
+
log.info(f"Processing leave-out year: {leave_out_year}")
|
|
447
|
+
|
|
448
|
+
# Create masks for training (all years except leave_out_year) and testing (only leave_out_year)
|
|
449
|
+
train_mask = hist.time.dt.year != leave_out_year
|
|
450
|
+
test_mask = hist.time.dt.year == leave_out_year
|
|
451
|
+
|
|
452
|
+
# Get training data (all years except the current one)
|
|
453
|
+
hist_train = hist.sel(time=train_mask)
|
|
454
|
+
hist_test = hist.sel(time=test_mask)
|
|
455
|
+
|
|
456
|
+
# Get corresponding reference data for training period
|
|
457
|
+
ref_train_mask = ref.time.dt.year != leave_out_year
|
|
458
|
+
ref_train = ref.sel(time=ref_train_mask)
|
|
459
|
+
|
|
460
|
+
# Train the bias correction model on the training data
|
|
461
|
+
QM_leave_out = sdba.EmpiricalQuantileMapping.train(
|
|
462
|
+
ref_train,
|
|
463
|
+
hist_train,
|
|
464
|
+
group="time.month",
|
|
465
|
+
kind="*" if variable in ["pr", "rsds", "sfcWind"] else "+",
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
# Apply bias correction to the left-out year
|
|
469
|
+
hist_corrected_year = QM_leave_out.adjust(
|
|
470
|
+
hist_test, extrapolation="constant", interp="linear"
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
# Apply variable-specific constraints
|
|
474
|
+
if variable == "hurs":
|
|
475
|
+
hist_corrected_year = hist_corrected_year.where(hist_corrected_year <= 100, 100)
|
|
476
|
+
hist_corrected_year = hist_corrected_year.where(hist_corrected_year >= 0, 0)
|
|
477
|
+
|
|
478
|
+
corrected_years.append(hist_corrected_year)
|
|
479
|
+
|
|
480
|
+
# Concatenate all corrected years and sort by time
|
|
481
|
+
hist_bs = xr.concat(corrected_years, dim="time").sortby("time")
|
|
482
|
+
|
|
483
|
+
log.info("Leave-one-out cross-validation bias correction completed")
|
|
484
|
+
return hist_bs
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def process_worker(num_threads, **kwargs) -> xr.DataArray:
|
|
488
|
+
variable = kwargs["variable"]
|
|
489
|
+
log = logger.getChild(variable)
|
|
490
|
+
try:
|
|
491
|
+
with ThreadPoolExecutor(
|
|
492
|
+
max_workers=num_threads, thread_name_prefix="climate"
|
|
493
|
+
) as executor:
|
|
494
|
+
return _climate_data_for_variable(executor, **kwargs)
|
|
495
|
+
except Exception as e:
|
|
496
|
+
log.exception(f"Process worker failed: {e}")
|
|
497
|
+
raise
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def _climate_data_for_variable(
|
|
501
|
+
executor: ThreadPoolExecutor,
|
|
502
|
+
*,
|
|
503
|
+
variable: str,
|
|
504
|
+
bbox: dict[str, tuple[float, float]],
|
|
505
|
+
cordex_domain: str,
|
|
506
|
+
rcp: str,
|
|
507
|
+
gcm: str,
|
|
508
|
+
rcm: str,
|
|
509
|
+
years_up_to: int,
|
|
510
|
+
years_obs: range,
|
|
511
|
+
obs: bool,
|
|
512
|
+
bias_correction: bool,
|
|
513
|
+
historical: bool,
|
|
514
|
+
remote: bool,
|
|
515
|
+
) -> xr.DataArray:
|
|
516
|
+
log = logger.getChild(variable)
|
|
517
|
+
|
|
518
|
+
pd.options.mode.chained_assignment = None
|
|
519
|
+
inventory_csv_url = (
|
|
520
|
+
INVENTORY_DATA_REMOTE_URL if remote else INVENTORY_DATA_LOCAL_PATH
|
|
521
|
+
)
|
|
522
|
+
data = pd.read_csv(inventory_csv_url)
|
|
523
|
+
column_to_use = "location" if remote else "hub"
|
|
524
|
+
|
|
525
|
+
# Filter data based on whether we need historical data
|
|
526
|
+
experiments = [rcp]
|
|
527
|
+
if historical or bias_correction:
|
|
528
|
+
experiments.append("historical")
|
|
529
|
+
|
|
530
|
+
filtered_data = data[
|
|
531
|
+
lambda x: (x["activity"].str.contains("FAO", na=False))
|
|
532
|
+
& (x["domain"] == cordex_domain)
|
|
533
|
+
& (x["model"].str.contains(gcm, na=False))
|
|
534
|
+
& (x["rcm"].str.contains(rcm, na=False))
|
|
535
|
+
& (x["experiment"].isin(experiments))
|
|
536
|
+
][["experiment", column_to_use]]
|
|
537
|
+
|
|
538
|
+
future_obs = None
|
|
539
|
+
if obs or bias_correction:
|
|
540
|
+
future_obs = executor.submit(
|
|
541
|
+
_thread_download_data,
|
|
542
|
+
url=None,
|
|
543
|
+
bbox=bbox,
|
|
544
|
+
variable=variable,
|
|
545
|
+
obs=True,
|
|
546
|
+
years_up_to=years_up_to,
|
|
547
|
+
years_obs=years_obs,
|
|
548
|
+
remote=remote,
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
if not obs:
|
|
552
|
+
download_fn = partial(
|
|
553
|
+
_thread_download_data,
|
|
554
|
+
bbox=bbox,
|
|
555
|
+
variable=variable,
|
|
556
|
+
obs=False,
|
|
557
|
+
years_obs=years_obs,
|
|
558
|
+
years_up_to=years_up_to,
|
|
559
|
+
remote=remote,
|
|
560
|
+
)
|
|
561
|
+
downloaded_models = list(
|
|
562
|
+
executor.map(download_fn, filtered_data[column_to_use])
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
# Add the downloaded models to the DataFrame
|
|
566
|
+
filtered_data["models"] = downloaded_models
|
|
567
|
+
|
|
568
|
+
if historical or bias_correction:
|
|
569
|
+
hist = filtered_data[filtered_data["experiment"] == "historical"]["models"].iloc[0]
|
|
570
|
+
proj = filtered_data[filtered_data["experiment"] == rcp]["models"].iloc[0]
|
|
571
|
+
|
|
572
|
+
hist = hist.interpolate_na(dim="time", method="linear")
|
|
573
|
+
proj = proj.interpolate_na(dim="time", method="linear")
|
|
574
|
+
else:
|
|
575
|
+
proj = filtered_data["models"].iloc[0]
|
|
576
|
+
proj = proj.interpolate_na(dim="time", method="linear")
|
|
577
|
+
|
|
578
|
+
if bias_correction and historical:
|
|
579
|
+
# Load observations for bias correction
|
|
580
|
+
ref = future_obs.result()
|
|
581
|
+
log.info("Training eqm with leave-one-out cross-validation")
|
|
582
|
+
|
|
583
|
+
# Use leave-one-out cross-validation for historical bias correction
|
|
584
|
+
hist_bs = _leave_one_out_bias_correction(ref, hist, variable, log)
|
|
585
|
+
|
|
586
|
+
# For projections, train on all historical data
|
|
587
|
+
QM_mo = sdba.EmpiricalQuantileMapping.train(
|
|
588
|
+
ref,
|
|
589
|
+
hist,
|
|
590
|
+
group="time.month",
|
|
591
|
+
kind="*" if variable in ["pr", "rsds", "sfcWind"] else "+",
|
|
592
|
+
)
|
|
593
|
+
log.info("Performing bias correction on projections with full historical training")
|
|
594
|
+
proj_bs = QM_mo.adjust(proj, extrapolation="constant", interp="linear")
|
|
595
|
+
log.info("Done!")
|
|
596
|
+
if variable == "hurs":
|
|
597
|
+
proj_bs = proj_bs.where(proj_bs <= 100, 100)
|
|
598
|
+
proj_bs = proj_bs.where(proj_bs >= 0, 0)
|
|
599
|
+
combined = xr.concat([hist_bs, proj_bs], dim="time")
|
|
600
|
+
return combined
|
|
601
|
+
|
|
602
|
+
elif not bias_correction and historical:
|
|
603
|
+
combined = xr.concat([hist, proj], dim="time")
|
|
604
|
+
return combined
|
|
605
|
+
|
|
606
|
+
elif bias_correction and not historical:
|
|
607
|
+
ref = future_obs.result()
|
|
608
|
+
log.info("Training eqm with historical data")
|
|
609
|
+
QM_mo = sdba.EmpiricalQuantileMapping.train(
|
|
610
|
+
ref,
|
|
611
|
+
hist,
|
|
612
|
+
group="time.month",
|
|
613
|
+
kind="*" if variable in ["pr", "rsds", "sfcWind"] else "+",
|
|
614
|
+
) # multiplicative approach for pr, rsds and wind speed
|
|
615
|
+
log.info("Performing bias correction with eqm")
|
|
616
|
+
proj_bs = QM_mo.adjust(proj, extrapolation="constant", interp="linear")
|
|
617
|
+
log.info("Done!")
|
|
618
|
+
if variable == "hurs":
|
|
619
|
+
proj_bs = proj_bs.where(proj_bs <= 100, 100)
|
|
620
|
+
proj_bs = proj_bs.where(proj_bs >= 0, 0)
|
|
621
|
+
return proj_bs
|
|
622
|
+
|
|
623
|
+
return proj
|
|
624
|
+
|
|
625
|
+
else: # when observations are True
|
|
626
|
+
downloaded_obs = future_obs.result()
|
|
627
|
+
log.info("Done!")
|
|
628
|
+
return downloaded_obs
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def _thread_download_data(url: str | None, **kwargs):
|
|
632
|
+
variable = kwargs["variable"]
|
|
633
|
+
temporal = "observations" if kwargs["obs"] else ("historical" if "historical" in str(url) else "projections")
|
|
634
|
+
log = logger.getChild(f"{variable}-{temporal}")
|
|
635
|
+
try:
|
|
636
|
+
return _download_data(url=url, **kwargs)
|
|
637
|
+
except Exception as e:
|
|
638
|
+
log.exception(f"Failed to process data from {url}: {e}")
|
|
639
|
+
raise
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def _download_data(
|
|
643
|
+
url: str | None,
|
|
644
|
+
bbox: dict[str, tuple[float, float]],
|
|
645
|
+
variable: str,
|
|
646
|
+
obs: bool,
|
|
647
|
+
years_obs: range,
|
|
648
|
+
years_up_to: int,
|
|
649
|
+
remote: bool,
|
|
650
|
+
) -> xr.DataArray:
|
|
651
|
+
temporal = "observations" if obs else ("historical" if url and "historical" in url else "projections")
|
|
652
|
+
log = logger.getChild(f"{variable}-{temporal}")
|
|
653
|
+
|
|
654
|
+
if obs:
|
|
655
|
+
var = VARIABLES_MAP[variable]
|
|
656
|
+
log.info(f"Establishing connection to ERA5 data for {variable}({var})")
|
|
657
|
+
if remote:
|
|
658
|
+
ds_var = xr.open_dataset(ERA5_DATA_REMOTE_URL)[var]
|
|
659
|
+
else:
|
|
660
|
+
ds_var = xr.open_dataset(ERA5_DATA_LOCAL_PATH)[var]
|
|
661
|
+
log.info(f"Connection to ERA5 data for {variable}({var}) has been established")
|
|
662
|
+
|
|
663
|
+
# Coordinate normalization and renaming for 'hurs'
|
|
664
|
+
if var == "hurs":
|
|
665
|
+
ds_var = ds_var.rename({"lat": "latitude", "lon": "longitude"})
|
|
666
|
+
ds_cropped = ds_var.sel(
|
|
667
|
+
longitude=slice(bbox["xlim"][0], bbox["xlim"][1]),
|
|
668
|
+
latitude=slice(bbox["ylim"][0], bbox["ylim"][1]),
|
|
669
|
+
)
|
|
670
|
+
else:
|
|
671
|
+
ds_var.coords["longitude"] = (ds_var.coords["longitude"] + 180) % 360 - 180
|
|
672
|
+
ds_var = ds_var.sortby(ds_var.longitude)
|
|
673
|
+
ds_cropped = ds_var.sel(
|
|
674
|
+
longitude=slice(bbox["xlim"][0], bbox["xlim"][1]),
|
|
675
|
+
latitude=slice(bbox["ylim"][1], bbox["ylim"][0]),
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
# Unit conversion
|
|
679
|
+
if var in ["t2mx", "t2mn", "t2m"]:
|
|
680
|
+
ds_cropped -= 273.15 # Convert from Kelvin to Celsius
|
|
681
|
+
ds_cropped.attrs["units"] = "°C"
|
|
682
|
+
elif var == "tp":
|
|
683
|
+
ds_cropped *= 1000 # Convert precipitation
|
|
684
|
+
ds_cropped.attrs["units"] = "mm"
|
|
685
|
+
elif var == "ssrd":
|
|
686
|
+
ds_cropped /= 86400 # Convert from J/m^2 to W/m^2
|
|
687
|
+
ds_cropped.attrs["units"] = "W m-2"
|
|
688
|
+
elif var == "sfcwind":
|
|
689
|
+
ds_cropped = ds_cropped * (
|
|
690
|
+
4.87 / np.log((67.8 * 10) - 5.42)
|
|
691
|
+
) # Convert wind speed from 10 m to 2 m
|
|
692
|
+
ds_cropped.attrs["units"] = "m s-1"
|
|
693
|
+
|
|
694
|
+
# Select years
|
|
695
|
+
years = [x for x in years_obs]
|
|
696
|
+
time_mask = (ds_cropped["time"].dt.year >= years[0]) & (
|
|
697
|
+
ds_cropped["time"].dt.year <= years[-1]
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
else:
|
|
701
|
+
log.info(f"Establishing connection to CORDEX data for {variable}")
|
|
702
|
+
ds_var = xr.open_dataset(url)[variable]
|
|
703
|
+
|
|
704
|
+
# Check if time dimension has a prefix, indicating variable is not available
|
|
705
|
+
time_dims = [dim for dim in ds_var.dims if dim.startswith('time_')]
|
|
706
|
+
if time_dims:
|
|
707
|
+
msg = f"Variable {variable} is not available for this model: {url}"
|
|
708
|
+
log.exception(msg)
|
|
709
|
+
raise ValueError(msg)
|
|
710
|
+
|
|
711
|
+
log.info(f"Connection to CORDEX data for {variable} has been established")
|
|
712
|
+
ds_cropped = ds_var.sel(
|
|
713
|
+
longitude=slice(bbox["xlim"][0], bbox["xlim"][1]),
|
|
714
|
+
latitude=slice(bbox["ylim"][1], bbox["ylim"][0]),
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
# Unit conversion
|
|
718
|
+
if variable in ["tas", "tasmax", "tasmin"]:
|
|
719
|
+
ds_cropped -= 273.15 # Convert from Kelvin to Celsius
|
|
720
|
+
ds_cropped.attrs["units"] = "°C"
|
|
721
|
+
elif variable == "pr":
|
|
722
|
+
ds_cropped *= 86400 # Convert from kg m^-2 s^-1 to mm/day
|
|
723
|
+
ds_cropped.attrs["units"] = "mm"
|
|
724
|
+
elif variable == "rsds":
|
|
725
|
+
ds_cropped.attrs["units"] = "W m-2"
|
|
726
|
+
elif variable == "sfcWind":
|
|
727
|
+
ds_cropped = ds_cropped * (
|
|
728
|
+
4.87 / np.log((67.8 * 10) - 5.42)
|
|
729
|
+
) # Convert wind speed from 10 m to 2 m
|
|
730
|
+
ds_cropped.attrs["units"] = "m s-1"
|
|
731
|
+
|
|
732
|
+
# Select years based on rcp
|
|
733
|
+
if "rcp" in url:
|
|
734
|
+
years = [x for x in range(2006, years_up_to + 1)]
|
|
735
|
+
else:
|
|
736
|
+
years = [x for x in DEFAULT_YEARS_OBS]
|
|
737
|
+
|
|
738
|
+
# Add missing dates
|
|
739
|
+
ds_cropped = ds_cropped.convert_calendar(
|
|
740
|
+
calendar="gregorian", missing=np.nan, align_on="date"
|
|
741
|
+
)
|
|
742
|
+
|
|
743
|
+
time_mask = (ds_cropped["time"].dt.year >= years[0]) & (
|
|
744
|
+
ds_cropped["time"].dt.year <= years[-1]
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
# subset years
|
|
748
|
+
ds_cropped = ds_cropped.sel(time=time_mask)
|
|
749
|
+
|
|
750
|
+
assert isinstance(ds_cropped, xr.DataArray)
|
|
751
|
+
|
|
752
|
+
if obs:
|
|
753
|
+
log.info(
|
|
754
|
+
f"ERA5 data for {variable} has been processed: unit conversion ({ds_cropped.attrs.get('units', 'unknown units')}), time selection ({years[0]}-{years[-1]})"
|
|
755
|
+
)
|
|
756
|
+
else:
|
|
757
|
+
log.info(
|
|
758
|
+
f"CORDEX data for {variable} has been processed: unit conversion ({ds_cropped.attrs.get('units', 'unknown units')}), calendar transformation (360-day to Gregorian), time selection ({years[0]}-{years[-1]})"
|
|
759
|
+
)
|
|
760
|
+
|
|
761
|
+
return ds_cropped
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
if __name__ == "__main__":
|
|
765
|
+
# Example 1: Get observational data
|
|
766
|
+
print("Getting observational data...")
|
|
767
|
+
obs_data = get_climate_data(
|
|
768
|
+
country="Togo",
|
|
769
|
+
obs=True,
|
|
770
|
+
years_obs=range(1990, 2011),
|
|
771
|
+
variables=["pr", "tasmax"]
|
|
772
|
+
)
|
|
773
|
+
print("Observational data keys:", list(obs_data.keys()))
|
|
774
|
+
|
|
775
|
+
# Example 2: Get CORDEX bc projection data and bc historical data
|
|
776
|
+
print("\nGetting CORDEX projection data...")
|
|
777
|
+
proj_data = get_climate_data(
|
|
778
|
+
country="Togo",
|
|
779
|
+
variables=["tasmax", "tasmin"],
|
|
780
|
+
cordex_domain="AFR-22",
|
|
781
|
+
rcp="rcp26",
|
|
782
|
+
gcm="MPI",
|
|
783
|
+
rcm="Reg",
|
|
784
|
+
years_up_to=2010,
|
|
785
|
+
historical=True,
|
|
786
|
+
bias_correction=True
|
|
787
|
+
)
|
|
788
|
+
print("Projection data keys:", list(proj_data.keys()))
|