disdrodb 0.1.3__py3-none-any.whl → 0.1.5__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.
Files changed (124) hide show
  1. disdrodb/__init__.py +4 -0
  2. disdrodb/_version.py +2 -2
  3. disdrodb/api/checks.py +70 -47
  4. disdrodb/api/configs.py +0 -2
  5. disdrodb/api/create_directories.py +0 -2
  6. disdrodb/api/info.py +3 -3
  7. disdrodb/api/io.py +48 -8
  8. disdrodb/api/path.py +116 -133
  9. disdrodb/api/search.py +12 -3
  10. disdrodb/cli/disdrodb_create_summary.py +113 -0
  11. disdrodb/cli/disdrodb_create_summary_station.py +11 -1
  12. disdrodb/cli/disdrodb_run_l0a_station.py +1 -1
  13. disdrodb/cli/disdrodb_run_l0b_station.py +2 -2
  14. disdrodb/cli/disdrodb_run_l0c_station.py +2 -2
  15. disdrodb/cli/disdrodb_run_l1_station.py +2 -2
  16. disdrodb/cli/disdrodb_run_l2e_station.py +2 -2
  17. disdrodb/cli/disdrodb_run_l2m_station.py +2 -2
  18. disdrodb/constants.py +1 -1
  19. disdrodb/data_transfer/download_data.py +123 -7
  20. disdrodb/etc/products/L1/global.yaml +1 -1
  21. disdrodb/etc/products/L2E/5MIN.yaml +1 -0
  22. disdrodb/etc/products/L2E/global.yaml +1 -1
  23. disdrodb/etc/products/L2M/GAMMA_GS_ND_MAE.yaml +6 -0
  24. disdrodb/etc/products/L2M/GAMMA_ML.yaml +1 -1
  25. disdrodb/etc/products/L2M/LOGNORMAL_GS_LOG_ND_MAE.yaml +6 -0
  26. disdrodb/etc/products/L2M/LOGNORMAL_GS_ND_MAE.yaml +6 -0
  27. disdrodb/etc/products/L2M/LOGNORMAL_ML.yaml +8 -0
  28. disdrodb/etc/products/L2M/global.yaml +11 -3
  29. disdrodb/issue/writer.py +2 -0
  30. disdrodb/l0/check_configs.py +49 -16
  31. disdrodb/l0/configs/LPM/l0a_encodings.yml +2 -2
  32. disdrodb/l0/configs/LPM/l0b_cf_attrs.yml +2 -2
  33. disdrodb/l0/configs/LPM/l0b_encodings.yml +2 -2
  34. disdrodb/l0/configs/LPM/raw_data_format.yml +2 -2
  35. disdrodb/l0/configs/PWS100/l0b_encodings.yml +1 -0
  36. disdrodb/l0/configs/SWS250/bins_diameter.yml +108 -0
  37. disdrodb/l0/configs/SWS250/bins_velocity.yml +83 -0
  38. disdrodb/l0/configs/SWS250/l0a_encodings.yml +18 -0
  39. disdrodb/l0/configs/SWS250/l0b_cf_attrs.yml +72 -0
  40. disdrodb/l0/configs/SWS250/l0b_encodings.yml +155 -0
  41. disdrodb/l0/configs/SWS250/raw_data_format.yml +148 -0
  42. disdrodb/l0/l0a_processing.py +10 -5
  43. disdrodb/l0/l0b_nc_processing.py +10 -6
  44. disdrodb/l0/l0b_processing.py +92 -72
  45. disdrodb/l0/l0c_processing.py +369 -251
  46. disdrodb/l0/readers/LPM/ARM/ARM_LPM.py +8 -1
  47. disdrodb/l0/readers/LPM/AUSTRALIA/MELBOURNE_2007_LPM.py +2 -2
  48. disdrodb/l0/readers/LPM/BELGIUM/ULIEGE.py +256 -0
  49. disdrodb/l0/readers/LPM/BRAZIL/CHUVA_LPM.py +2 -2
  50. disdrodb/l0/readers/LPM/BRAZIL/GOAMAZON_LPM.py +2 -2
  51. disdrodb/l0/readers/LPM/GERMANY/DWD.py +491 -0
  52. disdrodb/l0/readers/LPM/ITALY/GID_LPM.py +2 -2
  53. disdrodb/l0/readers/LPM/ITALY/GID_LPM_W.py +2 -2
  54. disdrodb/l0/readers/LPM/KIT/CHWALA.py +2 -2
  55. disdrodb/l0/readers/LPM/SLOVENIA/ARSO.py +107 -12
  56. disdrodb/l0/readers/LPM/SLOVENIA/UL.py +3 -3
  57. disdrodb/l0/readers/LPM/SWITZERLAND/INNERERIZ_LPM.py +2 -2
  58. disdrodb/l0/readers/PARSIVEL/NCAR/VORTEX2_2010.py +5 -14
  59. disdrodb/l0/readers/PARSIVEL/NCAR/VORTEX2_2010_UF.py +5 -14
  60. disdrodb/l0/readers/PARSIVEL/SLOVENIA/UL.py +117 -8
  61. disdrodb/l0/readers/PARSIVEL2/ARM/ARM_PARSIVEL2.py +4 -0
  62. disdrodb/l0/readers/PARSIVEL2/BRAZIL/CHUVA_PARSIVEL2.py +10 -14
  63. disdrodb/l0/readers/PARSIVEL2/BRAZIL/GOAMAZON_PARSIVEL2.py +10 -14
  64. disdrodb/l0/readers/PARSIVEL2/CANADA/UQAM_NC.py +69 -0
  65. disdrodb/l0/readers/PARSIVEL2/DENMARK/DTU.py +8 -14
  66. disdrodb/l0/readers/PARSIVEL2/DENMARK/EROSION_raw.py +382 -0
  67. disdrodb/l0/readers/PARSIVEL2/FINLAND/FMI_PARSIVEL2.py +4 -0
  68. disdrodb/l0/readers/PARSIVEL2/FRANCE/OSUG.py +1 -1
  69. disdrodb/l0/readers/PARSIVEL2/GREECE/NOA.py +127 -0
  70. disdrodb/l0/readers/PARSIVEL2/ITALY/HYDROX.py +239 -0
  71. disdrodb/l0/readers/PARSIVEL2/MPI/BCO_PARSIVEL2.py +136 -0
  72. disdrodb/l0/readers/PARSIVEL2/MPI/BOWTIE.py +220 -0
  73. disdrodb/l0/readers/PARSIVEL2/NASA/LPVEX.py +109 -0
  74. disdrodb/l0/readers/PARSIVEL2/NCAR/FARM_PARSIVEL2.py +5 -11
  75. disdrodb/l0/readers/PARSIVEL2/NCAR/PERILS_MIPS.py +4 -17
  76. disdrodb/l0/readers/PARSIVEL2/NCAR/RELAMPAGO_PARSIVEL2.py +5 -14
  77. disdrodb/l0/readers/PARSIVEL2/NCAR/SNOWIE_PJ.py +10 -13
  78. disdrodb/l0/readers/PARSIVEL2/NCAR/SNOWIE_SB.py +10 -13
  79. disdrodb/l0/readers/PARSIVEL2/NETHERLANDS/DELFT_NC.py +3 -0
  80. disdrodb/l0/readers/PARSIVEL2/PHILIPPINES/PANGASA.py +232 -0
  81. disdrodb/l0/readers/PARSIVEL2/SPAIN/CENER.py +6 -18
  82. disdrodb/l0/readers/PARSIVEL2/SPAIN/GRANADA.py +120 -0
  83. disdrodb/l0/readers/PARSIVEL2/USA/C3WE.py +7 -25
  84. disdrodb/l0/readers/PWS100/AUSTRIA/HOAL.py +321 -0
  85. disdrodb/l0/readers/SW250/BELGIUM/KMI.py +239 -0
  86. disdrodb/l1/beard_model.py +31 -129
  87. disdrodb/l1/fall_velocity.py +156 -57
  88. disdrodb/l1/filters.py +25 -28
  89. disdrodb/l1/processing.py +12 -14
  90. disdrodb/l1_env/routines.py +46 -17
  91. disdrodb/l2/empirical_dsd.py +6 -0
  92. disdrodb/l2/processing.py +3 -3
  93. disdrodb/metadata/checks.py +132 -125
  94. disdrodb/metadata/geolocation.py +0 -2
  95. disdrodb/psd/fitting.py +180 -210
  96. disdrodb/psd/models.py +1 -1
  97. disdrodb/routines/__init__.py +54 -0
  98. disdrodb/{l0/routines.py → routines/l0.py} +288 -418
  99. disdrodb/{l1/routines.py → routines/l1.py} +60 -92
  100. disdrodb/{l2/routines.py → routines/l2.py} +284 -485
  101. disdrodb/{routines.py → routines/wrappers.py} +100 -7
  102. disdrodb/scattering/axis_ratio.py +95 -85
  103. disdrodb/scattering/permittivity.py +24 -0
  104. disdrodb/scattering/routines.py +56 -36
  105. disdrodb/summary/routines.py +147 -45
  106. disdrodb/utils/archiving.py +434 -0
  107. disdrodb/utils/attrs.py +2 -0
  108. disdrodb/utils/cli.py +5 -5
  109. disdrodb/utils/dask.py +62 -1
  110. disdrodb/utils/decorators.py +31 -0
  111. disdrodb/utils/encoding.py +10 -1
  112. disdrodb/{l2 → utils}/event.py +1 -66
  113. disdrodb/utils/logger.py +1 -1
  114. disdrodb/utils/manipulations.py +22 -12
  115. disdrodb/utils/routines.py +166 -0
  116. disdrodb/utils/time.py +5 -293
  117. disdrodb/utils/xarray.py +3 -0
  118. disdrodb/viz/plots.py +109 -15
  119. {disdrodb-0.1.3.dist-info → disdrodb-0.1.5.dist-info}/METADATA +3 -2
  120. {disdrodb-0.1.3.dist-info → disdrodb-0.1.5.dist-info}/RECORD +124 -96
  121. {disdrodb-0.1.3.dist-info → disdrodb-0.1.5.dist-info}/entry_points.txt +1 -0
  122. {disdrodb-0.1.3.dist-info → disdrodb-0.1.5.dist-info}/WHEEL +0 -0
  123. {disdrodb-0.1.3.dist-info → disdrodb-0.1.5.dist-info}/licenses/LICENSE +0 -0
  124. {disdrodb-0.1.3.dist-info → disdrodb-0.1.5.dist-info}/top_level.txt +0 -0
