disdrodb 0.1.2__py3-none-any.whl → 0.1.4__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 (142) hide show
  1. disdrodb/__init__.py +68 -34
  2. disdrodb/_config.py +5 -4
  3. disdrodb/_version.py +16 -3
  4. disdrodb/accessor/__init__.py +20 -0
  5. disdrodb/accessor/methods.py +125 -0
  6. disdrodb/api/checks.py +177 -24
  7. disdrodb/api/configs.py +3 -3
  8. disdrodb/api/info.py +13 -13
  9. disdrodb/api/io.py +281 -22
  10. disdrodb/api/path.py +184 -195
  11. disdrodb/api/search.py +18 -9
  12. disdrodb/cli/disdrodb_create_summary.py +103 -0
  13. disdrodb/cli/disdrodb_create_summary_station.py +91 -0
  14. disdrodb/cli/disdrodb_run_l0.py +1 -1
  15. disdrodb/cli/disdrodb_run_l0_station.py +1 -1
  16. disdrodb/cli/disdrodb_run_l0a_station.py +1 -1
  17. disdrodb/cli/disdrodb_run_l0b.py +1 -1
  18. disdrodb/cli/disdrodb_run_l0b_station.py +3 -3
  19. disdrodb/cli/disdrodb_run_l0c.py +1 -1
  20. disdrodb/cli/disdrodb_run_l0c_station.py +3 -3
  21. disdrodb/cli/disdrodb_run_l1_station.py +2 -2
  22. disdrodb/cli/disdrodb_run_l2e_station.py +2 -2
  23. disdrodb/cli/disdrodb_run_l2m_station.py +2 -2
  24. disdrodb/configs.py +149 -4
  25. disdrodb/constants.py +61 -0
  26. disdrodb/data_transfer/download_data.py +127 -11
  27. disdrodb/etc/configs/attributes.yaml +339 -0
  28. disdrodb/etc/configs/encodings.yaml +473 -0
  29. disdrodb/etc/products/L1/global.yaml +13 -0
  30. disdrodb/etc/products/L2E/10MIN.yaml +12 -0
  31. disdrodb/etc/products/L2E/1MIN.yaml +1 -0
  32. disdrodb/etc/products/L2E/global.yaml +22 -0
  33. disdrodb/etc/products/L2M/10MIN.yaml +12 -0
  34. disdrodb/etc/products/L2M/GAMMA_ML.yaml +8 -0
  35. disdrodb/etc/products/L2M/NGAMMA_GS_LOG_ND_MAE.yaml +6 -0
  36. disdrodb/etc/products/L2M/NGAMMA_GS_ND_MAE.yaml +6 -0
  37. disdrodb/etc/products/L2M/NGAMMA_GS_Z_MAE.yaml +6 -0
  38. disdrodb/etc/products/L2M/global.yaml +26 -0
  39. disdrodb/issue/writer.py +2 -0
  40. disdrodb/l0/__init__.py +13 -0
  41. disdrodb/l0/configs/LPM/l0b_cf_attrs.yml +4 -4
  42. disdrodb/l0/configs/PARSIVEL/l0b_cf_attrs.yml +1 -1
  43. disdrodb/l0/configs/PARSIVEL/l0b_encodings.yml +3 -3
  44. disdrodb/l0/configs/PARSIVEL/raw_data_format.yml +1 -1
  45. disdrodb/l0/configs/PARSIVEL2/l0b_cf_attrs.yml +5 -5
  46. disdrodb/l0/configs/PARSIVEL2/l0b_encodings.yml +3 -3
  47. disdrodb/l0/configs/PARSIVEL2/raw_data_format.yml +1 -1
  48. disdrodb/l0/configs/PWS100/l0b_cf_attrs.yml +4 -4
  49. disdrodb/l0/configs/PWS100/raw_data_format.yml +1 -1
  50. disdrodb/l0/l0a_processing.py +37 -32
  51. disdrodb/l0/l0b_nc_processing.py +118 -8
  52. disdrodb/l0/l0b_processing.py +30 -65
  53. disdrodb/l0/l0c_processing.py +369 -259
  54. disdrodb/l0/readers/LPM/ARM/ARM_LPM.py +7 -0
  55. disdrodb/l0/readers/LPM/NETHERLANDS/DELFT_LPM_NC.py +66 -0
  56. disdrodb/l0/readers/LPM/SLOVENIA/{CRNI_VRH.py → UL.py} +3 -0
  57. disdrodb/l0/readers/LPM/SWITZERLAND/INNERERIZ_LPM.py +195 -0
  58. disdrodb/l0/readers/PARSIVEL/GPM/PIERS.py +0 -2
  59. disdrodb/l0/readers/PARSIVEL/JAPAN/JMA.py +4 -1
  60. disdrodb/l0/readers/PARSIVEL/NCAR/PECAN_MOBILE.py +1 -1
  61. disdrodb/l0/readers/PARSIVEL/NCAR/VORTEX2_2009.py +1 -1
  62. disdrodb/l0/readers/PARSIVEL2/ARM/ARM_PARSIVEL2.py +4 -0
  63. disdrodb/l0/readers/PARSIVEL2/BELGIUM/ILVO.py +168 -0
  64. disdrodb/l0/readers/PARSIVEL2/CANADA/UQAM_NC.py +69 -0
  65. disdrodb/l0/readers/PARSIVEL2/DENMARK/DTU.py +165 -0
  66. disdrodb/l0/readers/PARSIVEL2/FINLAND/FMI_PARSIVEL2.py +69 -0
  67. disdrodb/l0/readers/PARSIVEL2/FRANCE/ENPC_PARSIVEL2.py +255 -134
  68. disdrodb/l0/readers/PARSIVEL2/FRANCE/OSUG.py +525 -0
  69. disdrodb/l0/readers/PARSIVEL2/FRANCE/SIRTA_PARSIVEL2.py +1 -1
  70. disdrodb/l0/readers/PARSIVEL2/GPM/GCPEX.py +9 -7
  71. disdrodb/l0/readers/PARSIVEL2/KIT/BURKINA_FASO.py +1 -1
  72. disdrodb/l0/readers/PARSIVEL2/KIT/TEAMX.py +123 -0
  73. disdrodb/l0/readers/PARSIVEL2/{NETHERLANDS/DELFT.py → MPI/BCO_PARSIVEL2.py} +41 -71
  74. disdrodb/l0/readers/PARSIVEL2/MPI/BOWTIE.py +220 -0
  75. disdrodb/l0/readers/PARSIVEL2/NASA/APU.py +120 -0
  76. disdrodb/l0/readers/PARSIVEL2/NASA/LPVEX.py +109 -0
  77. disdrodb/l0/readers/PARSIVEL2/NCAR/FARM_PARSIVEL2.py +1 -0
  78. disdrodb/l0/readers/PARSIVEL2/NCAR/PECAN_FP3.py +1 -1
  79. disdrodb/l0/readers/PARSIVEL2/NCAR/PERILS_MIPS.py +126 -0
  80. disdrodb/l0/readers/PARSIVEL2/NCAR/PERILS_PIPS.py +165 -0
  81. disdrodb/l0/readers/PARSIVEL2/NCAR/VORTEX_SE_2016_P2.py +1 -1
  82. disdrodb/l0/readers/PARSIVEL2/NCAR/VORTEX_SE_2016_PIPS.py +20 -12
  83. disdrodb/l0/readers/PARSIVEL2/NETHERLANDS/DELFT_NC.py +5 -0
  84. disdrodb/l0/readers/PARSIVEL2/SPAIN/CENER.py +144 -0
  85. disdrodb/l0/readers/PARSIVEL2/SPAIN/CR1000DL.py +201 -0
  86. disdrodb/l0/readers/PARSIVEL2/SPAIN/LIAISE.py +137 -0
  87. disdrodb/l0/readers/PARSIVEL2/USA/C3WE.py +146 -0
  88. disdrodb/l0/readers/PWS100/FRANCE/ENPC_PWS100.py +105 -99
  89. disdrodb/l0/readers/PWS100/FRANCE/ENPC_PWS100_SIRTA.py +151 -0
  90. disdrodb/l1/__init__.py +5 -0
  91. disdrodb/l1/fall_velocity.py +46 -0
  92. disdrodb/l1/filters.py +34 -20
  93. disdrodb/l1/processing.py +46 -45
  94. disdrodb/l1/resampling.py +77 -66
  95. disdrodb/l1_env/routines.py +18 -3
  96. disdrodb/l2/__init__.py +7 -0
  97. disdrodb/l2/empirical_dsd.py +58 -10
  98. disdrodb/l2/processing.py +268 -117
  99. disdrodb/metadata/checks.py +132 -125
  100. disdrodb/metadata/standards.py +3 -1
  101. disdrodb/psd/fitting.py +631 -345
  102. disdrodb/psd/models.py +9 -6
  103. disdrodb/routines/__init__.py +54 -0
  104. disdrodb/{l0/routines.py → routines/l0.py} +316 -355
  105. disdrodb/{l1/routines.py → routines/l1.py} +76 -116
  106. disdrodb/routines/l2.py +1019 -0
  107. disdrodb/{routines.py → routines/wrappers.py} +98 -10
  108. disdrodb/scattering/__init__.py +16 -4
  109. disdrodb/scattering/axis_ratio.py +61 -37
  110. disdrodb/scattering/permittivity.py +504 -0
  111. disdrodb/scattering/routines.py +746 -184
  112. disdrodb/summary/__init__.py +17 -0
  113. disdrodb/summary/routines.py +4196 -0
  114. disdrodb/utils/archiving.py +434 -0
  115. disdrodb/utils/attrs.py +68 -125
  116. disdrodb/utils/cli.py +5 -5
  117. disdrodb/utils/compression.py +30 -1
  118. disdrodb/utils/dask.py +121 -9
  119. disdrodb/utils/dataframe.py +61 -7
  120. disdrodb/utils/decorators.py +31 -0
  121. disdrodb/utils/directories.py +35 -15
  122. disdrodb/utils/encoding.py +37 -19
  123. disdrodb/{l2 → utils}/event.py +15 -173
  124. disdrodb/utils/logger.py +14 -7
  125. disdrodb/utils/manipulations.py +81 -0
  126. disdrodb/utils/routines.py +166 -0
  127. disdrodb/utils/subsetting.py +214 -0
  128. disdrodb/utils/time.py +35 -177
  129. disdrodb/utils/writer.py +20 -7
  130. disdrodb/utils/xarray.py +5 -4
  131. disdrodb/viz/__init__.py +13 -0
  132. disdrodb/viz/plots.py +398 -0
  133. {disdrodb-0.1.2.dist-info → disdrodb-0.1.4.dist-info}/METADATA +4 -3
  134. {disdrodb-0.1.2.dist-info → disdrodb-0.1.4.dist-info}/RECORD +139 -98
  135. {disdrodb-0.1.2.dist-info → disdrodb-0.1.4.dist-info}/entry_points.txt +2 -0
  136. disdrodb/l1/encoding_attrs.py +0 -642
  137. disdrodb/l2/processing_options.py +0 -213
  138. disdrodb/l2/routines.py +0 -868
  139. /disdrodb/l0/readers/PARSIVEL/SLOVENIA/{UL_FGG.py → UL.py} +0 -0
  140. {disdrodb-0.1.2.dist-info → disdrodb-0.1.4.dist-info}/WHEEL +0 -0
  141. {disdrodb-0.1.2.dist-info → disdrodb-0.1.4.dist-info}/licenses/LICENSE +0 -0
  142. {disdrodb-0.1.2.dist-info → disdrodb-0.1.4.dist-info}/top_level.txt +0 -0
@@ -17,32 +17,55 @@
17
17
  """Implement PSD scattering routines."""
18
18
 
19
19
  import itertools
20
+ import logging
21
+ import os
20
22
 
21
23
  import dask
22
24
  import numpy as np
23
25
  import xarray as xr
24
- from pytmatrix import orientation, radar, refractive, tmatrix_aux
25
- from pytmatrix.psd import BinnedPSD, PSDIntegrator
26
- from pytmatrix.tmatrix import Scatterer
27
26
 
28
- from disdrodb.psd.models import create_psd, get_required_parameters
29
- from disdrodb.scattering.axis_ratio import check_axis_ratio, get_axis_ratio_method
27
+ from disdrodb.configs import get_scattering_table_dir
28
+ from disdrodb.constants import DIAMETER_DIMENSION
29
+ from disdrodb.l1.filters import filter_diameter_bins
30
+ from disdrodb.psd.models import BinnedPSD, create_psd, get_required_parameters
31
+ from disdrodb.scattering.axis_ratio import check_axis_ratio_model, get_axis_ratio_model
32
+ from disdrodb.scattering.permittivity import (
33
+ check_permittivity_model,
34
+ get_rayleigh_dielectric_factor,
35
+ get_refractive_index,
36
+ )
37
+ from disdrodb.utils.logger import log_info
38
+ from disdrodb.utils.manipulations import get_diameter_bin_edges
30
39
  from disdrodb.utils.warnings import suppress_warnings
31
40
 
32
- # Wavelengths for which the refractive index is defined in pytmatrix (in mm)
33
- wavelength_dict = {
34
- "S": tmatrix_aux.wl_S,
35
- "C": tmatrix_aux.wl_C,
36
- "X": tmatrix_aux.wl_X,
37
- "Ku": tmatrix_aux.wl_Ku,
38
- "Ka": tmatrix_aux.wl_Ka,
39
- "W": tmatrix_aux.wl_W,
41
+ logger = logging.getLogger(__name__)
42
+
43
+ RADAR_OPTIONS = [
44
+ "frequency",
45
+ "diameter_max",
46
+ "num_points",
47
+ "canting_angle_std",
48
+ "axis_ratio_model",
49
+ "permittivity_model",
50
+ "water_temperature",
51
+ "elevation_angle",
52
+ ]
53
+
54
+ # Common radar frequencies (in GHz)
55
+ frequency_dict = {
56
+ "S": 2.70, # e.g. NEXRAD radars
57
+ "C": 5.4, # e.g. MeteoSwiss Rad4Alp radars
58
+ "X": 9.4, # e.g. LTE MXPOL radar
59
+ "Ku": 13.6, # e.g. DPR-Ku
60
+ "K": 24.2, # e.g. MRR-PRO
61
+ "Ka": 35.5, # e.g. DPR-Ka
62
+ "W": 94.05, # e.g. CloudSat, EarthCare
40
63
  }
41
64
 
42
65
 
43
66
  def available_radar_bands():
44
67
  """Return a list of the available radar bands."""
45
- return list(wavelength_dict)
68
+ return list(frequency_dict)
46
69
 
47
70
 
48
71
  def check_radar_band(radar_band):
@@ -53,95 +76,465 @@ def check_radar_band(radar_band):
53
76
  return radar_band
54
77
 
55
78
 
56
- def get_radar_wavelength(radar_band):
57
- """Get the wavelength of a radar band."""
58
- wavelength = wavelength_dict[radar_band]
79
+ def _check_frequency(frequency):
80
+ """Check the validity of the specified frequency."""
81
+ if isinstance(frequency, str):
82
+ frequency = check_radar_band(frequency)
83
+ frequency = frequency_dict[frequency]
84
+ return frequency
85
+ if not isinstance(frequency, (int, float)):
86
+ raise TypeError(f"Frequency {frequency} must be a string or a number.")
87
+ return frequency
88
+
89
+
90
+ def ensure_numerical_frequency(frequency):
91
+ """Ensure that the frequencies are numerical values in GHz."""
92
+ if isinstance(frequency, (str, int, float)):
93
+ frequency = [frequency]
94
+ frequency = np.array([_check_frequency(f) for f in frequency])
95
+ return frequency.squeeze()
96
+
97
+
98
+ # Wavelength, Frequency Conversion
99
+ def wavelength_to_frequency(wavelength):
100
+ """Convert wavelength in millimeters to frequency in GHz."""
101
+ c = 299_792_458 # speed of light in m/s
102
+ frequency = c / np.array(wavelength) / 1e6
103
+ return frequency
104
+
105
+
106
+ def frequency_to_wavelength(frequency):
107
+ """Convert frequency in GHz to wavelength millimeters."""
108
+ c = 299_792_458 # speed of light in m/s
109
+ wavelength = c / np.array(frequency) / 1e6
59
110
  return wavelength
60
111
 
61
112
 
62
- def initialize_scatterer(wavelength, canting_angle_std=7, D_max=8, axis_ratio="Thurai2007"):
63
- """Initialize T-matrix scatterer object for a given wavelength."""
113
+ def get_backward_geometry(elevation_angle):
114
+ """Define backward geometry given a radar elevation angle."""
115
+ # - Format (thet0, thet0, phi0, phi0, alpha, beta
116
+ # - thet0, thet0, thet: The zenith angles of incident and scattered radiation (default to 90)
117
+ # - phi0, phi: The azimuth angles of incident and scattered radiation (default to 0 and 180)
118
+ # - alpha, beta: Defaults to 0.0, 0.0. Valid values: alpha = [0, 360] beta = [0, 180]
119
+
120
+ # Retrieve zenith angle of incident beam (from vertical)
121
+ theta = 90.0 - elevation_angle
122
+
123
+ # Return (thet0, thet0, phi0, phi0, alpha, beta) tuple
124
+ return (theta, 180 - theta, 0.0, 180, 0.0, 0.0)
125
+
126
+
127
+ def get_forward_geometry(elevation_angle):
128
+ """Define forward geometry given a radar elevation angle."""
129
+ # - Format (thet0, thet0, phi0, phi0, alpha, beta
130
+ # - thet0, thet0, thet: The zenith angles of incident and scattered radiation (default to 90)
131
+ # - phi0, phi: The azimuth angles of incident and scattered radiation (default to 0 and 180)
132
+ # - alpha, beta: Defaults to 0.0, 0.0. Valid values: alpha = [0, 360] beta = [0, 180]
133
+
134
+ # Retrieve zenith angle of incident beam (from vertical)
135
+ theta = 90.0 - elevation_angle
136
+
137
+ # Return (thet0, thet0, phi0, phi0, alpha, beta) tuple
138
+ return (theta, theta, 0.0, 0.0, 0.0, 0.0)
139
+
140
+
141
+ # from pytmatrix import tmatrix_aux
142
+ # get_backward_geometry(0)
143
+ # tmatrix_aux.geom_horiz_back
144
+ # get_backward_geometry(90)
145
+ # tmatrix_aux.geom_vert_back # phi0 varies (180 instead of pytmatrix 0)
146
+
147
+ # get_forward_geometry(0)
148
+ # tmatrix_aux.geom_horiz_forw
149
+ # get_forward_geometry(90) # theta and thet0 are 0 instead of 180
150
+ # tmatrix_aux.geom_vert_forw
151
+
152
+
153
+ def initialize_scatterer(
154
+ wavelength,
155
+ refractive_index,
156
+ num_points=1024,
157
+ diameter_max=8,
158
+ canting_angle_std=7,
159
+ axis_ratio_model="Thurai2007",
160
+ elevation_angle=0,
161
+ ):
162
+ """Initialize T-matrix scatterer object for a given frequency.
163
+
164
+ Load a scatterer object with cached scattering table.
165
+
166
+ If the scattering table does not exist at the specified location, it will be
167
+ created and saved to disk. If the file is found, it will be loaded and used
168
+ to configure the scatterer.
169
+
170
+ Parameters
171
+ ----------
172
+ wavelength : float
173
+ Radar wavelength in mm.
174
+ refractive_index: complex
175
+ Water refractive index.
176
+ num_points: int
177
+ Number of bins into which discretize the PSD.
178
+ diameter_max : float
179
+ Maximum drop diameter in millimeters for the scattering table.
180
+ canting_angle_std : float, optional
181
+ Standard deviation of the canting angle distribution in degrees,
182
+ by default 7.
183
+ axis_ratio_model: str
184
+ Axis ratio model used to shape hydrometeors. The default is ``"Thurai2007"``.
185
+ See available models with ``disdrodb.scattering.available_axis_ratio_models()``.
186
+ elevation_angle: str
187
+ Radar elevation angle in degrees.
188
+ Specify 90 degrees for vertically pointing radars.
189
+ The default is 0 degrees.
190
+ scattering_table_dir : str or Path, optional
191
+ Directory path where T-Matrix scattering tables are stored. If None, the default
192
+ location will be used.
193
+ verbose: bool
194
+ Whether to verbose the computation of the scattering table. The default is False.
195
+
196
+ Returns
197
+ -------
198
+ scatterer : Scatterer
199
+ A scatterer object with the PSD integrator configured and scattering
200
+ table loaded or generated.
201
+ """
202
+ from pytmatrix import orientation
203
+ from pytmatrix.psd import PSDIntegrator
204
+ from pytmatrix.tmatrix import Scatterer
205
+
64
206
  # Retrieve custom axis ratio function
65
- axis_ratio_func = get_axis_ratio_method(axis_ratio)
207
+ axis_ratio_func = get_axis_ratio_model(axis_ratio_model)
208
+
209
+ # Define radar dielectric factor
210
+ Kw_sqr = get_rayleigh_dielectric_factor(refractive_index)
66
211
 
67
- # Retrieve water complex refractive index
68
- # - Here we currently assume 10 °C
69
- # - m_w_0C and m_w_20C are also available
70
- # TODO: should be another dimension ? Or use scatterer.psd_integrator.m_func?
71
- water_refractive_index = refractive.m_w_10C[wavelength]
212
+ # Define backward and forward geometries
213
+ # - Format (thet0, thet0, phi0, phi0, alpha, beta
214
+ backward_geom = get_backward_geometry(elevation_angle)
215
+ forward_geom = get_forward_geometry(elevation_angle)
216
+
217
+ # ---------------------------------------------------------------.
218
+ # For W band limits diameter_max up to 9.5, otherwise the kernel dies !
219
+ if wavelength < 3.5:
220
+ diameter_max = min(diameter_max, 9.5)
72
221
 
73
222
  # ---------------------------------------------------------------.
74
223
  # Initialize Scatterer class
75
- scatterer = Scatterer(wavelength=wavelength, m=water_refractive_index)
224
+ # - By specifying m, we assume same refractive index for all particles diameters
225
+ scatterer = Scatterer(wavelength=wavelength, m=refractive_index, Kw_sqr=Kw_sqr)
226
+
227
+ # - Define geometry
228
+ scatterer.set_geometry(backward_geom)
229
+
230
+ # - Define orientation methods
231
+ # --> Alternatives: orient_averaged_adaptive, orient_single,
232
+ # --> Speed: orient_single > orient_averaged_fixed > orient_averaged_adaptive
233
+ scatterer.orient = orientation.orient_averaged_fixed
234
+
76
235
  # - Define particle orientation PDF for orientational averaging
77
236
  # --> The standard deviation of the angle with respect to vertical orientation (the canting angle).
78
237
  scatterer.or_pdf = orientation.gaussian_pdf(std=canting_angle_std)
79
- # - Define orientation methods
80
- # --> Alternatives: orient_averaged_fixed, orient_single
81
- scatterer.orient = orientation.orient_averaged_fixed
82
238
 
83
239
  # ---------------------------------------------------------------.
84
240
  # Initialize PSDIntegrator
85
241
  scatterer.psd_integrator = PSDIntegrator()
86
242
  # - Define axis_ratio_func
87
243
  # --> The Scatterer class expects horizontal to vertical
244
+ # --> Axis ratio model are defined to return vertical to horizontal aspect ratio !
88
245
  scatterer.psd_integrator.axis_ratio_func = lambda D: 1.0 / axis_ratio_func(D)
89
246
  # - Define function to compute refrative index (as function of D)
90
247
  # scatterer.psd_integrator.m_func = None # Use constant value of scatterer.m
91
248
  # - Define number of points over which to integrate
92
- scatterer.psd_integrator.num_points = 1024
249
+ scatterer.psd_integrator.num_points = num_points
93
250
  # - Define maximum drop diameter
94
- scatterer.psd_integrator.D_max = D_max
251
+ scatterer.psd_integrator.D_max = diameter_max
95
252
  # - Define geometries
96
- scatterer.psd_integrator.geometries = (tmatrix_aux.geom_horiz_back, tmatrix_aux.geom_horiz_forw)
253
+ # --> convention: first is backward, second is forward
254
+ scatterer.psd_integrator.geometries = (backward_geom, forward_geom)
255
+ return scatterer
256
+
257
+
258
+ def calculate_scatterer(
259
+ wavelength,
260
+ refractive_index,
261
+ num_points=1024,
262
+ diameter_max=8,
263
+ canting_angle_std=7,
264
+ axis_ratio_model="Thurai2007",
265
+ elevation_angle=0,
266
+ ):
267
+ """Initialize T-matrix scatterer object for a given frequency.
268
+
269
+ Load a scatterer object with cached scattering table.
270
+
271
+ If the scattering table does not exist at the specified location, it will be
272
+ created and saved to disk. If the file is found, it will be loaded and used
273
+ to configure the scatterer.
274
+
275
+ Parameters
276
+ ----------
277
+ wavelength : float
278
+ Radar wavelength in millimeters.
279
+ num_points: int
280
+ Number of bins into which discretize the PSD.
281
+ diameter_max : float
282
+ Maximum drop diameter in millimeters for the scattering table.
283
+ canting_angle_std : float, optional
284
+ Standard deviation of the canting angle distribution in degrees,
285
+ by default 7.
286
+ axis_ratio_model : str, optional
287
+ Axis ratio model used to shape hydrometeors. The default is ``"Thurai2007"``.
288
+ See available models with ``disdrodb.scattering.available_axis_ratio_models()``.
289
+ elevation_angle: str
290
+ Radar elevation angle in degrees.
291
+ Specify 90 degrees for vertically pointing radars.
292
+ The default is 0 degrees.
293
+
294
+ Returns
295
+ -------
296
+ scatterer : Scatterer
297
+ A scatterer object with the PSD integrator configured and scattering
298
+ table loaded or generated.
299
+ """
97
300
  # ---------------------------------------------------------------.
98
- # Initialize scattering table
301
+ # Initialize Scatterer class
302
+ scatterer = initialize_scatterer(
303
+ wavelength=wavelength,
304
+ refractive_index=refractive_index,
305
+ num_points=num_points,
306
+ diameter_max=diameter_max,
307
+ canting_angle_std=canting_angle_std,
308
+ axis_ratio_model=axis_ratio_model,
309
+ elevation_angle=elevation_angle,
310
+ )
311
+
312
+ # ---------------------------------------------------------------.
313
+ # Calculate scattering table
99
314
  scatterer.psd_integrator.init_scatter_table(scatterer)
100
315
  return scatterer
101
316
 
102
317
 
318
+ def load_scatterer(
319
+ frequency,
320
+ num_points=1024,
321
+ diameter_max=8,
322
+ canting_angle_std=7,
323
+ axis_ratio_model="Thurai2007",
324
+ permittivity_model="Turner2016",
325
+ water_temperature=10,
326
+ elevation_angle=0,
327
+ scattering_table_dir=None,
328
+ verbose=False,
329
+ ):
330
+ """
331
+ Load a scatterer object with cached scattering table.
332
+
333
+ If the scattering table does not exist at the specified location, it will be
334
+ created and saved to disk. If the file is found, it will be loaded and used
335
+ to configure the scatterer.
336
+
337
+ Parameters
338
+ ----------
339
+ frequency : float
340
+ Radar frequency in GHz.
341
+ num_points: int
342
+ Number of bins into which discretize the PSD.
343
+ diameter_max : float
344
+ Maximum drop diameter in millimeters for the scattering table.
345
+ canting_angle_std : float, optional
346
+ Standard deviation of the canting angle distribution in degrees,
347
+ by default 7.
348
+ axis_ratio_model : str, optional
349
+ Axis ratio model used to shape hydrometeors. The default is ``"Thurai2007"``.
350
+ See available models with ``disdrodb.scattering.available_axis_ratio_models()``.
351
+ permittivity_model : str
352
+ Permittivity model to use to compute the refractive index and the
353
+ rayleigh_dielectric_factor. The default is ``Turner2016``.
354
+ See available models with ``disdrodb.scattering.available_permittivity_models()``.
355
+ water_temperature : float
356
+ Water temperature in degree Celsius to be used in the permittivity model.
357
+ The default is 10 degC.
358
+ elevation_angle: str
359
+ Radar elevation angle in degrees.
360
+ Specify 90 degrees for vertically pointing radars.
361
+ The default is 0 degrees.
362
+ scattering_table_dir : str or Path, optional
363
+ Directory path where T-Matrix scattering tables are stored. If None, the default
364
+ location will be used.
365
+ verbose: bool
366
+ Whether to verbose the computation of the scattering table. The default is False.
367
+
368
+ Returns
369
+ -------
370
+ scatterer : Scatterer
371
+ A scatterer object with the PSD integrator configured and scattering
372
+ table loaded or generated.
373
+ """
374
+ # Define wavelength (in mm)
375
+ wavelength = frequency_to_wavelength(frequency)
376
+
377
+ # Define complex refractive index
378
+ refractive_index = get_refractive_index(
379
+ frequency=frequency,
380
+ temperature=water_temperature,
381
+ permittivity_model=permittivity_model,
382
+ )
383
+
384
+ # Retrieve scattering table directory
385
+ scattering_table_dir = get_scattering_table_dir(scattering_table_dir)
386
+
387
+ # Define a filename based on the key parameters
388
+ filename = "_".join(
389
+ [
390
+ "ScatteringTable",
391
+ f"wl-{wavelength:.2f}",
392
+ f"el-{elevation_angle:.1f}",
393
+ f"dmax-{diameter_max:.1f}",
394
+ f"npts-{num_points}",
395
+ f"m-{refractive_index:.3f}",
396
+ f"cant-{canting_angle_std:.1f}",
397
+ f"ar-{axis_ratio_model}.pkl",
398
+ ],
399
+ )
400
+ scatter_table_filepath = os.path.join(scattering_table_dir, filename)
401
+
402
+ # Load or create scattering table
403
+ if os.path.exists(scatter_table_filepath):
404
+ scatterer = initialize_scatterer(
405
+ wavelength=wavelength,
406
+ refractive_index=refractive_index,
407
+ num_points=num_points,
408
+ diameter_max=diameter_max,
409
+ canting_angle_std=canting_angle_std,
410
+ axis_ratio_model=axis_ratio_model,
411
+ elevation_angle=elevation_angle,
412
+ )
413
+ _ = scatterer.psd_integrator.load_scatter_table(scatter_table_filepath)
414
+
415
+ else:
416
+ if verbose:
417
+ msg = f"- Computing pyTmatrix {filename}"
418
+ log_info(logger=logger, msg=msg, verbose=verbose)
419
+
420
+ # Calculate scatterer
421
+ scatterer = calculate_scatterer(
422
+ wavelength=wavelength,
423
+ refractive_index=refractive_index,
424
+ num_points=num_points,
425
+ diameter_max=diameter_max,
426
+ canting_angle_std=canting_angle_std,
427
+ axis_ratio_model=axis_ratio_model,
428
+ elevation_angle=elevation_angle,
429
+ )
430
+
431
+ scatterer.psd_integrator.save_scatter_table(scatter_table_filepath)
432
+ return scatterer
433
+
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
+
470
+ ####----------------------------------------------------------------------
471
+ #### Scattering functions
472
+
473
+
103
474
  def compute_radar_variables(scatterer):
104
475
  """Compute radar variables for a given scatter object with a specified PSD.
105
476
 
106
477
  To speed up computations, this function should input a scatterer object with
107
478
  a preinitialized scattering table.
108
479
  """
109
- # Compute radar parameters
110
- radar_vars = {}
111
- scatterer.set_geometry(tmatrix_aux.geom_horiz_back)
112
- radar_vars["Zh"] = 10 * np.log10(radar.refl(scatterer, h_pol=True)) # dBZ
113
- radar_vars["Zdr"] = 10 * np.log10(radar.Zdr(scatterer)) # dB
114
- radar_vars["rho_hv"] = radar.rho_hv(scatterer)
115
- radar_vars["ldr"] = radar.ldr(scatterer)
116
- scatterer.set_geometry(tmatrix_aux.geom_horiz_forw)
117
- radar_vars["Kdp"] = radar.Kdp(scatterer)
118
- radar_vars["Ai"] = radar.Ai(scatterer)
480
+ from pytmatrix import radar
481
+
482
+ with suppress_warnings():
483
+ radar_vars = {}
484
+ # Retrieve backward and forward_geometries
485
+ # - Convention (first is backward, second is forward)
486
+ backward_geom = scatterer.psd_integrator.geometries[0]
487
+ forward_geom = scatterer.psd_integrator.geometries[1]
488
+
489
+ # Set backward scattering for reflectivity calculations
490
+ scatterer.set_geometry(backward_geom)
491
+
492
+ radar_vars["DBZH"] = 10 * np.log10(radar.refl(scatterer, h_pol=True)) # dBZ
493
+ radar_vars["DBZV"] = 10 * np.log10(radar.refl(scatterer, h_pol=False)) # dBZ
494
+
495
+ radar_vars["ZDR"] = 10 * np.log10(radar.Zdr(scatterer)) # dB
496
+ radar_vars["ZDR"] = np.where(np.isfinite(radar_vars["ZDR"]), radar_vars["ZDR"], np.nan)
497
+
498
+ radar_vars["LDR"] = 10 * np.log10(radar.ldr(scatterer)) # dBZ
499
+ radar_vars["LDR"] = np.where(np.isfinite(radar_vars["LDR"]), radar_vars["LDR"], np.nan)
500
+
501
+ radar_vars["RHOHV"] = radar.rho_hv(scatterer) # deg/km
502
+ radar_vars["DELTAHV"] = radar.delta_hv(scatterer) * 180.0 / np.pi # [deg]
503
+
504
+ # Set forward scattering for attenuation and phase calculations
505
+ scatterer.set_geometry(forward_geom)
506
+ radar_vars["KDP"] = radar.Kdp(scatterer) # deg/km
507
+ radar_vars["AH"] = radar.Ai(scatterer, h_pol=True) # dB/km
508
+ radar_vars["AV"] = radar.Ai(scatterer, h_pol=False) # dB/km
509
+ radar_vars["ADP"] = radar_vars["AH"] - radar_vars["AV"] # dB/km
119
510
  return radar_vars
120
511
 
121
512
 
513
+ # Radar variables computed by DISDRODB
514
+ # - Must reflect dictionary order output of compute_radar_variables
515
+ RADAR_VARIABLES = ["DBZH", "DBZV", "ZDR", "LDR", "RHOHV", "DELTAHV", "KDP", "AH", "AV", "ADP"]
516
+
517
+
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
526
+
527
+
122
528
  def _estimate_empirical_radar_parameters(
123
529
  drop_number_concentration,
124
530
  bin_edges,
125
531
  scatterer,
126
- output_dictionary,
127
532
  ):
128
- # Initialize bad results
129
- if output_dictionary:
130
- null_output = {"Zh": np.nan, "Zdr": np.nan, "rho_hv": np.nan, "ldr": np.nan, "Kdp": np.nan, "Ai": np.nan}
131
- else:
132
- null_output = np.array([np.nan, np.nan, np.nan, np.nan, np.nan, np.nan])
133
-
134
533
  # Assign PSD model to the scatterer object
135
534
  scatterer.psd = BinnedPSD(bin_edges, drop_number_concentration)
136
535
 
137
536
  # Get radar variables
138
- with suppress_warnings():
139
- try:
140
- radar_vars = compute_radar_variables(scatterer)
141
- output = radar_vars if output_dictionary else np.array(list(radar_vars.values()))
142
- except Exception:
143
- output = null_output
144
- return output
537
+ return _try_compute_radar_variables(scatterer)
145
538
 
146
539
 
147
540
  def _estimate_model_radar_parameters(
@@ -149,30 +542,16 @@ def _estimate_model_radar_parameters(
149
542
  psd_model,
150
543
  psd_parameters_names,
151
544
  scatterer,
152
- output_dictionary,
153
545
  ):
154
- # Initialize bad results
155
- if output_dictionary:
156
- null_output = {"Zh": np.nan, "Zdr": np.nan, "rho_hv": np.nan, "ldr": np.nan, "Kdp": np.nan, "Ai": np.nan}
157
- else:
158
- null_output = np.array([np.nan, np.nan, np.nan, np.nan, np.nan, np.nan])
159
-
160
546
  # Assign PSD model to the scatterer object
161
547
  parameters = dict(zip(psd_parameters_names, parameters))
162
548
  scatterer.psd = create_psd(psd_model, parameters)
163
549
 
164
550
  # Get radar variables
165
- with suppress_warnings():
166
- radar_vars = compute_radar_variables(scatterer)
167
- try:
168
- radar_vars = compute_radar_variables(scatterer)
169
- output = radar_vars if output_dictionary else np.array(list(radar_vars.values()))
170
- except Exception:
171
- output = null_output
172
- return output
551
+ return _try_compute_radar_variables(scatterer)
173
552
 
174
553
 
175
- def get_psd_parameters(ds):
554
+ def select_psd_parameters(ds):
176
555
  """Return a xr.Dataset with only the PSD parameters as variable."""
177
556
  psd_model = ds.attrs["disdrodb_psd_model"]
178
557
  required_parameters = get_required_parameters(psd_model)
@@ -184,26 +563,44 @@ def get_psd_parameters(ds):
184
563
 
185
564
  def get_model_radar_parameters(
186
565
  ds,
187
- radar_band,
188
- canting_angle_std=7,
566
+ frequency,
567
+ num_points=1024,
189
568
  diameter_max=10,
190
- axis_ratio="Thurai2007",
569
+ canting_angle_std=7,
570
+ axis_ratio_model="Thurai2007",
571
+ permittivity_model="Turner2016",
572
+ water_temperature=10,
573
+ elevation_angle=0,
191
574
  ):
192
575
  """Compute radar parameters from a PSD model.
193
576
 
577
+ This function retrieve values for a single set of parameter only !
578
+
194
579
  Parameters
195
580
  ----------
196
581
  ds : xarray.Dataset
197
582
  Dataset containing the parameters of the PSD model.
198
583
  The dataset attribute disdrodb_psd_model specifies the PSD model to use.
199
- radar_band : str
200
- Radar band to be used.
584
+ frequency : float
585
+ Frequency in GHz for which to compute the radar parameters.
201
586
  canting_angle_std : float, optional
202
- Standard deviation of the canting angle. The default value is 7.
587
+ Standard deviation of the canting angle. The default value is 10.
203
588
  diameter_max : float, optional
204
589
  Maximum diameter. The default value is 8 mm.
205
- axis_ratio : str, optional
206
- Method to compute the axis ratio. The default method is ``Thurai2007``.
590
+ axis_ratio_model : str, optional
591
+ Axis ratio model used to shape hydrometeors. The default is ``"Thurai2007"``.
592
+ See available models with ``disdrodb.scattering.available_axis_ratio_models()``.
593
+ permittivity_model : str
594
+ Permittivity model to use to compute the refractive index and the
595
+ rayleigh_dielectric_factor. The default is ``Turner2016``.
596
+ See available models with ``disdrodb.scattering.available_permittivity_models()``.
597
+ water_temperature : float
598
+ Water temperature in degree Celsius to be used in the permittivity model.
599
+ The default is 10 degC.
600
+ elevation_angle: str
601
+ Radar elevation angle in degrees.
602
+ Specify 90 degrees for vertically pointing radars.
603
+ The default is 0 degrees.
207
604
 
208
605
  Returns
209
606
  -------
@@ -213,36 +610,35 @@ def get_model_radar_parameters(
213
610
  # Retrieve psd model and parameters.
214
611
  psd_model = ds.attrs["disdrodb_psd_model"]
215
612
  required_parameters = get_required_parameters(psd_model)
216
- ds_parameters = get_psd_parameters(ds)
613
+ ds_parameters = select_psd_parameters(ds)
217
614
 
218
615
  # Check argument validity
219
- axis_ratio = check_axis_ratio(axis_ratio)
220
- radar_band = check_radar_band(radar_band)
221
-
222
- # Retrieve wavelengths in mm
223
- wavelength = get_radar_wavelength(radar_band)
616
+ axis_ratio_model = check_axis_ratio_model(axis_ratio_model)
617
+ permittivity_model = check_permittivity_model(permittivity_model)
224
618
 
225
619
  # Create DataArray with PSD parameters
226
- da_parameters = ds_parameters.to_array(dim="psd_parameters").compute()
620
+ da_parameters = ds_parameters.to_array(dim="psd_parameters")
227
621
 
228
622
  # Initialize scattering table
229
- scatterer = initialize_scatterer(
230
- wavelength=wavelength,
623
+ scatterer = load_scatterer(
624
+ frequency=frequency,
625
+ num_points=num_points,
626
+ diameter_max=diameter_max,
231
627
  canting_angle_std=canting_angle_std,
232
- D_max=diameter_max,
233
- axis_ratio=axis_ratio,
628
+ axis_ratio_model=axis_ratio_model,
629
+ permittivity_model=permittivity_model,
630
+ water_temperature=water_temperature,
631
+ elevation_angle=elevation_angle,
234
632
  )
235
633
 
236
634
  # Define kwargs
237
635
  kwargs = {
238
- "output_dictionary": False,
239
636
  "psd_model": psd_model,
240
637
  "psd_parameters_names": required_parameters,
241
638
  "scatterer": scatterer,
242
639
  }
243
640
 
244
641
  # Loop over each PSD (not in parallel --> dask="forbidden")
245
- # - It costs much more to initiate the scatterer rather than looping over timesteps !
246
642
  da_radar = xr.apply_ufunc(
247
643
  _estimate_model_radar_parameters,
248
644
  da_parameters,
@@ -250,83 +646,110 @@ def get_model_radar_parameters(
250
646
  input_core_dims=[["psd_parameters"]],
251
647
  output_core_dims=[["radar_variables"]],
252
648
  vectorize=True,
253
- dask="forbidden",
254
- dask_gufunc_kwargs={"output_sizes": {"radar_variables": 5}}, # lengths of the new output_core_dims dimensions.
649
+ dask="parallelized",
650
+ dask_gufunc_kwargs={
651
+ "output_sizes": {"radar_variables": len(RADAR_VARIABLES)},
652
+ "allow_rechunk": True,
653
+ }, # lengths of the new output_core_dims dimensions.
255
654
  output_dtypes=["float64"],
256
655
  )
257
656
 
258
- # Add parameters coordinates
259
- da_radar = da_radar.assign_coords({"radar_variables": ["Zh", "Zdr", "rho_hv", "ldr", "Kdp", "Ai"]})
260
-
261
- # Create parameters dataset
262
- ds_radar = da_radar.to_dataset(dim="radar_variables")
263
-
264
- # Expand dimensions for later merging
265
- dims_dict = {
266
- "radar_band": [radar_band],
267
- "axis_ratio": [axis_ratio],
268
- "canting_angle_std": [canting_angle_std],
269
- "diameter_max": [diameter_max],
270
- }
271
- ds_radar = ds_radar.expand_dims(dim=dims_dict)
657
+ # Finalize radar dataset (add name, coordinates)
658
+ ds_radar = _finalize_radar_dataset(
659
+ da_radar=da_radar,
660
+ frequency=frequency,
661
+ num_points=num_points,
662
+ diameter_max=diameter_max,
663
+ canting_angle_std=canting_angle_std,
664
+ axis_ratio_model=axis_ratio_model,
665
+ permittivity_model=permittivity_model,
666
+ water_temperature=water_temperature,
667
+ elevation_angle=elevation_angle,
668
+ )
272
669
  return ds_radar
273
670
 
274
671
 
275
672
  def get_empirical_radar_parameters(
276
673
  ds,
277
- radar_band=None,
674
+ frequency,
675
+ num_points=1024,
676
+ diameter_max=10,
278
677
  canting_angle_std=7,
279
- diameter_max=8,
280
- axis_ratio="Thurai2007",
678
+ axis_ratio_model="Thurai2007",
679
+ permittivity_model="Turner2016",
680
+ water_temperature=10,
681
+ elevation_angle=0,
281
682
  ):
282
- """Compute radar parameters from empirical drop number concentration.
683
+ """Compute radar parameters from an empirical drop number concentration.
684
+
685
+ This function retrieve values for a single set of parameter only !
283
686
 
284
687
  Parameters
285
688
  ----------
286
689
  ds : xarray.Dataset
287
690
  Dataset containing the drop number concentration variable.
288
- radar_band : str
289
- Radar band to be used.
691
+ frequency : float
692
+ Frequency in GHz for which to compute the radar parameters.
290
693
  canting_angle_std : float, optional
291
- Standard deviation of the canting angle. The default value is 7.
694
+ Standard deviation of the canting angle. The default value is 10.
292
695
  diameter_max : float, optional
293
696
  Maximum diameter. The default value is 8 mm.
294
- axis_ratio : str, optional
295
- Method to compute the axis ratio. The default method is ``Thurai2007``.
697
+ axis_ratio_model : str, optional
698
+ Axis ratio model used to shape hydrometeors. The default is ``"Thurai2007"``.
699
+ See available models with ``disdrodb.scattering.available_axis_ratio_models()``.
700
+ permittivity_model : str
701
+ Permittivity model to use to compute the refractive index and the
702
+ rayleigh_dielectric_factor. The default is ``Turner2016``.
703
+ See available models with ``disdrodb.scattering.available_permittivity_models()``.
704
+ water_temperature : float
705
+ Water temperature in degree Celsius to be used in the permittivity model.
706
+ The default is 10 degC.
707
+ elevation_angle: str
708
+ Radar elevation angle in degrees.
709
+ Specify 90 degrees for vertically pointing radars.
710
+ The default is 0 degrees.
296
711
 
297
712
  Returns
298
713
  -------
299
714
  xarray.Dataset
300
715
  Dataset containing the computed radar parameters.
301
716
  """
717
+ # Subset dataset based on diameter max
718
+ ds = filter_diameter_bins(ds=ds, maximum_diameter=diameter_max)
719
+
302
720
  # Define inputs
303
- da_drop_number_concentration = ds["drop_number_concentration"].compute()
721
+ da_drop_number_concentration = ds["drop_number_concentration"] # .compute()
722
+
723
+ # Set all zeros drop number concentration to np.nan
724
+ # --> Otherwise inf can appear in the output
725
+ # --> Note that if a single np.nan is present, the output simulation will be NaN values
726
+ valid_obs = da_drop_number_concentration.sum(dim=DIAMETER_DIMENSION) != 0
727
+ da_drop_number_concentration = da_drop_number_concentration.where(valid_obs)
304
728
 
305
729
  # Define bin edges
306
- bin_edges = np.append(ds["diameter_bin_lower"].compute().data, ds["diameter_bin_upper"].compute().data[-1])
730
+ bin_edges = get_diameter_bin_edges(ds)
307
731
 
308
732
  # Check argument validity
309
- axis_ratio = check_axis_ratio(axis_ratio)
310
- radar_band = check_radar_band(radar_band)
311
-
312
- # Retrieve wavelengths in mm
313
- wavelength = get_radar_wavelength(radar_band)
733
+ axis_ratio_model = check_axis_ratio_model(axis_ratio_model)
734
+ permittivity_model = check_permittivity_model(permittivity_model)
314
735
 
315
736
  # Initialize scattering table
316
- scatterer = initialize_scatterer(
317
- wavelength=wavelength,
737
+ scatterer = load_scatterer(
738
+ frequency=frequency,
739
+ num_points=num_points,
740
+ diameter_max=diameter_max,
318
741
  canting_angle_std=canting_angle_std,
319
- D_max=diameter_max,
320
- axis_ratio=axis_ratio,
742
+ axis_ratio_model=axis_ratio_model,
743
+ permittivity_model=permittivity_model,
744
+ water_temperature=water_temperature,
745
+ elevation_angle=elevation_angle,
321
746
  )
322
747
 
323
748
  # Define kwargs
324
749
  kwargs = {
325
- "output_dictionary": False,
326
750
  "bin_edges": bin_edges,
327
751
  "scatterer": scatterer,
328
752
  }
329
-
330
753
  # Loop over each PSD (not in parallel --> dask="forbidden")
331
754
  # - It costs much more to initiate the scatterer rather than looping over timesteps !
332
755
  da_radar = xr.apply_ufunc(
@@ -336,34 +759,140 @@ def get_empirical_radar_parameters(
336
759
  input_core_dims=[["diameter_bin_center"]],
337
760
  output_core_dims=[["radar_variables"]],
338
761
  vectorize=True,
339
- dask="forbidden",
340
- dask_gufunc_kwargs={"output_sizes": {"radar_variables": 5}}, # lengths of the new output_core_dims dimensions.
762
+ dask="parallelized",
763
+ dask_gufunc_kwargs={
764
+ "output_sizes": {"radar_variables": len(RADAR_VARIABLES)},
765
+ }, # lengths of the new output_core_dims dimensions.
341
766
  output_dtypes=["float64"],
342
767
  )
768
+ # Finalize radar dataset (add name, coordinates)
769
+ ds_radar = _finalize_radar_dataset(
770
+ da_radar=da_radar,
771
+ frequency=frequency,
772
+ num_points=num_points,
773
+ diameter_max=diameter_max,
774
+ canting_angle_std=canting_angle_std,
775
+ axis_ratio_model=axis_ratio_model,
776
+ permittivity_model=permittivity_model,
777
+ water_temperature=water_temperature,
778
+ elevation_angle=elevation_angle,
779
+ )
780
+ return ds_radar
343
781
 
782
+
783
+ def _finalize_radar_dataset(
784
+ da_radar,
785
+ frequency,
786
+ num_points,
787
+ diameter_max,
788
+ canting_angle_std,
789
+ axis_ratio_model,
790
+ permittivity_model,
791
+ water_temperature,
792
+ elevation_angle,
793
+ ):
344
794
  # Add parameters coordinates
345
- da_radar = da_radar.assign_coords({"radar_variables": ["Zh", "Zdr", "rho_hv", "ldr", "Kdp", "Ai"]})
795
+ da_radar = da_radar.assign_coords({"radar_variables": RADAR_VARIABLES})
346
796
 
347
797
  # Create parameters dataset
348
798
  ds_radar = da_radar.to_dataset(dim="radar_variables")
349
799
 
350
- # Expand dimensions for later merging
800
+ # Expand dimensions for later merging in get_radar_parameters()
351
801
  dims_dict = {
352
- "radar_band": [radar_band],
353
- "axis_ratio": [axis_ratio],
354
- "canting_angle_std": [canting_angle_std],
802
+ "frequency": [frequency],
355
803
  "diameter_max": [diameter_max],
804
+ "num_points": [num_points],
805
+ "axis_ratio_model": [axis_ratio_model],
806
+ "canting_angle_std": [canting_angle_std],
807
+ "permittivity_model": [permittivity_model],
808
+ "water_temperature": [water_temperature],
809
+ "elevation_angle": [elevation_angle],
356
810
  }
357
811
  ds_radar = ds_radar.expand_dims(dim=dims_dict)
358
812
  return ds_radar
359
813
 
360
814
 
815
+ ####----------------------------------------------------------------------
816
+ #### Wrapper for L2E and L2M products
817
+
818
+
819
+ def ensure_rounded_unique_array(arr, decimals=None):
820
+ """Ensure that the input array is a unique, rounded array."""
821
+ arr = np.atleast_1d(arr)
822
+ if decimals is not None:
823
+ arr = arr.round(decimals)
824
+ return np.unique(arr)
825
+
826
+
827
+ def get_list_simulations_params(
828
+ frequency,
829
+ num_points,
830
+ diameter_max,
831
+ canting_angle_std,
832
+ axis_ratio_model,
833
+ permittivity_model,
834
+ water_temperature,
835
+ elevation_angle,
836
+ ):
837
+ """Return list with the set of parameters required for each simulation."""
838
+ # Ensure numeric frequencies
839
+ frequency = ensure_numerical_frequency(frequency)
840
+
841
+ # Ensure arguments are unique set of values
842
+ # - Otherwise problems with non-unique xarray dataset coordinates
843
+ frequency = ensure_rounded_unique_array(frequency, decimals=2)
844
+ num_points = ensure_rounded_unique_array(num_points, decimals=0)
845
+ diameter_max = ensure_rounded_unique_array(diameter_max, decimals=1)
846
+ canting_angle_std = ensure_rounded_unique_array(canting_angle_std, decimals=1)
847
+ axis_ratio_model = ensure_rounded_unique_array(axis_ratio_model)
848
+ permittivity_model = ensure_rounded_unique_array(permittivity_model)
849
+ water_temperature = ensure_rounded_unique_array(water_temperature, decimals=1)
850
+ elevation_angle = ensure_rounded_unique_array(elevation_angle, decimals=1)
851
+
852
+ # Check parameters validity
853
+ axis_ratio_model = [check_axis_ratio_model(model) for model in axis_ratio_model]
854
+ permittivity_model = [check_permittivity_model(model) for model in permittivity_model]
855
+
856
+ # Order frequency from lowest to highest
857
+ # --> ['S', 'C', 'X', 'Ku', 'K', 'Ka', 'W']
858
+ frequency = sorted(frequency)
859
+
860
+ # Retrieve combination of parameters
861
+ list_params = [
862
+ {
863
+ "frequency": freq.item(),
864
+ "diameter_max": d_max.item(),
865
+ "num_points": n_p.item(),
866
+ "canting_angle_std": cas.item(),
867
+ "axis_ratio_model": ar.item(),
868
+ "permittivity_model": perm.item(),
869
+ "water_temperature": t_w.item(),
870
+ "elevation_angle": el.item(),
871
+ }
872
+ for freq, d_max, n_p, cas, ar, perm, t_w, el in itertools.product(
873
+ frequency,
874
+ diameter_max,
875
+ num_points,
876
+ canting_angle_std,
877
+ axis_ratio_model,
878
+ permittivity_model,
879
+ water_temperature,
880
+ elevation_angle,
881
+ )
882
+ ]
883
+ return list_params
884
+
885
+
361
886
  def get_radar_parameters(
362
887
  ds,
363
- radar_band=None,
364
- canting_angle_std=7,
888
+ frequency=None,
889
+ num_points=1024,
365
890
  diameter_max=8,
366
- axis_ratio="Thurai2007",
891
+ canting_angle_std=7,
892
+ axis_ratio_model="Thurai2007",
893
+ permittivity_model="Turner2016",
894
+ water_temperature=10,
895
+ elevation_angle=0,
367
896
  parallel=True,
368
897
  ):
369
898
  """Compute radar parameters from empirical drop number concentration or PSD model.
@@ -372,15 +901,31 @@ def get_radar_parameters(
372
901
  ----------
373
902
  ds : xarray.Dataset
374
903
  Dataset containing the drop number concentration variable.
375
- radar_band : str or list of str, optional
376
- Radar band(s) to be used.
377
- If ``None`` (the default), all available radar bands are used.
378
- canting_angle_std : float or list of float, optional
379
- Standard deviation of the canting angle. The default value is 7.
904
+ frequency : str, float, or list of str and float, optional
905
+ Frequencies in GHz for which to compute the radar parameters.
906
+ Alternatively, also strings can be used to specify common radar frequencies.
907
+ If ``None``, the common radar frequencies will be used.
908
+ See ``disdrodb.scattering.available_radar_bands()``.
909
+ num_points: int or lis tof integer, optional
910
+ Number of bins into which discretize the PSD.
380
911
  diameter_max : float or list of float, optional
381
912
  Maximum diameter. The default value is 8 mm.
382
- axis_ratio : str or list of str, optional
383
- Method to compute the axis ratio. The default method is ``Thurai2007``.
913
+ canting_angle_std : float or list of float, optional
914
+ Standard deviation of the canting angle. The default value is 7.
915
+ axis_ratio_model : str or list of str, optional
916
+ Models to compute the axis ratio. The default model is ``Thurai2007``.
917
+ See available models with ``disdrodb.scattering.available_axis_ratio_models()``.
918
+ permittivity_model : str str or list of str, optional
919
+ Permittivity model to use to compute the refractive index and the
920
+ rayleigh_dielectric_factor. The default is ``Turner2016``.
921
+ See available models with ``disdrodb.scattering.available_permittivity_models()``.
922
+ water_temperature : float or list of float, optional
923
+ Water temperature in degree Celsius to be used in the permittivity model.
924
+ The default is 10 degC.
925
+ elevation_angle: float or list of float, optional
926
+ Radar elevation angles in degrees.
927
+ Specify 90 degrees for vertically pointing radars.
928
+ The default is 0 degrees.
384
929
  parallel : bool, optional
385
930
  Whether to compute radar variables in parallel.
386
931
  The default value is ``True``.
@@ -393,43 +938,31 @@ def get_radar_parameters(
393
938
  # Decide whether to simulate radar parameters based on empirical PSD or model PSD
394
939
  if "disdrodb_psd_model" not in ds.attrs and "drop_number_concentration" not in ds:
395
940
  raise ValueError("The input dataset is not a DISDRODB L2E or L2M product.")
941
+
396
942
  # Model-based simulation
397
943
  if "disdrodb_psd_model" in ds.attrs:
398
944
  func = get_model_radar_parameters
399
- ds_subset = get_psd_parameters(ds).compute()
945
+ ds_subset = select_psd_parameters(ds)
400
946
  # Empirical PSD simulation
401
947
  else:
402
948
  func = get_empirical_radar_parameters
403
- ds_subset = ds[["drop_number_concentration"]].compute()
404
-
405
- # Initialize radar band if not provided
406
- if radar_band is None:
407
- radar_band = available_radar_bands()
949
+ ds_subset = ds[["drop_number_concentration"]]
408
950
 
409
- # Ensure parameters are list
410
- diameter_max = np.atleast_1d(diameter_max)
411
- canting_angle_std = np.atleast_1d(canting_angle_std)
412
- axis_ratio = np.atleast_1d(axis_ratio)
413
- radar_band = np.atleast_1d(radar_band)
951
+ # Define default frequencies if not specified
952
+ if frequency is None:
953
+ frequency = available_radar_bands()
414
954
 
415
- # Check parameters validity
416
- axis_ratio = [check_axis_ratio(method) for method in axis_ratio]
417
- radar_band = [check_radar_band(band) for band in radar_band]
418
-
419
- # Order radar band from longest to shortest wavelength
420
- # - ["S", "C", "X", "Ku", "Ka", "W"]
421
- radar_band = sorted(radar_band, key=lambda x: wavelength_dict[x])[::-1]
422
-
423
- # Retrieve combination of parameters
424
- list_params = [
425
- {
426
- "radar_band": rb.item(),
427
- "canting_angle_std": cas.item(),
428
- "axis_ratio": ar.item(),
429
- "diameter_max": d_max.item(),
430
- }
431
- for rb, cas, ar, d_max in itertools.product(radar_band, canting_angle_std, axis_ratio, diameter_max)
432
- ]
955
+ # Define parameters for all requested simulations
956
+ list_params = get_list_simulations_params(
957
+ frequency=frequency,
958
+ num_points=num_points,
959
+ diameter_max=diameter_max,
960
+ canting_angle_std=canting_angle_std,
961
+ axis_ratio_model=axis_ratio_model,
962
+ permittivity_model=permittivity_model,
963
+ water_temperature=water_temperature,
964
+ elevation_angle=elevation_angle,
965
+ )
433
966
 
434
967
  # Compute radar variables for each configuration in parallel
435
968
  # - The function expects the data into memory (no dask arrays !)
@@ -440,17 +973,46 @@ def get_radar_parameters(
440
973
  list_ds = [func(ds_subset, **params) for params in list_params]
441
974
 
442
975
  # Merge into a single dataset
443
- # - Order radar bands from longest to shortest wavelength
444
976
  ds_radar = xr.merge(list_ds)
445
- ds_radar = ds_radar.sel(radar_band=radar_band)
977
+
978
+ # Order frequency from lowest to highest
979
+ # --> ['S', 'C', 'X', 'Ku', 'K', 'Ka', 'W']
980
+ frequency = sorted(ds_radar["frequency"].to_numpy())
981
+ ds_radar = ds_radar.sel(frequency=frequency)
982
+
983
+ # Map default frequency to classical radar band
984
+ # --> This transform the frequency coordinate to dtype object
985
+ ds_radar = _replace_common_frequency_with_radar_band(ds_radar)
446
986
 
447
987
  # Copy global attributes from input dataset
448
988
  ds_radar.attrs = ds.attrs.copy()
449
989
 
450
- # Remove single dimensions (add info to attributes)
451
- parameters = ["radar_band", "canting_angle_std", "axis_ratio", "diameter_max"]
452
- for param in parameters:
990
+ # Remove single dimensions and add scattering settings information for single dimensions
991
+ scattering_string = ""
992
+ for param in RADAR_OPTIONS:
453
993
  if ds_radar.sizes[param] == 1:
454
- ds_radar.attrs[f"disdrodb_scattering_{param}"] = ds_radar[param].item()
994
+ value = ds_radar[param].item()
995
+ scattering_string += f"param: {value}; "
996
+
997
+ if scattering_string != "":
998
+ ds_radar.attrs["disdrodb_scattering_options"] = scattering_string
455
999
  ds_radar = ds_radar.squeeze()
456
1000
  return ds_radar
1001
+
1002
+
1003
+ def _map_frequency_to_band(f):
1004
+ """Function to map frequency value to radar band."""
1005
+ for band, val in frequency_dict.items():
1006
+ if np.isclose(f, val):
1007
+ return band
1008
+ return f
1009
+
1010
+
1011
+ def _replace_common_frequency_with_radar_band(ds_radar):
1012
+ """Replace dataset coordinates with radar band if the case."""
1013
+ # Map frequencies to radar bands
1014
+ frequency = ds_radar["frequency"].to_numpy()
1015
+ frequency = [_map_frequency_to_band(f) for f in frequency]
1016
+ # Update dataset with new coordinate labels
1017
+ ds_radar = ds_radar.assign_coords({"frequency": ("frequency", frequency)})
1018
+ return ds_radar