cloudnetpy 1.61.15__py3-none-any.whl → 1.61.17__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.
- cloudnetpy/categorize/atmos.py +1 -0
- cloudnetpy/categorize/atmos_utils.py +1 -1
- cloudnetpy/categorize/categorize.py +1 -0
- cloudnetpy/categorize/classify.py +1 -0
- cloudnetpy/categorize/containers.py +3 -1
- cloudnetpy/categorize/disdrometer.py +1 -0
- cloudnetpy/categorize/droplet.py +2 -2
- cloudnetpy/categorize/falling.py +1 -0
- cloudnetpy/categorize/freezing.py +1 -0
- cloudnetpy/categorize/insects.py +1 -0
- cloudnetpy/categorize/lidar.py +1 -0
- cloudnetpy/categorize/melting.py +1 -0
- cloudnetpy/categorize/model.py +1 -0
- cloudnetpy/categorize/mwr.py +1 -0
- cloudnetpy/categorize/radar.py +1 -0
- cloudnetpy/cloudnetarray.py +2 -0
- cloudnetpy/concat_lib.py +1 -0
- cloudnetpy/constants.py +1 -0
- cloudnetpy/datasource.py +5 -3
- cloudnetpy/instruments/basta.py +1 -0
- cloudnetpy/instruments/ceilo.py +2 -1
- cloudnetpy/instruments/cl61d.py +1 -0
- cloudnetpy/instruments/copernicus.py +2 -1
- cloudnetpy/instruments/disdrometer/parsivel.py +6 -4
- cloudnetpy/instruments/galileo.py +1 -0
- cloudnetpy/instruments/hatpro.py +1 -0
- cloudnetpy/instruments/lufft.py +1 -0
- cloudnetpy/instruments/mira.py +1 -0
- cloudnetpy/instruments/mrr.py +1 -1
- cloudnetpy/instruments/nc_lidar.py +1 -0
- cloudnetpy/instruments/nc_radar.py +1 -0
- cloudnetpy/instruments/pollyxt.py +1 -0
- cloudnetpy/instruments/radiometrics.py +2 -1
- cloudnetpy/instruments/rpg.py +2 -1
- cloudnetpy/instruments/rpg_reader.py +1 -1
- cloudnetpy/instruments/toa5.py +1 -1
- cloudnetpy/instruments/vaisala.py +7 -6
- cloudnetpy/instruments/weather_station.py +118 -65
- cloudnetpy/model_evaluation/file_handler.py +2 -2
- cloudnetpy/model_evaluation/metadata.py +1 -1
- cloudnetpy/model_evaluation/plotting/plot_tools.py +2 -2
- cloudnetpy/model_evaluation/plotting/plotting.py +11 -8
- cloudnetpy/model_evaluation/products/grid_methods.py +1 -1
- cloudnetpy/model_evaluation/products/model_products.py +7 -7
- cloudnetpy/model_evaluation/products/observation_products.py +8 -8
- cloudnetpy/model_evaluation/products/tools.py +5 -7
- cloudnetpy/model_evaluation/statistics/statistical_methods.py +2 -2
- cloudnetpy/output.py +3 -1
- cloudnetpy/plotting/plot_meta.py +2 -2
- cloudnetpy/plotting/plotting.py +36 -23
- cloudnetpy/products/classification.py +10 -9
- cloudnetpy/products/der.py +3 -2
- cloudnetpy/products/drizzle.py +3 -3
- cloudnetpy/products/drizzle_tools.py +1 -1
- cloudnetpy/products/ier.py +1 -0
- cloudnetpy/products/iwc.py +4 -3
- cloudnetpy/products/lwc.py +2 -3
- cloudnetpy/products/mwr_tools.py +2 -4
- cloudnetpy/products/product_tools.py +2 -1
- cloudnetpy/utils.py +9 -14
- cloudnetpy/version.py +1 -1
- {cloudnetpy-1.61.15.dist-info → cloudnetpy-1.61.17.dist-info}/METADATA +1 -1
- cloudnetpy-1.61.17.dist-info/RECORD +115 -0
- {cloudnetpy-1.61.15.dist-info → cloudnetpy-1.61.17.dist-info}/WHEEL +1 -1
- cloudnetpy-1.61.15.dist-info/RECORD +0 -115
- {cloudnetpy-1.61.15.dist-info → cloudnetpy-1.61.17.dist-info}/LICENSE +0 -0
- {cloudnetpy-1.61.15.dist-info → cloudnetpy-1.61.17.dist-info}/top_level.txt +0 -0
@@ -47,12 +47,16 @@ def ws2nc(
|
|
47
47
|
ws: WS
|
48
48
|
if site_meta["name"] == "Palaiseau":
|
49
49
|
ws = PalaiseauWS(weather_station_file, site_meta)
|
50
|
+
elif site_meta["name"] == "Bucharest":
|
51
|
+
ws = BucharestWS(weather_station_file, site_meta)
|
50
52
|
elif site_meta["name"] == "Granada":
|
51
53
|
ws = GranadaWS(weather_station_file, site_meta)
|
52
54
|
elif site_meta["name"] == "Kenttärova":
|
53
55
|
ws = KenttarovaWS(weather_station_file, site_meta)
|
54
56
|
elif site_meta["name"] == "Hyytiälä":
|
55
57
|
ws = HyytialaWS(weather_station_file, site_meta)
|
58
|
+
elif site_meta["name"] == "Galați":
|
59
|
+
ws = GalatiWS(weather_station_file, site_meta)
|
56
60
|
else:
|
57
61
|
msg = "Unsupported site"
|
58
62
|
raise ValueError(msg) # noqa: TRY301
|
@@ -66,6 +70,7 @@ def ws2nc(
|
|
66
70
|
ws.convert_pressure()
|
67
71
|
ws.convert_rainfall_rate()
|
68
72
|
ws.convert_rainfall_amount()
|
73
|
+
ws.normalize_rainfall_amount()
|
69
74
|
ws.calculate_rainfall_amount()
|
70
75
|
attributes = output.add_time_attribute(ATTRIBUTES, ws.date)
|
71
76
|
output.update_attributes(ws.data, attributes)
|
@@ -75,9 +80,11 @@ def ws2nc(
|
|
75
80
|
|
76
81
|
|
77
82
|
class WS(CloudnetInstrument):
|
78
|
-
def __init__(self):
|
83
|
+
def __init__(self, site_meta: dict):
|
79
84
|
super().__init__()
|
80
85
|
self._data: dict
|
86
|
+
self.site_meta = site_meta
|
87
|
+
self.instrument = instruments.GENERIC_WEATHER_STATION
|
81
88
|
|
82
89
|
date: list[str]
|
83
90
|
|
@@ -111,6 +118,15 @@ class WS(CloudnetInstrument):
|
|
111
118
|
x for ind, x in enumerate(self._data[key]) if ind in valid_ind
|
112
119
|
]
|
113
120
|
|
121
|
+
@staticmethod
|
122
|
+
def format_data(data: dict) -> dict:
|
123
|
+
for key, value in data.items():
|
124
|
+
new_value = np.array(value)
|
125
|
+
if key != "time":
|
126
|
+
new_value = ma.masked_where(np.isnan(new_value), new_value)
|
127
|
+
data[key] = new_value
|
128
|
+
return data
|
129
|
+
|
114
130
|
def convert_temperature_and_humidity(self) -> None:
|
115
131
|
temperature_kelvins = atmos_utils.c2k(self.data["air_temperature"][:])
|
116
132
|
self.data["air_temperature"].data = temperature_kelvins
|
@@ -123,6 +139,17 @@ class WS(CloudnetInstrument):
|
|
123
139
|
def convert_pressure(self) -> None:
|
124
140
|
self.data["air_pressure"].data = self.data["air_pressure"][:] * 100 # hPa to Pa
|
125
141
|
|
142
|
+
def normalize_rainfall_amount(self) -> None:
|
143
|
+
if "rainfall_amount" in self.data:
|
144
|
+
amount = self.data["rainfall_amount"][:]
|
145
|
+
offset = 0
|
146
|
+
for i in range(1, len(amount)):
|
147
|
+
if amount[i] + offset < amount[i - 1]:
|
148
|
+
offset += amount[i - 1]
|
149
|
+
amount[i] += offset
|
150
|
+
amount -= amount[0]
|
151
|
+
self.data["rainfall_amount"].data = amount
|
152
|
+
|
126
153
|
def convert_time(self) -> None:
|
127
154
|
pass
|
128
155
|
|
@@ -132,50 +159,30 @@ class WS(CloudnetInstrument):
|
|
132
159
|
|
133
160
|
class PalaiseauWS(WS):
|
134
161
|
def __init__(self, filenames: list[str], site_meta: dict):
|
135
|
-
super().__init__()
|
136
|
-
|
137
|
-
raise ValueError
|
138
|
-
self.filename = filenames[0]
|
139
|
-
self.site_meta = site_meta
|
140
|
-
self.instrument = instruments.GENERIC_WEATHER_STATION
|
162
|
+
super().__init__(site_meta)
|
163
|
+
self.filenames = filenames
|
141
164
|
self._data = self._read_data()
|
142
165
|
|
143
166
|
def _read_data(self) -> dict:
|
144
167
|
timestamps, values, header = [], [], []
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
"
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
"DateTime(yyyy-mm-ddThh:mm:ssZ)",
|
164
|
-
"Windspeed(m/s)",
|
165
|
-
"Winddirection(degres)",
|
166
|
-
"Airtemperature(°C)",
|
167
|
-
"Relativehumidity(%)",
|
168
|
-
"Pressure(hPa)",
|
169
|
-
"Precipitationrate(mm/min)",
|
170
|
-
"24-hrcumulatedprecipitationsince00UT(mm)",
|
171
|
-
]
|
172
|
-
column_titles = [row for row in header if "Col." in row]
|
173
|
-
error_msg = "Unexpected weather station file format"
|
174
|
-
if len(column_titles) != len(expected_identifiers):
|
175
|
-
raise ValueError(error_msg)
|
176
|
-
for title, identifier in zip(column_titles, expected_identifiers, strict=True):
|
177
|
-
if identifier not in title:
|
178
|
-
raise ValueError(error_msg)
|
168
|
+
for filename in self.filenames:
|
169
|
+
with open(filename, encoding="latin-1") as f:
|
170
|
+
data = f.readlines()
|
171
|
+
for row in data:
|
172
|
+
if not (columns := row.split()):
|
173
|
+
continue
|
174
|
+
if row.startswith("#"):
|
175
|
+
header_row = "".join(columns)
|
176
|
+
if header_row not in header:
|
177
|
+
header.append(header_row)
|
178
|
+
else:
|
179
|
+
timestamp = datetime.datetime.strptime(
|
180
|
+
columns[0], "%Y-%m-%dT%H:%M:%SZ"
|
181
|
+
).replace(tzinfo=datetime.timezone.utc)
|
182
|
+
values.append([timestamp] + [float(x) for x in columns[1:]])
|
183
|
+
timestamps.append(timestamp)
|
184
|
+
|
185
|
+
self._validate_header(header)
|
179
186
|
return {"time": timestamps, "values": values}
|
180
187
|
|
181
188
|
def convert_time(self) -> None:
|
@@ -212,15 +219,39 @@ class PalaiseauWS(WS):
|
|
212
219
|
self.data["rainfall_amount"][:] / 1000
|
213
220
|
) # mm -> m
|
214
221
|
|
222
|
+
@staticmethod
|
223
|
+
def _validate_header(header: list[str]) -> None:
|
224
|
+
expected_identifiers = [
|
225
|
+
"DateTime(yyyy-mm-ddThh:mm:ssZ)",
|
226
|
+
"Windspeed(m/s)",
|
227
|
+
"Winddirection(deg",
|
228
|
+
"Airtemperature",
|
229
|
+
"Relativehumidity(%)",
|
230
|
+
"Pressure(hPa)",
|
231
|
+
"Precipitationrate(mm/min)",
|
232
|
+
"precipitation",
|
233
|
+
]
|
234
|
+
column_titles = [row for row in header if "Col." in row]
|
235
|
+
error_msg = "Unexpected weather station file format"
|
236
|
+
if len(column_titles) != len(expected_identifiers):
|
237
|
+
raise ValueError(error_msg)
|
238
|
+
for title, identifier in zip(column_titles, expected_identifiers, strict=True):
|
239
|
+
if identifier not in title:
|
240
|
+
raise ValueError(error_msg)
|
241
|
+
|
242
|
+
|
243
|
+
class BucharestWS(PalaiseauWS):
|
244
|
+
def convert_rainfall_rate(self) -> None:
|
245
|
+
rainfall_rate = self.data["rainfall_rate"][:]
|
246
|
+
self.data["rainfall_rate"].data = rainfall_rate * MM_H_TO_M_S
|
247
|
+
|
215
248
|
|
216
249
|
class GranadaWS(WS):
|
217
250
|
def __init__(self, filenames: list[str], site_meta: dict):
|
218
251
|
if len(filenames) != 1:
|
219
252
|
raise ValueError
|
220
|
-
super().__init__()
|
253
|
+
super().__init__(site_meta)
|
221
254
|
self.filename = filenames[0]
|
222
|
-
self.site_meta = site_meta
|
223
|
-
self.instrument = instruments.GENERIC_WEATHER_STATION
|
224
255
|
self._data = self._read_data()
|
225
256
|
|
226
257
|
def _read_data(self) -> dict:
|
@@ -264,10 +295,8 @@ class GranadaWS(WS):
|
|
264
295
|
|
265
296
|
class KenttarovaWS(WS):
|
266
297
|
def __init__(self, filenames: list[str], site_meta: dict):
|
267
|
-
super().__init__()
|
298
|
+
super().__init__(site_meta)
|
268
299
|
self.filenames = filenames
|
269
|
-
self.site_meta = site_meta
|
270
|
-
self.instrument = instruments.GENERIC_WEATHER_STATION
|
271
300
|
self._data = self._read_data()
|
272
301
|
|
273
302
|
def _read_data(self) -> dict:
|
@@ -302,12 +331,7 @@ class KenttarovaWS(WS):
|
|
302
331
|
merged = {key: [*merged[key], *data[key]] for key in merged}
|
303
332
|
else:
|
304
333
|
merged = data
|
305
|
-
|
306
|
-
new_value = np.array(value)
|
307
|
-
if key != "time":
|
308
|
-
new_value = ma.masked_where(np.isnan(new_value), new_value)
|
309
|
-
merged[key] = new_value
|
310
|
-
return merged
|
334
|
+
return self.format_data(merged)
|
311
335
|
|
312
336
|
def convert_rainfall_rate(self) -> None:
|
313
337
|
# Rainfall rate is 10-minute averaged in mm h-1
|
@@ -320,18 +344,15 @@ class KenttarovaWS(WS):
|
|
320
344
|
|
321
345
|
|
322
346
|
class HyytialaWS(WS):
|
323
|
-
"""
|
324
|
-
Hyytiälä rain-gauge variables: a = Pluvio400 and b = Pluvio200.
|
347
|
+
"""Hyytiälä rain-gauge variables: a = Pluvio400 and b = Pluvio200.
|
325
348
|
E.g.
|
326
349
|
- AaRNRT/mm = amount of non-real-time rain total (Pluvio400) [mm]
|
327
|
-
- BbRT/mm = Bucket content in real-time (Pluvio200) [mm]
|
350
|
+
- BbRT/mm = Bucket content in real-time (Pluvio200) [mm].
|
328
351
|
"""
|
329
352
|
|
330
353
|
def __init__(self, filenames: list[str], site_meta: dict):
|
331
|
-
super().__init__()
|
354
|
+
super().__init__(site_meta)
|
332
355
|
self.filename = filenames[0]
|
333
|
-
self.site_meta = site_meta
|
334
|
-
self.instrument = instruments.GENERIC_WEATHER_STATION
|
335
356
|
self._data = self._read_data()
|
336
357
|
|
337
358
|
def _read_data(self) -> dict:
|
@@ -381,12 +402,7 @@ class HyytialaWS(WS):
|
|
381
402
|
"wind_direction": raw_data["WD/ds"],
|
382
403
|
"rainfall_rate": raw_data["AaNRT/mm"],
|
383
404
|
}
|
384
|
-
|
385
|
-
new_value = np.array(value)
|
386
|
-
if key != "time":
|
387
|
-
new_value = ma.masked_where(np.isnan(new_value), new_value)
|
388
|
-
data[key] = new_value
|
389
|
-
return data
|
405
|
+
return self.format_data(data)
|
390
406
|
|
391
407
|
def convert_pressure(self) -> None:
|
392
408
|
self.data["air_pressure"].data = (
|
@@ -394,6 +410,43 @@ class HyytialaWS(WS):
|
|
394
410
|
) # kPa to Pa
|
395
411
|
|
396
412
|
|
413
|
+
class GalatiWS(WS):
|
414
|
+
def __init__(self, filenames: list[str], site_meta: dict):
|
415
|
+
super().__init__(site_meta)
|
416
|
+
self.filename = filenames[0]
|
417
|
+
self._data = self._read_data()
|
418
|
+
|
419
|
+
def _read_data(self) -> dict:
|
420
|
+
with open(self.filename, newline="") as f:
|
421
|
+
reader = csv.DictReader(f)
|
422
|
+
raw_data: dict = {key: [] for key in reader.fieldnames} # type: ignore[union-attr]
|
423
|
+
for row in reader:
|
424
|
+
for key, value in row.items():
|
425
|
+
parsed_value: float | datetime.datetime
|
426
|
+
if key == "TimeStamp":
|
427
|
+
parsed_value = datetime.datetime.strptime(
|
428
|
+
value, "%Y-%m-%d %H:%M:%S.%f"
|
429
|
+
)
|
430
|
+
else:
|
431
|
+
try:
|
432
|
+
parsed_value = float(value)
|
433
|
+
except ValueError:
|
434
|
+
parsed_value = math.nan
|
435
|
+
raw_data[key].append(parsed_value)
|
436
|
+
data = {
|
437
|
+
"time": raw_data["TimeStamp"],
|
438
|
+
"air_temperature": raw_data["Temperature"],
|
439
|
+
"relative_humidity": raw_data["RH"],
|
440
|
+
"air_pressure": raw_data["Atmospheric_pressure"],
|
441
|
+
"rainfall_rate": raw_data["Precipitations"],
|
442
|
+
}
|
443
|
+
return self.format_data(data)
|
444
|
+
|
445
|
+
def convert_pressure(self) -> None:
|
446
|
+
mmHg2Pa = 133.322
|
447
|
+
self.data["air_pressure"].data = self.data["air_pressure"][:] * mmHg2Pa
|
448
|
+
|
449
|
+
|
397
450
|
ATTRIBUTES = {
|
398
451
|
"rainfall_amount": MetaData(
|
399
452
|
long_name="Rainfall amount",
|
@@ -145,7 +145,7 @@ def _augment_global_attributes(root_group: netCDF4.Dataset) -> None:
|
|
145
145
|
|
146
146
|
|
147
147
|
def _add_source(root_ground: netCDF4.Dataset, objects: tuple, files: tuple) -> None:
|
148
|
-
"""Generates source info for multiple files"""
|
148
|
+
"""Generates source info for multiple files."""
|
149
149
|
model, obs = objects
|
150
150
|
model_files, obs_file = files
|
151
151
|
source = f"Observation file: {os.path.basename(obs_file)}"
|
@@ -160,7 +160,7 @@ def _add_source(root_ground: netCDF4.Dataset, objects: tuple, files: tuple) -> N
|
|
160
160
|
|
161
161
|
|
162
162
|
def add_time_attribute(date: datetime) -> dict:
|
163
|
-
"""
|
163
|
+
"""Adds time attribute with correct units.
|
164
164
|
|
165
165
|
Args:
|
166
166
|
attributes: Attributes of variables.
|
@@ -114,7 +114,7 @@ REGRID_PRODUCT_ATTRIBUTES = {
|
|
114
114
|
comment=(
|
115
115
|
"Cloud fraction generated from observations and by volume,\n"
|
116
116
|
"averaged onto the models grid with height and time. Volume is\n"
|
117
|
-
"space
|
117
|
+
"space within four grid points."
|
118
118
|
),
|
119
119
|
),
|
120
120
|
"cf_A": MetaData(
|
@@ -13,7 +13,7 @@ def parse_wanted_names(
|
|
13
13
|
*,
|
14
14
|
advance: bool = False,
|
15
15
|
) -> tuple[list, list]:
|
16
|
-
"""Returns standard and advection lists of product types to plot"""
|
16
|
+
"""Returns standard and advection lists of product types to plot."""
|
17
17
|
if variables:
|
18
18
|
names = variables
|
19
19
|
else:
|
@@ -76,7 +76,7 @@ def sort_cycles(names: list, model: str) -> tuple[list, list]:
|
|
76
76
|
|
77
77
|
|
78
78
|
def read_data_characters(nc_file: str, name: str, model: str) -> tuple:
|
79
|
-
"""Gets dimensions and data for plotting"""
|
79
|
+
"""Gets dimensions and data for plotting."""
|
80
80
|
nc = netCDF4.Dataset(nc_file)
|
81
81
|
data = nc.variables[name][:]
|
82
82
|
data = mask_small_values(data, name)
|
@@ -7,9 +7,11 @@ import matplotlib as mpl
|
|
7
7
|
import matplotlib.pyplot as plt
|
8
8
|
import netCDF4
|
9
9
|
import numpy as np
|
10
|
+
from matplotlib.axes import Axes
|
10
11
|
from matplotlib.colorbar import Colorbar
|
11
12
|
from matplotlib.colors import ListedColormap
|
12
13
|
from matplotlib.patches import Patch
|
14
|
+
from matplotlib.pyplot import Figure
|
13
15
|
from mpl_toolkits.axes_grid1 import make_axes_locatable
|
14
16
|
from numpy import ma
|
15
17
|
|
@@ -187,6 +189,7 @@ def get_group_plots(
|
|
187
189
|
show (bool): Show figure before saving if True
|
188
190
|
cycle (str): Name of cycle if exists
|
189
191
|
title (bool): True or False if wanted to add title to subfig
|
192
|
+
include_xlimits (bool): Show labels at the ends of x-axis
|
190
193
|
"""
|
191
194
|
fig, ax = initialize_figure(len(names))
|
192
195
|
model_run = model
|
@@ -248,6 +251,7 @@ def get_pair_plots(
|
|
248
251
|
show (bool): Show figure before saving if True
|
249
252
|
cycle (str): Name of cycle if exists
|
250
253
|
title (bool): True or False if wanted add title to subfig
|
254
|
+
include_xlimits (bool): Show labels at the ends of x-axis
|
251
255
|
"""
|
252
256
|
variable_info = ATTRIBUTES[product]
|
253
257
|
model_ax = names[0]
|
@@ -306,6 +310,7 @@ def get_single_plots(
|
|
306
310
|
show (bool): Show figure before saving if True
|
307
311
|
cycle (str): Name of cycle if exists
|
308
312
|
title (bool): True or False if wanted to add title to subfig
|
313
|
+
include_xlimits (bool): Show labels at the ends of x-axis
|
309
314
|
"""
|
310
315
|
figs = []
|
311
316
|
axes = []
|
@@ -387,6 +392,7 @@ def get_statistic_plots(
|
|
387
392
|
image_name (str, optional): Saving name of generated fig
|
388
393
|
show (bool): Show figure before saving if True
|
389
394
|
cycle (str): Name of cycle if exists
|
395
|
+
title (bool): True or False if wanted to add title to subfig
|
390
396
|
"""
|
391
397
|
model_run = model
|
392
398
|
name = ""
|
@@ -687,8 +693,8 @@ def plot_vertical_profile(
|
|
687
693
|
ax.xaxis.grid(which="major")
|
688
694
|
|
689
695
|
|
690
|
-
def initialize_figure(n_subplots: int, stat: str = "") -> tuple:
|
691
|
-
"""Set up fig and ax object, if subplot"""
|
696
|
+
def initialize_figure(n_subplots: int, stat: str = "") -> tuple[Figure, list[Axes]]:
|
697
|
+
"""Set up fig and ax object, if subplot."""
|
692
698
|
if n_subplots <= 0:
|
693
699
|
n_subplots = 1
|
694
700
|
fig, axes = plt.subplots(n_subplots, 1, figsize=(16, 4 + (n_subplots - 1) * 4.8))
|
@@ -714,7 +720,6 @@ def initialize_figure(n_subplots: int, stat: str = "") -> tuple:
|
|
714
720
|
right=0.75,
|
715
721
|
hspace=0.2,
|
716
722
|
)
|
717
|
-
axes = axes.flatten()
|
718
723
|
if stat == "vertical" and n_subplots > 1:
|
719
724
|
fig, axes = plt.subplots(
|
720
725
|
int(n_subplots / 2),
|
@@ -731,10 +736,8 @@ def initialize_figure(n_subplots: int, stat: str = "") -> tuple:
|
|
731
736
|
right=0.75,
|
732
737
|
hspace=0.16,
|
733
738
|
)
|
734
|
-
|
735
|
-
|
736
|
-
axes = [axes]
|
737
|
-
return fig, axes
|
739
|
+
axes_list = [axes] if isinstance(axes, Axes) else axes.flatten().tolist()
|
740
|
+
return fig, axes_list
|
738
741
|
|
739
742
|
|
740
743
|
def init_colorbar(plot, axis) -> Colorbar:
|
@@ -750,7 +753,7 @@ def _set_title(
|
|
750
753
|
variable_info,
|
751
754
|
model_name: str = "",
|
752
755
|
) -> None:
|
753
|
-
"""Generates subtitles for different product types"""
|
756
|
+
"""Generates subtitles for different product types."""
|
754
757
|
parts = field_name.split("_")
|
755
758
|
if parts[0] == product:
|
756
759
|
title = _get_product_title(variable_info)
|
@@ -154,7 +154,7 @@ class ProductGrid:
|
|
154
154
|
x_ind: np.ndarray,
|
155
155
|
y_ind: np.ndarray,
|
156
156
|
) -> np.ndarray | None:
|
157
|
-
"""Reshapes True observation values to windows shape"""
|
157
|
+
"""Reshapes True observation values to windows shape."""
|
158
158
|
window_size = tl.get_obs_window_size(x_ind, y_ind)
|
159
159
|
if window_size is not None:
|
160
160
|
return self._obs_data[ind].reshape(window_size)
|
@@ -54,7 +54,7 @@ class ModelManager(DataSource):
|
|
54
54
|
self.resolution_h = self._get_horizontal_resolution()
|
55
55
|
|
56
56
|
def _read_cycle_name(self, model_file: str) -> str:
|
57
|
-
"""Get cycle name from model_metadata.py for saving variable name(s)"""
|
57
|
+
"""Get cycle name from model_metadata.py for saving variable name(s)."""
|
58
58
|
try:
|
59
59
|
cycles = self.model_info.cycle
|
60
60
|
if cycles is None:
|
@@ -68,7 +68,7 @@ class ModelManager(DataSource):
|
|
68
68
|
return ""
|
69
69
|
|
70
70
|
def _generate_products(self) -> None:
|
71
|
-
"""Process needed data of model to a ModelManager object"""
|
71
|
+
"""Process needed data of model to a ModelManager object."""
|
72
72
|
cls = importlib.import_module(__name__).ModelManager
|
73
73
|
try:
|
74
74
|
name = f"_get_{self._product}"
|
@@ -123,10 +123,10 @@ class ModelManager(DataSource):
|
|
123
123
|
return q * p / (287 * t)
|
124
124
|
|
125
125
|
def _add_variables(self) -> None:
|
126
|
-
"""Add basic variables off model and cycle"""
|
126
|
+
"""Add basic variables off model and cycle."""
|
127
127
|
|
128
128
|
def _add_common_variables() -> None:
|
129
|
-
"""Model variables that are always the same within cycles"""
|
129
|
+
"""Model variables that are always the same within cycles."""
|
130
130
|
wanted_vars = self.model_vars.common_var
|
131
131
|
if wanted_vars is None:
|
132
132
|
msg = f"Model {self.model} has no common variables"
|
@@ -140,7 +140,7 @@ class ModelManager(DataSource):
|
|
140
140
|
self.append_data(data, f"{var}")
|
141
141
|
|
142
142
|
def _add_cycle_variables() -> None:
|
143
|
-
"""Add cycle depending variables"""
|
143
|
+
"""Add cycle depending variables."""
|
144
144
|
wanted_vars = self.model_vars.cycle_var
|
145
145
|
if wanted_vars is None:
|
146
146
|
msg = f"Model {self.model} has no cycle variables"
|
@@ -160,7 +160,7 @@ class ModelManager(DataSource):
|
|
160
160
|
_add_cycle_variables()
|
161
161
|
|
162
162
|
def cut_off_extra_levels(self, data: np.ndarray) -> np.ndarray:
|
163
|
-
"""Remove unused levels (over 22km) from model data"""
|
163
|
+
"""Remove unused levels (over 22km) from model data."""
|
164
164
|
try:
|
165
165
|
level = self.model_info.level
|
166
166
|
except KeyError:
|
@@ -169,7 +169,7 @@ class ModelManager(DataSource):
|
|
169
169
|
return data[:, :level] if data.ndim > 1 else data[:level]
|
170
170
|
|
171
171
|
def _calculate_wind_speed(self) -> np.ndarray:
|
172
|
-
"""Real wind from x- and y-components"""
|
172
|
+
"""Real wind from x- and y-components."""
|
173
173
|
u = self.getvar("uwind")
|
174
174
|
v = self.getvar("vwind")
|
175
175
|
u = self.cut_off_extra_levels(u)
|
@@ -58,7 +58,7 @@ class ObservationManager(DataSource):
|
|
58
58
|
return None
|
59
59
|
|
60
60
|
def _generate_product(self) -> None:
|
61
|
-
"""Process needed data of observation to a ObservationManager object"""
|
61
|
+
"""Process needed data of observation to a ObservationManager object."""
|
62
62
|
try:
|
63
63
|
if self.obs == "cf":
|
64
64
|
self.append_data(self._generate_cf(), "cf")
|
@@ -73,7 +73,7 @@ class ObservationManager(DataSource):
|
|
73
73
|
raise
|
74
74
|
|
75
75
|
def _generate_cf(self) -> np.ndarray:
|
76
|
-
"""Generates cloud fractions using categorize bits and masking conditions"""
|
76
|
+
"""Generates cloud fractions using categorize bits and masking conditions."""
|
77
77
|
categorize_bits = CategorizeBits(self._file)
|
78
78
|
cloud_mask = self._classify_basic_mask(categorize_bits.category_bits)
|
79
79
|
return self._mask_cloud_bits(cloud_mask)
|
@@ -91,7 +91,7 @@ class ObservationManager(DataSource):
|
|
91
91
|
|
92
92
|
@staticmethod
|
93
93
|
def _mask_cloud_bits(cloud_mask: np.ndarray) -> np.ndarray:
|
94
|
-
"""Creates cloud fraction"""
|
94
|
+
"""Creates cloud fraction."""
|
95
95
|
for i in [1, 3, 4, 5]:
|
96
96
|
cloud_mask[cloud_mask == i] = 1
|
97
97
|
for i in [2, 6, 7, 8]:
|
@@ -99,7 +99,7 @@ class ObservationManager(DataSource):
|
|
99
99
|
return cloud_mask
|
100
100
|
|
101
101
|
def _check_rainrate(self) -> bool:
|
102
|
-
"""Check if rainrate in file"""
|
102
|
+
"""Check if rainrate in file."""
|
103
103
|
try:
|
104
104
|
self.getvar("rainrate")
|
105
105
|
except RuntimeError:
|
@@ -122,7 +122,7 @@ class ObservationManager(DataSource):
|
|
122
122
|
return rainrate > rainrate_threshold
|
123
123
|
|
124
124
|
def _generate_iwc_masks(self) -> None:
|
125
|
-
"""Generates ice water content variables with different masks"""
|
125
|
+
"""Generates ice water content variables with different masks."""
|
126
126
|
# TODO: Differences with CloudnetPy (status=2) and Legacy data (status=3)
|
127
127
|
iwc = self.getvar(self.obs)
|
128
128
|
iwc_status = self.getvar("iwc_retrieval_status")
|
@@ -131,21 +131,21 @@ class ObservationManager(DataSource):
|
|
131
131
|
self._mask_iwc(iwc, iwc_status)
|
132
132
|
|
133
133
|
def _mask_iwc(self, iwc: np.ndarray, iwc_status: np.ndarray) -> None:
|
134
|
-
"""Leaves only reliable data and corrected liquid attenuation"""
|
134
|
+
"""Leaves only reliable data and corrected liquid attenuation."""
|
135
135
|
iwc_mask = ma.copy(iwc)
|
136
136
|
iwc_mask[np.bitwise_and(iwc_status != 1, iwc_status != 2)] = ma.masked
|
137
137
|
self.append_data(iwc_mask, "iwc")
|
138
138
|
|
139
139
|
def _mask_iwc_att(self, iwc: np.ndarray, iwc_status: np.ndarray) -> None:
|
140
140
|
"""Leaves only where reliable data, corrected liquid attenuation
|
141
|
-
and uncorrected liquid attenuation
|
141
|
+
and uncorrected liquid attenuation.
|
142
142
|
"""
|
143
143
|
iwc_att = ma.copy(iwc)
|
144
144
|
iwc_att[iwc_status > 3] = ma.masked
|
145
145
|
self.append_data(iwc_att, "iwc_att")
|
146
146
|
|
147
147
|
def _get_rain_iwc(self, iwc_status: np.ndarray) -> None:
|
148
|
-
"""Finds columns where is rain, return boolean of x-axis shape"""
|
148
|
+
"""Finds columns where is rain, return boolean of x-axis shape."""
|
149
149
|
iwc_rain = np.zeros(iwc_status.shape, dtype=bool)
|
150
150
|
iwc_rain[iwc_status == 5] = 1
|
151
151
|
iwc_rain = np.any(iwc_rain, axis=1)
|
@@ -10,7 +10,7 @@ from cloudnetpy.model_evaluation.products.observation_products import Observatio
|
|
10
10
|
|
11
11
|
|
12
12
|
def check_model_file_list(name: str, models: list) -> None:
|
13
|
-
"""Check that files in models are from same model and date"""
|
13
|
+
"""Check that files in models are from same model and date."""
|
14
14
|
for m in models:
|
15
15
|
if name not in m:
|
16
16
|
logging.error("Invalid model file set")
|
@@ -35,16 +35,14 @@ def calculate_advection_time(
|
|
35
35
|
wind: ma.MaskedArray,
|
36
36
|
sampling: int,
|
37
37
|
) -> np.ndarray:
|
38
|
-
"""Calculates time which variable takes to go through the time window
|
38
|
+
"""Calculates time which variable takes to go through the time window.
|
39
39
|
|
40
|
-
Notes
|
40
|
+
Notes:
|
41
41
|
Wind speed is stronger in upper levels, so advection time is more
|
42
42
|
there then lower levels. Effect is small in a mid-latitudes,
|
43
43
|
but visible in a tropics.
|
44
44
|
|
45
45
|
sampling = 1 -> hour, sampling 1/6 -> 10min
|
46
|
-
|
47
|
-
References
|
48
46
|
"""
|
49
47
|
t_adv = resolution * 1000 / wind / 60**2
|
50
48
|
t_adv[t_adv.mask] = 0
|
@@ -76,7 +74,7 @@ def get_adv_indices(
|
|
76
74
|
|
77
75
|
|
78
76
|
def get_obs_window_size(ind_x: np.ndarray, ind_y: np.ndarray) -> tuple | None:
|
79
|
-
"""Returns shape (tuple) of window area, where values are True"""
|
77
|
+
"""Returns shape (tuple) of window area, where values are True."""
|
80
78
|
x = np.where(ind_x)[0]
|
81
79
|
y = np.where(ind_y)[0]
|
82
80
|
if np.any(x) and np.any(y):
|
@@ -90,5 +88,5 @@ def add_date(model_obj: ModelManager, obs_obj: ObservationManager) -> None:
|
|
90
88
|
|
91
89
|
|
92
90
|
def average_column_sum(data: np.ndarray) -> np.ndarray:
|
93
|
-
"""Returns average sum of columns which have any data"""
|
91
|
+
"""Returns average sum of columns which have any data."""
|
94
92
|
return np.nanmean(np.nansum(data, 1) > 0)
|
@@ -9,7 +9,7 @@ sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
|
9
9
|
|
10
10
|
|
11
11
|
class DayStatistics:
|
12
|
-
"""Class for calculating statistical analysis of day scale products
|
12
|
+
"""Class for calculating statistical analysis of day scale products.
|
13
13
|
|
14
14
|
Class generates one statistical method at the time with given model data
|
15
15
|
and observation data of wanted product.
|
@@ -107,7 +107,7 @@ def combine_masked_indices(
|
|
107
107
|
model: ma.MaskedArray,
|
108
108
|
observation: ma.MaskedArray,
|
109
109
|
) -> tuple[ma.MaskedArray, ma.MaskedArray]:
|
110
|
-
"""Connects two array masked indices to one and add in two array same mask"""
|
110
|
+
"""Connects two array masked indices to one and add in two array same mask."""
|
111
111
|
observation[np.where(np.isnan(observation))] = ma.masked
|
112
112
|
model[model < np.min(observation)] = ma.masked
|
113
113
|
combine_mask = model.mask + observation.mask
|
cloudnetpy/output.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
"""Functions for file writing."""
|
2
|
+
|
2
3
|
import datetime
|
3
4
|
import logging
|
4
5
|
from os import PathLike
|
@@ -140,10 +141,11 @@ def get_l1b_title(instrument: Instrument, location: str) -> str:
|
|
140
141
|
|
141
142
|
|
142
143
|
def get_references(identifier: str | None = None, extra: list | None = None) -> str:
|
143
|
-
"""
|
144
|
+
"""Returns references.
|
144
145
|
|
145
146
|
Args:
|
146
147
|
identifier: Cloudnet file type, e.g., 'iwc'.
|
148
|
+
extra: List of additional references to include
|
147
149
|
|
148
150
|
"""
|
149
151
|
references = "https://doi.org/10.21105/joss.02123"
|
cloudnetpy/plotting/plot_meta.py
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
"""Metadata for plotting module."""
|
2
|
+
|
2
3
|
from collections.abc import Sequence
|
3
4
|
from typing import NamedTuple
|
4
5
|
|
5
6
|
|
6
7
|
class PlotMeta(NamedTuple):
|
7
|
-
"""
|
8
|
-
A class representing the metadata for plotting.
|
8
|
+
"""A class representing the metadata for plotting.
|
9
9
|
|
10
10
|
Attributes:
|
11
11
|
cmap: The colormap to be used for the plot.
|