@@ -20,7 +20,7 @@ import time
20
20
  from typing import Optional
21
21
 
22
22
  from disdrodb.api.search import available_stations, get_required_product
23
- from disdrodb.utils.cli import _execute_cmd
23
+ from disdrodb.utils.cli import execute_cmd
24
24
 
25
25
  ####--------------------------------------------------------------------------.
26
26
  #### Run DISDRODB Station Processing
@@ -239,7 +239,7 @@ def run_l0a_station(
239
239
  ],
240
240
  )
241
241
  # Execute command
242
- _execute_cmd(cmd)
242
+ execute_cmd(cmd)
243
243
 
244
244
 
245
245
  def run_l0b_station(
@@ -323,7 +323,7 @@ def run_l0b_station(
323
323
  ],
324
324
  )
325
325
  # Execute command
326
- _execute_cmd(cmd)
326
+ execute_cmd(cmd)
327
327
 
328
328
 
329
329
  def run_l0c_station(
@@ -407,7 +407,7 @@ def run_l0c_station(
407
407
  ],
408
408
  )
409
409
  # Execute command
410
- _execute_cmd(cmd)
410
+ execute_cmd(cmd)
411
411
 
412
412
 
413
413
  def run_l1_station(
@@ -483,7 +483,7 @@ def run_l1_station(
483
483
  ],
484
484
  )
