disdrodb 0.1.3__py3-none-any.whl → 0.1.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. disdrodb/__init__.py +4 -0
  2. disdrodb/_version.py +2 -2
  3. disdrodb/api/checks.py +70 -47
  4. disdrodb/api/configs.py +0 -2
  5. disdrodb/api/create_directories.py +0 -2
  6. disdrodb/api/info.py +3 -3
  7. disdrodb/api/io.py +48 -8
  8. disdrodb/api/path.py +116 -133
  9. disdrodb/api/search.py +12 -3
  10. disdrodb/cli/disdrodb_create_summary.py +113 -0
  11. disdrodb/cli/disdrodb_create_summary_station.py +11 -1
  12. disdrodb/cli/disdrodb_run_l0a_station.py +1 -1
  13. disdrodb/cli/disdrodb_run_l0b_station.py +2 -2
  14. disdrodb/cli/disdrodb_run_l0c_station.py +2 -2
  15. disdrodb/cli/disdrodb_run_l1_station.py +2 -2
  16. disdrodb/cli/disdrodb_run_l2e_station.py +2 -2
  17. disdrodb/cli/disdrodb_run_l2m_station.py +2 -2
  18. disdrodb/constants.py +1 -1
  19. disdrodb/data_transfer/download_data.py +123 -7
  20. disdrodb/etc/products/L1/global.yaml +1 -1
  21. disdrodb/etc/products/L2E/5MIN.yaml +1 -0
  22. disdrodb/etc/products/L2E/global.yaml +1 -1
  23. disdrodb/etc/products/L2M/GAMMA_GS_ND_MAE.yaml +6 -0
  24. disdrodb/etc/products/L2M/GAMMA_ML.yaml +1 -1
  25. disdrodb/etc/products/L2M/LOGNORMAL_GS_LOG_ND_MAE.yaml +6 -0
  26. disdrodb/etc/products/L2M/LOGNORMAL_GS_ND_MAE.yaml +6 -0
  27. disdrodb/etc/products/L2M/LOGNORMAL_ML.yaml +8 -0
  28. disdrodb/etc/products/L2M/global.yaml +11 -3
  29. disdrodb/issue/writer.py +2 -0
  30. disdrodb/l0/check_configs.py +49 -16
  31. disdrodb/l0/configs/LPM/l0a_encodings.yml +2 -2
  32. disdrodb/l0/configs/LPM/l0b_cf_attrs.yml +2 -2
  33. disdrodb/l0/configs/LPM/l0b_encodings.yml +2 -2
  34. disdrodb/l0/configs/LPM/raw_data_format.yml +2 -2
  35. disdrodb/l0/configs/PWS100/l0b_encodings.yml +1 -0
  36. disdrodb/l0/configs/SWS250/bins_diameter.yml +108 -0
  37. disdrodb/l0/configs/SWS250/bins_velocity.yml +83 -0
  38. disdrodb/l0/configs/SWS250/l0a_encodings.yml +18 -0
  39. disdrodb/l0/configs/SWS250/l0b_cf_attrs.yml +72 -0
  40. disdrodb/l0/configs/SWS250/l0b_encodings.yml +155 -0
  41. disdrodb/l0/configs/SWS250/raw_data_format.yml +148 -0
  42. disdrodb/l0/l0a_processing.py +10 -5
  43. disdrodb/l0/l0b_nc_processing.py +10 -6
  44. disdrodb/l0/l0b_processing.py +92 -72
  45. disdrodb/l0/l0c_processing.py +369 -251
  46. disdrodb/l0/readers/LPM/ARM/ARM_LPM.py +8 -1
  47. disdrodb/l0/readers/LPM/AUSTRALIA/MELBOURNE_2007_LPM.py +2 -2
  48. disdrodb/l0/readers/LPM/BELGIUM/ULIEGE.py +256 -0
  49. disdrodb/l0/readers/LPM/BRAZIL/CHUVA_LPM.py +2 -2
  50. disdrodb/l0/readers/LPM/BRAZIL/GOAMAZON_LPM.py +2 -2
  51. disdrodb/l0/readers/LPM/GERMANY/DWD.py +491 -0
  52. disdrodb/l0/readers/LPM/ITALY/GID_LPM.py +2 -2
  53. disdrodb/l0/readers/LPM/ITALY/GID_LPM_W.py +2 -2
  54. disdrodb/l0/readers/LPM/KIT/CHWALA.py +2 -2
  55. disdrodb/l0/readers/LPM/SLOVENIA/ARSO.py +107 -12
  56. disdrodb/l0/readers/LPM/SLOVENIA/UL.py +3 -3
  57. disdrodb/l0/readers/LPM/SWITZERLAND/INNERERIZ_LPM.py +2 -2
  58. disdrodb/l0/readers/PARSIVEL/NCAR/VORTEX2_2010.py +5 -14
  59. disdrodb/l0/readers/PARSIVEL/NCAR/VORTEX2_2010_UF.py +5 -14
  60. disdrodb/l0/readers/PARSIVEL/SLOVENIA/UL.py +117 -8
  61. disdrodb/l0/readers/PARSIVEL2/ARM/ARM_PARSIVEL2.py +4 -0
  62. disdrodb/l0/readers/PARSIVEL2/BRAZIL/CHUVA_PARSIVEL2.py +10 -14
  63. disdrodb/l0/readers/PARSIVEL2/BRAZIL/GOAMAZON_PARSIVEL2.py +10 -14
  64. disdrodb/l0/readers/PARSIVEL2/CANADA/UQAM_NC.py +69 -0
  65. disdrodb/l0/readers/PARSIVEL2/DENMARK/DTU.py +8 -14
  66. disdrodb/l0/readers/PARSIVEL2/DENMARK/EROSION_raw.py +382 -0
  67. disdrodb/l0/readers/PARSIVEL2/FINLAND/FMI_PARSIVEL2.py +4 -0
  68. disdrodb/l0/readers/PARSIVEL2/FRANCE/OSUG.py +1 -1
  69. disdrodb/l0/readers/PARSIVEL2/GREECE/NOA.py +127 -0
  70. disdrodb/l0/readers/PARSIVEL2/ITALY/HYDROX.py +239 -0
  71. disdrodb/l0/readers/PARSIVEL2/MPI/BCO_PARSIVEL2.py +136 -0
  72. disdrodb/l0/readers/PARSIVEL2/MPI/BOWTIE.py +220 -0
  73. disdrodb/l0/readers/PARSIVEL2/NASA/LPVEX.py +109 -0
  74. disdrodb/l0/readers/PARSIVEL2/NCAR/FARM_PARSIVEL2.py +5 -11
  75. disdrodb/l0/readers/PARSIVEL2/NCAR/PERILS_MIPS.py +4 -17
  76. disdrodb/l0/readers/PARSIVEL2/NCAR/RELAMPAGO_PARSIVEL2.py +5 -14
  77. disdrodb/l0/readers/PARSIVEL2/NCAR/SNOWIE_PJ.py +10 -13
  78. disdrodb/l0/readers/PARSIVEL2/NCAR/SNOWIE_SB.py +10 -13
  79. disdrodb/l0/readers/PARSIVEL2/NETHERLANDS/DELFT_NC.py +3 -0
  80. disdrodb/l0/readers/PARSIVEL2/PHILIPPINES/PANGASA.py +232 -0
  81. disdrodb/l0/readers/PARSIVEL2/SPAIN/CENER.py +6 -18
  82. disdrodb/l0/readers/PARSIVEL2/SPAIN/GRANADA.py +120 -0
  83. disdrodb/l0/readers/PARSIVEL2/USA/C3WE.py +7 -25
  84. disdrodb/l0/readers/PWS100/AUSTRIA/HOAL.py +321 -0
  85. disdrodb/l0/readers/SW250/BELGIUM/KMI.py +239 -0
  86. disdrodb/l1/beard_model.py +31 -129
  87. disdrodb/l1/fall_velocity.py +156 -57
  88. disdrodb/l1/filters.py +25 -28
  89. disdrodb/l1/processing.py +12 -14
  90. disdrodb/l1_env/routines.py +46 -17
  91. disdrodb/l2/empirical_dsd.py +6 -0
  92. disdrodb/l2/processing.py +3 -3
  93. disdrodb/metadata/checks.py +132 -125
  94. disdrodb/metadata/geolocation.py +0 -2
  95. disdrodb/psd/fitting.py +180 -210
  96. disdrodb/psd/models.py +1 -1
  97. disdrodb/routines/__init__.py +54 -0
  98. disdrodb/{l0/routines.py → routines/l0.py} +288 -418
  99. disdrodb/{l1/routines.py → routines/l1.py} +60 -92
  100. disdrodb/{l2/routines.py → routines/l2.py} +284 -485
  101. disdrodb/{routines.py → routines/wrappers.py} +100 -7
  102. disdrodb/scattering/axis_ratio.py +95 -85
  103. disdrodb/scattering/permittivity.py +24 -0
  104. disdrodb/scattering/routines.py +56 -36
  105. disdrodb/summary/routines.py +147 -45
  106. disdrodb/utils/archiving.py +434 -0
  107. disdrodb/utils/attrs.py +2 -0
  108. disdrodb/utils/cli.py +5 -5
  109. disdrodb/utils/dask.py +62 -1
  110. disdrodb/utils/decorators.py +31 -0
  111. disdrodb/utils/encoding.py +10 -1
  112. disdrodb/{l2 → utils}/event.py +1 -66
  113. disdrodb/utils/logger.py +1 -1
  114. disdrodb/utils/manipulations.py +22 -12
  115. disdrodb/utils/routines.py +166 -0
  116. disdrodb/utils/time.py +5 -293
  117. disdrodb/utils/xarray.py +3 -0
  118. disdrodb/viz/plots.py +109 -15
  119. {disdrodb-0.1.3.dist-info → disdrodb-0.1.5.dist-info}/METADATA +3 -2
  120. {disdrodb-0.1.3.dist-info → disdrodb-0.1.5.dist-info}/RECORD +124 -96
  121. {disdrodb-0.1.3.dist-info → disdrodb-0.1.5.dist-info}/entry_points.txt +1 -0
  122. {disdrodb-0.1.3.dist-info → disdrodb-0.1.5.dist-info}/WHEEL +0 -0
  123. {disdrodb-0.1.3.dist-info → disdrodb-0.1.5.dist-info}/licenses/LICENSE +0 -0
  124. {disdrodb-0.1.3.dist-info → disdrodb-0.1.5.dist-info}/top_level.txt +0 -0
