• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ltelab / disdrodb / 20103688028

10 Dec 2025 03:21PM UTC coverage: 90.697% (-3.1%) from 93.763%
20103688028

Pull #237

github

web-flow
Merge 78993c17d into 0693c3a0e
Pull Request #237: Add HC algorithms and product options tests

1295 of 1430 new or added lines in 45 files covered. (90.56%)

308 existing lines in 1 file now uncovered.

9925 of 10943 relevant lines covered (90.7%)

0.91 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.44
/disdrodb/l2/processing.py
1
# -----------------------------------------------------------------------------.
2
# Copyright (c) 2021-2023 DISDRODB developers
3
#
4
# This program is free software: you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
# -----------------------------------------------------------------------------.
17
"""Implement DISDRODB L2 processing."""
18
import numpy as np
1✔
19
import xarray as xr
1✔
20

21
from disdrodb.constants import DIAMETER_DIMENSION, METEOROLOGICAL_VARIABLES, VELOCITY_DIMENSION
1✔
22
from disdrodb.fall_velocity import get_rain_fall_velocity, get_rain_fall_velocity_from_ds
1✔
23
from disdrodb.l1_env.routines import load_env_dataset
1✔
24
from disdrodb.l2.empirical_dsd import (
1✔
25
    BINS_METRICS,
26
    add_bins_metrics,
27
    compute_integral_parameters,
28
    compute_spectrum_parameters,
29
    get_drop_number_concentration,
30
    get_effective_sampling_area,
31
    get_effective_sampling_interval,
32
    get_kinetic_energy_variables_from_drop_number,
33
    get_min_max_diameter,
34
    get_rain_accumulation,
35
    get_rain_rate_from_drop_number,
36
)
37
from disdrodb.psd import create_psd, estimate_model_parameters
1✔
38
from disdrodb.psd.fitting import compute_gof_stats
1✔
39
from disdrodb.utils.decorators import check_pytmatrix_availability
1✔
40
from disdrodb.utils.manipulations import (
1✔
41
    define_diameter_array,
42
    filter_diameter_bins,
43
    filter_velocity_bins,
44
)
45
from disdrodb.utils.writer import finalize_product
1✔
46

47

48
def define_velocity_array(ds):
1✔
49
    """
50
    Create the fall velocity DataArray using various methods.
51

52
    If 'velocity_bin_center' is a dimension in the dataset, returns a Dataset
53
    with 'measured_velocity', 'average_velocity', and 'fall_velocity' as variables.
54
    Otherwise, returns the 'fall_velocity' DataArray from the input dataset.
55

56
    Parameters
57
    ----------
58
    ds : xarray.Dataset
59
        The input dataset containing velocity variables.
60

61
    Returns
62
    -------
63
    velocity: xarray.DataArray
64
    """
65
    drop_number = ds["drop_number"]
1✔
66
    if "velocity_bin_center" in ds.dims:
1✔
67
        velocity = xr.Dataset(
1✔
68
            {
69
                "theoretical_velocity": xr.ones_like(drop_number) * ds["fall_velocity"],
70
                "measured_velocity": xr.ones_like(drop_number) * ds["velocity_bin_center"],
71
            },
72
        ).to_array(dim="velocity_method")
73
    else:
74
        velocity = ds["fall_velocity"]
1✔
75
    return velocity
1✔
76

77

78
####--------------------------------------------------------------------------
79
#### Extract drop spectrum
80

81

82
def retrieve_drop_spectrum(
1✔
83
    ds,
84
    ds_env,
85
    above_velocity_fraction=None,
86
    above_velocity_tolerance=None,
87
    below_velocity_fraction=None,
88
    below_velocity_tolerance=None,
89
    maintain_drops_smaller_than=1,
90
    maintain_drops_slower_than=2.5,
91
    maintain_smallest_drops=False,
92
    remove_splashing_drops=True,
93
    fall_velocity_model="Beard1976",
94
):
95
    """Retrieve the drop spectrum from the DISDRODB L1 product."""
96
    from disdrodb.fall_velocity.rain import get_rain_fall_velocity
1✔
97

98
    # Retrieve spectrum
99
    raw_spectrum = ds["raw_drop_number"].copy()
1✔
100

101
    # Retrieve coordinates
102
    diameter_upper = raw_spectrum["diameter_bin_upper"]
1✔
103
    diameter_lower = raw_spectrum["diameter_bin_lower"]
1✔
104
    velocity_upper = raw_spectrum["velocity_bin_upper"]
1✔
105

106
    # Retrieve rainfall mask
107
    raindrop_fall_velocity_upper = get_rain_fall_velocity(
1✔
108
        diameter=diameter_upper,
109
        model=fall_velocity_model,
110
        ds_env=ds_env,
111
    )
112
    raindrop_fall_velocity_lower = get_rain_fall_velocity(
1✔
113
        diameter=diameter_lower,
114
        model=fall_velocity_model,
115
        ds_env=ds_env,
116
    )
117
    rain_mask = define_rain_spectrum_mask(
1✔
118
        drop_number=raw_spectrum,
119
        fall_velocity_lower=raindrop_fall_velocity_lower,
120
        fall_velocity_upper=raindrop_fall_velocity_upper,
121
        above_velocity_fraction=above_velocity_fraction,
122
        above_velocity_tolerance=above_velocity_tolerance,
123
        below_velocity_fraction=below_velocity_fraction,
124
        below_velocity_tolerance=below_velocity_tolerance,
125
        maintain_drops_smaller_than=maintain_drops_smaller_than,
126
        maintain_drops_slower_than=maintain_drops_slower_than,
127
        maintain_smallest_drops=maintain_smallest_drops,
128
    )