485
485
  # Execute command
486
- _execute_cmd(cmd)
486
+ execute_cmd(cmd)
487
487
 
488
488
 
489
489
  def run_l2e_station(
@@ -559,7 +559,7 @@ def run_l2e_station(
559
559
  ],
560
560
  )
561
561
  # Execute command
562
- _execute_cmd(cmd)
562
+ execute_cmd(cmd)
563
563
 
564
564
 
565
565
  def run_l2m_station(
@@ -635,7 +635,36 @@ def run_l2m_station(
635
635
  ],
636
636
  )
637
637
  # Execute command
638
- _execute_cmd(cmd)
638
+ execute_cmd(cmd)
639
+
640
+
641
+ def create_summary_station(
642
+ data_source,
643
+ campaign_name,
644
+ station_name,
645
+ parallel=False,
646
+ temporal_resolution="1MIN",
647
+ data_archive_dir=None,
648
+ ):
649
+ """Create summary figures and tables for a DISDRODB station."""
650
+ # Define command
651
+ cmd = " ".join(
652
+ [
653
+ "disdrodb_create_summary_station",
654
+ # Station arguments
655
+ data_source,
656
+ campaign_name,
657
+ station_name,
658
+ "--data_archive_dir",
659
+ str(data_archive_dir),
660
+ "--parallel",
661
+ str(parallel),
662
+ "--temporal_resolution",
663
+ str(temporal_resolution),
664
+ ],
665
+ )
666
+ # Execute command
667
+ execute_cmd(cmd)
639
668
 
640
669
 
641
670
  ####--------------------------------------------------------------------------.
