disdrodb 0.1.2__py3-none-any.whl → 0.1.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- disdrodb/__init__.py +68 -34
- disdrodb/_config.py +5 -4
- disdrodb/_version.py +16 -3
- disdrodb/accessor/__init__.py +20 -0
- disdrodb/accessor/methods.py +125 -0
- disdrodb/api/checks.py +177 -24
- disdrodb/api/configs.py +3 -3
- disdrodb/api/info.py +13 -13
- disdrodb/api/io.py +281 -22
- disdrodb/api/path.py +184 -195
- disdrodb/api/search.py +18 -9
- disdrodb/cli/disdrodb_create_summary.py +103 -0
- disdrodb/cli/disdrodb_create_summary_station.py +91 -0
- disdrodb/cli/disdrodb_run_l0.py +1 -1
- disdrodb/cli/disdrodb_run_l0_station.py +1 -1
- disdrodb/cli/disdrodb_run_l0a_station.py +1 -1
- disdrodb/cli/disdrodb_run_l0b.py +1 -1
- disdrodb/cli/disdrodb_run_l0b_station.py +3 -3
- disdrodb/cli/disdrodb_run_l0c.py +1 -1
- disdrodb/cli/disdrodb_run_l0c_station.py +3 -3
- disdrodb/cli/disdrodb_run_l1_station.py +2 -2
- disdrodb/cli/disdrodb_run_l2e_station.py +2 -2
- disdrodb/cli/disdrodb_run_l2m_station.py +2 -2
- disdrodb/configs.py +149 -4
- disdrodb/constants.py +61 -0
- disdrodb/data_transfer/download_data.py +127 -11
- disdrodb/etc/configs/attributes.yaml +339 -0
- disdrodb/etc/configs/encodings.yaml +473 -0
- disdrodb/etc/products/L1/global.yaml +13 -0
- disdrodb/etc/products/L2E/10MIN.yaml +12 -0
- disdrodb/etc/products/L2E/1MIN.yaml +1 -0
- disdrodb/etc/products/L2E/global.yaml +22 -0
- disdrodb/etc/products/L2M/10MIN.yaml +12 -0
- disdrodb/etc/products/L2M/GAMMA_ML.yaml +8 -0
- disdrodb/etc/products/L2M/NGAMMA_GS_LOG_ND_MAE.yaml +6 -0
- disdrodb/etc/products/L2M/NGAMMA_GS_ND_MAE.yaml +6 -0
- disdrodb/etc/products/L2M/NGAMMA_GS_Z_MAE.yaml +6 -0
- disdrodb/etc/products/L2M/global.yaml +26 -0
- disdrodb/issue/writer.py +2 -0
- disdrodb/l0/__init__.py +13 -0
- disdrodb/l0/configs/LPM/l0b_cf_attrs.yml +4 -4
- disdrodb/l0/configs/PARSIVEL/l0b_cf_attrs.yml +1 -1
- disdrodb/l0/configs/PARSIVEL/l0b_encodings.yml +3 -3
- disdrodb/l0/configs/PARSIVEL/raw_data_format.yml +1 -1
- disdrodb/l0/configs/PARSIVEL2/l0b_cf_attrs.yml +5 -5
- disdrodb/l0/configs/PARSIVEL2/l0b_encodings.yml +3 -3
- disdrodb/l0/configs/PARSIVEL2/raw_data_format.yml +1 -1
- disdrodb/l0/configs/PWS100/l0b_cf_attrs.yml +4 -4
- disdrodb/l0/configs/PWS100/raw_data_format.yml +1 -1
- disdrodb/l0/l0a_processing.py +37 -32
- disdrodb/l0/l0b_nc_processing.py +118 -8
- disdrodb/l0/l0b_processing.py +30 -65
- disdrodb/l0/l0c_processing.py +369 -259
- disdrodb/l0/readers/LPM/ARM/ARM_LPM.py +7 -0
- disdrodb/l0/readers/LPM/NETHERLANDS/DELFT_LPM_NC.py +66 -0
- disdrodb/l0/readers/LPM/SLOVENIA/{CRNI_VRH.py → UL.py} +3 -0
- disdrodb/l0/readers/LPM/SWITZERLAND/INNERERIZ_LPM.py +195 -0
- disdrodb/l0/readers/PARSIVEL/GPM/PIERS.py +0 -2
- disdrodb/l0/readers/PARSIVEL/JAPAN/JMA.py +4 -1
- disdrodb/l0/readers/PARSIVEL/NCAR/PECAN_MOBILE.py +1 -1
- disdrodb/l0/readers/PARSIVEL/NCAR/VORTEX2_2009.py +1 -1
- disdrodb/l0/readers/PARSIVEL2/ARM/ARM_PARSIVEL2.py +4 -0
- disdrodb/l0/readers/PARSIVEL2/BELGIUM/ILVO.py +168 -0
- disdrodb/l0/readers/PARSIVEL2/CANADA/UQAM_NC.py +69 -0
- disdrodb/l0/readers/PARSIVEL2/DENMARK/DTU.py +165 -0
- disdrodb/l0/readers/PARSIVEL2/FINLAND/FMI_PARSIVEL2.py +69 -0
- disdrodb/l0/readers/PARSIVEL2/FRANCE/ENPC_PARSIVEL2.py +255 -134
- disdrodb/l0/readers/PARSIVEL2/FRANCE/OSUG.py +525 -0
- disdrodb/l0/readers/PARSIVEL2/FRANCE/SIRTA_PARSIVEL2.py +1 -1
- disdrodb/l0/readers/PARSIVEL2/GPM/GCPEX.py +9 -7
- disdrodb/l0/readers/PARSIVEL2/KIT/BURKINA_FASO.py +1 -1
- disdrodb/l0/readers/PARSIVEL2/KIT/TEAMX.py +123 -0
- disdrodb/l0/readers/PARSIVEL2/{NETHERLANDS/DELFT.py → MPI/BCO_PARSIVEL2.py} +41 -71
- disdrodb/l0/readers/PARSIVEL2/MPI/BOWTIE.py +220 -0
- disdrodb/l0/readers/PARSIVEL2/NASA/APU.py +120 -0
- disdrodb/l0/readers/PARSIVEL2/NASA/LPVEX.py +109 -0
- disdrodb/l0/readers/PARSIVEL2/NCAR/FARM_PARSIVEL2.py +1 -0
- disdrodb/l0/readers/PARSIVEL2/NCAR/PECAN_FP3.py +1 -1
- disdrodb/l0/readers/PARSIVEL2/NCAR/PERILS_MIPS.py +126 -0
- disdrodb/l0/readers/PARSIVEL2/NCAR/PERILS_PIPS.py +165 -0
- disdrodb/l0/readers/PARSIVEL2/NCAR/VORTEX_SE_2016_P2.py +1 -1
- disdrodb/l0/readers/PARSIVEL2/NCAR/VORTEX_SE_2016_PIPS.py +20 -12
- disdrodb/l0/readers/PARSIVEL2/NETHERLANDS/DELFT_NC.py +5 -0
- disdrodb/l0/readers/PARSIVEL2/SPAIN/CENER.py +144 -0
- disdrodb/l0/readers/PARSIVEL2/SPAIN/CR1000DL.py +201 -0
- disdrodb/l0/readers/PARSIVEL2/SPAIN/LIAISE.py +137 -0
- disdrodb/l0/readers/PARSIVEL2/USA/C3WE.py +146 -0
- disdrodb/l0/readers/PWS100/FRANCE/ENPC_PWS100.py +105 -99
- disdrodb/l0/readers/PWS100/FRANCE/ENPC_PWS100_SIRTA.py +151 -0
- disdrodb/l1/__init__.py +5 -0
- disdrodb/l1/fall_velocity.py +46 -0
- disdrodb/l1/filters.py +34 -20
- disdrodb/l1/processing.py +46 -45
- disdrodb/l1/resampling.py +77 -66
- disdrodb/l1_env/routines.py +18 -3
- disdrodb/l2/__init__.py +7 -0
- disdrodb/l2/empirical_dsd.py +58 -10
- disdrodb/l2/processing.py +268 -117
- disdrodb/metadata/checks.py +132 -125
- disdrodb/metadata/standards.py +3 -1
- disdrodb/psd/fitting.py +631 -345
- disdrodb/psd/models.py +9 -6
- disdrodb/routines/__init__.py +54 -0
- disdrodb/{l0/routines.py → routines/l0.py} +316 -355
- disdrodb/{l1/routines.py → routines/l1.py} +76 -116
- disdrodb/routines/l2.py +1019 -0
- disdrodb/{routines.py → routines/wrappers.py} +98 -10
- disdrodb/scattering/__init__.py +16 -4
- disdrodb/scattering/axis_ratio.py +61 -37
- disdrodb/scattering/permittivity.py +504 -0
- disdrodb/scattering/routines.py +746 -184
- disdrodb/summary/__init__.py +17 -0
- disdrodb/summary/routines.py +4196 -0
- disdrodb/utils/archiving.py +434 -0
- disdrodb/utils/attrs.py +68 -125
- disdrodb/utils/cli.py +5 -5
- disdrodb/utils/compression.py +30 -1
- disdrodb/utils/dask.py +121 -9
- disdrodb/utils/dataframe.py +61 -7
- disdrodb/utils/decorators.py +31 -0
- disdrodb/utils/directories.py +35 -15
- disdrodb/utils/encoding.py +37 -19
- disdrodb/{l2 → utils}/event.py +15 -173
- disdrodb/utils/logger.py +14 -7
- disdrodb/utils/manipulations.py +81 -0
- disdrodb/utils/routines.py +166 -0
- disdrodb/utils/subsetting.py +214 -0
- disdrodb/utils/time.py +35 -177
- disdrodb/utils/writer.py +20 -7
- disdrodb/utils/xarray.py +5 -4
- disdrodb/viz/__init__.py +13 -0
- disdrodb/viz/plots.py +398 -0
- {disdrodb-0.1.2.dist-info → disdrodb-0.1.4.dist-info}/METADATA +4 -3
- {disdrodb-0.1.2.dist-info → disdrodb-0.1.4.dist-info}/RECORD +139 -98
- {disdrodb-0.1.2.dist-info → disdrodb-0.1.4.dist-info}/entry_points.txt +2 -0
- disdrodb/l1/encoding_attrs.py +0 -642
- disdrodb/l2/processing_options.py +0 -213
- disdrodb/l2/routines.py +0 -868
- /disdrodb/l0/readers/PARSIVEL/SLOVENIA/{UL_FGG.py → UL.py} +0 -0
- {disdrodb-0.1.2.dist-info → disdrodb-0.1.4.dist-info}/WHEEL +0 -0
- {disdrodb-0.1.2.dist-info → disdrodb-0.1.4.dist-info}/licenses/LICENSE +0 -0
- {disdrodb-0.1.2.dist-info → disdrodb-0.1.4.dist-info}/top_level.txt +0 -0
disdrodb/psd/fitting.py
CHANGED
|
@@ -20,98 +20,150 @@ 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,
|
|
24
|
-
|
|
23
|
+
from scipy.special import gamma, gammaln # Regularized lower incomplete gamma function
|
|
24
|
+
|
|
25
|
+
from disdrodb.constants import DIAMETER_DIMENSION
|
|
26
|
+
from disdrodb.l1.fall_velocity import get_dataset_fall_velocity
|
|
27
|
+
from disdrodb.l2.empirical_dsd import (
|
|
28
|
+
get_median_volume_drop_diameter,
|
|
29
|
+
get_moment,
|
|
30
|
+
get_normalized_intercept_parameter_from_moments,
|
|
31
|
+
get_total_number_concentration,
|
|
32
|
+
)
|
|
25
33
|
from disdrodb.psd.models import ExponentialPSD, GammaPSD, LognormalPSD, NormalizedGammaPSD
|
|
34
|
+
from disdrodb.utils.manipulations import get_diameter_bin_edges
|
|
26
35
|
from disdrodb.utils.warnings import suppress_warnings
|
|
27
36
|
|
|
28
37
|
# gamma(>171) return inf !
|
|
29
38
|
|
|
39
|
+
####--------------------------------------------------------------------------------------.
|
|
40
|
+
#### Notes
|
|
41
|
+
## Variable requirements for fitting PSD Models
|
|
42
|
+
# - drop_number_concentration and diameter coordinates
|
|
43
|
+
# - Always recompute other parameters to ensure not use model parameters of L2M
|
|
44
|
+
|
|
45
|
+
# ML: None
|
|
46
|
+
|
|
47
|
+
# MOM: moments
|
|
48
|
+
# --> get_moment(drop_number_concentration, diameter, diameter_bin_width, moment)
|
|
49
|
+
|
|
50
|
+
# GS: fall_velocity if target optimization is R (rain)
|
|
51
|
+
# - NormalizedGamma: "Nw", "D50"
|
|
52
|
+
# --> get_normalized_intercept_parameter_from_moments(moment_3, moment_4)
|
|
53
|
+
# --> get_median_volume_drop_diameter(drop_number_concentration, diameter, diameter_bin_width):
|
|
54
|
+
# --> get_mean_volume_drop_diameter(moment_3, moment_4) (Dm)
|
|
55
|
+
|
|
56
|
+
# - LogNormal,Exponential, Gamma: Nt
|
|
57
|
+
# --> get_total_number_concentration(drop_number_concentration, diameter_bin_width)
|
|
58
|
+
|
|
30
59
|
|
|
31
60
|
####--------------------------------------------------------------------------------------.
|
|
32
61
|
#### Goodness of fit (GOF)
|
|
33
|
-
def compute_gof_stats(
|
|
62
|
+
def compute_gof_stats(obs, pred, dim=DIAMETER_DIMENSION):
|
|
34
63
|
"""
|
|
35
|
-
Compute various goodness-of-fit (GoF) statistics between
|
|
64
|
+
Compute various goodness-of-fit (GoF) statistics between obs and predicted values.
|
|
36
65
|
|
|
37
66
|
Parameters
|
|
38
67
|
----------
|
|
39
|
-
|
|
40
|
-
|
|
68
|
+
obs: xarray.DataArray
|
|
69
|
+
Observations DataArray with at least dimension ``dim``.
|
|
70
|
+
pred: xarray.DataArray
|
|
71
|
+
Predictions DataArray with at least dimension ``dim``.
|
|
72
|
+
dim: str
|
|
73
|
+
DataArray dimension over which to compute GOF statistics.
|
|
74
|
+
The default is DIAMETER_DIMENSION.
|
|
41
75
|
|
|
42
76
|
Returns
|
|
43
77
|
-------
|
|
44
|
-
|
|
78
|
+
ds: xarray.Dataset
|
|
79
|
+
Dataset containing the computed GoF statistics.
|
|
45
80
|
"""
|
|
46
81
|
from disdrodb.l2.empirical_dsd import get_mode_diameter
|
|
47
82
|
|
|
48
|
-
# Retrieve diameter bin width
|
|
49
|
-
diameter =
|
|
50
|
-
diameter_bin_width =
|
|
83
|
+
# Retrieve diameter and diameter bin width
|
|
84
|
+
diameter = obs["diameter_bin_center"]
|
|
85
|
+
diameter_bin_width = obs["diameter_bin_width"]
|
|
86
|
+
|
|
87
|
+
# Compute errors
|
|
88
|
+
error = obs - pred
|
|
51
89
|
|
|
52
|
-
#
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
90
|
+
# Compute max obs and pred
|
|
91
|
+
obs_max = obs.max(dim=dim, skipna=False)
|
|
92
|
+
pred_max = pred.max(dim=dim, skipna=False)
|
|
93
|
+
|
|
94
|
+
# Compute NaN mask
|
|
95
|
+
mask_nan = np.logical_or(np.isnan(obs_max), np.isnan(pred_max))
|
|
56
96
|
|
|
57
97
|
# Compute GOF statistics
|
|
58
98
|
with suppress_warnings():
|
|
59
|
-
# Compute Pearson
|
|
60
|
-
pearson_r = xr.corr(
|
|
61
|
-
|
|
62
|
-
# Compute
|
|
63
|
-
|
|
99
|
+
# Compute Pearson Correlation
|
|
100
|
+
pearson_r = xr.corr(obs, pred, dim=dim)
|
|
101
|
+
|
|
102
|
+
# Compute Mean Absolute Error (MAE)
|
|
103
|
+
mae = np.abs(error).mean(dim=dim, skipna=False)
|
|
104
|
+
|
|
105
|
+
# Compute maximum absolute error
|
|
106
|
+
max_error = np.abs(error).max(dim=dim, skipna=False)
|
|
107
|
+
relative_max_error = xr.where(max_error == 0, 0, xr.where(obs_max == 0, np.nan, max_error / obs_max))
|
|
108
|
+
|
|
109
|
+
# Compute deviation of N(D) at distribution mode
|
|
110
|
+
mode_deviation = obs_max - pred_max
|
|
111
|
+
mode_relative_deviation = xr.where(
|
|
112
|
+
mode_deviation == 0,
|
|
113
|
+
0,
|
|
114
|
+
xr.where(obs_max == 0, np.nan, mode_deviation / obs_max),
|
|
115
|
+
)
|
|
64
116
|
|
|
65
|
-
# Compute
|
|
66
|
-
|
|
67
|
-
|
|
117
|
+
# Compute diameter difference of the distribution mode
|
|
118
|
+
diameter_mode_pred = get_mode_diameter(pred, diameter)
|
|
119
|
+
diameter_mode_obs = get_mode_diameter(obs, diameter)
|
|
120
|
+
diameter_mode_deviation = diameter_mode_obs - diameter_mode_pred
|
|
68
121
|
|
|
69
122
|
# Compute difference in total number concentration
|
|
70
|
-
total_number_concentration_obs = (
|
|
71
|
-
total_number_concentration_pred = (
|
|
123
|
+
total_number_concentration_obs = (obs * diameter_bin_width).sum(dim=dim, skipna=False)
|
|
124
|
+
total_number_concentration_pred = (pred * diameter_bin_width).sum(dim=dim, skipna=False)
|
|
72
125
|
total_number_concentration_difference = total_number_concentration_pred - total_number_concentration_obs
|
|
73
126
|
|
|
74
127
|
# Compute Kullback-Leibler divergence
|
|
75
128
|
# - Compute pdf per bin
|
|
76
|
-
pk_pdf =
|
|
77
|
-
qk_pdf =
|
|
129
|
+
pk_pdf = obs / total_number_concentration_obs
|
|
130
|
+
qk_pdf = pred / total_number_concentration_pred
|
|
78
131
|
|
|
79
132
|
# - Compute probabilities per bin
|
|
80
133
|
pk = pk_pdf * diameter_bin_width
|
|
81
|
-
pk = pk / pk.sum(dim=
|
|
134
|
+
pk = pk / pk.sum(dim=dim, skipna=False) # this might not be necessary
|
|
82
135
|
qk = qk_pdf * diameter_bin_width
|
|
83
|
-
qk = qk / qk.sum(dim=
|
|
136
|
+
qk = qk / qk.sum(dim=dim, skipna=False) # this might not be necessary
|
|
84
137
|
|
|
85
|
-
# - Compute
|
|
138
|
+
# - Compute log probability ratio
|
|
139
|
+
epsilon = 1e-10
|
|
140
|
+
pk = xr.where(pk == 0, epsilon, pk)
|
|
141
|
+
qk = xr.where(qk == 0, epsilon, qk)
|
|
86
142
|
log_prob_ratio = np.log(pk / qk)
|
|
87
143
|
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
144
|
|
|
90
|
-
#
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
max_relative_deviation = max_deviation / fitted_values.max(dim="diameter_bin_center")
|
|
94
|
-
|
|
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
|
-
)
|
|
145
|
+
# - Compute divergence
|
|
146
|
+
kl_divergence = (pk * log_prob_ratio).sum(dim=dim, skipna=False)
|
|
147
|
+
kl_divergence = xr.where((error == 0).all(dim=dim), 0, kl_divergence)
|
|
100
148
|
|
|
101
149
|
# Create an xarray.Dataset to hold the computed statistics
|
|
102
150
|
ds = xr.Dataset(
|
|
103
151
|
{
|
|
104
|
-
"
|
|
105
|
-
"
|
|
106
|
-
"
|
|
107
|
-
"
|
|
108
|
-
"
|
|
109
|
-
"
|
|
110
|
-
"
|
|
111
|
-
"
|
|
112
|
-
"
|
|
152
|
+
"R2": pearson_r**2, # Squared Pearson correlation coefficient
|
|
153
|
+
"MAE": mae, # Mean Absolute Error
|
|
154
|
+
"MaxAE": max_error, # Maximum Absolute Error
|
|
155
|
+
"RelMaxAE": relative_max_error, # Relative Maximum Absolute Error
|
|
156
|
+
"PeakDiff": mode_deviation, # Difference at distribution peak
|
|
157
|
+
"RelPeakDiff": mode_relative_deviation, # Relative difference at peak
|
|
158
|
+
"DmodeDiff": diameter_mode_deviation, # Difference in mode diameters
|
|
159
|
+
"NtDiff": total_number_concentration_difference,
|
|
160
|
+
"KLDiv": kl_divergence, # Kullback-Leibler divergence
|
|
113
161
|
},
|
|
114
162
|
)
|
|
163
|
+
# Round
|
|
164
|
+
ds = ds.round(2)
|
|
165
|
+
# Mask where input obs or pred is NaN
|
|
166
|
+
ds = ds.where(~mask_nan)
|
|
115
167
|
return ds
|
|
116
168
|
|
|
117
169
|
|
|
@@ -176,13 +228,13 @@ def get_expected_probabilities(params, cdf_func, pdf_func, bin_edges, probabilit
|
|
|
176
228
|
def get_adjusted_nt(cdf, params, Nt, bin_edges):
|
|
177
229
|
"""Adjust Nt for the proportion of missing drops. See Johnson's et al., 2013 Eqs. 3 and 4."""
|
|
178
230
|
# Estimate proportion of missing drops (Johnson's 2011 Eqs. 3)
|
|
179
|
-
# --> Alternative:
|
|
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) # [-]
|
|
180
234
|
p = 1 - np.diff(cdf([bin_edges[0], bin_edges[-1]], params)).item() # [-]
|
|
181
|
-
# Adjusts Nt for the proportion of drops
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
return np.nan
|
|
185
|
-
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
|
|
186
238
|
|
|
187
239
|
|
|
188
240
|
def compute_negative_log_likelihood(
|
|
@@ -206,7 +258,7 @@ def compute_negative_log_likelihood(
|
|
|
206
258
|
bin_edges : array-like
|
|
207
259
|
Edges of the bins (length N+1).
|
|
208
260
|
counts : array-like
|
|
209
|
-
|
|
261
|
+
obs counts in each bin (length N).
|
|
210
262
|
cdf_func : callable
|
|
211
263
|
Cumulative distribution function of the distribution.
|
|
212
264
|
pdf_func : callable
|
|
@@ -259,6 +311,8 @@ def compute_negative_log_likelihood(
|
|
|
259
311
|
|
|
260
312
|
def estimate_lognormal_parameters(
|
|
261
313
|
counts,
|
|
314
|
+
mu,
|
|
315
|
+
sigma,
|
|
262
316
|
bin_edges,
|
|
263
317
|
probability_method="cdf",
|
|
264
318
|
likelihood="multinomial",
|
|
@@ -273,6 +327,12 @@ def estimate_lognormal_parameters(
|
|
|
273
327
|
----------
|
|
274
328
|
counts : array-like
|
|
275
329
|
The counts for each bin in the histogram.
|
|
330
|
+
mu: float
|
|
331
|
+
The initial guess of the mean of the log of the distribution.
|
|
332
|
+
A good default value is 0.
|
|
333
|
+
sigma: float
|
|
334
|
+
The initial guess of the standard deviation of the log distribution.
|
|
335
|
+
A good default value is 1.
|
|
276
336
|
bin_edges : array-like
|
|
277
337
|
The edges of the bins.
|
|
278
338
|
probability_method : str, optional
|
|
@@ -306,9 +366,9 @@ def estimate_lognormal_parameters(
|
|
|
306
366
|
----------
|
|
307
367
|
.. [1] https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.lognorm.html#scipy.stats.lognorm
|
|
308
368
|
"""
|
|
309
|
-
#
|
|
310
|
-
|
|
311
|
-
|
|
369
|
+
# Definite initial guess for the parameters
|
|
370
|
+
scale = np.exp(mu) # mu = np.log(scale)
|
|
371
|
+
initial_params = [sigma, scale]
|
|
312
372
|
|
|
313
373
|
# Initialize bad results
|
|
314
374
|
null_output = (
|
|
@@ -329,9 +389,6 @@ def estimate_lognormal_parameters(
|
|
|
329
389
|
sigma, scale = params
|
|
330
390
|
return sigma > 0 and scale > 0
|
|
331
391
|
|
|
332
|
-
# Definite initial guess for the parameters
|
|
333
|
-
initial_params = [1.0, 1.0] # sigma, scale
|
|
334
|
-
|
|
335
392
|
# Define bounds for sigma and scale
|
|
336
393
|
bounds = [(1e-6, None), (1e-6, None)]
|
|
337
394
|
|
|
@@ -375,6 +432,7 @@ def estimate_lognormal_parameters(
|
|
|
375
432
|
|
|
376
433
|
def estimate_exponential_parameters(
|
|
377
434
|
counts,
|
|
435
|
+
Lambda,
|
|
378
436
|
bin_edges,
|
|
379
437
|
probability_method="cdf",
|
|
380
438
|
likelihood="multinomial",
|
|
@@ -389,6 +447,10 @@ def estimate_exponential_parameters(
|
|
|
389
447
|
----------
|
|
390
448
|
counts : array-like
|
|
391
449
|
The counts for each bin in the histogram.
|
|
450
|
+
Lambda : float
|
|
451
|
+
The initial guess of the scale parameter.
|
|
452
|
+
scale = 1 / lambda correspond to the scale parameter of the scipy.stats.expon distribution.
|
|
453
|
+
A good default value is 1.
|
|
392
454
|
bin_edges : array-like
|
|
393
455
|
The edges of the bins.
|
|
394
456
|
probability_method : str, optional
|
|
@@ -421,6 +483,10 @@ def estimate_exponential_parameters(
|
|
|
421
483
|
----------
|
|
422
484
|
.. [1] https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.expon.html
|
|
423
485
|
"""
|
|
486
|
+
# Definite initial guess for parameters
|
|
487
|
+
scale = 1 / Lambda
|
|
488
|
+
initial_params = [scale]
|
|
489
|
+
|
|
424
490
|
# Initialize bad results
|
|
425
491
|
null_output = {"N0": np.nan, "Lambda": np.nan} if output_dictionary else np.array([np.nan, np.nan])
|
|
426
492
|
|
|
@@ -438,9 +504,6 @@ def estimate_exponential_parameters(
|
|
|
438
504
|
scale = params[0]
|
|
439
505
|
return scale > 0
|
|
440
506
|
|
|
441
|
-
# Definite initial guess for the scale parameter
|
|
442
|
-
initial_params = [1.0] # scale
|
|
443
|
-
|
|
444
507
|
# Define bounds for scale
|
|
445
508
|
bounds = [(1e-6, None)]
|
|
446
509
|
|
|
@@ -485,8 +548,8 @@ def estimate_exponential_parameters(
|
|
|
485
548
|
|
|
486
549
|
def estimate_gamma_parameters(
|
|
487
550
|
counts,
|
|
488
|
-
|
|
489
|
-
|
|
551
|
+
mu,
|
|
552
|
+
Lambda,
|
|
490
553
|
bin_edges,
|
|
491
554
|
probability_method="cdf",
|
|
492
555
|
likelihood="multinomial",
|
|
@@ -501,11 +564,13 @@ def estimate_gamma_parameters(
|
|
|
501
564
|
----------
|
|
502
565
|
counts : array-like
|
|
503
566
|
The counts for each bin in the histogram.
|
|
504
|
-
|
|
505
|
-
The
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
567
|
+
mu: float
|
|
568
|
+
The initial guess of the shape parameter.
|
|
569
|
+
a = mu + 1 correspond to the shape parameter of the scipy.stats.gamma distribution.
|
|
570
|
+
A good default value is 0.
|
|
571
|
+
lambda: float
|
|
572
|
+
The initial guess of the scale parameter.
|
|
573
|
+
scale = 1 / lambda correspond to the scale parameter of the scipy.stats.gamma distribution.
|
|
509
574
|
A good default value is 1.
|
|
510
575
|
bin_edges : array-like
|
|
511
576
|
The edges of the bins.
|
|
@@ -541,6 +606,11 @@ def estimate_gamma_parameters(
|
|
|
541
606
|
.. [1] https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.gamma.html
|
|
542
607
|
|
|
543
608
|
"""
|
|
609
|
+
# Define initial guess for parameters
|
|
610
|
+
a = mu + 1 # (mu = a-1, a = mu+1)
|
|
611
|
+
scale = 1 / Lambda
|
|
612
|
+
initial_params = [a, scale]
|
|
613
|
+
|
|
544
614
|
# Initialize bad results
|
|
545
615
|
null_output = (
|
|
546
616
|
{"N0": np.nan, "mu": np.nan, "lambda": np.nan} if output_dictionary else np.array([np.nan, np.nan, np.nan])
|
|
@@ -561,9 +631,6 @@ def estimate_gamma_parameters(
|
|
|
561
631
|
a, scale = params
|
|
562
632
|
return a > 0.1 and scale > 0 # using a > 0 cause some troubles
|
|
563
633
|
|
|
564
|
-
# Definite initial guess for the parameters
|
|
565
|
-
initial_params = [a, scale] # (mu=a-1, a=mu+1)
|
|
566
|
-
|
|
567
634
|
# Define bounds for a and scale
|
|
568
635
|
bounds = [(1e-6, None), (1e-6, None)]
|
|
569
636
|
|
|
@@ -603,7 +670,7 @@ def estimate_gamma_parameters(
|
|
|
603
670
|
|
|
604
671
|
# Compute N0
|
|
605
672
|
# - Use logarithmic computations to prevent overflow
|
|
606
|
-
# - N0 = Nt * Lambda ** (mu + 1) / gamma(mu + 1)
|
|
673
|
+
# - N0 = Nt * Lambda ** (mu + 1) / gamma(mu + 1) # [m-3 * mm^(-mu-1)]
|
|
607
674
|
with suppress_warnings():
|
|
608
675
|
log_N0 = np.log(Nt) + (mu + 1) * np.log(Lambda) - gammaln(mu + 1)
|
|
609
676
|
N0 = np.exp(log_N0)
|
|
@@ -617,12 +684,59 @@ def estimate_gamma_parameters(
|
|
|
617
684
|
return output
|
|
618
685
|
|
|
619
686
|
|
|
687
|
+
def _get_initial_lognormal_parameters(ds, mom_method=None):
|
|
688
|
+
default_mu = 0 # mu = np.log(scale)
|
|
689
|
+
default_sigma = 1
|
|
690
|
+
if mom_method is None or mom_method == "None":
|
|
691
|
+
ds_init = xr.Dataset(
|
|
692
|
+
{
|
|
693
|
+
"mu": default_mu,
|
|
694
|
+
"sigma": default_sigma,
|
|
695
|
+
},
|
|
696
|
+
)
|
|
697
|
+
else:
|
|
698
|
+
ds_init = get_mom_parameters(
|
|
699
|
+
ds=ds,
|
|
700
|
+
psd_model="LognormalPSD",
|
|
701
|
+
mom_methods=mom_method,
|
|
702
|
+
)
|
|
703
|
+
# If initialization results in some not finite number, set default value
|
|
704
|
+
ds_init["mu"] = xr.where(
|
|
705
|
+
np.logical_and(np.isfinite(ds_init["mu"]), ds_init["mu"] > 0),
|
|
706
|
+
ds_init["mu"],
|
|
707
|
+
default_mu,
|
|
708
|
+
)
|
|
709
|
+
ds_init["sigma"] = xr.where(np.isfinite(ds_init["sigma"]), ds_init["sigma"], default_sigma)
|
|
710
|
+
return ds_init
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
def _get_initial_exponential_parameters(ds, mom_method=None):
|
|
714
|
+
default_lambda = 1 # lambda = 1 /scale
|
|
715
|
+
if mom_method is None or mom_method == "None":
|
|
716
|
+
ds_init = xr.Dataset(
|
|
717
|
+
{
|
|
718
|
+
"Lambda": default_lambda,
|
|
719
|
+
},
|
|
720
|
+
)
|
|
721
|
+
else:
|
|
722
|
+
ds_init = get_mom_parameters(
|
|
723
|
+
ds=ds,
|
|
724
|
+
psd_model="ExponentialPSD",
|
|
725
|
+
mom_methods=mom_method,
|
|
726
|
+
)
|
|
727
|
+
# If initialization results in some not finite number, set default value
|
|
728
|
+
ds_init["Lambda"] = xr.where(np.isfinite(ds_init["Lambda"]), ds_init["Lambda"], default_lambda)
|
|
729
|
+
return ds_init
|
|
730
|
+
|
|
731
|
+
|
|
620
732
|
def _get_initial_gamma_parameters(ds, mom_method=None):
|
|
621
|
-
|
|
733
|
+
default_mu = 0 # a = mu + 1 | mu = a - 1
|
|
734
|
+
default_lambda = 1 # scale = 1 / Lambda
|
|
735
|
+
if mom_method is None or mom_method == "None":
|
|
622
736
|
ds_init = xr.Dataset(
|
|
623
737
|
{
|
|
624
|
-
"
|
|
625
|
-
"
|
|
738
|
+
"mu": default_mu,
|
|
739
|
+
"Lambda": default_lambda,
|
|
626
740
|
},
|
|
627
741
|
)
|
|
628
742
|
else:
|
|
@@ -631,11 +745,13 @@ def _get_initial_gamma_parameters(ds, mom_method=None):
|
|
|
631
745
|
psd_model="GammaPSD",
|
|
632
746
|
mom_methods=mom_method,
|
|
633
747
|
)
|
|
634
|
-
ds_init["a"] = ds_init["mu"] + 1
|
|
635
|
-
ds_init["scale"] = 1 / ds_init["Lambda"]
|
|
636
748
|
# If initialization results in some not finite number, set default value
|
|
637
|
-
ds_init["
|
|
638
|
-
|
|
749
|
+
ds_init["mu"] = xr.where(
|
|
750
|
+
np.logical_and(np.isfinite(ds_init["mu"]), ds_init["mu"] > -1),
|
|
751
|
+
ds_init["mu"],
|
|
752
|
+
default_mu,
|
|
753
|
+
)
|
|
754
|
+
ds_init["Lambda"] = xr.where(np.isfinite(ds_init["Lambda"]), ds_init["Lambda"], default_lambda)
|
|
639
755
|
return ds_init
|
|
640
756
|
|
|
641
757
|
|
|
@@ -663,13 +779,14 @@ def get_gamma_parameters(
|
|
|
663
779
|
with the specified mom_method.
|
|
664
780
|
init_method: str or list
|
|
665
781
|
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).
|
|
782
|
+
If None (or 'None'), the scale parameter is set to 1 and mu to 0 (a=1).
|
|
667
783
|
probability_method : str, optional
|
|
668
784
|
Method to compute probabilities. The default value is ``cdf``.
|
|
669
785
|
likelihood : str, optional
|
|
670
786
|
Likelihood function to use for fitting. The default value is ``multinomial``.
|
|
671
787
|
truncated_likelihood : bool, optional
|
|
672
788
|
Whether to use truncated likelihood. The default value is ``True``.
|
|
789
|
+
See Johnson et al., 2011 and 2011 for more information.
|
|
673
790
|
optimizer : str, optional
|
|
674
791
|
Optimization method to use. The default value is ``Nelder-Mead``.
|
|
675
792
|
|
|
@@ -687,12 +804,21 @@ def get_gamma_parameters(
|
|
|
687
804
|
The function uses `xr.apply_ufunc` to fit the lognormal distribution parameters
|
|
688
805
|
in parallel, leveraging Dask for parallel computation.
|
|
689
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
|
+
|
|
690
816
|
"""
|
|
691
817
|
# Define inputs
|
|
692
818
|
counts = ds["drop_number_concentration"] * ds["diameter_bin_width"]
|
|
693
|
-
diameter_breaks =
|
|
819
|
+
diameter_breaks = get_diameter_bin_edges(ds)
|
|
694
820
|
|
|
695
|
-
# Define initial parameters (
|
|
821
|
+
# Define initial parameters (mu, Lambda)
|
|
696
822
|
ds_init = _get_initial_gamma_parameters(ds, mom_method=init_method)
|
|
697
823
|
|
|
698
824
|
# Define kwargs
|
|
@@ -709,10 +835,10 @@ def get_gamma_parameters(
|
|
|
709
835
|
da_params = xr.apply_ufunc(
|
|
710
836
|
estimate_gamma_parameters,
|
|
711
837
|
counts,
|
|
712
|
-
ds_init["
|
|
713
|
-
ds_init["
|
|
838
|
+
ds_init["mu"],
|
|
839
|
+
ds_init["Lambda"],
|
|
714
840
|
kwargs=kwargs,
|
|
715
|
-
input_core_dims=[[
|
|
841
|
+
input_core_dims=[[DIAMETER_DIMENSION], [], []],
|
|
716
842
|
output_core_dims=[["parameters"]],
|
|
717
843
|
vectorize=True,
|
|
718
844
|
dask="parallelized",
|
|
@@ -720,8 +846,6 @@ def get_gamma_parameters(
|
|
|
720
846
|
output_dtypes=["float64"],
|
|
721
847
|
)
|
|
722
848
|
|
|
723
|
-
ds_init.isel(velocity_method=0, time=-3)
|
|
724
|
-
|
|
725
849
|
# Add parameters coordinates
|
|
726
850
|
da_params = da_params.assign_coords({"parameters": ["N0", "mu", "Lambda"]})
|
|
727
851
|
|
|
@@ -735,7 +859,7 @@ def get_gamma_parameters(
|
|
|
735
859
|
|
|
736
860
|
def get_lognormal_parameters(
|
|
737
861
|
ds,
|
|
738
|
-
init_method=None,
|
|
862
|
+
init_method=None,
|
|
739
863
|
probability_method="cdf",
|
|
740
864
|
likelihood="multinomial",
|
|
741
865
|
truncated_likelihood=True,
|
|
@@ -779,7 +903,10 @@ def get_lognormal_parameters(
|
|
|
779
903
|
"""
|
|
780
904
|
# Define inputs
|
|
781
905
|
counts = ds["drop_number_concentration"] * ds["diameter_bin_width"]
|
|
782
|
-
diameter_breaks =
|
|
906
|
+
diameter_breaks = get_diameter_bin_edges(ds)
|
|
907
|
+
|
|
908
|
+
# Define initial parameters (mu, sigma)
|
|
909
|
+
ds_init = _get_initial_lognormal_parameters(ds, mom_method=init_method)
|
|
783
910
|
|
|
784
911
|
# Define kwargs
|
|
785
912
|
kwargs = {
|
|
@@ -795,8 +922,10 @@ def get_lognormal_parameters(
|
|
|
795
922
|
da_params = xr.apply_ufunc(
|
|
796
923
|
estimate_lognormal_parameters,
|
|
797
924
|
counts,
|
|
925
|
+
ds_init["mu"],
|
|
926
|
+
ds_init["sigma"],
|
|
798
927
|
kwargs=kwargs,
|
|
799
|
-
input_core_dims=[[
|
|
928
|
+
input_core_dims=[[DIAMETER_DIMENSION], [], []],
|
|
800
929
|
output_core_dims=[["parameters"]],
|
|
801
930
|
vectorize=True,
|
|
802
931
|
dask="parallelized",
|
|
@@ -818,7 +947,7 @@ def get_lognormal_parameters(
|
|
|
818
947
|
|
|
819
948
|
def get_exponential_parameters(
|
|
820
949
|
ds,
|
|
821
|
-
init_method=None,
|
|
950
|
+
init_method=None,
|
|
822
951
|
probability_method="cdf",
|
|
823
952
|
likelihood="multinomial",
|
|
824
953
|
truncated_likelihood=True,
|
|
@@ -863,8 +992,11 @@ def get_exponential_parameters(
|
|
|
863
992
|
|
|
864
993
|
"""
|
|
865
994
|
# Define inputs
|
|
866
|
-
counts = ds["drop_number_concentration"] * ds["diameter_bin_width"]
|
|
867
|
-
diameter_breaks =
|
|
995
|
+
counts = ds["drop_number_concentration"] * ds["diameter_bin_width"] # mm-1 m-3 --> m-3
|
|
996
|
+
diameter_breaks = get_diameter_bin_edges(ds)
|
|
997
|
+
|
|
998
|
+
# Define initial parameters (Lambda)
|
|
999
|
+
ds_init = _get_initial_exponential_parameters(ds, mom_method=init_method)
|
|
868
1000
|
|
|
869
1001
|
# Define kwargs
|
|
870
1002
|
kwargs = {
|
|
@@ -880,8 +1012,9 @@ def get_exponential_parameters(
|
|
|
880
1012
|
da_params = xr.apply_ufunc(
|
|
881
1013
|
estimate_exponential_parameters,
|
|
882
1014
|
counts,
|
|
1015
|
+
ds_init["Lambda"],
|
|
883
1016
|
kwargs=kwargs,
|
|
884
|
-
input_core_dims=[[
|
|
1017
|
+
input_core_dims=[[DIAMETER_DIMENSION], []],
|
|
885
1018
|
output_core_dims=[["parameters"]],
|
|
886
1019
|
vectorize=True,
|
|
887
1020
|
dask="parallelized",
|
|
@@ -900,162 +1033,6 @@ def get_exponential_parameters(
|
|
|
900
1033
|
return ds_params
|
|
901
1034
|
|
|
902
1035
|
|
|
903
|
-
####-------------------------------------------------------------------------------------------------------------------.
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
def _estimate_gamma_parameters_johnson(
|
|
907
|
-
drop_number_concentration,
|
|
908
|
-
diameter,
|
|
909
|
-
diameter_breaks,
|
|
910
|
-
output_dictionary=True,
|
|
911
|
-
method="Nelder-Mead",
|
|
912
|
-
mu=0.5,
|
|
913
|
-
Lambda=3,
|
|
914
|
-
**kwargs,
|
|
915
|
-
):
|
|
916
|
-
"""Deprecated Maximum likelihood estimation of Gamma model.
|
|
917
|
-
|
|
918
|
-
N(D) = N_t * lambda**(mu+1) / gamma(mu+1) D**mu exp(-lambda*D)
|
|
919
|
-
|
|
920
|
-
Args:
|
|
921
|
-
spectra: The DSD for which to find parameters [mm-1 m-3].
|
|
922
|
-
widths: Class widths for each DSD bin [mm].
|
|
923
|
-
diams: Class-centre diameters for each DSD bin [mm].
|
|
924
|
-
mu: Initial value for shape parameter mu [-].
|
|
925
|
-
lambda_param: Initial value for slope parameter lambda [mm^-1].
|
|
926
|
-
kwargs: Extra arguments for the optimization process.
|
|
927
|
-
|
|
928
|
-
Returns
|
|
929
|
-
-------
|
|
930
|
-
Dictionary with estimated mu, lambda, and N0.
|
|
931
|
-
mu (shape) N0 (scale) lambda(slope)
|
|
932
|
-
|
|
933
|
-
Notes
|
|
934
|
-
-----
|
|
935
|
-
The last bin counts are not accounted in the fitting procedure !
|
|
936
|
-
|
|
937
|
-
References
|
|
938
|
-
----------
|
|
939
|
-
Johnson, R. W., D. V. Kliche, and P. L. Smith, 2011: Comparison of Estimators for Parameters of Gamma Distributions
|
|
940
|
-
with Left-Truncated Samples. J. Appl. Meteor. Climatol., 50, 296-310, https://doi.org/10.1175/2010JAMC2478.1
|
|
941
|
-
|
|
942
|
-
Johnson, R.W., Kliche, D., & Smith, P.L. (2010).
|
|
943
|
-
Maximum likelihood estimation of gamma parameters for coarsely binned and truncated raindrop size data.
|
|
944
|
-
Quarterly Journal of the Royal Meteorological Society, 140. DOI:10.1002/qj.2209
|
|
945
|
-
|
|
946
|
-
"""
|
|
947
|
-
# Initialize bad results
|
|
948
|
-
if output_dictionary:
|
|
949
|
-
null_output = {"mu": np.nan, "lambda": np.nan, "N0": np.nan}
|
|
950
|
-
else:
|
|
951
|
-
null_output = np.array([np.nan, np.nan, np.nan])
|
|
952
|
-
|
|
953
|
-
# Initialize parameters
|
|
954
|
-
# --> Ideally with method of moments estimate
|
|
955
|
-
# --> See equation 8 of Johnson's 2013
|
|
956
|
-
x0 = [mu, Lambda]
|
|
957
|
-
|
|
958
|
-
# Compute diameter_bin_width
|
|
959
|
-
diameter_bin_width = np.diff(diameter_breaks)
|
|
960
|
-
|
|
961
|
-
# Convert drop_number_concentration from mm-1 m-3 to m-3.
|
|
962
|
-
spectra = np.asarray(drop_number_concentration) * diameter_bin_width
|
|
963
|
-
|
|
964
|
-
# Define cost function
|
|
965
|
-
# - Parameter to be optimized on first positions
|
|
966
|
-
def _cost_function(parameters, spectra, diameter_breaks):
|
|
967
|
-
# Assume spectra to be in unit [m-3] (drop_number_concentration*diameter_bin_width) !
|
|
968
|
-
mu, Lambda = parameters
|
|
969
|
-
# Precompute gamma integrals between various diameter bins
|
|
970
|
-
# - gamminc(mu+1) already divides the integral by gamma(mu+1) !
|
|
971
|
-
pgamma_d = gammainc(mu + 1, Lambda * diameter_breaks)
|
|
972
|
-
# Compute probability with interval
|
|
973
|
-
delta_pgamma_bins = pgamma_d[1:] - pgamma_d[:-1]
|
|
974
|
-
# Compute normalization over interval
|
|
975
|
-
denominator = pgamma_d[-1] - pgamma_d[0]
|
|
976
|
-
# Compute cost function
|
|
977
|
-
# a = mu - 1, x = lambda
|
|
978
|
-
if mu > -1 and Lambda > 0:
|
|
979
|
-
cost = np.sum(-spectra * np.log(delta_pgamma_bins / denominator))
|
|
980
|
-
return cost
|
|
981
|
-
return np.inf
|
|
982
|
-
|
|
983
|
-
# Minimize the cost function
|
|
984
|
-
with suppress_warnings():
|
|
985
|
-
bounds = [(0, None), (0, None)] # Force mu and lambda to be non-negative
|
|
986
|
-
res = minimize(
|
|
987
|
-
_cost_function,
|
|
988
|
-
x0=x0,
|
|
989
|
-
args=(spectra, diameter_breaks),
|
|
990
|
-
method=method,
|
|
991
|
-
bounds=bounds,
|
|
992
|
-
**kwargs,
|
|
993
|
-
)
|
|
994
|
-
|
|
995
|
-
# Check if the fit had success
|
|
996
|
-
if not res.success:
|
|
997
|
-
return null_output
|
|
998
|
-
|
|
999
|
-
# Extract parameters
|
|
1000
|
-
mu = res.x[0] # [-]
|
|
1001
|
-
Lambda = res.x[1] # [mm-1]
|
|
1002
|
-
|
|
1003
|
-
# Estimate tilde_N_T using the total drop concentration
|
|
1004
|
-
tilde_N_T = np.sum(drop_number_concentration * diameter_bin_width) # [m-3]
|
|
1005
|
-
|
|
1006
|
-
# Estimate proportion of missing drops (Johnson's 2011 Eqs. 3)
|
|
1007
|
-
with suppress_warnings():
|
|
1008
|
-
D = diameter
|
|
1009
|
-
p = 1 - np.sum((Lambda ** (mu + 1)) / gamma(mu + 1) * D**mu * np.exp(-Lambda * D) * diameter_bin_width) # [-]
|
|
1010
|
-
|
|
1011
|
-
# Convert tilde_N_T to N_T using Johnson's 2013 Eqs. 3 and 4.
|
|
1012
|
-
# - Adjusts for the proportion of drops not observed
|
|
1013
|
-
N_T = tilde_N_T / (1 - p) # [m-3]
|
|
1014
|
-
|
|
1015
|
-
# Compute N0
|
|
1016
|
-
N0 = N_T * (Lambda ** (mu + 1)) / gamma(mu + 1) # [m-3 * mm^(-mu-1)]
|
|
1017
|
-
|
|
1018
|
-
# Compute Dm
|
|
1019
|
-
# Dm = (mu + 4)/ Lambda
|
|
1020
|
-
|
|
1021
|
-
# Compute Nw
|
|
1022
|
-
# Nw = N0* D^mu / f(mu) , with f(mu of the Normalized PSD)
|
|
1023
|
-
|
|
1024
|
-
# Define output
|
|
1025
|
-
output = {"mu": mu, "Lambda": Lambda, "N0": N0} if output_dictionary else np.array([mu, Lambda, N0])
|
|
1026
|
-
return output
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
def get_gamma_parameters_johnson2014(ds, method="Nelder-Mead"):
|
|
1030
|
-
"""Deprecated model. See Gamma Model with truncated_likelihood and 'pdf'."""
|
|
1031
|
-
drop_number_concentration = ds["drop_number_concentration"]
|
|
1032
|
-
diameter = ds["diameter_bin_center"]
|
|
1033
|
-
diameter_breaks = np.append(ds["diameter_bin_lower"].data, ds["diameter_bin_upper"].data[-1])
|
|
1034
|
-
# Define kwargs
|
|
1035
|
-
kwargs = {
|
|
1036
|
-
"output_dictionary": False,
|
|
1037
|
-
"diameter_breaks": diameter_breaks,
|
|
1038
|
-
"method": method,
|
|
1039
|
-
}
|
|
1040
|
-
da_params = xr.apply_ufunc(
|
|
1041
|
-
_estimate_gamma_parameters_johnson,
|
|
1042
|
-
drop_number_concentration,
|
|
1043
|
-
diameter,
|
|
1044
|
-
# diameter_bin_width,
|
|
1045
|
-
kwargs=kwargs,
|
|
1046
|
-
input_core_dims=[["diameter_bin_center"], ["diameter_bin_center"]], # ["diameter_bin_center"],
|
|
1047
|
-
output_core_dims=[["parameters"]],
|
|
1048
|
-
vectorize=True,
|
|
1049
|
-
)
|
|
1050
|
-
|
|
1051
|
-
# Add parameters coordinates
|
|
1052
|
-
da_params = da_params.assign_coords({"parameters": ["mu", "Lambda", "N0"]})
|
|
1053
|
-
|
|
1054
|
-
# Convert to skill Dataset
|
|
1055
|
-
ds_params = da_params.to_dataset(dim="parameters")
|
|
1056
|
-
return ds_params
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
1036
|
####-----------------------------------------------------------------------------------------.
|
|
1060
1037
|
#### Grid Search (GS)
|
|
1061
1038
|
|
|
@@ -1079,24 +1056,60 @@ def _compute_z(ND, D, dD):
|
|
|
1079
1056
|
return Z
|
|
1080
1057
|
|
|
1081
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
|
+
|
|
1082
1069
|
def _compute_cost_function(ND_obs, ND_preds, D, dD, V, target, transformation, error_order):
|
|
1083
1070
|
# Assume ND_obs of shape (D bins) and ND_preds of shape (# params, D bins)
|
|
1084
1071
|
if target == "ND":
|
|
1085
1072
|
if transformation == "identity":
|
|
1086
1073
|
errors = np.mean(np.abs(ND_obs[None, :] - ND_preds) ** error_order, axis=1)
|
|
1074
|
+
return errors
|
|
1087
1075
|
if transformation == "log":
|
|
1088
1076
|
errors = np.mean(np.abs(np.log(ND_obs[None, :] + 1) - np.log(ND_preds + 1)) ** error_order, axis=1)
|
|
1089
|
-
|
|
1077
|
+
return errors
|
|
1078
|
+
if transformation == "sqrt":
|
|
1090
1079
|
errors = np.mean(np.abs(np.sqrt(ND_obs[None, :]) - np.sqrt(ND_preds)) ** error_order, axis=1)
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
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)
|
|
1100
1113
|
|
|
1101
1114
|
|
|
1102
1115
|
def apply_exponential_gs(
|
|
@@ -1132,9 +1145,15 @@ def apply_exponential_gs(
|
|
|
1132
1145
|
transformation=transformation,
|
|
1133
1146
|
error_order=error_order,
|
|
1134
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])
|
|
1135
1154
|
|
|
1136
|
-
#
|
|
1137
|
-
best_index = np.
|
|
1155
|
+
# Otherwise, choose the best index
|
|
1156
|
+
best_index = np.nanargmin(errors)
|
|
1138
1157
|
return np.array([N0_arr[best_index].item(), lambda_arr[best_index].item()])
|
|
1139
1158
|
|
|
1140
1159
|
|
|
@@ -1163,8 +1182,15 @@ def _apply_gamma_gs(mu_values, lambda_values, Nt, ND_obs, D, dD, V, target, tran
|
|
|
1163
1182
|
error_order=error_order,
|
|
1164
1183
|
)
|
|
1165
1184
|
|
|
1166
|
-
#
|
|
1167
|
-
|
|
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)
|
|
1168
1194
|
return N0[best_index].item(), mu_arr[best_index].item(), lambda_arr[best_index].item()
|
|
1169
1195
|
|
|
1170
1196
|
|
|
@@ -1181,6 +1207,10 @@ def apply_gamma_gs(
|
|
|
1181
1207
|
error_order,
|
|
1182
1208
|
):
|
|
1183
1209
|
"""Estimate GammaPSD model parameters using Grid Search."""
|
|
1210
|
+
# Define parameters bounds
|
|
1211
|
+
mu_bounds = (0.01, 20)
|
|
1212
|
+
lambda_bounds = (0.01, 60)
|
|
1213
|
+
|
|
1184
1214
|
# Define initial set of parameters
|
|
1185
1215
|
mu_step = 0.5
|
|
1186
1216
|
lambda_step = 0.5
|
|
@@ -1200,10 +1230,13 @@ def apply_gamma_gs(
|
|
|
1200
1230
|
transformation=transformation,
|
|
1201
1231
|
error_order=error_order,
|
|
1202
1232
|
)
|
|
1233
|
+
if np.isnan(N0): # if np.nan, return immediately
|
|
1234
|
+
return np.array([N0, mu, Lambda])
|
|
1203
1235
|
|
|
1204
1236
|
# Second round of GS
|
|
1205
|
-
mu_values =
|
|
1206
|
-
lambda_values =
|
|
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
|
+
|
|
1207
1240
|
N0, mu, Lambda = _apply_gamma_gs(
|
|
1208
1241
|
mu_values=mu_values,
|
|
1209
1242
|
lambda_values=lambda_values,
|
|
@@ -1244,8 +1277,15 @@ def _apply_lognormal_gs(mu_values, sigma_values, Nt, ND_obs, D, dD, V, target, t
|
|
|
1244
1277
|
error_order=error_order,
|
|
1245
1278
|
)
|
|
1246
1279
|
|
|
1247
|
-
#
|
|
1248
|
-
|
|
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)
|
|
1249
1289
|
return Nt, mu_arr[best_index].item(), sigma_arr[best_index].item()
|
|
1250
1290
|
|
|
1251
1291
|
|
|
@@ -1262,10 +1302,16 @@ def apply_lognormal_gs(
|
|
|
1262
1302
|
error_order,
|
|
1263
1303
|
):
|
|
1264
1304
|
"""Estimate LognormalPSD model parameters using Grid Search."""
|
|
1305
|
+
# Define parameters bounds
|
|
1306
|
+
sigma_bounds = (0, np.inf) # > 0
|
|
1307
|
+
scale_bounds = (0.1, np.inf) # > 0
|
|
1308
|
+
# mu_bounds = (- np.inf, np.inf) # mu = np.log(scale)
|
|
1309
|
+
|
|
1265
1310
|
# Define initial set of parameters
|
|
1266
|
-
|
|
1267
|
-
sigma_step = 0.
|
|
1268
|
-
|
|
1311
|
+
scale_step = 0.2
|
|
1312
|
+
sigma_step = 0.2
|
|
1313
|
+
scale_values = np.arange(0.1, 20, step=scale_step)
|
|
1314
|
+
mu_values = np.log(scale_values) # TODO: define realistic values
|
|
1269
1315
|
sigma_values = np.arange(0, 20, step=sigma_step) # TODO: define realistic values
|
|
1270
1316
|
|
|
1271
1317
|
# First round of GS
|
|
@@ -1281,10 +1327,13 @@ def apply_lognormal_gs(
|
|
|
1281
1327
|
transformation=transformation,
|
|
1282
1328
|
error_order=error_order,
|
|
1283
1329
|
)
|
|
1330
|
+
if np.isnan(mu): # if np.nan, return immediately
|
|
1331
|
+
return np.array([Nt, mu, sigma])
|
|
1284
1332
|
|
|
1285
1333
|
# Second round of GS
|
|
1286
|
-
|
|
1287
|
-
|
|
1334
|
+
sigma_values = define_param_range(sigma, sigma_step, bounds=sigma_bounds)
|
|
1335
|
+
scale_values = define_param_range(np.exp(mu), scale_step, bounds=scale_bounds)
|
|
1336
|
+
mu_values = np.log(scale_values)
|
|
1288
1337
|
Nt, mu, sigma = _apply_lognormal_gs(
|
|
1289
1338
|
mu_values=mu_values,
|
|
1290
1339
|
sigma_values=sigma_values,
|
|
@@ -1335,8 +1384,16 @@ def apply_normalized_gamma_gs(
|
|
|
1335
1384
|
error_order=error_order,
|
|
1336
1385
|
)
|
|
1337
1386
|
|
|
1338
|
-
#
|
|
1339
|
-
|
|
1387
|
+
# Replace inf with NaN
|
|
1388
|
+
errors[~np.isfinite(errors)] = np.nan
|
|
1389
|
+
|
|
1390
|
+
# If all invalid, return NaN parameters
|
|
1391
|
+
if np.all(np.isnan(errors)):
|
|
1392
|
+
return np.array([np.nan, np.nan, np.nan])
|
|
1393
|
+
|
|
1394
|
+
# Otherwise, choose the best index
|
|
1395
|
+
best_index = np.nanargmin(errors)
|
|
1396
|
+
mu = mu_arr[best_index]
|
|
1340
1397
|
return np.array([Nw, mu, D50])
|
|
1341
1398
|
|
|
1342
1399
|
|
|
@@ -1346,6 +1403,12 @@ def get_exponential_parameters_gs(ds, target="ND", transformation="log", error_o
|
|
|
1346
1403
|
# "transformation": "log", "identity", "sqrt", # only for drop_number_concentration
|
|
1347
1404
|
# "error_order": 1, # MAE/MSE ... only for drop_number_concentration
|
|
1348
1405
|
|
|
1406
|
+
# Compute required variables
|
|
1407
|
+
ds["Nt"] = get_total_number_concentration(
|
|
1408
|
+
drop_number_concentration=ds["drop_number_concentration"],
|
|
1409
|
+
diameter_bin_width=ds["diameter_bin_width"],
|
|
1410
|
+
)
|
|
1411
|
+
|
|
1349
1412
|
# Define kwargs
|
|
1350
1413
|
kwargs = {
|
|
1351
1414
|
"D": ds["diameter_bin_center"].data,
|
|
@@ -1365,7 +1428,7 @@ def get_exponential_parameters_gs(ds, target="ND", transformation="log", error_o
|
|
|
1365
1428
|
# Other options
|
|
1366
1429
|
kwargs=kwargs,
|
|
1367
1430
|
# Settings
|
|
1368
|
-
input_core_dims=[[], [
|
|
1431
|
+
input_core_dims=[[], [DIAMETER_DIMENSION], [DIAMETER_DIMENSION]],
|
|
1369
1432
|
output_core_dims=[["parameters"]],
|
|
1370
1433
|
vectorize=True,
|
|
1371
1434
|
dask="parallelized",
|
|
@@ -1390,6 +1453,12 @@ def get_gamma_parameters_gs(ds, target="ND", transformation="log", error_order=1
|
|
|
1390
1453
|
# "transformation": "log", "identity", "sqrt", # only for drop_number_concentration
|
|
1391
1454
|
# "error_order": 1, # MAE/MSE ... only for drop_number_concentration
|
|
1392
1455
|
|
|
1456
|
+
# Compute required variables
|
|
1457
|
+
ds["Nt"] = get_total_number_concentration(
|
|
1458
|
+
drop_number_concentration=ds["drop_number_concentration"],
|
|
1459
|
+
diameter_bin_width=ds["diameter_bin_width"],
|
|
1460
|
+
)
|
|
1461
|
+
|
|
1393
1462
|
# Define kwargs
|
|
1394
1463
|
kwargs = {
|
|
1395
1464
|
"D": ds["diameter_bin_center"].data,
|
|
@@ -1409,7 +1478,7 @@ def get_gamma_parameters_gs(ds, target="ND", transformation="log", error_order=1
|
|
|
1409
1478
|
# Other options
|
|
1410
1479
|
kwargs=kwargs,
|
|
1411
1480
|
# Settings
|
|
1412
|
-
input_core_dims=[[], [
|
|
1481
|
+
input_core_dims=[[], [DIAMETER_DIMENSION], [DIAMETER_DIMENSION]],
|
|
1413
1482
|
output_core_dims=[["parameters"]],
|
|
1414
1483
|
vectorize=True,
|
|
1415
1484
|
dask="parallelized",
|
|
@@ -1434,6 +1503,12 @@ def get_lognormal_parameters_gs(ds, target="ND", transformation="log", error_ord
|
|
|
1434
1503
|
# "transformation": "log", "identity", "sqrt", # only for drop_number_concentration
|
|
1435
1504
|
# "error_order": 1, # MAE/MSE ... only for drop_number_concentration
|
|
1436
1505
|
|
|
1506
|
+
# Compute required variables
|
|
1507
|
+
ds["Nt"] = get_total_number_concentration(
|
|
1508
|
+
drop_number_concentration=ds["drop_number_concentration"],
|
|
1509
|
+
diameter_bin_width=ds["diameter_bin_width"],
|
|
1510
|
+
)
|
|
1511
|
+
|
|
1437
1512
|
# Define kwargs
|
|
1438
1513
|
kwargs = {
|
|
1439
1514
|
"D": ds["diameter_bin_center"].data,
|
|
@@ -1453,7 +1528,7 @@ def get_lognormal_parameters_gs(ds, target="ND", transformation="log", error_ord
|
|
|
1453
1528
|
# Other options
|
|
1454
1529
|
kwargs=kwargs,
|
|
1455
1530
|
# Settings
|
|
1456
|
-
input_core_dims=[[], [
|
|
1531
|
+
input_core_dims=[[], [DIAMETER_DIMENSION], [DIAMETER_DIMENSION]],
|
|
1457
1532
|
output_core_dims=[["parameters"]],
|
|
1458
1533
|
vectorize=True,
|
|
1459
1534
|
dask="parallelized",
|
|
@@ -1475,8 +1550,8 @@ def get_lognormal_parameters_gs(ds, target="ND", transformation="log", error_ord
|
|
|
1475
1550
|
def get_normalized_gamma_parameters_gs(ds, target="ND", transformation="log", error_order=1):
|
|
1476
1551
|
r"""Estimate $\mu$ of a Normalized Gamma distribution using Grid Search.
|
|
1477
1552
|
|
|
1478
|
-
The D50 and Nw parameters of the Normalized Gamma distribution are derived empirically from the
|
|
1479
|
-
$\mu$ is derived by minimizing the errors between the
|
|
1553
|
+
The D50 and Nw parameters of the Normalized Gamma distribution are derived empirically from the obs DSD.
|
|
1554
|
+
$\mu$ is derived by minimizing the errors between the obs DSD and modelled Normalized Gamma distribution.
|
|
1480
1555
|
|
|
1481
1556
|
Parameters
|
|
1482
1557
|
----------
|
|
@@ -1501,6 +1576,29 @@ def get_normalized_gamma_parameters_gs(ds, target="ND", transformation="log", er
|
|
|
1501
1576
|
# "transformation": "log", "identity", "sqrt", # only for drop_number_concentration
|
|
1502
1577
|
# "error_order": 1, # MAE/MSE ... only for drop_number_concentration
|
|
1503
1578
|
|
|
1579
|
+
# Compute required variables
|
|
1580
|
+
drop_number_concentration = ds["drop_number_concentration"]
|
|
1581
|
+
diameter_bin_width = ds["diameter_bin_width"]
|
|
1582
|
+
diameter = ds["diameter_bin_center"] / 1000 # conversion from mm to m
|
|
1583
|
+
m3 = get_moment(
|
|
1584
|
+
drop_number_concentration=drop_number_concentration,
|
|
1585
|
+
diameter=diameter, # m
|
|
1586
|
+
diameter_bin_width=diameter_bin_width, # mm
|
|
1587
|
+
moment=3,
|
|
1588
|
+
)
|
|
1589
|
+
m4 = get_moment(
|
|
1590
|
+
drop_number_concentration=drop_number_concentration,
|
|
1591
|
+
diameter=diameter, # m
|
|
1592
|
+
diameter_bin_width=diameter_bin_width, # mm
|
|
1593
|
+
moment=4,
|
|
1594
|
+
)
|
|
1595
|
+
ds["Nw"] = get_normalized_intercept_parameter_from_moments(moment_3=m3, moment_4=m4)
|
|
1596
|
+
ds["D50"] = get_median_volume_drop_diameter(
|
|
1597
|
+
drop_number_concentration=drop_number_concentration,
|
|
1598
|
+
diameter=diameter, # m
|
|
1599
|
+
diameter_bin_width=diameter_bin_width, # mm
|
|
1600
|
+
)
|
|
1601
|
+
|
|
1504
1602
|
# Define kwargs
|
|
1505
1603
|
kwargs = {
|
|
1506
1604
|
"D": ds["diameter_bin_center"].data,
|
|
@@ -1521,7 +1619,7 @@ def get_normalized_gamma_parameters_gs(ds, target="ND", transformation="log", er
|
|
|
1521
1619
|
# Other options
|
|
1522
1620
|
kwargs=kwargs,
|
|
1523
1621
|
# Settings
|
|
1524
|
-
input_core_dims=[[], [], [
|
|
1622
|
+
input_core_dims=[[], [], [DIAMETER_DIMENSION], [DIAMETER_DIMENSION]],
|
|
1525
1623
|
output_core_dims=[["parameters"]],
|
|
1526
1624
|
vectorize=True,
|
|
1527
1625
|
dask="parallelized",
|
|
@@ -1569,6 +1667,8 @@ def get_exponential_parameters_Zhang2008(moment_l, moment_m, l, m): # noqa: E74
|
|
|
1569
1667
|
Meteor. Climatol.,
|
|
1570
1668
|
https://doi.org/10.1175/2008JAMC1876.1
|
|
1571
1669
|
"""
|
|
1670
|
+
if l == m:
|
|
1671
|
+
raise ValueError("Equal l and m moment orders are not allowed.")
|
|
1572
1672
|
num = moment_l * gamma(m + 1)
|
|
1573
1673
|
den = moment_m * gamma(l + 1)
|
|
1574
1674
|
Lambda = np.power(num / den, (1 / (m - l)))
|
|
@@ -1593,21 +1693,21 @@ def get_exponential_parameters_M34(moment_3, moment_4):
|
|
|
1593
1693
|
return N0, Lambda
|
|
1594
1694
|
|
|
1595
1695
|
|
|
1596
|
-
def get_gamma_parameters_M012(M0, M1, M2):
|
|
1597
|
-
|
|
1696
|
+
# def get_gamma_parameters_M012(M0, M1, M2):
|
|
1697
|
+
# """Compute gamma distribution parameters following Cao et al., 2009.
|
|
1598
1698
|
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1699
|
+
# References
|
|
1700
|
+
# ----------
|
|
1701
|
+
# Cao, Q., and G. Zhang, 2009:
|
|
1702
|
+
# Errors in Estimating Raindrop Size Distribution Parameters Employing Disdrometer and Simulated Raindrop Spectra.
|
|
1703
|
+
# J. Appl. Meteor. Climatol., 48, 406-425, https://doi.org/10.1175/2008JAMC2026.1.
|
|
1704
|
+
# """
|
|
1705
|
+
# # TODO: really bad results. check formula !
|
|
1706
|
+
# G = M1**3 / M0 / M2
|
|
1707
|
+
# mu = 1 / (1 - G) - 2
|
|
1708
|
+
# Lambda = M0 / M1 * (mu + 1)
|
|
1709
|
+
# N0 = Lambda ** (mu + 1) * M0 / gamma(mu + 1)
|
|
1710
|
+
# return N0, mu, Lambda
|
|
1611
1711
|
|
|
1612
1712
|
|
|
1613
1713
|
def get_gamma_parameters_M234(M2, M3, M4):
|
|
@@ -1735,12 +1835,25 @@ def get_lognormal_parameters_M346(M3, M4, M6):
|
|
|
1735
1835
|
return Nt, mu, sigma
|
|
1736
1836
|
|
|
1737
1837
|
|
|
1838
|
+
def _compute_moments(ds, moments):
|
|
1839
|
+
list_moments = [
|
|
1840
|
+
get_moment(
|
|
1841
|
+
drop_number_concentration=ds["drop_number_concentration"],
|
|
1842
|
+
diameter=ds["diameter_bin_center"] / 1000, # m
|
|
1843
|
+
diameter_bin_width=ds["diameter_bin_width"], # mm
|
|
1844
|
+
moment=int(moment.replace("M", "")),
|
|
1845
|
+
)
|
|
1846
|
+
for moment in moments
|
|
1847
|
+
]
|
|
1848
|
+
return list_moments
|
|
1849
|
+
|
|
1850
|
+
|
|
1738
1851
|
def _get_gamma_parameters_mom(ds: xr.Dataset, mom_method: str) -> xr.Dataset:
|
|
1739
1852
|
# Get the correct function and list of variables for the requested method
|
|
1740
1853
|
func, needed_moments = MOM_METHODS_DICT["GammaPSD"][mom_method]
|
|
1741
1854
|
|
|
1742
|
-
#
|
|
1743
|
-
arrs =
|
|
1855
|
+
# Compute required moments
|
|
1856
|
+
arrs = _compute_moments(ds, moments=needed_moments)
|
|
1744
1857
|
|
|
1745
1858
|
# Apply the function. This will produce (mu, Lambda, N0) with the same coords/shapes as input data
|
|
1746
1859
|
N0, mu, Lambda = func(*arrs)
|
|
@@ -1761,8 +1874,8 @@ def _get_lognormal_parameters_mom(ds: xr.Dataset, mom_method: str) -> xr.Dataset
|
|
|
1761
1874
|
# Get the correct function and list of variables for the requested method
|
|
1762
1875
|
func, needed_moments = MOM_METHODS_DICT["LognormalPSD"][mom_method]
|
|
1763
1876
|
|
|
1764
|
-
#
|
|
1765
|
-
arrs =
|
|
1877
|
+
# Compute required moments
|
|
1878
|
+
arrs = _compute_moments(ds, moments=needed_moments)
|
|
1766
1879
|
|
|
1767
1880
|
# Apply the function. This will produce (mu, Lambda, N0) with the same coords/shapes as input data
|
|
1768
1881
|
Nt, mu, sigma = func(*arrs)
|
|
@@ -1783,8 +1896,8 @@ def _get_exponential_parameters_mom(ds: xr.Dataset, mom_method: str) -> xr.Datas
|
|
|
1783
1896
|
# Get the correct function and list of variables for the requested method
|
|
1784
1897
|
func, needed_moments = MOM_METHODS_DICT["ExponentialPSD"][mom_method]
|
|
1785
1898
|
|
|
1786
|
-
#
|
|
1787
|
-
arrs =
|
|
1899
|
+
# Compute required moments
|
|
1900
|
+
arrs = _compute_moments(ds, moments=needed_moments)
|
|
1788
1901
|
|
|
1789
1902
|
# Apply the function. This will produce (mu, Lambda, N0) with the same coords/shapes as input data
|
|
1790
1903
|
N0, Lambda = func(*arrs)
|
|
@@ -1803,6 +1916,79 @@ def _get_exponential_parameters_mom(ds: xr.Dataset, mom_method: str) -> xr.Datas
|
|
|
1803
1916
|
####--------------------------------------------------------------------------------------.
|
|
1804
1917
|
#### Routines dictionary
|
|
1805
1918
|
|
|
1919
|
+
####--------------------------------------------------------------------------------------.
|
|
1920
|
+
ATTRS_PARAMS_DICT = {
|
|
1921
|
+
"GammaPSD": {
|
|
1922
|
+
"N0": {
|
|
1923
|
+
"description": "Intercept parameter of the Gamma PSD",
|
|
1924
|
+
"standard_name": "particle_size_distribution_intercept",
|
|
1925
|
+
"units": "mm**(-1-mu) m-3",
|
|
1926
|
+
"long_name": "GammaPSD intercept parameter",
|
|
1927
|
+
},
|
|
1928
|
+
"mu": {
|
|
1929
|
+
"description": "Shape parameter of the Gamma PSD",
|
|
1930
|
+
"standard_name": "particle_size_distribution_shape",
|
|
1931
|
+
"units": "",
|
|
1932
|
+
"long_name": "GammaPSD shape parameter",
|
|
1933
|
+
},
|
|
1934
|
+
"Lambda": {
|
|
1935
|
+
"description": "Slope (rate) parameter of the Gamma PSD",
|
|
1936
|
+
"standard_name": "particle_size_distribution_slope",
|
|
1937
|
+
"units": "mm-1",
|
|
1938
|
+
"long_name": "GammaPSD slope parameter",
|
|
1939
|
+
},
|
|
1940
|
+
},
|
|
1941
|
+
"NormalizedGammaPSD": {
|
|
1942
|
+
"Nw": {
|
|
1943
|
+
"standard_name": "normalized_intercept_parameter",
|
|
1944
|
+
"units": "mm-1 m-3",
|
|
1945
|
+
"long_name": "NormalizedGammaPSD Normalized Intercept Parameter",
|
|
1946
|
+
},
|
|
1947
|
+
"mu": {
|
|
1948
|
+
"description": "Dimensionless shape parameter controlling the curvature of the Normalized Gamma PSD",
|
|
1949
|
+
"standard_name": "particle_size_distribution_shape",
|
|
1950
|
+
"units": "",
|
|
1951
|
+
"long_name": "NormalizedGammaPSD Shape Parameter ",
|
|
1952
|
+
},
|
|
1953
|
+
"D50": {
|
|
1954
|
+
"standard_name": "median_volume_diameter",
|
|
1955
|
+
"units": "mm",
|
|
1956
|
+
"long_name": "NormalizedGammaPSD Median Volume Drop Diameter",
|
|
1957
|
+
},
|
|
1958
|
+
},
|
|
1959
|
+
"LognormalPSD": {
|
|
1960
|
+
"Nt": {
|
|
1961
|
+
"standard_name": "number_concentration_of_rain_drops_in_air",
|
|
1962
|
+
"units": "m-3",
|
|
1963
|
+
"long_name": "Total Number Concentration",
|
|
1964
|
+
},
|
|
1965
|
+
"mu": {
|
|
1966
|
+
"description": "Mean of the Lognormal PSD",
|
|
1967
|
+
"units": "log(mm)",
|
|
1968
|
+
"long_name": "Mean of the Lognormal PSD",
|
|
1969
|
+
},
|
|
1970
|
+
"sigma": {
|
|
1971
|
+
"standard_name": "Standard Deviation of the Lognormal PSD",
|
|
1972
|
+
"units": "",
|
|
1973
|
+
"long_name": "Standard Deviation of the Lognormal PSD",
|
|
1974
|
+
},
|
|
1975
|
+
},
|
|
1976
|
+
"ExponentialPSD": {
|
|
1977
|
+
"N0": {
|
|
1978
|
+
"description": "Intercept parameter of the Exponential PSD",
|
|
1979
|
+
"standard_name": "particle_size_distribution_intercept",
|
|
1980
|
+
"units": "mm-1 m-3",
|
|
1981
|
+
"long_name": "ExponentialPSD intercept parameter",
|
|
1982
|
+
},
|
|
1983
|
+
"Lambda": {
|
|
1984
|
+
"description": "Slope (rate) parameter of the Exponential PSD",
|
|
1985
|
+
"standard_name": "particle_size_distribution_slope",
|
|
1986
|
+
"units": "mm-1",
|
|
1987
|
+
"long_name": "ExponentialPSD slope parameter",
|
|
1988
|
+
},
|
|
1989
|
+
},
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1806
1992
|
|
|
1807
1993
|
MOM_METHODS_DICT = {
|
|
1808
1994
|
"GammaPSD": {
|
|
@@ -1843,6 +2029,8 @@ OPTIMIZATION_ROUTINES_DICT = {
|
|
|
1843
2029
|
|
|
1844
2030
|
def available_mom_methods(psd_model):
|
|
1845
2031
|
"""Implemented MOM methods for a given PSD model."""
|
|
2032
|
+
if psd_model not in MOM_METHODS_DICT:
|
|
2033
|
+
raise NotImplementedError(f"No MOM methods available for {psd_model}")
|
|
1846
2034
|
return list(MOM_METHODS_DICT[psd_model])
|
|
1847
2035
|
|
|
1848
2036
|
|
|
@@ -1863,7 +2051,7 @@ def check_psd_model(psd_model, optimization):
|
|
|
1863
2051
|
f"{optimization} optimization is not available for 'psd_model' {psd_model}. "
|
|
1864
2052
|
f"Accepted PSD models are {valid_psd_models}."
|
|
1865
2053
|
)
|
|
1866
|
-
raise
|
|
2054
|
+
raise NotImplementedError(msg)
|
|
1867
2055
|
|
|
1868
2056
|
|
|
1869
2057
|
def check_target(target):
|
|
@@ -1921,11 +2109,14 @@ def check_optimizer(optimizer):
|
|
|
1921
2109
|
return optimizer
|
|
1922
2110
|
|
|
1923
2111
|
|
|
1924
|
-
def check_mom_methods(mom_methods, psd_model):
|
|
2112
|
+
def check_mom_methods(mom_methods, psd_model, allow_none=False):
|
|
1925
2113
|
"""Check valid mom_methods arguments."""
|
|
1926
|
-
if isinstance(mom_methods, str):
|
|
2114
|
+
if isinstance(mom_methods, (str, type(None))):
|
|
1927
2115
|
mom_methods = [mom_methods]
|
|
2116
|
+
mom_methods = [str(v) for v in mom_methods] # None --> 'None'
|
|
1928
2117
|
valid_mom_methods = available_mom_methods(psd_model)
|
|
2118
|
+
if allow_none:
|
|
2119
|
+
valid_mom_methods = [*valid_mom_methods, "None"]
|
|
1929
2120
|
invalid_mom_methods = np.array(mom_methods)[np.isin(mom_methods, valid_mom_methods, invert=True)]
|
|
1930
2121
|
if len(invalid_mom_methods) > 0:
|
|
1931
2122
|
raise ValueError(
|
|
@@ -1970,36 +2161,50 @@ def check_optimization_kwargs(optimization_kwargs, optimization, psd_model):
|
|
|
1970
2161
|
expected_arguments = dict_arguments.get(optimization, {})
|
|
1971
2162
|
|
|
1972
2163
|
# 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
|
-
|
|
2164
|
+
# missing_args = [arg for arg in expected_arguments if arg not in optimization_kwargs]
|
|
2165
|
+
# if missing_args:
|
|
2166
|
+
# raise ValueError(f"Missing required arguments for {optimization} optimization: {missing_args}")
|
|
1976
2167
|
|
|
1977
|
-
# Validate
|
|
1978
|
-
_ = [
|
|
2168
|
+
# Validate arguments values
|
|
2169
|
+
_ = [
|
|
2170
|
+
check(optimization_kwargs[arg])
|
|
2171
|
+
for arg, check in expected_arguments.items()
|
|
2172
|
+
if callable(check) and arg in optimization_kwargs
|
|
2173
|
+
]
|
|
1979
2174
|
|
|
1980
2175
|
# Further special checks
|
|
1981
|
-
if optimization == "MOM":
|
|
2176
|
+
if optimization == "MOM" and "mom_methods" in optimization_kwargs:
|
|
1982
2177
|
_ = check_mom_methods(mom_methods=optimization_kwargs["mom_methods"], psd_model=psd_model)
|
|
1983
|
-
if optimization == "ML" and optimization_kwargs
|
|
1984
|
-
_ = check_mom_methods(mom_methods=optimization_kwargs["init_method"], psd_model=psd_model)
|
|
2178
|
+
if optimization == "ML" and optimization_kwargs.get("init_method", None) is not None:
|
|
2179
|
+
_ = check_mom_methods(mom_methods=optimization_kwargs["init_method"], psd_model=psd_model, allow_none=True)
|
|
1985
2180
|
|
|
1986
2181
|
|
|
1987
2182
|
####--------------------------------------------------------------------------------------.
|
|
1988
2183
|
#### Wrappers for fitting
|
|
1989
2184
|
|
|
1990
2185
|
|
|
1991
|
-
def
|
|
2186
|
+
def _finalize_attributes(ds_params, psd_model, optimization, optimization_kwargs):
|
|
2187
|
+
ds_params.attrs["disdrodb_psd_model"] = psd_model
|
|
2188
|
+
ds_params.attrs["disdrodb_psd_optimization"] = optimization
|
|
2189
|
+
ds_params.attrs["disdrodb_psd_optimization_kwargs"] = ", ".join(
|
|
2190
|
+
[f"{k}: {v}" for k, v in optimization_kwargs.items()],
|
|
2191
|
+
)
|
|
2192
|
+
return ds_params
|
|
2193
|
+
|
|
2194
|
+
|
|
2195
|
+
def get_mom_parameters(ds: xr.Dataset, psd_model: str, mom_methods=None) -> xr.Dataset:
|
|
1992
2196
|
"""
|
|
1993
2197
|
Compute PSD model parameters using various method-of-moments (MOM) approaches.
|
|
1994
2198
|
|
|
1995
|
-
The method is specified by the `mom_methods`
|
|
2199
|
+
The method is specified by the `mom_methods` abbreviations, e.g. 'M012', 'M234', 'M246'.
|
|
1996
2200
|
|
|
1997
2201
|
Parameters
|
|
1998
2202
|
----------
|
|
1999
2203
|
ds : xarray.Dataset
|
|
2000
2204
|
An xarray Dataset with the required moments M0...M6 as data variables.
|
|
2001
|
-
mom_methods: str or list
|
|
2002
|
-
|
|
2205
|
+
mom_methods: str or list (optional)
|
|
2206
|
+
See valid values with disdrodb.psd.available_mom_methods(psd_model)
|
|
2207
|
+
If None (the default), compute model parameters with all available MOM methods.
|
|
2003
2208
|
|
|
2004
2209
|
Returns
|
|
2005
2210
|
-------
|
|
@@ -2010,6 +2215,8 @@ def get_mom_parameters(ds: xr.Dataset, psd_model: str, mom_methods: str) -> xr.D
|
|
|
2010
2215
|
"""
|
|
2011
2216
|
# Check inputs
|
|
2012
2217
|
check_psd_model(psd_model=psd_model, optimization="MOM")
|
|
2218
|
+
if mom_methods is None:
|
|
2219
|
+
mom_methods = available_mom_methods(psd_model)
|
|
2013
2220
|
mom_methods = check_mom_methods(mom_methods, psd_model=psd_model)
|
|
2014
2221
|
|
|
2015
2222
|
# Retrieve function
|
|
@@ -2017,13 +2224,21 @@ def get_mom_parameters(ds: xr.Dataset, psd_model: str, mom_methods: str) -> xr.D
|
|
|
2017
2224
|
|
|
2018
2225
|
# Compute parameters
|
|
2019
2226
|
if len(mom_methods) == 1:
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2227
|
+
ds_params = func(ds=ds, mom_method=mom_methods[0])
|
|
2228
|
+
else:
|
|
2229
|
+
list_ds = [func(ds=ds, mom_method=mom_method) for mom_method in mom_methods]
|
|
2230
|
+
ds_params = xr.concat(list_ds, dim="mom_method")
|
|
2231
|
+
ds_params = ds_params.assign_coords({"mom_method": mom_methods})
|
|
2232
|
+
|
|
2233
|
+
# Add model attributes
|
|
2234
|
+
optimization_kwargs = {"mom_methods": mom_methods}
|
|
2235
|
+
ds_params = _finalize_attributes(
|
|
2236
|
+
ds_params=ds_params,
|
|
2237
|
+
psd_model=psd_model,
|
|
2238
|
+
optimization="MOM",
|
|
2239
|
+
optimization_kwargs=optimization_kwargs,
|
|
2240
|
+
)
|
|
2241
|
+
return ds_params
|
|
2027
2242
|
|
|
2028
2243
|
|
|
2029
2244
|
def get_ml_parameters(
|
|
@@ -2052,7 +2267,7 @@ def get_ml_parameters(
|
|
|
2052
2267
|
The PSD model to fit. See ``available_psd_models()``.
|
|
2053
2268
|
init_method: str or list
|
|
2054
2269
|
The method(s) of moments used to initialize the PSD model parameters.
|
|
2055
|
-
See ``available_mom_methods(psd_model)``.
|
|
2270
|
+
Multiple methods can be specified. See ``available_mom_methods(psd_model)``.
|
|
2056
2271
|
probability_method : str, optional
|
|
2057
2272
|
Method to compute probabilities. The default value is ``cdf``.
|
|
2058
2273
|
likelihood : str, optional
|
|
@@ -2076,21 +2291,51 @@ def get_ml_parameters(
|
|
|
2076
2291
|
optimizer = check_optimizer(optimizer)
|
|
2077
2292
|
|
|
2078
2293
|
# Check valid init_method
|
|
2079
|
-
|
|
2080
|
-
init_method = check_mom_methods(mom_methods=init_method, psd_model=psd_model)
|
|
2294
|
+
init_method = check_mom_methods(mom_methods=init_method, psd_model=psd_model, allow_none=True)
|
|
2081
2295
|
|
|
2082
2296
|
# Retrieve estimation function
|
|
2083
2297
|
func = OPTIMIZATION_ROUTINES_DICT["ML"][psd_model]
|
|
2084
2298
|
|
|
2085
|
-
#
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2299
|
+
# Compute parameters
|
|
2300
|
+
if init_method is None or len(init_method) == 1:
|
|
2301
|
+
ds_params = func(
|
|
2302
|
+
ds=ds,
|
|
2303
|
+
init_method=init_method[0],
|
|
2304
|
+
probability_method=probability_method,
|
|
2305
|
+
likelihood=likelihood,
|
|
2306
|
+
truncated_likelihood=truncated_likelihood,
|
|
2307
|
+
optimizer=optimizer,
|
|
2308
|
+
)
|
|
2309
|
+
else:
|
|
2310
|
+
list_ds = [
|
|
2311
|
+
func(
|
|
2312
|
+
ds=ds,
|
|
2313
|
+
init_method=method,
|
|
2314
|
+
probability_method=probability_method,
|
|
2315
|
+
likelihood=likelihood,
|
|
2316
|
+
truncated_likelihood=truncated_likelihood,
|
|
2317
|
+
optimizer=optimizer,
|
|
2318
|
+
)
|
|
2319
|
+
for method in init_method
|
|
2320
|
+
]
|
|
2321
|
+
ds_params = xr.concat(list_ds, dim="init_method")
|
|
2322
|
+
ds_params = ds_params.assign_coords({"init_method": init_method})
|
|
2323
|
+
|
|
2324
|
+
# Add model attributes
|
|
2325
|
+
optimization_kwargs = {
|
|
2326
|
+
"init_method": init_method,
|
|
2327
|
+
"probability_method": "probability_method",
|
|
2328
|
+
"likelihood": likelihood,
|
|
2329
|
+
"truncated_likelihood": truncated_likelihood,
|
|
2330
|
+
"optimizer": optimizer,
|
|
2331
|
+
}
|
|
2332
|
+
ds_params = _finalize_attributes(
|
|
2333
|
+
ds_params=ds_params,
|
|
2334
|
+
psd_model=psd_model,
|
|
2335
|
+
optimization="ML",
|
|
2336
|
+
optimization_kwargs=optimization_kwargs,
|
|
2093
2337
|
)
|
|
2338
|
+
|
|
2094
2339
|
# Return dataset with parameters
|
|
2095
2340
|
return ds_params
|
|
2096
2341
|
|
|
@@ -2106,26 +2351,70 @@ def get_gs_parameters(ds, psd_model, target="ND", transformation="log", error_or
|
|
|
2106
2351
|
# Check valid transformation
|
|
2107
2352
|
transformation = check_transformation(transformation)
|
|
2108
2353
|
|
|
2354
|
+
# Check fall velocity is available if target R
|
|
2355
|
+
if "fall_velocity" not in ds:
|
|
2356
|
+
ds["fall_velocity"] = get_dataset_fall_velocity(ds)
|
|
2357
|
+
|
|
2109
2358
|
# Retrieve estimation function
|
|
2110
2359
|
func = OPTIMIZATION_ROUTINES_DICT["GS"][psd_model]
|
|
2111
2360
|
|
|
2112
2361
|
# Estimate parameters
|
|
2113
2362
|
ds_params = func(ds, target=target, transformation=transformation, error_order=error_order)
|
|
2114
2363
|
|
|
2364
|
+
# Add model attributes
|
|
2365
|
+
optimization_kwargs = {
|
|
2366
|
+
"target": target,
|
|
2367
|
+
"transformation": transformation,
|
|
2368
|
+
"error_order": error_order,
|
|
2369
|
+
}
|
|
2370
|
+
ds_params = _finalize_attributes(
|
|
2371
|
+
ds_params=ds_params,
|
|
2372
|
+
psd_model=psd_model,
|
|
2373
|
+
optimization="GS",
|
|
2374
|
+
optimization_kwargs=optimization_kwargs,
|
|
2375
|
+
)
|
|
2115
2376
|
# Return dataset with parameters
|
|
2116
2377
|
return ds_params
|
|
2117
2378
|
|
|
2118
2379
|
|
|
2380
|
+
def sanitize_drop_number_concentration(drop_number_concentration):
|
|
2381
|
+
"""Sanitize drop number concentration array.
|
|
2382
|
+
|
|
2383
|
+
If N(D) is all zero or contain not finite values, set everything to np.nan
|
|
2384
|
+
"""
|
|
2385
|
+
# Condition 1: all zeros along diameter_bin_center
|
|
2386
|
+
all_zero = (drop_number_concentration == 0).all(dim="diameter_bin_center")
|
|
2387
|
+
|
|
2388
|
+
# Condition 2: any non-finite along diameter_bin_center
|
|
2389
|
+
any_nonfinite = (~np.isfinite(drop_number_concentration)).any(dim="diameter_bin_center")
|
|
2390
|
+
|
|
2391
|
+
# Combine conditions
|
|
2392
|
+
invalid = all_zero | any_nonfinite
|
|
2393
|
+
|
|
2394
|
+
# Replace entire profile with NaN where invalid
|
|
2395
|
+
drop_number_concentration = drop_number_concentration.where(~invalid, np.nan)
|
|
2396
|
+
return drop_number_concentration
|
|
2397
|
+
|
|
2398
|
+
|
|
2119
2399
|
def estimate_model_parameters(
|
|
2120
2400
|
ds,
|
|
2121
2401
|
psd_model,
|
|
2122
2402
|
optimization,
|
|
2123
|
-
optimization_kwargs,
|
|
2403
|
+
optimization_kwargs=None,
|
|
2124
2404
|
):
|
|
2125
2405
|
"""Routine to estimate PSD model parameters."""
|
|
2406
|
+
# Check inputs arguments
|
|
2407
|
+
optimization_kwargs = {} if optimization_kwargs is None else optimization_kwargs
|
|
2126
2408
|
optimization = check_optimization(optimization)
|
|
2127
2409
|
check_optimization_kwargs(optimization_kwargs=optimization_kwargs, optimization=optimization, psd_model=psd_model)
|
|
2128
2410
|
|
|
2411
|
+
# Check N(D)
|
|
2412
|
+
# --> If all 0, set to np.nan
|
|
2413
|
+
# --> If any is not finite --> set to np.nan
|
|
2414
|
+
if "drop_number_concentration" not in ds:
|
|
2415
|
+
raise ValueError("'drop_number_concentration' variable not present in input xarray.Dataset.")
|
|
2416
|
+
ds["drop_number_concentration"] = sanitize_drop_number_concentration(ds["drop_number_concentration"])
|
|
2417
|
+
|
|
2129
2418
|
# Define function
|
|
2130
2419
|
dict_func = {
|
|
2131
2420
|
"ML": get_ml_parameters,
|
|
@@ -2137,10 +2426,7 @@ def estimate_model_parameters(
|
|
|
2137
2426
|
# Retrieve parameters
|
|
2138
2427
|
ds_params = func(ds, psd_model=psd_model, **optimization_kwargs)
|
|
2139
2428
|
|
|
2140
|
-
#
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
if optimization == "GS":
|
|
2144
|
-
ds_params.attrs["disdrodb_psd_optimization_target"] = optimization_kwargs["target"]
|
|
2145
|
-
|
|
2429
|
+
# Add parameters attributes (and units)
|
|
2430
|
+
for var, attrs in ATTRS_PARAMS_DICT[psd_model].items():
|
|
2431
|
+
ds_params[var].attrs = attrs
|
|
2146
2432
|
return ds_params
|