disdrodb 0.1.3__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 (62) hide show
  1. disdrodb/__init__.py +4 -0
  2. disdrodb/_version.py +2 -2
  3. disdrodb/api/checks.py +70 -47
  4. disdrodb/api/configs.py +0 -2
  5. disdrodb/api/info.py +3 -3
  6. disdrodb/api/io.py +48 -8
  7. disdrodb/api/path.py +116 -133
  8. disdrodb/api/search.py +12 -3
  9. disdrodb/cli/disdrodb_create_summary.py +103 -0
  10. disdrodb/cli/disdrodb_create_summary_station.py +1 -1
  11. disdrodb/cli/disdrodb_run_l0a_station.py +1 -1
  12. disdrodb/cli/disdrodb_run_l0b_station.py +2 -2
  13. disdrodb/cli/disdrodb_run_l0c_station.py +2 -2
  14. disdrodb/cli/disdrodb_run_l1_station.py +2 -2
  15. disdrodb/cli/disdrodb_run_l2e_station.py +2 -2
  16. disdrodb/cli/disdrodb_run_l2m_station.py +2 -2
  17. disdrodb/data_transfer/download_data.py +123 -7
  18. disdrodb/issue/writer.py +2 -0
  19. disdrodb/l0/l0a_processing.py +10 -5
  20. disdrodb/l0/l0b_nc_processing.py +10 -6
  21. disdrodb/l0/l0b_processing.py +26 -61
  22. disdrodb/l0/l0c_processing.py +369 -251
  23. disdrodb/l0/readers/LPM/ARM/ARM_LPM.py +7 -0
  24. disdrodb/l0/readers/PARSIVEL2/ARM/ARM_PARSIVEL2.py +4 -0
  25. disdrodb/l0/readers/PARSIVEL2/CANADA/UQAM_NC.py +69 -0
  26. disdrodb/l0/readers/PARSIVEL2/MPI/BCO_PARSIVEL2.py +136 -0
  27. disdrodb/l0/readers/PARSIVEL2/MPI/BOWTIE.py +220 -0
  28. disdrodb/l0/readers/PARSIVEL2/NASA/LPVEX.py +109 -0
  29. disdrodb/l0/readers/PARSIVEL2/NETHERLANDS/DELFT_NC.py +3 -0
  30. disdrodb/l1/fall_velocity.py +46 -0
  31. disdrodb/l1/processing.py +1 -1
  32. disdrodb/l2/processing.py +1 -1
  33. disdrodb/metadata/checks.py +132 -125
  34. disdrodb/psd/fitting.py +172 -205
  35. disdrodb/psd/models.py +1 -1
  36. disdrodb/routines/__init__.py +54 -0
  37. disdrodb/{l0/routines.py → routines/l0.py} +288 -418
  38. disdrodb/{l1/routines.py → routines/l1.py} +60 -92
  39. disdrodb/{l2/routines.py → routines/l2.py} +249 -462
  40. disdrodb/{routines.py → routines/wrappers.py} +95 -7
  41. disdrodb/scattering/axis_ratio.py +5 -1
  42. disdrodb/scattering/permittivity.py +18 -0
  43. disdrodb/scattering/routines.py +56 -36
  44. disdrodb/summary/routines.py +110 -34
  45. disdrodb/utils/archiving.py +434 -0
  46. disdrodb/utils/cli.py +5 -5
  47. disdrodb/utils/dask.py +62 -1
  48. disdrodb/utils/decorators.py +31 -0
  49. disdrodb/utils/encoding.py +5 -1
  50. disdrodb/{l2 → utils}/event.py +1 -66
  51. disdrodb/utils/logger.py +1 -1
  52. disdrodb/utils/manipulations.py +22 -12
  53. disdrodb/utils/routines.py +166 -0
  54. disdrodb/utils/time.py +3 -291
  55. disdrodb/utils/xarray.py +3 -0
  56. disdrodb/viz/plots.py +85 -14
  57. {disdrodb-0.1.3.dist-info → disdrodb-0.1.4.dist-info}/METADATA +2 -2
  58. {disdrodb-0.1.3.dist-info → disdrodb-0.1.4.dist-info}/RECORD +62 -54
  59. {disdrodb-0.1.3.dist-info → disdrodb-0.1.4.dist-info}/entry_points.txt +1 -0
  60. {disdrodb-0.1.3.dist-info → disdrodb-0.1.4.dist-info}/WHEEL +0 -0
  61. {disdrodb-0.1.3.dist-info → disdrodb-0.1.4.dist-info}/licenses/LICENSE +0 -0
  62. {disdrodb-0.1.3.dist-info → disdrodb-0.1.4.dist-info}/top_level.txt +0 -0
@@ -20,7 +20,7 @@ import time
20
20
  from typing import Optional
21
21
 
22
22
  from disdrodb.api.search import available_stations, get_required_product
23
- from disdrodb.utils.cli import _execute_cmd
23
+ from disdrodb.utils.cli import execute_cmd
24
24
 
25
25
  ####--------------------------------------------------------------------------.
26
26
  #### Run DISDRODB Station Processing
@@ -239,7 +239,7 @@ def run_l0a_station(
239
239
  ],
240
240
  )
241
241
  # Execute command
242
- _execute_cmd(cmd)
242
+ execute_cmd(cmd)
243
243
 
244
244
 
245
245
  def run_l0b_station(
@@ -323,7 +323,7 @@ def run_l0b_station(
323
323
  ],
324
324
  )
325
325
  # Execute command
326
- _execute_cmd(cmd)
326
+ execute_cmd(cmd)
327
327
 
328
328
 
329
329
  def run_l0c_station(
@@ -407,7 +407,7 @@ def run_l0c_station(
407
407
  ],
408
408
  )
409
409
  # Execute command
410
- _execute_cmd(cmd)
410
+ execute_cmd(cmd)
411
411
 
412
412
 
413
413
  def run_l1_station(
@@ -483,7 +483,7 @@ def run_l1_station(
483
483
  ],
484
484
  )
485
485
  # Execute command
486
- _execute_cmd(cmd)
486
+ execute_cmd(cmd)
487
487
 
488
488
 
489
489
  def run_l2e_station(
@@ -559,7 +559,7 @@ def run_l2e_station(
559
559
  ],
560
560
  )
561
561
  # Execute command
562
- _execute_cmd(cmd)
562
+ execute_cmd(cmd)
563
563
 
564
564
 
565
565
  def run_l2m_station(
@@ -635,7 +635,33 @@ def run_l2m_station(
635
635
  ],
636
636
  )
637
637
  # Execute command
638
- _execute_cmd(cmd)
638
+ execute_cmd(cmd)
639
+
640
+
641
+ def create_summary_station(
642
+ data_source,
643
+ campaign_name,
644
+ station_name,
645
+ parallel=False,
646
+ data_archive_dir=None,
647
+ ):
648
+ """Create summary figures and tables for a DISDRODB station."""
649
+ # Define command
650
+ cmd = " ".join(
651
+ [
652
+ "disdrodb_create_summary_station",
653
+ # Station arguments
654
+ data_source,
655
+ campaign_name,
656
+ station_name,
657
+ "--data_archive_dir",
658
+ str(data_archive_dir),
659
+ "--parallel",
660
+ str(parallel),
661
+ ],
662
+ )
663
+ # Execute command
664
+ execute_cmd(cmd)
639
665
 
640
666
 
641
667
  ####--------------------------------------------------------------------------.
@@ -1409,4 +1435,66 @@ def run_l2m(
1409
1435
  print(f"{product} processing of {data_source} {campaign_name} {station_name} station ended.")
1410
1436
 
1411
1437
 
1438
+ def create_summary(
1439
+ data_sources=None,
1440
+ campaign_names=None,
1441
+ station_names=None,
1442
+ parallel=False,
1443
+ data_archive_dir=None,
1444
+ metadata_archive_dir=None,
1445
+ ):
1446
+ """Create summary figures and tables for a set of DISDRODB station.
1447
+
1448
+ Parameters
1449
+ ----------
1450
+ data_sources : list
1451
+ Name of data source(s) to process.
1452
+ The name(s) must be UPPER CASE.
1453
+ If campaign_names and station are not specified, process all stations.
1454
+ The default value is ``None``.
1455
+ campaign_names : list
1456
+ Name of the campaign(s) to process.
1457
+ The name(s) must be UPPER CASE.
1458
+ The default value is ``None``.
1459
+ station_names : list
1460
+ Station names to process.
1461
+ The default value is ``None``.
1462
+ data_archive_dir : str (optional)
1463
+ The directory path where the DISDRODB Data Archive is located.
1464
+ The directory path must end with ``<...>/DISDRODB``.
1465
+ If ``None``, it uses the ``data_archive_dir`` path specified
1466
+ in the DISDRODB active configuration.
1467
+ """
1468
+ # Get list of available stations
1469
+ list_info = available_stations(
1470
+ # DISDRODB root directories
1471
+ data_archive_dir=data_archive_dir,
1472
+ metadata_archive_dir=metadata_archive_dir,
1473
+ # Stations arguments
1474
+ data_sources=data_sources,
1475
+ campaign_names=campaign_names,
1476
+ station_names=station_names,
1477
+ # Search options
1478
+ product="L2E",
1479
+ product_kwargs={"rolling": False, "sample_interval": 60},
1480
+ raise_error_if_empty=True,
1481
+ )
1482
+
1483
+ # Loop over stations
1484
+ print(f"Creation of summaries for {len(list_info)} stations has started.")
1485
+ for data_source, campaign_name, station_name in list_info:
1486
+ # Run processing
1487
+ create_summary_station(
1488
+ # DISDRODB root directories
1489
+ data_archive_dir=data_archive_dir,
1490
+ # Station arguments
1491
+ data_source=data_source,
1492
+ campaign_name=campaign_name,
1493
+ station_name=station_name,
1494
+ # Processing option
1495
+ parallel=parallel,
1496
+ )
1497
+ print("Creation of station summaries has terminated.")
1498
+
1499
+
1412
1500
  ####--------------------------------------------------------------------------.
@@ -295,6 +295,8 @@ def get_axis_ratio_thurai_2005(diameter):
295
295
  """
296
296
  Compute the axis ratio of raindrops using the Thurai et al. (2005) model.
297
297
 
298
+ This model is assumed to be valid only for particles up to 5 mm.
299
+
298
300
  Parameters
299
301
  ----------
300
302
  diameter : array-like
@@ -311,7 +313,6 @@ def get_axis_ratio_thurai_2005(diameter):
311
313
  J. Atmos. Oceanic Technol., 22, 966-978, https://doi.org/10.1175/JTECH1767.1
312
314
 
313
315
  """
314
- # Valid between 1 and 5 mm
315
316
  axis_ratio = 0.9707 + 4.26e-2 * diameter - 4.29e-2 * diameter**2 + 6.5e-3 * diameter**3 - 3e-4 * diameter**4
316
317
  return axis_ratio
317
318
 
@@ -350,6 +351,9 @@ def get_axis_ratio_thurai_2007(diameter):
350
351
  # Combine axis ratio
351
352
  axis_ratio_below_1_5 = xr.where(diameter > 0.7, axis_ratio_below_1_5, axis_ratio_below_0_7)
352
353
  axis_ratio = xr.where(diameter > 1.5, axis_ratio_above_1_5, axis_ratio_below_1_5)
354
+
355
+ # Ensure np.nan is preserved in arrays
356
+ axis_ratio = xr.where(np.isnan(diameter), np.nan, axis_ratio)
353
357
  return axis_ratio
354
358
 
355
359
 
@@ -124,6 +124,24 @@ def get_refractive_index(temperature, frequency, permittivity_model):
124
124
  >>> m = get_refractive_index(temperature=temperature, frequency=frequency, permittivity_model="Liebe1991")
125
125
 
126
126
  """
127
+ # Ensure input is numpy array or xr.DataArray
128
+ frequency = ensure_array(frequency)
129
+ temperature = ensure_array(temperature)
130
+
131
+ # If both inputs are numpy (or dask) arrays with size > 1 → raise error
132
+ if (
133
+ not isinstance(temperature, xr.DataArray)
134
+ and not isinstance(frequency, xr.DataArray)
135
+ and np.size(temperature) > 1
136
+ and np.size(frequency) > 1
137
+ ):
138
+ raise ValueError(
139
+ "get_refractive_index does not support broadcasting plain numpy/dask arrays "
140
+ "when both `temperature` and `frequency` have size > 1. "
141
+ "Please provide both input as xarray.DataArray objects "
142
+ "with different dimensions to enable labeled broadcasting.",
143
+ )
144
+
127
145
  # Retrieve refractive_index function
128
146
  func = get_refractive_index_function(permittivity_model)
129
147
 
@@ -432,6 +432,41 @@ def load_scatterer(
432
432
  return scatterer
433
433
 
434
434
 
435
+ def precompute_scattering_tables(
436
+ frequency,
437
+ num_points,
438
+ diameter_max,
439
+ canting_angle_std,
440
+ axis_ratio_model,
441
+ permittivity_model,
442
+ water_temperature,
443
+ elevation_angle,
444
+ verbose=True,
445
+ ):
446
+ """Precompute the pyTMatrix scattering tables required for radar variables simulations."""
447
+ from disdrodb.scattering.routines import get_list_simulations_params, load_scatterer
448
+
449
+ # Define parameters for all requested simulations
450
+ list_params = get_list_simulations_params(
451
+ frequency=frequency,
452
+ num_points=num_points,
453
+ diameter_max=diameter_max,
454
+ canting_angle_std=canting_angle_std,
455
+ axis_ratio_model=axis_ratio_model,
456
+ permittivity_model=permittivity_model,
457
+ water_temperature=water_temperature,
458
+ elevation_angle=elevation_angle,
459
+ )
460
+
461
+ # Compute require scattering tables
462
+ for params in list_params:
463
+ # Initialize scattering table
464
+ _ = load_scatterer(
465
+ verbose=verbose,
466
+ **params,
467
+ )
468
+
469
+
435
470
  ####----------------------------------------------------------------------
436
471
  #### Scattering functions
437
472
 
@@ -458,12 +493,10 @@ def compute_radar_variables(scatterer):
458
493
  radar_vars["DBZV"] = 10 * np.log10(radar.refl(scatterer, h_pol=False)) # dBZ
459
494
 
460
495
  radar_vars["ZDR"] = 10 * np.log10(radar.Zdr(scatterer)) # dB
461
- if ~np.isfinite(radar_vars["ZDR"]):
462
- radar_vars["ZDR"] = np.nan
496
+ radar_vars["ZDR"] = np.where(np.isfinite(radar_vars["ZDR"]), radar_vars["ZDR"], np.nan)
463
497
 
464
498
  radar_vars["LDR"] = 10 * np.log10(radar.ldr(scatterer)) # dBZ
465
- if ~np.isfinite(radar_vars["LDR"]):
466
- radar_vars["LDR"] = np.nan
499
+ radar_vars["LDR"] = np.where(np.isfinite(radar_vars["LDR"]), radar_vars["LDR"], np.nan)
467
500
 
468
501
  radar_vars["RHOHV"] = radar.rho_hv(scatterer) # deg/km
469
502
  radar_vars["DELTAHV"] = radar.delta_hv(scatterer) * 180.0 / np.pi # [deg]
@@ -482,29 +515,26 @@ def compute_radar_variables(scatterer):
482
515
  RADAR_VARIABLES = ["DBZH", "DBZV", "ZDR", "LDR", "RHOHV", "DELTAHV", "KDP", "AH", "AV", "ADP"]
483
516
 
484
517
 
485
- def _initialize_null_output(output_dictionary):
486
- if output_dictionary:
487
- return dict.fromkeys(RADAR_VARIABLES, np.nan)
488
- return np.zeros(len(RADAR_VARIABLES)) * np.nan
518
+ def _try_compute_radar_variables(scatterer):
519
+ with suppress_warnings():
520
+ try:
521
+ radar_vars = compute_radar_variables(scatterer)
522
+ output = np.array(list(radar_vars.values()))
523
+ except Exception:
524
+ output = np.zeros(len(RADAR_VARIABLES)) * np.nan
525
+ return output
489
526
 
490
527
 
491
528
  def _estimate_empirical_radar_parameters(
492
529
  drop_number_concentration,
493
530
  bin_edges,
494
531
  scatterer,
495
- output_dictionary,
496
532
  ):
497
533
  # Assign PSD model to the scatterer object
498
534
  scatterer.psd = BinnedPSD(bin_edges, drop_number_concentration)
499
535
 
500
536
  # Get radar variables
501
- with suppress_warnings():
502
- try:
503
- radar_vars = compute_radar_variables(scatterer)
504
- output = radar_vars if output_dictionary else np.array(list(radar_vars.values()))
505
- except Exception:
506
- output = _initialize_null_output(output_dictionary)
507
- return output
537
+ return _try_compute_radar_variables(scatterer)
508
538
 
509
539
 
510
540
  def _estimate_model_radar_parameters(
@@ -512,23 +542,16 @@ def _estimate_model_radar_parameters(
512
542
  psd_model,
513
543
  psd_parameters_names,
514
544
  scatterer,
515
- output_dictionary,
516
545
  ):
517
546
  # Assign PSD model to the scatterer object
518
547
  parameters = dict(zip(psd_parameters_names, parameters))
519
548
  scatterer.psd = create_psd(psd_model, parameters)
520
549
 
521
550
  # Get radar variables
522
- with suppress_warnings():
523
- try:
524
- radar_vars = compute_radar_variables(scatterer)
525
- output = radar_vars if output_dictionary else np.array(list(radar_vars.values()))
526
- except Exception:
527
- output = _initialize_null_output(output_dictionary)
528
- return output
551
+ return _try_compute_radar_variables(scatterer)
529
552
 
530
553
 
531
- def get_psd_parameters(ds):
554
+ def select_psd_parameters(ds):
532
555
  """Return a xr.Dataset with only the PSD parameters as variable."""
533
556
  psd_model = ds.attrs["disdrodb_psd_model"]
534
557
  required_parameters = get_required_parameters(psd_model)
@@ -587,14 +610,14 @@ def get_model_radar_parameters(
587
610
  # Retrieve psd model and parameters.
588
611
  psd_model = ds.attrs["disdrodb_psd_model"]
589
612
  required_parameters = get_required_parameters(psd_model)
590
- ds_parameters = get_psd_parameters(ds)
613
+ ds_parameters = select_psd_parameters(ds)
591
614
 
592
615
  # Check argument validity
593
616
  axis_ratio_model = check_axis_ratio_model(axis_ratio_model)
594
617
  permittivity_model = check_permittivity_model(permittivity_model)
595
618
 
596
619
  # Create DataArray with PSD parameters
597
- da_parameters = ds_parameters.to_array(dim="psd_parameters").compute()
620
+ da_parameters = ds_parameters.to_array(dim="psd_parameters")
598
621
 
599
622
  # Initialize scattering table
600
623
  scatterer = load_scatterer(
@@ -610,7 +633,6 @@ def get_model_radar_parameters(
610
633
 
611
634
  # Define kwargs
612
635
  kwargs = {
613
- "output_dictionary": False,
614
636
  "psd_model": psd_model,
615
637
  "psd_parameters_names": required_parameters,
616
638
  "scatterer": scatterer,
@@ -624,9 +646,10 @@ def get_model_radar_parameters(
624
646
  input_core_dims=[["psd_parameters"]],
625
647
  output_core_dims=[["radar_variables"]],
626
648
  vectorize=True,
627
- dask="forbidden",
649
+ dask="parallelized",
628
650
  dask_gufunc_kwargs={
629
651
  "output_sizes": {"radar_variables": len(RADAR_VARIABLES)},
652
+ "allow_rechunk": True,
630
653
  }, # lengths of the new output_core_dims dimensions.
631
654
  output_dtypes=["float64"],
632
655
  )
@@ -695,7 +718,7 @@ def get_empirical_radar_parameters(
695
718
  ds = filter_diameter_bins(ds=ds, maximum_diameter=diameter_max)
696
719
 
697
720
  # Define inputs
698
- da_drop_number_concentration = ds["drop_number_concentration"].compute()
721
+ da_drop_number_concentration = ds["drop_number_concentration"] # .compute()
699
722
 
700
723
  # Set all zeros drop number concentration to np.nan
701
724
  # --> Otherwise inf can appear in the output
@@ -724,11 +747,9 @@ def get_empirical_radar_parameters(
724
747
 
725
748
  # Define kwargs
726
749
  kwargs = {
727
- "output_dictionary": False,
728
750
  "bin_edges": bin_edges,
729
751
  "scatterer": scatterer,
730
752
  }
731
-
732
753
  # Loop over each PSD (not in parallel --> dask="forbidden")
733
754
  # - It costs much more to initiate the scatterer rather than looping over timesteps !
734
755
  da_radar = xr.apply_ufunc(
@@ -738,13 +759,12 @@ def get_empirical_radar_parameters(
738
759
  input_core_dims=[["diameter_bin_center"]],
739
760
  output_core_dims=[["radar_variables"]],
740
761
  vectorize=True,
741
- dask="forbidden",
762
+ dask="parallelized",
742
763
  dask_gufunc_kwargs={
743
764
  "output_sizes": {"radar_variables": len(RADAR_VARIABLES)},
744
765
  }, # lengths of the new output_core_dims dimensions.
745
766
  output_dtypes=["float64"],
746
767
  )
747
-
748
768
  # Finalize radar dataset (add name, coordinates)
749
769
  ds_radar = _finalize_radar_dataset(
750
770
  da_radar=da_radar,
@@ -922,11 +942,11 @@ def get_radar_parameters(
922
942
  # Model-based simulation
923
943
  if "disdrodb_psd_model" in ds.attrs:
924
944
  func = get_model_radar_parameters
925
- ds_subset = get_psd_parameters(ds).compute()
945
+ ds_subset = select_psd_parameters(ds)
926
946
  # Empirical PSD simulation
927
947
  else:
928
948
  func = get_empirical_radar_parameters
929
- ds_subset = ds[["drop_number_concentration"]].compute()
949
+ ds_subset = ds[["drop_number_concentration"]]
930
950
 
931
951
  # Define default frequencies if not specified
932
952
  if frequency is None: