disdrodb 0.1.2__py3-none-any.whl → 0.1.3__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 (123) hide show
  1. disdrodb/__init__.py +64 -34
  2. disdrodb/_config.py +5 -4
  3. disdrodb/_version.py +16 -3
  4. disdrodb/accessor/__init__.py +20 -0
  5. disdrodb/accessor/methods.py +125 -0
  6. disdrodb/api/checks.py +139 -9
  7. disdrodb/api/configs.py +4 -2
  8. disdrodb/api/info.py +10 -10
  9. disdrodb/api/io.py +237 -18
  10. disdrodb/api/path.py +81 -75
  11. disdrodb/api/search.py +6 -6
  12. disdrodb/cli/disdrodb_create_summary_station.py +91 -0
  13. disdrodb/cli/disdrodb_run_l0.py +1 -1
  14. disdrodb/cli/disdrodb_run_l0_station.py +1 -1
  15. disdrodb/cli/disdrodb_run_l0b.py +1 -1
  16. disdrodb/cli/disdrodb_run_l0b_station.py +1 -1
  17. disdrodb/cli/disdrodb_run_l0c.py +1 -1
  18. disdrodb/cli/disdrodb_run_l0c_station.py +1 -1
  19. disdrodb/cli/disdrodb_run_l2e_station.py +1 -1
  20. disdrodb/configs.py +149 -4
  21. disdrodb/constants.py +61 -0
  22. disdrodb/data_transfer/download_data.py +5 -5
  23. disdrodb/etc/configs/attributes.yaml +339 -0
  24. disdrodb/etc/configs/encodings.yaml +473 -0
  25. disdrodb/etc/products/L1/global.yaml +13 -0
  26. disdrodb/etc/products/L2E/10MIN.yaml +12 -0
  27. disdrodb/etc/products/L2E/1MIN.yaml +1 -0
  28. disdrodb/etc/products/L2E/global.yaml +22 -0
  29. disdrodb/etc/products/L2M/10MIN.yaml +12 -0
  30. disdrodb/etc/products/L2M/GAMMA_ML.yaml +8 -0
  31. disdrodb/etc/products/L2M/NGAMMA_GS_LOG_ND_MAE.yaml +6 -0
  32. disdrodb/etc/products/L2M/NGAMMA_GS_ND_MAE.yaml +6 -0
  33. disdrodb/etc/products/L2M/NGAMMA_GS_Z_MAE.yaml +6 -0
  34. disdrodb/etc/products/L2M/global.yaml +26 -0
  35. disdrodb/l0/__init__.py +13 -0
  36. disdrodb/l0/configs/LPM/l0b_cf_attrs.yml +4 -4
  37. disdrodb/l0/configs/PARSIVEL/l0b_cf_attrs.yml +1 -1
  38. disdrodb/l0/configs/PARSIVEL/l0b_encodings.yml +3 -3
  39. disdrodb/l0/configs/PARSIVEL/raw_data_format.yml +1 -1
  40. disdrodb/l0/configs/PARSIVEL2/l0b_cf_attrs.yml +5 -5
  41. disdrodb/l0/configs/PARSIVEL2/l0b_encodings.yml +3 -3
  42. disdrodb/l0/configs/PARSIVEL2/raw_data_format.yml +1 -1
  43. disdrodb/l0/configs/PWS100/l0b_cf_attrs.yml +4 -4
  44. disdrodb/l0/configs/PWS100/raw_data_format.yml +1 -1
  45. disdrodb/l0/l0a_processing.py +30 -30
  46. disdrodb/l0/l0b_nc_processing.py +108 -2
  47. disdrodb/l0/l0b_processing.py +4 -4
  48. disdrodb/l0/l0c_processing.py +5 -13
  49. disdrodb/l0/readers/LPM/NETHERLANDS/DELFT_LPM_NC.py +66 -0
  50. disdrodb/l0/readers/LPM/SLOVENIA/{CRNI_VRH.py → UL.py} +3 -0
  51. disdrodb/l0/readers/LPM/SWITZERLAND/INNERERIZ_LPM.py +195 -0
  52. disdrodb/l0/readers/PARSIVEL/GPM/PIERS.py +0 -2
  53. disdrodb/l0/readers/PARSIVEL/JAPAN/JMA.py +4 -1
  54. disdrodb/l0/readers/PARSIVEL/NCAR/PECAN_MOBILE.py +1 -1
  55. disdrodb/l0/readers/PARSIVEL/NCAR/VORTEX2_2009.py +1 -1
  56. disdrodb/l0/readers/PARSIVEL2/BELGIUM/ILVO.py +168 -0
  57. disdrodb/l0/readers/PARSIVEL2/DENMARK/DTU.py +165 -0
  58. disdrodb/l0/readers/PARSIVEL2/FINLAND/FMI_PARSIVEL2.py +69 -0
  59. disdrodb/l0/readers/PARSIVEL2/FRANCE/ENPC_PARSIVEL2.py +255 -134
  60. disdrodb/l0/readers/PARSIVEL2/FRANCE/OSUG.py +525 -0
  61. disdrodb/l0/readers/PARSIVEL2/FRANCE/SIRTA_PARSIVEL2.py +1 -1
  62. disdrodb/l0/readers/PARSIVEL2/GPM/GCPEX.py +9 -7
  63. disdrodb/l0/readers/PARSIVEL2/KIT/BURKINA_FASO.py +1 -1
  64. disdrodb/l0/readers/PARSIVEL2/KIT/TEAMX.py +123 -0
  65. disdrodb/l0/readers/PARSIVEL2/NASA/APU.py +120 -0
  66. disdrodb/l0/readers/PARSIVEL2/NCAR/FARM_PARSIVEL2.py +1 -0
  67. disdrodb/l0/readers/PARSIVEL2/NCAR/PECAN_FP3.py +1 -1
  68. disdrodb/l0/readers/PARSIVEL2/NCAR/PERILS_MIPS.py +126 -0
  69. disdrodb/l0/readers/PARSIVEL2/NCAR/PERILS_PIPS.py +165 -0
  70. disdrodb/l0/readers/PARSIVEL2/NCAR/VORTEX_SE_2016_P2.py +1 -1
  71. disdrodb/l0/readers/PARSIVEL2/NCAR/VORTEX_SE_2016_PIPS.py +20 -12
  72. disdrodb/l0/readers/PARSIVEL2/NETHERLANDS/DELFT_NC.py +2 -0
  73. disdrodb/l0/readers/PARSIVEL2/SPAIN/CENER.py +144 -0
  74. disdrodb/l0/readers/PARSIVEL2/SPAIN/CR1000DL.py +201 -0
  75. disdrodb/l0/readers/PARSIVEL2/SPAIN/LIAISE.py +137 -0
  76. disdrodb/l0/readers/PARSIVEL2/{NETHERLANDS/DELFT.py → USA/C3WE.py} +65 -85
  77. disdrodb/l0/readers/PWS100/FRANCE/ENPC_PWS100.py +105 -99
  78. disdrodb/l0/readers/PWS100/FRANCE/ENPC_PWS100_SIRTA.py +151 -0
  79. disdrodb/l0/routines.py +105 -14
  80. disdrodb/l1/__init__.py +5 -0
  81. disdrodb/l1/filters.py +34 -20
  82. disdrodb/l1/processing.py +45 -44
  83. disdrodb/l1/resampling.py +77 -66
  84. disdrodb/l1/routines.py +35 -43
  85. disdrodb/l1_env/routines.py +18 -3
  86. disdrodb/l2/__init__.py +7 -0
  87. disdrodb/l2/empirical_dsd.py +58 -10
  88. disdrodb/l2/event.py +27 -120
  89. disdrodb/l2/processing.py +267 -116
  90. disdrodb/l2/routines.py +618 -254
  91. disdrodb/metadata/standards.py +3 -1
  92. disdrodb/psd/fitting.py +463 -144
  93. disdrodb/psd/models.py +8 -5
  94. disdrodb/routines.py +3 -3
  95. disdrodb/scattering/__init__.py +16 -4
  96. disdrodb/scattering/axis_ratio.py +56 -36
  97. disdrodb/scattering/permittivity.py +486 -0
  98. disdrodb/scattering/routines.py +701 -159
  99. disdrodb/summary/__init__.py +17 -0
  100. disdrodb/summary/routines.py +4120 -0
  101. disdrodb/utils/attrs.py +68 -125
  102. disdrodb/utils/compression.py +30 -1
  103. disdrodb/utils/dask.py +59 -8
  104. disdrodb/utils/dataframe.py +61 -7
  105. disdrodb/utils/directories.py +35 -15
  106. disdrodb/utils/encoding.py +33 -19
  107. disdrodb/utils/logger.py +13 -6
  108. disdrodb/utils/manipulations.py +71 -0
  109. disdrodb/utils/subsetting.py +214 -0
  110. disdrodb/utils/time.py +165 -19
  111. disdrodb/utils/writer.py +20 -7
  112. disdrodb/utils/xarray.py +2 -4
  113. disdrodb/viz/__init__.py +13 -0
  114. disdrodb/viz/plots.py +327 -0
  115. {disdrodb-0.1.2.dist-info → disdrodb-0.1.3.dist-info}/METADATA +3 -2
  116. {disdrodb-0.1.2.dist-info → disdrodb-0.1.3.dist-info}/RECORD +121 -88
  117. {disdrodb-0.1.2.dist-info → disdrodb-0.1.3.dist-info}/entry_points.txt +1 -0
  118. disdrodb/l1/encoding_attrs.py +0 -642
  119. disdrodb/l2/processing_options.py +0 -213
  120. /disdrodb/l0/readers/PARSIVEL/SLOVENIA/{UL_FGG.py → UL.py} +0 -0
  121. {disdrodb-0.1.2.dist-info → disdrodb-0.1.3.dist-info}/WHEEL +0 -0
  122. {disdrodb-0.1.2.dist-info → disdrodb-0.1.3.dist-info}/licenses/LICENSE +0 -0
  123. {disdrodb-0.1.2.dist-info → disdrodb-0.1.3.dist-info}/top_level.txt +0 -0
disdrodb/psd/fitting.py CHANGED
@@ -22,96 +22,147 @@ from scipy.integrate import quad
22
22
  from scipy.optimize import minimize
23
23
  from scipy.special import gamma, gammainc, gammaln # Regularized lower incomplete gamma function
24
24
 
25
+ from disdrodb.constants import DIAMETER_DIMENSION
26
+ from disdrodb.l2.empirical_dsd import (
27
+ get_median_volume_drop_diameter,
28
+ get_moment,
29
+ get_normalized_intercept_parameter_from_moments,
30
+ get_total_number_concentration,
31
+ )
25
32
  from disdrodb.psd.models import ExponentialPSD, GammaPSD, LognormalPSD, NormalizedGammaPSD
33
+ from disdrodb.utils.manipulations import get_diameter_bin_edges
26
34
  from disdrodb.utils.warnings import suppress_warnings
27
35
 
28
36
  # gamma(>171) return inf !
29
37
 
38
+ ####--------------------------------------------------------------------------------------.
39
+ #### Notes
40
+ ## Variable requirements for fitting PSD Models
41
+ # - drop_number_concentration and diameter coordinates
42
+ # - Always recompute other parameters to ensure not use model parameters of L2M
43
+
44
+ # ML: None
45
+
46
+ # MOM: moments
47
+ # --> get_moment(drop_number_concentration, diameter, diameter_bin_width, moment)
48
+
49
+ # GS: fall_velocity if target optimization is R (rain)
50
+ # - NormalizedGamma: "Nw", "D50"
51
+ # --> get_normalized_intercept_parameter_from_moments(moment_3, moment_4)
52
+ # --> get_median_volume_drop_diameter(drop_number_concentration, diameter, diameter_bin_width):
53
+ # --> get_mean_volume_drop_diameter(moment_3, moment_4) (Dm)
54
+
55
+ # - LogNormal,Exponential, Gamma: Nt
56
+ # --> get_total_number_concentration(drop_number_concentration, diameter_bin_width)
57
+
30
58
 
31
59
  ####--------------------------------------------------------------------------------------.
32
60
  #### Goodness of fit (GOF)
33
- def compute_gof_stats(drop_number_concentration, psd):
61
+ def compute_gof_stats(obs, pred, dim=DIAMETER_DIMENSION):
34
62
  """
35
- Compute various goodness-of-fit (GoF) statistics between observed and predicted values.
63
+ Compute various goodness-of-fit (GoF) statistics between obs and predicted values.
36
64
 
37
65
  Parameters
38
66
  ----------
39
- - drop_number_concentration: xarray.DataArray with dimensions ('time', 'diameter_bin_center')
40
- - psd: instance of PSD class
67
+ obs: xarray.DataArray
68
+ Observations DataArray with at least dimension ``dim``.
69
+ pred: xarray.DataArray
70
+ Predictions DataArray with at least dimension ``dim``.
71
+ dim: str
72
+ DataArray dimension over which to compute GOF statistics.
73
+ The default is DIAMETER_DIMENSION.
41
74
 
42
75
  Returns
43
76
  -------
44
- - ds: xarray.Dataset containing the computed GoF statistics
77
+ ds: xarray.Dataset
78
+ Dataset containing the computed GoF statistics.
45
79
  """
46
80
  from disdrodb.l2.empirical_dsd import get_mode_diameter
47
81
 
48
- # Retrieve diameter bin width
49
- diameter = drop_number_concentration["diameter_bin_center"]
50
- diameter_bin_width = drop_number_concentration["diameter_bin_width"]
82
+ # Retrieve diameter and diameter bin width
83
+ diameter = obs["diameter_bin_center"]
84
+ diameter_bin_width = obs["diameter_bin_width"]
85
+
86
+ # Compute errors
87
+ error = obs - pred
88
+
89
+ # Compute max obs and pred
90
+ obs_max = obs.max(dim=dim, skipna=False)
91
+ pred_max = pred.max(dim=dim, skipna=False)
51
92
 
52
- # Define observed and predicted values and compute errors
53
- observed_values = drop_number_concentration
54
- fitted_values = psd(diameter) # .transpose(*observed_values.dims)
55
- error = observed_values - fitted_values
93
+ # Compute NaN mask
94
+ mask_nan = np.logical_or(np.isnan(obs_max), np.isnan(pred_max))
56
95
 
57
96
  # Compute GOF statistics
58
97
  with suppress_warnings():
59
- # Compute Pearson correlation
60
- pearson_r = xr.corr(observed_values, fitted_values, dim="diameter_bin_center")
61
-
62
- # Compute MSE
63
- mse = (error**2).mean(dim="diameter_bin_center")
98
+ # Compute Pearson Correlation
99
+ pearson_r = xr.corr(obs, pred, dim=dim)
100
+
101
+ # Compute Mean Absolute Error (MAE)
102
+ mae = np.abs(error).mean(dim=dim, skipna=False)
103
+
104
+ # Compute maximum absolute error
105
+ max_error = np.abs(error).max(dim=dim, skipna=False)
106
+ relative_max_error = xr.where(max_error == 0, 0, xr.where(obs_max == 0, np.nan, max_error / obs_max))
107
+
108
+ # Compute deviation of N(D) at distribution mode
109
+ mode_deviation = obs_max - pred_max
110
+ mode_relative_deviation = xr.where(
111
+ mode_deviation == 0,
112
+ 0,
113
+ xr.where(obs_max == 0, np.nan, mode_deviation / obs_max),
114
+ )
64
115
 
65
- # Compute maximum error
66
- max_error = error.max(dim="diameter_bin_center")
67
- relative_max_error = error.max(dim="diameter_bin_center") / observed_values.max(dim="diameter_bin_center")
116
+ # Compute diameter difference of the distribution mode
117
+ diameter_mode_pred = get_mode_diameter(pred, diameter)
118
+ diameter_mode_obs = get_mode_diameter(obs, diameter)
119
+ diameter_mode_deviation = diameter_mode_obs - diameter_mode_pred
68
120
 
69
121
  # Compute difference in total number concentration
70
- total_number_concentration_obs = (observed_values * diameter_bin_width).sum(dim="diameter_bin_center")
71
- total_number_concentration_pred = (fitted_values * diameter_bin_width).sum(dim="diameter_bin_center")
122
+ total_number_concentration_obs = (obs * diameter_bin_width).sum(dim=dim, skipna=False)
123
+ total_number_concentration_pred = (pred * diameter_bin_width).sum(dim=dim, skipna=False)
72
124
  total_number_concentration_difference = total_number_concentration_pred - total_number_concentration_obs
73
125
 
74
126
  # Compute Kullback-Leibler divergence
75
127
  # - Compute pdf per bin
76
- pk_pdf = observed_values / total_number_concentration_obs
77
- qk_pdf = fitted_values / total_number_concentration_pred
128
+ pk_pdf = obs / total_number_concentration_obs
129
+ qk_pdf = pred / total_number_concentration_pred
78
130
 
79
131
  # - Compute probabilities per bin
80
132
  pk = pk_pdf * diameter_bin_width
81
- pk = pk / pk.sum(dim="diameter_bin_center") # this might not be necessary
133
+ pk = pk / pk.sum(dim=dim, skipna=False) # this might not be necessary
82
134
  qk = qk_pdf * diameter_bin_width
83
- qk = qk / qk.sum(dim="diameter_bin_center") # this might not be necessary
135
+ qk = qk / qk.sum(dim=dim, skipna=False) # this might not be necessary
84
136
 
85
- # - Compute divergence
137
+ # - Compute log probability ratio
138
+ epsilon = 1e-10
139
+ pk = xr.where(pk == 0, epsilon, pk)
140
+ qk = xr.where(qk == 0, epsilon, qk)
86
141
  log_prob_ratio = np.log(pk / qk)
87
142
  log_prob_ratio = log_prob_ratio.where(np.isfinite(log_prob_ratio))
88
- kl_divergence = (pk * log_prob_ratio).sum(dim="diameter_bin_center")
89
-
90
- # Other statistics that can be computed also from different diameter discretization
91
- # - Compute max deviation at distribution mode
92
- max_deviation = observed_values.max(dim="diameter_bin_center") - fitted_values.max(dim="diameter_bin_center")
93
- max_relative_deviation = max_deviation / fitted_values.max(dim="diameter_bin_center")
94
143
 
95
- # - Compute diameter difference of the distribution mode
96
- diameter_mode_deviation = get_mode_diameter(observed_values, diameter) - get_mode_diameter(
97
- fitted_values,
98
- diameter,
99
- )
144
+ # - Compute divergence
145
+ kl_divergence = (pk * log_prob_ratio).sum(dim=dim, skipna=False)
146
+ kl_divergence = xr.where((error == 0).all(dim=dim), 0, kl_divergence)
100
147
 
101
148
  # Create an xarray.Dataset to hold the computed statistics
102
149
  ds = xr.Dataset(
103
150
  {
104
- "r2": pearson_r**2, # Squared Pearson correlation coefficient
105
- "mse": mse, # Mean Squared Error
106
- "max_error": max_error, # Maximum Absolute Error
107
- "relative_max_error": relative_max_error, # Relative Maximum Error
108
- "total_number_concentration_difference": total_number_concentration_difference,
109
- "kl_divergence": kl_divergence, # Kullback-Leibler divergence
110
- "max_deviation": max_deviation, # Deviation at distribution mode
111
- "max_relative_deviation": max_relative_deviation, # Relative deviation at mode
112
- "diameter_mode_deviation": diameter_mode_deviation, # Difference in mode diameters
151
+ "R2": pearson_r**2, # Squared Pearson correlation coefficient
152
+ "MAE": mae, # Mean Absolute Error
153
+ "MaxAE": max_error, # Maximum Absolute Error
154
+ "RelMaxAE": relative_max_error, # Relative Maximum Absolute Error
155
+ "PeakDiff": mode_deviation, # Difference at distribution peak
156
+ "RelPeakDiff": mode_relative_deviation, # Relative difference at peak
157
+ "DmodeDiff": diameter_mode_deviation, # Difference in mode diameters
158
+ "NtDiff": total_number_concentration_difference,
159
+ "KLDiv": kl_divergence, # Kullback-Leibler divergence
113
160
  },
114
161
  )
162
+ # Round
163
+ ds = ds.round(2)
164
+ # Mask where input obs or pred is NaN
165
+ ds = ds.where(~mask_nan)
115
166
  return ds
116
167
 
117
168
 
@@ -178,7 +229,7 @@ def get_adjusted_nt(cdf, params, Nt, bin_edges):
178
229
  # Estimate proportion of missing drops (Johnson's 2011 Eqs. 3)
179
230
  # --> Alternative: p = 1 - np.sum(pdf(diameter, params)* diameter_bin_width) # [-]
180
231
  p = 1 - np.diff(cdf([bin_edges[0], bin_edges[-1]], params)).item() # [-]
181
- # Adjusts Nt for the proportion of drops not observed
232
+ # Adjusts Nt for the proportion of drops not obs
182
233
  # p = np.clip(p, 0, 1 - 1e-12)
183
234
  if np.isclose(p, 1, atol=1e-12):
184
235
  return np.nan
@@ -206,7 +257,7 @@ def compute_negative_log_likelihood(
206
257
  bin_edges : array-like
207
258
  Edges of the bins (length N+1).
208
259
  counts : array-like
209
- Observed counts in each bin (length N).
260
+ obs counts in each bin (length N).
210
261
  cdf_func : callable
211
262
  Cumulative distribution function of the distribution.
212
263
  pdf_func : callable
@@ -259,6 +310,8 @@ def compute_negative_log_likelihood(
259
310
 
260
311
  def estimate_lognormal_parameters(
261
312
  counts,
313
+ mu,
314
+ sigma,
262
315
  bin_edges,
263
316
  probability_method="cdf",
264
317
  likelihood="multinomial",
@@ -273,6 +326,12 @@ def estimate_lognormal_parameters(
273
326
  ----------
274
327
  counts : array-like
275
328
  The counts for each bin in the histogram.
329
+ mu: float
330
+ The initial guess of the mean of the log of the distribution.
331
+ A good default value is 0.
332
+ sigma: float
333
+ The initial guess of the standard deviation of the log distribution.
334
+ A good default value is 1.
276
335
  bin_edges : array-like
277
336
  The edges of the bins.
278
337
  probability_method : str, optional
@@ -306,9 +365,9 @@ def estimate_lognormal_parameters(
306
365
  ----------
307
366
  .. [1] https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.lognorm.html#scipy.stats.lognorm
308
367
  """
309
- # LogNormal
310
- # - mu = log(scale)
311
- # - loc = 0
368
+ # Definite initial guess for the parameters
369
+ scale = np.exp(mu) # mu = np.log(scale)
370
+ initial_params = [sigma, scale]
312
371
 
313
372
  # Initialize bad results
314
373
  null_output = (
@@ -329,9 +388,6 @@ def estimate_lognormal_parameters(
329
388
  sigma, scale = params
330
389
  return sigma > 0 and scale > 0
331
390
 
332
- # Definite initial guess for the parameters
333
- initial_params = [1.0, 1.0] # sigma, scale
334
-
335
391
  # Define bounds for sigma and scale
336
392
  bounds = [(1e-6, None), (1e-6, None)]
337
393
 
@@ -375,6 +431,7 @@ def estimate_lognormal_parameters(
375
431
 
376
432
  def estimate_exponential_parameters(
377
433
  counts,
434
+ Lambda,
378
435
  bin_edges,
379
436
  probability_method="cdf",
380
437
  likelihood="multinomial",
@@ -389,6 +446,10 @@ def estimate_exponential_parameters(
389
446
  ----------
390
447
  counts : array-like
391
448
  The counts for each bin in the histogram.
449
+ Lambda : float
450
+ The initial guess of the scale parameter.
451
+ scale = 1 / lambda correspond to the scale parameter of the scipy.stats.expon distribution.
452
+ A good default value is 1.
392
453
  bin_edges : array-like
393
454
  The edges of the bins.
394
455
  probability_method : str, optional
@@ -421,6 +482,10 @@ def estimate_exponential_parameters(
421
482
  ----------
422
483
  .. [1] https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.expon.html
423
484
  """
485
+ # Definite initial guess for parameters
486
+ scale = 1 / Lambda
487
+ initial_params = [scale]
488
+
424
489
  # Initialize bad results
425
490
  null_output = {"N0": np.nan, "Lambda": np.nan} if output_dictionary else np.array([np.nan, np.nan])
426
491
 
@@ -438,9 +503,6 @@ def estimate_exponential_parameters(
438
503
  scale = params[0]
439
504
  return scale > 0
440
505
 
441
- # Definite initial guess for the scale parameter
442
- initial_params = [1.0] # scale
443
-
444
506
  # Define bounds for scale
445
507
  bounds = [(1e-6, None)]
446
508
 
@@ -485,8 +547,8 @@ def estimate_exponential_parameters(
485
547
 
486
548
  def estimate_gamma_parameters(
487
549
  counts,
488
- a,
489
- scale,
550
+ mu,
551
+ Lambda,
490
552
  bin_edges,
491
553
  probability_method="cdf",
492
554
  likelihood="multinomial",
@@ -501,11 +563,13 @@ def estimate_gamma_parameters(
501
563
  ----------
502
564
  counts : array-like
503
565
  The counts for each bin in the histogram.
504
- a: float
505
- The shape parameter of the scipy.stats.gamma distribution.
506
- A good default value is 1.
507
- scale: float
508
- The scale parameter of the scipy.stats.gamma distribution.
566
+ mu: float
567
+ The initial guess of the shape parameter.
568
+ a = mu + 1 correspond to the shape parameter of the scipy.stats.gamma distribution.
569
+ A good default value is 0.
570
+ lambda: float
571
+ The initial guess of the scale parameter.
572
+ scale = 1 / lambda correspond to the scale parameter of the scipy.stats.gamma distribution.
509
573
  A good default value is 1.
510
574
  bin_edges : array-like
511
575
  The edges of the bins.
@@ -541,6 +605,11 @@ def estimate_gamma_parameters(
541
605
  .. [1] https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.gamma.html
542
606
 
543
607
  """
608
+ # Define initial guess for parameters
609
+ a = mu + 1 # (mu = a-1, a = mu+1)
610
+ scale = 1 / Lambda
611
+ initial_params = [a, scale]
612
+
544
613
  # Initialize bad results
545
614
  null_output = (
546
615
  {"N0": np.nan, "mu": np.nan, "lambda": np.nan} if output_dictionary else np.array([np.nan, np.nan, np.nan])
@@ -561,9 +630,6 @@ def estimate_gamma_parameters(
561
630
  a, scale = params
562
631
  return a > 0.1 and scale > 0 # using a > 0 cause some troubles
563
632
 
564
- # Definite initial guess for the parameters
565
- initial_params = [a, scale] # (mu=a-1, a=mu+1)
566
-
567
633
  # Define bounds for a and scale
568
634
  bounds = [(1e-6, None), (1e-6, None)]
569
635
 
@@ -617,12 +683,59 @@ def estimate_gamma_parameters(
617
683
  return output
618
684
 
619
685
 
686
+ def _get_initial_lognormal_parameters(ds, mom_method=None):
687
+ default_mu = 0 # mu = np.log(scale)
688
+ default_sigma = 1
689
+ if mom_method is None or mom_method == "None":
690
+ ds_init = xr.Dataset(
691
+ {
692
+ "mu": default_mu,
693
+ "sigma": default_sigma,
694
+ },
695
+ )
696
+ else:
697
+ ds_init = get_mom_parameters(
698
+ ds=ds,
699
+ psd_model="LognormalPSD",
700
+ mom_methods=mom_method,
701
+ )
702
+ # If initialization results in some not finite number, set default value
703
+ ds_init["mu"] = xr.where(
704
+ np.logical_and(np.isfinite(ds_init["mu"]), ds_init["mu"] > 0),
705
+ ds_init["mu"],
706
+ default_mu,
707
+ )
708
+ ds_init["sigma"] = xr.where(np.isfinite(ds_init["sigma"]), ds_init["sigma"], default_sigma)
709
+ return ds_init
710
+
711
+
712
+ def _get_initial_exponential_parameters(ds, mom_method=None):
713
+ default_lambda = 1 # lambda = 1 /scale
714
+ if mom_method is None or mom_method == "None":
715
+ ds_init = xr.Dataset(
716
+ {
717
+ "Lambda": default_lambda,
718
+ },
719
+ )
720
+ else:
721
+ ds_init = get_mom_parameters(
722
+ ds=ds,
723
+ psd_model="ExponentialPSD",
724
+ mom_methods=mom_method,
725
+ )
726
+ # If initialization results in some not finite number, set default value
727
+ ds_init["Lambda"] = xr.where(np.isfinite(ds_init["Lambda"]), ds_init["Lambda"], default_lambda)
728
+ return ds_init
729
+
730
+
620
731
  def _get_initial_gamma_parameters(ds, mom_method=None):
621
- if mom_method is None:
732
+ default_mu = 0 # a = mu + 1 | mu = a - 1
733
+ default_lambda = 1 # scale = 1 / Lambda
734
+ if mom_method is None or mom_method == "None":
622
735
  ds_init = xr.Dataset(
623
736
  {
624
- "a": xr.ones_like(ds["M1"]),
625
- "scale": xr.ones_like(ds["M1"]),
737
+ "mu": default_mu,
738
+ "Lambda": default_lambda,
626
739
  },
627
740
  )
628
741
  else:
@@ -631,11 +744,13 @@ def _get_initial_gamma_parameters(ds, mom_method=None):
631
744
  psd_model="GammaPSD",
632
745
  mom_methods=mom_method,
633
746
  )
634
- ds_init["a"] = ds_init["mu"] + 1
635
- ds_init["scale"] = 1 / ds_init["Lambda"]
636
747
  # If initialization results in some not finite number, set default value
637
- ds_init["a"] = xr.where(np.isfinite(ds_init["a"]), ds_init["a"], ds["M1"])
638
- ds_init["scale"] = xr.where(np.isfinite(ds_init["scale"]), ds_init["scale"], ds["M1"])
748
+ ds_init["mu"] = xr.where(
749
+ np.logical_and(np.isfinite(ds_init["mu"]), ds_init["mu"] > -1),
750
+ ds_init["mu"],
751
+ default_mu,
752
+ )
753
+ ds_init["Lambda"] = xr.where(np.isfinite(ds_init["Lambda"]), ds_init["Lambda"], default_lambda)
639
754
  return ds_init
640
755
 
641
756
 
@@ -663,7 +778,7 @@ def get_gamma_parameters(
663
778
  with the specified mom_method.
664
779
  init_method: str or list
665
780
  The method(s) of moments used to initialize the gamma parameters.
666
- If None, the scale parameter is set to 1 and mu to 0 (a=1).
781
+ If None (or 'None'), the scale parameter is set to 1 and mu to 0 (a=1).
667
782
  probability_method : str, optional
668
783
  Method to compute probabilities. The default value is ``cdf``.
669
784
  likelihood : str, optional
@@ -690,9 +805,9 @@ def get_gamma_parameters(
690
805
  """
691
806
  # Define inputs
692
807
  counts = ds["drop_number_concentration"] * ds["diameter_bin_width"]
693
- diameter_breaks = np.append(ds["diameter_bin_lower"].data, ds["diameter_bin_upper"].data[-1])
808
+ diameter_breaks = get_diameter_bin_edges(ds)
694
809
 
695
- # Define initial parameters (a, scale)
810
+ # Define initial parameters (mu, Lambda)
696
811
  ds_init = _get_initial_gamma_parameters(ds, mom_method=init_method)
697
812
 
698
813
  # Define kwargs
@@ -709,10 +824,10 @@ def get_gamma_parameters(
709
824
  da_params = xr.apply_ufunc(
710
825
  estimate_gamma_parameters,
711
826
  counts,
712
- ds_init["a"],
713
- ds_init["scale"],
827
+ ds_init["mu"],
828
+ ds_init["Lambda"],
714
829
  kwargs=kwargs,
715
- input_core_dims=[["diameter_bin_center"], [], []],
830
+ input_core_dims=[[DIAMETER_DIMENSION], [], []],
716
831
  output_core_dims=[["parameters"]],
717
832
  vectorize=True,
718
833
  dask="parallelized",
@@ -720,8 +835,6 @@ def get_gamma_parameters(
720
835
  output_dtypes=["float64"],
721
836
  )
722
837
 
723
- ds_init.isel(velocity_method=0, time=-3)
724
-
725
838
  # Add parameters coordinates
726
839
  da_params = da_params.assign_coords({"parameters": ["N0", "mu", "Lambda"]})
727
840
 
@@ -735,7 +848,7 @@ def get_gamma_parameters(
735
848
 
736
849
  def get_lognormal_parameters(
737
850
  ds,
738
- init_method=None, # noqa: ARG001
851
+ init_method=None,
739
852
  probability_method="cdf",
740
853
  likelihood="multinomial",
741
854
  truncated_likelihood=True,
@@ -779,7 +892,10 @@ def get_lognormal_parameters(
779
892
  """
780
893
  # Define inputs
781
894
  counts = ds["drop_number_concentration"] * ds["diameter_bin_width"]
782
- diameter_breaks = np.append(ds["diameter_bin_lower"].data, ds["diameter_bin_upper"].data[-1])
895
+ diameter_breaks = get_diameter_bin_edges(ds)
896
+
897
+ # Define initial parameters (mu, sigma)
898
+ ds_init = _get_initial_lognormal_parameters(ds, mom_method=init_method)
783
899
 
784
900
  # Define kwargs
785
901
  kwargs = {
@@ -795,8 +911,10 @@ def get_lognormal_parameters(
795
911
  da_params = xr.apply_ufunc(
796
912
  estimate_lognormal_parameters,
797
913
  counts,
914
+ ds_init["mu"],
915
+ ds_init["sigma"],
798
916
  kwargs=kwargs,
799
- input_core_dims=[["diameter_bin_center"]],
917
+ input_core_dims=[[DIAMETER_DIMENSION], [], []],
800
918
  output_core_dims=[["parameters"]],
801
919
  vectorize=True,
802
920
  dask="parallelized",
@@ -818,7 +936,7 @@ def get_lognormal_parameters(
818
936
 
819
937
  def get_exponential_parameters(
820
938
  ds,
821
- init_method=None, # noqa: ARG001
939
+ init_method=None,
822
940
  probability_method="cdf",
823
941
  likelihood="multinomial",
824
942
  truncated_likelihood=True,
@@ -864,7 +982,10 @@ def get_exponential_parameters(
864
982
  """
865
983
  # Define inputs
866
984
  counts = ds["drop_number_concentration"] * ds["diameter_bin_width"]
867
- diameter_breaks = np.append(ds["diameter_bin_lower"].data, ds["diameter_bin_upper"].data[-1])
985
+ diameter_breaks = get_diameter_bin_edges(ds)
986
+
987
+ # Define initial parameters (Lambda)
988
+ ds_init = _get_initial_exponential_parameters(ds, mom_method=init_method)
868
989
 
869
990
  # Define kwargs
870
991
  kwargs = {
@@ -880,8 +1001,9 @@ def get_exponential_parameters(
880
1001
  da_params = xr.apply_ufunc(
881
1002
  estimate_exponential_parameters,
882
1003
  counts,
1004
+ ds_init["Lambda"],
883
1005
  kwargs=kwargs,
884
- input_core_dims=[["diameter_bin_center"]],
1006
+ input_core_dims=[[DIAMETER_DIMENSION], []],
885
1007
  output_core_dims=[["parameters"]],
886
1008
  vectorize=True,
887
1009
  dask="parallelized",
@@ -1009,7 +1131,7 @@ def _estimate_gamma_parameters_johnson(
1009
1131
  p = 1 - np.sum((Lambda ** (mu + 1)) / gamma(mu + 1) * D**mu * np.exp(-Lambda * D) * diameter_bin_width) # [-]
1010
1132
 
1011
1133
  # Convert tilde_N_T to N_T using Johnson's 2013 Eqs. 3 and 4.
1012
- # - Adjusts for the proportion of drops not observed
1134
+ # - Adjusts for the proportion of drops not obs
1013
1135
  N_T = tilde_N_T / (1 - p) # [m-3]
1014
1136
 
1015
1137
  # Compute N0
@@ -1030,7 +1152,8 @@ def get_gamma_parameters_johnson2014(ds, method="Nelder-Mead"):
1030
1152
  """Deprecated model. See Gamma Model with truncated_likelihood and 'pdf'."""
1031
1153
  drop_number_concentration = ds["drop_number_concentration"]
1032
1154
  diameter = ds["diameter_bin_center"]
1033
- diameter_breaks = np.append(ds["diameter_bin_lower"].data, ds["diameter_bin_upper"].data[-1])
1155
+ diameter_breaks = get_diameter_bin_edges(ds)
1156
+
1034
1157
  # Define kwargs
1035
1158
  kwargs = {
1036
1159
  "output_dictionary": False,
@@ -1043,7 +1166,7 @@ def get_gamma_parameters_johnson2014(ds, method="Nelder-Mead"):
1043
1166
  diameter,
1044
1167
  # diameter_bin_width,
1045
1168
  kwargs=kwargs,
1046
- input_core_dims=[["diameter_bin_center"], ["diameter_bin_center"]], # ["diameter_bin_center"],
1169
+ input_core_dims=[[DIAMETER_DIMENSION], [DIAMETER_DIMENSION]], # [DIAMETER_DIMENSION],
1047
1170
  output_core_dims=[["parameters"]],
1048
1171
  vectorize=True,
1049
1172
  )
@@ -1346,6 +1469,12 @@ def get_exponential_parameters_gs(ds, target="ND", transformation="log", error_o
1346
1469
  # "transformation": "log", "identity", "sqrt", # only for drop_number_concentration
1347
1470
  # "error_order": 1, # MAE/MSE ... only for drop_number_concentration
1348
1471
 
1472
+ # Compute required variables
1473
+ ds["Nt"] = get_total_number_concentration(
1474
+ drop_number_concentration=ds["drop_number_concentration"],
1475
+ diameter_bin_width=ds["diameter_bin_width"],
1476
+ )
1477
+
1349
1478
  # Define kwargs
1350
1479
  kwargs = {
1351
1480
  "D": ds["diameter_bin_center"].data,
@@ -1365,7 +1494,7 @@ def get_exponential_parameters_gs(ds, target="ND", transformation="log", error_o
1365
1494
  # Other options
1366
1495
  kwargs=kwargs,
1367
1496
  # Settings
1368
- input_core_dims=[[], ["diameter_bin_center"], ["diameter_bin_center"]],
1497
+ input_core_dims=[[], [DIAMETER_DIMENSION], [DIAMETER_DIMENSION]],
1369
1498
  output_core_dims=[["parameters"]],
1370
1499
  vectorize=True,
1371
1500
  dask="parallelized",
@@ -1390,6 +1519,12 @@ def get_gamma_parameters_gs(ds, target="ND", transformation="log", error_order=1
1390
1519
  # "transformation": "log", "identity", "sqrt", # only for drop_number_concentration
1391
1520
  # "error_order": 1, # MAE/MSE ... only for drop_number_concentration
1392
1521
 
1522
+ # Compute required variables
1523
+ ds["Nt"] = get_total_number_concentration(
1524
+ drop_number_concentration=ds["drop_number_concentration"],
1525
+ diameter_bin_width=ds["diameter_bin_width"],
1526
+ )
1527
+
1393
1528
  # Define kwargs
1394
1529
  kwargs = {
1395
1530
  "D": ds["diameter_bin_center"].data,
@@ -1409,7 +1544,7 @@ def get_gamma_parameters_gs(ds, target="ND", transformation="log", error_order=1
1409
1544
  # Other options
1410
1545
  kwargs=kwargs,
1411
1546
  # Settings
1412
- input_core_dims=[[], ["diameter_bin_center"], ["diameter_bin_center"]],
1547
+ input_core_dims=[[], [DIAMETER_DIMENSION], [DIAMETER_DIMENSION]],
1413
1548
  output_core_dims=[["parameters"]],
1414
1549
  vectorize=True,
1415
1550
  dask="parallelized",
@@ -1434,6 +1569,12 @@ def get_lognormal_parameters_gs(ds, target="ND", transformation="log", error_ord
1434
1569
  # "transformation": "log", "identity", "sqrt", # only for drop_number_concentration
1435
1570
  # "error_order": 1, # MAE/MSE ... only for drop_number_concentration
1436
1571
 
1572
+ # Compute required variables
1573
+ ds["Nt"] = get_total_number_concentration(
1574
+ drop_number_concentration=ds["drop_number_concentration"],
1575
+ diameter_bin_width=ds["diameter_bin_width"],
1576
+ )
1577
+
1437
1578
  # Define kwargs
1438
1579
  kwargs = {
1439
1580
  "D": ds["diameter_bin_center"].data,
@@ -1453,7 +1594,7 @@ def get_lognormal_parameters_gs(ds, target="ND", transformation="log", error_ord
1453
1594
  # Other options
1454
1595
  kwargs=kwargs,
1455
1596
  # Settings
1456
- input_core_dims=[[], ["diameter_bin_center"], ["diameter_bin_center"]],
1597
+ input_core_dims=[[], [DIAMETER_DIMENSION], [DIAMETER_DIMENSION]],
1457
1598
  output_core_dims=[["parameters"]],
1458
1599
  vectorize=True,
1459
1600
  dask="parallelized",
@@ -1475,8 +1616,8 @@ def get_lognormal_parameters_gs(ds, target="ND", transformation="log", error_ord
1475
1616
  def get_normalized_gamma_parameters_gs(ds, target="ND", transformation="log", error_order=1):
1476
1617
  r"""Estimate $\mu$ of a Normalized Gamma distribution using Grid Search.
1477
1618
 
1478
- The D50 and Nw parameters of the Normalized Gamma distribution are derived empirically from the observed DSD.
1479
- $\mu$ is derived by minimizing the errors between the observed DSD and modelled Normalized Gamma distribution.
1619
+ The D50 and Nw parameters of the Normalized Gamma distribution are derived empirically from the obs DSD.
1620
+ $\mu$ is derived by minimizing the errors between the obs DSD and modelled Normalized Gamma distribution.
1480
1621
 
1481
1622
  Parameters
1482
1623
  ----------
@@ -1501,6 +1642,29 @@ def get_normalized_gamma_parameters_gs(ds, target="ND", transformation="log", er
1501
1642
  # "transformation": "log", "identity", "sqrt", # only for drop_number_concentration
1502
1643
  # "error_order": 1, # MAE/MSE ... only for drop_number_concentration
1503
1644
 
1645
+ # Compute required variables
1646
+ drop_number_concentration = ds["drop_number_concentration"]
1647
+ diameter_bin_width = ds["diameter_bin_width"]
1648
+ diameter = ds["diameter_bin_center"] / 1000 # conversion from mm to m
1649
+ m3 = get_moment(
1650
+ drop_number_concentration=drop_number_concentration,
1651
+ diameter=diameter, # m
1652
+ diameter_bin_width=diameter_bin_width, # mm
1653
+ moment=3,
1654
+ )
1655
+ m4 = get_moment(
1656
+ drop_number_concentration=drop_number_concentration,
1657
+ diameter=diameter, # m
1658
+ diameter_bin_width=diameter_bin_width, # mm
1659
+ moment=4,
1660
+ )
1661
+ ds["Nw"] = get_normalized_intercept_parameter_from_moments(moment_3=m3, moment_4=m4)
1662
+ ds["D50"] = get_median_volume_drop_diameter(
1663
+ drop_number_concentration=drop_number_concentration,
1664
+ diameter=diameter, # m
1665
+ diameter_bin_width=diameter_bin_width, # mm
1666
+ )
1667
+
1504
1668
  # Define kwargs
1505
1669
  kwargs = {
1506
1670
  "D": ds["diameter_bin_center"].data,
@@ -1521,7 +1685,7 @@ def get_normalized_gamma_parameters_gs(ds, target="ND", transformation="log", er
1521
1685
  # Other options
1522
1686
  kwargs=kwargs,
1523
1687
  # Settings
1524
- input_core_dims=[[], [], ["diameter_bin_center"], ["diameter_bin_center"]],
1688
+ input_core_dims=[[], [], [DIAMETER_DIMENSION], [DIAMETER_DIMENSION]],
1525
1689
  output_core_dims=[["parameters"]],
1526
1690
  vectorize=True,
1527
1691
  dask="parallelized",
@@ -1735,12 +1899,25 @@ def get_lognormal_parameters_M346(M3, M4, M6):
1735
1899
  return Nt, mu, sigma
1736
1900
 
1737
1901
 
1902
+ def _compute_moments(ds, moments):
1903
+ list_moments = [
1904
+ get_moment(
1905
+ drop_number_concentration=ds["drop_number_concentration"],
1906
+ diameter=ds["diameter_bin_center"] / 1000, # m
1907
+ diameter_bin_width=ds["diameter_bin_width"], # mm
1908
+ moment=int(moment.replace("M", "")),
1909
+ )
1910
+ for moment in moments
1911
+ ]
1912
+ return list_moments
1913
+
1914
+
1738
1915
  def _get_gamma_parameters_mom(ds: xr.Dataset, mom_method: str) -> xr.Dataset:
1739
1916
  # Get the correct function and list of variables for the requested method
1740
1917
  func, needed_moments = MOM_METHODS_DICT["GammaPSD"][mom_method]
1741
1918
 
1742
- # Extract the required arrays from the dataset
1743
- arrs = [ds[var_name] for var_name in needed_moments]
1919
+ # Compute required moments
1920
+ arrs = _compute_moments(ds, moments=needed_moments)
1744
1921
 
1745
1922
  # Apply the function. This will produce (mu, Lambda, N0) with the same coords/shapes as input data
1746
1923
  N0, mu, Lambda = func(*arrs)
@@ -1761,8 +1938,8 @@ def _get_lognormal_parameters_mom(ds: xr.Dataset, mom_method: str) -> xr.Dataset
1761
1938
  # Get the correct function and list of variables for the requested method
1762
1939
  func, needed_moments = MOM_METHODS_DICT["LognormalPSD"][mom_method]
1763
1940
 
1764
- # Extract the required arrays from the dataset
1765
- arrs = [ds[var_name] for var_name in needed_moments]
1941
+ # Compute required moments
1942
+ arrs = _compute_moments(ds, moments=needed_moments)
1766
1943
 
1767
1944
  # Apply the function. This will produce (mu, Lambda, N0) with the same coords/shapes as input data
1768
1945
  Nt, mu, sigma = func(*arrs)
@@ -1783,8 +1960,8 @@ def _get_exponential_parameters_mom(ds: xr.Dataset, mom_method: str) -> xr.Datas
1783
1960
  # Get the correct function and list of variables for the requested method
1784
1961
  func, needed_moments = MOM_METHODS_DICT["ExponentialPSD"][mom_method]
1785
1962
 
1786
- # Extract the required arrays from the dataset
1787
- arrs = [ds[var_name] for var_name in needed_moments]
1963
+ # Compute required moments
1964
+ arrs = _compute_moments(ds, moments=needed_moments)
1788
1965
 
1789
1966
  # Apply the function. This will produce (mu, Lambda, N0) with the same coords/shapes as input data
1790
1967
  N0, Lambda = func(*arrs)
@@ -1803,6 +1980,79 @@ def _get_exponential_parameters_mom(ds: xr.Dataset, mom_method: str) -> xr.Datas
1803
1980
  ####--------------------------------------------------------------------------------------.
1804
1981
  #### Routines dictionary
1805
1982
 
1983
+ ####--------------------------------------------------------------------------------------.
1984
+ ATTRS_PARAMS_DICT = {
1985
+ "GammaPSD": {
1986
+ "N0": {
1987
+ "description": "Intercept parameter of the Gamma PSD",
1988
+ "standard_name": "particle_size_distribution_intercept",
1989
+ "units": "mm**(-1-mu) m-3",
1990
+ "long_name": "GammaPSD intercept parameter",
1991
+ },
1992
+ "mu": {
1993
+ "description": "Shape parameter of the Gamma PSD",
1994
+ "standard_name": "particle_size_distribution_shape",
1995
+ "units": "",
1996
+ "long_name": "GammaPSD shape parameter",
1997
+ },
1998
+ "Lambda": {
1999
+ "description": "Slope (rate) parameter of the Gamma PSD",
2000
+ "standard_name": "particle_size_distribution_slope",
2001
+ "units": "mm-1",
2002
+ "long_name": "GammaPSD slope parameter",
2003
+ },
2004
+ },
2005
+ "NormalizedGammaPSD": {
2006
+ "Nw": {
2007
+ "standard_name": "normalized_intercept_parameter",
2008
+ "units": "mm-1 m-3",
2009
+ "long_name": "NormalizedGammaPSD Normalized Intercept Parameter",
2010
+ },
2011
+ "mu": {
2012
+ "description": "Dimensionless shape parameter controlling the curvature of the Normalized Gamma PSD",
2013
+ "standard_name": "particle_size_distribution_shape",
2014
+ "units": "",
2015
+ "long_name": "NormalizedGammaPSD Shape Parameter ",
2016
+ },
2017
+ "D50": {
2018
+ "standard_name": "median_volume_diameter",
2019
+ "units": "mm",
2020
+ "long_name": "NormalizedGammaPSD Median Volume Drop Diameter",
2021
+ },
2022
+ },
2023
+ "LognormalPSD": {
2024
+ "Nt": {
2025
+ "standard_name": "number_concentration_of_rain_drops_in_air",
2026
+ "units": "m-3",
2027
+ "long_name": "Total Number Concentration",
2028
+ },
2029
+ "mu": {
2030
+ "description": "Mean of the Lognormal PSD",
2031
+ "units": "log(mm)",
2032
+ "long_name": "Mean of the Lognormal PSD",
2033
+ },
2034
+ "sigma": {
2035
+ "standard_name": "Standard Deviation of the Lognormal PSD",
2036
+ "units": "",
2037
+ "long_name": "Standard Deviation of the Lognormal PSD",
2038
+ },
2039
+ },
2040
+ "ExponentialPSD": {
2041
+ "N0": {
2042
+ "description": "Intercept parameter of the Exponential PSD",
2043
+ "standard_name": "particle_size_distribution_intercept",
2044
+ "units": "mm-1 m-3",
2045
+ "long_name": "ExponentialPSD intercept parameter",
2046
+ },
2047
+ "Lambda": {
2048
+ "description": "Slope (rate) parameter of the Exponential PSD",
2049
+ "standard_name": "particle_size_distribution_slope",
2050
+ "units": "mm-1",
2051
+ "long_name": "ExponentialPSD slope parameter",
2052
+ },
2053
+ },
2054
+ }
2055
+
1806
2056
 
1807
2057
  MOM_METHODS_DICT = {
1808
2058
  "GammaPSD": {
@@ -1843,6 +2093,8 @@ OPTIMIZATION_ROUTINES_DICT = {
1843
2093
 
1844
2094
  def available_mom_methods(psd_model):
1845
2095
  """Implemented MOM methods for a given PSD model."""
2096
+ if psd_model not in MOM_METHODS_DICT:
2097
+ raise NotImplementedError(f"No MOM methods available for {psd_model}")
1846
2098
  return list(MOM_METHODS_DICT[psd_model])
1847
2099
 
1848
2100
 
@@ -1863,7 +2115,7 @@ def check_psd_model(psd_model, optimization):
1863
2115
  f"{optimization} optimization is not available for 'psd_model' {psd_model}. "
1864
2116
  f"Accepted PSD models are {valid_psd_models}."
1865
2117
  )
1866
- raise ValueError(msg)
2118
+ raise NotImplementedError(msg)
1867
2119
 
1868
2120
 
1869
2121
  def check_target(target):
@@ -1921,11 +2173,14 @@ def check_optimizer(optimizer):
1921
2173
  return optimizer
1922
2174
 
1923
2175
 
1924
- def check_mom_methods(mom_methods, psd_model):
2176
+ def check_mom_methods(mom_methods, psd_model, allow_none=False):
1925
2177
  """Check valid mom_methods arguments."""
1926
- if isinstance(mom_methods, str):
2178
+ if isinstance(mom_methods, (str, type(None))):
1927
2179
  mom_methods = [mom_methods]
2180
+ mom_methods = [str(v) for v in mom_methods] # None --> 'None'
1928
2181
  valid_mom_methods = available_mom_methods(psd_model)
2182
+ if allow_none:
2183
+ valid_mom_methods = [*valid_mom_methods, "None"]
1929
2184
  invalid_mom_methods = np.array(mom_methods)[np.isin(mom_methods, valid_mom_methods, invert=True)]
1930
2185
  if len(invalid_mom_methods) > 0:
1931
2186
  raise ValueError(
@@ -1970,36 +2225,50 @@ def check_optimization_kwargs(optimization_kwargs, optimization, psd_model):
1970
2225
  expected_arguments = dict_arguments.get(optimization, {})
1971
2226
 
1972
2227
  # Check for missing arguments in optimization_kwargs
1973
- missing_args = [arg for arg in expected_arguments if arg not in optimization_kwargs]
1974
- if missing_args:
1975
- raise ValueError(f"Missing required arguments for {optimization} optimization: {missing_args}")
2228
+ # missing_args = [arg for arg in expected_arguments if arg not in optimization_kwargs]
2229
+ # if missing_args:
2230
+ # raise ValueError(f"Missing required arguments for {optimization} optimization: {missing_args}")
1976
2231
 
1977
- # Validate argument values
1978
- _ = [check(optimization_kwargs[arg]) for arg, check in expected_arguments.items() if callable(check)]
2232
+ # Validate arguments values
2233
+ _ = [
2234
+ check(optimization_kwargs[arg])
2235
+ for arg, check in expected_arguments.items()
2236
+ if callable(check) and arg in optimization_kwargs
2237
+ ]
1979
2238
 
1980
2239
  # Further special checks
1981
- if optimization == "MOM":
2240
+ if optimization == "MOM" and "mom_methods" in optimization_kwargs:
1982
2241
  _ = check_mom_methods(mom_methods=optimization_kwargs["mom_methods"], psd_model=psd_model)
1983
- if optimization == "ML" and optimization_kwargs["init_method"] is not None:
1984
- _ = check_mom_methods(mom_methods=optimization_kwargs["init_method"], psd_model=psd_model)
2242
+ if optimization == "ML" and optimization_kwargs.get("init_method", None) is not None:
2243
+ _ = check_mom_methods(mom_methods=optimization_kwargs["init_method"], psd_model=psd_model, allow_none=True)
1985
2244
 
1986
2245
 
1987
2246
  ####--------------------------------------------------------------------------------------.
1988
2247
  #### Wrappers for fitting
1989
2248
 
1990
2249
 
1991
- def get_mom_parameters(ds: xr.Dataset, psd_model: str, mom_methods: str) -> xr.Dataset:
2250
+ def _finalize_attributes(ds_params, psd_model, optimization, optimization_kwargs):
2251
+ ds_params.attrs["disdrodb_psd_model"] = psd_model
2252
+ ds_params.attrs["disdrodb_psd_optimization"] = optimization
2253
+ ds_params.attrs["disdrodb_psd_optimization_kwargs"] = ", ".join(
2254
+ [f"{k}: {v}" for k, v in optimization_kwargs.items()],
2255
+ )
2256
+ return ds_params
2257
+
2258
+
2259
+ def get_mom_parameters(ds: xr.Dataset, psd_model: str, mom_methods=None) -> xr.Dataset:
1992
2260
  """
1993
2261
  Compute PSD model parameters using various method-of-moments (MOM) approaches.
1994
2262
 
1995
- The method is specified by the `mom_methods` acronym, e.g. 'M012', 'M234', 'M246'.
2263
+ The method is specified by the `mom_methods` abbreviations, e.g. 'M012', 'M234', 'M246'.
1996
2264
 
1997
2265
  Parameters
1998
2266
  ----------
1999
2267
  ds : xarray.Dataset
2000
2268
  An xarray Dataset with the required moments M0...M6 as data variables.
2001
- mom_methods: str or list
2002
- Valid MOM methods are {'M012', 'M234', 'M246', 'M456', 'M346'}.
2269
+ mom_methods: str or list (optional)
2270
+ See valid values with disdrodb.psd.available_mom_methods(psd_model)
2271
+ If None (the default), compute model parameters with all available MOM methods.
2003
2272
 
2004
2273
  Returns
2005
2274
  -------
@@ -2010,6 +2279,8 @@ def get_mom_parameters(ds: xr.Dataset, psd_model: str, mom_methods: str) -> xr.D
2010
2279
  """
2011
2280
  # Check inputs
2012
2281
  check_psd_model(psd_model=psd_model, optimization="MOM")
2282
+ if mom_methods is None:
2283
+ mom_methods = available_mom_methods(psd_model)
2013
2284
  mom_methods = check_mom_methods(mom_methods, psd_model=psd_model)
2014
2285
 
2015
2286
  # Retrieve function
@@ -2017,13 +2288,21 @@ def get_mom_parameters(ds: xr.Dataset, psd_model: str, mom_methods: str) -> xr.D
2017
2288
 
2018
2289
  # Compute parameters
2019
2290
  if len(mom_methods) == 1:
2020
- ds = func(ds=ds, mom_method=mom_methods[0])
2021
- ds.attrs["mom_method"] = mom_methods[0]
2022
- return ds
2023
- list_ds = [func(ds=ds, mom_method=mom_method) for mom_method in mom_methods]
2024
- ds = xr.concat(list_ds, dim="mom_method")
2025
- ds = ds.assign_coords({"mom_method": mom_methods})
2026
- return ds
2291
+ ds_params = func(ds=ds, mom_method=mom_methods[0])
2292
+ else:
2293
+ list_ds = [func(ds=ds, mom_method=mom_method) for mom_method in mom_methods]
2294
+ ds_params = xr.concat(list_ds, dim="mom_method")
2295
+ ds_params = ds_params.assign_coords({"mom_method": mom_methods})
2296
+
2297
+ # Add model attributes
2298
+ optimization_kwargs = {"mom_methods": mom_methods}
2299
+ ds_params = _finalize_attributes(
2300
+ ds_params=ds_params,
2301
+ psd_model=psd_model,
2302
+ optimization="MOM",
2303
+ optimization_kwargs=optimization_kwargs,
2304
+ )
2305
+ return ds_params
2027
2306
 
2028
2307
 
2029
2308
  def get_ml_parameters(
@@ -2052,7 +2331,7 @@ def get_ml_parameters(
2052
2331
  The PSD model to fit. See ``available_psd_models()``.
2053
2332
  init_method: str or list
2054
2333
  The method(s) of moments used to initialize the PSD model parameters.
2055
- See ``available_mom_methods(psd_model)``.
2334
+ Multiple methods can be specified. See ``available_mom_methods(psd_model)``.
2056
2335
  probability_method : str, optional
2057
2336
  Method to compute probabilities. The default value is ``cdf``.
2058
2337
  likelihood : str, optional
@@ -2076,21 +2355,51 @@ def get_ml_parameters(
2076
2355
  optimizer = check_optimizer(optimizer)
2077
2356
 
2078
2357
  # Check valid init_method
2079
- if init_method is not None:
2080
- init_method = check_mom_methods(mom_methods=init_method, psd_model=psd_model)
2358
+ init_method = check_mom_methods(mom_methods=init_method, psd_model=psd_model, allow_none=True)
2081
2359
 
2082
2360
  # Retrieve estimation function
2083
2361
  func = OPTIMIZATION_ROUTINES_DICT["ML"][psd_model]
2084
2362
 
2085
- # Retrieve parameters
2086
- ds_params = func(
2087
- ds=ds,
2088
- init_method=init_method,
2089
- probability_method=probability_method,
2090
- likelihood=likelihood,
2091
- truncated_likelihood=truncated_likelihood,
2092
- optimizer=optimizer,
2363
+ # Compute parameters
2364
+ if init_method is None or len(init_method) == 1:
2365
+ ds_params = func(
2366
+ ds=ds,
2367
+ init_method=init_method[0],
2368
+ probability_method=probability_method,
2369
+ likelihood=likelihood,
2370
+ truncated_likelihood=truncated_likelihood,
2371
+ optimizer=optimizer,
2372
+ )
2373
+ else:
2374
+ list_ds = [
2375
+ func(
2376
+ ds=ds,
2377
+ init_method=method,
2378
+ probability_method=probability_method,
2379
+ likelihood=likelihood,
2380
+ truncated_likelihood=truncated_likelihood,
2381
+ optimizer=optimizer,
2382
+ )
2383
+ for method in init_method
2384
+ ]
2385
+ ds_params = xr.concat(list_ds, dim="init_method")
2386
+ ds_params = ds_params.assign_coords({"init_method": init_method})
2387
+
2388
+ # Add model attributes
2389
+ optimization_kwargs = {
2390
+ "init_method": init_method,
2391
+ "probability_method": "probability_method",
2392
+ "likelihood": likelihood,
2393
+ "truncated_likelihood": truncated_likelihood,
2394
+ "optimizer": optimizer,
2395
+ }
2396
+ ds_params = _finalize_attributes(
2397
+ ds_params=ds_params,
2398
+ psd_model=psd_model,
2399
+ optimization="ML",
2400
+ optimization_kwargs=optimization_kwargs,
2093
2401
  )
2402
+
2094
2403
  # Return dataset with parameters
2095
2404
  return ds_params
2096
2405
 
@@ -2112,6 +2421,18 @@ def get_gs_parameters(ds, psd_model, target="ND", transformation="log", error_or
2112
2421
  # Estimate parameters
2113
2422
  ds_params = func(ds, target=target, transformation=transformation, error_order=error_order)
2114
2423
 
2424
+ # Add model attributes
2425
+ optimization_kwargs = {
2426
+ "target": target,
2427
+ "transformation": transformation,
2428
+ "error_order": error_order,
2429
+ }
2430
+ ds_params = _finalize_attributes(
2431
+ ds_params=ds_params,
2432
+ psd_model=psd_model,
2433
+ optimization="GS",
2434
+ optimization_kwargs=optimization_kwargs,
2435
+ )
2115
2436
  # Return dataset with parameters
2116
2437
  return ds_params
2117
2438
 
@@ -2120,9 +2441,10 @@ def estimate_model_parameters(
2120
2441
  ds,
2121
2442
  psd_model,
2122
2443
  optimization,
2123
- optimization_kwargs,
2444
+ optimization_kwargs=None,
2124
2445
  ):
2125
2446
  """Routine to estimate PSD model parameters."""
2447
+ optimization_kwargs = {} if optimization_kwargs is None else optimization_kwargs
2126
2448
  optimization = check_optimization(optimization)
2127
2449
  check_optimization_kwargs(optimization_kwargs=optimization_kwargs, optimization=optimization, psd_model=psd_model)
2128
2450
 
@@ -2137,10 +2459,7 @@ def estimate_model_parameters(
2137
2459
  # Retrieve parameters
2138
2460
  ds_params = func(ds, psd_model=psd_model, **optimization_kwargs)
2139
2461
 
2140
- # Finalize attributes
2141
- ds_params.attrs["disdrodb_psd_model"] = psd_model
2142
- ds_params.attrs["disdrodb_psd_optimization"] = optimization
2143
- if optimization == "GS":
2144
- ds_params.attrs["disdrodb_psd_optimization_target"] = optimization_kwargs["target"]
2145
-
2462
+ # Add parameters attributes (and units)
2463
+ for var, attrs in ATTRS_PARAMS_DICT[psd_model].items():
2464
+ ds_params[var].attrs = attrs
2146
2465
  return ds_params