disdrodb 0.1.3__py3-none-any.whl → 0.1.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- disdrodb/__init__.py +4 -0
- disdrodb/_version.py +2 -2
- disdrodb/api/checks.py +70 -47
- disdrodb/api/configs.py +0 -2
- disdrodb/api/info.py +3 -3
- disdrodb/api/io.py +48 -8
- disdrodb/api/path.py +116 -133
- disdrodb/api/search.py +12 -3
- disdrodb/cli/disdrodb_create_summary.py +103 -0
- disdrodb/cli/disdrodb_create_summary_station.py +1 -1
- disdrodb/cli/disdrodb_run_l0a_station.py +1 -1
- disdrodb/cli/disdrodb_run_l0b_station.py +2 -2
- disdrodb/cli/disdrodb_run_l0c_station.py +2 -2
- 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/data_transfer/download_data.py +123 -7
- disdrodb/issue/writer.py +2 -0
- disdrodb/l0/l0a_processing.py +10 -5
- disdrodb/l0/l0b_nc_processing.py +10 -6
- disdrodb/l0/l0b_processing.py +26 -61
- disdrodb/l0/l0c_processing.py +369 -251
- disdrodb/l0/readers/LPM/ARM/ARM_LPM.py +7 -0
- disdrodb/l0/readers/PARSIVEL2/ARM/ARM_PARSIVEL2.py +4 -0
- disdrodb/l0/readers/PARSIVEL2/CANADA/UQAM_NC.py +69 -0
- disdrodb/l0/readers/PARSIVEL2/MPI/BCO_PARSIVEL2.py +136 -0
- disdrodb/l0/readers/PARSIVEL2/MPI/BOWTIE.py +220 -0
- disdrodb/l0/readers/PARSIVEL2/NASA/LPVEX.py +109 -0
- disdrodb/l0/readers/PARSIVEL2/NETHERLANDS/DELFT_NC.py +3 -0
- disdrodb/l1/fall_velocity.py +46 -0
- disdrodb/l1/processing.py +1 -1
- disdrodb/l2/processing.py +1 -1
- disdrodb/metadata/checks.py +132 -125
- disdrodb/psd/fitting.py +172 -205
- disdrodb/psd/models.py +1 -1
- disdrodb/routines/__init__.py +54 -0
- disdrodb/{l0/routines.py → routines/l0.py} +288 -418
- disdrodb/{l1/routines.py → routines/l1.py} +60 -92
- disdrodb/{l2/routines.py → routines/l2.py} +249 -462
- disdrodb/{routines.py → routines/wrappers.py} +95 -7
- disdrodb/scattering/axis_ratio.py +5 -1
- disdrodb/scattering/permittivity.py +18 -0
- disdrodb/scattering/routines.py +56 -36
- disdrodb/summary/routines.py +110 -34
- disdrodb/utils/archiving.py +434 -0
- disdrodb/utils/cli.py +5 -5
- disdrodb/utils/dask.py +62 -1
- disdrodb/utils/decorators.py +31 -0
- disdrodb/utils/encoding.py +5 -1
- disdrodb/{l2 → utils}/event.py +1 -66
- disdrodb/utils/logger.py +1 -1
- disdrodb/utils/manipulations.py +22 -12
- disdrodb/utils/routines.py +166 -0
- disdrodb/utils/time.py +3 -291
- disdrodb/utils/xarray.py +3 -0
- disdrodb/viz/plots.py +85 -14
- {disdrodb-0.1.3.dist-info → disdrodb-0.1.4.dist-info}/METADATA +2 -2
- {disdrodb-0.1.3.dist-info → disdrodb-0.1.4.dist-info}/RECORD +62 -54
- {disdrodb-0.1.3.dist-info → disdrodb-0.1.4.dist-info}/entry_points.txt +1 -0
- {disdrodb-0.1.3.dist-info → disdrodb-0.1.4.dist-info}/WHEEL +0 -0
- {disdrodb-0.1.3.dist-info → disdrodb-0.1.4.dist-info}/licenses/LICENSE +0 -0
- {disdrodb-0.1.3.dist-info → disdrodb-0.1.4.dist-info}/top_level.txt +0 -0
disdrodb/psd/fitting.py
CHANGED
|
@@ -20,9 +20,10 @@ import scipy.stats as ss
|
|
|
20
20
|
import xarray as xr
|
|
21
21
|
from scipy.integrate import quad
|
|
22
22
|
from scipy.optimize import minimize
|
|
23
|
-
from scipy.special import gamma,
|
|
23
|
+
from scipy.special import gamma, gammaln # Regularized lower incomplete gamma function
|
|
24
24
|
|
|
25
25
|
from disdrodb.constants import DIAMETER_DIMENSION
|
|
26
|
+
from disdrodb.l1.fall_velocity import get_dataset_fall_velocity
|
|
26
27
|
from disdrodb.l2.empirical_dsd import (
|
|
27
28
|
get_median_volume_drop_diameter,
|
|
28
29
|
get_moment,
|
|
@@ -227,13 +228,13 @@ def get_expected_probabilities(params, cdf_func, pdf_func, bin_edges, probabilit
|
|
|
227
228
|
def get_adjusted_nt(cdf, params, Nt, bin_edges):
|
|
228
229
|
"""Adjust Nt for the proportion of missing drops. See Johnson's et al., 2013 Eqs. 3 and 4."""
|
|
229
230
|
# Estimate proportion of missing drops (Johnson's 2011 Eqs. 3)
|
|
230
|
-
# --> Alternative:
|
|
231
|
+
# --> Alternative:
|
|
232
|
+
# - p = 1 - np.sum(pdf(diameter, params)* diameter_bin_width) # [-]
|
|
233
|
+
# - p = 1 - np.sum((Lambda ** (mu + 1)) / gamma(mu + 1) * D**mu * np.exp(-Lambda * D) * diameter_bin_width) # [-]
|
|
231
234
|
p = 1 - np.diff(cdf([bin_edges[0], bin_edges[-1]], params)).item() # [-]
|
|
232
|
-
# Adjusts Nt for the proportion of drops
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
return np.nan
|
|
236
|
-
return Nt / (1 - p) # [m-3]
|
|
235
|
+
# Adjusts Nt for the proportion of missing drops
|
|
236
|
+
nt_adj = np.nan if np.isclose(p, 1, atol=1e-12) else Nt / (1 - p) # [m-3]
|
|
237
|
+
return nt_adj
|
|
237
238
|
|
|
238
239
|
|
|
239
240
|
def compute_negative_log_likelihood(
|
|
@@ -669,7 +670,7 @@ def estimate_gamma_parameters(
|
|
|
669
670
|
|
|
670
671
|
# Compute N0
|
|
671
672
|
# - Use logarithmic computations to prevent overflow
|
|
672
|
-
# - N0 = Nt * Lambda ** (mu + 1) / gamma(mu + 1)
|
|
673
|
+
# - N0 = Nt * Lambda ** (mu + 1) / gamma(mu + 1) # [m-3 * mm^(-mu-1)]
|
|
673
674
|
with suppress_warnings():
|
|
674
675
|
log_N0 = np.log(Nt) + (mu + 1) * np.log(Lambda) - gammaln(mu + 1)
|
|
675
676
|
N0 = np.exp(log_N0)
|
|
@@ -785,6 +786,7 @@ def get_gamma_parameters(
|
|
|
785
786
|
Likelihood function to use for fitting. The default value is ``multinomial``.
|
|
786
787
|
truncated_likelihood : bool, optional
|
|
787
788
|
Whether to use truncated likelihood. The default value is ``True``.
|
|
789
|
+
See Johnson et al., 2011 and 2011 for more information.
|
|
788
790
|
optimizer : str, optional
|
|
789
791
|
Optimization method to use. The default value is ``Nelder-Mead``.
|
|
790
792
|
|
|
@@ -802,6 +804,15 @@ def get_gamma_parameters(
|
|
|
802
804
|
The function uses `xr.apply_ufunc` to fit the lognormal distribution parameters
|
|
803
805
|
in parallel, leveraging Dask for parallel computation.
|
|
804
806
|
|
|
807
|
+
References
|
|
808
|
+
----------
|
|
809
|
+
Johnson, R. W., D. V. Kliche, and P. L. Smith, 2011: Comparison of Estimators for Parameters of Gamma Distributions
|
|
810
|
+
with Left-Truncated Samples. J. Appl. Meteor. Climatol., 50, 296-310, https://doi.org/10.1175/2010JAMC2478.1
|
|
811
|
+
|
|
812
|
+
Johnson, R.W., Kliche, D., & Smith, P.L. (2010).
|
|
813
|
+
Maximum likelihood estimation of gamma parameters for coarsely binned and truncated raindrop size data.
|
|
814
|
+
Quarterly Journal of the Royal Meteorological Society, 140. DOI:10.1002/qj.2209
|
|
815
|
+
|
|
805
816
|
"""
|
|
806
817
|
# Define inputs
|
|
807
818
|
counts = ds["drop_number_concentration"] * ds["diameter_bin_width"]
|
|
@@ -981,7 +992,7 @@ def get_exponential_parameters(
|
|
|
981
992
|
|
|
982
993
|
"""
|
|
983
994
|
# Define inputs
|
|
984
|
-
counts = ds["drop_number_concentration"] * ds["diameter_bin_width"]
|
|
995
|
+
counts = ds["drop_number_concentration"] * ds["diameter_bin_width"] # mm-1 m-3 --> m-3
|
|
985
996
|
diameter_breaks = get_diameter_bin_edges(ds)
|
|
986
997
|
|
|
987
998
|
# Define initial parameters (Lambda)
|
|
@@ -1022,163 +1033,6 @@ def get_exponential_parameters(
|
|
|
1022
1033
|
return ds_params
|
|
1023
1034
|
|
|
1024
1035
|
|
|
1025
|
-
####-------------------------------------------------------------------------------------------------------------------.
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
def _estimate_gamma_parameters_johnson(
|
|
1029
|
-
drop_number_concentration,
|
|
1030
|
-
diameter,
|
|
1031
|
-
diameter_breaks,
|
|
1032
|
-
output_dictionary=True,
|
|
1033
|
-
method="Nelder-Mead",
|
|
1034
|
-
mu=0.5,
|
|
1035
|
-
Lambda=3,
|
|
1036
|
-
**kwargs,
|
|
1037
|
-
):
|
|
1038
|
-
"""Deprecated Maximum likelihood estimation of Gamma model.
|
|
1039
|
-
|
|
1040
|
-
N(D) = N_t * lambda**(mu+1) / gamma(mu+1) D**mu exp(-lambda*D)
|
|
1041
|
-
|
|
1042
|
-
Args:
|
|
1043
|
-
spectra: The DSD for which to find parameters [mm-1 m-3].
|
|
1044
|
-
widths: Class widths for each DSD bin [mm].
|
|
1045
|
-
diams: Class-centre diameters for each DSD bin [mm].
|
|
1046
|
-
mu: Initial value for shape parameter mu [-].
|
|
1047
|
-
lambda_param: Initial value for slope parameter lambda [mm^-1].
|
|
1048
|
-
kwargs: Extra arguments for the optimization process.
|
|
1049
|
-
|
|
1050
|
-
Returns
|
|
1051
|
-
-------
|
|
1052
|
-
Dictionary with estimated mu, lambda, and N0.
|
|
1053
|
-
mu (shape) N0 (scale) lambda(slope)
|
|
1054
|
-
|
|
1055
|
-
Notes
|
|
1056
|
-
-----
|
|
1057
|
-
The last bin counts are not accounted in the fitting procedure !
|
|
1058
|
-
|
|
1059
|
-
References
|
|
1060
|
-
----------
|
|
1061
|
-
Johnson, R. W., D. V. Kliche, and P. L. Smith, 2011: Comparison of Estimators for Parameters of Gamma Distributions
|
|
1062
|
-
with Left-Truncated Samples. J. Appl. Meteor. Climatol., 50, 296-310, https://doi.org/10.1175/2010JAMC2478.1
|
|
1063
|
-
|
|
1064
|
-
Johnson, R.W., Kliche, D., & Smith, P.L. (2010).
|
|
1065
|
-
Maximum likelihood estimation of gamma parameters for coarsely binned and truncated raindrop size data.
|
|
1066
|
-
Quarterly Journal of the Royal Meteorological Society, 140. DOI:10.1002/qj.2209
|
|
1067
|
-
|
|
1068
|
-
"""
|
|
1069
|
-
# Initialize bad results
|
|
1070
|
-
if output_dictionary:
|
|
1071
|
-
null_output = {"mu": np.nan, "lambda": np.nan, "N0": np.nan}
|
|
1072
|
-
else:
|
|
1073
|
-
null_output = np.array([np.nan, np.nan, np.nan])
|
|
1074
|
-
|
|
1075
|
-
# Initialize parameters
|
|
1076
|
-
# --> Ideally with method of moments estimate
|
|
1077
|
-
# --> See equation 8 of Johnson's 2013
|
|
1078
|
-
x0 = [mu, Lambda]
|
|
1079
|
-
|
|
1080
|
-
# Compute diameter_bin_width
|
|
1081
|
-
diameter_bin_width = np.diff(diameter_breaks)
|
|
1082
|
-
|
|
1083
|
-
# Convert drop_number_concentration from mm-1 m-3 to m-3.
|
|
1084
|
-
spectra = np.asarray(drop_number_concentration) * diameter_bin_width
|
|
1085
|
-
|
|
1086
|
-
# Define cost function
|
|
1087
|
-
# - Parameter to be optimized on first positions
|
|
1088
|
-
def _cost_function(parameters, spectra, diameter_breaks):
|
|
1089
|
-
# Assume spectra to be in unit [m-3] (drop_number_concentration*diameter_bin_width) !
|
|
1090
|
-
mu, Lambda = parameters
|
|
1091
|
-
# Precompute gamma integrals between various diameter bins
|
|
1092
|
-
# - gamminc(mu+1) already divides the integral by gamma(mu+1) !
|
|
1093
|
-
pgamma_d = gammainc(mu + 1, Lambda * diameter_breaks)
|
|
1094
|
-
# Compute probability with interval
|
|
1095
|
-
delta_pgamma_bins = pgamma_d[1:] - pgamma_d[:-1]
|
|
1096
|
-
# Compute normalization over interval
|
|
1097
|
-
denominator = pgamma_d[-1] - pgamma_d[0]
|
|
1098
|
-
# Compute cost function
|
|
1099
|
-
# a = mu - 1, x = lambda
|
|
1100
|
-
if mu > -1 and Lambda > 0:
|
|
1101
|
-
cost = np.sum(-spectra * np.log(delta_pgamma_bins / denominator))
|
|
1102
|
-
return cost
|
|
1103
|
-
return np.inf
|
|
1104
|
-
|
|
1105
|
-
# Minimize the cost function
|
|
1106
|
-
with suppress_warnings():
|
|
1107
|
-
bounds = [(0, None), (0, None)] # Force mu and lambda to be non-negative
|
|
1108
|
-
res = minimize(
|
|
1109
|
-
_cost_function,
|
|
1110
|
-
x0=x0,
|
|
1111
|
-
args=(spectra, diameter_breaks),
|
|
1112
|
-
method=method,
|
|
1113
|
-
bounds=bounds,
|
|
1114
|
-
**kwargs,
|
|
1115
|
-
)
|
|
1116
|
-
|
|
1117
|
-
# Check if the fit had success
|
|
1118
|
-
if not res.success:
|
|
1119
|
-
return null_output
|
|
1120
|
-
|
|
1121
|
-
# Extract parameters
|
|
1122
|
-
mu = res.x[0] # [-]
|
|
1123
|
-
Lambda = res.x[1] # [mm-1]
|
|
1124
|
-
|
|
1125
|
-
# Estimate tilde_N_T using the total drop concentration
|
|
1126
|
-
tilde_N_T = np.sum(drop_number_concentration * diameter_bin_width) # [m-3]
|
|
1127
|
-
|
|
1128
|
-
# Estimate proportion of missing drops (Johnson's 2011 Eqs. 3)
|
|
1129
|
-
with suppress_warnings():
|
|
1130
|
-
D = diameter
|
|
1131
|
-
p = 1 - np.sum((Lambda ** (mu + 1)) / gamma(mu + 1) * D**mu * np.exp(-Lambda * D) * diameter_bin_width) # [-]
|
|
1132
|
-
|
|
1133
|
-
# Convert tilde_N_T to N_T using Johnson's 2013 Eqs. 3 and 4.
|
|
1134
|
-
# - Adjusts for the proportion of drops not obs
|
|
1135
|
-
N_T = tilde_N_T / (1 - p) # [m-3]
|
|
1136
|
-
|
|
1137
|
-
# Compute N0
|
|
1138
|
-
N0 = N_T * (Lambda ** (mu + 1)) / gamma(mu + 1) # [m-3 * mm^(-mu-1)]
|
|
1139
|
-
|
|
1140
|
-
# Compute Dm
|
|
1141
|
-
# Dm = (mu + 4)/ Lambda
|
|
1142
|
-
|
|
1143
|
-
# Compute Nw
|
|
1144
|
-
# Nw = N0* D^mu / f(mu) , with f(mu of the Normalized PSD)
|
|
1145
|
-
|
|
1146
|
-
# Define output
|
|
1147
|
-
output = {"mu": mu, "Lambda": Lambda, "N0": N0} if output_dictionary else np.array([mu, Lambda, N0])
|
|
1148
|
-
return output
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
def get_gamma_parameters_johnson2014(ds, method="Nelder-Mead"):
|
|
1152
|
-
"""Deprecated model. See Gamma Model with truncated_likelihood and 'pdf'."""
|
|
1153
|
-
drop_number_concentration = ds["drop_number_concentration"]
|
|
1154
|
-
diameter = ds["diameter_bin_center"]
|
|
1155
|
-
diameter_breaks = get_diameter_bin_edges(ds)
|
|
1156
|
-
|
|
1157
|
-
# Define kwargs
|
|
1158
|
-
kwargs = {
|
|
1159
|
-
"output_dictionary": False,
|
|
1160
|
-
"diameter_breaks": diameter_breaks,
|
|
1161
|
-
"method": method,
|
|
1162
|
-
}
|
|
1163
|
-
da_params = xr.apply_ufunc(
|
|
1164
|
-
_estimate_gamma_parameters_johnson,
|
|
1165
|
-
drop_number_concentration,
|
|
1166
|
-
diameter,
|
|
1167
|
-
# diameter_bin_width,
|
|
1168
|
-
kwargs=kwargs,
|
|
1169
|
-
input_core_dims=[[DIAMETER_DIMENSION], [DIAMETER_DIMENSION]], # [DIAMETER_DIMENSION],
|
|
1170
|
-
output_core_dims=[["parameters"]],
|
|
1171
|
-
vectorize=True,
|
|
1172
|
-
)
|
|
1173
|
-
|
|
1174
|
-
# Add parameters coordinates
|
|
1175
|
-
da_params = da_params.assign_coords({"parameters": ["mu", "Lambda", "N0"]})
|
|
1176
|
-
|
|
1177
|
-
# Convert to skill Dataset
|
|
1178
|
-
ds_params = da_params.to_dataset(dim="parameters")
|
|
1179
|
-
return ds_params
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
1036
|
####-----------------------------------------------------------------------------------------.
|
|
1183
1037
|
#### Grid Search (GS)
|
|
1184
1038
|
|
|
@@ -1202,24 +1056,60 @@ def _compute_z(ND, D, dD):
|
|
|
1202
1056
|
return Z
|
|
1203
1057
|
|
|
1204
1058
|
|
|
1059
|
+
def _compute_target_variable_error(target, ND_obs, ND_preds, D, dD, V):
|
|
1060
|
+
if target == "Z":
|
|
1061
|
+
errors = np.abs(_compute_z(ND_obs, D, dD) - _compute_z(ND_preds, D, dD))
|
|
1062
|
+
elif target == "R":
|
|
1063
|
+
errors = np.abs(_compute_rain_rate(ND_obs, D, dD, V) - _compute_rain_rate(ND_preds, D, dD, V))
|
|
1064
|
+
else: # if target == "LWC":
|
|
1065
|
+
errors = np.abs(_compute_lwc(ND_obs, D, dD) - _compute_lwc(ND_preds, D, dD))
|
|
1066
|
+
return errors
|
|
1067
|
+
|
|
1068
|
+
|
|
1205
1069
|
def _compute_cost_function(ND_obs, ND_preds, D, dD, V, target, transformation, error_order):
|
|
1206
1070
|
# Assume ND_obs of shape (D bins) and ND_preds of shape (# params, D bins)
|
|
1207
1071
|
if target == "ND":
|
|
1208
1072
|
if transformation == "identity":
|
|
1209
1073
|
errors = np.mean(np.abs(ND_obs[None, :] - ND_preds) ** error_order, axis=1)
|
|
1074
|
+
return errors
|
|
1210
1075
|
if transformation == "log":
|
|
1211
1076
|
errors = np.mean(np.abs(np.log(ND_obs[None, :] + 1) - np.log(ND_preds + 1)) ** error_order, axis=1)
|
|
1212
|
-
|
|
1077
|
+
return errors
|
|
1078
|
+
if transformation == "sqrt":
|
|
1213
1079
|
errors = np.mean(np.abs(np.sqrt(ND_obs[None, :]) - np.sqrt(ND_preds)) ** error_order, axis=1)
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1080
|
+
return errors
|
|
1081
|
+
# if target in ["Z", "R", "LWC"]:
|
|
1082
|
+
return _compute_target_variable_error(target, ND_obs, ND_preds, D, dD, V)
|
|
1083
|
+
|
|
1084
|
+
|
|
1085
|
+
def define_param_range(center, step, bounds, factor=2, refinement=20):
|
|
1086
|
+
"""
|
|
1087
|
+
Create a refined parameter search range around a center value, constrained to bounds.
|
|
1088
|
+
|
|
1089
|
+
Parameters
|
|
1090
|
+
----------
|
|
1091
|
+
center : float
|
|
1092
|
+
Center of the range (e.g., current best estimate).
|
|
1093
|
+
step : float
|
|
1094
|
+
Coarse step size used in the first search.
|
|
1095
|
+
bounds : tuple of (float, float)
|
|
1096
|
+
Lower and upper bounds (can include -np.inf, np.inf).
|
|
1097
|
+
factor : float, optional
|
|
1098
|
+
How wide the refined range extends from the center (in multiples of step).
|
|
1099
|
+
Default = 2.
|
|
1100
|
+
refinement : int, optional
|
|
1101
|
+
Factor to refine the step size (smaller step = finer grid).
|
|
1102
|
+
Default = 20.
|
|
1103
|
+
|
|
1104
|
+
Returns
|
|
1105
|
+
-------
|
|
1106
|
+
np.ndarray
|
|
1107
|
+
Array of values constrained to bounds.
|
|
1108
|
+
"""
|
|
1109
|
+
lower = max(center - factor * step, bounds[0])
|
|
1110
|
+
upper = min(center + factor * step, bounds[1])
|
|
1111
|
+
new_step = step / refinement
|
|
1112
|
+
return np.arange(lower, upper, new_step)
|
|
1223
1113
|
|
|
1224
1114
|
|
|
1225
1115
|
def apply_exponential_gs(
|
|
@@ -1255,9 +1145,15 @@ def apply_exponential_gs(
|
|
|
1255
1145
|
transformation=transformation,
|
|
1256
1146
|
error_order=error_order,
|
|
1257
1147
|
)
|
|
1148
|
+
# Replace inf with NaN
|
|
1149
|
+
errors[~np.isfinite(errors)] = np.nan
|
|
1150
|
+
|
|
1151
|
+
# If all invalid, return NaN parameters
|
|
1152
|
+
if np.all(np.isnan(errors)):
|
|
1153
|
+
return np.array([np.nan, np.nan])
|
|
1258
1154
|
|
|
1259
|
-
#
|
|
1260
|
-
best_index = np.
|
|
1155
|
+
# Otherwise, choose the best index
|
|
1156
|
+
best_index = np.nanargmin(errors)
|
|
1261
1157
|
return np.array([N0_arr[best_index].item(), lambda_arr[best_index].item()])
|
|
1262
1158
|
|
|
1263
1159
|
|
|
@@ -1286,8 +1182,15 @@ def _apply_gamma_gs(mu_values, lambda_values, Nt, ND_obs, D, dD, V, target, tran
|
|
|
1286
1182
|
error_order=error_order,
|
|
1287
1183
|
)
|
|
1288
1184
|
|
|
1289
|
-
#
|
|
1290
|
-
|
|
1185
|
+
# Replace inf with NaN
|
|
1186
|
+
errors[~np.isfinite(errors)] = np.nan
|
|
1187
|
+
|
|
1188
|
+
# If all invalid, return NaN parameters
|
|
1189
|
+
if np.all(np.isnan(errors)):
|
|
1190
|
+
return np.array([np.nan, np.nan, np.nan])
|
|
1191
|
+
|
|
1192
|
+
# Otherwise, choose the best index
|
|
1193
|
+
best_index = np.nanargmin(errors)
|
|
1291
1194
|
return N0[best_index].item(), mu_arr[best_index].item(), lambda_arr[best_index].item()
|
|
1292
1195
|
|
|
1293
1196
|
|
|
@@ -1304,6 +1207,10 @@ def apply_gamma_gs(
|
|
|
1304
1207
|
error_order,
|
|
1305
1208
|
):
|
|
1306
1209
|
"""Estimate GammaPSD model parameters using Grid Search."""
|
|
1210
|
+
# Define parameters bounds
|
|
1211
|
+
mu_bounds = (0.01, 20)
|
|
1212
|
+
lambda_bounds = (0.01, 60)
|
|
1213
|
+
|
|
1307
1214
|
# Define initial set of parameters
|
|
1308
1215
|
mu_step = 0.5
|
|
1309
1216
|
lambda_step = 0.5
|
|
@@ -1323,10 +1230,13 @@ def apply_gamma_gs(
|
|
|
1323
1230
|
transformation=transformation,
|
|
1324
1231
|
error_order=error_order,
|
|
1325
1232
|
)
|
|
1233
|
+
if np.isnan(N0): # if np.nan, return immediately
|
|
1234
|
+
return np.array([N0, mu, Lambda])
|
|
1326
1235
|
|
|
1327
1236
|
# Second round of GS
|
|
1328
|
-
mu_values =
|
|
1329
|
-
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
|
+
|
|
1330
1240
|
N0, mu, Lambda = _apply_gamma_gs(
|
|
1331
1241
|
mu_values=mu_values,
|
|
1332
1242
|
lambda_values=lambda_values,
|
|
@@ -1367,8 +1277,15 @@ def _apply_lognormal_gs(mu_values, sigma_values, Nt, ND_obs, D, dD, V, target, t
|
|
|
1367
1277
|
error_order=error_order,
|
|
1368
1278
|
)
|
|
1369
1279
|
|
|
1370
|
-
#
|
|
1371
|
-
|
|
1280
|
+
# Replace inf with NaN
|
|
1281
|
+
errors[~np.isfinite(errors)] = np.nan
|
|
1282
|
+
|
|
1283
|
+
# If all invalid, return NaN parameters
|
|
1284
|
+
if np.all(np.isnan(errors)):
|
|
1285
|
+
return np.array([np.nan, np.nan, np.nan])
|
|
1286
|
+
|
|
1287
|
+
# Otherwise, choose the best index
|
|
1288
|
+
best_index = np.nanargmin(errors)
|
|
1372
1289
|
return Nt, mu_arr[best_index].item(), sigma_arr[best_index].item()
|
|
1373
1290
|
|
|
1374
1291
|
|
|
@@ -1385,10 +1302,16 @@ def apply_lognormal_gs(
|
|
|
1385
1302
|
error_order,
|
|
1386
1303
|
):
|
|
1387
1304
|
"""Estimate LognormalPSD model parameters using Grid Search."""
|
|
1305
|
+
# Define parameters bounds
|
|
1306
|
+
sigma_bounds = (0, np.inf) # > 0
|
|
1307
|
+
scale_bounds = (0.1, np.inf) # > 0
|
|
1308
|
+
# mu_bounds = (- np.inf, np.inf) # mu = np.log(scale)
|
|
1309
|
+
|
|
1388
1310
|
# Define initial set of parameters
|
|
1389
|
-
|
|
1390
|
-
sigma_step = 0.
|
|
1391
|
-
|
|
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
|
|
1392
1315
|
sigma_values = np.arange(0, 20, step=sigma_step) # TODO: define realistic values
|
|
1393
1316
|
|
|
1394
1317
|
# First round of GS
|
|
@@ -1404,10 +1327,13 @@ def apply_lognormal_gs(
|
|
|
1404
1327
|
transformation=transformation,
|
|
1405
1328
|
error_order=error_order,
|
|
1406
1329
|
)
|
|
1330
|
+
if np.isnan(mu): # if np.nan, return immediately
|
|
1331
|
+
return np.array([Nt, mu, sigma])
|
|
1407
1332
|
|
|
1408
1333
|
# Second round of GS
|
|
1409
|
-
|
|
1410
|
-
|
|
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)
|
|
1411
1337
|
Nt, mu, sigma = _apply_lognormal_gs(
|
|
1412
1338
|
mu_values=mu_values,
|
|
1413
1339
|
sigma_values=sigma_values,
|
|
@@ -1458,8 +1384,16 @@ def apply_normalized_gamma_gs(
|
|
|
1458
1384
|
error_order=error_order,
|
|
1459
1385
|
)
|
|
1460
1386
|
|
|
1461
|
-
#
|
|
1462
|
-
|
|
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]
|
|
1463
1397
|
return np.array([Nw, mu, D50])
|
|
1464
1398
|
|
|
1465
1399
|
|
|
@@ -1733,6 +1667,8 @@ def get_exponential_parameters_Zhang2008(moment_l, moment_m, l, m): # noqa: E74
|
|
|
1733
1667
|
Meteor. Climatol.,
|
|
1734
1668
|
https://doi.org/10.1175/2008JAMC1876.1
|
|
1735
1669
|
"""
|
|
1670
|
+
if l == m:
|
|
1671
|
+
raise ValueError("Equal l and m moment orders are not allowed.")
|
|
1736
1672
|
num = moment_l * gamma(m + 1)
|
|
1737
1673
|
den = moment_m * gamma(l + 1)
|
|
1738
1674
|
Lambda = np.power(num / den, (1 / (m - l)))
|
|
@@ -1757,21 +1693,21 @@ def get_exponential_parameters_M34(moment_3, moment_4):
|
|
|
1757
1693
|
return N0, Lambda
|
|
1758
1694
|
|
|
1759
1695
|
|
|
1760
|
-
def get_gamma_parameters_M012(M0, M1, M2):
|
|
1761
|
-
|
|
1696
|
+
# def get_gamma_parameters_M012(M0, M1, M2):
|
|
1697
|
+
# """Compute gamma distribution parameters following Cao et al., 2009.
|
|
1762
1698
|
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
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
|
|
1775
1711
|
|
|
1776
1712
|
|
|
1777
1713
|
def get_gamma_parameters_M234(M2, M3, M4):
|
|
@@ -2415,6 +2351,10 @@ def get_gs_parameters(ds, psd_model, target="ND", transformation="log", error_or
|
|
|
2415
2351
|
# Check valid transformation
|
|
2416
2352
|
transformation = check_transformation(transformation)
|
|
2417
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
|
+
|
|
2418
2358
|
# Retrieve estimation function
|
|
2419
2359
|
func = OPTIMIZATION_ROUTINES_DICT["GS"][psd_model]
|
|
2420
2360
|
|
|
@@ -2437,6 +2377,25 @@ def get_gs_parameters(ds, psd_model, target="ND", transformation="log", error_or
|
|
|
2437
2377
|
return ds_params
|
|
2438
2378
|
|
|
2439
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
|
+
|
|
2440
2399
|
def estimate_model_parameters(
|
|
2441
2400
|
ds,
|
|
2442
2401
|
psd_model,
|
|
@@ -2444,10 +2403,18 @@ def estimate_model_parameters(
|
|
|
2444
2403
|
optimization_kwargs=None,
|
|
2445
2404
|
):
|
|
2446
2405
|
"""Routine to estimate PSD model parameters."""
|
|
2406
|
+
# Check inputs arguments
|
|
2447
2407
|
optimization_kwargs = {} if optimization_kwargs is None else optimization_kwargs
|
|
2448
2408
|
optimization = check_optimization(optimization)
|
|
2449
2409
|
check_optimization_kwargs(optimization_kwargs=optimization_kwargs, optimization=optimization, psd_model=psd_model)
|
|
2450
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
|
+
|
|
2451
2418
|
# Define function
|
|
2452
2419
|
dict_func = {
|
|
2453
2420
|
"ML": get_ml_parameters,
|
disdrodb/psd/models.py
CHANGED
|
@@ -93,7 +93,7 @@ def get_psd_model_formula(psd_model):
|
|
|
93
93
|
return PSD_MODELS_DICT[psd_model].formula
|
|
94
94
|
|
|
95
95
|
|
|
96
|
-
def create_psd(psd_model, parameters):
|
|
96
|
+
def create_psd(psd_model, parameters):
|
|
97
97
|
"""Define a PSD from a dictionary or xr.Dataset of parameters."""
|
|
98
98
|
psd_class = get_psd_model(psd_model)
|
|
99
99
|
psd = psd_class.from_parameters(parameters)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# -----------------------------------------------------------------------------.
|
|
2
|
+
# Copyright (c) 2021-2023 DISDRODB developers
|
|
3
|
+
#
|
|
4
|
+
# This program is free software: you can redistribute it and/or modify
|
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
|
6
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
16
|
+
# -----------------------------------------------------------------------------.
|
|
17
|
+
"""DISDRODB L0 software."""
|
|
18
|
+
from disdrodb.routines.wrappers import (
|
|
19
|
+
create_summary,
|
|
20
|
+
create_summary_station,
|
|
21
|
+
run_l0,
|
|
22
|
+
run_l0_station,
|
|
23
|
+
run_l0a,
|
|
24
|
+
run_l0a_station,
|
|
25
|
+
run_l0b,
|
|
26
|
+
run_l0b_station,
|
|
27
|
+
run_l0c,
|
|
28
|
+
run_l0c_station,
|
|
29
|
+
run_l1,
|
|
30
|
+
run_l1_station,
|
|
31
|
+
run_l2e,
|
|
32
|
+
run_l2e_station,
|
|
33
|
+
run_l2m,
|
|
34
|
+
run_l2m_station,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"create_summary",
|
|
39
|
+
"create_summary_station",
|
|
40
|
+
"run_l0",
|
|
41
|
+
"run_l0_station",
|
|
42
|
+
"run_l0a",
|
|
43
|
+
"run_l0a_station",
|
|
44
|
+
"run_l0b",
|
|
45
|
+
"run_l0b_station",
|
|
46
|
+
"run_l0c",
|
|
47
|
+
"run_l0c_station",
|
|
48
|
+
"run_l1",
|
|
49
|
+
"run_l1_station",
|
|
50
|
+
"run_l2e",
|
|
51
|
+
"run_l2e_station",
|
|
52
|
+
"run_l2m",
|
|
53
|
+
"run_l2m_station",
|
|
54
|
+
]
|