129

130
    # Set to 0 spectrum not classified as liquid or mixed
131
    if "precipitation_type" in ds:
1✔
132
        raw_spectrum = xr.where(ds["precipitation_type"].isin([0, 2]), raw_spectrum, 0)
1✔
133

134
    # Retrieve drop spectrum
135
    # - Liquid + Mixed
136
    drop_spectrum = raw_spectrum.where(rain_mask, 0)
1✔
137

138
    # Optionally mask area affected by splashing
139
    if remove_splashing_drops and "flag_splashing" in ds:
1✔
140
        flag_splashing = ds["flag_splashing"]
1✔
141
        splash_mask = (diameter_lower >= 0.0) & (diameter_upper <= 6) & (velocity_upper <= 0.6)
1✔
142

143
        drop_spectrum = xr.where(flag_splashing == 1, drop_spectrum.where(~splash_mask, 0), drop_spectrum)
1✔
144
    return drop_spectrum
1✔
145

146

147
def define_rain_spectrum_mask(
1✔
148
    drop_number,
149
    fall_velocity_lower,
150
    fall_velocity_upper,
151
    above_velocity_fraction=None,
152
    above_velocity_tolerance=None,
153
    below_velocity_fraction=None,
154
    below_velocity_tolerance=None,
155
    maintain_drops_smaller_than=1,  # 1,   # 2
156
    maintain_drops_slower_than=2.5,  # 2.5, # 3
157
    maintain_smallest_drops=False,
158
):
159
    """Define a mask for the drop spectrum based on fall velocity thresholds.
160

161
    Parameters
162
    ----------
163
    drop_number : xarray.DataArray
164
        Array of drop counts per diameter and velocity bins.
165
    fall_velocity_lower : array-like
166
        The expected terminal fall velocities lower bound for rain drops of given size interval.
167
    fall_velocity_upper : array-like
168
         The expected terminal fall velocities upper bound for rain drops of given size interval.
169
    above_velocity_fraction : float, optional
170
        Fraction of terminal fall velocity above which rain drops are considered too fast.
171
        Either specify ``above_velocity_fraction`` or ``above_velocity_tolerance``.
172
    above_velocity_tolerance : float, optional
173
        Absolute tolerance above which rain drops terminal fall velocities are considered too fast.
174
        Either specify ``above_velocity_fraction`` or ``above_velocity_tolerance``.
175
    below_velocity_fraction : float, optional
176
        Fraction of terminal fall velocity below which rain drops are considered too slow.
177
        Either specify ``below_velocity_fraction`` or ``below_velocity_tolerance``.
178
    below_velocity_tolerance : float, optional
179
        Absolute tolerance below which rain drops terminal fall velocities are considered too slow.
180
         Either specify ``below_velocity_fraction`` or ``below_velocity_tolerance``.
181
    maintain_smallest : bool, optional
182
        If True, ensures that the small rain drops in the spectrum are retained in the mask.
183
        The smallest rain drops are characterized by ``maintain_drops_smaller_than``
184
        and ``maintain_drops_slower_than`` arguments.
185
        Defaults to False.
186
    maintain_drops_smaller_than : float, optional
187
        The diameter threshold to use for keeping the smallest rain drop.
188
        Defaults to 1 mm.
189
    maintain_drops_slower_than : float, optional
190
        The fall velocity threshold to use for keeping the smallest rain drops.
191
        Defaults to 2.5 m/s.
192

193
    Returns
194
    -------
195
    xarray.DataArray
196
        A boolean mask array indicating valid bins according to the specified criteria.
197

198
    """
199
    # Ensure it creates a 2D mask if the fall_velocity does not vary over time
200
    if "time" in drop_number.dims and "time" not in fall_velocity_lower.dims:
1✔
201
        drop_number = drop_number.isel(time=0)
1✔
202

203
    # Check arguments
204
    if above_velocity_fraction is not None and above_velocity_tolerance is not None:
1✔
205
        raise ValueError("Either specify 'above_velocity_fraction' or 'above_velocity_tolerance'.")
1✔
206
    if below_velocity_fraction is not None and below_velocity_tolerance is not None:
1✔
207
        raise ValueError("Either specify 'below_velocity_fraction' or 'below_velocity_tolerance'.")
1✔
208

209
    # Define above/below velocity thresholds
210
    if above_velocity_fraction is not None:
1✔
211
        above_fall_velocity = fall_velocity_upper * (1 + above_velocity_fraction)
1✔
212
    elif above_velocity_tolerance is not None:
1✔
213
        above_fall_velocity = fall_velocity_upper + above_velocity_tolerance
1✔
214
    else:
215
        above_fall_velocity = np.inf
1✔
216

217
    if below_velocity_fraction is not None:
1✔
218
        below_fall_velocity = fall_velocity_lower * (1 - below_velocity_fraction)
1✔
219
    elif below_velocity_tolerance is not None:
1✔
220
        below_fall_velocity = fall_velocity_lower - below_velocity_tolerance
1✔
221
    else:
222
        below_fall_velocity = 0
1✔
223

224
    # Define velocity 2D array
225
    velocity_lower = xr.ones_like(drop_number) * drop_number["velocity_bin_lower"]
1✔
226
    velocity_upper = xr.ones_like(drop_number) * drop_number["velocity_bin_upper"]