@@ -1409,4 +1438,68 @@ def run_l2m(
1409
1438
  print(f"{product} processing of {data_source} {campaign_name} {station_name} station ended.")
1410
1439
 
1411
1440
 
1441
+ def create_summary(
1442
+ data_sources=None,
1443
+ campaign_names=None,
1444
+ station_names=None,
1445
+ parallel=False,
1446
+ temporal_resolution="1MIN",
1447
+ data_archive_dir=None,
1448
+ metadata_archive_dir=None,
1449
+ ):
1450
+ """Create summary figures and tables for a set of DISDRODB station.
1451
+
1452
+ Parameters
1453
+ ----------
1454
+ data_sources : list
1455
+ Name of data source(s) to process.
1456
+ The name(s) must be UPPER CASE.
1457
+ If campaign_names and station are not specified, process all stations.
1458
+ The default value is ``None``.
1459
+ campaign_names : list
1460
+ Name of the campaign(s) to process.
1461
+ The name(s) must be UPPER CASE.
1462
+ The default value is ``None``.
1463
+ station_names : list
1464
+ Station names to process.
1465
+ The default value is ``None``.
1466
+ data_archive_dir : str (optional)
1467
+ The directory path where the DISDRODB Data Archive is located.
1468
+ The directory path must end with ``<...>/DISDRODB``.
1469
+ If ``None``, it uses the ``data_archive_dir`` path specified
1470
+ in the DISDRODB active configuration.
1471
+ """
1472
+ # Get list of available stations
1473
+ list_info = available_stations(
1474
+ # DISDRODB root directories
1475
+ data_archive_dir=data_archive_dir,
1476
+ metadata_archive_dir=metadata_archive_dir,
1477
+ # Stations arguments
1478
+ data_sources=data_sources,
1479
+ campaign_names=campaign_names,
1480
+ station_names=station_names,
1481
+ # Search options
1482
+ product="L2E",
1483
+ product_kwargs={"rolling": False, "sample_interval": 60},
1484
+ raise_error_if_empty=True,
1485
+ )
1486
+
1487
+ # Loop over stations
1488
+ print(f"Creation of summaries for {len(list_info)} stations has started.")
1489
+ for data_source, campaign_name, station_name in list_info:
1490
+ # Run processing
1491
+ create_summary_station(
1492
+ # DISDRODB root directories
1493
+ data_archive_dir=data_archive_dir,
1494
+ # Station arguments
1495
+ data_source=data_source,
1496
+ campaign_name=campaign_name,
1497
+ station_name=station_name,
1498
+ # Processing option
1499
+ parallel=parallel,
1500
+ temporal_resolution=temporal_resolution,
1501
+ )
1502
+ print("Creation of station summaries has terminated.")
1503
+
1504
+
1412
1505
  ####--------------------------------------------------------------------------.
@@ -20,90 +20,6 @@ import numpy as np
20
20
  import xarray as xr
21
21
 
22
22
 
23
- def available_axis_ratio_models():
24
- """Return a list of the available drop axis ratio models."""
25
- return list(AXIS_RATIO_MODELS)
26
-
27
-
28
- def get_axis_ratio_model(model):
29
- """Return the specified drop axis ratio model.
30
-
31
- Parameters
32
- ----------
33
- model : str
34
- The model to use for calculating the axis ratio. Available models are:
35
- 'Thurai2005', 'Thurai2007', 'Battaglia2010', 'Brandes2002',
36
- 'Pruppacher1970', 'Beard1987', 'Andsager1999'.
37
-
38
- Returns
39
- -------
40
- callable
41
- A function which compute the vertical-to-horizontal axis ratio given a
42
- particle diameter in mm.
43
-
44
- Notes
45
- -----
46
- This function serves as a wrapper to various axis ratio models for raindrops.
47
- It returns the appropriate model based on the `model` parameter.
48
-
49
- Please note that the axis ratio function to be provided to pyTmatrix expects to
50
- return a horizontal-to-vertical axis ratio !
51
-
52
- """
53
- model = check_axis_ratio_model(model)
54
- return AXIS_RATIO_MODELS[model]
55
-
56
-
57
- def check_axis_ratio_model(model):
58
- """Check validity of the specified drop axis ratio model."""
59
- available_models = available_axis_ratio_models()
60
- if model not in available_models:
61
- raise ValueError(f"{model} is an invalid axis-ratio model. Valid models: {available_models}.")
62
- return model
63
-
64
-
65
- def get_axis_ratio(diameter, model):
66
- """
67
- Compute the axis ratio of raindrops using the specified model.
68
-
69
- Parameters
70
- ----------
71
- diameter : array-like
72
- Raindrops diameter in mm.
73
- model : str
74
- The axis ratio model to use for calculating the axis ratio. Available models are:
75
- 'Thurai2005', 'Thurai2007', 'Battaglia2010', 'Brandes2002',
76
- 'Pruppacher1970', 'Beard1987', 'Andsager1999'.
77
-
78
- Returns
79
- -------
80
- axis_ratio : array-like
81
- The vertical-to-horizontal drop axis ratio corresponding to the input diameters.
82
- Values of 1 indicate spherical particles, while values <1 indicate oblate particles.
83
- Values >1 means prolate particles.
84
-
85
- Notes
86
- -----
87
- This function serves as a wrapper to various axis ratio models for raindrops.
88
- It selects and applies the appropriate model based on the `model` parameter.
89
-
90
- Examples
91
- --------
92
- >>> diameter = np.array([0.5, 1.0, 2.0, 3.0])
93
- >>> axis_ratio = get_axis_ratio(diameter, model="Brandes2002")
94
-
95
- """
96
- # Retrieve axis ratio function
97
- axis_ratio_func = get_axis_ratio_model(model)
98
-
99
- # Retrieve axis ratio
100
- axis_ratio = axis_ratio_func(diameter)
101
-
102
- # Clip values between 0 and 1
103
- axis_ratio = np.clip(axis_ratio, 0, 1)
104
- return axis_ratio
105
-
106
-
107
23
  def get_axis_ratio_andsager_1999(diameter):
108
24
  """
109
25
  Compute the axis ratio of raindrops using the Andsager et al. (1999) model.
@@ -295,6 +211,8 @@ def get_axis_ratio_thurai_2005(diameter):
295
211
  """
296
212
  Compute the axis ratio of raindrops using the Thurai et al. (2005) model.
297
213
 
214
+ This model is assumed to be valid only for particles up to 5 mm.
215
+
298
216
  Parameters
299
217
  ----------
300
218
  diameter : array-like
@@ -311,7 +229,6 @@ def get_axis_ratio_thurai_2005(diameter):
311
229
  J. Atmos. Oceanic Technol., 22, 966-978, https://doi.org/10.1175/JTECH1767.1
312
230
 
313
231
  """
314
- # Valid between 1 and 5 mm
315
232
  axis_ratio = 0.9707 + 4.26e-2 * diameter - 4.29e-2 * diameter**2 + 6.5e-3 * diameter**3 - 3e-4 * diameter**4
316
233
  return axis_ratio
317
234
 
@@ -350,6 +267,9 @@ def get_axis_ratio_thurai_2007(diameter):
350
267
  # Combine axis ratio
351
268
  axis_ratio_below_1_5 = xr.where(diameter > 0.7, axis_ratio_below_1_5, axis_ratio_below_0_7)
352
269
  axis_ratio = xr.where(diameter > 1.5, axis_ratio_above_1_5, axis_ratio_below_1_5)
270
+
271
+ # Ensure np.nan is preserved in arrays
272
+ axis_ratio = xr.where(np.isnan(diameter), np.nan, axis_ratio)
353
273
  return axis_ratio
354
274
 
355
275
 
@@ -362,3 +282,93 @@ AXIS_RATIO_MODELS = {
362
282
  "Beard1987": get_axis_ratio_beard_1987,
363
283
  "Andsager1999": get_axis_ratio_andsager_1999,
364
284
  }
285
+
286
+
287
+ def available_axis_ratio_models():
288
+ """Return a list of the available drop axis ratio models."""
289
+ return list(AXIS_RATIO_MODELS)
290
+
291
+
292
+ def check_axis_ratio_model(model):
293
+ """Check validity of the specified drop axis ratio model."""
294
+ available_models = available_axis_ratio_models()
295
+ if model not in available_models:
296
+ raise ValueError(f"{model} is an invalid axis-ratio model. Valid models: {available_models}.")
297
+ return model
298
+
299
+
300
+ def get_axis_ratio_model(model):
301
+ """Return the specified drop axis ratio model.
302
+
303
+ Parameters
304
+ ----------
305
+ model : str
306
+ The model to use for calculating the axis ratio. Available models are:
307
+ 'Thurai2005', 'Thurai2007', 'Battaglia2010', 'Brandes2002',
308
+ 'Pruppacher1970', 'Beard1987', 'Andsager1999'.
309
+
310
+ Returns
311
+ -------
312
+ callable
313
+ A function which compute the vertical-to-horizontal axis ratio given a
314
+ particle diameter in mm.
315
+
316
+ Notes
317
+ -----
318
+ This function serves as a wrapper to various axis ratio models for raindrops.
319
+ It returns the appropriate model based on the `model` parameter.
320
+
321
+ Please note that the axis ratio function to be provided to pyTmatrix expects to
322
+ return a horizontal-to-vertical axis ratio !
323
+
324
+ """
325
+ model = check_axis_ratio_model(model)
326
+ return AXIS_RATIO_MODELS[model]
327
+
328
+
329
+ def get_axis_ratio(diameter, model):
330
+ """
331
+ Compute the axis ratio of raindrops using the specified model.
332
+
333
+ Parameters
334
+ ----------
335
+ diameter : array-like
336
+ Raindrops diameter in mm.
337
+ model : str
338
+ The axis ratio model to use for calculating the axis ratio. Available models are:
339
+ 'Thurai2005', 'Thurai2007', 'Battaglia2010', 'Brandes2002',
340
+ 'Pruppacher1970', 'Beard1987', 'Andsager1999'.
341
+
342
+ Returns
343
+ -------
344
+ axis_ratio : array-like
345
+ The vertical-to-horizontal drop axis ratio corresponding to the input diameters.
346
+ Values of 1 indicate spherical particles, while values <1 indicate oblate particles.
347
+ Values >1 means prolate particles.
348
+
349
+ Notes
350
+ -----
351
+ This function serves as a wrapper to various axis ratio models for raindrops.
352
+ It selects and applies the appropriate model based on the `model` parameter.
353
+
354
+ Examples
355
+ --------
356
+ >>> diameter = np.array([0.5, 1.0, 2.0, 3.0])
357
+ >>> axis_ratio = get_axis_ratio(diameter, model="Brandes2002")
358
+
359
+ """
360
+ # Retrieve axis ratio function
361
+ axis_ratio_func = get_axis_ratio_model(model)
362
+
363
+ # Retrieve axis ratio
364
+ axis_ratio = axis_ratio_func(diameter)
365
+
366
+ # Clip values between 0 and 1
367
+ axis_ratio = np.clip(axis_ratio, 0, 1)
368
+
369
+ # Add attributes
370
+ if isinstance(axis_ratio, xr.DataArray):
371
+ axis_ratio.name = "axis_ratio"
372
+ axis_ratio.attrs["units"] = ""
373
+ axis_ratio.attrs["model"] = model
374
+ return axis_ratio
@@ -124,11 +124,35 @@ def get_refractive_index(temperature, frequency, permittivity_model):
124
124
  >>> m = get_refractive_index(temperature=temperature, frequency=frequency, permittivity_model="Liebe1991")
125
125
 
126
126
  """
127
+ # Ensure input is numpy array or xr.DataArray
128
+ frequency = ensure_array(frequency)
129
+ temperature = ensure_array(temperature)
130
+
131
+ # If both inputs are numpy (or dask) arrays with size > 1 → raise error
132
+ if (
133
+ not isinstance(temperature, xr.DataArray)
134
+ and not isinstance(frequency, xr.DataArray)
135
+ and np.size(temperature) > 1
136
+ and np.size(frequency) > 1
137
+ ):
138
+ raise ValueError(
139
+ "get_refractive_index does not support broadcasting plain numpy/dask arrays "
140
+ "when both `temperature` and `frequency` have size > 1. "
141
+ "Please provide both input as xarray.DataArray objects "
142
+ "with different dimensions to enable labeled broadcasting.",
143
+ )
144
+
127
145
  # Retrieve refractive_index function
128
146
  func = get_refractive_index_function(permittivity_model)
129
147
 
130
148
  # Retrieve refractive_index
131
149
  refractive_index = func(temperature=temperature, frequency=frequency)
150
+
151
+ # Add attributes
152
+ if isinstance(refractive_index, xr.DataArray):
153
+ refractive_index.name = "refractive_index"
154
+ refractive_index.attrs["units"] = ""
155
+ refractive_index.attrs["model"] = permittivity_model
132
156
  return refractive_index
133
157
 
134
158
 
@@ -432,6 +432,41 @@ def load_scatterer(
432
432
  return scatterer
433
433
 
434
434
 
435
+ def precompute_scattering_tables(
436
+ frequency,
437
+ num_points,
438
+ diameter_max,
439
+ canting_angle_std,
440
+ axis_ratio_model,
441
+ permittivity_model,
442
+ water_temperature,
443
+ elevation_angle,
444
+ verbose=True,
445
+ ):
446
+ """Precompute the pyTMatrix scattering tables required for radar variables simulations."""
447
+ from disdrodb.scattering.routines import get_list_simulations_params, load_scatterer
448
+
449
+ # Define parameters for all requested simulations
450
+ list_params = get_list_simulations_params(
451
+ frequency=frequency,
452
+ num_points=num_points,
453
+ diameter_max=diameter_max,
454
+ canting_angle_std=canting_angle_std,
455
+ axis_ratio_model=axis_ratio_model,
456
+ permittivity_model=permittivity_model,
457
+ water_temperature=water_temperature,
458
+ elevation_angle=elevation_angle,
459
+ )
460
+
461
+ # Compute require scattering tables
462
+ for params in list_params:
463
+ # Initialize scattering table
464
+ _ = load_scatterer(
465
+ verbose=verbose,
466
+ **params,
467
+ )
468
+
469
+
435
470
  ####----------------------------------------------------------------------
436
471
  #### Scattering functions
437
472
 
@@ -458,12 +493,10 @@ def compute_radar_variables(scatterer):
458
493
  radar_vars["DBZV"] = 10 * np.log10(radar.refl(scatterer, h_pol=False)) # dBZ
459
494
 
460
495
  radar_vars["ZDR"] = 10 * np.log10(radar.Zdr(scatterer)) # dB
461
- if ~np.isfinite(radar_vars["ZDR"]):
462
- radar_vars["ZDR"] = np.nan
496
+ radar_vars["ZDR"] = np.where(np.isfinite(radar_vars["ZDR"]), radar_vars["ZDR"], np.nan)
463
497
 
464
498
  radar_vars["LDR"] = 10 * np.log10(radar.ldr(scatterer)) # dBZ
465
- if ~np.isfinite(radar_vars["LDR"]):
466
- radar_vars["LDR"] = np.nan
499
+ radar_vars["LDR"] = np.where(np.isfinite(radar_vars["LDR"]), radar_vars["LDR"], np.nan)
467
500
 
468
501
  radar_vars["RHOHV"] = radar.rho_hv(scatterer) # deg/km
469
502
  radar_vars["DELTAHV"] = radar.delta_hv(scatterer) * 180.0 / np.pi # [deg]
@@ -482,29 +515,26 @@ def compute_radar_variables(scatterer):
482
515
  RADAR_VARIABLES = ["DBZH", "DBZV", "ZDR", "LDR", "RHOHV", "DELTAHV", "KDP", "AH", "AV", "ADP"]
483
516
 
484
517
 
485
- def _initialize_null_output(output_dictionary):
486
- if output_dictionary:
487
- return dict.fromkeys(RADAR_VARIABLES, np.nan)
488
- return np.zeros(len(RADAR_VARIABLES)) * np.nan
518
+ def _try_compute_radar_variables(scatterer):
519
+ with suppress_warnings():
520
+ try:
521
+ radar_vars = compute_radar_variables(scatterer)
522
+ output = np.array(list(radar_vars.values()))
523
+ except Exception:
524
+ output = np.zeros(len(RADAR_VARIABLES)) * np.nan
525
+ return output
489
526
 
490
527
 
491
528
  def _estimate_empirical_radar_parameters(
492
529
  drop_number_concentration,
493
530
  bin_edges,
494
531
  scatterer,
495
- output_dictionary,
496
532
  ):
497
533
  # Assign PSD model to the scatterer object
498
534
  scatterer.psd = BinnedPSD(bin_edges, drop_number_concentration)
499
535
 
500
536
  # Get radar variables
501
- with suppress_warnings():
502
- try:
503
- radar_vars = compute_radar_variables(scatterer)
504
- output = radar_vars if output_dictionary else np.array(list(radar_vars.values()))
505
- except Exception:
506
- output = _initialize_null_output(output_dictionary)
507
- return output
537
+ return _try_compute_radar_variables(scatterer)
508
538
 
509
539
 
510
540
  def _estimate_model_radar_parameters(
@@ -512,23 +542,16 @@ def _estimate_model_radar_parameters(
512
542
  psd_model,
513
543
  psd_parameters_names,
514
544
  scatterer,
515
- output_dictionary,
516
545
  ):
517
546
  # Assign PSD model to the scatterer object
518
547
  parameters = dict(zip(psd_parameters_names, parameters))
519
548
  scatterer.psd = create_psd(psd_model, parameters)
520
549
 
521
550
  # Get radar variables
522
- with suppress_warnings():
523
- try:
524
- radar_vars = compute_radar_variables(scatterer)
525
- output = radar_vars if output_dictionary else np.array(list(radar_vars.values()))
526
- except Exception:
527
- output = _initialize_null_output(output_dictionary)
528
- return output
551
+ return _try_compute_radar_variables(scatterer)
529
552
 
530
553
 
531
- def get_psd_parameters(ds):
554
+ def select_psd_parameters(ds):
532
555
  """Return a xr.Dataset with only the PSD parameters as variable."""
533
556
  psd_model = ds.attrs["disdrodb_psd_model"]
534
557
  required_parameters = get_required_parameters(psd_model)
@@ -587,14 +610,14 @@ def get_model_radar_parameters(
587
610
  # Retrieve psd model and parameters.
588
611
  psd_model = ds.attrs["disdrodb_psd_model"]
589
612
  required_parameters = get_required_parameters(psd_model)
590
- ds_parameters = get_psd_parameters(ds)
613
+ ds_parameters = select_psd_parameters(ds)
591
614
 
592
615
  # Check argument validity
593
616
  axis_ratio_model = check_axis_ratio_model(axis_ratio_model)
594
617
  permittivity_model = check_permittivity_model(permittivity_model)
595
618
 
596
619
  # Create DataArray with PSD parameters
597
- da_parameters = ds_parameters.to_array(dim="psd_parameters").compute()
620
+ da_parameters = ds_parameters.to_array(dim="psd_parameters")
598
621
 
599
622
  # Initialize scattering table
600
623
  scatterer = load_scatterer(
@@ -610,7 +633,6 @@ def get_model_radar_parameters(
610
633
 
611
634
  # Define kwargs
612
635
  kwargs = {
613
- "output_dictionary": False,
614
636
  "psd_model": psd_model,
615
637
  "psd_parameters_names": required_parameters,
616
638
  "scatterer": scatterer,
@@ -624,9 +646,10 @@ def get_model_radar_parameters(
624
646
  input_core_dims=[["psd_parameters"]],
625
647
  output_core_dims=[["radar_variables"]],
626
648
  vectorize=True,
627
- dask="forbidden",
649
+ dask="parallelized",
628
650
  dask_gufunc_kwargs={
629
651
  "output_sizes": {"radar_variables": len(RADAR_VARIABLES)},
652
+ "allow_rechunk": True,
630
653
  }, # lengths of the new output_core_dims dimensions.
631
654
  output_dtypes=["float64"],
632
655
  )
@@ -695,7 +718,7 @@ def get_empirical_radar_parameters(
695
718
  ds = filter_diameter_bins(ds=ds, maximum_diameter=diameter_max)
696
719
 
697
720
  # Define inputs
698
- da_drop_number_concentration = ds["drop_number_concentration"].compute()
721
+ da_drop_number_concentration = ds["drop_number_concentration"] # .compute()
699
722
 
700
723
  # Set all zeros drop number concentration to np.nan
701
724
  # --> Otherwise inf can appear in the output
@@ -724,11 +747,9 @@ def get_empirical_radar_parameters(
724
747
 
725
748
  # Define kwargs
726
749
  kwargs = {
727
- "output_dictionary": False,
728
750
  "bin_edges": bin_edges,
729
751
  "scatterer": scatterer,
730
752
  }
731
-
732
753
  # Loop over each PSD (not in parallel --> dask="forbidden")
733
754
  # - It costs much more to initiate the scatterer rather than looping over timesteps !
734
755
  da_radar = xr.apply_ufunc(
@@ -738,13 +759,12 @@ def get_empirical_radar_parameters(
738
759
  input_core_dims=[["diameter_bin_center"]],
739
760
  output_core_dims=[["radar_variables"]],
740
761
  vectorize=True,
741
- dask="forbidden",
762
+ dask="parallelized",
742
763
  dask_gufunc_kwargs={
743
764
  "output_sizes": {"radar_variables": len(RADAR_VARIABLES)},
744
765
  }, # lengths of the new output_core_dims dimensions.
745
766
  output_dtypes=["float64"],
746
767
  )
747
-
748
768
  # Finalize radar dataset (add name, coordinates)
749
769
  ds_radar = _finalize_radar_dataset(
750
770
  da_radar=da_radar,
@@ -922,11 +942,11 @@ def get_radar_parameters(
922
942
  # Model-based simulation
923
943
  if "disdrodb_psd_model" in ds.attrs:
924
944
  func = get_model_radar_parameters
925
- ds_subset = get_psd_parameters(ds).compute()
945
+ ds_subset = select_psd_parameters(ds)
926
946
  # Empirical PSD simulation
927
947
  else:
928
948
  func = get_empirical_radar_parameters
929
- ds_subset = ds[["drop_number_concentration"]].compute()
949
+ ds_subset = ds[["drop_number_concentration"]]
930
950
 
931
951
  # Define default frequencies if not specified
932
952
  if frequency is None: