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
disdrodb/l2/processing.py CHANGED
@@ -19,12 +19,13 @@
19
19
  import numpy as np
20
20
  import xarray as xr
21
21
 
22
- from disdrodb.l1.encoding_attrs import get_attrs_dict, get_encoding_dict
22
+ from disdrodb.constants import DIAMETER_DIMENSION
23
23
  from disdrodb.l1.fall_velocity import get_raindrop_fall_velocity
24
24
  from disdrodb.l1_env.routines import load_env_dataset
25
25
  from disdrodb.l2.empirical_dsd import (
26
+ BINS_METRICS,
27
+ add_bins_metrics,
26
28
  compute_integral_parameters,
27
- compute_qc_bins_metrics,
28
29
  compute_spectrum_parameters,
29
30
  get_drop_average_velocity,
30
31
  get_drop_number_concentration,
@@ -35,10 +36,9 @@ from disdrodb.l2.empirical_dsd import (
35
36
  )
36
37
  from disdrodb.psd import create_psd, estimate_model_parameters
37
38
  from disdrodb.psd.fitting import compute_gof_stats
38
- from disdrodb.utils.attrs import set_attrs
39
39
  from disdrodb.utils.decorators import check_pytmatrix_availability
40
- from disdrodb.utils.encoding import set_encodings
41
40
  from disdrodb.utils.time import ensure_sample_interval_in_seconds
41
+ from disdrodb.utils.writer import finalize_product
42
42
 
43
43
 
44
44
  def define_diameter_array(diameter_min=0, diameter_max=10, diameter_spacing=0.05):
@@ -109,26 +109,122 @@ def define_velocity_array(ds):
109
109
  return velocity
110
110
 
111
111
 
112
+ ####--------------------------------------------------------------------------
113
+ #### Timesteps filtering functions
114
+
115
+
116
+ def select_timesteps_with_drops(ds, minimum_ndrops=0):
117
+ """Select timesteps with at least the specified number of drops."""
118
+ valid_timesteps = ds["N"].to_numpy() >= minimum_ndrops
119
+ if not valid_timesteps.any().item():
120
+ raise ValueError(f"No timesteps with N >= {minimum_ndrops}.")
121
+ if "time" in ds.dims:
122
+ ds = ds.isel(time=valid_timesteps, drop=False)
123
+ return ds
124
+
125
+
126
+ def select_timesteps_with_minimum_nbins(ds, minimum_nbins):
127
+ """Select timesteps with at least the specified number of diameter bins with drops."""
128
+ if minimum_nbins == 0:
129
+ return ds
130
+ valid_timesteps = ds["Nbins"].to_numpy() >= minimum_nbins
131
+ if not valid_timesteps.any().item():
132
+ raise ValueError(f"No timesteps with Nbins >= {minimum_nbins}.")
133
+ if "time" in ds.dims:
134
+ ds = ds.isel(time=valid_timesteps, drop=False)
135
+ return ds
136
+
137
+
138
+ def select_timesteps_with_minimum_rain_rate(ds, minimum_rain_rate):
139
+ """Select timesteps with at least the specified rain rate."""
140
+ if minimum_rain_rate == 0:
141
+ return ds
142
+ # Ensure dimensionality of R is 1
143
+ # - Collapse velocity_method
144
+ dims_to_agg = set(ds["R"].dims) - {"time"}
145
+ da_r = ds["R"].max(dim=dims_to_agg)
146
+ # Determine valid timesteps
147
+ valid_timesteps = da_r.to_numpy() >= minimum_rain_rate
148
+ if not valid_timesteps.any().item():
149
+ raise ValueError(f"No timesteps with rain rate (R) >= {minimum_rain_rate} mm/hr.")
150
+ if "time" in ds.dims:
151
+ ds = ds.isel(time=valid_timesteps, drop=False)
152
+ return ds
153
+
154
+
112
155
  ####--------------------------------------------------------------------------
113
156
  #### L2 Empirical Parameters
114
157
 
115
158
 
116
- def generate_l2_empirical(ds, ds_env=None, compute_spectra=False):
159
+ def _ensure_present(container, required, kind):
160
+ """Raise a ValueError if any of `required` are missing from the `container`."""
161
+ missing = [item for item in required if item not in container]
162
+ if missing:
163
+ raise ValueError(f"Dataset is missing required {kind}: {', '.join(missing)}")
164
+
165
+
166
+ def check_l2e_input_dataset(ds):
167
+ """Check dataset validity for L2E production."""
168
+ from disdrodb.scattering import RADAR_OPTIONS
169
+
170
+ # Check minimum required variables, coordinates and dimensions are presents
171
+ required_variables = ["drop_number", "fall_velocity"]
172
+ required_coords = [
173
+ "diameter_bin_center",
174
+ "diameter_bin_width",
175
+ "sample_interval",
176
+ ]
177
+ required_attributes = ["sensor_name"]
178
+ required_dims = [DIAMETER_DIMENSION]
179
+ _ensure_present(list(ds.data_vars), required=required_variables, kind="variables")
180
+ _ensure_present(list(ds.coords), required=required_coords, kind="coords")
181
+ _ensure_present(list(ds.dims), required=required_dims, kind="dimensions")
182
+ _ensure_present(list(ds.attrs), required=required_attributes, kind="attributes")
183
+
184
+ # Remove dimensions and coordinates generated by L2E routine
185
+ # - This allow to recursively repass L2E product to the generate_l2e function
186
+ unallowed_dims = [dim for dim in ds.dims if dim in ["source", "velocity_method", *RADAR_OPTIONS]]
187
+ ds = ds.drop_dims(unallowed_dims)
188
+ unallowed_coords = [coord for coord in ds.coords if coord in ["source", "velocity_method", *RADAR_OPTIONS]]
189
+ ds = ds.drop_vars(unallowed_coords)
190
+ return ds
191
+
192
+
193
+ def generate_l2e(
194
+ ds,
195
+ ds_env=None,
196
+ compute_spectra=False,
197
+ compute_percentage_contribution=False,
198
+ minimum_ndrops=1,
199
+ minimum_nbins=1,
200
+ minimum_rain_rate=0.01,
201
+ ):
117
202
  """Generate the DISDRODB L2E dataset from the DISDRODB L1 dataset.
118
203
 
119
204
  Parameters
120
205
  ----------
121
206
  ds : xarray.Dataset
122
207
  DISDRODB L1 dataset.
208
+ Alternatively, a xarray dataset with at least:
209
+
210
+ - variables: drop_number, fall_velocity
211
+ - dimension: DIAMETER_DIMENSION
212
+ - coordinates: diameter_bin_center, diameter_bin_width, sample_interval
213
+ - attributes: sensor_name
214
+
123
215
  ds_env : xarray.Dataset, optional
124
- Environmental dataset used for fall velocity and water density estimates.
125
- If None, a default environment dataset will be loaded.
216
+ Environmental dataset used for fall velocity and water density estimates.
217
+ If None, a default environment dataset will be loaded.
126
218
 
127
219
  Returns
128
220
  -------
129
221
  xarray.Dataset
130
- DISRODB L2E dataset.
222
+ DISDRODB L2E dataset.
131
223
  """
224
+ # Check and prepapre input dataset
225
+ ds = check_l2e_input_dataset(ds)
226
+
227
+ # -------------------------------------------------------
132
228
  # Initialize L2E dataset
133
229
  ds_l2 = xr.Dataset()
134
230
 
@@ -137,24 +233,20 @@ def generate_l2_empirical(ds, ds_env=None, compute_spectra=False):
137
233
 
138
234
  # -------------------------------------------------------
139
235
  #### Preprocessing
140
- # Discard all timesteps without measured drops
141
- # - This allow to speed up processing
142
- # - Regularization can be done at the end
143
- ds = ds.isel(time=ds["N"] > 0)
236
+ # Select timesteps with at least the specified number of drops
237
+ ds = select_timesteps_with_drops(ds, minimum_ndrops=minimum_ndrops)
144
238
 
145
- # Count number of diameter bins with data
146
- if "Nbins" not in ds:
147
- # Add bins statistics
148
- ds.update(compute_qc_bins_metrics(ds))
239
+ # Add bins metrics to resampled data if missing
240
+ ds = add_bins_metrics(ds)
241
+
242
+ # Remove timesteps with not enough bins with drops
243
+ ds = select_timesteps_with_minimum_nbins(ds, minimum_nbins=minimum_nbins)
149
244
 
150
245
  # Retrieve ENV dataset or take defaults
151
246
  # --> Used for fall velocity and water density estimates
152
247
  if ds_env is None:
153
248
  ds_env = load_env_dataset(ds)
154
-
155
- # TODO: Derive water density as function of ENV (temperature, ...)
156
- # --> (T == 10){density_water <- 999.7}else if(T == 20){density_water <- 998.2}else{density_water <- 995.7}
157
- water_density = 1000 # kg / m3
249
+ water_density = ds_env.get("water_density", 1000) # kg / m3
158
250
 
159
251
  # Determine if the velocity dimension is available
160
252
  has_velocity_dimension = "velocity_bin_center" in ds.dims
@@ -172,6 +264,7 @@ def generate_l2_empirical(ds, ds_env=None, compute_spectra=False):
172
264
 
173
265
  # Copy relevant L1 variables to L2 product
174
266
  variables = [
267
+ "raw_drop_number", # 2D V x D
175
268
  "drop_number", # 2D V x D
176
269
  "drop_counts", # 1D D
177
270
  "sample_interval",
@@ -184,6 +277,7 @@ def generate_l2_empirical(ds, ds_env=None, compute_spectra=False):
184
277
 
185
278
  variables = [var for var in variables if var in ds]
186
279
  ds_l2.update(ds[variables])
280
+ ds_l2.update(ds[BINS_METRICS])
187
281
 
188
282
  # -------------------------------------------------------------------------------------------
189
283
  # Compute and add drop average velocity if an optical disdrometer (i.e OTT Parsivel or ThiesLPM)
@@ -206,7 +300,7 @@ def generate_l2_empirical(ds, ds_env=None, compute_spectra=False):
206
300
  ds_l2["drop_number_concentration"] = drop_number_concentration
207
301
 
208
302
  # -------------------------------------------------------
209
- #### Compute L2 spectra
303
+ #### Compute R, LWC, KE and Z spectra
210
304
  if compute_spectra:
211
305
  ds_spectrum = compute_spectrum_parameters(
212
306
  drop_number_concentration,
@@ -217,6 +311,10 @@ def generate_l2_empirical(ds, ds_env=None, compute_spectra=False):
217
311
  )
218
312
  ds_l2.update(ds_spectrum)
219
313
 
314
+ if compute_percentage_contribution:
315
+ # TODO: Implement percentage contribution computation
316
+ pass
317
+
220
318
  # ----------------------------------------------------------------------------
221
319
  #### Compute L2 integral parameters from drop_number_concentration
222
320
  ds_parameters = compute_integral_parameters(
@@ -242,7 +340,7 @@ def generate_l2_empirical(ds, ds_env=None, compute_spectra=False):
242
340
 
243
341
  # -------------------------------------------------------
244
342
  #### Compute KE integral parameters directly from drop_number
245
- # - The kinetic energy variables can be computed using the actual measured fall velocity by the sensor.
343
+ # The kinetic energy variables can be computed using the actual measured fall velocity by the sensor.
246
344
  if has_velocity_dimension:
247
345
  ds_ke = get_kinetic_energy_variables_from_drop_number(
248
346
  drop_number=drop_number,
@@ -261,24 +359,18 @@ def generate_l2_empirical(ds, ds_env=None, compute_spectra=False):
261
359
  for var in ke_vars:
262
360
  ds_parameters[var] = ds_ke[var]
263
361
 
264
- # ----------------------------------------------------------------------------
265
- #### Finalize L2 Dataset
266
362
  # Add DSD integral parameters
267
363
  ds_l2.update(ds_parameters)
268
364
 
269
- # ----------------------------------------------------------------------------.
270
- #### Add encodings and attributes
271
- # Add variables attributes
272
- attrs_dict = get_attrs_dict()
273
- ds_l2 = set_attrs(ds_l2, attrs_dict=attrs_dict)
274
-
275
- # Add variables encoding
276
- encoding_dict = get_encoding_dict()
277
- ds_l2 = set_encodings(ds_l2, encoding_dict=encoding_dict)
365
+ # ----------------------------------------------------------------------------
366
+ #### Finalize L2 Dataset
367
+ ds_l2 = select_timesteps_with_minimum_rain_rate(ds_l2, minimum_rain_rate=minimum_rain_rate)
278
368
 
279
369
  # Add global attributes
280
370
  ds_l2.attrs = attrs
281
371
 
372
+ # Add variables attributes and encodings
373
+ ds_l2 = finalize_product(ds_l2, product="L2E")
282
374
  return ds_l2
283
375
 
284
376
 
@@ -286,22 +378,74 @@ def generate_l2_empirical(ds, ds_env=None, compute_spectra=False):
286
378
  #### L2 Model Parameters
287
379
 
288
380
 
289
- def generate_l2_model(
381
+ def _get_default_optimization(psd_model):
382
+ """PSD model defaults."""
383
+ defaults = {
384
+ "ExponentialPSD": "ML",
385
+ "GammaPSD": "ML",
386
+ "LognormalPSD": "ML",
387
+ "NormalizedGammaPSD": "GS",
388
+ }
389
+ optimization = defaults[psd_model]
390
+ return optimization
391
+
392
+
393
+ def check_l2m_input_dataset(ds):
394
+ """Check dataset validity for L2M production."""
395
+ # Retrieve drop_number concentration (if not available) from drop_number
396
+ # --> This allow people to use directly L1 datasets to generate L2M datasets
397
+ if "drop_number_concentration" not in ds:
398
+ if "drop_number" in ds:
399
+ check_l2e_input_dataset(ds)
400
+ sample_interval = ensure_sample_interval_in_seconds(ds["sample_interval"])
401
+ sampling_area = get_effective_sampling_area(
402
+ sensor_name=ds.attrs["sensor_name"],
403
+ diameter=ds["diameter_bin_center"] / 1000,
404
+ ) # m2
405
+ # Compute drop number concentration (Nt) [#/m3/mm]
406
+ ds["drop_number_concentration"] = get_drop_number_concentration(
407
+ drop_number=ds["drop_number"],
408
+ velocity=define_velocity_array(ds), # fall_velocity (and optionally also velocity_bin_center)
409
+ diameter_bin_width=ds["diameter_bin_width"], # mm
410
+ sample_interval=sample_interval,
411
+ sampling_area=sampling_area,
412
+ )
413
+ else:
414
+ raise ValueError("Please provide DISDRODB L1 or L2E dataset !")
415
+
416
+ # Check minimum required variables, coordinates and dimensions are presents
417
+ required_variables = ["drop_number_concentration"]
418
+ required_coords = [
419
+ "diameter_bin_center",
420
+ "diameter_bin_width",
421
+ "diameter_bin_lower",
422
+ "diameter_bin_upper",
423
+ "sample_interval",
424
+ ]
425
+ required_dims = [DIAMETER_DIMENSION]
426
+ _ensure_present(list(ds.data_vars), required=required_variables, kind="variables")
427
+ _ensure_present(list(ds.coords), required=required_coords, kind="coords")
428
+ _ensure_present(list(ds.dims), required=required_dims, kind="dimensions")
429
+ return ds
430
+
431
+
432
+ def generate_l2m(
290
433
  ds,
291
- ds_env=None,
292
- fall_velocity_method="Beard1976",
434
+ psd_model,
435
+ # Fitting options
436
+ optimization=None,
437
+ optimization_kwargs=None,
293
438
  # PSD discretization
294
439
  diameter_min=0,
295
440
  diameter_max=10,
296
441
  diameter_spacing=0.05,
297
- # Fitting options
298
- psd_model=None,
299
- optimization=None,
300
- optimization_kwargs=None,
442
+ # Processing options
443
+ ds_env=None,
444
+ fall_velocity_method="Beard1976",
301
445
  # Filtering options
302
- min_nbins=4,
303
- remove_timesteps_with_few_bins=False,
304
- mask_timesteps_with_few_bins=False,
446
+ minimum_ndrops=1,
447
+ minimum_nbins=3,
448
+ minimum_rain_rate=0.01,
305
449
  # GOF metrics options
306
450
  gof_metrics=True,
307
451
  ):
@@ -316,6 +460,8 @@ def generate_l2_model(
316
460
  ----------
317
461
  ds : xarray.Dataset
318
462
  DISDRODB L2E dataset.
463
+ psd_model : str
464
+ The PSD model to fit. See ``disdrodb.psd.available_psd_models()``.
319
465
  ds_env : xarray.Dataset, optional
320
466
  Environmental dataset used for fall velocity and water density estimates.
321
467
  If None, a default environment dataset will be loaded.
@@ -325,13 +471,14 @@ def generate_l2_model(
325
471
  Maximum PSD diameter. The default value is 8 mm.
326
472
  diameter_spacing : float, optional
327
473
  PSD diameter spacing. The default value is 0.05 mm.
328
- psd_model : str
329
- The PSD model to fit. See ``available_psd_models()``.
330
474
  optimization : str, optional
331
475
  The fitting optimization procedure. Either "GS" (Grid Search), "ML (Maximum Likelihood)
332
476
  or "MOM" (Method of Moments).
333
477
  optimization_kwargs : dict, optional
334
478
  Dictionary with arguments to customize the fitting procedure.
479
+ minimum_nbins: int
480
+ Minimum number of bins with drops required to fit the PSD model.
481
+ The default value is 5.
335
482
  gof_metrics : bool, optional
336
483
  Whether to add goodness-of-fit metrics to the output dataset. The default is True.
337
484
 
@@ -340,51 +487,37 @@ def generate_l2_model(
340
487
  xarray.Dataset
341
488
  DISDRODB L2M dataset.
342
489
  """
343
- # ----------------------------------------------------------------------------.
344
- #### NOTES
345
- # - Final processing: Optionally filter dataset only when PSD has fitted ?
346
- # --> but good to have everything to compare across models
490
+ ####------------------------------------------------------.
491
+ #### Define default PSD model and optimization
492
+ psd_model = "NormalizedGammaPSD" if psd_model is None else psd_model
493
+ optimization = _get_default_optimization(psd_model) if optimization is None else optimization
347
494
 
348
495
  # ----------------------------------------------------------------------------.
496
+ #### Preprocessing
349
497
  # Retrieve attributes
350
498
  attrs = ds.attrs.copy()
351
499
 
352
- # -------------------------------------------------------
353
- # Derive water density as function of ENV (temperature, ...)
354
- # TODO --> Add into ds_env !
355
- # --> (T == 10){density_water <- 999.7}else if(T == 20){density_water <- 998.2}else{density_water <- 995.7}
356
- water_density = 1000 # kg / m3
500
+ # Check and prepare dataset
501
+ ds = check_l2m_input_dataset(ds)
357
502
 
358
- ####------------------------------------------------------.
359
- #### Preprocessing
360
- # Count number of diameter bins with data
361
- if "Nbins" not in ds:
362
- # Add bins statistics
363
- ds.update(compute_qc_bins_metrics(ds))
503
+ # Retrieve measurement interval
504
+ # - If dataset is opened with decode_timedelta=False, sample_interval is already in seconds !
505
+ sample_interval = ensure_sample_interval_in_seconds(ds["sample_interval"])
364
506
 
365
- # Identify timesteps with enough diameter bins with counted trops
366
- valid_timesteps = ds["Nbins"] >= min_nbins
507
+ # Select timesteps with at least the specified number of drops
508
+ ds = select_timesteps_with_drops(ds, minimum_ndrops=minimum_ndrops)
367
509
 
368
- # Drop such timesteps if asked
369
- if remove_timesteps_with_few_bins:
370
- mask_timesteps_with_few_bins = False
371
- ds = ds.isel(time=valid_timesteps, drop=False)
510
+ # Add bins metrics if missing
511
+ ds = add_bins_metrics(ds)
512
+
513
+ # Remove timesteps with not enough bins with drops
514
+ ds = select_timesteps_with_minimum_nbins(ds, minimum_nbins=minimum_nbins)
372
515
 
373
516
  # Retrieve ENV dataset or take defaults
374
517
  # --> Used for fall velocity and water density estimates
375
518
  if ds_env is None:
376
519
  ds_env = load_env_dataset(ds)
377
-
378
- ####------------------------------------------------------.
379
- #### Define default PSD optimization arguments
380
- if psd_model is None and optimization is None:
381
- psd_model = "NormalizedGammaPSD"
382
- optimization = "GS"
383
- optimization_kwargs = {
384
- "target": "ND",
385
- "transformation": "identity",
386
- "error_order": 1, # MAE
387
- }
520
+ water_density = ds_env.get("water_density", 1000) # kg / m3
388
521
 
389
522
  ####------------------------------------------------------.
390
523
  #### Retrieve PSD parameters
@@ -394,11 +527,7 @@ def generate_l2_model(
394
527
  optimization=optimization,
395
528
  optimization_kwargs=optimization_kwargs,
396
529
  )
397
-
398
- ####------------------------------------------------------.
399
- #### Mask timesteps with few bins if asked
400
- if mask_timesteps_with_few_bins:
401
- ds_psd_params = ds_psd_params.where(valid_timesteps)
530
+ psd_fitting_attrs = ds_psd_params.attrs
402
531
 
403
532
  ####-------------------------------------------------------
404
533
  #### Create PSD
@@ -415,10 +544,6 @@ def generate_l2_model(
415
544
  )
416
545
  diameter_bin_width = diameter["diameter_bin_width"]
417
546
 
418
- # Retrieve time of integration
419
- # - If dataset is opened with decode_timedelta=False, sample_interval is already in seconds !
420
- sample_interval = ensure_sample_interval_in_seconds(ds["sample_interval"])
421
-
422
547
  # Retrieve drop number concentration
423
548
  drop_number_concentration = psd(diameter)
424
549
 
@@ -442,22 +567,29 @@ def generate_l2_model(
442
567
 
443
568
  # Add GOF statistics if asked
444
569
  if gof_metrics:
445
- ds_gof = compute_gof_stats(drop_number_concentration=ds["drop_number_concentration"], psd=psd)
570
+ ds_gof = compute_gof_stats(
571
+ obs=ds["drop_number_concentration"], # empirical N(D)
572
+ pred=psd(ds["diameter_bin_center"]), # fitted N(D) on empirical diameter bins !
573
+ )
446
574
  ds_params.update(ds_gof)
447
575
 
448
- #### ----------------------------------------------------------------------------.
449
- #### Add encodings and attributes
450
- # Add variables attributes
451
- attrs_dict = get_attrs_dict()
452
- ds_params = set_attrs(ds_params, attrs_dict=attrs_dict)
576
+ # Add empirical drop_number_concentration and fall velocity
577
+ # - To reuse output dataset to create another L2M dataset or to compute other GOF metrics
578
+ ds_params["drop_number_concentration"] = ds["drop_number_concentration"]
579
+ ds_params["fall_velocity"] = ds["fall_velocity"]
580
+ ds_params["N"] = ds["N"]
581
+ ds_params.update(ds[BINS_METRICS])
453
582
 
454
- # Add variables encoding
455
- encoding_dict = get_encoding_dict()
456
- ds_params = set_encodings(ds_params, encoding_dict=encoding_dict)
583
+ #### ----------------------------------------------------------------------------.
584
+ #### Finalize dataset
585
+ ds_params = select_timesteps_with_minimum_rain_rate(ds_params, minimum_rain_rate=minimum_rain_rate)
457
586
 
458
587
  # Add global attributes
459
588
  ds_params.attrs = attrs
460
- ds_params.attrs["disdrodb_psd_model"] = psd_name
589
+ ds_params.attrs.update(psd_fitting_attrs)
590
+
591
+ # Add variables attributes and encodings
592
+ ds_params = finalize_product(ds_params, product="L2M")
461
593
 
462
594
  # Return dataset
463
595
  return ds_params
@@ -470,10 +602,14 @@ def generate_l2_model(
470
602
  @check_pytmatrix_availability
471
603
  def generate_l2_radar(
472
604
  ds,
473
- radar_band=None,
474
- canting_angle_std=7,
605
+ frequency=None,
606
+ num_points=1024,
475
607
  diameter_max=10,
476
- axis_ratio="Thurai2007",
608
+ canting_angle_std=7,
609
+ axis_ratio_model="Thurai2007",
610
+ permittivity_model="Turner2016",
611
+ water_temperature=10,
612
+ elevation_angle=0,
477
613
  parallel=True,
478
614
  ):
479
615
  """Simulate polarimetric radar variables from empirical drop number concentration or the estimated PSD.
@@ -482,15 +618,31 @@ def generate_l2_radar(
482
618
  ----------
483
619
  ds : xarray.Dataset
484
620
  Dataset containing the drop number concentration variable or the PSD parameters.
485
- radar_band : str or list of str, optional
486
- Radar band(s) to be used.
487
- If ``None`` (the default), all available radar bands are used.
621
+ frequency : str, float, or list of str and float, optional
622
+ Frequencies in GHz for which to compute the radar parameters.
623
+ Alternatively, also strings can be used to specify common radar frequencies.
624
+ If ``None``, the common radar frequencies will be used.
625
+ See ``disdrodb.scattering.available_radar_bands()``.
626
+ num_points: int or list of integer, optional
627
+ Number of bins into which discretize the PSD.
628
+ diameter_max : float or list of float, optional
629
+ Maximum diameter. The default value is 10 mm.
488
630
  canting_angle_std : float or list of float, optional
489
631
  Standard deviation of the canting angle. The default value is 7.
490
- diameter_max : float or list of float, optional
491
- Maximum diameter. The default value is 8 mm.
492
- axis_ratio : str or list of str, optional
493
- Method to compute the axis ratio. The default method is ``Thurai2007``.
632
+ axis_ratio_model : str or list of str, optional
633
+ Models to compute the axis ratio. The default model is ``Thurai2007``.
634
+ See available models with ``disdrodb.scattering.available_axis_ratio_models()``.
635
+ permittivity_model : str str or list of str, optional
636
+ Permittivity model to use to compute the refractive index and the
637
+ rayleigh_dielectric_factor. The default is ``Turner2016``.
638
+ See available models with ``disdrodb.scattering.available_permittivity_models()``.
639
+ water_temperature : float or list of float, optional
640
+ Water temperature in degree Celsius to be used in the permittivity model.
641
+ The default is 10 degC.
642
+ elevation_angle : float or list of float, optional
643
+ Radar elevation angles in degrees.
644
+ Specify 90 degrees for vertically pointing radars.
645
+ The default is 0 degrees.
494
646
  parallel : bool, optional
495
647
  Whether to compute radar variables in parallel.
496
648
  The default value is ``True``.
@@ -507,22 +659,21 @@ def generate_l2_radar(
507
659
  # Retrieve radar variables from L2E drop number concentration or from estimated L2M PSD model
508
660
  ds_radar = get_radar_parameters(
509
661
  ds=ds,
510
- radar_band=radar_band,
511
- canting_angle_std=canting_angle_std,
662
+ frequency=frequency,
663
+ num_points=num_points,
512
664
  diameter_max=diameter_max,
513
- axis_ratio=axis_ratio,
665
+ canting_angle_std=canting_angle_std,
666
+ axis_ratio_model=axis_ratio_model,
667
+ permittivity_model=permittivity_model,
668
+ water_temperature=water_temperature,
669
+ elevation_angle=elevation_angle,
514
670
  parallel=parallel,
515
671
  )
516
672
 
517
673
  #### ----------------------------------------------------------------------------.
518
- #### Add encodings and attributes
519
- # Add variables attributes
520
- attrs_dict = get_attrs_dict()
521
- ds_radar = set_attrs(ds_radar, attrs_dict=attrs_dict)
522
-
523
- # Add variables encoding
524
- encoding_dict = get_encoding_dict()
525
- ds_radar = set_encodings(ds_radar, encoding_dict=encoding_dict)
674
+ #### Finalize dataset
675
+ # Add variables attributes and encodings
676
+ ds_radar = finalize_product(ds_radar)
526
677
 
527
678
  # Return dataset
528
679
  return ds_radar