1✔
227

228
    # Define mask
229
    mask = np.logical_and(
1✔
230
        velocity_upper > below_fall_velocity,
231
        velocity_lower < above_fall_velocity,
232
    )
233

234
    # Maintant smallest drops
235
    if maintain_smallest_drops:
1✔
236
        mask_smallest = np.logical_and(
1✔
237
            drop_number["diameter_bin_upper"] <= maintain_drops_smaller_than,
238
            drop_number["velocity_bin_upper"] <= maintain_drops_slower_than,
239
        )
240
        mask = np.logical_or(mask, mask_smallest)
1✔
241

242
    return mask
1✔
243

244

245
####--------------------------------------------------------------------------
246
#### Timesteps filtering functions
247

248

249
def select_timesteps_with_drops(ds, minimum_ndrops=0):
1✔
250
    """Select timesteps with at least the specified number of drops."""
251
    # If not a unique time dimension, skip subsetting
252
    if ds["N"].dims != ("time",):
1✔
253
        return ds
1✔
254
    # Otherwise subset time dimension
255
    valid_timesteps = ds["N"].to_numpy() >= minimum_ndrops
1✔
256
    if not valid_timesteps.any().item():
1✔
257
        raise ValueError(f"No timesteps with N >= {minimum_ndrops}.")
×
258
    if "time" in ds.dims:
1✔
259
        ds = ds.isel(time=valid_timesteps, drop=False)
1✔
260
    return ds
1✔
261

262

263
def select_timesteps_with_minimum_nbins(ds, minimum_nbins):
1✔
264
    """Select timesteps with at least the specified number of diameter bins with drops."""
265
    # If not a unique time dimension, skip subsetting
266
    if ds["Nbins"].dims != ("time",):
1✔
267
        return ds
1✔
268
    # Otherwise subset time dimension
269
    if minimum_nbins == 0:
1✔
270
        return ds
×
271
    valid_timesteps = ds["Nbins"].to_numpy() >= minimum_nbins
1✔
272
    if not valid_timesteps.any().item():
1✔
273
        raise ValueError(f"No timesteps with Nbins >= {minimum_nbins}.")
×
274
    if "time" in ds.dims:
1✔
275
        ds = ds.isel(time=valid_timesteps, drop=False)
1✔
276
    return ds
1✔
277

278

279
def select_timesteps_with_minimum_rain_rate(ds, minimum_rain_rate):
1✔
280
    """Select timesteps with at least the specified rain rate."""
281
    if minimum_rain_rate == 0:
1✔
282
        return ds
1✔
283
    # Ensure dimensionality of R is 1
284
    # - Collapse velocity_method
285
    dims_to_agg = set(ds["R"].dims) - {"time"}
1✔
286
    da_r = ds["R"].max(dim=dims_to_agg)
1✔
287
    # Determine valid timesteps
288
    valid_timesteps = da_r.to_numpy() >= minimum_rain_rate
1✔
289
    if not valid_timesteps.any().item():
1✔
290
        raise ValueError(f"No timesteps with rain rate (R) >= {minimum_rain_rate} mm/hr.")
×
291
    if "time" in ds.dims:
1✔
292
        ds = ds.isel(time=valid_timesteps, drop=False)
1✔
293
    return ds
1✔
294

295

296
####--------------------------------------------------------------------------
297
#### L2 Empirical Parameters
298

299

300
def _ensure_present(container, required, kind):
1✔
301
    """Raise a ValueError if any of `required` are missing from the `container`."""
302
    missing = [item for item in required if item not in container]
1✔
303
    if missing:
1✔
304
        raise ValueError(f"Dataset is missing required {kind}: {', '.join(missing)}")
1✔
305

306

307
def check_l2e_input_dataset(ds):
1✔
308
    """Check dataset validity for L2E production."""
309
    from disdrodb.scattering import RADAR_OPTIONS
1✔
310

311
    # Check minimum required variables, coordinates and dimensions are presents
312
    required_variables = ["raw_drop_number"]
1✔
313
    required_coords = [
1✔
314
        "diameter_bin_center",
315
        "diameter_bin_width",
316
        "sample_interval",
317
    ]
318
    required_attributes = ["sensor_name"]
1✔
319
    required_dims = [DIAMETER_DIMENSION]
1✔
320
    _ensure_present(list(ds.data_vars), required=required_variables, kind="variables")
1✔
321
    _ensure_present(list(ds.coords), required=required_coords, kind="coords")
1✔
322
    _ensure_present(list(ds.dims), required=required_dims, kind="dimensions")
1✔
323
    _ensure_present(list(ds.attrs), required=required_attributes, kind="attributes")
1✔
324

325
    # Remove dimensions and coordinates generated by L2E routine
326
    # - This allow to recursively repass L2E product to the generate_l2e function
327
    unallowed_dims = [dim for dim in ds.dims if dim in ["source", "velocity_method", *RADAR_OPTIONS]]
1✔
328
    ds = ds.drop_dims(unallowed_dims)
1✔
329
    unallowed_coords = [coord for coord in ds.coords if coord in ["source", "velocity_method", *RADAR_OPTIONS]]
1✔
330
    ds = ds.drop_vars(unallowed_coords)
1✔
331
    return ds
1✔
332

333