disdrodb/psd/fitting.py CHANGED
@@ -20,9 +20,10 @@ import scipy.stats as ss
20
20
  import xarray as xr
21
21
  from scipy.integrate import quad
22
22
  from scipy.optimize import minimize
23
- from scipy.special import gamma, gammainc, gammaln # Regularized lower incomplete gamma function
23
+ from scipy.special import gamma, gammaln # Regularized lower incomplete gamma function
24
24
 
25
25
  from disdrodb.constants import DIAMETER_DIMENSION
26
+ from disdrodb.l1.fall_velocity import get_raindrop_fall_velocity_from_ds
26
27
  from disdrodb.l2.empirical_dsd import (
27
28
  get_median_volume_drop_diameter,
28
29
  get_moment,
@@ -227,13 +228,13 @@ def get_expected_probabilities(params, cdf_func, pdf_func, bin_edges, probabilit
227
228
  def get_adjusted_nt(cdf, params, Nt, bin_edges):
228
229
  """Adjust Nt for the proportion of missing drops. See Johnson's et al., 2013 Eqs. 3 and 4."""
229
230
  # Estimate proportion of missing drops (Johnson's 2011 Eqs. 3)
230
- # --> Alternative: p = 1 - np.sum(pdf(diameter, params)* diameter_bin_width) # [-]
231
+ # --> Alternative:
232
+ # - p = 1 - np.sum(pdf(diameter, params)* diameter_bin_width) # [-]
233
+ # - p = 1 - np.sum((Lambda ** (mu + 1)) / gamma(mu + 1) * D**mu * np.exp(-Lambda * D) * diameter_bin_width) # [-]
231
234
  p = 1 - np.diff(cdf([bin_edges[0], bin_edges[-1]], params)).item() # [-]
232
- # Adjusts Nt for the proportion of drops not obs
233
- # p = np.clip(p, 0, 1 - 1e-12)
234
- if np.isclose(p, 1, atol=1e-12):
235
- return np.nan
236
- return Nt / (1 - p) # [m-3]
235
+ # Adjusts Nt for the proportion of missing drops
236
+ nt_adj = np.nan if np.isclose(p, 1, atol=1e-12) else Nt / (1 - p) # [m-3]
237
+ return nt_adj
237
238
 
238
239
 
239
240
  def compute_negative_log_likelihood(
@@ -606,7 +607,7 @@ def estimate_gamma_parameters(
606
607
 
607
608
  """
608
609
  # Define initial guess for parameters
609
- a = mu + 1 # (mu = a-1, a = mu+1)
610
+ a = mu + 1 # (mu = a-1, a = mu+1) (a > 0 --> mu=-1)
610
611
  scale = 1 / Lambda
611
612
  initial_params = [a, scale]
612
613
 
@@ -669,7 +670,7 @@ def estimate_gamma_parameters(
669
670
 
670
671
  # Compute N0
671
672
  # - Use logarithmic computations to prevent overflow
672
- # - N0 = Nt * Lambda ** (mu + 1) / gamma(mu + 1)
673
+ # - N0 = Nt * Lambda ** (mu + 1) / gamma(mu + 1) # [m-3 * mm^(-mu-1)]
673
674
  with suppress_warnings():
674
675
  log_N0 = np.log(Nt) + (mu + 1) * np.log(Lambda) - gammaln(mu + 1)
675
676
  N0 = np.exp(log_N0)
@@ -785,6 +786,7 @@ def get_gamma_parameters(
785
786
  Likelihood function to use for fitting. The default value is ``multinomial``.
786
787
  truncated_likelihood : bool, optional
787
788
  Whether to use truncated likelihood. The default value is ``True``.
789
+ See Johnson et al., 2011 and 2011 for more information.
788
790
  optimizer : str, optional
789
791
  Optimization method to use. The default value is ``Nelder-Mead``.
790
792
 
@@ -802,6 +804,15 @@ def get_gamma_parameters(
802
804
  The function uses `xr.apply_ufunc` to fit the lognormal distribution parameters
803
805
  in parallel, leveraging Dask for parallel computation.
804
806
 
807
+ References
808
+ ----------
809
+ Johnson, R. W., D. V. Kliche, and P. L. Smith, 2011: Comparison of Estimators for Parameters of Gamma Distributions
810
+ with Left-Truncated Samples. J. Appl. Meteor. Climatol., 50, 296-310, https://doi.org/10.1175/2010JAMC2478.1
811
+
812
+ Johnson, R.W., Kliche, D., & Smith, P.L. (2010).
813
+ Maximum likelihood estimation of gamma parameters for coarsely binned and truncated raindrop size data.
814
+ Quarterly Journal of the Royal Meteorological Society, 140. DOI:10.1002/qj.2209
815
+
805
816
  """
806
817
  # Define inputs
807
818
  counts = ds["drop_number_concentration"] * ds["diameter_bin_width"]
@@ -981,7 +992,7 @@ def get_exponential_parameters(
981
992
 
982
993
  """
983
994
  # Define inputs
984
- counts = ds["drop_number_concentration"] * ds["diameter_bin_width"]
995
+ counts = ds["drop_number_concentration"] * ds["diameter_bin_width"] # mm-1 m-3 --> m-3
985
996
  diameter_breaks = get_diameter_bin_edges(ds)
986
997
 
987
998
  # Define initial parameters (Lambda)
@@ -1022,163 +1033,6 @@ def get_exponential_parameters(
1022
1033
  return ds_params
1023
1034
 
1024
1035
 
1025
- ####-------------------------------------------------------------------------------------------------------------------.
1026
-
1027
-
1028
- def _estimate_gamma_parameters_johnson(
1029
- drop_number_concentration,
1030
- diameter,
1031
- diameter_breaks,
1032
- output_dictionary=True,
1033
- method="Nelder-Mead",
1034
- mu=0.5,
1035
- Lambda=3,
1036
- **kwargs,
1037
- ):
1038
- """Deprecated Maximum likelihood estimation of Gamma model.
1039
-
1040
- N(D) = N_t * lambda**(mu+1) / gamma(mu+1) D**mu exp(-lambda*D)
1041
-
1042
- Args:
1043
- spectra: The DSD for which to find parameters [mm-1 m-3].
1044
- widths: Class widths for each DSD bin [mm].
1045
- diams: Class-centre diameters for each DSD bin [mm].
1046
- mu: Initial value for shape parameter mu [-].
1047
- lambda_param: Initial value for slope parameter lambda [mm^-1].
1048
- kwargs: Extra arguments for the optimization process.
1049
-
1050
- Returns
1051
- -------
1052
- Dictionary with estimated mu, lambda, and N0.
1053
- mu (shape) N0 (scale) lambda(slope)
1054
-
1055
- Notes
1056
- -----
1057
- The last bin counts are not accounted in the fitting procedure !
1058
-
1059
- References
1060
- ----------
1061
- Johnson, R. W., D. V. Kliche, and P. L. Smith, 2011: Comparison of Estimators for Parameters of Gamma Distributions
1062
- with Left-Truncated Samples. J. Appl. Meteor. Climatol., 50, 296-310, https://doi.org/10.1175/2010JAMC2478.1
1063
-
1064
- Johnson, R.W., Kliche, D., & Smith, P.L. (2010).
1065
- Maximum likelihood estimation of gamma parameters for coarsely binned and truncated raindrop size data.
1066
- Quarterly Journal of the Royal Meteorological Society, 140. DOI:10.1002/qj.2209
1067
-
1068
- """
1069
- # Initialize bad results
1070
- if output_dictionary:
1071
- null_output = {"mu": np.nan, "lambda": np.nan, "N0": np.nan}
1072
- else:
1073
- null_output = np.array([np.nan, np.nan, np.nan])
1074
-
1075
- # Initialize parameters
1076
- # --> Ideally with method of moments estimate
1077
- # --> See equation 8 of Johnson's 2013
1078
- x0 = [mu, Lambda]
1079
-
1080
- # Compute diameter_bin_width
1081
- diameter_bin_width = np.diff(diameter_breaks)
1082
-
1083
- # Convert drop_number_concentration from mm-1 m-3 to m-3.
1084
- spectra = np.asarray(drop_number_concentration) * diameter_bin_width
1085
-
1086
- # Define cost function
1087
- # - Parameter to be optimized on first positions
1088
- def _cost_function(parameters, spectra, diameter_breaks):
1089
- # Assume spectra to be in unit [m-3] (drop_number_concentration*diameter_bin_width) !
1090
- mu, Lambda = parameters
1091
- # Precompute gamma integrals between various diameter bins
1092
- # - gamminc(mu+1) already divides the integral by gamma(mu+1) !
1093
- pgamma_d = gammainc(mu + 1, Lambda * diameter_breaks)
1094
- # Compute probability with interval
1095
- delta_pgamma_bins = pgamma_d[1:] - pgamma_d[:-1]
1096
- # Compute normalization over interval
1097
- denominator = pgamma_d[-1] - pgamma_d[0]
1098
- # Compute cost function
1099
- # a = mu - 1, x = lambda
1100
- if mu > -1 and Lambda > 0:
1101
- cost = np.sum(-spectra * np.log(delta_pgamma_bins / denominator))
1102
- return cost
1103
- return np.inf
1104
-
1105
- # Minimize the cost function
1106
- with suppress_warnings():
1107
- bounds = [(0, None), (0, None)] # Force mu and lambda to be non-negative
1108
- res = minimize(
1109
- _cost_function,
1110
- x0=x0,
1111
- args=(spectra, diameter_breaks),
1112
- method=method,
1113
- bounds=bounds,
1114
- **kwargs,
1115
- )
1116
-
1117
- # Check if the fit had success
1118
- if not res.success:
1119
- return null_output
1120
-
1121
- # Extract parameters
1122
- mu = res.x[0] # [-]
1123
- Lambda = res.x[1] # [mm-1]
1124
-
1125
- # Estimate tilde_N_T using the total drop concentration
1126
- tilde_N_T = np.sum(drop_number_concentration * diameter_bin_width) # [m-3]
1127
-
1128
- # Estimate proportion of missing drops (Johnson's 2011 Eqs. 3)
1129
- with suppress_warnings():
1130
- D = diameter
1131
- p = 1 - np.sum((Lambda ** (mu + 1)) / gamma(mu + 1) * D**mu * np.exp(-Lambda * D) * diameter_bin_width) # [-]
1132
-
1133
- # Convert tilde_N_T to N_T using Johnson's 2013 Eqs. 3 and 4.
1134
- # - Adjusts for the proportion of drops not obs
1135
- N_T = tilde_N_T / (1 - p) # [m-3]
1136
-
1137
- # Compute N0
1138
- N0 = N_T * (Lambda ** (mu + 1)) / gamma(mu + 1) # [m-3 * mm^(-mu-1)]
1139
-
1140
- # Compute Dm
1141
- # Dm = (mu + 4)/ Lambda
1142
-
1143
- # Compute Nw
1144
- # Nw = N0* D^mu / f(mu) , with f(mu of the Normalized PSD)
1145
-
1146
- # Define output
1147
- output = {"mu": mu, "Lambda": Lambda, "N0": N0} if output_dictionary else np.array([mu, Lambda, N0])
1148
- return output
1149
-
1150
-
1151
- def get_gamma_parameters_johnson2014(ds, method="Nelder-Mead"):
1152
- """Deprecated model. See Gamma Model with truncated_likelihood and 'pdf'."""
1153
- drop_number_concentration = ds["drop_number_concentration"]
1154
- diameter = ds["diameter_bin_center"]
1155
- diameter_breaks = get_diameter_bin_edges(ds)
1156
-
1157
- # Define kwargs
1158
- kwargs = {
1159
- "output_dictionary": False,
1160
- "diameter_breaks": diameter_breaks,
1161
- "method": method,
1162
- }
1163
- da_params = xr.apply_ufunc(
1164
- _estimate_gamma_parameters_johnson,
1165
- drop_number_concentration,
1166
- diameter,
1167
- # diameter_bin_width,
1168
- kwargs=kwargs,
1169
- input_core_dims=[[DIAMETER_DIMENSION], [DIAMETER_DIMENSION]], # [DIAMETER_DIMENSION],
1170
- output_core_dims=[["parameters"]],
1171
- vectorize=True,
1172
- )
1173
-
1174
- # Add parameters coordinates
1175
- da_params = da_params.assign_coords({"parameters": ["mu", "Lambda", "N0"]})
1176
-
1177
- # Convert to skill Dataset
1178
- ds_params = da_params.to_dataset(dim="parameters")
1179
- return ds_params
1180
-
1181
-
1182
1036
  ####-----------------------------------------------------------------------------------------.
1183
1037
  #### Grid Search (GS)
1184
1038
 
@@ -1202,24 +1056,60 @@ def _compute_z(ND, D, dD):
1202
1056
  return Z
1203
1057
 
1204
1058
 
1059
+ def _compute_target_variable_error(target, ND_obs, ND_preds, D, dD, V):
1060
+ if target == "Z":
1061
+ errors = np.abs(_compute_z(ND_obs, D, dD) - _compute_z(ND_preds, D, dD))
1062
+ elif target == "R":
1063
+ errors = np.abs(_compute_rain_rate(ND_obs, D, dD, V) - _compute_rain_rate(ND_preds, D, dD, V))
1064
+ else: # if target == "LWC":
1065
+ errors = np.abs(_compute_lwc(ND_obs, D, dD) - _compute_lwc(ND_preds, D, dD))
1066
+ return errors
1067
+
1068
+
1205
1069
  def _compute_cost_function(ND_obs, ND_preds, D, dD, V, target, transformation, error_order):
1206
1070
  # Assume ND_obs of shape (D bins) and ND_preds of shape (# params, D bins)
1207
1071
  if target == "ND":
1208
1072
  if transformation == "identity":
1209
1073
  errors = np.mean(np.abs(ND_obs[None, :] - ND_preds) ** error_order, axis=1)
1074
+ return errors
1210
1075
  if transformation == "log":
1211
1076
  errors = np.mean(np.abs(np.log(ND_obs[None, :] + 1) - np.log(ND_preds + 1)) ** error_order, axis=1)
1212
- if transformation == "np.sqrt":
1077
+ return errors
1078
+ if transformation == "sqrt":
1213
1079
  errors = np.mean(np.abs(np.sqrt(ND_obs[None, :]) - np.sqrt(ND_preds)) ** error_order, axis=1)
1214
- elif target == "Z":
1215
- errors = np.abs(_compute_z(ND_obs, D, dD) - _compute_z(ND_preds, D, dD))
1216
- elif target == "R":
1217
- errors = np.abs(_compute_rain_rate(ND_obs, D, dD, V) - _compute_rain_rate(ND_preds, D, dD, V))
1218
- elif target == "LWC":
1219
- errors = np.abs(_compute_lwc(ND_obs, D, dD) - _compute_lwc(ND_preds, D, dD))
1220
- else:
1221
- raise ValueError("Invalid target")
1222
- return errors
1080
+ return errors
1081
+ # if target in ["Z", "R", "LWC"]:
1082
+ return _compute_target_variable_error(target, ND_obs, ND_preds, D, dD, V)
1083
+
1084
+
1085
+ def define_param_range(center, step, bounds, factor=2, refinement=20):
1086
+ """
1087
+ Create a refined parameter search range around a center value, constrained to bounds.
1088
+
1089
+ Parameters
1090
+ ----------
1091
+ center : float
1092
+ Center of the range (e.g., current best estimate).
1093
+ step : float
1094
+ Coarse step size used in the first search.
1095
+ bounds : tuple of (float, float)
1096
+ Lower and upper bounds (can include -np.inf, np.inf).
1097
+ factor : float, optional
1098
+ How wide the refined range extends from the center (in multiples of step).
1099
+ Default = 2.
1100
+ refinement : int, optional
1101
+ Factor to refine the step size (smaller step = finer grid).
1102
+ Default = 20.
1103
+
1104
+ Returns
1105
+ -------
1106
+ np.ndarray
1107
+ Array of values constrained to bounds.
1108
+ """
1109
+ lower = max(center - factor * step, bounds[0])
1110
+ upper = min(center + factor * step, bounds[1])
1111
+ new_step = step / refinement
1112
+ return np.arange(lower, upper, new_step)
1223
1113
 
1224
1114
 
1225
1115
  def apply_exponential_gs(
@@ -1255,9 +1145,15 @@ def apply_exponential_gs(
1255
1145
  transformation=transformation,
1256
1146
  error_order=error_order,
1257
1147
  )
1148
+ # Replace inf with NaN
1149
+ errors[~np.isfinite(errors)] = np.nan
1150
+
1151
+ # If all invalid, return NaN parameters
1152
+ if np.all(np.isnan(errors)):
1153
+ return np.array([np.nan, np.nan])
1258
1154
 
1259
- # Identify best parameter set
1260
- best_index = np.argmin(errors)
1155
+ # Otherwise, choose the best index
1156
+ best_index = np.nanargmin(errors)
1261
1157
  return np.array([N0_arr[best_index].item(), lambda_arr[best_index].item()])
1262
1158
 
1263
1159
 
@@ -1286,8 +1182,15 @@ def _apply_gamma_gs(mu_values, lambda_values, Nt, ND_obs, D, dD, V, target, tran
1286
1182
  error_order=error_order,
1287
1183
  )
1288
1184
 
1289
- # Best parameter
1290
- best_index = np.argmin(errors)
1185
+ # Replace inf with NaN
1186
+ errors[~np.isfinite(errors)] = np.nan
1187
+
1188
+ # If all invalid, return NaN parameters
1189
+ if np.all(np.isnan(errors)):
1190
+ return np.array([np.nan, np.nan, np.nan])
1191
+
1192
+ # Otherwise, choose the best index
1193
+ best_index = np.nanargmin(errors)
1291
1194
  return N0[best_index].item(), mu_arr[best_index].item(), lambda_arr[best_index].item()
1292
1195
 
1293
1196
 
@@ -1304,10 +1207,14 @@ def apply_gamma_gs(
1304
1207
  error_order,
1305
1208
  ):
1306
1209
  """Estimate GammaPSD model parameters using Grid Search."""
1210
+ # Define parameters bounds
1211
+ mu_bounds = (-1, 40)
1212
+ lambda_bounds = (0, 60)
1213
+
1307
1214
  # Define initial set of parameters
1308
- mu_step = 0.5
1215
+ mu_step = 0.25
1309
1216
  lambda_step = 0.5
1310
- mu_values = np.arange(0.01, 20, step=mu_step)
1217
+ mu_values = np.arange(0, 40, step=mu_step)
1311
1218
  lambda_values = np.arange(0, 60, step=lambda_step)
1312
1219
 
1313
1220
  # First round of GS
@@ -1323,10 +1230,13 @@ def apply_gamma_gs(
1323
1230
  transformation=transformation,
1324
1231
  error_order=error_order,
1325
1232
  )
1233
+ if np.isnan(N0): # if np.nan, return immediately
1234
+ return np.array([N0, mu, Lambda])
1326
1235
 
1327
1236
  # Second round of GS
1328
- mu_values = np.arange(mu - mu_step * 2, mu + mu_step * 2, step=mu_step / 20)
1329
- lambda_values = np.arange(Lambda - lambda_step * 2, Lambda + lambda_step * 2, step=lambda_step / 20)
1237
+ mu_values = define_param_range(mu, mu_step, bounds=mu_bounds)
1238
+ lambda_values = define_param_range(Lambda, lambda_step, bounds=lambda_bounds)
1239
+
1330
1240
  N0, mu, Lambda = _apply_gamma_gs(
1331
1241
  mu_values=mu_values,
1332
1242
  lambda_values=lambda_values,
@@ -1367,8 +1277,15 @@ def _apply_lognormal_gs(mu_values, sigma_values, Nt, ND_obs, D, dD, V, target, t
1367
1277
  error_order=error_order,
1368
1278
  )
1369
1279
 
1370
- # Best parameter
1371
- best_index = np.argmin(errors)
1280
+ # Replace inf with NaN
1281
+ errors[~np.isfinite(errors)] = np.nan
1282
+
1283
+ # If all invalid, return NaN parameters
1284
+ if np.all(np.isnan(errors)):
1285
+ return np.array([np.nan, np.nan, np.nan])
1286
+
1287
+ # Otherwise, choose the best index
1288
+ best_index = np.nanargmin(errors)
1372
1289
  return Nt, mu_arr[best_index].item(), sigma_arr[best_index].item()
1373
1290
 
1374
1291
 
@@ -1385,11 +1302,19 @@ def apply_lognormal_gs(
1385
1302
  error_order,
1386
1303
  ):
1387
1304
  """Estimate LognormalPSD model parameters using Grid Search."""
1305
+ # Define parameters bounds
1306
+ sigma_bounds = (0, np.inf) # > 0
1307
+ scale_bounds = (0, np.inf) # > 0
1308
+ # mu_bounds = (- np.inf, np.inf) # mu = np.log(scale)
1309
+
1388
1310
  # Define initial set of parameters
1389
- mu_step = 0.5
1390
- sigma_step = 0.5
1391
- mu_values = np.arange(0.01, 20, step=mu_step) # TODO: define realistic values
1392
- sigma_values = np.arange(0, 20, step=sigma_step) # TODO: define realistic values
1311
+ # --> Typically sigma between 0 and 3
1312
+ # --> Typically mu between -2 and 2
1313
+ scale_step = 0.2
1314
+ sigma_step = 0.2
1315
+ scale_values = np.arange(scale_step, 20, step=scale_step)
1316
+ mu_values = np.log(scale_values)
1317
+ sigma_values = np.arange(0, 3, step=sigma_step)
1393
1318
 
1394
1319
  # First round of GS
1395
1320
  Nt, mu, sigma = _apply_lognormal_gs(
@@ -1404,10 +1329,14 @@ def apply_lognormal_gs(
1404
1329
  transformation=transformation,
1405
1330
  error_order=error_order,
1406
1331
  )
1332
+ if np.isnan(mu): # if np.nan, return immediately
1333
+ return np.array([Nt, mu, sigma])
1407
1334
 
1408
1335
  # Second round of GS
1409
- mu_values = np.arange(mu - mu_step * 2, mu + mu_step * 2, step=mu_step / 20)
1410
- sigma_values = np.arange(sigma - sigma_step * 2, sigma + sigma_step * 2, step=sigma_step / 20)
1336
+ sigma_values = define_param_range(sigma, sigma_step, bounds=sigma_bounds)
1337
+ scale_values = define_param_range(np.exp(mu), scale_step, bounds=scale_bounds)
1338
+ with suppress_warnings():
1339
+ mu_values = np.log(scale_values)
1411
1340
  Nt, mu, sigma = _apply_lognormal_gs(
1412
1341
  mu_values=mu_values,
1413
1342
  sigma_values=sigma_values,
@@ -1439,7 +1368,7 @@ def apply_normalized_gamma_gs(
1439
1368
  ):
1440
1369
  """Estimate NormalizedGammaPSD model parameters using Grid Search."""
1441
1370
  # Define set of mu values
1442
- mu_arr = np.arange(0.01, 20, step=0.01)
1371
+ mu_arr = np.arange(-4, 30, step=0.01)
1443
1372
 
1444
1373
  # Perform grid search
1445
1374
  with suppress_warnings():
@@ -1458,8 +1387,16 @@ def apply_normalized_gamma_gs(
1458
1387
  error_order=error_order,
1459
1388
  )
1460
1389
 
1461
- # Identify best parameter set
1462
- mu = mu_arr[np.argmin(errors)]
1390
+ # Replace inf with NaN
1391
+ errors[~np.isfinite(errors)] = np.nan
1392
+
1393
+ # If all invalid, return NaN parameters
1394
+ if np.all(np.isnan(errors)):
1395
+ return np.array([np.nan, np.nan, np.nan])
1396
+
1397
+ # Otherwise, choose the best index
1398
+ best_index = np.nanargmin(errors)
1399
+ mu = mu_arr[best_index]
1463
1400
  return np.array([Nw, mu, D50])
1464
1401
 
1465
1402
 
@@ -1733,6 +1670,8 @@ def get_exponential_parameters_Zhang2008(moment_l, moment_m, l, m): # noqa: E74
1733
1670
  Meteor. Climatol.,
1734
1671
  https://doi.org/10.1175/2008JAMC1876.1
1735
1672
  """
1673
+ if l == m:
1674
+ raise ValueError("Equal l and m moment orders are not allowed.")
1736
1675
  num = moment_l * gamma(m + 1)
1737
1676
  den = moment_m * gamma(l + 1)
1738
1677
  Lambda = np.power(num / den, (1 / (m - l)))
@@ -1757,21 +1696,21 @@ def get_exponential_parameters_M34(moment_3, moment_4):
1757
1696
  return N0, Lambda
1758
1697
 
1759
1698
 
1760
- def get_gamma_parameters_M012(M0, M1, M2):
1761
- """Compute gamma distribution parameters following Cao et al., 2009.
1699
+ # def get_gamma_parameters_M012(M0, M1, M2):
1700
+ # """Compute gamma distribution parameters following Cao et al., 2009.
1762
1701
 
1763
- References
1764
- ----------
1765
- Cao, Q., and G. Zhang, 2009:
1766
- Errors in Estimating Raindrop Size Distribution Parameters Employing Disdrometer and Simulated Raindrop Spectra.
1767
- J. Appl. Meteor. Climatol., 48, 406-425, https://doi.org/10.1175/2008JAMC2026.1.
1768
- """
1769
- # TODO: really bad results. check formula !
1770
- G = M1**3 / M0 / M2
1771
- mu = 1 / (1 - G) - 2
1772
- Lambda = M0 / M1 * (mu + 1)
1773
- N0 = Lambda ** (mu + 1) * M0 / gamma(mu + 1)
1774
- return N0, mu, Lambda
1702
+ # References
1703
+ # ----------
1704
+ # Cao, Q., and G. Zhang, 2009:
1705
+ # Errors in Estimating Raindrop Size Distribution Parameters Employing Disdrometer and Simulated Raindrop Spectra.
1706
+ # J. Appl. Meteor. Climatol., 48, 406-425, https://doi.org/10.1175/2008JAMC2026.1.
1707
+ # """
1708
+ # # TODO: really bad results. check formula !
1709
+ # G = M1**3 / M0 / M2
1710
+ # mu = 1 / (1 - G) - 2
1711
+ # Lambda = M0 / M1 * (mu + 1)
1712
+ # N0 = Lambda ** (mu + 1) * M0 / gamma(mu + 1)
1713
+ # return N0, mu, Lambda
1775
1714
 
1776
1715
 
1777
1716
  def get_gamma_parameters_M234(M2, M3, M4):
@@ -2415,6 +2354,10 @@ def get_gs_parameters(ds, psd_model, target="ND", transformation="log", error_or
2415
2354
  # Check valid transformation
2416
2355
  transformation = check_transformation(transformation)
2417
2356
 
2357
+ # Check fall velocity is available if target R
2358
+ if "fall_velocity" not in ds:
2359
+ ds["fall_velocity"] = get_raindrop_fall_velocity_from_ds(ds)
2360
+
2418
2361
  # Retrieve estimation function
2419
2362
  func = OPTIMIZATION_ROUTINES_DICT["GS"][psd_model]
2420
2363
 
@@ -2437,6 +2380,25 @@ def get_gs_parameters(ds, psd_model, target="ND", transformation="log", error_or
2437
2380
  return ds_params
2438
2381
 
2439
2382
 
2383
+ def sanitize_drop_number_concentration(drop_number_concentration):
2384
+ """Sanitize drop number concentration array.
2385
+
2386
+ If N(D) is all zero or contain not finite values, set everything to np.nan
2387
+ """
2388
+ # Condition 1: all zeros along diameter_bin_center
2389
+ all_zero = (drop_number_concentration == 0).all(dim="diameter_bin_center")
2390
+
2391
+ # Condition 2: any non-finite along diameter_bin_center
2392
+ any_nonfinite = (~np.isfinite(drop_number_concentration)).any(dim="diameter_bin_center")
2393
+
2394
+ # Combine conditions
2395
+ invalid = all_zero | any_nonfinite
2396
+
2397
+ # Replace entire profile with NaN where invalid
2398
+ drop_number_concentration = drop_number_concentration.where(~invalid, np.nan)
2399
+ return drop_number_concentration
2400
+
2401
+
2440
2402
  def estimate_model_parameters(
2441
2403
  ds,
2442
2404
  psd_model,
@@ -2444,10 +2406,18 @@ def estimate_model_parameters(
2444
2406
  optimization_kwargs=None,
2445
2407
  ):
2446
2408
  """Routine to estimate PSD model parameters."""
2409
+ # Check inputs arguments
2447
2410
  optimization_kwargs = {} if optimization_kwargs is None else optimization_kwargs
2448
2411
  optimization = check_optimization(optimization)
2449
2412
  check_optimization_kwargs(optimization_kwargs=optimization_kwargs, optimization=optimization, psd_model=psd_model)
2450
2413
 
2414
+ # Check N(D)
2415
+ # --> If all 0, set to np.nan
2416
+ # --> If any is not finite --> set to np.nan
2417
+ if "drop_number_concentration" not in ds:
2418
+ raise ValueError("'drop_number_concentration' variable not present in input xarray.Dataset.")
2419
+ ds["drop_number_concentration"] = sanitize_drop_number_concentration(ds["drop_number_concentration"])
2420
+
2451
2421
  # Define function
2452
2422
  dict_func = {
2453
2423
  "ML": get_ml_parameters,
disdrodb/psd/models.py CHANGED
@@ -93,7 +93,7 @@ def get_psd_model_formula(psd_model):
93
93
  return PSD_MODELS_DICT[psd_model].formula
94
94
 
95
95
 
96
- def create_psd(psd_model, parameters): # TODO: check name around
96
+ def create_psd(psd_model, parameters):
97
97
  """Define a PSD from a dictionary or xr.Dataset of parameters."""
98
98
  psd_class = get_psd_model(psd_model)
99
99
  psd = psd_class.from_parameters(parameters)
@@ -0,0 +1,54 @@
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
+ """DISDRODB L0 software."""
18
+ from disdrodb.routines.wrappers import (
19
+ create_summary,
20
+ create_summary_station,
21
+ run_l0,
22
+ run_l0_station,
23
+ run_l0a,
24
+ run_l0a_station,
25
+ run_l0b,
26
+ run_l0b_station,
27
+ run_l0c,
28
+ run_l0c_station,
29
+ run_l1,
30
+ run_l1_station,
31
+ run_l2e,
32
+ run_l2e_station,
33
+ run_l2m,
34
+ run_l2m_station,
35
+ )
36
+
37
+ __all__ = [
38
+ "create_summary",
39
+ "create_summary_station",
40
+ "run_l0",
41
+ "run_l0_station",
42
+ "run_l0a",
43
+ "run_l0a_station",
44
+ "run_l0b",
45
+ "run_l0b_station",
46
+ "run_l0c",
47
+ "run_l0c_station",
48
+ "run_l1",
49
+ "run_l1_station",
50
+ "run_l2e",
51
+ "run_l2e_station",
52
+ "run_l2m",
53
+ "run_l2m_station",
54
+ ]