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
|
@@ -0,0 +1,4196 @@
|
|
|
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
|
+
"""Utilities to create summary statistics."""
|
|
18
|
+
import gc
|
|
19
|
+
import importlib
|
|
20
|
+
import os
|
|
21
|
+
import shutil
|
|
22
|
+
import subprocess
|
|
23
|
+
import tempfile
|
|
24
|
+
|
|
25
|
+
import matplotlib.lines as mlines
|
|
26
|
+
import matplotlib.pyplot as plt
|
|
27
|
+
import numpy as np
|
|
28
|
+
import pandas as pd
|
|
29
|
+
import xarray as xr
|
|
30
|
+
from matplotlib.colors import ListedColormap, LogNorm, Normalize
|
|
31
|
+
from matplotlib.gridspec import GridSpec
|
|
32
|
+
from scipy.optimize import curve_fit
|
|
33
|
+
|
|
34
|
+
import disdrodb
|
|
35
|
+
from disdrodb.api.path import define_station_dir
|
|
36
|
+
from disdrodb.constants import DIAMETER_DIMENSION, VELOCITY_DIMENSION
|
|
37
|
+
from disdrodb.l2.empirical_dsd import get_drop_average_velocity
|
|
38
|
+
from disdrodb.scattering import RADAR_OPTIONS
|
|
39
|
+
from disdrodb.utils.dataframe import compute_2d_histogram, log_arange
|
|
40
|
+
from disdrodb.utils.event import group_timesteps_into_event
|
|
41
|
+
from disdrodb.utils.manipulations import (
|
|
42
|
+
get_diameter_bin_edges,
|
|
43
|
+
resample_drop_number_concentration,
|
|
44
|
+
unstack_radar_variables,
|
|
45
|
+
)
|
|
46
|
+
from disdrodb.utils.warnings import suppress_warnings
|
|
47
|
+
from disdrodb.utils.yaml import write_yaml
|
|
48
|
+
from disdrodb.viz import compute_dense_lines, max_blend_images, to_rgba
|
|
49
|
+
|
|
50
|
+
####-----------------------------------------------------------------
|
|
51
|
+
#### PDF Latex Utilities
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def is_latex_engine_available() -> bool:
|
|
55
|
+
"""
|
|
56
|
+
Determine whether the Tectonic TeX/LaTeX engine is installed and accessible.
|
|
57
|
+
|
|
58
|
+
Returns
|
|
59
|
+
-------
|
|
60
|
+
bool
|
|
61
|
+
True if tectonic is found, False otherwise.
|
|
62
|
+
"""
|
|
63
|
+
return shutil.which("tectonic") is not None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def save_table_to_pdf(
|
|
67
|
+
df: pd.DataFrame,
|
|
68
|
+
filepath: str,
|
|
69
|
+
index=True,
|
|
70
|
+
caption=None,
|
|
71
|
+
fontsize: str = r"\tiny",
|
|
72
|
+
orientation: str = "landscape",
|
|
73
|
+
) -> None:
|
|
74
|
+
r"""Render a pandas DataFrame as a well-formatted table in PDF via LaTeX.
|
|
75
|
+
|
|
76
|
+
Parameters
|
|
77
|
+
----------
|
|
78
|
+
df : pd.DataFrame
|
|
79
|
+
The data to render.
|
|
80
|
+
filepath : str
|
|
81
|
+
File path where write the final PDF (e.g. '<...>/table.pdf').
|
|
82
|
+
caption : str, optional
|
|
83
|
+
LaTeX caption for the table environment.
|
|
84
|
+
fontsize : str, optional
|
|
85
|
+
LaTeX font-size command to wrap the table (e.g. '\\small').
|
|
86
|
+
The default is '\\tiny'.
|
|
87
|
+
orientation : {'portrait', 'landscape'}
|
|
88
|
+
Page orientation. If 'landscape', the table will be laid out horizontally.
|
|
89
|
+
The default is 'landscape'.
|
|
90
|
+
"""
|
|
91
|
+
# Export table to LaTeX
|
|
92
|
+
table_tex = df.to_latex(
|
|
93
|
+
index=index,
|
|
94
|
+
longtable=True,
|
|
95
|
+
caption=caption,
|
|
96
|
+
label=None,
|
|
97
|
+
escape=False,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Define LaTeX document
|
|
101
|
+
opts = "a4paper"
|
|
102
|
+
doc = [
|
|
103
|
+
f"\\documentclass[{opts}]{{article}}",
|
|
104
|
+
"\\usepackage[margin=0.1in]{geometry}",
|
|
105
|
+
# Reduce column separation
|
|
106
|
+
"\\setlength{\\tabcolsep}{3pt}",
|
|
107
|
+
"\\usepackage{booktabs}",
|
|
108
|
+
"\\usepackage{longtable}",
|
|
109
|
+
"\\usepackage{caption}",
|
|
110
|
+
"\\captionsetup[longtable]{font=tiny}",
|
|
111
|
+
"\\usepackage{pdflscape}",
|
|
112
|
+
"\\begin{document}",
|
|
113
|
+
# Remove page numbers
|
|
114
|
+
"\\pagestyle{empty}",
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
if orientation.lower() == "landscape":
|
|
118
|
+
doc.append("\\begin{landscape}")
|
|
119
|
+
|
|
120
|
+
doc.append(f"{{{fontsize}\n{table_tex}\n}}")
|
|
121
|
+
|
|
122
|
+
if orientation.lower() == "landscape":
|
|
123
|
+
doc.append("\\end{landscape}")
|
|
124
|
+
|
|
125
|
+
doc.append("\\end{document}")
|
|
126
|
+
document = "\n".join(doc)
|
|
127
|
+
|
|
128
|
+
# Compile with pdflatex in a temp dir
|
|
129
|
+
with tempfile.TemporaryDirectory() as td:
|
|
130
|
+
tex_path = os.path.join(td, "table.tex")
|
|
131
|
+
with open(tex_path, "w", encoding="utf-8") as f:
|
|
132
|
+
f.write(document)
|
|
133
|
+
for _ in range(2):
|
|
134
|
+
subprocess.run(
|
|
135
|
+
[
|
|
136
|
+
"tectonic",
|
|
137
|
+
"--outdir",
|
|
138
|
+
td,
|
|
139
|
+
tex_path,
|
|
140
|
+
],
|
|
141
|
+
cwd=td,
|
|
142
|
+
stdout=subprocess.DEVNULL,
|
|
143
|
+
stderr=subprocess.DEVNULL,
|
|
144
|
+
check=True,
|
|
145
|
+
)
|
|
146
|
+
# Move result
|
|
147
|
+
shutil.move(os.path.join(td, "table.pdf"), filepath)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
####-----------------------------------------------------------------
|
|
151
|
+
#### Tables summaries
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def create_table_rain_summary(df):
|
|
155
|
+
"""Create rainy table summary."""
|
|
156
|
+
# Initialize dictionary
|
|
157
|
+
table = {}
|
|
158
|
+
|
|
159
|
+
# Keep rows with R > 0
|
|
160
|
+
df = df[df["R"] > 0]
|
|
161
|
+
|
|
162
|
+
# Number of years, months, days, minutes
|
|
163
|
+
if df.index.name == "time":
|
|
164
|
+
df = df.reset_index()
|
|
165
|
+
time = np.sort(np.asanyarray(df["time"]))
|
|
166
|
+
|
|
167
|
+
# Define start_time and end_time
|
|
168
|
+
start_time = pd.Timestamp(time[0])
|
|
169
|
+
end_time = pd.Timestamp(time[-1])
|
|
170
|
+
|
|
171
|
+
# Define years and years-month coverage
|
|
172
|
+
start_year = start_time.year
|
|
173
|
+
start_month = start_time.month_name()
|
|
174
|
+
end_year = end_time.year
|
|
175
|
+
end_month = end_time.month_name()
|
|
176
|
+
if start_year == end_year:
|
|
177
|
+
years_coverage = f"{start_year}"
|
|
178
|
+
years_month_coverage = f"{start_month[0:3]}-{end_month[0:3]} {start_year}"
|
|
179
|
+
else:
|
|
180
|
+
years_coverage = f"{start_year} - {end_year}"
|
|
181
|
+
years_month_coverage = f"{start_month[0:3]} {start_year} - {end_month[0:3]} {end_year}"
|
|
182
|
+
table["years_coverage"] = years_coverage
|
|
183
|
+
table["years_month_coverage"] = years_month_coverage
|
|
184
|
+
|
|
185
|
+
# Rainy minutes statistics
|
|
186
|
+
table["n_rainy_minutes"] = len(df["R"])
|
|
187
|
+
table["n_rainy_minutes_<0.1"] = df["R"].between(0, 0.1, inclusive="right").sum().item()
|
|
188
|
+
table["n_rainy_minutes_0.1_1"] = df["R"].between(0.1, 1, inclusive="right").sum().item()
|
|
189
|
+
table["n_rainy_minutes_1_10"] = df["R"].between(1, 10, inclusive="right").sum().item()
|
|
190
|
+
table["n_rainy_minutes_10_25"] = df["R"].between(10, 25, inclusive="right").sum().item()
|
|
191
|
+
table["n_rainy_minutes_25_50"] = df["R"].between(25, 50, inclusive="right").sum().item()
|
|
192
|
+
table["n_rainy_minutes_50_100"] = df["R"].between(50, 100, inclusive="right").sum().item()
|
|
193
|
+
table["n_rainy_minutes_100_200"] = df["R"].between(100, 200, inclusive="right").sum().item()
|
|
194
|
+
table["n_rainy_minutes_>200"] = np.sum(df["R"] > 200).item()
|
|
195
|
+
|
|
196
|
+
# Minutes with larger Dmax
|
|
197
|
+
table["n_minutes_Dmax_>7"] = np.sum(df["Dmax"] > 7).item()
|
|
198
|
+
table["n_minutes_Dmax_>8"] = np.sum(df["Dmax"] > 8).item()
|
|
199
|
+
table["n_minutes_Dmax_>9"] = np.sum(df["Dmax"] > 9).item()
|
|
200
|
+
return table
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def create_table_dsd_summary(df):
|
|
204
|
+
"""Create table with integral DSD parameters statistics."""
|
|
205
|
+
# Define additional variables
|
|
206
|
+
df["log10(Nw)"] = np.log10(df["Nw"])
|
|
207
|
+
df["log10(Nt)"] = np.log10(df["Nt"])
|
|
208
|
+
|
|
209
|
+
# List of variables to summarize
|
|
210
|
+
variables = ["W", "R", "Z", "D50", "Dm", "sigma_m", "Dmax", "Nw", "log10(Nw)", "Nt", "log10(Nt)"]
|
|
211
|
+
|
|
212
|
+
# Define subset dataframe
|
|
213
|
+
df_subset = df[variables]
|
|
214
|
+
|
|
215
|
+
# Prepare summary DataFrame
|
|
216
|
+
stats_cols = [
|
|
217
|
+
"MIN",
|
|
218
|
+
"Q1",
|
|
219
|
+
"Q5",
|
|
220
|
+
"Q10",
|
|
221
|
+
"Q50",
|
|
222
|
+
"Q90",
|
|
223
|
+
"Q95",
|
|
224
|
+
"Q99",
|
|
225
|
+
"MAX",
|
|
226
|
+
"MEAN",
|
|
227
|
+
"STD",
|
|
228
|
+
"MAD",
|
|
229
|
+
"SKEWNESS",
|
|
230
|
+
"KURTOSIS",
|
|
231
|
+
]
|
|
232
|
+
df_stats = pd.DataFrame(index=variables, columns=stats_cols)
|
|
233
|
+
|
|
234
|
+
# Compute DSD integral parameters statistics
|
|
235
|
+
df_stats["MIN"] = df_subset.min()
|
|
236
|
+
df_stats["Q1"] = df_subset.quantile(0.01)
|
|
237
|
+
df_stats["Q5"] = df_subset.quantile(0.05)
|
|
238
|
+
df_stats["Q10"] = df_subset.quantile(0.10)
|
|
239
|
+
df_stats["Q50"] = df_subset.median()
|
|
240
|
+
df_stats["Q90"] = df_subset.quantile(0.90)
|
|
241
|
+
df_stats["Q95"] = df_subset.quantile(0.95)
|
|
242
|
+
df_stats["Q99"] = df_subset.quantile(0.99)
|
|
243
|
+
df_stats["MAX"] = df_subset.max()
|
|
244
|
+
df_stats["MEAN"] = df_subset.mean()
|
|
245
|
+
df_stats["STD"] = df_subset.std()
|
|
246
|
+
df_stats["MAD"] = df_subset.apply(lambda x: np.mean(np.abs(x - x.mean())))
|
|
247
|
+
df_stats["SKEWNESS"] = df_subset.skew()
|
|
248
|
+
df_stats["KURTOSIS"] = df_subset.kurt()
|
|
249
|
+
|
|
250
|
+
# Round statistics
|
|
251
|
+
df_stats = df_stats.astype(float).round(2)
|
|
252
|
+
return df_stats
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def create_table_events_summary(df):
|
|
256
|
+
"""Creata table with events statistics."""
|
|
257
|
+
# Event file
|
|
258
|
+
# - Events are separated by 1 hour or more rain-free periods in rain rate time series.
|
|
259
|
+
# - The events that are less than 'min_duration' minutes or the rain total is less than 0.1 mm
|
|
260
|
+
# are not reported.
|
|
261
|
+
event_settings = {
|
|
262
|
+
"neighbor_min_size": 2,
|
|
263
|
+
"neighbor_time_interval": "5MIN",
|
|
264
|
+
"event_max_time_gap": "1H",
|
|
265
|
+
"event_min_duration": "5MIN",
|
|
266
|
+
"event_min_size": 3,
|
|
267
|
+
}
|
|
268
|
+
# Keep rows with R > 0
|
|
269
|
+
df = df[df["R"] > 0]
|
|
270
|
+
|
|
271
|
+
# Number of years, months, days, minutes
|
|
272
|
+
if df.index.name == "time":
|
|
273
|
+
df = df.reset_index()
|
|
274
|
+
timesteps = np.sort(np.asanyarray(df["time"]))
|
|
275
|
+
|
|
276
|
+
# Define event list
|
|
277
|
+
event_list = group_timesteps_into_event(
|
|
278
|
+
timesteps=timesteps,
|
|
279
|
+
neighbor_min_size=event_settings["neighbor_min_size"],
|
|
280
|
+
neighbor_time_interval=event_settings["neighbor_time_interval"],
|
|
281
|
+
event_max_time_gap=event_settings["event_max_time_gap"],
|
|
282
|
+
event_min_duration=event_settings["event_min_duration"],
|
|
283
|
+
event_min_size=event_settings["event_min_size"],
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# Create dataframe with statistics for each event
|
|
287
|
+
events_stats = []
|
|
288
|
+
rain_thresholds = [0.1, 1, 5, 10, 20, 50, 100]
|
|
289
|
+
for event in event_list:
|
|
290
|
+
# Retrieve event start_time and end_time
|
|
291
|
+
start, end = event["start_time"], event["end_time"]
|
|
292
|
+
# Retrieve event dataframe
|
|
293
|
+
df_event = df[(df["time"] >= start) & (df["time"] <= end)]
|
|
294
|
+
# Initialize event record
|
|
295
|
+
event_stats = {
|
|
296
|
+
# Event time info
|
|
297
|
+
"start_time": start,
|
|
298
|
+
"end_time": end,
|
|
299
|
+
"duration": int((end - start) / np.timedelta64(1, "m")),
|
|
300
|
+
# Rainy minutes above thresholds
|
|
301
|
+
**{f"rainy_minutes_>{thr}": int((df_event["R"] > thr).sum()) for thr in rain_thresholds},
|
|
302
|
+
# Total precipitation (mm)
|
|
303
|
+
"P_total": df_event["P"].sum(),
|
|
304
|
+
# R statistics
|
|
305
|
+
"mean_R": df_event["R"].mean(),
|
|
306
|
+
"median_R": df_event["R"].median(),
|
|
307
|
+
"max_R": df_event["R"].max(),
|
|
308
|
+
# DSD statistics
|
|
309
|
+
"max_Dmax": df_event["Dmax"].max(),
|
|
310
|
+
"mean_Dm": df_event["Dm"].mean(),
|
|
311
|
+
"median_Dm": df_event["Dm"].median(),
|
|
312
|
+
"max_Dm": df_event["Dm"].max(),
|
|
313
|
+
"mean_sigma_m": df_event["sigma_m"].mean(),
|
|
314
|
+
"median_sigma_m": df_event["sigma_m"].median(),
|
|
315
|
+
"max_sigma_m": df_event["sigma_m"].max(),
|
|
316
|
+
"mean_W": df_event["W"].mean(),
|
|
317
|
+
"median_W": df_event["W"].median(),
|
|
318
|
+
"max_W": df_event["W"].max(),
|
|
319
|
+
"max_Z": df_event["Z"].max(),
|
|
320
|
+
"mean_Nbins": int(df_event["Nbins"].mean()),
|
|
321
|
+
"max_Nbins": int(df_event["Nbins"].max()),
|
|
322
|
+
# TODO in future:
|
|
323
|
+
# - rain_detected = True
|
|
324
|
+
# - snow_detected = True
|
|
325
|
+
# - hail_detected = True
|
|
326
|
+
}
|
|
327
|
+
events_stats.append(event_stats)
|
|
328
|
+
|
|
329
|
+
df_events = pd.DataFrame.from_records(events_stats)
|
|
330
|
+
return df_events
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def prepare_latex_table_dsd_summary(df):
|
|
334
|
+
"""Prepare a DataFrame with DSD statistics for LaTeX table output."""
|
|
335
|
+
df = df.copy()
|
|
336
|
+
# Round float columns to nearest integer, leave ints unchanged
|
|
337
|
+
float_cols = df.select_dtypes(include=["float"]).columns
|
|
338
|
+
df[float_cols] = df[float_cols].astype(float).round(decimals=2).astype(str)
|
|
339
|
+
# Rename
|
|
340
|
+
rename_dict = {
|
|
341
|
+
"W": r"$W\,[\mathrm{g}\,\mathrm{m}^{-3}]$", # [g/m3]
|
|
342
|
+
"R": r"$R\,[\mathrm{mm}\,\mathrm{h}^{-1}]$", # [mm/hr]
|
|
343
|
+
"Z": r"$Z\,[\mathrm{dBZ}]$", # [dBZ]
|
|
344
|
+
"D50": r"$D_{50}\,[\mathrm{mm}]$", # [mm]
|
|
345
|
+
"Dm": r"$D_{m}\,[\mathrm{mm}]$", # [mm]
|
|
346
|
+
"sigma_m": r"$\sigma_{m}\,[\mathrm{mm}]$", # [mm]
|
|
347
|
+
"Dmax": r"$D_{\max}\,[\mathrm{mm}]$", # [mm]
|
|
348
|
+
"Nw": r"$N_{w}\,[\mathrm{mm}^{-1}\,\mathrm{m}^{-3}]$", # [mm$^{-1}$ m$^{-3}$]
|
|
349
|
+
"log10(Nw)": r"$\log_{10}(N_{w})$", # [$\log_{10}$(mm$^{-1}$ m$^{-3}$)]
|
|
350
|
+
"Nt": r"$N_{t}\,[\mathrm{m}^{-3}]$", # [m$^{-3}$]
|
|
351
|
+
"log10(Nt)": r"$\log_{10}(N_{t})$", # [log10(m$^{-3}$)]
|
|
352
|
+
}
|
|
353
|
+
df = df.rename(index=rename_dict)
|
|
354
|
+
return df
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def prepare_latex_table_events_summary(df):
|
|
358
|
+
"""Prepare a DataFrame with events statistics for LaTeX table output."""
|
|
359
|
+
df = df.copy()
|
|
360
|
+
# Round datetime to minutes
|
|
361
|
+
df["start_time"] = df["start_time"].dt.strftime("%Y-%m-%d %H:%M")
|
|
362
|
+
df["end_time"] = df["end_time"].dt.strftime("%Y-%m-%d %H:%M")
|
|
363
|
+
# Round float columns to nearest integer, leave ints unchanged
|
|
364
|
+
float_cols = df.select_dtypes(include=["float"]).columns
|
|
365
|
+
df[float_cols] = df[float_cols].astype(float).round(decimals=2).astype(str)
|
|
366
|
+
# Rename
|
|
367
|
+
rename_dict = {
|
|
368
|
+
"start_time": r"Start",
|
|
369
|
+
"end_time": r"End",
|
|
370
|
+
"duration": r"Min.",
|
|
371
|
+
"rainy_minutes_>0.1": r"R>0.1",
|
|
372
|
+
"rainy_minutes_>1": r"R>1",
|
|
373
|
+
"rainy_minutes_>5": r"R>5",
|
|
374
|
+
# 'rainy_minutes_>10': r'R>10',
|
|
375
|
+
# 'rainy_minutes_>20': r'R>20',
|
|
376
|
+
"rainy_minutes_>50": r"R>50",
|
|
377
|
+
# 'rainy_minutes_>100': r'R>100',
|
|
378
|
+
"P_total": r"$P_{\mathrm{tot}} [mm]$",
|
|
379
|
+
"mean_R": r"$R_{\mathrm{mean}}$",
|
|
380
|
+
"median_R": r"$R_{\mathrm{median}}$",
|
|
381
|
+
"max_R": r"$R_{\max}$",
|
|
382
|
+
"max_Dmax": r"$D_{\max}$",
|
|
383
|
+
"mean_Dm": r"$D_{m,\mathrm{mean}}$",
|
|
384
|
+
"median_Dm": r"$D_{m,\mathrm{median}}$",
|
|
385
|
+
"max_Dm": r"$D_{m,\max}$",
|
|
386
|
+
"mean_sigma_m": r"$\sigma_{m,\mathrm{mean}}$",
|
|
387
|
+
"median_sigma_m": r"$\sigma_{m,\mathrm{median}}$",
|
|
388
|
+
"max_sigma_m": r"$\sigma_{m,\max}$",
|
|
389
|
+
"mean_W": r"$W_{\mathrm{mean}}$",
|
|
390
|
+
"median_W": r"$W_{\mathrm{median}}$",
|
|
391
|
+
"max_W": r"$W_{\max}$",
|
|
392
|
+
"max_Z": r"$Z_{\max}$",
|
|
393
|
+
"mean_Nbins": r"$N_{\mathrm{bins},\mathrm{mean}}$",
|
|
394
|
+
"max_Nbins": r"$N_{\mathrm{bins},\max}$",
|
|
395
|
+
}
|
|
396
|
+
df = df[list(rename_dict)]
|
|
397
|
+
df = df.rename(columns=rename_dict)
|
|
398
|
+
return df
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
####-------------------------------------------------------------------
|
|
402
|
+
#### Powerlaw routines
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def fit_powerlaw_with_ransac(x, y):
|
|
406
|
+
"""Fit powerlaw relationship with RANSAC algorithm."""
|
|
407
|
+
from sklearn.linear_model import LinearRegression, RANSACRegressor
|
|
408
|
+
|
|
409
|
+
x = np.asanyarray(x)
|
|
410
|
+
y = np.asanyarray(y)
|
|
411
|
+
X = np.log10(x).reshape(-1, 1)
|
|
412
|
+
Y = np.log10(y)
|
|
413
|
+
ransac = RANSACRegressor(
|
|
414
|
+
estimator=LinearRegression(),
|
|
415
|
+
min_samples=0.5,
|
|
416
|
+
residual_threshold=0.3,
|
|
417
|
+
random_state=42,
|
|
418
|
+
)
|
|
419
|
+
ransac.fit(X, Y)
|
|
420
|
+
b = ransac.estimator_.coef_[0] # slope
|
|
421
|
+
loga = ransac.estimator_.intercept_ # intercept
|
|
422
|
+
a = 10**loga
|
|
423
|
+
return a, b
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def fit_powerlaw(x, y, xbins, quantile=0.5, min_counts=10, x_in_db=False, use_ransac=True):
|
|
427
|
+
"""
|
|
428
|
+
Fit a power-law relationship ``y = a * x**b`` to binned median values.
|
|
429
|
+
|
|
430
|
+
This function bins ``x`` into intervals defined by ``xbins``, computes the
|
|
431
|
+
median of ``y`` in each bin (robust to outliers), and fits a power-law model
|
|
432
|
+
using the RANSAC or Levenberg-Marquardt algorithm.
|
|
433
|
+
Optionally, ``x`` can be converted from decibel units to linear scale automatically before fitting.
|
|
434
|
+
|
|
435
|
+
Parameters
|
|
436
|
+
----------
|
|
437
|
+
x : array_like
|
|
438
|
+
Independent variable values. Must be positive and finite after filtering.
|
|
439
|
+
y : array_like
|
|
440
|
+
Dependent variable values. Must be positive and finite after filtering.
|
|
441
|
+
xbins : array_like
|
|
442
|
+
Bin edges for grouping ``x`` values (passed to ``pandas.cut``).
|
|
443
|
+
quantile : float, optional
|
|
444
|
+
Quantile of ``y`` to compute in each bin (between 0 and 1).
|
|
445
|
+
For example: 0.5 = median, 0.25 = lower quartile, 0.75 = upper quartile.
|
|
446
|
+
Default is 0.5 (median)
|
|
447
|
+
x_in_db : bool, optional
|
|
448
|
+
If True, converts ``x`` values from decibels (dB) to linear scale using
|
|
449
|
+
:func:`disdrodb.idecibel`. Default is False.
|
|
450
|
+
use_ransac: bool, optional
|
|
451
|
+
Whether to fit the powerlaw using the Random Sample Consensus (RANSAC)
|
|
452
|
+
algorithm or using the Levenberg-Marquardt algorithm.
|
|
453
|
+
The default is True.
|
|
454
|
+
To fit with RANSAC, scikit-learn must be installed.
|
|
455
|
+
|
|
456
|
+
Returns
|
|
457
|
+
-------
|
|
458
|
+
params : tuple of float
|
|
459
|
+
Estimated parameters ``(a, b)`` of the power-law relationship.
|
|
460
|
+
params_std : tuple of float
|
|
461
|
+
One standard deviation uncertainties ``(a_std, b_std)`` estimated from
|
|
462
|
+
the covariance matrix of the fit.
|
|
463
|
+
Parameters standard deviation is currently
|
|
464
|
+
not available if fitting with the RANSAC algorithm.
|
|
465
|
+
|
|
466
|
+
Notes
|
|
467
|
+
-----
|
|
468
|
+
- This implementation uses median statistics within bins, which reduces
|
|
469
|
+
the influence of outliers.
|
|
470
|
+
- Both ``x`` and ``y`` are filtered to retain only positive, finite values
|
|
471
|
+
before binning.
|
|
472
|
+
- Fitting is performed on the bin centers (midpoints between bin edges).
|
|
473
|
+
|
|
474
|
+
See Also
|
|
475
|
+
--------
|
|
476
|
+
predict_from_powerlaw : Predict values from the fitted power-law parameters.
|
|
477
|
+
inverse_powerlaw_parameters : Compute parameters of the inverse power law.
|
|
478
|
+
|
|
479
|
+
Examples
|
|
480
|
+
--------
|
|
481
|
+
>>> import numpy as np
|
|
482
|
+
>>> x = np.linspace(1, 50, 200)
|
|
483
|
+
>>> y = 2 * x**1.5 + np.random.normal(0, 5, size=x.size)
|
|
484
|
+
>>> xbins = np.arange(0, 60, 5)
|
|
485
|
+
>>> (a, b), (a_std, b_std) = fit_powerlaw(x, y, xbins)
|
|
486
|
+
"""
|
|
487
|
+
# Set min_counts to 0 during pytest execution in order to test the summary routine
|
|
488
|
+
if os.environ.get("PYTEST_CURRENT_TEST"):
|
|
489
|
+
min_counts = 0
|
|
490
|
+
|
|
491
|
+
# Check if RANSAC algorithm is available
|
|
492
|
+
sklearn_available = importlib.util.find_spec("sklearn") is not None
|
|
493
|
+
if use_ransac and not sklearn_available:
|
|
494
|
+
use_ransac = False
|
|
495
|
+
|
|
496
|
+
# Ensure numpy array
|
|
497
|
+
x = np.asanyarray(x)
|
|
498
|
+
y = np.asanyarray(y)
|
|
499
|
+
|
|
500
|
+
# Ensure values > 0 and finite
|
|
501
|
+
valid_values = (x > 0) & (y > 0) & np.isfinite(x) & np.isfinite(y)
|
|
502
|
+
x = x[valid_values]
|
|
503
|
+
y = y[valid_values]
|
|
504
|
+
|
|
505
|
+
# Define dataframe
|
|
506
|
+
df_data = pd.DataFrame({"x": x, "y": y})
|
|
507
|
+
|
|
508
|
+
# Alternative code
|
|
509
|
+
# from disdrodb.utils.dataframe import compute_1d_histogram
|
|
510
|
+
# df_agg = compute_1d_histogram(
|
|
511
|
+
# df=df_data,
|
|
512
|
+
# column="x",
|
|
513
|
+
# variables="y",
|
|
514
|
+
# bins=xbins,
|
|
515
|
+
# prefix_name=True,
|
|
516
|
+
# include_quantiles=False
|
|
517
|
+
# )
|
|
518
|
+
# df_agg["count"] # instead of N
|
|
519
|
+
|
|
520
|
+
# - Keep only data within bin range
|
|
521
|
+
df_data = df_data[(df_data["x"] >= xbins[0]) & (df_data["x"] < xbins[-1])]
|
|
522
|
+
# - Define bins
|
|
523
|
+
df_data["x_bins"] = pd.cut(df_data["x"], bins=xbins, right=False)
|
|
524
|
+
# - Remove data outside specified bins
|
|
525
|
+
df_data = df_data[df_data["x_bins"].cat.codes != -1]
|
|
526
|
+
|
|
527
|
+
# Derive median y values at various x points
|
|
528
|
+
# - This typically remove outliers
|
|
529
|
+
df_agg = df_data.groupby(by="x_bins", observed=True)["y"].agg(
|
|
530
|
+
y=lambda s: s.quantile(quantile),
|
|
531
|
+
n="count",
|
|
532
|
+
mad=lambda s: (s - s.median()).abs().median(),
|
|
533
|
+
)
|
|
534
|
+
df_agg["x"] = np.array([iv.left + (iv.right - iv.left) / 2 for iv in df_agg.index])
|
|
535
|
+
|
|
536
|
+
# If input is in decibel, convert to linear scale
|
|
537
|
+
if x_in_db:
|
|
538
|
+
df_agg["x"] = disdrodb.idecibel(df_agg["x"])
|
|
539
|
+
|
|
540
|
+
# Remove bins with less than n counts
|
|
541
|
+
df_agg = df_agg[df_agg["n"] > min_counts]
|
|
542
|
+
if len(df_agg) < 5:
|
|
543
|
+
raise ValueError("Not enough data to fit a power law.")
|
|
544
|
+
|
|
545
|
+
# Estimate sigma based on MAD
|
|
546
|
+
sigma = df_agg["mad"]
|
|
547
|
+
|
|
548
|
+
# Fit the data
|
|
549
|
+
with suppress_warnings():
|
|
550
|
+
if use_ransac:
|
|
551
|
+
a, b = fit_powerlaw_with_ransac(x=df_agg["x"], y=df_agg["y"])
|
|
552
|
+
a_std = None
|
|
553
|
+
b_std = None
|
|
554
|
+
else:
|
|
555
|
+
|
|
556
|
+
(a, b), pcov = curve_fit(
|
|
557
|
+
lambda x, a, b: a * np.power(x, b),
|
|
558
|
+
df_agg["x"],
|
|
559
|
+
df_agg["y"],
|
|
560
|
+
method="lm",
|
|
561
|
+
sigma=sigma,
|
|
562
|
+
absolute_sigma=True,
|
|
563
|
+
maxfev=10_000, # max n iterations
|
|
564
|
+
)
|
|
565
|
+
(a_std, b_std) = np.sqrt(np.diag(pcov))
|
|
566
|
+
a_std = float(a_std)
|
|
567
|
+
b_std = float(b_std)
|
|
568
|
+
|
|
569
|
+
# Return the parameters and their standard deviation
|
|
570
|
+
return (float(a), float(b)), (a_std, b_std)
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def predict_from_powerlaw(x, a, b):
|
|
574
|
+
"""
|
|
575
|
+
Predict values from a power-law relationship ``y = a * x**b``.
|
|
576
|
+
|
|
577
|
+
Parameters
|
|
578
|
+
----------
|
|
579
|
+
x : array_like
|
|
580
|
+
Independent variable values.
|
|
581
|
+
a : float
|
|
582
|
+
Power-law coefficient.
|
|
583
|
+
b : float
|
|
584
|
+
Power-law exponent.
|
|
585
|
+
|
|
586
|
+
Returns
|
|
587
|
+
-------
|
|
588
|
+
y : ndarray
|
|
589
|
+
Predicted dependent variable values.
|
|
590
|
+
|
|
591
|
+
Notes
|
|
592
|
+
-----
|
|
593
|
+
This function does not check for invalid (negative or zero) ``x`` values.
|
|
594
|
+
Ensure that ``x`` is compatible with the model before calling.
|
|
595
|
+
"""
|
|
596
|
+
return a * np.power(x, b)
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def inverse_powerlaw_parameters(a, b):
|
|
600
|
+
"""
|
|
601
|
+
Compute parameters of the inverse power-law relationship.
|
|
602
|
+
|
|
603
|
+
Given a model ``y = a * x**b``, this returns parameters ``(A, B)``
|
|
604
|
+
such that the inverse relation ``x = A * y**B`` holds.
|
|
605
|
+
|
|
606
|
+
Parameters
|
|
607
|
+
----------
|
|
608
|
+
a : float
|
|
609
|
+
Power-law coefficient in ``y = a * x**b``.
|
|
610
|
+
b : float
|
|
611
|
+
Power-law exponent in ``y = a * x**b``.
|
|
612
|
+
|
|
613
|
+
Returns
|
|
614
|
+
-------
|
|
615
|
+
A : float
|
|
616
|
+
Coefficient of the inverse power-law model.
|
|
617
|
+
B : float
|
|
618
|
+
Exponent of the inverse power-law model.
|
|
619
|
+
"""
|
|
620
|
+
A = 1 / (a ** (1 / b))
|
|
621
|
+
B = 1 / b
|
|
622
|
+
return A, B
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def predict_from_inverse_powerlaw(x, a, b):
|
|
626
|
+
"""
|
|
627
|
+
Predict values from the inverse power-law relationship.
|
|
628
|
+
|
|
629
|
+
Given parameters ``a`` and ``b`` from ``x = a * y**b``, this function computes
|
|
630
|
+
``y`` given ``x``.
|
|
631
|
+
|
|
632
|
+
Parameters
|
|
633
|
+
----------
|
|
634
|
+
x : array_like
|
|
635
|
+
Values of ``x`` (independent variable in the original power law).
|
|
636
|
+
a : float
|
|
637
|
+
Power-law coefficient of the inverse power-law model.
|
|
638
|
+
b : float
|
|
639
|
+
Power-law exponent of the inverse power-law model.
|
|
640
|
+
|
|
641
|
+
Returns
|
|
642
|
+
-------
|
|
643
|
+
y : ndarray
|
|
644
|
+
Predicted dependent variable values.
|
|
645
|
+
"""
|
|
646
|
+
return (x ** (1 / b)) / (a ** (1 / b))
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
####-------------------------------------------------------------------
|
|
650
|
+
#### Drop spectrum plots
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def plot_drop_spectrum(drop_number, norm=None, add_colorbar=True, title="Drop Spectrum"):
|
|
654
|
+
"""Plot the drop spectrum."""
|
|
655
|
+
cmap = plt.get_cmap("Spectral_r").copy()
|
|
656
|
+
cmap.set_under("none")
|
|
657
|
+
if "time" in drop_number.dims:
|
|
658
|
+
drop_number = drop_number.sum(dim="time")
|
|
659
|
+
if norm is None:
|
|
660
|
+
norm = LogNorm(vmin=1, vmax=None) if drop_number.sum() > 0 else None
|
|
661
|
+
|
|
662
|
+
p = drop_number.plot.pcolormesh(
|
|
663
|
+
x=DIAMETER_DIMENSION,
|
|
664
|
+
y=VELOCITY_DIMENSION,
|
|
665
|
+
cmap=cmap,
|
|
666
|
+
extend="max",
|
|
667
|
+
norm=norm,
|
|
668
|
+
add_colorbar=add_colorbar,
|
|
669
|
+
cbar_kwargs={"label": "Number of particles"},
|
|
670
|
+
)
|
|
671
|
+
p.axes.set_xlabel("Diamenter [mm]")
|
|
672
|
+
p.axes.set_ylabel("Fall velocity [m/s]")
|
|
673
|
+
p.axes.set_title(title)
|
|
674
|
+
return p
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def plot_raw_and_filtered_spectrums(
|
|
678
|
+
raw_drop_number,
|
|
679
|
+
drop_number,
|
|
680
|
+
theoretical_average_velocity,
|
|
681
|
+
measured_average_velocity=None,
|
|
682
|
+
norm=None,
|
|
683
|
+
figsize=(8, 4),
|
|
684
|
+
dpi=300,
|
|
685
|
+
):
|
|
686
|
+
"""Plot raw and filtered drop spectrum."""
|
|
687
|
+
# Drop number matrix
|
|
688
|
+
cmap = plt.get_cmap("Spectral_r").copy()
|
|
689
|
+
cmap.set_under("none")
|
|
690
|
+
|
|
691
|
+
if norm is None:
|
|
692
|
+
norm = LogNorm(1, None)
|
|
693
|
+
|
|
694
|
+
fig = plt.figure(figsize=figsize, dpi=dpi)
|
|
695
|
+
gs = GridSpec(1, 2, width_ratios=[1, 1.15], wspace=0.05) # More space for ax2
|
|
696
|
+
ax1 = fig.add_subplot(gs[0])
|
|
697
|
+
ax2 = fig.add_subplot(gs[1])
|
|
698
|
+
|
|
699
|
+
raw_drop_number.plot.pcolormesh(
|
|
700
|
+
x=DIAMETER_DIMENSION,
|
|
701
|
+
y=VELOCITY_DIMENSION,
|
|
702
|
+
ax=ax1,
|
|
703
|
+
cmap=cmap,
|
|
704
|
+
norm=norm,
|
|
705
|
+
extend="max",
|
|
706
|
+
add_colorbar=False,
|
|
707
|
+
)
|
|
708
|
+
theoretical_average_velocity.plot(ax=ax1, c="k", linestyle="dashed")
|
|
709
|
+
if measured_average_velocity is not None:
|
|
710
|
+
measured_average_velocity.plot(ax=ax1, c="k", linestyle="dotted")
|
|
711
|
+
ax1.set_xlabel("Diamenter [mm]")
|
|
712
|
+
ax1.set_ylabel("Fall velocity [m/s]")
|
|
713
|
+
ax1.set_title("Raw Spectrum")
|
|
714
|
+
drop_number.plot.pcolormesh(
|
|
715
|
+
x=DIAMETER_DIMENSION,
|
|
716
|
+
y=VELOCITY_DIMENSION,
|
|
717
|
+
cmap=cmap,
|
|
718
|
+
extend="max",
|
|
719
|
+
ax=ax2,
|
|
720
|
+
norm=norm,
|
|
721
|
+
cbar_kwargs={"label": "Number of particles"},
|
|
722
|
+
)
|
|
723
|
+
theoretical_average_velocity.plot(ax=ax2, c="k", linestyle="dashed", label="Theoretical velocity")
|
|
724
|
+
if measured_average_velocity is not None:
|
|
725
|
+
measured_average_velocity.plot(ax=ax2, c="k", linestyle="dotted", label="Measured average velocity")
|
|
726
|
+
ax2.set_yticks([])
|
|
727
|
+
ax2.set_yticklabels([])
|
|
728
|
+
ax2.set_xlabel("Diamenter [mm]")
|
|
729
|
+
ax2.set_ylabel("")
|
|
730
|
+
ax2.set_title("Filtered Spectrum")
|
|
731
|
+
ax2.legend(loc="lower right", frameon=False)
|
|
732
|
+
return fig
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
####-------------------------------------------------------------------
|
|
736
|
+
#### N(D) Climatological plots
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
def create_nd_dataframe(ds, variables=None):
|
|
740
|
+
"""Create pandas Dataframe with N(D) data."""
|
|
741
|
+
# Define variables to select
|
|
742
|
+
if isinstance(variables, str):
|
|
743
|
+
variables = [variables]
|
|
744
|
+
variables = [] if variables is None else variables
|
|
745
|
+
variables = ["drop_number_concentration", "Nw", "diameter_bin_center", "Dm", "D50", "R", *variables]
|
|
746
|
+
variables = np.unique(variables).tolist()
|
|
747
|
+
|
|
748
|
+
# Retrieve stacked N(D) dataframe
|
|
749
|
+
ds_stack = ds[variables].stack( # noqa: PD013
|
|
750
|
+
dim={"obs": ["time", "diameter_bin_center"]},
|
|
751
|
+
)
|
|
752
|
+
# Drop coordinates
|
|
753
|
+
coords_to_drop = [
|
|
754
|
+
"velocity_method",
|
|
755
|
+
"sample_interval",
|
|
756
|
+
*RADAR_OPTIONS,
|
|
757
|
+
]
|
|
758
|
+
df_nd = ds_stack.to_dataframe().drop(columns=coords_to_drop, errors="ignore")
|
|
759
|
+
df_nd["D"] = df_nd["diameter_bin_center"]
|
|
760
|
+
df_nd["N(D)"] = df_nd["drop_number_concentration"]
|
|
761
|
+
df_nd = df_nd[df_nd["R"] != 0]
|
|
762
|
+
df_nd = df_nd[df_nd["N(D)"] != 0]
|
|
763
|
+
|
|
764
|
+
# Compute normalized density
|
|
765
|
+
df_nd["D/D50"] = df_nd["D"] / df_nd["D50"]
|
|
766
|
+
df_nd["D/Dm"] = df_nd["D"] / df_nd["Dm"]
|
|
767
|
+
df_nd["N(D)/Nw"] = df_nd["N(D)"] / df_nd["Nw"]
|
|
768
|
+
df_nd["log10[N(D)/Nw]"] = np.log10(df_nd["N(D)/Nw"])
|
|
769
|
+
return df_nd
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
def plot_normalized_dsd_density(df_nd, x="D/D50", figsize=(8, 8), dpi=300):
|
|
773
|
+
"""Plot normalized DSD N(D)/Nw ~ D/D50 (or D/Dm) density."""
|
|
774
|
+
ds_stats = compute_2d_histogram(
|
|
775
|
+
df_nd,
|
|
776
|
+
x=x,
|
|
777
|
+
y="N(D)/Nw",
|
|
778
|
+
x_bins=np.arange(0, 4, 0.025),
|
|
779
|
+
y_bins=log_arange(1e-5, 50, log_step=0.1, base=10),
|
|
780
|
+
)
|
|
781
|
+
cmap = plt.get_cmap("Spectral_r").copy()
|
|
782
|
+
cmap.set_under(alpha=0)
|
|
783
|
+
norm = LogNorm(1, None)
|
|
784
|
+
|
|
785
|
+
ds_stats = ds_stats.isel({"N(D)/Nw": ds_stats["N(D)/Nw"] > 0})
|
|
786
|
+
|
|
787
|
+
fig, ax = plt.subplots(1, 1, figsize=figsize, dpi=dpi)
|
|
788
|
+
p = ds_stats["count"].plot.pcolormesh(
|
|
789
|
+
x=x,
|
|
790
|
+
y="N(D)/Nw",
|
|
791
|
+
ax=ax,
|
|
792
|
+
vmin=1,
|
|
793
|
+
cmap=cmap,
|
|
794
|
+
norm=norm,
|
|
795
|
+
extend="max",
|
|
796
|
+
yscale="log",
|
|
797
|
+
)
|
|
798
|
+
ax.set_ylim(1e-5, 20)
|
|
799
|
+
ax.set_xlim(0, 4)
|
|
800
|
+
ax.set_xlabel(f"{x} [-]")
|
|
801
|
+
ax.set_ylabel(r"$N(D)/N_w$ [-]")
|
|
802
|
+
ax.set_title("Normalized DSD")
|
|
803
|
+
return p
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
def plot_dsd_density(df_nd, diameter_bin_edges, figsize=(8, 8), dpi=300):
|
|
807
|
+
"""Plot N(D) ~ D density."""
|
|
808
|
+
ds_stats = compute_2d_histogram(
|
|
809
|
+
df_nd,
|
|
810
|
+
x="D",
|
|
811
|
+
y="N(D)",
|
|
812
|
+
x_bins=diameter_bin_edges,
|
|
813
|
+
y_bins=log_arange(0.1, 20_000, log_step=0.1, base=10),
|
|
814
|
+
)
|
|
815
|
+
cmap = plt.get_cmap("Spectral_r").copy()
|
|
816
|
+
cmap.set_under(alpha=0)
|
|
817
|
+
norm = LogNorm(1, None)
|
|
818
|
+
fig, ax = plt.subplots(1, 1, figsize=figsize, dpi=dpi)
|
|
819
|
+
p = ds_stats["count"].plot.pcolormesh(x="D", y="N(D)", ax=ax, cmap=cmap, norm=norm, extend="max", yscale="log")
|
|
820
|
+
ax.set_xlim(0, 8)
|
|
821
|
+
ax.set_ylim(1, 20_000)
|
|
822
|
+
ax.set_xlabel(r"$D$ [mm]")
|
|
823
|
+
ax.set_ylabel(r"$N(D)$ [m$^{-3}$ mm$^{-1}$]")
|
|
824
|
+
ax.set_title("DSD")
|
|
825
|
+
return p
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
def plot_dsd_with_dense_lines(drop_number_concentration, r, figsize=(8, 8), dpi=300):
|
|
829
|
+
"""Plot N(D) ~ D using dense lines."""
|
|
830
|
+
# Define intervals for rain rates
|
|
831
|
+
r_bins = [0, 2, 5, 10, 50, 100, 500]
|
|
832
|
+
|
|
833
|
+
# Define N(D) bins and diameter bin edeges
|
|
834
|
+
y_bins = log_arange(1, 20_000, log_step=0.025, base=10)
|
|
835
|
+
diameter_bin_edges = np.arange(0, 8, 0.01)
|
|
836
|
+
|
|
837
|
+
# Resample N(D) to high resolution !
|
|
838
|
+
# - quadratic, pchip
|
|
839
|
+
da = resample_drop_number_concentration(
|
|
840
|
+
drop_number_concentration.compute(),
|
|
841
|
+
diameter_bin_edges=diameter_bin_edges,
|
|
842
|
+
method="linear",
|
|
843
|
+
)
|
|
844
|
+
ds_resampled = xr.Dataset(
|
|
845
|
+
{
|
|
846
|
+
"R": r.compute(),
|
|
847
|
+
"drop_number_concentration": da,
|
|
848
|
+
},
|
|
849
|
+
)
|
|
850
|
+
|
|
851
|
+
# Define diameter bin edges to compute dense lines
|
|
852
|
+
x_bins = da.disdrodb.diameter_bin_edges
|
|
853
|
+
|
|
854
|
+
# Define discrete colormap (one color per rain-interval):
|
|
855
|
+
cmap_list = [
|
|
856
|
+
plt.get_cmap("Reds"),
|
|
857
|
+
plt.get_cmap("Oranges"),
|
|
858
|
+
plt.get_cmap("Purples"),
|
|
859
|
+
plt.get_cmap("Greens"),
|
|
860
|
+
plt.get_cmap("Blues"),
|
|
861
|
+
plt.get_cmap("Grays"),
|
|
862
|
+
]
|
|
863
|
+
cmap_list = [ListedColormap(cmap(np.arange(0, cmap.N))[-40:]) for cmap in cmap_list]
|
|
864
|
+
|
|
865
|
+
# Compute dense lines
|
|
866
|
+
dict_rgb = {}
|
|
867
|
+
for i in range(0, len(r_bins) - 1):
|
|
868
|
+
|
|
869
|
+
# Define dataset subset
|
|
870
|
+
idx_rain_interval = np.logical_and(ds_resampled["R"] >= r_bins[i], ds_resampled["R"] < r_bins[i + 1])
|
|
871
|
+
da = ds_resampled.isel(time=idx_rain_interval)["drop_number_concentration"]
|
|
872
|
+
if da.sizes["time"] == 0:
|
|
873
|
+
continue
|
|
874
|
+
|
|
875
|
+
# Retrieve dense lines
|
|
876
|
+
da_dense_lines = compute_dense_lines(
|
|
877
|
+
da=da,
|
|
878
|
+
coord="diameter_bin_center",
|
|
879
|
+
x_bins=x_bins,
|
|
880
|
+
y_bins=y_bins,
|
|
881
|
+
normalization="max",
|
|
882
|
+
)
|
|
883
|
+
|
|
884
|
+
# Define cmap
|
|
885
|
+
cmap = cmap_list[i]
|
|
886
|
+
|
|
887
|
+
# Map colors and transparency
|
|
888
|
+
# da_rgb = to_rgba(da_dense_lines, cmap=cmap, scaling="linear")
|
|
889
|
+
# da_rgb = to_rgba(da_dense_lines, cmap=cmap, scaling="exp")
|
|
890
|
+
# da_rgb = to_rgba(da_dense_lines, cmap=cmap, scaling="log")
|
|
891
|
+
da_rgb = to_rgba(da_dense_lines, cmap=cmap, scaling="sqrt")
|
|
892
|
+
|
|
893
|
+
# Add to dictionary
|
|
894
|
+
dict_rgb[i] = da_rgb
|
|
895
|
+
|
|
896
|
+
# Blend images with max-alpha
|
|
897
|
+
ds_rgb = xr.concat(dict_rgb.values(), dim="r_class")
|
|
898
|
+
da_blended = max_blend_images(ds_rgb, dim="r_class")
|
|
899
|
+
|
|
900
|
+
# Prepare legend handles
|
|
901
|
+
handles = []
|
|
902
|
+
labels = []
|
|
903
|
+
for i in range(len(r_bins) - 1):
|
|
904
|
+
color = cmap_list[i](0.8) # pick a representative color from each cmap
|
|
905
|
+
handle = mlines.Line2D([], [], color=color, alpha=0.6, linewidth=2)
|
|
906
|
+
label = f"[{r_bins[i]} - {r_bins[i+1]}]"
|
|
907
|
+
handles.append(handle)
|
|
908
|
+
labels.append(label)
|
|
909
|
+
|
|
910
|
+
# Create figure
|
|
911
|
+
fig, ax = plt.subplots(1, 1, figsize=figsize, dpi=dpi)
|
|
912
|
+
|
|
913
|
+
p = ax.pcolormesh(
|
|
914
|
+
da_blended["diameter_bin_center"],
|
|
915
|
+
da_blended["drop_number_concentration"],
|
|
916
|
+
da_blended.data,
|
|
917
|
+
)
|
|
918
|
+
|
|
919
|
+
# Set axis scale and limits
|
|
920
|
+
ax.set_yscale("log")
|
|
921
|
+
ax.set_xlim(0, 8)
|
|
922
|
+
ax.set_ylim(1, 20_000)
|
|
923
|
+
|
|
924
|
+
# Add axis labels and title
|
|
925
|
+
ax.set_xlabel(r"$D$ [mm]")
|
|
926
|
+
ax.set_ylabel(r"$N(D)$ [m$^{-3}$ mm$^{-1}$]")
|
|
927
|
+
ax.set_title("DSD")
|
|
928
|
+
|
|
929
|
+
# Add legend with title
|
|
930
|
+
ax.legend(handles, labels, title="Rain rate [mm/hr]", loc="upper right")
|
|
931
|
+
|
|
932
|
+
# Return figure
|
|
933
|
+
return p
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
####-------------------------------------------------------------------
|
|
937
|
+
#### DSD parameters plots
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
def define_lognorm_max_value(value):
|
|
941
|
+
"""Round up to next nice number: 90->100, 400->500, 1200->2000."""
|
|
942
|
+
if value <= 0:
|
|
943
|
+
return 1
|
|
944
|
+
magnitude = 10 ** np.floor(np.log10(value))
|
|
945
|
+
first_digit = value / magnitude
|
|
946
|
+
nice_value = 1 if first_digit <= 1 else 2 if first_digit <= 2 else 5 if first_digit <= 5 else 10
|
|
947
|
+
return nice_value * magnitude
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
def plot_dsd_params_relationships(df, add_nt=False, dpi=300):
|
|
951
|
+
"""Create a figure illustrating the relationships between DSD parameters."""
|
|
952
|
+
# TODO: option to use D50 instead of Dm
|
|
953
|
+
|
|
954
|
+
# Compute the required datasets for the plots
|
|
955
|
+
# - Dm vs Nw
|
|
956
|
+
ds_Dm_Nw_stats = compute_2d_histogram(
|
|
957
|
+
df,
|
|
958
|
+
x="Dm",
|
|
959
|
+
y="Nw",
|
|
960
|
+
variables=["R", "W", "Nt"],
|
|
961
|
+
x_bins=np.arange(0, 8, 0.1),
|
|
962
|
+
y_bins=log_arange(10, 1_000_000, log_step=0.05, base=10),
|
|
963
|
+
)
|
|
964
|
+
|
|
965
|
+
# - Dm vs LWC (W)
|
|
966
|
+
ds_Dm_LWC_stats = compute_2d_histogram(
|
|
967
|
+
df,
|
|
968
|
+
x="Dm",
|
|
969
|
+
y="W",
|
|
970
|
+
variables=["R", "Nw", "Nt"],
|
|
971
|
+
x_bins=np.arange(0, 8, 0.1),
|
|
972
|
+
y_bins=log_arange(0.01, 10, log_step=0.05, base=10),
|
|
973
|
+
)
|
|
974
|
+
|
|
975
|
+
# - Dm vs R
|
|
976
|
+
ds_Dm_R_stats = compute_2d_histogram(
|
|
977
|
+
df,
|
|
978
|
+
x="Dm",
|
|
979
|
+
y="R",
|
|
980
|
+
variables=["Nw", "W", "Nt"],
|
|
981
|
+
x_bins=np.arange(0, 8, 0.1),
|
|
982
|
+
y_bins=log_arange(0.1, 500, log_step=0.05, base=10),
|
|
983
|
+
)
|
|
984
|
+
|
|
985
|
+
# - Dm vs Nt
|
|
986
|
+
ds_Dm_Nt_stats = compute_2d_histogram(
|
|
987
|
+
df,
|
|
988
|
+
x="Dm",
|
|
989
|
+
y="Nt",
|
|
990
|
+
variables=["R", "W", "Nw"],
|
|
991
|
+
x_bins=np.arange(0, 8, 0.1),
|
|
992
|
+
y_bins=log_arange(1, 100_000, log_step=0.05, base=10),
|
|
993
|
+
)
|
|
994
|
+
|
|
995
|
+
# Define different colormaps for each column
|
|
996
|
+
cmap_counts = plt.get_cmap("viridis")
|
|
997
|
+
cmap_lwc = plt.get_cmap("YlOrRd")
|
|
998
|
+
cmap_r = plt.get_cmap("Blues")
|
|
999
|
+
cmap_nt = plt.get_cmap("Greens")
|
|
1000
|
+
|
|
1001
|
+
# Define normalizations for each variable
|
|
1002
|
+
norm_counts = LogNorm(1, None)
|
|
1003
|
+
norm_lwc = LogNorm(0.01, 10)
|
|
1004
|
+
norm_r = LogNorm(0.1, 500)
|
|
1005
|
+
# norm_nw = LogNorm(10, 100000)
|
|
1006
|
+
norm_nt = LogNorm(1, 10000)
|
|
1007
|
+
|
|
1008
|
+
# Define axis limits
|
|
1009
|
+
dm_lim = (0.3, 6)
|
|
1010
|
+
nw_lim = (10, 1_000_000)
|
|
1011
|
+
lwc_lim = (0.01, 10)
|
|
1012
|
+
r_lim = (0.1, 500)
|
|
1013
|
+
nt_lim = (1, 100_000)
|
|
1014
|
+
|
|
1015
|
+
# Define figure size
|
|
1016
|
+
if add_nt:
|
|
1017
|
+
figsize = (16, 16)
|
|
1018
|
+
nrows = 4
|
|
1019
|
+
height_ratios = [0.2, 1, 1, 1, 1]
|
|
1020
|
+
else:
|
|
1021
|
+
figsize = (16, 12)
|
|
1022
|
+
nrows = 3
|
|
1023
|
+
height_ratios = [0.2, 1, 1, 1]
|
|
1024
|
+
|
|
1025
|
+
# Create figure with 4x4 subplots
|
|
1026
|
+
fig = plt.figure(figsize=figsize, dpi=dpi)
|
|
1027
|
+
gs = fig.add_gridspec(nrows + 1, 4, height_ratios=height_ratios, hspace=0.05, wspace=0.02)
|
|
1028
|
+
axes = np.empty((nrows + 1, 4), dtype=object)
|
|
1029
|
+
|
|
1030
|
+
# Create colorbar axes in the bottom row of the gridspec
|
|
1031
|
+
cbar_axes = [fig.add_subplot(gs[0, j]) for j in range(4)]
|
|
1032
|
+
|
|
1033
|
+
# Create axes for the grid
|
|
1034
|
+
for i in range(1, nrows + 1):
|
|
1035
|
+
for j in range(4):
|
|
1036
|
+
axes[i, j] = fig.add_subplot(gs[i, j])
|
|
1037
|
+
|
|
1038
|
+
# Create empty subplot for diagonal elements (when y-axis variable matches column variable)
|
|
1039
|
+
# axes[2, 1].set_visible(False)
|
|
1040
|
+
# axes[3, 2].set_visible(False)
|
|
1041
|
+
# axes[4, 3].set_visible(False)
|
|
1042
|
+
|
|
1043
|
+
####-------------------------------------------------------------------.
|
|
1044
|
+
#### Dm vs Nw
|
|
1045
|
+
#### - Counts
|
|
1046
|
+
im_counts = ds_Dm_Nw_stats["count"].plot.pcolormesh(
|
|
1047
|
+
x="Dm",
|
|
1048
|
+
y="Nw",
|
|
1049
|
+
cmap=cmap_counts,
|
|
1050
|
+
norm=norm_counts,
|
|
1051
|
+
extend="max",
|
|
1052
|
+
yscale="log",
|
|
1053
|
+
add_colorbar=False,
|
|
1054
|
+
ax=axes[1, 0],
|
|
1055
|
+
)
|
|
1056
|
+
axes[1, 0].set_ylabel(r"$N_w$ [mm$^{-1}$ m$^{-3}$]")
|
|
1057
|
+
axes[1, 0].set_xlim(*dm_lim)
|
|
1058
|
+
axes[1, 0].set_ylim(nw_lim)
|
|
1059
|
+
|
|
1060
|
+
#### - LWC
|
|
1061
|
+
im_lwc = ds_Dm_Nw_stats["W_median"].plot.pcolormesh(
|
|
1062
|
+
x="Dm",
|
|
1063
|
+
y="Nw",
|
|
1064
|
+
cmap=cmap_lwc,
|
|
1065
|
+
norm=norm_lwc,
|
|
1066
|
+
extend="both",
|
|
1067
|
+
yscale="log",
|
|
1068
|
+
add_colorbar=False,
|
|
1069
|
+
ax=axes[1, 1],
|
|
1070
|
+
)
|
|
1071
|
+
axes[1, 1].set_xlim(*dm_lim)
|
|
1072
|
+
axes[1, 1].set_ylim(nw_lim)
|
|
1073
|
+
|
|
1074
|
+
#### - R
|
|
1075
|
+
im_r = ds_Dm_Nw_stats["R_median"].plot.pcolormesh(
|
|
1076
|
+
x="Dm",
|
|
1077
|
+
y="Nw",
|
|
1078
|
+
cmap=cmap_r,
|
|
1079
|
+
norm=norm_r,
|
|
1080
|
+
extend="both",
|
|
1081
|
+
yscale="log",
|
|
1082
|
+
add_colorbar=False,
|
|
1083
|
+
ax=axes[1, 2],
|
|
1084
|
+
)
|
|
1085
|
+
axes[1, 2].set_xlim(*dm_lim)
|
|
1086
|
+
axes[1, 2].set_ylim(nw_lim)
|
|
1087
|
+
axes[1, 2].set_yticklabels([])
|
|
1088
|
+
|
|
1089
|
+
#### - Nt
|
|
1090
|
+
im_nt = ds_Dm_Nw_stats["Nt_median"].plot.pcolormesh(
|
|
1091
|
+
x="Dm",
|
|
1092
|
+
y="Nw",
|
|
1093
|
+
cmap=cmap_nt,
|
|
1094
|
+
norm=norm_nt,
|
|
1095
|
+
extend="both",
|
|
1096
|
+
yscale="log",
|
|
1097
|
+
add_colorbar=False,
|
|
1098
|
+
ax=axes[1, 3],
|
|
1099
|
+
)
|
|
1100
|
+
axes[1, 3].set_xlim(*dm_lim)
|
|
1101
|
+
axes[1, 3].set_ylim(nw_lim)
|
|
1102
|
+
axes[1, 3].set_yticklabels([])
|
|
1103
|
+
|
|
1104
|
+
####-------------------------------------------------------------------.
|
|
1105
|
+
#### Dm vs LWC
|
|
1106
|
+
#### - Counts
|
|
1107
|
+
ds_Dm_LWC_stats["count"].plot.pcolormesh(
|
|
1108
|
+
x="Dm",
|
|
1109
|
+
y="W",
|
|
1110
|
+
cmap=cmap_counts,
|
|
1111
|
+
norm=norm_counts,
|
|
1112
|
+
extend="max",
|
|
1113
|
+
yscale="log",
|
|
1114
|
+
add_colorbar=False,
|
|
1115
|
+
ax=axes[2, 0],
|
|
1116
|
+
)
|
|
1117
|
+
axes[2, 0].set_ylabel(r"LWC [g/m³]")
|
|
1118
|
+
axes[2, 0].set_xlim(*dm_lim)
|
|
1119
|
+
axes[2, 0].set_ylim(lwc_lim)
|
|
1120
|
+
|
|
1121
|
+
#### - LWC
|
|
1122
|
+
# - Empty (diagonal where y-axis is W) - handled above in the loop
|
|
1123
|
+
ds_Dm_LWC_stats["R_median"].plot.pcolormesh(
|
|
1124
|
+
x="Dm",
|
|
1125
|
+
y="W",
|
|
1126
|
+
cmap=cmap_r,
|
|
1127
|
+
norm=norm_r,
|
|
1128
|
+
alpha=0, # fully transparent
|
|
1129
|
+
extend="both",
|
|
1130
|
+
yscale="log",
|
|
1131
|
+
add_colorbar=False,
|
|
1132
|
+
ax=axes[2, 1],
|
|
1133
|
+
)
|
|
1134
|
+
axes[2, 1].set_xlim(*dm_lim)
|
|
1135
|
+
axes[2, 1].set_ylim(lwc_lim)
|
|
1136
|
+
axes[2, 1].set_yticklabels([])
|
|
1137
|
+
|
|
1138
|
+
#### - R
|
|
1139
|
+
ds_Dm_LWC_stats["R_median"].plot.pcolormesh(
|
|
1140
|
+
x="Dm",
|
|
1141
|
+
y="W",
|
|
1142
|
+
cmap=cmap_r,
|
|
1143
|
+
norm=norm_r,
|
|
1144
|
+
extend="both",
|
|
1145
|
+
yscale="log",
|
|
1146
|
+
add_colorbar=False,
|
|
1147
|
+
ax=axes[2, 2],
|
|
1148
|
+
)
|
|
1149
|
+
axes[2, 2].set_xlim(*dm_lim)
|
|
1150
|
+
axes[2, 2].set_ylim(lwc_lim)
|
|
1151
|
+
axes[2, 2].set_yticklabels([])
|
|
1152
|
+
|
|
1153
|
+
#### - Nt
|
|
1154
|
+
im_nt = ds_Dm_LWC_stats["Nt_median"].plot.pcolormesh(
|
|
1155
|
+
x="Dm",
|
|
1156
|
+
y="W",
|
|
1157
|
+
cmap=cmap_nt,
|
|
1158
|
+
norm=norm_nt,
|
|
1159
|
+
extend="both",
|
|
1160
|
+
yscale="log",
|
|
1161
|
+
add_colorbar=False,
|
|
1162
|
+
ax=axes[2, 3],
|
|
1163
|
+
)
|
|
1164
|
+
axes[2, 3].set_xlim(*dm_lim)
|
|
1165
|
+
axes[2, 3].set_ylim(lwc_lim)
|
|
1166
|
+
axes[2, 3].set_yticklabels([])
|
|
1167
|
+
|
|
1168
|
+
####-------------------------------------------------------------------.
|
|
1169
|
+
#### Dm vs R
|
|
1170
|
+
#### - Counts
|
|
1171
|
+
ds_Dm_R_stats["count"].plot.pcolormesh(
|
|
1172
|
+
x="Dm",
|
|
1173
|
+
y="R",
|
|
1174
|
+
cmap=cmap_counts,
|
|
1175
|
+
norm=norm_counts,
|
|
1176
|
+
extend="max",
|
|
1177
|
+
yscale="log",
|
|
1178
|
+
add_colorbar=False,
|
|
1179
|
+
ax=axes[3, 0],
|
|
1180
|
+
)
|
|
1181
|
+
axes[3, 0].set_ylabel(r"$R$ [mm h$^{-1}$]")
|
|
1182
|
+
axes[3, 0].set_xlim(*dm_lim)
|
|
1183
|
+
axes[3, 0].set_ylim(r_lim)
|
|
1184
|
+
|
|
1185
|
+
#### - LWC
|
|
1186
|
+
im_lwc = ds_Dm_R_stats["W_median"].plot.pcolormesh(
|
|
1187
|
+
x="Dm",
|
|
1188
|
+
y="R",
|
|
1189
|
+
cmap=cmap_lwc,
|
|
1190
|
+
norm=norm_lwc,
|
|
1191
|
+
extend="both",
|
|
1192
|
+
yscale="log",
|
|
1193
|
+
add_colorbar=False,
|
|
1194
|
+
ax=axes[3, 1],
|
|
1195
|
+
)
|
|
1196
|
+
axes[3, 1].set_xlim(*dm_lim)
|
|
1197
|
+
axes[3, 1].set_ylim(r_lim)
|
|
1198
|
+
axes[3, 1].set_yticklabels([])
|
|
1199
|
+
|
|
1200
|
+
#### - R
|
|
1201
|
+
# - Empty (diagonal where y-axis is R) - handled above in the loop
|
|
1202
|
+
#### - Nt
|
|
1203
|
+
ds_Dm_R_stats["Nt_median"].plot.pcolormesh(
|
|
1204
|
+
x="Dm",
|
|
1205
|
+
y="R",
|
|
1206
|
+
cmap=cmap_nt,
|
|
1207
|
+
norm=norm_nt,
|
|
1208
|
+
alpha=0, # fully transparent
|
|
1209
|
+
extend="both",
|
|
1210
|
+
yscale="log",
|
|
1211
|
+
add_colorbar=False,
|
|
1212
|
+
ax=axes[3, 2],
|
|
1213
|
+
)
|
|
1214
|
+
axes[3, 2].set_xlim(*dm_lim)
|
|
1215
|
+
axes[3, 2].set_ylim(r_lim)
|
|
1216
|
+
axes[3, 2].set_yticklabels([])
|
|
1217
|
+
|
|
1218
|
+
#### - Nt
|
|
1219
|
+
ds_Dm_R_stats["Nt_median"].plot.pcolormesh(
|
|
1220
|
+
x="Dm",
|
|
1221
|
+
y="R",
|
|
1222
|
+
cmap=cmap_nt,
|
|
1223
|
+
norm=norm_nt,
|
|
1224
|
+
extend="both",
|
|
1225
|
+
yscale="log",
|
|
1226
|
+
add_colorbar=False,
|
|
1227
|
+
ax=axes[3, 3],
|
|
1228
|
+
)
|
|
1229
|
+
axes[3, 3].set_xlim(*dm_lim)
|
|
1230
|
+
axes[3, 3].set_ylim(r_lim)
|
|
1231
|
+
axes[3, 3].set_yticklabels([])
|
|
1232
|
+
|
|
1233
|
+
####-------------------------------------------------------------------.
|
|
1234
|
+
#### Dm vs Nt
|
|
1235
|
+
if add_nt:
|
|
1236
|
+
#### - Counts
|
|
1237
|
+
ds_Dm_Nt_stats["count"].plot.pcolormesh(
|
|
1238
|
+
x="Dm",
|
|
1239
|
+
y="Nt",
|
|
1240
|
+
cmap=cmap_counts,
|
|
1241
|
+
norm=norm_counts,
|
|
1242
|
+
extend="max",
|
|
1243
|
+
yscale="log",
|
|
1244
|
+
add_colorbar=False,
|
|
1245
|
+
ax=axes[4, 0],
|
|
1246
|
+
)
|
|
1247
|
+
axes[4, 0].set_ylabel(r"$N_t$ [m$^{-3}$]")
|
|
1248
|
+
axes[4, 0].set_xlabel(r"$D_m$ [mm]")
|
|
1249
|
+
axes[4, 0].set_xlim(*dm_lim)
|
|
1250
|
+
axes[4, 0].set_ylim(nt_lim)
|
|
1251
|
+
|
|
1252
|
+
#### - LWC
|
|
1253
|
+
ds_Dm_Nt_stats["W_median"].plot.pcolormesh(
|
|
1254
|
+
x="Dm",
|
|
1255
|
+
y="Nt",
|
|
1256
|
+
cmap=cmap_lwc,
|
|
1257
|
+
norm=norm_lwc,
|
|
1258
|
+
extend="both",
|
|
1259
|
+
yscale="log",
|
|
1260
|
+
add_colorbar=False,
|
|
1261
|
+
ax=axes[4, 1],
|
|
1262
|
+
)
|
|
1263
|
+
axes[4, 1].set_xlabel(r"$D_m$ [mm]")
|
|
1264
|
+
axes[4, 1].set_xlim(*dm_lim)
|
|
1265
|
+
axes[4, 1].set_ylim(nt_lim)
|
|
1266
|
+
axes[4, 1].set_yticklabels([])
|
|
1267
|
+
|
|
1268
|
+
#### - R
|
|
1269
|
+
ds_Dm_Nt_stats["R_median"].plot.pcolormesh(
|
|
1270
|
+
x="Dm",
|
|
1271
|
+
y="Nt",
|
|
1272
|
+
cmap=cmap_r,
|
|
1273
|
+
norm=norm_r,
|
|
1274
|
+
extend="both",
|
|
1275
|
+
yscale="log",
|
|
1276
|
+
add_colorbar=False,
|
|
1277
|
+
ax=axes[4, 2],
|
|
1278
|
+
)
|
|
1279
|
+
axes[4, 2].set_xlabel(r"$D_m$ [mm]")
|
|
1280
|
+
axes[4, 2].set_xlim(*dm_lim)
|
|
1281
|
+
axes[4, 2].set_ylim(nt_lim)
|
|
1282
|
+
axes[4, 2].set_yticklabels([])
|
|
1283
|
+
|
|
1284
|
+
#### - Nt
|
|
1285
|
+
# - Empty plot - handled above in the loop
|
|
1286
|
+
ds_Dm_Nt_stats["R_median"].plot.pcolormesh(
|
|
1287
|
+
x="Dm",
|
|
1288
|
+
y="Nt",
|
|
1289
|
+
cmap=cmap_r,
|
|
1290
|
+
norm=norm_r,
|
|
1291
|
+
alpha=0, # fully transparent
|
|
1292
|
+
extend="both",
|
|
1293
|
+
yscale="log",
|
|
1294
|
+
add_colorbar=False,
|
|
1295
|
+
ax=axes[4, 2],
|
|
1296
|
+
)
|
|
1297
|
+
axes[4, 3].set_xlabel(r"$D_m$ [mm]")
|
|
1298
|
+
axes[4, 3].set_xlim(*dm_lim)
|
|
1299
|
+
axes[4, 3].set_ylim(nt_lim)
|
|
1300
|
+
axes[4, 3].set_yticklabels([])
|
|
1301
|
+
|
|
1302
|
+
####-------------------------------------------------------------------.
|
|
1303
|
+
#### Finalize figure
|
|
1304
|
+
# Remove x ticks and labels for all but bottom row
|
|
1305
|
+
for i in range(1, nrows):
|
|
1306
|
+
for j in range(4):
|
|
1307
|
+
if axes[i, j].get_visible():
|
|
1308
|
+
axes[i, j].set_xticklabels([])
|
|
1309
|
+
axes[i, j].set_xticks([])
|
|
1310
|
+
axes[i, j].set_xlabel("")
|
|
1311
|
+
|
|
1312
|
+
# Remove y ticks and labels for all but left row
|
|
1313
|
+
for i in range(1, nrows + 1):
|
|
1314
|
+
for j in range(1, 4):
|
|
1315
|
+
if axes[i, j].get_visible():
|
|
1316
|
+
axes[i, j].set_yticks([])
|
|
1317
|
+
axes[i, j].set_yticklabels([])
|
|
1318
|
+
axes[i, j].set_ylabel("")
|
|
1319
|
+
|
|
1320
|
+
# -------------------------------------------------.
|
|
1321
|
+
# Add colorbars
|
|
1322
|
+
# - Counts colorbar
|
|
1323
|
+
cbar1 = plt.colorbar(im_counts, cax=cbar_axes[0], orientation="horizontal", extend="both")
|
|
1324
|
+
cbar1.set_label("Counts", fontweight="bold")
|
|
1325
|
+
cbar1.ax.xaxis.set_label_position("top")
|
|
1326
|
+
cbar1.ax.set_aspect(0.25)
|
|
1327
|
+
# - LWC colorbar
|
|
1328
|
+
cbar2 = plt.colorbar(im_lwc, cax=cbar_axes[1], orientation="horizontal", extend="both")
|
|
1329
|
+
cbar2.set_label("Median LWC [g/m³]", fontweight="bold")
|
|
1330
|
+
cbar2.ax.xaxis.set_label_position("top")
|
|
1331
|
+
cbar2.ax.set_aspect(0.25)
|
|
1332
|
+
# - R colorbar
|
|
1333
|
+
cbar3 = plt.colorbar(im_r, cax=cbar_axes[2], orientation="horizontal", extend="both")
|
|
1334
|
+
cbar3.set_label("Median R [mm/h]", fontweight="bold")
|
|
1335
|
+
cbar3.ax.xaxis.set_label_position("top")
|
|
1336
|
+
cbar3.ax.set_aspect(0.3)
|
|
1337
|
+
# - Nt colorbar
|
|
1338
|
+
cbar4 = plt.colorbar(im_nt, cax=cbar_axes[3], orientation="horizontal", extend="both")
|
|
1339
|
+
cbar4.set_label("Median $N_t$ [m$^{-3}$]", fontweight="bold")
|
|
1340
|
+
cbar4.ax.xaxis.set_label_position("top")
|
|
1341
|
+
cbar4.ax.set_aspect(0.3)
|
|
1342
|
+
|
|
1343
|
+
# -------------------------------------------------.
|
|
1344
|
+
# Return figure
|
|
1345
|
+
return fig
|
|
1346
|
+
|
|
1347
|
+
|
|
1348
|
+
def plot_dsd_params_density(df, log_dm=False, lwc=True, log_normalize=False, figsize=(10, 10), dpi=300):
|
|
1349
|
+
"""Generate a figure with various DSD relationships.
|
|
1350
|
+
|
|
1351
|
+
All histograms are computed first, then normalized, and finally plotted together.
|
|
1352
|
+
|
|
1353
|
+
Parameters
|
|
1354
|
+
----------
|
|
1355
|
+
df : pandas.DataFrame
|
|
1356
|
+
DataFrame containing DSD parameters (Dm, Nt, Nw, LWC/W, R, sigma_m, M2, M3, M4, M6)
|
|
1357
|
+
log_dm : bool, optional
|
|
1358
|
+
If True, use linear scale for Dm axes. If False, use log scale. Default is True.
|
|
1359
|
+
lwc : bool, optional
|
|
1360
|
+
If True, use Liquid Water Content (W). If False, use Rain Rate (R). Default is True.
|
|
1361
|
+
figsize : tuple, optional
|
|
1362
|
+
Figure size (width, height) in inches. Default is (18, 18).
|
|
1363
|
+
|
|
1364
|
+
Returns
|
|
1365
|
+
-------
|
|
1366
|
+
fig : matplotlib.figure.Figure
|
|
1367
|
+
The figure object containing all subplots
|
|
1368
|
+
axes : numpy.ndarray
|
|
1369
|
+
Array of all subplot axes
|
|
1370
|
+
"""
|
|
1371
|
+
# TODO: option to use D50 instead of Dm
|
|
1372
|
+
|
|
1373
|
+
# Common parameters
|
|
1374
|
+
cmap = plt.get_cmap("Spectral_r").copy()
|
|
1375
|
+
norm = Normalize(0, 1) # Normalized data goes from 0 to 1
|
|
1376
|
+
|
|
1377
|
+
# Define the water variable based on lwc flag
|
|
1378
|
+
df["LWC"] = df["W"]
|
|
1379
|
+
water_var = "LWC" if lwc else "R"
|
|
1380
|
+
water_label = "LWC [g/m³]" if lwc else "R [mm/h]"
|
|
1381
|
+
|
|
1382
|
+
log_step = 0.05
|
|
1383
|
+
linear_step = 0.1
|
|
1384
|
+
|
|
1385
|
+
# Dm range and scale settings
|
|
1386
|
+
if not log_dm:
|
|
1387
|
+
dm_bins = np.arange(0, 8, linear_step)
|
|
1388
|
+
dm_scale = None
|
|
1389
|
+
dm_lim = (0, 6)
|
|
1390
|
+
dm_ticklabels = [0, 2, 4, 6]
|
|
1391
|
+
else:
|
|
1392
|
+
dm_bins = log_arange(0.1, 10, log_step=log_step, base=10)
|
|
1393
|
+
dm_scale = "log"
|
|
1394
|
+
dm_lim = (0.3, 6)
|
|
1395
|
+
dm_ticklabels = [0.5, 1, 2, 5]
|
|
1396
|
+
|
|
1397
|
+
# Nt and Nw range
|
|
1398
|
+
nt_bins = log_arange(1, 100_000, log_step=log_step, base=10)
|
|
1399
|
+
nw_bins = log_arange(1, 1_000_000, log_step=log_step, base=10)
|
|
1400
|
+
nw_lim = (10, 1_000_000)
|
|
1401
|
+
nt_lim = (1, 100_000)
|
|
1402
|
+
|
|
1403
|
+
# Water range
|
|
1404
|
+
if lwc:
|
|
1405
|
+
water_bins = log_arange(0.001, 10, log_step=log_step, base=10)
|
|
1406
|
+
water_lim = (0.005, 10)
|
|
1407
|
+
else:
|
|
1408
|
+
water_bins = log_arange(0.1, 500, log_step=log_step, base=10)
|
|
1409
|
+
water_lim = (0.1, 500)
|
|
1410
|
+
|
|
1411
|
+
# Define sigma_m bins
|
|
1412
|
+
sigma_bins = np.arange(0, 4, linear_step / 2)
|
|
1413
|
+
sigma_lim = (0, 3)
|
|
1414
|
+
|
|
1415
|
+
# Compute all histograms first
|
|
1416
|
+
# 1. Dm vs Nt
|
|
1417
|
+
ds_stats_dm_nt = compute_2d_histogram(
|
|
1418
|
+
df,
|
|
1419
|
+
x="Dm",
|
|
1420
|
+
y="Nt",
|
|
1421
|
+
x_bins=dm_bins,
|
|
1422
|
+
y_bins=nt_bins,
|
|
1423
|
+
)
|
|
1424
|
+
|
|
1425
|
+
# 2. Dm vs Nw
|
|
1426
|
+
ds_stats_dm_nw = compute_2d_histogram(
|
|
1427
|
+
df,
|
|
1428
|
+
x="Dm",
|
|
1429
|
+
y="Nw",
|
|
1430
|
+
x_bins=dm_bins,
|
|
1431
|
+
y_bins=nw_bins,
|
|
1432
|
+
)
|
|
1433
|
+
|
|
1434
|
+
# 3. Dm vs LWC/R
|
|
1435
|
+
ds_stats_dm_w = compute_2d_histogram(
|
|
1436
|
+
df,
|
|
1437
|
+
x="Dm",
|
|
1438
|
+
y=water_var,
|
|
1439
|
+
x_bins=dm_bins,
|
|
1440
|
+
y_bins=water_bins,
|
|
1441
|
+
)
|
|
1442
|
+
|
|
1443
|
+
# 4. LWC/R vs Nt
|
|
1444
|
+
ds_stats_w_nt = compute_2d_histogram(
|
|
1445
|
+
df,
|
|
1446
|
+
x=water_var,
|
|
1447
|
+
y="Nt",
|
|
1448
|
+
x_bins=water_bins,
|
|
1449
|
+
y_bins=nt_bins,
|
|
1450
|
+
)
|
|
1451
|
+
|
|
1452
|
+
# 5. LWC/R vs Nw
|
|
1453
|
+
ds_stats_w_nw = compute_2d_histogram(
|
|
1454
|
+
df,
|
|
1455
|
+
x=water_var,
|
|
1456
|
+
y="Nw",
|
|
1457
|
+
x_bins=water_bins,
|
|
1458
|
+
y_bins=nw_bins,
|
|
1459
|
+
)
|
|
1460
|
+
|
|
1461
|
+
# 6. LWC/R vs sigma_m
|
|
1462
|
+
ds_stats_w_sigma = compute_2d_histogram(
|
|
1463
|
+
df,
|
|
1464
|
+
x=water_var,
|
|
1465
|
+
y="sigma_m",
|
|
1466
|
+
x_bins=water_bins,
|
|
1467
|
+
y_bins=sigma_bins,
|
|
1468
|
+
)
|
|
1469
|
+
|
|
1470
|
+
# 7. M2 vs M4
|
|
1471
|
+
ds_stats_m2_m4 = compute_2d_histogram(
|
|
1472
|
+
df,
|
|
1473
|
+
x="M2",
|
|
1474
|
+
y="M4",
|
|
1475
|
+
x_bins=log_arange(1, 10_000, log_step=log_step, base=10),
|
|
1476
|
+
y_bins=log_arange(1, 40_000, log_step=log_step, base=10),
|
|
1477
|
+
)
|
|
1478
|
+
|
|
1479
|
+
# 8. M3 vs M6
|
|
1480
|
+
ds_stats_m3_m6 = compute_2d_histogram(
|
|
1481
|
+
df,
|
|
1482
|
+
x="M3",
|
|
1483
|
+
y="M6",
|
|
1484
|
+
x_bins=log_arange(1, 10_000, log_step=log_step, base=10),
|
|
1485
|
+
y_bins=log_arange(0.1, 1000_000, log_step=log_step, base=10),
|
|
1486
|
+
)
|
|
1487
|
+
|
|
1488
|
+
# 9. M2 vs M6
|
|
1489
|
+
ds_stats_m2_m6 = compute_2d_histogram(
|
|
1490
|
+
df,
|
|
1491
|
+
x="M2",
|
|
1492
|
+
y="M6",
|
|
1493
|
+
x_bins=log_arange(1, 10_000, log_step=log_step, base=10),
|
|
1494
|
+
y_bins=log_arange(0.1, 1000_000, log_step=log_step, base=10),
|
|
1495
|
+
)
|
|
1496
|
+
|
|
1497
|
+
# Define normalization
|
|
1498
|
+
def max_normalize(ds):
|
|
1499
|
+
return ds["count"].where(ds["count"] > 0) / ds["count"].max().item()
|
|
1500
|
+
|
|
1501
|
+
def log_max_normalize(ds):
|
|
1502
|
+
counts = ds["count"].where(ds["count"] > 0)
|
|
1503
|
+
log_counts = np.log10(counts)
|
|
1504
|
+
max_log = float(log_counts.max().item())
|
|
1505
|
+
return log_counts / max_log
|
|
1506
|
+
|
|
1507
|
+
normalizer = log_max_normalize if log_normalize else max_normalize
|
|
1508
|
+
|
|
1509
|
+
# Normalize all histograms
|
|
1510
|
+
ds_stats_dm_nt["normalized"] = normalizer(ds_stats_dm_nt)
|
|
1511
|
+
ds_stats_dm_nw["normalized"] = normalizer(ds_stats_dm_nw)
|
|
1512
|
+
ds_stats_dm_w["normalized"] = normalizer(ds_stats_dm_w)
|
|
1513
|
+
ds_stats_w_nt["normalized"] = normalizer(ds_stats_w_nt)
|
|
1514
|
+
ds_stats_w_nw["normalized"] = normalizer(ds_stats_w_nw)
|
|
1515
|
+
ds_stats_w_sigma["normalized"] = normalizer(ds_stats_w_sigma)
|
|
1516
|
+
ds_stats_m2_m4["normalized"] = normalizer(ds_stats_m2_m4)
|
|
1517
|
+
ds_stats_m3_m6["normalized"] = normalizer(ds_stats_m3_m6)
|
|
1518
|
+
ds_stats_m2_m6["normalized"] = normalizer(ds_stats_m2_m6)
|
|
1519
|
+
|
|
1520
|
+
# Set up figure and axes
|
|
1521
|
+
fig, axes = plt.subplots(3, 3, figsize=figsize, dpi=dpi)
|
|
1522
|
+
fig.subplots_adjust(hspace=0.05, wspace=0.35)
|
|
1523
|
+
|
|
1524
|
+
# COLUMN 1: All plots with Dm as x-axis
|
|
1525
|
+
# 1. Dm vs Nt (0,0)
|
|
1526
|
+
_ = ds_stats_dm_nt["normalized"].plot.pcolormesh(
|
|
1527
|
+
x="Dm",
|
|
1528
|
+
y="Nt",
|
|
1529
|
+
cmap=cmap,
|
|
1530
|
+
norm=norm,
|
|
1531
|
+
extend="max",
|
|
1532
|
+
xscale=dm_scale,
|
|
1533
|
+
yscale="log",
|
|
1534
|
+
add_colorbar=False,
|
|
1535
|
+
ax=axes[0, 0],
|
|
1536
|
+
)
|
|
1537
|
+
axes[0, 0].set_xlabel("") # Hide x labels except for bottom row
|
|
1538
|
+
axes[0, 0].set_ylabel(r"$N_t$ [m$^{-3}$]")
|
|
1539
|
+
axes[0, 0].set_xlim(*dm_lim)
|
|
1540
|
+
axes[0, 0].set_ylim(*nt_lim)
|
|
1541
|
+
axes[0, 0].set_title(r"$D_m$ vs $N_t$")
|
|
1542
|
+
|
|
1543
|
+
# 2. Dm vs Nw (1,0)
|
|
1544
|
+
_ = ds_stats_dm_nw["normalized"].plot.pcolormesh(
|
|
1545
|
+
x="Dm",
|
|
1546
|
+
y="Nw",
|
|
1547
|
+
cmap=cmap,
|
|
1548
|
+
norm=norm,
|
|
1549
|
+
extend="max",
|
|
1550
|
+
xscale=dm_scale,
|
|
1551
|
+
yscale="log",
|
|
1552
|
+
add_colorbar=False,
|
|
1553
|
+
ax=axes[1, 0],
|
|
1554
|
+
)
|
|
1555
|
+
axes[1, 0].set_xlabel("") # Hide x labels except for bottom row
|
|
1556
|
+
axes[1, 0].set_ylabel(r"$N_w$ [mm$^{-1}$ m$^{-3}$]")
|
|
1557
|
+
axes[1, 0].set_xlim(*dm_lim)
|
|
1558
|
+
axes[1, 0].set_ylim(*nw_lim)
|
|
1559
|
+
axes[1, 0].set_title(r"$D_m$ vs $N_w$")
|
|
1560
|
+
|
|
1561
|
+
# 3. Dm vs LWC/R (2,0)
|
|
1562
|
+
_ = ds_stats_dm_w["normalized"].plot.pcolormesh(
|
|
1563
|
+
x="Dm",
|
|
1564
|
+
y=water_var,
|
|
1565
|
+
cmap=cmap,
|
|
1566
|
+
norm=norm,
|
|
1567
|
+
extend="max",
|
|
1568
|
+
xscale=dm_scale,
|
|
1569
|
+
yscale="log",
|
|
1570
|
+
add_colorbar=False,
|
|
1571
|
+
ax=axes[2, 0],
|
|
1572
|
+
)
|
|
1573
|
+
axes[2, 0].set_xlabel(r"$D_m$ [mm]")
|
|
1574
|
+
axes[2, 0].set_ylabel(water_label)
|
|
1575
|
+
axes[2, 0].set_xlim(*dm_lim)
|
|
1576
|
+
if lwc:
|
|
1577
|
+
axes[2, 0].set_ylim(*water_lim)
|
|
1578
|
+
axes[2, 0].set_yticks([0.01, 0.1, 0.5, 1, 5])
|
|
1579
|
+
axes[2, 0].set_yticklabels(["0.01", "0.1", "0.5", "1", "5"])
|
|
1580
|
+
else:
|
|
1581
|
+
axes[2, 0].set_ylim(*water_lim)
|
|
1582
|
+
axes[2, 0].set_title(f"$D_m$ vs {water_var}")
|
|
1583
|
+
|
|
1584
|
+
axes[2, 0].set_xticks(dm_ticklabels)
|
|
1585
|
+
axes[2, 0].set_xticklabels([str(v) for v in dm_ticklabels])
|
|
1586
|
+
|
|
1587
|
+
# COLUMN 2: All plots with LWC/R as x-axis
|
|
1588
|
+
# 4. LWC/R vs Nt (0,1)
|
|
1589
|
+
_ = ds_stats_w_nt["normalized"].plot.pcolormesh(
|
|
1590
|
+
x=water_var,
|
|
1591
|
+
y="Nt",
|
|
1592
|
+
cmap=cmap,
|
|
1593
|
+
norm=norm,
|
|
1594
|
+
extend="max",
|
|
1595
|
+
xscale="log",
|
|
1596
|
+
yscale="log",
|
|
1597
|
+
add_colorbar=False,
|
|
1598
|
+
ax=axes[0, 1],
|
|
1599
|
+
)
|
|
1600
|
+
axes[0, 1].set_xlabel("") # Hide x labels except for bottom row
|
|
1601
|
+
axes[0, 1].set_ylabel(r"$N_t$ [m$^{-3}$]")
|
|
1602
|
+
if lwc:
|
|
1603
|
+
axes[0, 1].set_xlim(*water_lim)
|
|
1604
|
+
axes[0, 1].set_xticks([0.01, 0.1, 1, 10])
|
|
1605
|
+
axes[0, 1].set_xticklabels(["0.01", "0.1", "1", "10"])
|
|
1606
|
+
else:
|
|
1607
|
+
axes[0, 1].set_xlim(*water_lim)
|
|
1608
|
+
axes[0, 1].set_ylim(*nt_lim)
|
|
1609
|
+
axes[0, 1].set_title(f"{water_var} vs $N_t$")
|
|
1610
|
+
|
|
1611
|
+
# 5. LWC/R vs Nw (1,1)
|
|
1612
|
+
_ = ds_stats_w_nw["normalized"].plot.pcolormesh(
|
|
1613
|
+
x=water_var,
|
|
1614
|
+
y="Nw",
|
|
1615
|
+
cmap=cmap,
|
|
1616
|
+
norm=norm,
|
|
1617
|
+
extend="max",
|
|
1618
|
+
xscale="log",
|
|
1619
|
+
yscale="log",
|
|
1620
|
+
add_colorbar=False,
|
|
1621
|
+
ax=axes[1, 1],
|
|
1622
|
+
)
|
|
1623
|
+
axes[1, 1].set_xlabel("") # Hide x labels except for bottom row
|
|
1624
|
+
axes[1, 1].set_ylabel(r"$N_w$ [mm$^{-1}$ m$^{-3}$]")
|
|
1625
|
+
if lwc:
|
|
1626
|
+
axes[1, 1].set_xlim(*water_lim)
|
|
1627
|
+
axes[1, 1].set_xticks([0.01, 0.1, 1, 10])
|
|
1628
|
+
axes[1, 1].set_xticklabels(["0.01", "0.1", "1", "10"])
|
|
1629
|
+
else:
|
|
1630
|
+
axes[1, 1].set_xlim(*water_lim)
|
|
1631
|
+
axes[1, 1].set_ylim(*nw_lim)
|
|
1632
|
+
axes[1, 1].set_title(f"{water_var} vs $N_w$")
|
|
1633
|
+
|
|
1634
|
+
# 6. LWC/R vs sigma_m (2,1)
|
|
1635
|
+
_ = ds_stats_w_sigma["normalized"].plot.pcolormesh(
|
|
1636
|
+
x=water_var,
|
|
1637
|
+
y="sigma_m",
|
|
1638
|
+
cmap=cmap,
|
|
1639
|
+
norm=norm,
|
|
1640
|
+
extend="max",
|
|
1641
|
+
xscale="log",
|
|
1642
|
+
add_colorbar=False,
|
|
1643
|
+
ax=axes[2, 1],
|
|
1644
|
+
)
|
|
1645
|
+
axes[2, 1].set_xlabel(water_label)
|
|
1646
|
+
axes[2, 1].set_ylabel(r"$\sigma_m$ [mm]")
|
|
1647
|
+
if lwc:
|
|
1648
|
+
axes[2, 1].set_xlim(*water_lim)
|
|
1649
|
+
axes[2, 1].set_xticks([0.01, 0.1, 1, 10])
|
|
1650
|
+
axes[2, 1].set_xticklabels(["0.01", "0.1", "1", "10"])
|
|
1651
|
+
else:
|
|
1652
|
+
axes[2, 1].set_xlim(*water_lim)
|
|
1653
|
+
axes[2, 1].set_ylim(*sigma_lim)
|
|
1654
|
+
|
|
1655
|
+
axes[2, 1].set_title(rf"{water_var} vs $\sigma_m$")
|
|
1656
|
+
|
|
1657
|
+
# COLUMN 3: Moment relationships
|
|
1658
|
+
# 7. M2 vs M4 (0,2)
|
|
1659
|
+
_ = ds_stats_m2_m4["normalized"].plot.pcolormesh(
|
|
1660
|
+
x="M2",
|
|
1661
|
+
y="M4",
|
|
1662
|
+
cmap=cmap,
|
|
1663
|
+
norm=norm,
|
|
1664
|
+
extend="max",
|
|
1665
|
+
xscale="log",
|
|
1666
|
+
yscale="log",
|
|
1667
|
+
add_colorbar=False,
|
|
1668
|
+
ax=axes[0, 2],
|
|
1669
|
+
)
|
|
1670
|
+
axes[0, 2].set_xlabel("") # Hide x labels except for bottom row
|
|
1671
|
+
axes[0, 2].set_ylabel(r"M4 [m$^{-3}$ mm$^{4}$]")
|
|
1672
|
+
axes[0, 2].set_xlim(1, 10_000)
|
|
1673
|
+
axes[0, 2].set_ylim(1, 40_000)
|
|
1674
|
+
axes[0, 2].set_title(r"M2 vs M4")
|
|
1675
|
+
|
|
1676
|
+
# 8. M3 vs M6 (1,2)
|
|
1677
|
+
_ = ds_stats_m3_m6["normalized"].plot.pcolormesh(
|
|
1678
|
+
x="M3",
|
|
1679
|
+
y="M6",
|
|
1680
|
+
cmap=cmap,
|
|
1681
|
+
norm=norm,
|
|
1682
|
+
extend="max",
|
|
1683
|
+
xscale="log",
|
|
1684
|
+
yscale="log",
|
|
1685
|
+
add_colorbar=False,
|
|
1686
|
+
ax=axes[1, 2],
|
|
1687
|
+
)
|
|
1688
|
+
axes[1, 2].set_xlabel("") # Hide x labels except for bottom row
|
|
1689
|
+
axes[1, 2].set_ylabel(r"M6 [m$^{-3}$ mm$^{6}$]")
|
|
1690
|
+
axes[1, 2].set_xlim(1, 10_000)
|
|
1691
|
+
axes[1, 2].set_ylim(0.1, 1000_000)
|
|
1692
|
+
axes[1, 2].set_title(r"M3 vs M6")
|
|
1693
|
+
|
|
1694
|
+
# 9. M2 vs M6 (2,2)
|
|
1695
|
+
_ = ds_stats_m2_m6["normalized"].plot.pcolormesh(
|
|
1696
|
+
x="M2",
|
|
1697
|
+
y="M6",
|
|
1698
|
+
cmap=cmap,
|
|
1699
|
+
norm=norm,
|
|
1700
|
+
extend="max",
|
|
1701
|
+
xscale="log",
|
|
1702
|
+
yscale="log",
|
|
1703
|
+
add_colorbar=False,
|
|
1704
|
+
ax=axes[2, 2],
|
|
1705
|
+
)
|
|
1706
|
+
axes[2, 2].set_xlabel(r"M* [m$^{-3}$ mm$^{*}$]")
|
|
1707
|
+
axes[2, 2].set_ylabel(r"M6 [m$^{-3}$ mm$^{6}$]")
|
|
1708
|
+
axes[2, 2].set_xlim(1, 10_000)
|
|
1709
|
+
axes[2, 2].set_ylim(0.1, 1000_000)
|
|
1710
|
+
axes[2, 2].set_title(r"M2 vs M6")
|
|
1711
|
+
|
|
1712
|
+
# Remove x-axis ticks and ticklabels for all but bottom row
|
|
1713
|
+
for i in range(2):
|
|
1714
|
+
for j in range(3):
|
|
1715
|
+
axes[i, j].set_xticklabels([])
|
|
1716
|
+
axes[i, j].tick_params(axis="x", which="both", bottom=False)
|
|
1717
|
+
|
|
1718
|
+
# Add subplot titles as text in top left corner of each plot
|
|
1719
|
+
title_bbox_dict = {
|
|
1720
|
+
"facecolor": "white",
|
|
1721
|
+
"alpha": 0.7,
|
|
1722
|
+
"edgecolor": "none",
|
|
1723
|
+
"pad": 1,
|
|
1724
|
+
}
|
|
1725
|
+
for ax in axes.flatten():
|
|
1726
|
+
# Add text in top left corner with some padding
|
|
1727
|
+
ax.text(
|
|
1728
|
+
0.03,
|
|
1729
|
+
0.95,
|
|
1730
|
+
ax.get_title(),
|
|
1731
|
+
transform=ax.transAxes,
|
|
1732
|
+
fontsize=11,
|
|
1733
|
+
# fontweight='bold',
|
|
1734
|
+
ha="left",
|
|
1735
|
+
va="top",
|
|
1736
|
+
bbox=title_bbox_dict,
|
|
1737
|
+
)
|
|
1738
|
+
ax.set_title("")
|
|
1739
|
+
|
|
1740
|
+
return fig
|
|
1741
|
+
|
|
1742
|
+
|
|
1743
|
+
def plot_dmax_relationships(df, diameter_bin_edges, dmax="Dmax", diameter_max=10, norm_vmax=None, dpi=300):
|
|
1744
|
+
"""
|
|
1745
|
+
Plot 2x2 subplots showing relationships between Dmax and precipitation parameters.
|
|
1746
|
+
|
|
1747
|
+
Parameters
|
|
1748
|
+
----------
|
|
1749
|
+
df : DataFrame
|
|
1750
|
+
Input dataframe containing the precipitation data
|
|
1751
|
+
dmax : str, default "Dmax"
|
|
1752
|
+
Column name for maximum diameter
|
|
1753
|
+
vmax : float, default 10
|
|
1754
|
+
Maximum value for Dmax axis limits
|
|
1755
|
+
dpi : int, default 300
|
|
1756
|
+
Resolution for the figure
|
|
1757
|
+
"""
|
|
1758
|
+
# Compute 2D histograms
|
|
1759
|
+
# - Dmax-R
|
|
1760
|
+
ds_stats_dmax_r = compute_2d_histogram(
|
|
1761
|
+
df,
|
|
1762
|
+
x=dmax,
|
|
1763
|
+
y="R",
|
|
1764
|
+
x_bins=diameter_bin_edges,
|
|
1765
|
+
y_bins=log_arange(0.1, 500, log_step=0.05, base=10),
|
|
1766
|
+
)
|
|
1767
|
+
# - Dmax-Nw
|
|
1768
|
+
ds_stats_dmax_nw = compute_2d_histogram(
|
|
1769
|
+
df,
|
|
1770
|
+
x=dmax,
|
|
1771
|
+
y="Nw",
|
|
1772
|
+
x_bins=diameter_bin_edges,
|
|
1773
|
+
y_bins=log_arange(10, 1_000_000, log_step=0.05, base=10),
|
|
1774
|
+
)
|
|
1775
|
+
# - Dmax-Nt
|
|
1776
|
+
ds_stats_dmax_nt = compute_2d_histogram(
|
|
1777
|
+
df,
|
|
1778
|
+
x=dmax,
|
|
1779
|
+
y="Nt",
|
|
1780
|
+
x_bins=diameter_bin_edges,
|
|
1781
|
+
y_bins=log_arange(1, 100_000, log_step=0.05, base=10),
|
|
1782
|
+
)
|
|
1783
|
+
# - Dmax-Dm
|
|
1784
|
+
ds_stats_dmax_dm = compute_2d_histogram(
|
|
1785
|
+
df,
|
|
1786
|
+
x=dmax,
|
|
1787
|
+
y="Dm",
|
|
1788
|
+
variables=["R", "Nw", "sigma_m"],
|
|
1789
|
+
x_bins=diameter_bin_edges,
|
|
1790
|
+
y_bins=np.arange(0, 8, 0.05),
|
|
1791
|
+
)
|
|
1792
|
+
|
|
1793
|
+
# Define vmax for counts
|
|
1794
|
+
if norm_vmax:
|
|
1795
|
+
norm_vmax = define_lognorm_max_value(ds_stats_dmax_r["count"].max().item())
|
|
1796
|
+
|
|
1797
|
+
# Define plotting parameters
|
|
1798
|
+
cmap = plt.get_cmap("Spectral_r").copy()
|
|
1799
|
+
cmap.set_under(alpha=0)
|
|
1800
|
+
norm = LogNorm(1, norm_vmax)
|
|
1801
|
+
|
|
1802
|
+
# Create figure with 2x2 subplots
|
|
1803
|
+
figsize = (8, 6)
|
|
1804
|
+
fig = plt.figure(figsize=figsize, dpi=dpi)
|
|
1805
|
+
|
|
1806
|
+
# Create main gridspec with larger space between plots and colorbar
|
|
1807
|
+
# - Horizontal colorbar
|
|
1808
|
+
main_gs = fig.add_gridspec(2, 1, height_ratios=[1, 0.20], hspace=0.15)
|
|
1809
|
+
|
|
1810
|
+
# - Vertical colorbar
|
|
1811
|
+
# main_gs = fig.add_gridspec(1, 2, width_ratios=[1, 0.20], wspace=0.15)
|
|
1812
|
+
|
|
1813
|
+
# Create nested gridspec for the 2x2 subplots with smaller internal spacing
|
|
1814
|
+
subplots_gs = main_gs[0].subgridspec(2, 2, hspace=0.05, wspace=0.05)
|
|
1815
|
+
|
|
1816
|
+
# Create the 2x2 subplot grid
|
|
1817
|
+
axes = np.array(
|
|
1818
|
+
[
|
|
1819
|
+
[fig.add_subplot(subplots_gs[0, 0]), fig.add_subplot(subplots_gs[0, 1])],
|
|
1820
|
+
[fig.add_subplot(subplots_gs[1, 0]), fig.add_subplot(subplots_gs[1, 1])],
|
|
1821
|
+
],
|
|
1822
|
+
)
|
|
1823
|
+
|
|
1824
|
+
# - Dmax vs R (top-left)
|
|
1825
|
+
ax1 = axes[0, 0]
|
|
1826
|
+
p1 = ds_stats_dmax_r["count"].plot.pcolormesh(
|
|
1827
|
+
x=dmax,
|
|
1828
|
+
y="R",
|
|
1829
|
+
cmap=cmap,
|
|
1830
|
+
norm=norm,
|
|
1831
|
+
extend="max",
|
|
1832
|
+
yscale="log",
|
|
1833
|
+
add_colorbar=False,
|
|
1834
|
+
ax=ax1,
|
|
1835
|
+
)
|
|
1836
|
+
ax1.set_xlabel(r"$D_{max}$ [mm]")
|
|
1837
|
+
ax1.set_ylabel(r"$R$ [mm h$^{-1}$]")
|
|
1838
|
+
ax1.set_xlim(0.2, diameter_max)
|
|
1839
|
+
ax1.set_ylim(0.1, 500)
|
|
1840
|
+
|
|
1841
|
+
# - Dmax vs Nw (top-right)
|
|
1842
|
+
ax2 = axes[0, 1]
|
|
1843
|
+
_ = ds_stats_dmax_nw["count"].plot.pcolormesh(
|
|
1844
|
+
x=dmax,
|
|
1845
|
+
y="Nw",
|
|
1846
|
+
cmap=cmap,
|
|
1847
|
+
norm=norm,
|
|
1848
|
+
extend="max",
|
|
1849
|
+
yscale="log",
|
|
1850
|
+
add_colorbar=False,
|
|
1851
|
+
ax=ax2,
|
|
1852
|
+
)
|
|
1853
|
+
ax2.set_xlabel(r"$D_{max}$ [mm]")
|
|
1854
|
+
ax2.set_ylabel(r"$N_w$ [mm$^{-1}$ m$^{-3}$]")
|
|
1855
|
+
ax2.set_xlim(0.2, diameter_max)
|
|
1856
|
+
ax2.set_ylim(10, 1_000_000)
|
|
1857
|
+
|
|
1858
|
+
# - Dmax vs Nt (bottom-left)
|
|
1859
|
+
ax3 = axes[1, 0]
|
|
1860
|
+
_ = ds_stats_dmax_nt["count"].plot.pcolormesh(
|
|
1861
|
+
x=dmax,
|
|
1862
|
+
y="Nt",
|
|
1863
|
+
cmap=cmap,
|
|
1864
|
+
norm=norm,
|
|
1865
|
+
extend="max",
|
|
1866
|
+
yscale="log",
|
|
1867
|
+
add_colorbar=False,
|
|
1868
|
+
ax=ax3,
|
|
1869
|
+
)
|
|
1870
|
+
ax3.set_xlabel(r"$D_{max}$ [mm]")
|
|
1871
|
+
ax3.set_ylabel(r"$N_t$ [m$^{-3}$]")
|
|
1872
|
+
ax3.set_xlim(0.2, diameter_max)
|
|
1873
|
+
ax3.set_ylim(1, 100_000)
|
|
1874
|
+
|
|
1875
|
+
# - Dmax vs Dm (bottom-right)
|
|
1876
|
+
ax4 = axes[1, 1]
|
|
1877
|
+
_ = ds_stats_dmax_dm["count"].plot.pcolormesh(
|
|
1878
|
+
x=dmax,
|
|
1879
|
+
y="Dm",
|
|
1880
|
+
cmap=cmap,
|
|
1881
|
+
norm=norm,
|
|
1882
|
+
extend="max",
|
|
1883
|
+
add_colorbar=False,
|
|
1884
|
+
ax=ax4,
|
|
1885
|
+
)
|
|
1886
|
+
ax4.set_xlabel(r"$D_{max}$ [mm]")
|
|
1887
|
+
ax4.set_ylabel(r"$D_m$ [mm]")
|
|
1888
|
+
ax4.set_xlim(0.2, diameter_max)
|
|
1889
|
+
ax4.set_ylim(0, 6)
|
|
1890
|
+
|
|
1891
|
+
# Remove xaxis labels and ticklables labels on first row
|
|
1892
|
+
for ax in axes[0, :]: # First row (both columns)
|
|
1893
|
+
ax.set_xlabel("")
|
|
1894
|
+
ax.set_xticks([])
|
|
1895
|
+
ax.set_xticklabels([])
|
|
1896
|
+
ax.tick_params(axis="x", which="both", bottom=True, top=False, labelbottom=False)
|
|
1897
|
+
|
|
1898
|
+
# Move y-axis of second column to the right
|
|
1899
|
+
for ax in axes[:, 1]: # Second column (both rows)
|
|
1900
|
+
ax.yaxis.tick_right()
|
|
1901
|
+
ax.yaxis.set_label_position("right")
|
|
1902
|
+
|
|
1903
|
+
# Add titles as legends in upper corners
|
|
1904
|
+
title_bbox_dict = {
|
|
1905
|
+
"facecolor": "white",
|
|
1906
|
+
"alpha": 0.7,
|
|
1907
|
+
"edgecolor": "none",
|
|
1908
|
+
"pad": 1,
|
|
1909
|
+
}
|
|
1910
|
+
axes[0, 0].text(
|
|
1911
|
+
0.05,
|
|
1912
|
+
0.95,
|
|
1913
|
+
r"$D_{max}$ vs $R$",
|
|
1914
|
+
transform=axes[0, 0].transAxes,
|
|
1915
|
+
fontsize=12,
|
|
1916
|
+
verticalalignment="top",
|
|
1917
|
+
bbox=title_bbox_dict,
|
|
1918
|
+
)
|
|
1919
|
+
|
|
1920
|
+
axes[0, 1].text(
|
|
1921
|
+
0.05,
|
|
1922
|
+
0.95,
|
|
1923
|
+
r"$D_{max}$ vs $N_w$",
|
|
1924
|
+
transform=axes[0, 1].transAxes,
|
|
1925
|
+
fontsize=12,
|
|
1926
|
+
verticalalignment="top",
|
|
1927
|
+
bbox=title_bbox_dict,
|
|
1928
|
+
)
|
|
1929
|
+
|
|
1930
|
+
axes[1, 0].text(
|
|
1931
|
+
0.05,
|
|
1932
|
+
0.95,
|
|
1933
|
+
r"$D_{max}$ vs $N_t$",
|
|
1934
|
+
transform=axes[1, 0].transAxes,
|
|
1935
|
+
fontsize=12,
|
|
1936
|
+
verticalalignment="top",
|
|
1937
|
+
bbox=title_bbox_dict,
|
|
1938
|
+
)
|
|
1939
|
+
|
|
1940
|
+
axes[1, 1].text(
|
|
1941
|
+
0.05,
|
|
1942
|
+
0.95,
|
|
1943
|
+
r"$D_{max}$ vs $D_m$",
|
|
1944
|
+
transform=axes[1, 1].transAxes,
|
|
1945
|
+
fontsize=12,
|
|
1946
|
+
verticalalignment="top",
|
|
1947
|
+
bbox=title_bbox_dict,
|
|
1948
|
+
)
|
|
1949
|
+
|
|
1950
|
+
# Add colorbar
|
|
1951
|
+
cax = fig.add_subplot(main_gs[1])
|
|
1952
|
+
# - Horizontal colorbar
|
|
1953
|
+
cbar = fig.colorbar(p1, cax=cax, extend="max", orientation="horizontal")
|
|
1954
|
+
cbar.set_label("Counts", labelpad=10)
|
|
1955
|
+
cbar.ax.set_aspect(0.1)
|
|
1956
|
+
cbar.ax.xaxis.set_label_position("top")
|
|
1957
|
+
|
|
1958
|
+
# - Vertical colorbar
|
|
1959
|
+
# cbar = fig.colorbar(p2, cax=cax, extend="max")
|
|
1960
|
+
# cbar.set_label('Count', rotation=270, labelpad=10)
|
|
1961
|
+
# cbar.ax.set_aspect(10)
|
|
1962
|
+
return fig
|
|
1963
|
+
|
|
1964
|
+
|
|
1965
|
+
####-------------------------------------------------------------------
|
|
1966
|
+
#### Radar plots
|
|
1967
|
+
|
|
1968
|
+
|
|
1969
|
+
def _define_coeff_string(a):
|
|
1970
|
+
# - Format a coefficient as m * 10^{e}
|
|
1971
|
+
m_str, e_str = f"{a:.2e}".split("e")
|
|
1972
|
+
m, e = float(m_str), int(e_str)
|
|
1973
|
+
# Build coefficient string
|
|
1974
|
+
a_str = f"{a:.2f}" if e >= -1 else f"{m:.2f} \\times 10^{{{e}}}"
|
|
1975
|
+
return a_str
|
|
1976
|
+
|
|
1977
|
+
|
|
1978
|
+
def get_symbol_str(symbol, pol=""):
|
|
1979
|
+
"""Generate symbol string with optional polarization subscript.
|
|
1980
|
+
|
|
1981
|
+
Parameters
|
|
1982
|
+
----------
|
|
1983
|
+
symbol : str
|
|
1984
|
+
The base symbol (e.g., 'A', 'Z', 'z')
|
|
1985
|
+
pol : str, optional
|
|
1986
|
+
Polarization identifier (e.g., 'H', 'V')
|
|
1987
|
+
|
|
1988
|
+
Returns
|
|
1989
|
+
-------
|
|
1990
|
+
str
|
|
1991
|
+
LaTeX formatted symbol string
|
|
1992
|
+
"""
|
|
1993
|
+
if pol:
|
|
1994
|
+
return rf"{symbol}_{{\mathrm{{{pol}}}}}"
|
|
1995
|
+
return symbol
|
|
1996
|
+
|
|
1997
|
+
|
|
1998
|
+
def plot_A_R(
|
|
1999
|
+
df,
|
|
2000
|
+
a,
|
|
2001
|
+
r,
|
|
2002
|
+
cmap=None,
|
|
2003
|
+
norm=None,
|
|
2004
|
+
add_colorbar=True,
|
|
2005
|
+
add_fit=True,
|
|
2006
|
+
pol="",
|
|
2007
|
+
title=None,
|
|
2008
|
+
ax=None,
|
|
2009
|
+
figsize=(8, 8),
|
|
2010
|
+
dpi=300,
|
|
2011
|
+
legend_fontsize=14,
|
|
2012
|
+
):
|
|
2013
|
+
"""Create a 2D histogram of A vs R."""
|
|
2014
|
+
# Define a_min and a_max
|
|
2015
|
+
a_min = 0.001
|
|
2016
|
+
a_max = 10
|
|
2017
|
+
a_bins = log_arange(a_min, a_max, log_step=0.025, base=10)
|
|
2018
|
+
rlims = (0.1, 500)
|
|
2019
|
+
r_bins = log_arange(*rlims, log_step=0.025, base=10)
|
|
2020
|
+
|
|
2021
|
+
# Compute 2D histogram
|
|
2022
|
+
ds_stats = compute_2d_histogram(
|
|
2023
|
+
df,
|
|
2024
|
+
x=r,
|
|
2025
|
+
y=a,
|
|
2026
|
+
x_bins=r_bins,
|
|
2027
|
+
y_bins=a_bins,
|
|
2028
|
+
)
|
|
2029
|
+
|
|
2030
|
+
# Define colormap and norm
|
|
2031
|
+
if cmap is None:
|
|
2032
|
+
cmap = plt.get_cmap("Spectral_r").copy()
|
|
2033
|
+
cmap.set_under(alpha=0)
|
|
2034
|
+
norm = LogNorm(1, None) if norm is None else norm
|
|
2035
|
+
|
|
2036
|
+
# Define ticks and ticklabels
|
|
2037
|
+
r_ticks = [0.1, 1, 10, 50, 100, 500]
|
|
2038
|
+
a_ticks = [0.001, 0.01, 0.1, 0.5, 1, 5] # Adapt on a_max
|
|
2039
|
+
|
|
2040
|
+
# Define A symbol
|
|
2041
|
+
a_symbol = get_symbol_str("A", pol)
|
|
2042
|
+
|
|
2043
|
+
# Set default title if not provided
|
|
2044
|
+
if title is None:
|
|
2045
|
+
title = rf"${a_symbol}$ vs $R$"
|
|
2046
|
+
|
|
2047
|
+
# Create figure if ax is None
|
|
2048
|
+
if ax is None:
|
|
2049
|
+
fig, ax = plt.subplots(1, 1, figsize=figsize, dpi=dpi)
|
|
2050
|
+
|
|
2051
|
+
# Plot 2D histogram
|
|
2052
|
+
p = ds_stats["count"].plot.pcolormesh(
|
|
2053
|
+
x=r,
|
|
2054
|
+
y=a,
|
|
2055
|
+
ax=ax,
|
|
2056
|
+
cmap=cmap,
|
|
2057
|
+
norm=norm,
|
|
2058
|
+
add_colorbar=add_colorbar,
|
|
2059
|
+
extend="max",
|
|
2060
|
+
xscale="log",
|
|
2061
|
+
yscale="log",
|
|
2062
|
+
cbar_kwargs={"label": "Counts"} if add_colorbar else {},
|
|
2063
|
+
)
|
|
2064
|
+
ax.set_xlabel(r"$R$ [mm h$^{-1}$]")
|
|
2065
|
+
ax.set_ylabel(rf"${a_symbol}$ [dB km$^{{-1}}$]")
|
|
2066
|
+
ax.set_ylim(a_min, a_max)
|
|
2067
|
+
ax.set_xlim(*rlims)
|
|
2068
|
+
ax.set_xticks(r_ticks)
|
|
2069
|
+
ax.set_xticklabels([str(v) for v in r_ticks])
|
|
2070
|
+
ax.set_yticks(a_ticks)
|
|
2071
|
+
ax.set_yticklabels([str(v) for v in a_ticks])
|
|
2072
|
+
ax.set_title(title)
|
|
2073
|
+
if add_fit:
|
|
2074
|
+
# Fit powerlaw k = a * R ** b
|
|
2075
|
+
(a_c, b), _ = fit_powerlaw(x=df[r], y=df[a], xbins=r_bins, x_in_db=False)
|
|
2076
|
+
# Invert for R = A * k ** B
|
|
2077
|
+
A_c, B = inverse_powerlaw_parameters(a_c, b)
|
|
2078
|
+
# Define legend title
|
|
2079
|
+
a_str = _define_coeff_string(a_c)
|
|
2080
|
+
A_str = _define_coeff_string(A_c)
|
|
2081
|
+
legend_str = rf"${a_symbol} = {a_str} \, R^{{{b:.2f}}}$" "\n" rf"$R = {A_str} \, {a_symbol}^{{{B:.2f}}}$"
|
|
2082
|
+
# Get power law predictions
|
|
2083
|
+
x_pred = np.arange(*rlims)
|
|
2084
|
+
r_pred = predict_from_powerlaw(x_pred, a=a_c, b=b)
|
|
2085
|
+
# Add fitted power law
|
|
2086
|
+
ax.plot(x_pred, r_pred, linestyle="dashed", color="black")
|
|
2087
|
+
# Add legend
|
|
2088
|
+
legend_bbox_dict = {"facecolor": "white", "edgecolor": "black", "alpha": 0.7}
|
|
2089
|
+
ax.text(
|
|
2090
|
+
0.05,
|
|
2091
|
+
0.95,
|
|
2092
|
+
legend_str,
|
|
2093
|
+
transform=ax.transAxes,
|
|
2094
|
+
ha="left",
|
|
2095
|
+
va="top",
|
|
2096
|
+
fontsize=legend_fontsize,
|
|
2097
|
+
bbox=legend_bbox_dict,
|
|
2098
|
+
)
|
|
2099
|
+
return p
|
|
2100
|
+
|
|
2101
|
+
|
|
2102
|
+
def plot_A_Z(
|
|
2103
|
+
df,
|
|
2104
|
+
a,
|
|
2105
|
+
z,
|
|
2106
|
+
cmap=None,
|
|
2107
|
+
norm=None,
|
|
2108
|
+
add_colorbar=True,
|
|
2109
|
+
add_fit=True,
|
|
2110
|
+
pol="",
|
|
2111
|
+
title=None,
|
|
2112
|
+
ax=None,
|
|
2113
|
+
a_lim=(0.0001, 10),
|
|
2114
|
+
z_lim=(0, 70),
|
|
2115
|
+
figsize=(8, 8),
|
|
2116
|
+
dpi=300,
|
|
2117
|
+
legend_fontsize=14,
|
|
2118
|
+
):
|
|
2119
|
+
"""Create a 2D histogram of A vs Z."""
|
|
2120
|
+
# Define bins
|
|
2121
|
+
a_bins = log_arange(*a_lim, log_step=0.025, base=10)
|
|
2122
|
+
z_bins = np.arange(*z_lim, 0.5)
|
|
2123
|
+
|
|
2124
|
+
# Compute 2d histogram
|
|
2125
|
+
ds_stats = compute_2d_histogram(
|
|
2126
|
+
df,
|
|
2127
|
+
x=z,
|
|
2128
|
+
y=a,
|
|
2129
|
+
x_bins=z_bins,
|
|
2130
|
+
y_bins=a_bins,
|
|
2131
|
+
)
|
|
2132
|
+
|
|
2133
|
+
# Define colormap and norm
|
|
2134
|
+
if cmap is None:
|
|
2135
|
+
cmap = plt.get_cmap("Spectral_r").copy()
|
|
2136
|
+
cmap.set_under(alpha=0)
|
|
2137
|
+
if norm is None:
|
|
2138
|
+
norm = LogNorm(1, None)
|
|
2139
|
+
|
|
2140
|
+
# Ticks
|
|
2141
|
+
a_ticks = [0.001, 0.01, 0.1, 0.5, 1, 5]
|
|
2142
|
+
|
|
2143
|
+
# Define symbols
|
|
2144
|
+
a_symbol = get_symbol_str("A", pol)
|
|
2145
|
+
z_symbol = get_symbol_str("Z", pol)
|
|
2146
|
+
z_lower_symbol = get_symbol_str("z", pol)
|
|
2147
|
+
|
|
2148
|
+
# Set default title if not provided
|
|
2149
|
+
if title is None:
|
|
2150
|
+
title = rf"${a_symbol}$ vs ${z_symbol}$"
|
|
2151
|
+
|
|
2152
|
+
# Create figure if ax is None
|
|
2153
|
+
if ax is None:
|
|
2154
|
+
fig, ax = plt.subplots(1, 1, figsize=figsize, dpi=dpi)
|
|
2155
|
+
|
|
2156
|
+
# Plot 2D histogram
|
|
2157
|
+
p = ds_stats["count"].plot.pcolormesh(
|
|
2158
|
+
x=z,
|
|
2159
|
+
y=a,
|
|
2160
|
+
ax=ax,
|
|
2161
|
+
cmap=cmap,
|
|
2162
|
+
norm=norm,
|
|
2163
|
+
add_colorbar=add_colorbar,
|
|
2164
|
+
extend="max",
|
|
2165
|
+
xscale=None,
|
|
2166
|
+
yscale="log",
|
|
2167
|
+
cbar_kwargs={"label": "Counts"} if add_colorbar else {},
|
|
2168
|
+
)
|
|
2169
|
+
ax.set_xlabel(rf"${z_symbol}$ [dBZ]")
|
|
2170
|
+
ax.set_ylabel(rf"${a_symbol}$ [dB km$^{{-1}}$]")
|
|
2171
|
+
ax.set_xlim(*z_lim)
|
|
2172
|
+
ax.set_ylim(*a_lim)
|
|
2173
|
+
ax.set_yticks(a_ticks)
|
|
2174
|
+
ax.set_yticklabels([str(v) for v in a_ticks])
|
|
2175
|
+
ax.set_title(title)
|
|
2176
|
+
|
|
2177
|
+
# Fit and plot the power law
|
|
2178
|
+
if add_fit:
|
|
2179
|
+
# Fit powerlaw k = a * Z ** b (Z in dBZ -> x_in_db=True)
|
|
2180
|
+
(a_c, b), _ = fit_powerlaw(
|
|
2181
|
+
x=df[z],
|
|
2182
|
+
y=df[a],
|
|
2183
|
+
xbins=z_bins,
|
|
2184
|
+
x_in_db=True,
|
|
2185
|
+
)
|
|
2186
|
+
# Invert for Z = A * k ** B
|
|
2187
|
+
A_c, B = inverse_powerlaw_parameters(a_c, b)
|
|
2188
|
+
# Legend text
|
|
2189
|
+
a_str = _define_coeff_string(a_c)
|
|
2190
|
+
A_str = _define_coeff_string(A_c)
|
|
2191
|
+
legend_str = (
|
|
2192
|
+
rf"${a_symbol} = {a_str} \, {z_lower_symbol}^{{{b:.2f}}}$"
|
|
2193
|
+
"\n"
|
|
2194
|
+
rf"${z_lower_symbol} = {A_str} \, {a_symbol}^{{{B:.2f}}}$"
|
|
2195
|
+
)
|
|
2196
|
+
# Predictions
|
|
2197
|
+
x_pred = np.arange(*z_lim)
|
|
2198
|
+
x_pred_linear = disdrodb.idecibel(x_pred) # convert to linear for prediction
|
|
2199
|
+
y_pred = predict_from_powerlaw(x_pred_linear, a=a_c, b=b)
|
|
2200
|
+
ax.plot(x_pred, y_pred, linestyle="dashed", color="black")
|
|
2201
|
+
# Add legend
|
|
2202
|
+
legend_bbox_dict = {"facecolor": "white", "edgecolor": "black", "alpha": 0.7}
|
|
2203
|
+
ax.text(
|
|
2204
|
+
0.05,
|
|
2205
|
+
0.95,
|
|
2206
|
+
legend_str,
|
|
2207
|
+
transform=ax.transAxes,
|
|
2208
|
+
ha="left",
|
|
2209
|
+
va="top",
|
|
2210
|
+
fontsize=legend_fontsize,
|
|
2211
|
+
bbox=legend_bbox_dict,
|
|
2212
|
+
)
|
|
2213
|
+
return p
|
|
2214
|
+
|
|
2215
|
+
|
|
2216
|
+
def plot_A_KDP(
|
|
2217
|
+
df,
|
|
2218
|
+
a,
|
|
2219
|
+
kdp,
|
|
2220
|
+
log_a=True,
|
|
2221
|
+
log_kdp=False,
|
|
2222
|
+
a_lim=(0.001, 10),
|
|
2223
|
+
kdp_lim=None,
|
|
2224
|
+
pol="",
|
|
2225
|
+
ax=None,
|
|
2226
|
+
cmap=None,
|
|
2227
|
+
norm=None,
|
|
2228
|
+
add_colorbar=True,
|
|
2229
|
+
add_fit=True,
|
|
2230
|
+
title=None,
|
|
2231
|
+
figsize=(8, 8),
|
|
2232
|
+
dpi=300,
|
|
2233
|
+
legend_fontsize=14,
|
|
2234
|
+
):
|
|
2235
|
+
"""Create a 2D histogram of k(H/V) vs KDP."""
|
|
2236
|
+
# Bins & limits for a
|
|
2237
|
+
if log_a:
|
|
2238
|
+
a_bins = log_arange(*a_lim, log_step=0.025, base=10)
|
|
2239
|
+
yscale = "log"
|
|
2240
|
+
a_ticks = [0.001, 0.01, 0.1, 0.5, 1, 5]
|
|
2241
|
+
else:
|
|
2242
|
+
a_bins = np.arange(a_lim[0], a_lim[1], 0.01)
|
|
2243
|
+
yscale = None
|
|
2244
|
+
a_ticks = None
|
|
2245
|
+
|
|
2246
|
+
# Bins & limits for KDP
|
|
2247
|
+
if log_kdp:
|
|
2248
|
+
kdp_lim = (0.05, 10) if kdp_lim is None else kdp_lim
|
|
2249
|
+
kdp_bins = log_arange(*kdp_lim, log_step=0.05, base=10)
|
|
2250
|
+
xscale = "log"
|
|
2251
|
+
kdp_ticks = [0.05, 0.1, 0.5, 1, 5, 10]
|
|
2252
|
+
else:
|
|
2253
|
+
kdp_lim = (0, 8) if kdp_lim is None else kdp_lim
|
|
2254
|
+
kdp_bins = np.arange(kdp_lim[0], kdp_lim[1], 0.1)
|
|
2255
|
+
xscale = None
|
|
2256
|
+
kdp_ticks = None
|
|
2257
|
+
|
|
2258
|
+
# Compute 2D histogram
|
|
2259
|
+
ds_stats = compute_2d_histogram(
|
|
2260
|
+
df,
|
|
2261
|
+
x=kdp,
|
|
2262
|
+
y=a,
|
|
2263
|
+
x_bins=kdp_bins,
|
|
2264
|
+
y_bins=a_bins,
|
|
2265
|
+
)
|
|
2266
|
+
|
|
2267
|
+
# Colormap & norm
|
|
2268
|
+
if cmap is None:
|
|
2269
|
+
cmap = plt.get_cmap("Spectral_r").copy()
|
|
2270
|
+
cmap.set_under(alpha=0)
|
|
2271
|
+
if norm is None:
|
|
2272
|
+
norm = LogNorm(1, None)
|
|
2273
|
+
|
|
2274
|
+
# Define symbols
|
|
2275
|
+
a_symbol = get_symbol_str("A", pol)
|
|
2276
|
+
|
|
2277
|
+
# Set default title if not provided
|
|
2278
|
+
if title is None:
|
|
2279
|
+
title = rf"${a_symbol}$ vs $K_{{\mathrm{{DP}}}}$"
|
|
2280
|
+
|
|
2281
|
+
# Create figure if ax is None
|
|
2282
|
+
if ax is None:
|
|
2283
|
+
fig, ax = plt.subplots(1, 1, figsize=figsize, dpi=dpi)
|
|
2284
|
+
|
|
2285
|
+
# Plot 2D histogram
|
|
2286
|
+
p = ds_stats["count"].plot.pcolormesh(
|
|
2287
|
+
x=kdp,
|
|
2288
|
+
y=a,
|
|
2289
|
+
ax=ax,
|
|
2290
|
+
cmap=cmap,
|
|
2291
|
+
norm=norm,
|
|
2292
|
+
add_colorbar=add_colorbar,
|
|
2293
|
+
extend="max",
|
|
2294
|
+
xscale=xscale,
|
|
2295
|
+
yscale=yscale,
|
|
2296
|
+
cbar_kwargs={"label": "Counts"} if add_colorbar else {},
|
|
2297
|
+
)
|
|
2298
|
+
ax.set_xlabel(r"$K_{\mathrm{DP}}$ [deg km$^{-1}$]")
|
|
2299
|
+
ax.set_ylabel(rf"${a_symbol}$ [dB km$^{{-1}}$]")
|
|
2300
|
+
ax.set_xlim(*kdp_lim)
|
|
2301
|
+
ax.set_ylim(*a_lim)
|
|
2302
|
+
if kdp_ticks is not None:
|
|
2303
|
+
ax.set_xticks(kdp_ticks)
|
|
2304
|
+
ax.set_xticklabels([str(v) for v in kdp_ticks])
|
|
2305
|
+
if a_ticks is not None:
|
|
2306
|
+
ax.set_yticks(a_ticks)
|
|
2307
|
+
ax.set_yticklabels([str(v) for v in a_ticks])
|
|
2308
|
+
ax.set_title(title)
|
|
2309
|
+
|
|
2310
|
+
# Fit and overlay power law: k = a * KDP^b
|
|
2311
|
+
if add_fit:
|
|
2312
|
+
(a_c, b), _ = fit_powerlaw(
|
|
2313
|
+
x=df[kdp],
|
|
2314
|
+
y=df[a],
|
|
2315
|
+
xbins=kdp_bins,
|
|
2316
|
+
x_in_db=False,
|
|
2317
|
+
)
|
|
2318
|
+
# Invert: KDP = A * k^B
|
|
2319
|
+
A_c, B = inverse_powerlaw_parameters(a_c, b)
|
|
2320
|
+
|
|
2321
|
+
a_str = _define_coeff_string(a_c)
|
|
2322
|
+
A_str = _define_coeff_string(A_c)
|
|
2323
|
+
legend_str = (
|
|
2324
|
+
rf"${a_symbol} = {a_str}\,K_{{\mathrm{{DP}}}}^{{{b:.2f}}}$"
|
|
2325
|
+
"\n"
|
|
2326
|
+
rf"$K_{{\mathrm{{DP}}}} = {A_str}\,{a_symbol}^{{{B:.2f}}}$"
|
|
2327
|
+
)
|
|
2328
|
+
|
|
2329
|
+
# Predictions along KDP axis
|
|
2330
|
+
if log_kdp:
|
|
2331
|
+
x_pred = np.logspace(np.log10(kdp_lim[0]), np.log10(kdp_lim[1]), 400)
|
|
2332
|
+
else:
|
|
2333
|
+
x_pred = np.arange(kdp_lim[0], kdp_lim[1], 0.05)
|
|
2334
|
+
y_pred = predict_from_powerlaw(x_pred, a=a_c, b=b)
|
|
2335
|
+
|
|
2336
|
+
ax.plot(x_pred, y_pred, linestyle="dashed", color="black")
|
|
2337
|
+
# Add legend
|
|
2338
|
+
legend_bbox_dict = {"facecolor": "white", "edgecolor": "black", "alpha": 0.7}
|
|
2339
|
+
ax.text(
|
|
2340
|
+
0.05,
|
|
2341
|
+
0.95,
|
|
2342
|
+
legend_str,
|
|
2343
|
+
transform=ax.transAxes,
|
|
2344
|
+
ha="left",
|
|
2345
|
+
va="top",
|
|
2346
|
+
fontsize=legend_fontsize,
|
|
2347
|
+
bbox=legend_bbox_dict,
|
|
2348
|
+
)
|
|
2349
|
+
|
|
2350
|
+
return p
|
|
2351
|
+
|
|
2352
|
+
|
|
2353
|
+
def plot_R_Z(
|
|
2354
|
+
df,
|
|
2355
|
+
z,
|
|
2356
|
+
r,
|
|
2357
|
+
cmap=None,
|
|
2358
|
+
norm=None,
|
|
2359
|
+
add_colorbar=True,
|
|
2360
|
+
add_fit=True,
|
|
2361
|
+
pol="",
|
|
2362
|
+
title=None,
|
|
2363
|
+
ax=None,
|
|
2364
|
+
figsize=(8, 8),
|
|
2365
|
+
dpi=300,
|
|
2366
|
+
legend_fontsize=14,
|
|
2367
|
+
):
|
|
2368
|
+
"""Create a 2D histogram of Z vs R."""
|
|
2369
|
+
# Define axis limits
|
|
2370
|
+
z_lims = (0, 70)
|
|
2371
|
+
r_lims = (0.1, 500)
|
|
2372
|
+
|
|
2373
|
+
# Compute 2d histogram
|
|
2374
|
+
ds_stats = compute_2d_histogram(
|
|
2375
|
+
df,
|
|
2376
|
+
x=z,
|
|
2377
|
+
y=r,
|
|
2378
|
+
x_bins=np.arange(*z_lims, 0.5),
|
|
2379
|
+
y_bins=log_arange(*r_lims, log_step=0.05, base=10),
|
|
2380
|
+
)
|
|
2381
|
+
|
|
2382
|
+
# Define colormap and norm
|
|
2383
|
+
if cmap is None:
|
|
2384
|
+
cmap = plt.get_cmap("Spectral_r").copy()
|
|
2385
|
+
cmap.set_under(alpha=0)
|
|
2386
|
+
norm = LogNorm(1, None) if norm is None else norm
|
|
2387
|
+
|
|
2388
|
+
# Define rain ticks and ticklabels
|
|
2389
|
+
r_ticks = [0.1, 1, 10, 50, 100, 500]
|
|
2390
|
+
|
|
2391
|
+
# Define symbols
|
|
2392
|
+
z_symbol = get_symbol_str("Z", pol)
|
|
2393
|
+
z_lower_symbol = get_symbol_str("z", pol)
|
|
2394
|
+
|
|
2395
|
+
# Set default title if not provided
|
|
2396
|
+
if title is None:
|
|
2397
|
+
title = rf"${z_symbol}$ vs $R$"
|
|
2398
|
+
|
|
2399
|
+
# Create figure if ax is None
|
|
2400
|
+
if ax is None:
|
|
2401
|
+
fig, ax = plt.subplots(1, 1, figsize=figsize, dpi=dpi)
|
|
2402
|
+
|
|
2403
|
+
# Plot 2D histogram
|
|
2404
|
+
p = ds_stats["count"].plot.pcolormesh(
|
|
2405
|
+
x=z,
|
|
2406
|
+
y=r,
|
|
2407
|
+
ax=ax,
|
|
2408
|
+
cmap=cmap,
|
|
2409
|
+
norm=norm,
|
|
2410
|
+
add_colorbar=add_colorbar,
|
|
2411
|
+
extend="max",
|
|
2412
|
+
yscale="log",
|
|
2413
|
+
cbar_kwargs={"label": "Counts"} if add_colorbar else {},
|
|
2414
|
+
)
|
|
2415
|
+
ax.set_ylabel(r"$R$ [mm h$^{-1}$]")
|
|
2416
|
+
ax.set_xlabel(rf"${z_symbol}$ [dBZ]")
|
|
2417
|
+
ax.set_xlim(*z_lims)
|
|
2418
|
+
ax.set_ylim(*r_lims)
|
|
2419
|
+
ax.set_yticks(r_ticks)
|
|
2420
|
+
ax.set_yticklabels([str(v) for v in r_ticks])
|
|
2421
|
+
ax.set_title(title)
|
|
2422
|
+
|
|
2423
|
+
# Fit and plot the powerlaw
|
|
2424
|
+
if add_fit:
|
|
2425
|
+
# Fit powerlaw R = a * z ** b
|
|
2426
|
+
(a, b), _ = fit_powerlaw(x=df[z], y=df[r], xbins=np.arange(10, 50, 1), x_in_db=True)
|
|
2427
|
+
# Invert for z = A * R ** B
|
|
2428
|
+
A, B = inverse_powerlaw_parameters(a, b)
|
|
2429
|
+
# Define legend title
|
|
2430
|
+
a_str = _define_coeff_string(a)
|
|
2431
|
+
A_str = _define_coeff_string(A)
|
|
2432
|
+
legend_str = (
|
|
2433
|
+
rf"$R = {a_str} \, {z_lower_symbol}^{{{b:.2f}}}$" "\n" rf"${z_lower_symbol} = {A_str} \, R^{{{B:.2f}}}$"
|
|
2434
|
+
)
|
|
2435
|
+
# Get power law predictions
|
|
2436
|
+
x_pred = np.arange(*z_lims)
|
|
2437
|
+
x_pred_linear = disdrodb.idecibel(x_pred)
|
|
2438
|
+
r_pred = predict_from_powerlaw(x_pred_linear, a=a, b=b)
|
|
2439
|
+
# Add fitted powerlaw
|
|
2440
|
+
ax.plot(x_pred, r_pred, linestyle="dashed", color="black")
|
|
2441
|
+
# Add legend
|
|
2442
|
+
legend_bbox_dict = {"facecolor": "white", "edgecolor": "black", "alpha": 0.7}
|
|
2443
|
+
ax.text(
|
|
2444
|
+
0.05,
|
|
2445
|
+
0.95,
|
|
2446
|
+
legend_str,
|
|
2447
|
+
transform=ax.transAxes,
|
|
2448
|
+
ha="left",
|
|
2449
|
+
va="top",
|
|
2450
|
+
fontsize=legend_fontsize,
|
|
2451
|
+
bbox=legend_bbox_dict,
|
|
2452
|
+
)
|
|
2453
|
+
return p
|
|
2454
|
+
|
|
2455
|
+
|
|
2456
|
+
def plot_R_KDP(
|
|
2457
|
+
df,
|
|
2458
|
+
kdp,
|
|
2459
|
+
r,
|
|
2460
|
+
kdp_lim=None,
|
|
2461
|
+
r_lim=None,
|
|
2462
|
+
cmap=None,
|
|
2463
|
+
norm=None,
|
|
2464
|
+
add_colorbar=True,
|
|
2465
|
+
log_scale=False,
|
|
2466
|
+
add_fit=True,
|
|
2467
|
+
title=None,
|
|
2468
|
+
ax=None,
|
|
2469
|
+
figsize=(8, 8),
|
|
2470
|
+
dpi=300,
|
|
2471
|
+
legend_fontsize=14,
|
|
2472
|
+
):
|
|
2473
|
+
"""Create a 2D histogram of KDP vs R."""
|
|
2474
|
+
# Define bins
|
|
2475
|
+
if not log_scale:
|
|
2476
|
+
kdp_lim = (0, 8) if kdp_lim is None else kdp_lim
|
|
2477
|
+
r_lim = (0, 200) if r_lim is None else r_lim
|
|
2478
|
+
xbins = np.arange(*kdp_lim, 0.1)
|
|
2479
|
+
ybins = np.arange(*r_lim, 1)
|
|
2480
|
+
xscale = None
|
|
2481
|
+
yscale = None
|
|
2482
|
+
else:
|
|
2483
|
+
kdp_lim = (0.1, 10) if kdp_lim is None else kdp_lim
|
|
2484
|
+
r_lim = (0.1, 500) if r_lim is None else r_lim
|
|
2485
|
+
xbins = log_arange(*kdp_lim, log_step=0.05, base=10)
|
|
2486
|
+
ybins = log_arange(*r_lim, log_step=0.05, base=10)
|
|
2487
|
+
xscale = "log"
|
|
2488
|
+
yscale = "log"
|
|
2489
|
+
|
|
2490
|
+
# Compute 2d histogram
|
|
2491
|
+
ds_stats = compute_2d_histogram(
|
|
2492
|
+
df,
|
|
2493
|
+
x=kdp,
|
|
2494
|
+
y=r,
|
|
2495
|
+
x_bins=xbins,
|
|
2496
|
+
y_bins=ybins,
|
|
2497
|
+
# y_bins=log_arange(0.1, 500, log_step=0.05, base=10),
|
|
2498
|
+
)
|
|
2499
|
+
|
|
2500
|
+
# Define colormap and norm
|
|
2501
|
+
if cmap is None:
|
|
2502
|
+
cmap = plt.get_cmap("Spectral_r").copy()
|
|
2503
|
+
cmap.set_under(alpha=0)
|
|
2504
|
+
norm = LogNorm(1, None) if norm is None else norm
|
|
2505
|
+
|
|
2506
|
+
# Set default title if not provided
|
|
2507
|
+
if title is None:
|
|
2508
|
+
title = r"$K_{\mathrm{DP}}$ vs $R$"
|
|
2509
|
+
|
|
2510
|
+
# Create figure if ax is None
|
|
2511
|
+
if ax is None:
|
|
2512
|
+
fig, ax = plt.subplots(1, 1, figsize=figsize, dpi=dpi)
|
|
2513
|
+
|
|
2514
|
+
# Plot 2D histogram
|
|
2515
|
+
p = ds_stats["count"].plot.pcolormesh(
|
|
2516
|
+
x=kdp,
|
|
2517
|
+
y=r,
|
|
2518
|
+
ax=ax,
|
|
2519
|
+
cmap=cmap,
|
|
2520
|
+
norm=norm,
|
|
2521
|
+
add_colorbar=add_colorbar,
|
|
2522
|
+
xscale=xscale,
|
|
2523
|
+
yscale=yscale,
|
|
2524
|
+
extend="max",
|
|
2525
|
+
cbar_kwargs={"label": "Counts"} if add_colorbar else {},
|
|
2526
|
+
)
|
|
2527
|
+
ax.set_ylabel(r"$R$ [mm h$^{-1}$]")
|
|
2528
|
+
ax.set_xlabel(r"$K_{\mathrm{DP}}$ [deg km$^{-1}$]")
|
|
2529
|
+
ax.set_xlim(*kdp_lim)
|
|
2530
|
+
ax.set_ylim(*r_lim)
|
|
2531
|
+
ax.set_title(title)
|
|
2532
|
+
|
|
2533
|
+
# Fit and plot the power law
|
|
2534
|
+
if add_fit:
|
|
2535
|
+
# Fit powerlaw R = a * KDP ** b
|
|
2536
|
+
(a, b), _ = fit_powerlaw(x=df[kdp], y=df[r], xbins=xbins, x_in_db=False)
|
|
2537
|
+
# Invert for KDP = A * R ** B
|
|
2538
|
+
A, B = inverse_powerlaw_parameters(a, b)
|
|
2539
|
+
# Define legend title
|
|
2540
|
+
a_str = _define_coeff_string(a)
|
|
2541
|
+
A_str = _define_coeff_string(A)
|
|
2542
|
+
legend_str = (
|
|
2543
|
+
rf"$R = {a_str} \, K_{{\mathrm{{DP}}}}^{{{b:.2f}}}$"
|
|
2544
|
+
"\n"
|
|
2545
|
+
rf"$K_{{\mathrm{{DP}}}} = {A_str} \, R^{{{B:.2f}}}$"
|
|
2546
|
+
)
|
|
2547
|
+
# Get power law predictions
|
|
2548
|
+
x_pred = np.arange(*kdp_lim)
|
|
2549
|
+
r_pred = predict_from_powerlaw(x_pred, a=a, b=b)
|
|
2550
|
+
# Add fitted line
|
|
2551
|
+
ax.plot(x_pred, r_pred, linestyle="dashed", color="black")
|
|
2552
|
+
# Add legend
|
|
2553
|
+
legend_bbox_dict = {"facecolor": "white", "edgecolor": "black", "alpha": 0.7}
|
|
2554
|
+
ax.text(
|
|
2555
|
+
0.05,
|
|
2556
|
+
0.95,
|
|
2557
|
+
legend_str,
|
|
2558
|
+
transform=ax.transAxes,
|
|
2559
|
+
ha="left",
|
|
2560
|
+
va="top",
|
|
2561
|
+
fontsize=legend_fontsize,
|
|
2562
|
+
bbox=legend_bbox_dict,
|
|
2563
|
+
)
|
|
2564
|
+
return p
|
|
2565
|
+
|
|
2566
|
+
|
|
2567
|
+
def plot_ZDR_Z(
|
|
2568
|
+
df,
|
|
2569
|
+
z,
|
|
2570
|
+
zdr,
|
|
2571
|
+
zdr_lim=(0, 2.5),
|
|
2572
|
+
z_lim=(0, 70),
|
|
2573
|
+
cmap=None,
|
|
2574
|
+
norm=None,
|
|
2575
|
+
add_colorbar=True,
|
|
2576
|
+
add_fit=True,
|
|
2577
|
+
pol="",
|
|
2578
|
+
title=None,
|
|
2579
|
+
ax=None,
|
|
2580
|
+
figsize=(8, 8),
|
|
2581
|
+
dpi=300,
|
|
2582
|
+
legend_fontsize=14,
|
|
2583
|
+
):
|
|
2584
|
+
"""Create a 2D histogram of Zdr vs Z."""
|
|
2585
|
+
# Compute 2d histogram
|
|
2586
|
+
ds_stats = compute_2d_histogram(
|
|
2587
|
+
df,
|
|
2588
|
+
x=z,
|
|
2589
|
+
y=zdr,
|
|
2590
|
+
x_bins=np.arange(*z_lim, 0.5),
|
|
2591
|
+
y_bins=np.arange(*zdr_lim, 0.025),
|
|
2592
|
+
)
|
|
2593
|
+
|
|
2594
|
+
# Define colormap and norm
|
|
2595
|
+
if cmap is None:
|
|
2596
|
+
cmap = plt.get_cmap("Spectral_r").copy()
|
|
2597
|
+
cmap.set_under(alpha=0)
|
|
2598
|
+
norm = LogNorm(1, None) if norm is None else norm
|
|
2599
|
+
|
|
2600
|
+
# Define symbols
|
|
2601
|
+
z_symbol = get_symbol_str("Z", pol)
|
|
2602
|
+
z_lower_symbol = get_symbol_str("z", pol)
|
|
2603
|
+
|
|
2604
|
+
# Set default title if not provided
|
|
2605
|
+
if title is None:
|
|
2606
|
+
title = rf"$Z_{{\mathrm{{DR}}}}$ vs ${z_symbol}$"
|
|
2607
|
+
|
|
2608
|
+
# Create figure if ax is None
|
|
2609
|
+
if ax is None:
|
|
2610
|
+
fig, ax = plt.subplots(1, 1, figsize=figsize, dpi=dpi)
|
|
2611
|
+
|
|
2612
|
+
# Plot 2D histogram
|
|
2613
|
+
p = ds_stats["count"].plot.pcolormesh(
|
|
2614
|
+
x=z,
|
|
2615
|
+
y=zdr,
|
|
2616
|
+
ax=ax,
|
|
2617
|
+
cmap=cmap,
|
|
2618
|
+
norm=norm,
|
|
2619
|
+
add_colorbar=add_colorbar,
|
|
2620
|
+
extend="max",
|
|
2621
|
+
cbar_kwargs={"label": "Counts"} if add_colorbar else {},
|
|
2622
|
+
)
|
|
2623
|
+
ax.set_xlabel(rf"${z_symbol}$ [dBZ]")
|
|
2624
|
+
ax.set_ylabel(r"$Z_{DR}$ [dB]")
|
|
2625
|
+
ax.set_xlim(*z_lim)
|
|
2626
|
+
ax.set_ylim(*zdr_lim)
|
|
2627
|
+
ax.set_title(title)
|
|
2628
|
+
|
|
2629
|
+
# Fit and plot the power law
|
|
2630
|
+
if add_fit:
|
|
2631
|
+
# Fit powerlaw ZDR = a * Z ** b
|
|
2632
|
+
(a, b), _ = fit_powerlaw(
|
|
2633
|
+
x=df[z],
|
|
2634
|
+
y=df[zdr],
|
|
2635
|
+
xbins=np.arange(5, 40, 1),
|
|
2636
|
+
x_in_db=True,
|
|
2637
|
+
)
|
|
2638
|
+
# Invert for Z = A * ZDR ** B
|
|
2639
|
+
A, B = inverse_powerlaw_parameters(a, b)
|
|
2640
|
+
# Define legend title
|
|
2641
|
+
a_str = _define_coeff_string(a)
|
|
2642
|
+
A_str = _define_coeff_string(A)
|
|
2643
|
+
legend_str = (
|
|
2644
|
+
rf"$Z_{{\mathrm{{DR}}}} = {a_str} \, {z_lower_symbol}^{{{b:.2f}}}$"
|
|
2645
|
+
"\n"
|
|
2646
|
+
rf"${z_lower_symbol} = {A_str} \, Z_{{\mathrm{{DR}}}}^{{{B:.2f}}}$"
|
|
2647
|
+
)
|
|
2648
|
+
# Get power law predictions
|
|
2649
|
+
x_pred = np.arange(0, 70)
|
|
2650
|
+
x_pred_linear = disdrodb.idecibel(x_pred)
|
|
2651
|
+
r_pred = predict_from_powerlaw(x_pred_linear, a=a, b=b)
|
|
2652
|
+
# Add fitted line
|
|
2653
|
+
ax.plot(x_pred, r_pred, linestyle="dashed", color="black")
|
|
2654
|
+
# Add legend
|
|
2655
|
+
legend_bbox_dict = {"facecolor": "white", "edgecolor": "black", "alpha": 0.7}
|
|
2656
|
+
ax.text(
|
|
2657
|
+
0.05,
|
|
2658
|
+
0.95,
|
|
2659
|
+
legend_str,
|
|
2660
|
+
transform=ax.transAxes,
|
|
2661
|
+
ha="left",
|
|
2662
|
+
va="top",
|
|
2663
|
+
fontsize=legend_fontsize,
|
|
2664
|
+
bbox=legend_bbox_dict,
|
|
2665
|
+
)
|
|
2666
|
+
return p
|
|
2667
|
+
|
|
2668
|
+
|
|
2669
|
+
def plot_KDP_Z(
|
|
2670
|
+
df,
|
|
2671
|
+
kdp,
|
|
2672
|
+
z,
|
|
2673
|
+
z_lim=(0, 70),
|
|
2674
|
+
log_kdp=False,
|
|
2675
|
+
kdp_lim=None,
|
|
2676
|
+
cmap=None,
|
|
2677
|
+
norm=None,
|
|
2678
|
+
add_colorbar=True,
|
|
2679
|
+
add_fit=True,
|
|
2680
|
+
pol="",
|
|
2681
|
+
title=None,
|
|
2682
|
+
ax=None,
|
|
2683
|
+
figsize=(8, 8),
|
|
2684
|
+
dpi=300,
|
|
2685
|
+
legend_fontsize=14,
|
|
2686
|
+
):
|
|
2687
|
+
"""Create a 2D histogram of KDP vs Z."""
|
|
2688
|
+
# Bins & limits
|
|
2689
|
+
z_bins = np.arange(*z_lim, 0.5)
|
|
2690
|
+
if log_kdp:
|
|
2691
|
+
kdp_lim = (0.01, 10) if kdp_lim is None else kdp_lim
|
|
2692
|
+
kdp_bins = log_arange(*kdp_lim, log_step=0.05, base=10)
|
|
2693
|
+
yscale = "log"
|
|
2694
|
+
kdp_ticks = [0.01, 0.1, 0.5, 1, 5, 10]
|
|
2695
|
+
else:
|
|
2696
|
+
kdp_lim = (0, 10) if kdp_lim is None else kdp_lim
|
|
2697
|
+
kdp_bins = np.arange(*kdp_lim, 0.1)
|
|
2698
|
+
yscale = None
|
|
2699
|
+
kdp_ticks = None
|
|
2700
|
+
|
|
2701
|
+
# Compute 2D histogram
|
|
2702
|
+
ds_stats = compute_2d_histogram(
|
|
2703
|
+
df,
|
|
2704
|
+
x=z,
|
|
2705
|
+
y=kdp,
|
|
2706
|
+
x_bins=z_bins,
|
|
2707
|
+
y_bins=kdp_bins,
|
|
2708
|
+
)
|
|
2709
|
+
|
|
2710
|
+
# Colormap & norm
|
|
2711
|
+
if cmap is None:
|
|
2712
|
+
cmap = plt.get_cmap("Spectral_r").copy()
|
|
2713
|
+
cmap.set_under(alpha=0)
|
|
2714
|
+
if norm is None:
|
|
2715
|
+
norm = LogNorm(1, None)
|
|
2716
|
+
|
|
2717
|
+
# Define symbols
|
|
2718
|
+
z_symbol = get_symbol_str("Z", pol)
|
|
2719
|
+
z_lower_symbol = get_symbol_str("z", pol)
|
|
2720
|
+
|
|
2721
|
+
# Set default title if not provided
|
|
2722
|
+
if title is None:
|
|
2723
|
+
title = rf"$K_{{\mathrm{{DP}}}}$ vs ${z_symbol}$"
|
|
2724
|
+
|
|
2725
|
+
# Create figure if ax is None
|
|
2726
|
+
if ax is None:
|
|
2727
|
+
fig, ax = plt.subplots(1, 1, figsize=figsize, dpi=dpi)
|
|
2728
|
+
|
|
2729
|
+
# Plot 2D histogram
|
|
2730
|
+
p = ds_stats["count"].plot.pcolormesh(
|
|
2731
|
+
x=z,
|
|
2732
|
+
y=kdp,
|
|
2733
|
+
ax=ax,
|
|
2734
|
+
cmap=cmap,
|
|
2735
|
+
norm=norm,
|
|
2736
|
+
add_colorbar=add_colorbar,
|
|
2737
|
+
extend="max",
|
|
2738
|
+
yscale=yscale,
|
|
2739
|
+
cbar_kwargs={"label": "Counts"} if add_colorbar else {},
|
|
2740
|
+
)
|
|
2741
|
+
ax.set_xlabel(rf"${z_symbol}$ [dBZ]")
|
|
2742
|
+
ax.set_ylabel(r"$K_{\mathrm{DP}}$ [deg km$^{-1}$]")
|
|
2743
|
+
ax.set_xlim(*z_lim)
|
|
2744
|
+
ax.set_ylim(*kdp_lim)
|
|
2745
|
+
if kdp_ticks is not None:
|
|
2746
|
+
ax.set_yticks(kdp_ticks)
|
|
2747
|
+
ax.set_yticklabels([str(v) for v in kdp_ticks])
|
|
2748
|
+
ax.set_title(title)
|
|
2749
|
+
|
|
2750
|
+
# Fit and overlay power law
|
|
2751
|
+
if add_fit:
|
|
2752
|
+
# Fit: KDP = a * Z^b (Z in dBZ → x_in_db=True)
|
|
2753
|
+
(a, b), _ = fit_powerlaw(
|
|
2754
|
+
x=df[z],
|
|
2755
|
+
y=df[kdp],
|
|
2756
|
+
xbins=np.arange(15, 50),
|
|
2757
|
+
x_in_db=True,
|
|
2758
|
+
)
|
|
2759
|
+
# Invert: Z = A * KDP^B
|
|
2760
|
+
A, B = inverse_powerlaw_parameters(a, b)
|
|
2761
|
+
|
|
2762
|
+
# Define legend title
|
|
2763
|
+
a_str = _define_coeff_string(a)
|
|
2764
|
+
A_str = _define_coeff_string(A)
|
|
2765
|
+
legend_str = (
|
|
2766
|
+
rf"$K_{{\mathrm{{DP}}}} = {a_str}\,{z_lower_symbol}^{{{b:.2f}}}$"
|
|
2767
|
+
"\n"
|
|
2768
|
+
rf"${z_lower_symbol} = {A_str}\,K_{{\mathrm{{DP}}}}^{{{B:.2f}}}$"
|
|
2769
|
+
)
|
|
2770
|
+
|
|
2771
|
+
# Get power law predictions
|
|
2772
|
+
x_pred = np.arange(*z_lim)
|
|
2773
|
+
x_pred_linear = disdrodb.idecibel(x_pred)
|
|
2774
|
+
y_pred = predict_from_powerlaw(x_pred_linear, a=a, b=b)
|
|
2775
|
+
# Add fitted power law
|
|
2776
|
+
ax.plot(x_pred, y_pred, linestyle="dashed", color="black")
|
|
2777
|
+
# Add legend
|
|
2778
|
+
legend_bbox_dict = {"facecolor": "white", "edgecolor": "black", "alpha": 0.7}
|
|
2779
|
+
ax.text(
|
|
2780
|
+
0.05,
|
|
2781
|
+
0.95,
|
|
2782
|
+
legend_str,
|
|
2783
|
+
transform=ax.transAxes,
|
|
2784
|
+
ha="left",
|
|
2785
|
+
va="top",
|
|
2786
|
+
fontsize=legend_fontsize,
|
|
2787
|
+
bbox=legend_bbox_dict,
|
|
2788
|
+
)
|
|
2789
|
+
|
|
2790
|
+
return p
|
|
2791
|
+
|
|
2792
|
+
|
|
2793
|
+
def plot_ADP_KDP_ZDR(
|
|
2794
|
+
df,
|
|
2795
|
+
adp,
|
|
2796
|
+
kdp,
|
|
2797
|
+
zdr,
|
|
2798
|
+
y_lim=(0, 0.015),
|
|
2799
|
+
zdr_lim=(0, 6),
|
|
2800
|
+
cmap=None,
|
|
2801
|
+
norm=None,
|
|
2802
|
+
add_colorbar=True,
|
|
2803
|
+
title=None,
|
|
2804
|
+
ax=None,
|
|
2805
|
+
figsize=(8, 8),
|
|
2806
|
+
dpi=300,
|
|
2807
|
+
):
|
|
2808
|
+
"""Create a 2D histogram of ADP/KDP vs ZDR.
|
|
2809
|
+
|
|
2810
|
+
References
|
|
2811
|
+
----------
|
|
2812
|
+
Ryzhkov, A., P. Zhang, and J. Hu, 2025.
|
|
2813
|
+
Suggested Modifications for the S-Band Polarimetric Radar Rainfall Estimation Algorithm.
|
|
2814
|
+
J. Hydrometeor., 26, 1053-1062. https://doi.org/10.1175/JHM-D-25-0014.1.
|
|
2815
|
+
"""
|
|
2816
|
+
# Compute ADP/KDP
|
|
2817
|
+
df["ADP/KDP"] = df[adp] / df[kdp]
|
|
2818
|
+
|
|
2819
|
+
# Bins & limits
|
|
2820
|
+
y_bins = np.arange(y_lim[0], y_lim[1], (y_lim[1] - y_lim[0]) / 200)
|
|
2821
|
+
zdr_bins = np.arange(zdr_lim[0], zdr_lim[1] + 0.025, 0.025)
|
|
2822
|
+
|
|
2823
|
+
# Compute 2D histogram
|
|
2824
|
+
ds_stats = compute_2d_histogram(
|
|
2825
|
+
df,
|
|
2826
|
+
x=zdr,
|
|
2827
|
+
y="ADP/KDP",
|
|
2828
|
+
x_bins=zdr_bins,
|
|
2829
|
+
y_bins=y_bins,
|
|
2830
|
+
)
|
|
2831
|
+
|
|
2832
|
+
# Colormap & norm
|
|
2833
|
+
if cmap is None:
|
|
2834
|
+
cmap = plt.get_cmap("Spectral_r").copy()
|
|
2835
|
+
cmap.set_under(alpha=0)
|
|
2836
|
+
if norm is None:
|
|
2837
|
+
norm = LogNorm(1, None)
|
|
2838
|
+
|
|
2839
|
+
# Set default title if not provided
|
|
2840
|
+
if title is None:
|
|
2841
|
+
title = r"$A_{\mathrm{DP}} / K_{\mathrm{DP}}$ vs $Z_{\mathrm{DR}}$"
|
|
2842
|
+
|
|
2843
|
+
# Create figure if ax is None
|
|
2844
|
+
if ax is None:
|
|
2845
|
+
fig, ax = plt.subplots(1, 1, figsize=figsize, dpi=dpi)
|
|
2846
|
+
|
|
2847
|
+
# Plot 2D histogram
|
|
2848
|
+
p = ds_stats["count"].plot.pcolormesh(
|
|
2849
|
+
x=zdr,
|
|
2850
|
+
y="ADP/KDP",
|
|
2851
|
+
ax=ax,
|
|
2852
|
+
cmap=cmap,
|
|
2853
|
+
norm=norm,
|
|
2854
|
+
add_colorbar=add_colorbar,
|
|
2855
|
+
extend="max",
|
|
2856
|
+
cbar_kwargs={"label": "Counts"} if add_colorbar else {},
|
|
2857
|
+
)
|
|
2858
|
+
ax.set_xlabel(r"$Z_{\mathrm{DR}}$ [dB]")
|
|
2859
|
+
ax.set_ylabel(r"$A_{\mathrm{DP}} / K_{\mathrm{DP}}$ [dB deg$^{{-1}}$]")
|
|
2860
|
+
ax.set_xlim(*zdr_lim)
|
|
2861
|
+
ax.set_ylim(*y_lim)
|
|
2862
|
+
ax.set_title(title)
|
|
2863
|
+
|
|
2864
|
+
return p
|
|
2865
|
+
|
|
2866
|
+
|
|
2867
|
+
def plot_A_KDP_ZDR(
|
|
2868
|
+
df,
|
|
2869
|
+
a,
|
|
2870
|
+
kdp,
|
|
2871
|
+
zdr,
|
|
2872
|
+
y_lim=(0, 0.05),
|
|
2873
|
+
zdr_lim=(0, 3),
|
|
2874
|
+
cmap=None,
|
|
2875
|
+
norm=None,
|
|
2876
|
+
add_colorbar=True,
|
|
2877
|
+
pol="",
|
|
2878
|
+
title=None,
|
|
2879
|
+
ax=None,
|
|
2880
|
+
figsize=(8, 8),
|
|
2881
|
+
dpi=300,
|
|
2882
|
+
):
|
|
2883
|
+
"""Create a 2D histogram of k/KDP vs ZDR.
|
|
2884
|
+
|
|
2885
|
+
References
|
|
2886
|
+
----------
|
|
2887
|
+
Ryzhkov, A., P. Zhang, and J. Hu, 2025.
|
|
2888
|
+
Suggested Modifications for the S-Band Polarimetric Radar Rainfall Estimation Algorithm.
|
|
2889
|
+
J. Hydrometeor., 26, 1053-1062. https://doi.org/10.1175/JHM-D-25-0014.1.
|
|
2890
|
+
"""
|
|
2891
|
+
# Compute A/KDP
|
|
2892
|
+
df["A/KDP"] = df[a] / df[kdp]
|
|
2893
|
+
|
|
2894
|
+
# Bins & limits
|
|
2895
|
+
y_bins = np.arange(y_lim[0], y_lim[1], (y_lim[1] - y_lim[0]) / 200)
|
|
2896
|
+
x_bins = np.arange(zdr_lim[0], zdr_lim[1] + 0.025, 0.025)
|
|
2897
|
+
|
|
2898
|
+
# Compute 2D histogram
|
|
2899
|
+
ds_stats = compute_2d_histogram(
|
|
2900
|
+
df,
|
|
2901
|
+
x=zdr,
|
|
2902
|
+
y="A/KDP",
|
|
2903
|
+
x_bins=x_bins,
|
|
2904
|
+
y_bins=y_bins,
|
|
2905
|
+
)
|
|
2906
|
+
|
|
2907
|
+
# Colormap & norm
|
|
2908
|
+
if cmap is None:
|
|
2909
|
+
cmap = plt.get_cmap("Spectral_r").copy()
|
|
2910
|
+
cmap.set_under(alpha=0)
|
|
2911
|
+
if norm is None:
|
|
2912
|
+
norm = LogNorm(1, None)
|
|
2913
|
+
|
|
2914
|
+
# Define symbols
|
|
2915
|
+
a_symbol = get_symbol_str("A", pol)
|
|
2916
|
+
|
|
2917
|
+
# Set default title if not provided
|
|
2918
|
+
if title is None:
|
|
2919
|
+
title = rf"${a_symbol} / K_{{\mathrm{{DP}}}}$ vs $Z_{{\mathrm{{DR}}}}$"
|
|
2920
|
+
|
|
2921
|
+
# Create figure if ax is None
|
|
2922
|
+
if ax is None:
|
|
2923
|
+
fig, ax = plt.subplots(1, 1, figsize=figsize, dpi=dpi)
|
|
2924
|
+
|
|
2925
|
+
# Plot 2D histogram
|
|
2926
|
+
p = ds_stats["count"].plot.pcolormesh(
|
|
2927
|
+
x=zdr,
|
|
2928
|
+
y="A/KDP",
|
|
2929
|
+
ax=ax,
|
|
2930
|
+
cmap=cmap,
|
|
2931
|
+
norm=norm,
|
|
2932
|
+
add_colorbar=add_colorbar,
|
|
2933
|
+
extend="max",
|
|
2934
|
+
cbar_kwargs={"label": "Counts"} if add_colorbar else {},
|
|
2935
|
+
)
|
|
2936
|
+
ax.set_xlabel(r"$Z_{\mathrm{DR}}$ [dB]")
|
|
2937
|
+
ax.set_ylabel(rf"${a_symbol} / K_{{\mathrm{{DP}}}}$ [dB/deg]")
|
|
2938
|
+
ax.set_xlim(*zdr_lim)
|
|
2939
|
+
ax.set_ylim(*y_lim)
|
|
2940
|
+
ax.set_title(title)
|
|
2941
|
+
|
|
2942
|
+
return p
|
|
2943
|
+
|
|
2944
|
+
|
|
2945
|
+
def plot_KDP_Z_ZDR(
|
|
2946
|
+
df,
|
|
2947
|
+
kdp,
|
|
2948
|
+
z,
|
|
2949
|
+
zdr,
|
|
2950
|
+
y_lim=None,
|
|
2951
|
+
zdr_lim=(0, 5),
|
|
2952
|
+
z_linear=True,
|
|
2953
|
+
cmap=None,
|
|
2954
|
+
norm=None,
|
|
2955
|
+
add_colorbar=True,
|
|
2956
|
+
title=None,
|
|
2957
|
+
ax=None,
|
|
2958
|
+
figsize=(8, 8),
|
|
2959
|
+
dpi=300,
|
|
2960
|
+
):
|
|
2961
|
+
"""Create a 2D histogram of (KDP/Z) vs ZDR with log-scale y-axis (no fit)."""
|
|
2962
|
+
# Define y limits and KDP/Z
|
|
2963
|
+
if z_linear:
|
|
2964
|
+
df["KDP/Z"] = df[kdp] / disdrodb.idecibel(df[z])
|
|
2965
|
+
y_lim = (1e-6, 1e-3) if y_lim is None else y_lim
|
|
2966
|
+
y_label = r"$K_{\mathrm{DP}} / Z$ [deg km$^{-1}$ / mm$^6$ m$^{-3}$]"
|
|
2967
|
+
|
|
2968
|
+
else:
|
|
2969
|
+
df["KDP/Z"] = df[kdp] / df[z]
|
|
2970
|
+
y_lim = (1e-5, 1e-1) if y_lim is None else y_lim
|
|
2971
|
+
y_label = r"$K_{\mathrm{DP}} / Z$ [deg km$^{-1}$ / dBZ]"
|
|
2972
|
+
|
|
2973
|
+
# Define bins
|
|
2974
|
+
y_bins = log_arange(y_lim[0], y_lim[1], log_step=0.025, base=10)
|
|
2975
|
+
x_bins = np.arange(zdr_lim[0], zdr_lim[1] + 0.025, 0.025)
|
|
2976
|
+
|
|
2977
|
+
# Compute 2D histogram
|
|
2978
|
+
ds_stats = compute_2d_histogram(
|
|
2979
|
+
df,
|
|
2980
|
+
x=zdr,
|
|
2981
|
+
y="KDP/Z",
|
|
2982
|
+
x_bins=x_bins,
|
|
2983
|
+
y_bins=y_bins,
|
|
2984
|
+
)
|
|
2985
|
+
|
|
2986
|
+
# Colormap & norm
|
|
2987
|
+
if cmap is None:
|
|
2988
|
+
cmap = plt.get_cmap("Spectral_r").copy()
|
|
2989
|
+
cmap.set_under(alpha=0)
|
|
2990
|
+
if norm is None:
|
|
2991
|
+
norm = LogNorm(1, None)
|
|
2992
|
+
|
|
2993
|
+
# Set default title if not provided
|
|
2994
|
+
if title is None:
|
|
2995
|
+
title = r"$K_{\mathrm{DP}}/Z$ vs $Z_{\mathrm{DR}}$"
|
|
2996
|
+
|
|
2997
|
+
# Create figure if ax is None
|
|
2998
|
+
if ax is None:
|
|
2999
|
+
fig, ax = plt.subplots(1, 1, figsize=figsize, dpi=dpi)
|
|
3000
|
+
|
|
3001
|
+
# Plot 2D histogram
|
|
3002
|
+
p = ds_stats["count"].plot.pcolormesh(
|
|
3003
|
+
x=zdr,
|
|
3004
|
+
y="KDP/Z",
|
|
3005
|
+
ax=ax,
|
|
3006
|
+
cmap=cmap,
|
|
3007
|
+
norm=norm,
|
|
3008
|
+
add_colorbar=add_colorbar,
|
|
3009
|
+
extend="max",
|
|
3010
|
+
yscale="log",
|
|
3011
|
+
cbar_kwargs={"label": "Counts"} if add_colorbar else {},
|
|
3012
|
+
)
|
|
3013
|
+
ax.set_xlabel(r"$Z_{\mathrm{DR}}$ [dB]")
|
|
3014
|
+
ax.set_ylabel(y_label)
|
|
3015
|
+
ax.set_xlim(*zdr_lim)
|
|
3016
|
+
ax.set_ylim(*y_lim)
|
|
3017
|
+
ax.set_title(title)
|
|
3018
|
+
return p
|
|
3019
|
+
|
|
3020
|
+
|
|
3021
|
+
def plot_KED_R(
|
|
3022
|
+
df,
|
|
3023
|
+
log_r=True,
|
|
3024
|
+
log_ked=False,
|
|
3025
|
+
add_fit=True,
|
|
3026
|
+
cmap=None,
|
|
3027
|
+
norm=None,
|
|
3028
|
+
add_colorbar=True,
|
|
3029
|
+
title=None,
|
|
3030
|
+
ax=None,
|
|
3031
|
+
legend_fontsize=14,
|
|
3032
|
+
figsize=(8, 8),
|
|
3033
|
+
dpi=300,
|
|
3034
|
+
):
|
|
3035
|
+
"""Create a 2D histogram of KED vs R."""
|
|
3036
|
+
if log_r:
|
|
3037
|
+
r_bins = log_arange(0.1, 500, log_step=0.05, base=10)
|
|
3038
|
+
r_lims = (0.1, 500)
|
|
3039
|
+
r_ticks = [0.1, 1, 10, 50, 100, 500]
|
|
3040
|
+
xscale = "log"
|
|
3041
|
+
else:
|
|
3042
|
+
r_bins = np.arange(0, 500, step=2)
|
|
3043
|
+
r_lims = (0, 500)
|
|
3044
|
+
r_ticks = None
|
|
3045
|
+
xscale = "linear"
|
|
3046
|
+
if log_ked:
|
|
3047
|
+
ked_bins = log_arange(1, 50, log_step=0.025, base=10)
|
|
3048
|
+
ked_lims = (1, 50)
|
|
3049
|
+
ked_ticks = [1, 10, 50]
|
|
3050
|
+
yscale = "log"
|
|
3051
|
+
else:
|
|
3052
|
+
ked_bins = np.arange(0, 50, step=1)
|
|
3053
|
+
ked_lims = (0, 50)
|
|
3054
|
+
ked_ticks = None
|
|
3055
|
+
yscale = "linear"
|
|
3056
|
+
|
|
3057
|
+
# Compute 2d histogram
|
|
3058
|
+
ds_stats = compute_2d_histogram(
|
|
3059
|
+
df,
|
|
3060
|
+
x="R",
|
|
3061
|
+
y="KED",
|
|
3062
|
+
x_bins=r_bins,
|
|
3063
|
+
y_bins=ked_bins,
|
|
3064
|
+
)
|
|
3065
|
+
|
|
3066
|
+
# Define colormap and norm
|
|
3067
|
+
if cmap is None:
|
|
3068
|
+
cmap = plt.get_cmap("Spectral_r").copy()
|
|
3069
|
+
cmap.set_under(alpha=0)
|
|
3070
|
+
norm = LogNorm(1, None) if norm is None else norm
|
|
3071
|
+
|
|
3072
|
+
# Set default title if not provided
|
|
3073
|
+
if title is None:
|
|
3074
|
+
title = "KED vs R"
|
|
3075
|
+
|
|
3076
|
+
# Create figure if ax is None
|
|
3077
|
+
if ax is None:
|
|
3078
|
+
fig, ax = plt.subplots(1, 1, figsize=figsize, dpi=dpi)
|
|
3079
|
+
|
|
3080
|
+
# Plot 2D histogram
|
|
3081
|
+
p = ds_stats["count"].plot.pcolormesh(
|
|
3082
|
+
x="R",
|
|
3083
|
+
y="KED",
|
|
3084
|
+
ax=ax,
|
|
3085
|
+
cmap=cmap,
|
|
3086
|
+
norm=norm,
|
|
3087
|
+
add_colorbar=add_colorbar,
|
|
3088
|
+
extend="max",
|
|
3089
|
+
xscale=xscale,
|
|
3090
|
+
yscale=yscale,
|
|
3091
|
+
cbar_kwargs={"label": "Counts"} if add_colorbar else {},
|
|
3092
|
+
)
|
|
3093
|
+
ax.set_xlabel(r"$R$ [mm h$^{-1}$]")
|
|
3094
|
+
ax.set_ylabel(r"KED [J m$^{-2}$ mm$^{-1}$]")
|
|
3095
|
+
ax.set_xlim(*r_lims)
|
|
3096
|
+
ax.set_ylim(*ked_lims)
|
|
3097
|
+
if r_ticks is not None:
|
|
3098
|
+
ax.set_xticks(r_ticks)
|
|
3099
|
+
ax.set_xticklabels([str(v) for v in r_ticks])
|
|
3100
|
+
if ked_ticks is not None:
|
|
3101
|
+
ax.set_yticks(ked_ticks)
|
|
3102
|
+
ax.set_yticklabels([str(v) for v in ked_ticks])
|
|
3103
|
+
ax.set_title("KED vs R")
|
|
3104
|
+
# Fit and plot a powerlaw
|
|
3105
|
+
if add_fit:
|
|
3106
|
+
# Fit a power law KED = a * R**b
|
|
3107
|
+
(a, b), _ = fit_powerlaw(
|
|
3108
|
+
x=df["R"],
|
|
3109
|
+
y=df["KED"],
|
|
3110
|
+
xbins=r_bins,
|
|
3111
|
+
x_in_db=False,
|
|
3112
|
+
)
|
|
3113
|
+
# Invert for R = A * KED**B
|
|
3114
|
+
A, B = inverse_powerlaw_parameters(a, b)
|
|
3115
|
+
# Define legend string
|
|
3116
|
+
a_str = _define_coeff_string(a)
|
|
3117
|
+
A_str = _define_coeff_string(A)
|
|
3118
|
+
legend_str = rf"$\mathrm{{KED}} = {a_str}\,R^{{{b:.2f}}}$" "\n" rf"$R = {A_str}\,\mathrm{{KED}}^{{{B:.2f}}}$"
|
|
3119
|
+
# Get power law predictions
|
|
3120
|
+
x_pred = np.arange(r_lims[0], r_lims[1])
|
|
3121
|
+
y_pred = predict_from_powerlaw(x_pred, a=a, b=b)
|
|
3122
|
+
# Add fitted powerlaw
|
|
3123
|
+
ax.plot(x_pred, y_pred, linestyle="dashed", color="black")
|
|
3124
|
+
# Add legend
|
|
3125
|
+
legend_bbox_dict = {"facecolor": "white", "edgecolor": "black", "alpha": 0.7}
|
|
3126
|
+
ax.text(
|
|
3127
|
+
0.05,
|
|
3128
|
+
0.95,
|
|
3129
|
+
legend_str,
|
|
3130
|
+
transform=ax.transAxes,
|
|
3131
|
+
ha="left",
|
|
3132
|
+
va="top",
|
|
3133
|
+
fontsize=legend_fontsize,
|
|
3134
|
+
bbox=legend_bbox_dict,
|
|
3135
|
+
)
|
|
3136
|
+
|
|
3137
|
+
return p
|
|
3138
|
+
|
|
3139
|
+
|
|
3140
|
+
def plot_KEF_R(
|
|
3141
|
+
df,
|
|
3142
|
+
log_r=True,
|
|
3143
|
+
log_kef=True,
|
|
3144
|
+
add_fit=True,
|
|
3145
|
+
cmap=None,
|
|
3146
|
+
norm=None,
|
|
3147
|
+
add_colorbar=True,
|
|
3148
|
+
title=None,
|
|
3149
|
+
ax=None,
|
|
3150
|
+
legend_fontsize=14,
|
|
3151
|
+
figsize=(8, 8),
|
|
3152
|
+
dpi=300,
|
|
3153
|
+
):
|
|
3154
|
+
"""Create a 2D histogram of KEF vs R."""
|
|
3155
|
+
if log_r:
|
|
3156
|
+
r_bins = log_arange(0.1, 500, log_step=0.05, base=10)
|
|
3157
|
+
r_lims = (0.1, 500)
|
|
3158
|
+
r_ticks = [0.1, 1, 10, 50, 100, 500]
|
|
3159
|
+
xscale = "log"
|
|
3160
|
+
else:
|
|
3161
|
+
r_bins = np.arange(0, 500, step=2)
|
|
3162
|
+
r_lims = (0, 500)
|
|
3163
|
+
r_ticks = None
|
|
3164
|
+
xscale = "linear"
|
|
3165
|
+
if log_kef:
|
|
3166
|
+
kef_bins = log_arange(0.1, 10_000, log_step=0.05, base=10)
|
|
3167
|
+
kef_lims = (0.1, 10_000)
|
|
3168
|
+
kef_ticks = [0.1, 1, 10, 100, 1000, 10000]
|
|
3169
|
+
yscale = "log"
|
|
3170
|
+
else:
|
|
3171
|
+
kef_bins = np.arange(0, 5000, step=50)
|
|
3172
|
+
kef_lims = (0, 5000)
|
|
3173
|
+
kef_ticks = None
|
|
3174
|
+
yscale = "linear"
|
|
3175
|
+
|
|
3176
|
+
# Compute 2d histogram
|
|
3177
|
+
ds_stats = compute_2d_histogram(
|
|
3178
|
+
df,
|
|
3179
|
+
x="R",
|
|
3180
|
+
y="KEF",
|
|
3181
|
+
x_bins=r_bins,
|
|
3182
|
+
y_bins=kef_bins,
|
|
3183
|
+
)
|
|
3184
|
+
|
|
3185
|
+
# Define colormap and norm
|
|
3186
|
+
if cmap is None:
|
|
3187
|
+
cmap = plt.get_cmap("Spectral_r").copy()
|
|
3188
|
+
cmap.set_under(alpha=0)
|
|
3189
|
+
norm = LogNorm(1, None) if norm is None else norm
|
|
3190
|
+
|
|
3191
|
+
# Set default title if not provided
|
|
3192
|
+
if title is None:
|
|
3193
|
+
title = "KEF vs R"
|
|
3194
|
+
|
|
3195
|
+
# Create figure if ax is None
|
|
3196
|
+
if ax is None:
|
|
3197
|
+
fig, ax = plt.subplots(1, 1, figsize=figsize, dpi=dpi)
|
|
3198
|
+
|
|
3199
|
+
# Plot 2D histogram
|
|
3200
|
+
p = ds_stats["count"].plot.pcolormesh(
|
|
3201
|
+
x="R",
|
|
3202
|
+
y="KEF",
|
|
3203
|
+
ax=ax,
|
|
3204
|
+
cmap=cmap,
|
|
3205
|
+
norm=norm,
|
|
3206
|
+
add_colorbar=add_colorbar,
|
|
3207
|
+
extend="max",
|
|
3208
|
+
xscale=xscale,
|
|
3209
|
+
yscale=yscale,
|
|
3210
|
+
cbar_kwargs={"label": "Counts"} if add_colorbar else {},
|
|
3211
|
+
)
|
|
3212
|
+
ax.set_xlabel(r"$R$ [mm h$^{-1}$]")
|
|
3213
|
+
ax.set_ylabel(r"KEF [J m$^{-2}$ h$^{-1}$]")
|
|
3214
|
+
ax.set_xlim(*r_lims)
|
|
3215
|
+
ax.set_ylim(*kef_lims)
|
|
3216
|
+
if r_ticks is not None:
|
|
3217
|
+
ax.set_xticks(r_ticks)
|
|
3218
|
+
ax.set_xticklabels([str(v) for v in r_ticks])
|
|
3219
|
+
if kef_ticks is not None:
|
|
3220
|
+
ax.set_yticks(kef_ticks)
|
|
3221
|
+
ax.set_yticklabels([str(v) for v in kef_ticks])
|
|
3222
|
+
ax.set_title(title)
|
|
3223
|
+
|
|
3224
|
+
# Fit and plot the power law
|
|
3225
|
+
# - Alternative fit model: a + I *(1 - b*exp(c*I)) (a is upper limit)
|
|
3226
|
+
if add_fit:
|
|
3227
|
+
# Fit power law KEF = a * R ** b
|
|
3228
|
+
(a, b), _ = fit_powerlaw(
|
|
3229
|
+
x=df["R"],
|
|
3230
|
+
y=df["KEF"],
|
|
3231
|
+
xbins=r_bins,
|
|
3232
|
+
x_in_db=False,
|
|
3233
|
+
)
|
|
3234
|
+
# Invert parameters for R = A * KEF ** B
|
|
3235
|
+
A, B = inverse_powerlaw_parameters(a, b)
|
|
3236
|
+
# Define legend string
|
|
3237
|
+
a_str = _define_coeff_string(a)
|
|
3238
|
+
A_str = _define_coeff_string(A)
|
|
3239
|
+
legend_str = rf"$\mathrm{{KEF}} = {a_str}\,R^{{{b:.2f}}}$" "\n" rf"$R = {A_str}\,\mathrm{{KEF}}^{{{B:.2f}}}$"
|
|
3240
|
+
# Get power law predictions
|
|
3241
|
+
x_pred = np.arange(*r_lims)
|
|
3242
|
+
kef_pred = predict_from_powerlaw(x_pred, a=a, b=b)
|
|
3243
|
+
# Add fitted powerlaw
|
|
3244
|
+
ax.plot(x_pred, kef_pred, linestyle="dashed", color="black")
|
|
3245
|
+
# Add legend
|
|
3246
|
+
legend_bbox_dict = {"facecolor": "white", "edgecolor": "black", "alpha": 0.7}
|
|
3247
|
+
ax.text(
|
|
3248
|
+
0.05,
|
|
3249
|
+
0.95,
|
|
3250
|
+
legend_str,
|
|
3251
|
+
transform=ax.transAxes,
|
|
3252
|
+
ha="left",
|
|
3253
|
+
va="top",
|
|
3254
|
+
fontsize=legend_fontsize,
|
|
3255
|
+
bbox=legend_bbox_dict,
|
|
3256
|
+
)
|
|
3257
|
+
return p
|
|
3258
|
+
|
|
3259
|
+
|
|
3260
|
+
def plot_KEF_Z(
|
|
3261
|
+
df,
|
|
3262
|
+
z="Z",
|
|
3263
|
+
log_kef=True,
|
|
3264
|
+
add_fit=True,
|
|
3265
|
+
pol="",
|
|
3266
|
+
cmap=None,
|
|
3267
|
+
norm=None,
|
|
3268
|
+
add_colorbar=True,
|
|
3269
|
+
title=None,
|
|
3270
|
+
ax=None,
|
|
3271
|
+
legend_fontsize=14,
|
|
3272
|
+
figsize=(8, 8),
|
|
3273
|
+
dpi=300,
|
|
3274
|
+
):
|
|
3275
|
+
"""Create a 2D histogram of KEF vs Z."""
|
|
3276
|
+
# Define limits and bins
|
|
3277
|
+
z_lims = (0, 70)
|
|
3278
|
+
z_bins = np.arange(*z_lims, step=1)
|
|
3279
|
+
|
|
3280
|
+
if log_kef:
|
|
3281
|
+
kef_lims = (0.1, 10_000)
|
|
3282
|
+
kef_bins = log_arange(*kef_lims, log_step=0.05, base=10)
|
|
3283
|
+
kef_ticks = [0.1, 1, 10, 100, 1000, 10000]
|
|
3284
|
+
yscale = "log"
|
|
3285
|
+
else:
|
|
3286
|
+
kef_lims = (0, 5000)
|
|
3287
|
+
kef_bins = np.arange(*kef_lims, step=50)
|
|
3288
|
+
kef_ticks = None
|
|
3289
|
+
yscale = "linear"
|
|
3290
|
+
|
|
3291
|
+
# Compute 2d histogram
|
|
3292
|
+
ds_stats = compute_2d_histogram(
|
|
3293
|
+
df,
|
|
3294
|
+
x=z,
|
|
3295
|
+
y="KEF",
|
|
3296
|
+
x_bins=z_bins,
|
|
3297
|
+
y_bins=kef_bins,
|
|
3298
|
+
)
|
|
3299
|
+
|
|
3300
|
+
# Define colormap and norm
|
|
3301
|
+
if cmap is None:
|
|
3302
|
+
cmap = plt.get_cmap("Spectral_r").copy()
|
|
3303
|
+
cmap.set_under(alpha=0)
|
|
3304
|
+
norm = LogNorm(1, None) if norm is None else norm
|
|
3305
|
+
|
|
3306
|
+
# Define symbols
|
|
3307
|
+
z_symbol = get_symbol_str("Z", pol)
|
|
3308
|
+
z_lower_symbol = get_symbol_str("z", pol)
|
|
3309
|
+
|
|
3310
|
+
# Set default title if not provided
|
|
3311
|
+
if title is None:
|
|
3312
|
+
title = rf"KEF vs ${z_symbol}$"
|
|
3313
|
+
|
|
3314
|
+
# Create figure if ax is None
|
|
3315
|
+
if ax is None:
|
|
3316
|
+
fig, ax = plt.subplots(1, 1, figsize=figsize, dpi=dpi)
|
|
3317
|
+
|
|
3318
|
+
# Plot 2D histogram
|
|
3319
|
+
p = ds_stats["count"].plot.pcolormesh(
|
|
3320
|
+
x=z,
|
|
3321
|
+
y="KEF",
|
|
3322
|
+
ax=ax,
|
|
3323
|
+
cmap=cmap,
|
|
3324
|
+
norm=norm,
|
|
3325
|
+
add_colorbar=add_colorbar,
|
|
3326
|
+
extend="max",
|
|
3327
|
+
yscale=yscale,
|
|
3328
|
+
cbar_kwargs={"label": "Counts"} if add_colorbar else {},
|
|
3329
|
+
)
|
|
3330
|
+
ax.set_xlabel(rf"${z_symbol}$ [dB]")
|
|
3331
|
+
ax.set_ylabel(r"KEF [$J$ m$^{-2}$ h$^{-1}$]")
|
|
3332
|
+
ax.set_xlim(*z_lims)
|
|
3333
|
+
ax.set_ylim(*kef_lims)
|
|
3334
|
+
if kef_ticks is not None:
|
|
3335
|
+
ax.set_yticks(kef_ticks)
|
|
3336
|
+
ax.set_yticklabels([str(v) for v in kef_ticks])
|
|
3337
|
+
ax.set_title(title)
|
|
3338
|
+
|
|
3339
|
+
# Fit and plot the powerlaw
|
|
3340
|
+
if add_fit:
|
|
3341
|
+
# Fit power law KEF = a * Z ** b
|
|
3342
|
+
(a, b), _ = fit_powerlaw(
|
|
3343
|
+
x=df[z],
|
|
3344
|
+
y=df["KEF"],
|
|
3345
|
+
xbins=z_bins,
|
|
3346
|
+
x_in_db=True,
|
|
3347
|
+
)
|
|
3348
|
+
# Invert parameters for Z = A * KEF ** B
|
|
3349
|
+
A, B = inverse_powerlaw_parameters(a, b)
|
|
3350
|
+
# Define legend string
|
|
3351
|
+
a_str = _define_coeff_string(a)
|
|
3352
|
+
A_str = _define_coeff_string(A)
|
|
3353
|
+
legend_str = (
|
|
3354
|
+
rf"$\mathrm{{KEF}} = {a_str}\;{z_lower_symbol}^{{{b:.2f}}}$"
|
|
3355
|
+
"\n"
|
|
3356
|
+
rf"${z_lower_symbol} = {A_str}\;\mathrm{{KEF}}^{{{B:.2f}}}$"
|
|
3357
|
+
)
|
|
3358
|
+
# Get power law predictions
|
|
3359
|
+
x_pred = np.arange(*z_lims)
|
|
3360
|
+
x_pred_linear = disdrodb.idecibel(x_pred)
|
|
3361
|
+
kef_pred = predict_from_powerlaw(x_pred_linear, a=a, b=b)
|
|
3362
|
+
# Add fitted powerlaw
|
|
3363
|
+
ax.plot(x_pred, kef_pred, linestyle="dashed", color="black")
|
|
3364
|
+
# Add legend
|
|
3365
|
+
legend_bbox_dict = {"facecolor": "white", "edgecolor": "black", "alpha": 0.7}
|
|
3366
|
+
ax.text(
|
|
3367
|
+
0.05,
|
|
3368
|
+
0.95,
|
|
3369
|
+
legend_str,
|
|
3370
|
+
transform=ax.transAxes,
|
|
3371
|
+
ha="left",
|
|
3372
|
+
va="top",
|
|
3373
|
+
fontsize=legend_fontsize,
|
|
3374
|
+
bbox=legend_bbox_dict,
|
|
3375
|
+
)
|
|
3376
|
+
|
|
3377
|
+
return p
|
|
3378
|
+
|
|
3379
|
+
|
|
3380
|
+
def plot_TKE_Z(
|
|
3381
|
+
df,
|
|
3382
|
+
z="Z",
|
|
3383
|
+
log_tke=True,
|
|
3384
|
+
add_fit=True,
|
|
3385
|
+
cmap=None,
|
|
3386
|
+
norm=None,
|
|
3387
|
+
add_colorbar=True,
|
|
3388
|
+
title=None,
|
|
3389
|
+
ax=None,
|
|
3390
|
+
legend_fontsize=14,
|
|
3391
|
+
figsize=(8, 8),
|
|
3392
|
+
dpi=300,
|
|
3393
|
+
):
|
|
3394
|
+
"""Create a 2D histogram of TKE vs Z."""
|
|
3395
|
+
z_bins = np.arange(0, 70, step=1)
|
|
3396
|
+
z_lims = (0, 70)
|
|
3397
|
+
if log_tke:
|
|
3398
|
+
tke_bins = log_arange(0.01, 500, log_step=0.05, base=10)
|
|
3399
|
+
tke_lims = (0.01, 200)
|
|
3400
|
+
tke_ticks = [0.01, 0.1, 1, 10, 100, 200]
|
|
3401
|
+
yscale = "log"
|
|
3402
|
+
else:
|
|
3403
|
+
tke_bins = np.arange(0, 200, step=1)
|
|
3404
|
+
tke_lims = (0, 200)
|
|
3405
|
+
tke_ticks = None
|
|
3406
|
+
yscale = "linear"
|
|
3407
|
+
|
|
3408
|
+
# Compute 2d histogram
|
|
3409
|
+
ds_stats = compute_2d_histogram(
|
|
3410
|
+
df,
|
|
3411
|
+
x=z,
|
|
3412
|
+
y="TKE",
|
|
3413
|
+
x_bins=z_bins,
|
|
3414
|
+
y_bins=tke_bins,
|
|
3415
|
+
)
|
|
3416
|
+
|
|
3417
|
+
# Define colormap and norm
|
|
3418
|
+
if cmap is None:
|
|
3419
|
+
cmap = plt.get_cmap("Spectral_r").copy()
|
|
3420
|
+
cmap.set_under(alpha=0)
|
|
3421
|
+
norm = LogNorm(1, None) if norm is None else norm
|
|
3422
|
+
|
|
3423
|
+
# Set default title if not provided
|
|
3424
|
+
if title is None:
|
|
3425
|
+
title = "TKE vs Z"
|
|
3426
|
+
|
|
3427
|
+
# Create figure if ax is None
|
|
3428
|
+
if ax is None:
|
|
3429
|
+
fig, ax = plt.subplots(1, 1, figsize=figsize, dpi=dpi)
|
|
3430
|
+
|
|
3431
|
+
# Plot 2D histogram
|
|
3432
|
+
p = ds_stats["count"].plot.pcolormesh(
|
|
3433
|
+
x=z,
|
|
3434
|
+
y="TKE",
|
|
3435
|
+
ax=ax,
|
|
3436
|
+
cmap=cmap,
|
|
3437
|
+
norm=norm,
|
|
3438
|
+
add_colorbar=add_colorbar,
|
|
3439
|
+
extend="max",
|
|
3440
|
+
yscale=yscale,
|
|
3441
|
+
cbar_kwargs={"label": "Counts"} if add_colorbar else {},
|
|
3442
|
+
)
|
|
3443
|
+
ax.set_xlabel(r"$Z$ [dB]")
|
|
3444
|
+
ax.set_ylabel(r"TKE [$J$ m$^{-2}$]")
|
|
3445
|
+
ax.set_xlim(*z_lims)
|
|
3446
|
+
ax.set_ylim(*tke_lims)
|
|
3447
|
+
if tke_ticks is not None:
|
|
3448
|
+
ax.set_yticks(tke_ticks)
|
|
3449
|
+
ax.set_yticklabels([str(v) for v in tke_ticks])
|
|
3450
|
+
ax.set_title(title)
|
|
3451
|
+
|
|
3452
|
+
# Fit and plot the powerlaw
|
|
3453
|
+
if add_fit:
|
|
3454
|
+
# Fit power law TKE = a * Z ** b
|
|
3455
|
+
(a, b), _ = fit_powerlaw(
|
|
3456
|
+
x=df[z],
|
|
3457
|
+
y=df["TKE"],
|
|
3458
|
+
xbins=z_bins,
|
|
3459
|
+
x_in_db=True,
|
|
3460
|
+
)
|
|
3461
|
+
# Invert parameters for Z = A * KEF ** B
|
|
3462
|
+
A, B = inverse_powerlaw_parameters(a, b)
|
|
3463
|
+
# Define legend string
|
|
3464
|
+
a_str = _define_coeff_string(a)
|
|
3465
|
+
A_str = _define_coeff_string(A)
|
|
3466
|
+
legend_str = rf"$\mathrm{{TKE}} = {a_str}\;z^{{{b:.2f}}}$" "\n" rf"$z = {A_str}\;\mathrm{{TKE}}^{{{B:.2f}}}$"
|
|
3467
|
+
# Get power law predictions
|
|
3468
|
+
x_pred = np.arange(*z_lims)
|
|
3469
|
+
x_pred_linear = disdrodb.idecibel(x_pred)
|
|
3470
|
+
y_pred = predict_from_powerlaw(x_pred_linear, a=a, b=b)
|
|
3471
|
+
# Add fitted powerlaw
|
|
3472
|
+
ax.plot(x_pred, y_pred, linestyle="dashed", color="black")
|
|
3473
|
+
# Add legend
|
|
3474
|
+
legend_bbox_dict = {"facecolor": "white", "edgecolor": "black", "alpha": 0.7}
|
|
3475
|
+
ax.text(
|
|
3476
|
+
0.05,
|
|
3477
|
+
0.95,
|
|
3478
|
+
legend_str,
|
|
3479
|
+
transform=ax.transAxes,
|
|
3480
|
+
ha="left",
|
|
3481
|
+
va="top",
|
|
3482
|
+
fontsize=legend_fontsize,
|
|
3483
|
+
bbox=legend_bbox_dict,
|
|
3484
|
+
)
|
|
3485
|
+
|
|
3486
|
+
return p
|
|
3487
|
+
|
|
3488
|
+
|
|
3489
|
+
####-----------------------------------------------------------------------.
|
|
3490
|
+
#### Radar and Kinetic Energy Summary figures
|
|
3491
|
+
|
|
3492
|
+
|
|
3493
|
+
def plot_radar_relationships(df, band):
|
|
3494
|
+
"""Create 3x3 multipanel figure with radar relationships."""
|
|
3495
|
+
# Check band
|
|
3496
|
+
if band not in {"X", "C", "S"}:
|
|
3497
|
+
raise ValueError("Plotting function developed only for bands: 'X', 'C', 'S'.")
|
|
3498
|
+
|
|
3499
|
+
# Define columns
|
|
3500
|
+
z = f"DBZH_{band}"
|
|
3501
|
+
zdr = f"ZDR_{band}"
|
|
3502
|
+
kdp = f"KDP_{band}"
|
|
3503
|
+
a = f"AH_{band}"
|
|
3504
|
+
adp = f"ADP_{band}"
|
|
3505
|
+
|
|
3506
|
+
# Define limits
|
|
3507
|
+
adp_kdp_ylim_dict = {
|
|
3508
|
+
"X": (0.0, 0.05),
|
|
3509
|
+
"C": (0.0, 0.05),
|
|
3510
|
+
"S": (0.0, 0.015),
|
|
3511
|
+
}
|
|
3512
|
+
adp_kdp_ylim = adp_kdp_ylim_dict[band]
|
|
3513
|
+
|
|
3514
|
+
a_ylim_dict = {
|
|
3515
|
+
"S": (0.00001, 1),
|
|
3516
|
+
"C": (0.0001, 10),
|
|
3517
|
+
"X": (0.0001, 10),
|
|
3518
|
+
}
|
|
3519
|
+
a_ylim = a_ylim_dict[band]
|
|
3520
|
+
|
|
3521
|
+
# Define plotting settings
|
|
3522
|
+
add_colorbar = False
|
|
3523
|
+
norm = LogNorm(1, None)
|
|
3524
|
+
legend_fontsize = 12
|
|
3525
|
+
|
|
3526
|
+
# Initialize figure
|
|
3527
|
+
fig = plt.figure(figsize=(10, 12), dpi=300) # Slightly taller to accommodate colorbar
|
|
3528
|
+
# fig.suptitle(f'C-band Polarimetric Radar Variables Relationships', fontsize=16, y=0.96)
|
|
3529
|
+
|
|
3530
|
+
# Create gridspec with space for colorbar at bottom
|
|
3531
|
+
gs = GridSpec(
|
|
3532
|
+
4,
|
|
3533
|
+
3,
|
|
3534
|
+
figure=fig,
|
|
3535
|
+
height_ratios=[1, 1, 1, 0.05],
|
|
3536
|
+
hspace=0.35,
|
|
3537
|
+
wspace=0.35,
|
|
3538
|
+
left=0.05,
|
|
3539
|
+
right=0.95,
|
|
3540
|
+
top=0.93,
|
|
3541
|
+
bottom=0.08,
|
|
3542
|
+
)
|
|
3543
|
+
|
|
3544
|
+
# Create subplots using gridspec
|
|
3545
|
+
axes = []
|
|
3546
|
+
for i in range(3):
|
|
3547
|
+
for j in range(3):
|
|
3548
|
+
ax = fig.add_subplot(gs[i, j])
|
|
3549
|
+
axes.append(ax)
|
|
3550
|
+
|
|
3551
|
+
# Flatten axes for easier indexing
|
|
3552
|
+
ax = np.array(axes).flatten()
|
|
3553
|
+
|
|
3554
|
+
# - R vs Z_H
|
|
3555
|
+
p = plot_R_Z(
|
|
3556
|
+
df,
|
|
3557
|
+
z=z,
|
|
3558
|
+
r="R",
|
|
3559
|
+
pol="H",
|
|
3560
|
+
norm=norm,
|
|
3561
|
+
add_colorbar=add_colorbar,
|
|
3562
|
+
legend_fontsize=legend_fontsize,
|
|
3563
|
+
ax=ax[0],
|
|
3564
|
+
)
|
|
3565
|
+
|
|
3566
|
+
# - Define norm for other plots
|
|
3567
|
+
norm = p.norm
|
|
3568
|
+
|
|
3569
|
+
# - R vs K_DP
|
|
3570
|
+
plot_R_KDP(
|
|
3571
|
+
df,
|
|
3572
|
+
kdp=kdp,
|
|
3573
|
+
r="R",
|
|
3574
|
+
log_scale=True,
|
|
3575
|
+
legend_fontsize=legend_fontsize,
|
|
3576
|
+
norm=norm,
|
|
3577
|
+
add_colorbar=add_colorbar,
|
|
3578
|
+
ax=ax[1],
|
|
3579
|
+
)
|
|
3580
|
+
|
|
3581
|
+
# - Z_DR vs Z_H
|
|
3582
|
+
plot_ZDR_Z(
|
|
3583
|
+
df,
|
|
3584
|
+
z=z,
|
|
3585
|
+
zdr=zdr,
|
|
3586
|
+
pol="H",
|
|
3587
|
+
legend_fontsize=legend_fontsize,
|
|
3588
|
+
norm=norm,
|
|
3589
|
+
add_colorbar=add_colorbar,
|
|
3590
|
+
ax=ax[2],
|
|
3591
|
+
)
|
|
3592
|
+
|
|
3593
|
+
# - A_H vs Z_H
|
|
3594
|
+
plot_A_Z(
|
|
3595
|
+
df,
|
|
3596
|
+
a=a,
|
|
3597
|
+
z=z,
|
|
3598
|
+
pol="H",
|
|
3599
|
+
legend_fontsize=legend_fontsize,
|
|
3600
|
+
norm=norm,
|
|
3601
|
+
add_colorbar=add_colorbar,
|
|
3602
|
+
a_lim=a_ylim,
|
|
3603
|
+
ax=ax[3],
|
|
3604
|
+
)
|
|
3605
|
+
|
|
3606
|
+
# - A_H vs K_DP
|
|
3607
|
+
plot_A_KDP(
|
|
3608
|
+
df,
|
|
3609
|
+
a=a,
|
|
3610
|
+
kdp=kdp,
|
|
3611
|
+
pol="H",
|
|
3612
|
+
legend_fontsize=legend_fontsize,
|
|
3613
|
+
norm=norm,
|
|
3614
|
+
add_colorbar=add_colorbar,
|
|
3615
|
+
ax=ax[4],
|
|
3616
|
+
)
|
|
3617
|
+
# plot_A_KDP(df, a=a, kdp=kdp, log_a=True, log_kdp=True,
|
|
3618
|
+
# legend_fontsize=legend_fontsize, norm=norm, add_colorbar=add_colorbar, ax=ax[4]))
|
|
3619
|
+
|
|
3620
|
+
# - A_H vs R
|
|
3621
|
+
plot_A_R(df, a=a, r="R", pol="H", legend_fontsize=legend_fontsize, norm=norm, add_colorbar=add_colorbar, ax=ax[5])
|
|
3622
|
+
|
|
3623
|
+
# - K_DP vs Z_H
|
|
3624
|
+
plot_KDP_Z(
|
|
3625
|
+
df,
|
|
3626
|
+
kdp=kdp,
|
|
3627
|
+
z=z,
|
|
3628
|
+
pol="H",
|
|
3629
|
+
legend_fontsize=legend_fontsize,
|
|
3630
|
+
norm=norm,
|
|
3631
|
+
add_colorbar=add_colorbar,
|
|
3632
|
+
log_kdp=True,
|
|
3633
|
+
ax=ax[6],
|
|
3634
|
+
)
|
|
3635
|
+
|
|
3636
|
+
# - A_DP/K_DP vs Z_DR
|
|
3637
|
+
plot_ADP_KDP_ZDR(df, adp=adp, kdp=kdp, zdr=zdr, norm=norm, add_colorbar=add_colorbar, y_lim=adp_kdp_ylim, ax=ax[7])
|
|
3638
|
+
# plot_A_KDP_ZDR(df, a=a, kdp=kdp, zdr=zdr, y_lim=(0, 0.3), norm=norm, add_colorbar=add_colorbar)
|
|
3639
|
+
|
|
3640
|
+
# - K_DP/Z vs Z_DR
|
|
3641
|
+
p = plot_KDP_Z_ZDR(df, kdp=kdp, z=z, zdr=zdr, norm=norm, add_colorbar=add_colorbar, z_linear=False, ax=ax[8])
|
|
3642
|
+
# plot_KDP_Z_ZDR(df, kdp=kdp, z=z, zdr=zdr, norm=norm, add_colorbar=add_colorbar, z_linear=True, ax=ax[8])
|
|
3643
|
+
|
|
3644
|
+
# - Add colorbar
|
|
3645
|
+
cax = fig.add_subplot(gs[3, :]) # Spans all columns in the bottom row
|
|
3646
|
+
cbar = plt.colorbar(p, cax=cax, orientation="horizontal", extend="max", extendfrac=0.025)
|
|
3647
|
+
cbar.ax.set_aspect(0.1)
|
|
3648
|
+
cbar.set_label("Counts", fontsize=12, labelpad=6)
|
|
3649
|
+
cbar.ax.tick_params(labelsize=12)
|
|
3650
|
+
cbar.ax.xaxis.set_label_position("top")
|
|
3651
|
+
return fig
|
|
3652
|
+
|
|
3653
|
+
|
|
3654
|
+
def plot_kinetic_energy_relationships(df):
|
|
3655
|
+
"""Create a 2x2 multipanel figure showing kinetic energy relationships."""
|
|
3656
|
+
# Define plotting settings
|
|
3657
|
+
add_colorbar = False
|
|
3658
|
+
norm = LogNorm(1, None)
|
|
3659
|
+
legend_fontsize = 12
|
|
3660
|
+
# Initialize figure
|
|
3661
|
+
fig = plt.figure(figsize=(9, 10), dpi=300)
|
|
3662
|
+
|
|
3663
|
+
# Create gridspec with space for colorbar at bottom
|
|
3664
|
+
gs = GridSpec(
|
|
3665
|
+
3,
|
|
3666
|
+
2,
|
|
3667
|
+
figure=fig,
|
|
3668
|
+
height_ratios=[1, 1, 0.05],
|
|
3669
|
+
hspace=0.3,
|
|
3670
|
+
wspace=0.25,
|
|
3671
|
+
left=0.05,
|
|
3672
|
+
right=0.95,
|
|
3673
|
+
top=0.93,
|
|
3674
|
+
bottom=0.08,
|
|
3675
|
+
)
|
|
3676
|
+
|
|
3677
|
+
# Create subplots using gridspec
|
|
3678
|
+
axes = []
|
|
3679
|
+
for i in range(2):
|
|
3680
|
+
for j in range(2):
|
|
3681
|
+
ax = fig.add_subplot(gs[i, j])
|
|
3682
|
+
axes.append(ax)
|
|
3683
|
+
|
|
3684
|
+
# Flatten axes for easier indexing
|
|
3685
|
+
ax = np.array(axes).flatten()
|
|
3686
|
+
|
|
3687
|
+
# Plot the specific functions you requested:
|
|
3688
|
+
|
|
3689
|
+
# KED vs R (linear KED)
|
|
3690
|
+
p = plot_KED_R(df, norm=norm, legend_fontsize=legend_fontsize, add_colorbar=add_colorbar, ax=ax[0])
|
|
3691
|
+
|
|
3692
|
+
# Define norm for other plots based on first plot
|
|
3693
|
+
norm = p.norm
|
|
3694
|
+
|
|
3695
|
+
# KEF vs R
|
|
3696
|
+
plot_KEF_R(df, norm=norm, legend_fontsize=legend_fontsize, add_colorbar=add_colorbar, ax=ax[1])
|
|
3697
|
+
|
|
3698
|
+
# KEF vs Z_H
|
|
3699
|
+
plot_KEF_Z(df, z="Z", norm=norm, legend_fontsize=legend_fontsize, add_colorbar=add_colorbar, ax=ax[2])
|
|
3700
|
+
|
|
3701
|
+
# TKE vs Z_H
|
|
3702
|
+
p_last = plot_TKE_Z(df, z="Z", norm=norm, legend_fontsize=legend_fontsize, add_colorbar=add_colorbar, ax=ax[3])
|
|
3703
|
+
|
|
3704
|
+
# Add colorbar at the bottom
|
|
3705
|
+
cax = fig.add_subplot(gs[2, :]) # Spans all columns in the bottom row
|
|
3706
|
+
cbar = plt.colorbar(p_last, cax=cax, orientation="horizontal", extend="max", extendfrac=0.025)
|
|
3707
|
+
cbar.ax.set_aspect(0.1)
|
|
3708
|
+
cbar.set_label("Counts", fontsize=12, labelpad=10)
|
|
3709
|
+
cbar.ax.tick_params(labelsize=12)
|
|
3710
|
+
cbar.ax.xaxis.set_label_position("top")
|
|
3711
|
+
|
|
3712
|
+
return fig
|
|
3713
|
+
|
|
3714
|
+
|
|
3715
|
+
####-----------------------------------------------------------------------.
|
|
3716
|
+
#### Summary routine
|
|
3717
|
+
|
|
3718
|
+
|
|
3719
|
+
def define_filename(prefix, extension, data_source, campaign_name, station_name):
|
|
3720
|
+
"""Define filename for summary files."""
|
|
3721
|
+
if extension in ["png", "jpeg"]:
|
|
3722
|
+
filename = f"Figure.{prefix}.{data_source}.{campaign_name}.{station_name}.{extension}"
|
|
3723
|
+
if extension in ["csv", "parquet", "pdf", "yaml", "yml"]:
|
|
3724
|
+
filename = f"Table.{prefix}.{data_source}.{campaign_name}.{station_name}.{extension}"
|
|
3725
|
+
if extension in ["nc"]:
|
|
3726
|
+
filename = f"Dataset.{prefix}.{data_source}.{campaign_name}.{station_name}.{extension}"
|
|
3727
|
+
return filename
|
|
3728
|
+
|
|
3729
|
+
|
|
3730
|
+
def create_l2_dataframe(ds):
|
|
3731
|
+
"""Create pandas Dataframe for L2 analysis."""
|
|
3732
|
+
# - Drop array variables and convert to pandas
|
|
3733
|
+
df = ds.drop_dims([DIAMETER_DIMENSION, VELOCITY_DIMENSION]).to_pandas()
|
|
3734
|
+
# - Drop coordinates
|
|
3735
|
+
coords_to_drop = ["velocity_method", "sample_interval", *RADAR_OPTIONS]
|
|
3736
|
+
df = df.drop(columns=coords_to_drop, errors="ignore")
|
|
3737
|
+
# - Drop rows with missing rain
|
|
3738
|
+
df = df[df["R"] > 0]
|
|
3739
|
+
return df
|
|
3740
|
+
|
|
3741
|
+
|
|
3742
|
+
def prepare_summary_dataset(ds, velocity_method="fall_velocity", source="drop_number"):
|
|
3743
|
+
"""Prepare the L2E or L2M dataset to be converted to a dataframe."""
|
|
3744
|
+
# Select fall velocity method
|
|
3745
|
+
if "velocity_method" in ds.dims:
|
|
3746
|
+
ds = ds.sel(velocity_method=velocity_method)
|
|
3747
|
+
|
|
3748
|
+
# Select first occurrence of radars options (except frequency)
|
|
3749
|
+
for dim in RADAR_OPTIONS:
|
|
3750
|
+
if dim in ds.dims and dim != "frequency":
|
|
3751
|
+
ds = ds.isel({dim: 0})
|
|
3752
|
+
|
|
3753
|
+
# Unstack frequency dimension
|
|
3754
|
+
ds = unstack_radar_variables(ds)
|
|
3755
|
+
|
|
3756
|
+
# For kinetic energy variables, select source="drop_number"
|
|
3757
|
+
if "source" in ds.dims:
|
|
3758
|
+
ds = ds.sel(source=source)
|
|
3759
|
+
|
|
3760
|
+
# Select only timesteps with R > 0
|
|
3761
|
+
# - We save R with 2 decimals accuracy ... so 0.01 is the smallest value
|
|
3762
|
+
rainy_timesteps = np.logical_and(ds["Rm"].compute() >= 0.01, ds["R"].compute() >= 0.01)
|
|
3763
|
+
ds = ds.isel(time=rainy_timesteps)
|
|
3764
|
+
return ds
|
|
3765
|
+
|
|
3766
|
+
|
|
3767
|
+
def generate_station_summary(ds, summary_dir_path, data_source, campaign_name, station_name):
|
|
3768
|
+
"""Generate station summary using L2E dataset."""
|
|
3769
|
+
# Create summary directory if does not exist
|
|
3770
|
+
os.makedirs(summary_dir_path, exist_ok=True)
|
|
3771
|
+
|
|
3772
|
+
####---------------------------------------------------------------------.
|
|
3773
|
+
#### Prepare dataset
|
|
3774
|
+
ds = prepare_summary_dataset(ds)
|
|
3775
|
+
|
|
3776
|
+
# Ensure all data are in memory
|
|
3777
|
+
ds = ds.compute()
|
|
3778
|
+
|
|
3779
|
+
####---------------------------------------------------------------------.
|
|
3780
|
+
#### Create drop spectrum figures and statistics
|
|
3781
|
+
# Compute sum of raw and filtered spectrum over time
|
|
3782
|
+
|
|
3783
|
+
raw_drop_number = ds["raw_drop_number"].sum(dim="time")
|
|
3784
|
+
drop_number = ds["drop_number"].sum(dim="time")
|
|
3785
|
+
|
|
3786
|
+
# Define theoretical and measured average velocity
|
|
3787
|
+
theoretical_average_velocity = ds["fall_velocity"].mean(dim="time")
|
|
3788
|
+
measured_average_velocity = get_drop_average_velocity(drop_number)
|
|
3789
|
+
|
|
3790
|
+
# Save raw and filtered spectrum over time & theoretical and measured average fall velocity
|
|
3791
|
+
ds_stats = xr.Dataset()
|
|
3792
|
+
ds_stats["raw_drop_number"] = raw_drop_number
|
|
3793
|
+
ds_stats["drop_number"] = raw_drop_number
|
|
3794
|
+
ds_stats["theoretical_average_velocity"] = theoretical_average_velocity
|
|
3795
|
+
ds_stats["measured_average_velocity"] = measured_average_velocity
|
|
3796
|
+
filename = define_filename(
|
|
3797
|
+
prefix="SpectrumStats",
|
|
3798
|
+
extension="nc",
|
|
3799
|
+
data_source=data_source,
|
|
3800
|
+
campaign_name=campaign_name,
|
|
3801
|
+
station_name=station_name,
|
|
3802
|
+
)
|
|
3803
|
+
ds_stats.to_netcdf(os.path.join(summary_dir_path, filename))
|
|
3804
|
+
|
|
3805
|
+
# Create figures with raw and filtered spectrum
|
|
3806
|
+
# - Raw
|
|
3807
|
+
filename = define_filename(
|
|
3808
|
+
prefix="SpectrumRaw",
|
|
3809
|
+
extension="png",
|
|
3810
|
+
data_source=data_source,
|
|
3811
|
+
campaign_name=campaign_name,
|
|
3812
|
+
station_name=station_name,
|
|
3813
|
+
)
|
|
3814
|
+
p = plot_drop_spectrum(raw_drop_number, title="Raw Drop Spectrum")
|
|
3815
|
+
p.figure.savefig(os.path.join(summary_dir_path, filename))
|
|
3816
|
+
plt.close()
|
|
3817
|
+
|
|
3818
|
+
# - Filtered
|
|
3819
|
+
filename = define_filename(
|
|
3820
|
+
prefix="SpectrumFiltered",
|
|
3821
|
+
extension="png",
|
|
3822
|
+
data_source=data_source,
|
|
3823
|
+
campaign_name=campaign_name,
|
|
3824
|
+
station_name=station_name,
|
|
3825
|
+
)
|
|
3826
|
+
p = plot_drop_spectrum(drop_number, title="Filtered Drop Spectrum")
|
|
3827
|
+
p.figure.savefig(os.path.join(summary_dir_path, filename))
|
|
3828
|
+
plt.close()
|
|
3829
|
+
|
|
3830
|
+
# Create figure comparing raw and filtered spectrum
|
|
3831
|
+
filename = define_filename(
|
|
3832
|
+
prefix="SpectrumSummary",
|
|
3833
|
+
extension="png",
|
|
3834
|
+
data_source=data_source,
|
|
3835
|
+
campaign_name=campaign_name,
|
|
3836
|
+
station_name=station_name,
|
|
3837
|
+
)
|
|
3838
|
+
|
|
3839
|
+
fig = plot_raw_and_filtered_spectrums(
|
|
3840
|
+
raw_drop_number=raw_drop_number,
|
|
3841
|
+
drop_number=drop_number,
|
|
3842
|
+
theoretical_average_velocity=theoretical_average_velocity,
|
|
3843
|
+
measured_average_velocity=measured_average_velocity,
|
|
3844
|
+
)
|
|
3845
|
+
fig.savefig(os.path.join(summary_dir_path, filename))
|
|
3846
|
+
plt.close()
|
|
3847
|
+
|
|
3848
|
+
####---------------------------------------------------------------------.
|
|
3849
|
+
#### Create L2E 1MIN dataframe
|
|
3850
|
+
df = create_l2_dataframe(ds)
|
|
3851
|
+
|
|
3852
|
+
# Define diameter bin edges
|
|
3853
|
+
diameter_bin_edges = get_diameter_bin_edges(ds)
|
|
3854
|
+
|
|
3855
|
+
# ---------------------------------------------------------------------.
|
|
3856
|
+
#### Save L2E 1MIN Parquet
|
|
3857
|
+
l2e_parquet_filename = f"L2E.1MIN.PARQUET.{data_source}.{campaign_name}.{station_name}.parquet"
|
|
3858
|
+
l2e_parquet_filepath = os.path.join(summary_dir_path, l2e_parquet_filename)
|
|
3859
|
+
df.to_parquet(l2e_parquet_filepath, engine="pyarrow", compression="snappy")
|
|
3860
|
+
|
|
3861
|
+
#### ---------------------------------------------------------------------.
|
|
3862
|
+
#### Create table with rain summary
|
|
3863
|
+
table_rain_summary = create_table_rain_summary(df)
|
|
3864
|
+
table_rain_summary_filename = f"Station_Summary.{data_source}.{campaign_name}.{station_name}.yaml"
|
|
3865
|
+
table_rain_summary_filepath = os.path.join(summary_dir_path, table_rain_summary_filename)
|
|
3866
|
+
write_yaml(table_rain_summary, filepath=table_rain_summary_filepath)
|
|
3867
|
+
|
|
3868
|
+
# ---------------------------------------------------------------------.
|
|
3869
|
+
#### Creata table with events summary
|
|
3870
|
+
table_events_summary = create_table_events_summary(df)
|
|
3871
|
+
# - Save table as csv
|
|
3872
|
+
table_events_summary_csv_filename = f"Events_Summary.{data_source}.{campaign_name}.{station_name}.csv"
|
|
3873
|
+
table_events_summary_csv_filepath = os.path.join(summary_dir_path, table_events_summary_csv_filename)
|
|
3874
|
+
table_events_summary.to_csv(table_events_summary_csv_filepath)
|
|
3875
|
+
# - Save table as pdf
|
|
3876
|
+
if is_latex_engine_available():
|
|
3877
|
+
table_events_summary_pdf_filename = f"Events_Summary.{data_source}.{campaign_name}.{station_name}.pdf"
|
|
3878
|
+
table_events_summary_pdf_filepath = os.path.join(summary_dir_path, table_events_summary_pdf_filename)
|
|
3879
|
+
save_table_to_pdf(
|
|
3880
|
+
df=prepare_latex_table_events_summary(table_events_summary),
|
|
3881
|
+
filepath=table_events_summary_pdf_filepath,
|
|
3882
|
+
index=True,
|
|
3883
|
+
caption="Events Summary",
|
|
3884
|
+
orientation="landscape",
|
|
3885
|
+
)
|
|
3886
|
+
|
|
3887
|
+
# ---------------------------------------------------------------------.
|
|
3888
|
+
#### Create table with integral DSD parameters statistics
|
|
3889
|
+
table_dsd_summary = create_table_dsd_summary(df)
|
|
3890
|
+
# - Save table as csv
|
|
3891
|
+
table_dsd_summary_csv_filename = f"DSD_Summary.{data_source}.{campaign_name}.{station_name}.csv"
|
|
3892
|
+
table_dsd_summary_csv_filepath = os.path.join(summary_dir_path, table_dsd_summary_csv_filename)
|
|
3893
|
+
table_dsd_summary.to_csv(table_dsd_summary_csv_filepath)
|
|
3894
|
+
# - Save table as pdf
|
|
3895
|
+
if is_latex_engine_available():
|
|
3896
|
+
table_dsd_summary_pdf_filename = f"DSD_Summary.{data_source}.{campaign_name}.{station_name}.pdf"
|
|
3897
|
+
table_dsd_summary_pdf_filepath = os.path.join(summary_dir_path, table_dsd_summary_pdf_filename)
|
|
3898
|
+
save_table_to_pdf(
|
|
3899
|
+
df=prepare_latex_table_dsd_summary(table_dsd_summary),
|
|
3900
|
+
index=True,
|
|
3901
|
+
filepath=table_dsd_summary_pdf_filepath,
|
|
3902
|
+
caption="DSD Summary",
|
|
3903
|
+
orientation="portrait", # "landscape",
|
|
3904
|
+
)
|
|
3905
|
+
|
|
3906
|
+
#### ---------------------------------------------------------------------.
|
|
3907
|
+
#### Create L2E RADAR Summary Plots
|
|
3908
|
+
# Summary plots at X, C, S bands
|
|
3909
|
+
if "DBZH_X" in df:
|
|
3910
|
+
filename = define_filename(
|
|
3911
|
+
prefix="Radar_Band_X",
|
|
3912
|
+
extension="png",
|
|
3913
|
+
data_source=data_source,
|
|
3914
|
+
campaign_name=campaign_name,
|
|
3915
|
+
station_name=station_name,
|
|
3916
|
+
)
|
|
3917
|
+
fig = plot_radar_relationships(df, band="X")
|
|
3918
|
+
fig.savefig(os.path.join(summary_dir_path, filename))
|
|
3919
|
+
if "DBZH_C" in df:
|
|
3920
|
+
filename = define_filename(
|
|
3921
|
+
prefix="Radar_Band_C",
|
|
3922
|
+
extension="png",
|
|
3923
|
+
data_source=data_source,
|
|
3924
|
+
campaign_name=campaign_name,
|
|
3925
|
+
station_name=station_name,
|
|
3926
|
+
)
|
|
3927
|
+
fig = plot_radar_relationships(df, band="C")
|
|
3928
|
+
fig.savefig(os.path.join(summary_dir_path, filename))
|
|
3929
|
+
if "DBZH_S" in df:
|
|
3930
|
+
filename = define_filename(
|
|
3931
|
+
prefix="Radar_Band_S",
|
|
3932
|
+
extension="png",
|
|
3933
|
+
data_source=data_source,
|
|
3934
|
+
campaign_name=campaign_name,
|
|
3935
|
+
station_name=station_name,
|
|
3936
|
+
)
|
|
3937
|
+
fig = plot_radar_relationships(df, band="S")
|
|
3938
|
+
fig.savefig(os.path.join(summary_dir_path, filename))
|
|
3939
|
+
|
|
3940
|
+
# ---------------------------------------------------------------------.
|
|
3941
|
+
#### - Create Z-R figure
|
|
3942
|
+
filename = define_filename(
|
|
3943
|
+
prefix="Z-R",
|
|
3944
|
+
extension="png",
|
|
3945
|
+
data_source=data_source,
|
|
3946
|
+
campaign_name=campaign_name,
|
|
3947
|
+
station_name=station_name,
|
|
3948
|
+
)
|
|
3949
|
+
|
|
3950
|
+
p = plot_R_Z(df, z="Z", r="R", title=r"$Z$ vs $R$")
|
|
3951
|
+
p.figure.savefig(os.path.join(summary_dir_path, filename))
|
|
3952
|
+
plt.close()
|
|
3953
|
+
|
|
3954
|
+
#### ---------------------------------------------------------------------.
|
|
3955
|
+
#### Create L2E Kinetic Energy Summary Plots
|
|
3956
|
+
filename = define_filename(
|
|
3957
|
+
prefix="KineticEnergy",
|
|
3958
|
+
extension="png",
|
|
3959
|
+
data_source=data_source,
|
|
3960
|
+
campaign_name=campaign_name,
|
|
3961
|
+
station_name=station_name,
|
|
3962
|
+
)
|
|
3963
|
+
fig = plot_kinetic_energy_relationships(df)
|
|
3964
|
+
fig.savefig(os.path.join(summary_dir_path, filename))
|
|
3965
|
+
|
|
3966
|
+
#### ---------------------------------------------------------------------.
|
|
3967
|
+
#### Create L2E DSD Parameters summary plots
|
|
3968
|
+
#### - Create DSD parameters density figures with LWC
|
|
3969
|
+
filename = define_filename(
|
|
3970
|
+
prefix="DSD_Params_Density_with_LWC_LinearDm_MaxNormalized",
|
|
3971
|
+
extension="png",
|
|
3972
|
+
data_source=data_source,
|
|
3973
|
+
campaign_name=campaign_name,
|
|
3974
|
+
station_name=station_name,
|
|
3975
|
+
)
|
|
3976
|
+
fig = plot_dsd_params_density(df, log_dm=False, lwc=True, log_normalize=False)
|
|
3977
|
+
fig.savefig(os.path.join(summary_dir_path, filename))
|
|
3978
|
+
plt.close()
|
|
3979
|
+
|
|
3980
|
+
filename = define_filename(
|
|
3981
|
+
prefix="DSD_Params_Density_with_LWC_LogDm_MaxNormalized",
|
|
3982
|
+
extension="png",
|
|
3983
|
+
data_source=data_source,
|
|
3984
|
+
campaign_name=campaign_name,
|
|
3985
|
+
station_name=station_name,
|
|
3986
|
+
)
|
|
3987
|
+
fig = plot_dsd_params_density(df, log_dm=True, lwc=True, log_normalize=False)
|
|
3988
|
+
fig.savefig(os.path.join(summary_dir_path, filename))
|
|
3989
|
+
plt.close()
|
|
3990
|
+
|
|
3991
|
+
filename = define_filename(
|
|
3992
|
+
prefix="DSD_Params_Density_with_LWC_LinearDm_LogNormalized",
|
|
3993
|
+
extension="png",
|
|
3994
|
+
data_source=data_source,
|
|
3995
|
+
campaign_name=campaign_name,
|
|
3996
|
+
station_name=station_name,
|
|
3997
|
+
)
|
|
3998
|
+
fig = plot_dsd_params_density(df, log_dm=False, lwc=True, log_normalize=True)
|
|
3999
|
+
fig.savefig(os.path.join(summary_dir_path, filename))
|
|
4000
|
+
plt.close()
|
|
4001
|
+
|
|
4002
|
+
filename = define_filename(
|
|
4003
|
+
prefix="DSD_Params_Density_with_LWC_LogDm_LogNormalized",
|
|
4004
|
+
extension="png",
|
|
4005
|
+
data_source=data_source,
|
|
4006
|
+
campaign_name=campaign_name,
|
|
4007
|
+
station_name=station_name,
|
|
4008
|
+
)
|
|
4009
|
+
fig = plot_dsd_params_density(df, log_dm=True, lwc=True, log_normalize=True)
|
|
4010
|
+
fig.savefig(os.path.join(summary_dir_path, filename))
|
|
4011
|
+
plt.close()
|
|
4012
|
+
|
|
4013
|
+
###------------------------------------------------------------------------.
|
|
4014
|
+
#### - Create DSD parameters density figures with R
|
|
4015
|
+
filename = define_filename(
|
|
4016
|
+
prefix="DSD_Params_Density_with_R_LinearDm_MaxNormalized",
|
|
4017
|
+
extension="png",
|
|
4018
|
+
data_source=data_source,
|
|
4019
|
+
campaign_name=campaign_name,
|
|
4020
|
+
station_name=station_name,
|
|
4021
|
+
)
|
|
4022
|
+
fig = plot_dsd_params_density(df, log_dm=False, lwc=False, log_normalize=False)
|
|
4023
|
+
fig.savefig(os.path.join(summary_dir_path, filename))
|
|
4024
|
+
plt.close()
|
|
4025
|
+
|
|
4026
|
+
filename = define_filename(
|
|
4027
|
+
prefix="DSD_Params_Density_with_R_LogDm_MaxNormalized",
|
|
4028
|
+
extension="png",
|
|
4029
|
+
data_source=data_source,
|
|
4030
|
+
campaign_name=campaign_name,
|
|
4031
|
+
station_name=station_name,
|
|
4032
|
+
)
|
|
4033
|
+
fig = plot_dsd_params_density(df, log_dm=True, lwc=False, log_normalize=False)
|
|
4034
|
+
fig.savefig(os.path.join(summary_dir_path, filename))
|
|
4035
|
+
plt.close()
|
|
4036
|
+
|
|
4037
|
+
filename = define_filename(
|
|
4038
|
+
prefix="DSD_Params_Density_with_R_LinearDm_LogNormalized",
|
|
4039
|
+
extension="png",
|
|
4040
|
+
data_source=data_source,
|
|
4041
|
+
campaign_name=campaign_name,
|
|
4042
|
+
station_name=station_name,
|
|
4043
|
+
)
|
|
4044
|
+
fig = plot_dsd_params_density(df, log_dm=False, lwc=False, log_normalize=True)
|
|
4045
|
+
fig.savefig(os.path.join(summary_dir_path, filename))
|
|
4046
|
+
plt.close()
|
|
4047
|
+
|
|
4048
|
+
filename = define_filename(
|
|
4049
|
+
prefix="DSD_Params_Density_with_R_LogDm_LogNormalized",
|
|
4050
|
+
extension="png",
|
|
4051
|
+
data_source=data_source,
|
|
4052
|
+
campaign_name=campaign_name,
|
|
4053
|
+
station_name=station_name,
|
|
4054
|
+
)
|
|
4055
|
+
fig = plot_dsd_params_density(df, log_dm=True, lwc=False, log_normalize=True)
|
|
4056
|
+
fig.savefig(os.path.join(summary_dir_path, filename))
|
|
4057
|
+
plt.close()
|
|
4058
|
+
|
|
4059
|
+
###------------------------------------------------------------------------.
|
|
4060
|
+
#### - Create DSD parameters relationship figures
|
|
4061
|
+
filename = define_filename(
|
|
4062
|
+
prefix="DSD_Params_Relations",
|
|
4063
|
+
extension="png",
|
|
4064
|
+
data_source=data_source,
|
|
4065
|
+
campaign_name=campaign_name,
|
|
4066
|
+
station_name=station_name,
|
|
4067
|
+
)
|
|
4068
|
+
fig = plot_dsd_params_relationships(df, add_nt=True)
|
|
4069
|
+
fig.savefig(os.path.join(summary_dir_path, filename))
|
|
4070
|
+
plt.close()
|
|
4071
|
+
|
|
4072
|
+
###------------------------------------------------------------------------.
|
|
4073
|
+
#### - Create Dmax relationship figures
|
|
4074
|
+
filename = define_filename(
|
|
4075
|
+
prefix="DSD_Dmax_Relations",
|
|
4076
|
+
extension="png",
|
|
4077
|
+
data_source=data_source,
|
|
4078
|
+
campaign_name=campaign_name,
|
|
4079
|
+
station_name=station_name,
|
|
4080
|
+
)
|
|
4081
|
+
fig = plot_dmax_relationships(df, diameter_bin_edges=diameter_bin_edges, dmax="Dmax", diameter_max=10)
|
|
4082
|
+
fig.savefig(os.path.join(summary_dir_path, filename))
|
|
4083
|
+
plt.close()
|
|
4084
|
+
|
|
4085
|
+
#### ---------------------------------------------------------------------.
|
|
4086
|
+
#### Create L2E QC summary plots
|
|
4087
|
+
# TODO:
|
|
4088
|
+
|
|
4089
|
+
####------------------------------------------------------------------------.
|
|
4090
|
+
#### Free space - Remove df from memory
|
|
4091
|
+
del df
|
|
4092
|
+
gc.collect()
|
|
4093
|
+
|
|
4094
|
+
####------------------------------------------------------------------------.
|
|
4095
|
+
#### Create N(D) densities
|
|
4096
|
+
df_nd = create_nd_dataframe(ds)
|
|
4097
|
+
|
|
4098
|
+
#### - Plot N(D) vs D
|
|
4099
|
+
filename = define_filename(
|
|
4100
|
+
prefix="N(D)",
|
|
4101
|
+
extension="png",
|
|
4102
|
+
data_source=data_source,
|
|
4103
|
+
campaign_name=campaign_name,
|
|
4104
|
+
station_name=station_name,
|
|
4105
|
+
)
|
|
4106
|
+
p = plot_dsd_density(df_nd, diameter_bin_edges=diameter_bin_edges)
|
|
4107
|
+
p.figure.savefig(os.path.join(summary_dir_path, filename))
|
|
4108
|
+
plt.close()
|
|
4109
|
+
|
|
4110
|
+
#### - Plot N(D)/Nw vs D/Dm
|
|
4111
|
+
filename = define_filename(
|
|
4112
|
+
prefix="N(D)_Normalized",
|
|
4113
|
+
extension="png",
|
|
4114
|
+
data_source=data_source,
|
|
4115
|
+
campaign_name=campaign_name,
|
|
4116
|
+
station_name=station_name,
|
|
4117
|
+
)
|
|
4118
|
+
p = plot_normalized_dsd_density(df_nd)
|
|
4119
|
+
p.figure.savefig(os.path.join(summary_dir_path, filename))
|
|
4120
|
+
plt.close()
|
|
4121
|
+
|
|
4122
|
+
#### Free space - Remove df_nd from memory
|
|
4123
|
+
del df_nd
|
|
4124
|
+
gc.collect()
|
|
4125
|
+
|
|
4126
|
+
#### - Plot N(D) vs D with DenseLines
|
|
4127
|
+
# Extract required variables and free memory
|
|
4128
|
+
drop_number_concentration = ds["drop_number_concentration"].compute().copy()
|
|
4129
|
+
r = ds["R"].compute().copy()
|
|
4130
|
+
del ds
|
|
4131
|
+
gc.collect()
|
|
4132
|
+
|
|
4133
|
+
# Create figure
|
|
4134
|
+
filename = define_filename(
|
|
4135
|
+
prefix="N(D)_DenseLines",
|
|
4136
|
+
extension="png",
|
|
4137
|
+
data_source=data_source,
|
|
4138
|
+
campaign_name=campaign_name,
|
|
4139
|
+
station_name=station_name,
|
|
4140
|
+
)
|
|
4141
|
+
p = plot_dsd_with_dense_lines(drop_number_concentration=drop_number_concentration, r=r)
|
|
4142
|
+
p.figure.savefig(os.path.join(summary_dir_path, filename))
|
|
4143
|
+
plt.close()
|
|
4144
|
+
|
|
4145
|
+
|
|
4146
|
+
####------------------------------------------------------------------------.
|
|
4147
|
+
#### Wrappers
|
|
4148
|
+
|
|
4149
|
+
|
|
4150
|
+
def create_station_summary(
|
|
4151
|
+
data_source,
|
|
4152
|
+
campaign_name,
|
|
4153
|
+
station_name,
|
|
4154
|
+
parallel=False,
|
|
4155
|
+
data_archive_dir=None,
|
|
4156
|
+
):
|
|
4157
|
+
"""Create summary figures and tables for a DISDRODB station."""
|
|
4158
|
+
# Print processing info
|
|
4159
|
+
print(f"Creation of station summary for {data_source} {campaign_name} {station_name} has started.")
|
|
4160
|
+
|
|
4161
|
+
# Define station summary directory
|
|
4162
|
+
summary_dir_path = define_station_dir(
|
|
4163
|
+
product="SUMMARY",
|
|
4164
|
+
data_source=data_source,
|
|
4165
|
+
campaign_name=campaign_name,
|
|
4166
|
+
station_name=station_name,
|
|
4167
|
+
data_archive_dir=data_archive_dir,
|
|
4168
|
+
check_exists=False,
|
|
4169
|
+
)
|
|
4170
|
+
os.makedirs(summary_dir_path, exist_ok=True)
|
|
4171
|
+
|
|
4172
|
+
# Load L2E 1MIN dataset
|
|
4173
|
+
ds = disdrodb.open_dataset(
|
|
4174
|
+
data_archive_dir=data_archive_dir,
|
|
4175
|
+
data_source=data_source,
|
|
4176
|
+
campaign_name=campaign_name,
|
|
4177
|
+
station_name=station_name,
|
|
4178
|
+
product="L2E",
|
|
4179
|
+
product_kwargs={"rolling": False, "sample_interval": 60},
|
|
4180
|
+
parallel=parallel,
|
|
4181
|
+
chunks=-1,
|
|
4182
|
+
compute=True,
|
|
4183
|
+
)
|
|
4184
|
+
|
|
4185
|
+
# Generate station summary figures and table
|
|
4186
|
+
generate_station_summary(
|
|
4187
|
+
ds=ds,
|
|
4188
|
+
summary_dir_path=summary_dir_path,
|
|
4189
|
+
data_source=data_source,
|
|
4190
|
+
campaign_name=campaign_name,
|
|
4191
|
+
station_name=station_name,
|
|
4192
|
+
)
|
|
4193
|
+
|
|
4194
|
+
print(f"Creation of station summary for {data_source} {campaign_name} {station_name} has terminated.")
|
|
4195
|
+
|
|
4196
|
+
# -------------------------------------------------------------------------------------------------.
|