334
def generate_l2e(
1✔
335
    ds,
336
    ds_env=None,
337
    compute_spectra=False,
338
    compute_percentage_contribution=False,
339
    # Filtering options
340
    minimum_ndrops=1,
341
    minimum_nbins=1,
342
    minimum_rain_rate=0.01,
343
    minimum_diameter=0,
344
    maximum_diameter=10,
345
    minimum_velocity=0,
346
    maximum_velocity=12,
347
    keep_mixed_precipitation=False,
348
    # Spectrum filtering options
349
    fall_velocity_model="Beard1976",
350
    above_velocity_fraction=0.5,
351
    above_velocity_tolerance=None,
352
    below_velocity_fraction=0.5,
353
    below_velocity_tolerance=None,
354
    maintain_drops_smaller_than=1,  # 2
355
    maintain_drops_slower_than=2.5,  # 3
356
    maintain_smallest_drops=True,
357
    remove_splashing_drops=True,
358
):
359
    """Generate the DISDRODB L2E dataset from the DISDRODB L1 dataset.
360

361
    Parameters
362
    ----------
363
    ds : xarray.Dataset
364
        DISDRODB L1 dataset.
365
        Alternatively, a xarray dataset with at least:
366
            - variables: raw_drop_number
367
            - dimension: DIAMETER_DIMENSION
368
            - coordinates: diameter_bin_center, diameter_bin_width, sample_interval
369
            - attributes: sensor_name
370
    ds_env : xarray.Dataset, optional
371
        Environmental dataset used for fall velocity and water density estimates.
372
        If None, a default environment dataset will be loaded.
373
    fall_velocity_model : str, optional
374
        Model name to estimate drop fall velocity.
375
        The default method is ``"Beard1976"``.
376
    minimum_diameter : float, optional
377
        Minimum diameter for filtering. The default value is 0 mm.
378
    maximum_diameter : float, optional
379
        Maximum diameter for filtering. The default value is 10 mm.
380
    minimum_velocity : float, optional
381
        Minimum velocity for filtering. The default value is 0 m/s.
382
    maximum_velocity : float, optional
383
        Maximum velocity for filtering. The default value is 12 m/s.
384
    above_velocity_fraction : float, optional
385
        Fraction of drops above velocity threshold. The default value is 0.5.
386
    above_velocity_tolerance : float or None, optional
387
        Tolerance for above velocity filtering. The default value is ``None``.
388
    below_velocity_fraction : float, optional
389
        Fraction of drops below velocity threshold. The default value is 0.5.
390
    below_velocity_tolerance : float or None, optional
391
        Tolerance for below velocity filtering. The default value is ``None``.
392
    maintain_drops_smaller_than : float, optional
393
        Threshold for small diameter drops. The default value is 1.
394
    maintain_drops_slower_than : float, optional
395
        Threshold for small velocity drops. The default value is 2.5.
396
    maintain_smallest_drops : bool, optional
397
        Whether to maintain the smallest drops. The default value is ``True``.
398
    remove_splashing_drops: bool, optional
399
        Whether to mask splashing drops. The default value is ``True``.
400

401
    Returns
402
    -------
403
    xarray.Dataset
404
        DISDRODB L2E dataset.
405
    """
406
    # Check and prepapre input dataset
407
    ds = check_l2e_input_dataset(ds)
1✔
408

409
    # Select only dry and rainy timesteps
410
    if "precipitation_type" in ds:
1✔
411
        if keep_mixed_precipitation:  # class 4
1✔
NEW
412
            ds = ds.isel(time=ds["precipitation_type"].isin([-1, 0, 4]), drop=True)
×
413
        else:
414
            ds = ds.isel(time=ds["precipitation_type"].isin([-1, 0]), drop=True)
1✔
415

416
    # Determine if the velocity dimension is available
417
    has_velocity_dimension = VELOCITY_DIMENSION in ds.dims
1✔
418

419
    # - Filter diameter bins
420
    ds = filter_diameter_bins(ds=ds, minimum_diameter=minimum_diameter, maximum_diameter=maximum_diameter)
1✔
421
    # - Filter velocity bins
422
    if has_velocity_dimension:
1✔
423
        ds = filter_velocity_bins(ds=ds, minimum_velocity=minimum_velocity, maximum_velocity=maximum_velocity)
1✔
424

425
    # -------------------------------------------------------------------------------------------
426
    # Compute fall velocity
427
    ds["fall_velocity"] = get_rain_fall_velocity_from_ds(ds=ds, ds_env=ds_env, model=fall_velocity_model)
1✔
428

429
    # -------------------------------------------------------
430
    # Retrieve filtered spectrum and drop counts (summing over velocity dimension if present)
431
    if has_velocity_dimension:
1✔
432
        drop_number = retrieve_drop_spectrum(
1✔
433
            ds=ds,
434
            ds_env=ds_env,
435
            above_velocity_fraction=above_velocity_fraction,
436
            above_velocity_tolerance=above_velocity_tolerance,
437
            below_velocity_fraction=below_velocity_fraction,
438
            below_velocity_tolerance=below_velocity_tolerance,
439
            maintain_drops_smaller_than=maintain_drops_smaller_than,
440
            maintain_drops_slower_than=maintain_drops_slower_than,
441
            maintain_smallest_drops=maintain_smallest_drops,
442
            remove_splashing_drops=remove_splashing_drops,
443
            fall_velocity_model=fall_velocity_model,
444
        )
445
        drop_counts = drop_number.sum(dim=VELOCITY_DIMENSION)  # 1D (diameter)
1✔
446
        drop_counts_raw = ds["raw_drop_number"].sum(dim=VELOCITY_DIMENSION)  # 1D (diameter)
1✔
447
    else:
448
        drop_number = ds["raw_drop_number"]  # no filtering applied
1✔
449
        drop_counts = ds["raw_drop_number"]  # 1D (diameter)
1✔
450
        drop_counts_raw = ds["raw_drop_number"]
1✔
451

452
    ds["drop_number"] = drop_number
1✔
453
    ds["drop_counts"] = drop_counts
1✔
454

455
    # -------------------------------------------------------
456
    # Compute drop statistics
457
    # - Compute minimum and max drop diameter observed
458
    min_drop_diameter, max_drop_diameter = get_min_max_diameter(drop_counts)
1✔
459

460
    # - Add rain drop statistics
461
    ds["Dmin"] = min_drop_diameter
1✔
462
    ds["Dmax"] = max_drop_diameter
1✔
463
    ds["N"] = drop_counts.sum(dim=DIAMETER_DIMENSION)
1✔
464
    ds["Nraw"] = drop_counts_raw.sum(dim=DIAMETER_DIMENSION)
1✔
465
    ds["Nremoved"] = ds["Nraw"] - ds["N"]
1✔
466

467
    # - Add bins statistics
468
    ds = add_bins_metrics(ds)
1✔
469

470
    # -------------------------------------------------------
471
    # Initialize L2E dataset
472
    ds_l2 = xr.Dataset()
1✔
473

474
    # Retrieve attributes
475
    attrs = ds.attrs.copy()
1✔
476

477
    # -------------------------------------------------------
478
    #### Preprocessing
479
    # Select timesteps with at least the specified number of drops
480
    ds = select_timesteps_with_drops(ds, minimum_ndrops=minimum_ndrops)
1✔
481

482
    # Remove timesteps with not enough bins with drops
483
    ds = select_timesteps_with_minimum_nbins(ds, minimum_nbins=minimum_nbins)
1✔
484

485
    # Retrieve ENV dataset or take defaults
486
    # --> Used for fall velocity and water density estimates
487
    if ds_env is None:
1✔
488
        ds_env = load_env_dataset(ds)
1✔
489
    water_density = ds_env.get("water_density", 1000)  # kg / m3
1✔
490

491
    # Determine if the velocity dimension is available
492
    has_velocity_dimension = "velocity_bin_center" in ds.dims
1✔
493

494
    # -------------------------------------------------------
495
    # Extract variables from L1
496
    sensor_name = ds.attrs["sensor_name"]
1✔
497
    diameter = ds["diameter_bin_center"] / 1000  # m
1✔
498
    diameter_bin_width = ds["diameter_bin_width"]  # mm
1✔
499
    drop_number = ds["drop_number"]
1✔
500

501
    # Retrieve effective sampling interval [s]
502
    sample_interval = get_effective_sampling_interval(ds, sensor_name=sensor_name)  # s
1✔
503

504
    # Retrieve effective sampling area [m2]
505
    sampling_area = get_effective_sampling_area(sensor_name=sensor_name, diameter=diameter)  # m2
1✔
506

507
    # Copy relevant L1 variables to L2 product
508
    variables = [
1✔
509
        # L1 inputs
510
        "sample_interval",
511
        "fall_velocity",
512
        "raw_drop_number",  # 2D V x D
513
        "drop_number",  # 2D V x D
514
        # Drop statistics
515
        "drop_counts",  # 1D D
516
        "N",
517
        "Nremoved",
518
        "Nraw",
519
        "Dmin",
520
        "Dmax",
521
        # L0C QC
522
        "qc_time",
523
        # L1 flags and variables
524
        "qc_resampling",
525
        "precipitation_type",
526
        "hydrometeor_type",
527
        "n_margin_fallers",
528
        "n_splashing",
529
        "flag_graupel",
530
        "flag_hail",
531
        "flag_spikes",
532
        "flag_splashing",
533
        "flag_wind_artefacts",
534
        *METEOROLOGICAL_VARIABLES,
535
    ]
536

537
    variables = [var for var in variables if var in ds]
1✔
538
    ds_l2.update(ds[variables])
1✔
539
    ds_l2.update(ds[BINS_METRICS])
1✔
540

541
    # -------------------------------------------------------------------------------------------
542
    # Compute and add drop average velocity if an optical disdrometer (i.e OTT Parsivel or ThiesLPM)
543
    # - We recompute it because if the input dataset is aggregated, it must be updated !
544
    # if has_velocity_dimension:
545
    #     ds["drop_average_velocity"] = get_drop_average_velocity(ds["drop_number"])
546

547
    # -------------------------------------------------------------------------------------------
548
    # Define velocity array with dimension 'velocity_method'
549
    velocity = define_velocity_array(ds)
1✔
550
    velocity = velocity.fillna(0)
1✔
551

552
    # Compute drop number concentration (Nt) [#/m3/mm]
553
    drop_number_concentration = get_drop_number_concentration(
1✔
554
        drop_number=drop_number,
555
        velocity=velocity,
556
        diameter_bin_width=diameter_bin_width,
557
        sample_interval=sample_interval,
558
        sampling_area=sampling_area,
559
    )
560
    ds_l2["drop_number_concentration"] = drop_number_concentration
1✔
561

562
    # -------------------------------------------------------
563
    #### Compute R, LWC, KE and Z spectra
564
    if compute_spectra:
1✔
565
        ds_spectrum = compute_spectrum_parameters(
×
566
            drop_number_concentration,
567
            velocity=ds["fall_velocity"],
568
            diameter=diameter,
569
            sample_interval=sample_interval,
570
            water_density=water_density,
571
        )
572
        ds_l2.update(ds_spectrum)
×
573

574
    if compute_percentage_contribution:
1✔
575
        # TODO: Implement percentage contribution computation
576
        pass
×
577

578
    # ----------------------------------------------------------------------------
579
    #### Compute L2 integral parameters from drop_number_concentration
580
    ds_parameters = compute_integral_parameters(
1✔
581
        drop_number_concentration=drop_number_concentration,
582
        velocity=ds["fall_velocity"],
583
        diameter=diameter,
584
        diameter_bin_width=diameter_bin_width,
585
        sample_interval=sample_interval,
586
        water_density=water_density,
587
    )
588

589
    # -------------------------------------------------------
590
    #### Compute R and P from drop number (without velocity assumptions)
591
    # - Rain rate and accumulation computed with this method are not influenced by the fall velocity of drops !
592
    ds_l2["Rm"] = get_rain_rate_from_drop_number(
1✔
593
        drop_number=drop_number,
594
        sampling_area=sampling_area,
595
        diameter=diameter,
596
        sample_interval=sample_interval,
597
    )
598
    # Compute rain accumulation (P) [mm]
599
    ds_l2["Pm"] = get_rain_accumulation(rain_rate=ds_l2["Rm"], sample_interval=sample_interval)
1✔
600

601
    # -------------------------------------------------------
602
    #### Compute KE integral parameters directly from drop_number
603
    # The kinetic energy variables can be computed using the actual measured fall velocity by the sensor.
604
    if has_velocity_dimension:
1✔
605
        ds_ke = get_kinetic_energy_variables_from_drop_number(
1✔
606
            drop_number=drop_number,
607
            diameter=diameter,
608
            velocity=velocity,
609
            sampling_area=sampling_area,
610
            sample_interval=sample_interval,
611
            water_density=water_density,
612
        )
613
        # Combine integral parameters
614
        ke_vars = list(ds_ke.data_vars)
1✔
615
        ds_ke = ds_ke.expand_dims(dim={"source": ["drop_number"]}, axis=-1)
1✔
616
        ds_ke_dsd = ds_parameters[ke_vars].expand_dims(dim={"source": ["drop_number_concentration"]}, axis=-1)
1✔
617
        ds_ke = xr.concat((ds_ke_dsd, ds_ke), dim="source")
1✔
618
        ds_parameters = ds_parameters.drop_vars(ke_vars)
1✔
619
        for var in ke_vars:
1✔
620
            ds_parameters[var] = ds_ke[var]
1✔
621

622
    # Add DSD integral parameters
623
    ds_l2.update(ds_parameters)
1✔
624

625
    # ----------------------------------------------------------------------------
626
    #### Finalize L2 Dataset
627
    ds_l2 = select_timesteps_with_minimum_rain_rate(ds_l2, minimum_rain_rate=minimum_rain_rate)
1✔
628

629
    # Add global attributes
630
    ds_l2.attrs = attrs
1✔
631

632
    # Add variables attributes and encodings
633
    ds_l2 = finalize_product(ds_l2, product="L2E")
1✔
634
    return ds_l2
1✔
635

636

637
####--------------------------------------------------------------------------
638
#### L2 Model Parameters
639

640

641
def _get_default_optimization(psd_model):
1✔
642
    """PSD model defaults."""
643
    defaults = {
1✔
644
        "ExponentialPSD": "ML",
645
        "GammaPSD": "ML",
646
        "LognormalPSD": "ML",
647
        "NormalizedGammaPSD": "GS",
648
    }
649
    optimization = defaults[psd_model]
1✔
650
    return optimization
1✔
651

652

653
def check_l2m_input_dataset(ds):
1✔
654
    """Check dataset validity for L2M production."""
655
    # Retrieve drop_number concentration (if not available) from drop_number
656
    # --> This allow people to use directly L1 datasets to generate L2M datasets
657
    if "drop_number_concentration" not in ds:
1✔
658
        if "drop_number" in ds:
×
659
            check_l2e_input_dataset(ds)
×
NEW
660
            sample_interval = get_effective_sampling_interval(ds, sensor_name=ds.attrs["sensor_name"])
×
661
            sampling_area = get_effective_sampling_area(
×
662
                sensor_name=ds.attrs["sensor_name"],
663
                diameter=ds["diameter_bin_center"] / 1000,
664
            )  # m2
665
            # Compute drop number concentration (Nt) [#/m3/mm]
666
            ds["drop_number_concentration"] = get_drop_number_concentration(
×
667
                drop_number=ds["drop_number"],
668
                velocity=define_velocity_array(ds),  # fall_velocity (and optionally also velocity_bin_center)
669
                diameter_bin_width=ds["diameter_bin_width"],  # mm
670
                sample_interval=sample_interval,
671
                sampling_area=sampling_area,
672
            )
673
        else:
674
            raise ValueError("Please provide DISDRODB L1 or L2E dataset !")
×
675

676
    # Check minimum required variables, coordinates and dimensions are presents
677
    required_variables = ["drop_number_concentration"]
1✔
678
    required_coords = [
1✔
679
        "diameter_bin_center",
680
        "diameter_bin_width",
681
        "diameter_bin_lower",
682
        "diameter_bin_upper",
683
        "sample_interval",
684
    ]
685
    required_dims = [DIAMETER_DIMENSION]
1✔
686
    _ensure_present(list(ds.data_vars), required=required_variables, kind="variables")
1✔
687
    _ensure_present(list(ds.coords), required=required_coords, kind="coords")
1✔
688
    _ensure_present(list(ds.dims), required=required_dims, kind="dimensions")
1✔
689
    return ds
1✔
690

691

692
def generate_l2m(
1✔
693
    ds,
694
    psd_model,
695
    # Fitting options
696
    optimization=None,
697
    optimization_kwargs=None,
698
    # PSD discretization
699
    diameter_min=0,
700
    diameter_max=10,
701
    diameter_spacing=0.05,
702
    # Processing options
703
    ds_env=None,
704
    fall_velocity_model="Beard1976",
705
    # Filtering options
706
    minimum_ndrops=1,
707
    minimum_nbins=3,
708
    minimum_rain_rate=0.01,
709
    # GOF metrics options
710
    gof_metrics=True,
711
):
712
    """
713
    Generate the DISDRODB L2M dataset from a DISDRODB L2E dataset.
714

715
    This function estimates PSD model parameters and successively computes DSD integral parameters.
716
    Optionally, radar variables at various bands are simulated using T-matrix simulations.
717
    Goodness-of-fit metrics of the PSD can also be optionally included into the output dataset.
718

719
    Parameters
720
    ----------
721
    ds : xarray.Dataset
722
        DISDRODB L2E dataset.
723
    psd_model : str
724
        The PSD model to fit. See ``disdrodb.psd.available_psd_models()``.
725
    ds_env : xarray.Dataset, optional
726
        Environmental dataset used for fall velocity and water density estimates.
727
        If None, a default environment dataset will be loaded.
728
    diameter_min : float, optional
729
        Minimum PSD diameter. The default value is 0 mm.
730
    diameter_max : float, optional
731
        Maximum PSD diameter. The default value is 8 mm.
732
    diameter_spacing : float, optional
733
        PSD diameter spacing. The default value is 0.05 mm.
734
    optimization : str, optional
735
        The fitting optimization procedure. Either "GS" (Grid Search), "ML (Maximum Likelihood)
736
        or "MOM" (Method of Moments).
737
    optimization_kwargs : dict, optional
738
        Dictionary with arguments to customize the fitting procedure.
739
    minimum_nbins: int
740
        Minimum number of bins with drops required to fit the PSD model.
741
        The default value is 5.
742
    gof_metrics : bool, optional
743
        Whether to add goodness-of-fit metrics to the output dataset. The default is True.
744

745
    Returns
746
    -------
747
    xarray.Dataset
748
        DISDRODB L2M dataset.
749
    """
750
    ####------------------------------------------------------.
751
    #### Define default PSD model and optimization
752
    psd_model = "NormalizedGammaPSD" if psd_model is None else psd_model
1✔
753
    optimization = _get_default_optimization(psd_model) if optimization is None else optimization
1✔
754

755
    # ----------------------------------------------------------------------------.
756
    #### Preprocessing
757
    # Retrieve attributes
758
    attrs = ds.attrs.copy()
1✔
759

760
    # Check and prepare dataset
761
    ds = check_l2m_input_dataset(ds)
1✔
762

763
    # Retrieve measurement interval
764
    # - If dataset is opened with decode_timedelta=False, sample_interval is already in seconds !
765
    sample_interval = get_effective_sampling_interval(ds, sensor_name=ds.attrs["sensor_name"])
1✔
766

767
    # Select timesteps with at least the specified number of drops
768
    ds = select_timesteps_with_drops(ds, minimum_ndrops=minimum_ndrops)
1✔
769

770
    # Add bins metrics if missing
771
    ds = add_bins_metrics(ds)
1✔
772

773
    # Remove timesteps with not enough bins with drops
774
    ds = select_timesteps_with_minimum_nbins(ds, minimum_nbins=minimum_nbins)
1✔
775

776
    # Retrieve ENV dataset or take defaults
777
    # --> Used for fall velocity and water density estimates
778
    if ds_env is None:
1✔
779
        ds_env = load_env_dataset(ds)
1✔
780
    water_density = ds_env.get("water_density", 1000)  # kg / m3
1✔
781

782
    ####------------------------------------------------------.
783
    #### Retrieve PSD parameters
784
    ds_psd_params = estimate_model_parameters(
1✔
785
        ds=ds,
786
        psd_model=psd_model,
787
        optimization=optimization,
788
        optimization_kwargs=optimization_kwargs,
789
    )
790
    psd_fitting_attrs = ds_psd_params.attrs
1✔
791

792
    ####-------------------------------------------------------
793
    #### Create PSD
794
    psd_name = ds_psd_params.attrs["disdrodb_psd_model"]
1✔
795
    psd = create_psd(psd_name, parameters=ds_psd_params)
1✔
796

797
    ####-------------------------------------------------------
798
    #### Compute integral parameters
799
    # Define diameter array
800
    diameter = define_diameter_array(
1✔
801
        diameter_min=diameter_min,
802
        diameter_max=diameter_max,
803
        diameter_spacing=diameter_spacing,
804
    )
805
    diameter_bin_width = diameter["diameter_bin_width"]
1✔
806

807
    # Retrieve drop number concentration
808
    drop_number_concentration = psd(diameter)
1✔
809

810
    # Retrieve fall velocity for each new diameter bin
811
    velocity = get_rain_fall_velocity(diameter=diameter, model=fall_velocity_model, ds_env=ds_env)  # mm
1✔
812

813
    # Compute integral parameters
814
    ds_params = compute_integral_parameters(
1✔
815
        drop_number_concentration=drop_number_concentration,
816
        velocity=velocity,
817
        diameter=diameter / 1000,  # in meters !
818
        diameter_bin_width=diameter_bin_width,
819
        sample_interval=sample_interval,
820
        water_density=water_density,
821
    )
822

823
    #### ----------------------------------------------------------------------------
824
    #### Create L2 Dataset
825
    # Update with PSD parameters
826
    ds_params.update(ds_psd_params)
1✔
827

828
    # Add GOF statistics if asked
829
    if gof_metrics:
1✔
830
        ds_gof = compute_gof_stats(
1✔
831
            obs=ds["drop_number_concentration"],  # empirical N(D)
832
            pred=psd(ds["diameter_bin_center"]),  # fitted N(D) on empirical diameter bins !
833
        )
834
        ds_params.update(ds_gof)
1✔
835

836
    # Add empirical drop_number_concentration and fall velocity
837
    # - To reuse output dataset to create another L2M dataset or to compute other GOF metrics
838
    # Copy relevant L1 variables to L2 product
839
    variables = [
1✔
840
        "drop_number_concentration",
841
        "fall_velocity",
842
        "N",
843
        *METEOROLOGICAL_VARIABLES,
844
    ]
845
    variables = [var for var in variables if var in ds]
1✔
846
    ds_params.update(ds[variables])
1✔
847
    ds_params.update(ds[BINS_METRICS])
1✔
848

849
    #### ----------------------------------------------------------------------------.
850
    #### Finalize dataset
851
    ds_params = select_timesteps_with_minimum_rain_rate(ds_params, minimum_rain_rate=minimum_rain_rate)
1✔
852

853
    # Add global attributes
854
    ds_params.attrs = attrs
1✔
855
    ds_params.attrs.update(psd_fitting_attrs)
1✔
856

857
    # Add variables attributes and encodings
858
    ds_params = finalize_product(ds_params, product="L2M")
1✔
859

860
    # Return dataset
861
    return ds_params
1✔
862

863

864
####-------------------------------------------------------------------------------------------------------------------.
865
#### L2 Radar Parameters
866

867

868
@check_pytmatrix_availability
1✔
869
def generate_l2_radar(
1✔
870
    ds,
871
    frequency=None,
872
    num_points=1024,
873
    diameter_max=10,
874
    canting_angle_std=7,
875
    axis_ratio_model="Thurai2007",
876
    permittivity_model="Turner2016",
877
    water_temperature=10,
878
    elevation_angle=0,
879
    parallel=True,
880
):
881
    """Simulate polarimetric radar variables from empirical drop number concentration or the estimated PSD.
882

883
    Parameters
884
    ----------
885
    ds : xarray.Dataset
886
        Dataset containing the drop number concentration variable or the PSD parameters.
887
    frequency : str, float, or list of str and float, optional
888
        Frequencies in GHz for which to compute the radar parameters.
889
        Alternatively, also strings can be used to specify common radar frequencies.
890
        If ``None``, the common radar frequencies will be used.
891
        See ``disdrodb.scattering.available_radar_bands()``.
892
    num_points: int or list of integer, optional
893
        Number of bins into which discretize the PSD.
894
    diameter_max : float or list of float, optional
895
        Maximum diameter. The default value is 10 mm.
896
    canting_angle_std : float or list of float, optional
897
        Standard deviation of the canting angle.  The default value is 7.
898
    axis_ratio_model : str or list of str, optional
899
        Models to compute the axis ratio. The default model is ``Thurai2007``.
900
        See available models with ``disdrodb.scattering.available_axis_ratio_models()``.
901
    permittivity_model : str str or list of str, optional
902
        Permittivity model to use to compute the refractive index and the
903
        rayleigh_dielectric_factor. The default is ``Turner2016``.
904
        See available models with ``disdrodb.scattering.available_permittivity_models()``.
905
    water_temperature : float or list of float, optional
906
        Water temperature in degree Celsius to be used in the permittivity model.
907
        The default is 10 degC.
908
    elevation_angle : float or list of float, optional
909
        Radar elevation angles in degrees.
910
        Specify 90 degrees for vertically pointing radars.
911
        The default is 0 degrees.
912
    parallel : bool, optional
913
        Whether to compute radar variables in parallel.
914
        The default value is ``True``.
915

916
    Returns
917
    -------
918
    xarray.Dataset
919
        Dataset containing the computed radar parameters.
920
    """
921
    # Import here to avoid pytmatrix has mandatory dependency
922
    # - It is only needed for radar simulation
923
    from disdrodb.scattering import get_radar_parameters
×
924

925
    # Retrieve radar variables from L2E drop number concentration or from estimated L2M PSD model
926
    ds_radar = get_radar_parameters(
×
927
        ds=ds,
928
        frequency=frequency,
929
        num_points=num_points,
930
        diameter_max=diameter_max,
931
        canting_angle_std=canting_angle_std,
932
        axis_ratio_model=axis_ratio_model,
933
        permittivity_model=permittivity_model,
934
        water_temperature=water_temperature,
935
        elevation_angle=elevation_angle,
936
        parallel=parallel,
937
    )
938

939
    #### ----------------------------------------------------------------------------.
940
    #### Finalize dataset
941
    # Add variables attributes and encodings
942
    ds_radar = finalize_product(ds_radar)
×
943

944
    # Return dataset
945
    return ds_radar
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc