cloudnetpy 1.49.9__py3-none-any.whl → 1.87.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cloudnetpy/categorize/__init__.py +1 -2
- cloudnetpy/categorize/atmos_utils.py +297 -67
- cloudnetpy/categorize/attenuation.py +31 -0
- cloudnetpy/categorize/attenuations/__init__.py +37 -0
- cloudnetpy/categorize/attenuations/gas_attenuation.py +30 -0
- cloudnetpy/categorize/attenuations/liquid_attenuation.py +84 -0
- cloudnetpy/categorize/attenuations/melting_attenuation.py +78 -0
- cloudnetpy/categorize/attenuations/rain_attenuation.py +84 -0
- cloudnetpy/categorize/categorize.py +332 -156
- cloudnetpy/categorize/classify.py +127 -125
- cloudnetpy/categorize/containers.py +107 -76
- cloudnetpy/categorize/disdrometer.py +40 -0
- cloudnetpy/categorize/droplet.py +23 -21
- cloudnetpy/categorize/falling.py +53 -24
- cloudnetpy/categorize/freezing.py +25 -12
- cloudnetpy/categorize/insects.py +35 -23
- cloudnetpy/categorize/itu.py +243 -0
- cloudnetpy/categorize/lidar.py +36 -41
- cloudnetpy/categorize/melting.py +34 -26
- cloudnetpy/categorize/model.py +84 -37
- cloudnetpy/categorize/mwr.py +18 -14
- cloudnetpy/categorize/radar.py +215 -102
- cloudnetpy/cli.py +578 -0
- cloudnetpy/cloudnetarray.py +43 -89
- cloudnetpy/concat_lib.py +218 -78
- cloudnetpy/constants.py +28 -10
- cloudnetpy/datasource.py +61 -86
- cloudnetpy/exceptions.py +49 -20
- cloudnetpy/instruments/__init__.py +5 -0
- cloudnetpy/instruments/basta.py +29 -12
- cloudnetpy/instruments/bowtie.py +135 -0
- cloudnetpy/instruments/ceilo.py +138 -115
- cloudnetpy/instruments/ceilometer.py +164 -80
- cloudnetpy/instruments/cl61d.py +21 -5
- cloudnetpy/instruments/cloudnet_instrument.py +74 -36
- cloudnetpy/instruments/copernicus.py +108 -30
- cloudnetpy/instruments/da10.py +54 -0
- cloudnetpy/instruments/disdrometer/common.py +126 -223
- cloudnetpy/instruments/disdrometer/parsivel.py +453 -94
- cloudnetpy/instruments/disdrometer/thies.py +254 -87
- cloudnetpy/instruments/fd12p.py +201 -0
- cloudnetpy/instruments/galileo.py +65 -23
- cloudnetpy/instruments/hatpro.py +123 -49
- cloudnetpy/instruments/instruments.py +113 -1
- cloudnetpy/instruments/lufft.py +39 -17
- cloudnetpy/instruments/mira.py +268 -61
- cloudnetpy/instruments/mrr.py +187 -0
- cloudnetpy/instruments/nc_lidar.py +19 -8
- cloudnetpy/instruments/nc_radar.py +109 -55
- cloudnetpy/instruments/pollyxt.py +135 -51
- cloudnetpy/instruments/radiometrics.py +313 -59
- cloudnetpy/instruments/rain_e_h3.py +171 -0
- cloudnetpy/instruments/rpg.py +321 -189
- cloudnetpy/instruments/rpg_reader.py +74 -40
- cloudnetpy/instruments/toa5.py +49 -0
- cloudnetpy/instruments/vaisala.py +95 -343
- cloudnetpy/instruments/weather_station.py +774 -105
- cloudnetpy/metadata.py +90 -19
- cloudnetpy/model_evaluation/file_handler.py +55 -52
- cloudnetpy/model_evaluation/metadata.py +46 -20
- cloudnetpy/model_evaluation/model_metadata.py +1 -1
- cloudnetpy/model_evaluation/plotting/plot_tools.py +32 -37
- cloudnetpy/model_evaluation/plotting/plotting.py +327 -117
- cloudnetpy/model_evaluation/products/advance_methods.py +92 -83
- cloudnetpy/model_evaluation/products/grid_methods.py +88 -63
- cloudnetpy/model_evaluation/products/model_products.py +43 -35
- cloudnetpy/model_evaluation/products/observation_products.py +41 -35
- cloudnetpy/model_evaluation/products/product_resampling.py +17 -7
- cloudnetpy/model_evaluation/products/tools.py +29 -20
- cloudnetpy/model_evaluation/statistics/statistical_methods.py +30 -20
- cloudnetpy/model_evaluation/tests/e2e/conftest.py +3 -3
- cloudnetpy/model_evaluation/tests/e2e/process_cf/main.py +9 -5
- cloudnetpy/model_evaluation/tests/e2e/process_cf/tests.py +15 -14
- cloudnetpy/model_evaluation/tests/e2e/process_iwc/main.py +9 -5
- cloudnetpy/model_evaluation/tests/e2e/process_iwc/tests.py +15 -14
- cloudnetpy/model_evaluation/tests/e2e/process_lwc/main.py +9 -5
- cloudnetpy/model_evaluation/tests/e2e/process_lwc/tests.py +15 -14
- cloudnetpy/model_evaluation/tests/unit/conftest.py +42 -41
- cloudnetpy/model_evaluation/tests/unit/test_advance_methods.py +41 -48
- cloudnetpy/model_evaluation/tests/unit/test_grid_methods.py +216 -194
- cloudnetpy/model_evaluation/tests/unit/test_model_products.py +23 -21
- cloudnetpy/model_evaluation/tests/unit/test_observation_products.py +37 -38
- cloudnetpy/model_evaluation/tests/unit/test_plot_tools.py +43 -40
- cloudnetpy/model_evaluation/tests/unit/test_plotting.py +30 -36
- cloudnetpy/model_evaluation/tests/unit/test_statistical_methods.py +68 -31
- cloudnetpy/model_evaluation/tests/unit/test_tools.py +33 -26
- cloudnetpy/model_evaluation/utils.py +2 -1
- cloudnetpy/output.py +170 -111
- cloudnetpy/plotting/__init__.py +2 -1
- cloudnetpy/plotting/plot_meta.py +562 -822
- cloudnetpy/plotting/plotting.py +1142 -704
- cloudnetpy/products/__init__.py +1 -0
- cloudnetpy/products/classification.py +370 -88
- cloudnetpy/products/der.py +85 -55
- cloudnetpy/products/drizzle.py +77 -34
- cloudnetpy/products/drizzle_error.py +15 -11
- cloudnetpy/products/drizzle_tools.py +79 -59
- cloudnetpy/products/epsilon.py +211 -0
- cloudnetpy/products/ier.py +27 -50
- cloudnetpy/products/iwc.py +55 -48
- cloudnetpy/products/lwc.py +96 -70
- cloudnetpy/products/mwr_tools.py +186 -0
- cloudnetpy/products/product_tools.py +170 -128
- cloudnetpy/utils.py +455 -240
- cloudnetpy/version.py +2 -2
- {cloudnetpy-1.49.9.dist-info → cloudnetpy-1.87.3.dist-info}/METADATA +44 -40
- cloudnetpy-1.87.3.dist-info/RECORD +127 -0
- {cloudnetpy-1.49.9.dist-info → cloudnetpy-1.87.3.dist-info}/WHEEL +1 -1
- cloudnetpy-1.87.3.dist-info/entry_points.txt +2 -0
- docs/source/conf.py +2 -2
- cloudnetpy/categorize/atmos.py +0 -361
- cloudnetpy/products/mwr_multi.py +0 -68
- cloudnetpy/products/mwr_single.py +0 -75
- cloudnetpy-1.49.9.dist-info/RECORD +0 -112
- {cloudnetpy-1.49.9.dist-info → cloudnetpy-1.87.3.dist-info/licenses}/LICENSE +0 -0
- {cloudnetpy-1.49.9.dist-info → cloudnetpy-1.87.3.dist-info}/top_level.txt +0 -0
cloudnetpy/cli.py
ADDED
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import base64
|
|
3
|
+
import datetime
|
|
4
|
+
import gzip
|
|
5
|
+
import hashlib
|
|
6
|
+
import importlib
|
|
7
|
+
import logging
|
|
8
|
+
import re
|
|
9
|
+
import shutil
|
|
10
|
+
from os import PathLike
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from tempfile import TemporaryDirectory
|
|
13
|
+
from typing import TYPE_CHECKING, Final, Literal, cast
|
|
14
|
+
|
|
15
|
+
import requests
|
|
16
|
+
from cloudnet_api_client import APIClient, CloudnetAPIError
|
|
17
|
+
from cloudnet_api_client.containers import Instrument, ProductMetadata, RawMetadata
|
|
18
|
+
|
|
19
|
+
from cloudnetpy import concat_lib, instruments
|
|
20
|
+
from cloudnetpy.categorize import CategorizeInput, generate_categorize
|
|
21
|
+
from cloudnetpy.exceptions import PlottingError
|
|
22
|
+
from cloudnetpy.plotting import generate_figure
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from collections.abc import Callable
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
cloudnet_api_url: Final = "https://cloudnet.fmi.fi/api/"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def run(args: argparse.Namespace, tmpdir: str, client: APIClient) -> None:
|
|
32
|
+
cat_files = {}
|
|
33
|
+
|
|
34
|
+
# Instrument based products
|
|
35
|
+
if source_instruments := _get_source_instruments(args.products, client):
|
|
36
|
+
for product, possible_instruments in source_instruments.items():
|
|
37
|
+
if not possible_instruments:
|
|
38
|
+
continue
|
|
39
|
+
meta = _fetch_raw_meta(possible_instruments, args, client)
|
|
40
|
+
instrument = _select_instrument(meta, product)
|
|
41
|
+
if not instrument:
|
|
42
|
+
logging.info("No instrument found for %s", product)
|
|
43
|
+
continue
|
|
44
|
+
meta = _filter_by_instrument(meta, instrument)
|
|
45
|
+
meta = _filter_by_suffix(meta, product)
|
|
46
|
+
if not meta:
|
|
47
|
+
logging.info("No suitable data available for %s", product)
|
|
48
|
+
continue
|
|
49
|
+
output_filepath = _process_instrument_product(
|
|
50
|
+
product, meta, instrument, tmpdir, args, client
|
|
51
|
+
)
|
|
52
|
+
_plot(output_filepath, product, args)
|
|
53
|
+
cat_files[product] = output_filepath
|
|
54
|
+
|
|
55
|
+
prod_sources = _get_product_sources(args.products, client)
|
|
56
|
+
|
|
57
|
+
# Categorize based products
|
|
58
|
+
if "categorize" in args.products:
|
|
59
|
+
cat_filepath = _process_categorize(cat_files, args, client)
|
|
60
|
+
_plot(cat_filepath, "categorize", args)
|
|
61
|
+
else:
|
|
62
|
+
cat_filepath = None
|
|
63
|
+
cat_products = [p for p in prod_sources if "categorize" in prod_sources[p]]
|
|
64
|
+
for product in cat_products:
|
|
65
|
+
if cat_filepath is None:
|
|
66
|
+
cat_filepath = _fetch_product(args, "categorize", client)
|
|
67
|
+
if cat_filepath is None:
|
|
68
|
+
logging.info("No categorize data available for {}")
|
|
69
|
+
break
|
|
70
|
+
l2_filename = _process_cat_product(product, cat_filepath)
|
|
71
|
+
_plot(l2_filename, product, args)
|
|
72
|
+
|
|
73
|
+
# MWR-L1c based products
|
|
74
|
+
mwrpy_products = [p for p in prod_sources if "mwr-l1c" in prod_sources[p]]
|
|
75
|
+
for product in mwrpy_products:
|
|
76
|
+
if "mwr-l1c" in cat_files:
|
|
77
|
+
mwrpy_filepath = cat_files.get("mwr-l1c")
|
|
78
|
+
else:
|
|
79
|
+
mwrpy_filepath = _fetch_product(args, "mwr-l1c", client)
|
|
80
|
+
if mwrpy_filepath is None:
|
|
81
|
+
logging.info("No MWR-L1c data available for %s", product)
|
|
82
|
+
break
|
|
83
|
+
l2_filename = _process_mwrpy_product(product, mwrpy_filepath, args)
|
|
84
|
+
_plot(l2_filename, product, args)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _process_categorize(
|
|
88
|
+
input_files: dict, args: argparse.Namespace, client: APIClient
|
|
89
|
+
) -> str | None:
|
|
90
|
+
cat_filepath = _create_categorize_filepath(args)
|
|
91
|
+
|
|
92
|
+
input_files["model"] = _fetch_model(args, client)
|
|
93
|
+
if input_files["model"] is None:
|
|
94
|
+
logging.info("No model data available for this date.")
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
for product in ("radar", "lidar", "disdrometer"):
|
|
98
|
+
if product not in input_files and (
|
|
99
|
+
filepath := _fetch_product(args, product, client)
|
|
100
|
+
):
|
|
101
|
+
input_files[product] = filepath
|
|
102
|
+
|
|
103
|
+
if mwr := _fetch_mwr(args, client):
|
|
104
|
+
input_files["mwr"] = mwr
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
logging.info("Processing categorize...")
|
|
108
|
+
generate_categorize(cast("CategorizeInput", input_files), cat_filepath)
|
|
109
|
+
logging.info("Processed categorize to %s", cat_filepath)
|
|
110
|
+
except NameError:
|
|
111
|
+
logging.info("No data available for this date.")
|
|
112
|
+
return None
|
|
113
|
+
return cat_filepath
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _fetch_mwr(args: argparse.Namespace, client: APIClient) -> str | None:
|
|
117
|
+
mwr_sources = [
|
|
118
|
+
("mwr-single", None),
|
|
119
|
+
("mwr", None),
|
|
120
|
+
("radar", "rpg-fmcw-35"),
|
|
121
|
+
("radar", "rpg-fmcw-94"),
|
|
122
|
+
]
|
|
123
|
+
for product, source in mwr_sources:
|
|
124
|
+
mwr = _fetch_product(args, product, client, source=source)
|
|
125
|
+
if mwr:
|
|
126
|
+
return mwr
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _process_instrument_product(
|
|
131
|
+
product: str,
|
|
132
|
+
meta: list[RawMetadata],
|
|
133
|
+
instrument: Instrument,
|
|
134
|
+
tmpdir: str,
|
|
135
|
+
args: argparse.Namespace,
|
|
136
|
+
client: APIClient,
|
|
137
|
+
) -> str | None:
|
|
138
|
+
output_filepath = _create_instrument_filepath(instrument, args)
|
|
139
|
+
site_obj = meta[0].site
|
|
140
|
+
site_meta: dict = {
|
|
141
|
+
"name": site_obj.human_readable_name,
|
|
142
|
+
"latitude": site_obj.latitude,
|
|
143
|
+
"longitude": site_obj.longitude,
|
|
144
|
+
"altitude": site_obj.altitude,
|
|
145
|
+
}
|
|
146
|
+
input_files: list[Path] | Path
|
|
147
|
+
input_files = _fetch_raw(meta, args, client)
|
|
148
|
+
input_files = [_unzip_gz_file(f) for f in input_files]
|
|
149
|
+
if args.dl:
|
|
150
|
+
return None
|
|
151
|
+
input_folder = input_files[0].parent
|
|
152
|
+
calibration = _get_calibration(instrument, args, client)
|
|
153
|
+
fun: Callable
|
|
154
|
+
match (product, instrument.instrument_id):
|
|
155
|
+
case ("radar", _id) if "mira" in _id:
|
|
156
|
+
fun = instruments.mira2nc
|
|
157
|
+
case ("radar", _id) if "rpg" in _id:
|
|
158
|
+
fun = instruments.rpg2nc
|
|
159
|
+
input_files = input_folder
|
|
160
|
+
case ("radar", _id) if "basta" in _id:
|
|
161
|
+
fun = instruments.basta2nc
|
|
162
|
+
_check_input(input_files)
|
|
163
|
+
input_files = input_files[0]
|
|
164
|
+
case ("radar", _id) if "copernicus" in _id:
|
|
165
|
+
fun = instruments.copernicus2nc
|
|
166
|
+
case ("radar", _id) if "galileo" in _id:
|
|
167
|
+
fun = instruments.galileo2nc
|
|
168
|
+
case ("disdrometer", _id) if "parsivel" in _id:
|
|
169
|
+
fun = instruments.parsivel2nc
|
|
170
|
+
case ("disdrometer", _id) if "thies" in _id:
|
|
171
|
+
fun = instruments.thies2nc
|
|
172
|
+
input_files = _concatenate_(input_files, tmpdir)
|
|
173
|
+
case ("lidar", _id) if "pollyxt" in _id:
|
|
174
|
+
site_meta["snr_limit"] = calibration.get("snr_limit", 25)
|
|
175
|
+
fun = instruments.pollyxt2nc
|
|
176
|
+
case ("lidar", _id) if _id == "cl61d":
|
|
177
|
+
fun = instruments.ceilo2nc
|
|
178
|
+
variables = ["x_pol", "p_pol", "beta_att", "time", "tilt_angle"]
|
|
179
|
+
concat_file = Path(tmpdir) / "tmp.nc"
|
|
180
|
+
concat_lib.bundle_netcdf_files(
|
|
181
|
+
input_files,
|
|
182
|
+
datetime.date.fromisoformat(args.date),
|
|
183
|
+
concat_file,
|
|
184
|
+
variables=variables,
|
|
185
|
+
)
|
|
186
|
+
input_files = concat_file
|
|
187
|
+
site_meta["model"] = instrument.instrument_id
|
|
188
|
+
case ("lidar", _id):
|
|
189
|
+
fun = instruments.ceilo2nc
|
|
190
|
+
input_files = _concatenate_(input_files, tmpdir)
|
|
191
|
+
site_meta["model"] = instrument.instrument_id
|
|
192
|
+
if factor := calibration.get("calibration_factor"):
|
|
193
|
+
site_meta["calibration_factor"] = factor
|
|
194
|
+
case ("mwr", _id):
|
|
195
|
+
fun = instruments.hatpro2nc
|
|
196
|
+
input_files = input_folder
|
|
197
|
+
case ("mwr-l1c", _id):
|
|
198
|
+
fun = instruments.hatpro2l1c
|
|
199
|
+
site_meta = {**site_meta, **calibration}
|
|
200
|
+
coefficients, links = _fetch_coefficient_files(calibration, tmpdir)
|
|
201
|
+
site_meta["coefficientFiles"] = coefficients
|
|
202
|
+
site_meta["coefficientLinks"] = links
|
|
203
|
+
input_files = input_folder
|
|
204
|
+
case ("mrr", _id):
|
|
205
|
+
fun = instruments.mrr2nc
|
|
206
|
+
case ("weather-station", _id):
|
|
207
|
+
fun = instruments.ws2nc
|
|
208
|
+
logging.info("Processing %s...", product)
|
|
209
|
+
fun(input_files, output_filepath, site_meta, date=args.date)
|
|
210
|
+
logging.info("Processed %s: %s", product, output_filepath)
|
|
211
|
+
return output_filepath
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _concatenate_(input_files: list[Path], tmpdir: str) -> Path:
|
|
215
|
+
if len(input_files) > 1:
|
|
216
|
+
concat_file = Path(tmpdir) / "tmp.nc"
|
|
217
|
+
try:
|
|
218
|
+
concat_lib.concatenate_files(input_files, concat_file)
|
|
219
|
+
except OSError:
|
|
220
|
+
concat_lib.concatenate_text_files(input_files, concat_file)
|
|
221
|
+
return concat_file
|
|
222
|
+
return input_files[0]
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _fetch_coefficient_files(
|
|
226
|
+
calibration: dict, tmpdir: str
|
|
227
|
+
) -> tuple[list[str], list[str]]:
|
|
228
|
+
msg = "No calibration coefficients found"
|
|
229
|
+
if not (coeffs := calibration.get("retrieval_coefficients")):
|
|
230
|
+
raise ValueError(msg)
|
|
231
|
+
if not (links := coeffs[0].get("links")):
|
|
232
|
+
raise ValueError(msg)
|
|
233
|
+
coefficient_paths = []
|
|
234
|
+
for filename in links:
|
|
235
|
+
res = requests.get(filename, timeout=60)
|
|
236
|
+
res.raise_for_status()
|
|
237
|
+
filepath = Path(tmpdir) / Path(filename).name
|
|
238
|
+
filepath.write_bytes(res.content)
|
|
239
|
+
coefficient_paths.append(str(filepath))
|
|
240
|
+
return coefficient_paths, links
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _get_calibration(
|
|
244
|
+
instrument: Instrument, args: argparse.Namespace, client: APIClient
|
|
245
|
+
) -> dict:
|
|
246
|
+
try:
|
|
247
|
+
calibration = client.calibration(
|
|
248
|
+
date=args.date,
|
|
249
|
+
instrument_pid=instrument.pid,
|
|
250
|
+
)
|
|
251
|
+
return calibration.get("data", {})
|
|
252
|
+
except CloudnetAPIError:
|
|
253
|
+
return {}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _create_instrument_filepath(
|
|
257
|
+
instrument: Instrument, args: argparse.Namespace
|
|
258
|
+
) -> str:
|
|
259
|
+
folder = _create_output_folder("instrument", args)
|
|
260
|
+
pid = _shorten_pid(instrument.pid)
|
|
261
|
+
filename = (
|
|
262
|
+
f"{args.date.replace('-', '')}_{args.site}_{instrument.instrument_id}_{pid}.nc"
|
|
263
|
+
)
|
|
264
|
+
return str(folder / filename)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _create_categorize_filepath(args: argparse.Namespace) -> str:
|
|
268
|
+
folder = _create_output_folder("geophysical", args)
|
|
269
|
+
filename = f"{args.date.replace('-', '')}_{args.site}_categorize.nc"
|
|
270
|
+
return str(folder / filename)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _create_input_folder(end_point: str, args: argparse.Namespace) -> Path:
|
|
274
|
+
folder = args.input / args.site / args.date / end_point
|
|
275
|
+
folder.mkdir(parents=True, exist_ok=True)
|
|
276
|
+
return folder
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _create_output_folder(end_point: str, args: argparse.Namespace) -> Path:
|
|
280
|
+
folder = args.output / args.site / args.date / end_point
|
|
281
|
+
folder.mkdir(parents=True, exist_ok=True)
|
|
282
|
+
return folder
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _fetch_raw_meta(
|
|
286
|
+
instruments: list[str], args: argparse.Namespace, client: APIClient
|
|
287
|
+
) -> list[RawMetadata]:
|
|
288
|
+
return client.raw_files(
|
|
289
|
+
site_id=args.site,
|
|
290
|
+
date=args.date,
|
|
291
|
+
instrument_id=instruments,
|
|
292
|
+
status=["uploaded", "processed"],
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _filter_by_instrument(
|
|
297
|
+
meta: list[RawMetadata], instrument: Instrument
|
|
298
|
+
) -> list[RawMetadata]:
|
|
299
|
+
return [m for m in meta if m.instrument.pid == instrument.pid]
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _filter_by_suffix(meta: list[RawMetadata], product: str) -> list[RawMetadata]:
|
|
303
|
+
if product == "radar":
|
|
304
|
+
meta = [m for m in meta if not m.filename.lower().endswith(".lv0")]
|
|
305
|
+
elif product == "mwr":
|
|
306
|
+
meta = [m for m in meta if re.search(r"\.(lwp|iwv)", m.filename, re.IGNORECASE)]
|
|
307
|
+
elif product == "mwr-l1c":
|
|
308
|
+
meta = [m for m in meta if not m.filename.lower().endswith(".nc")]
|
|
309
|
+
return meta
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _get_source_instruments(
|
|
313
|
+
products: list[str], client: APIClient
|
|
314
|
+
) -> dict[str, list[str]]:
|
|
315
|
+
source_instruments = {}
|
|
316
|
+
for product in products:
|
|
317
|
+
prod, model = _parse_instrument(product)
|
|
318
|
+
all_possible = client.product(prod).source_instrument_ids
|
|
319
|
+
if all_possible and (match := [i for i in all_possible if i == model]):
|
|
320
|
+
source_instruments[prod] = match
|
|
321
|
+
else:
|
|
322
|
+
source_instruments[prod] = list(all_possible)
|
|
323
|
+
return source_instruments
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _get_product_sources(
|
|
327
|
+
products: list[str], client: APIClient
|
|
328
|
+
) -> dict[str, list[str]]:
|
|
329
|
+
source_products = {}
|
|
330
|
+
for product in products:
|
|
331
|
+
prod, _ = _parse_instrument(product)
|
|
332
|
+
product_obj = client.product(prod)
|
|
333
|
+
if product_obj.source_product_ids:
|
|
334
|
+
source_products[prod] = list(product_obj.source_product_ids)
|
|
335
|
+
return source_products
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _parse_instrument(s: str) -> tuple[str, str | None]:
|
|
339
|
+
if "[" in s and s.endswith("]"):
|
|
340
|
+
name = s[: s.index("[")]
|
|
341
|
+
value = s[s.index("[") + 1 : -1]
|
|
342
|
+
else:
|
|
343
|
+
name = s
|
|
344
|
+
value = None
|
|
345
|
+
return name, value
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _select_instrument(meta: list[RawMetadata], product: str) -> Instrument | None:
|
|
349
|
+
instruments = _get_unique_instruments(meta)
|
|
350
|
+
if len(instruments) == 0:
|
|
351
|
+
logging.info("No instruments found")
|
|
352
|
+
return None
|
|
353
|
+
if len(instruments) > 1:
|
|
354
|
+
logging.info("Multiple instruments found for %s", product)
|
|
355
|
+
logging.info("Please specify which one to use")
|
|
356
|
+
for i, instrument in enumerate(instruments):
|
|
357
|
+
logging.info("%d: %s", i + 1, instrument.name)
|
|
358
|
+
ind = int(input("Select: ")) - 1
|
|
359
|
+
selected_instrument = instruments[ind]
|
|
360
|
+
else:
|
|
361
|
+
selected_instrument = instruments[0]
|
|
362
|
+
logging.info("Single instrument found: %s", selected_instrument.name)
|
|
363
|
+
return selected_instrument
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _get_unique_instruments(meta: list[RawMetadata]) -> list[Instrument]:
|
|
367
|
+
unique_instruments = list({m.instrument for m in meta})
|
|
368
|
+
return sorted(unique_instruments, key=lambda x: x.name)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _fetch_product(
|
|
372
|
+
args: argparse.Namespace, product: str, client: APIClient, source: str | None = None
|
|
373
|
+
) -> str | None:
|
|
374
|
+
meta = client.files(product_id=product, date=args.date, site_id=args.site)
|
|
375
|
+
if source:
|
|
376
|
+
meta = [
|
|
377
|
+
m
|
|
378
|
+
for m in meta
|
|
379
|
+
if m.instrument is not None and m.instrument.instrument_id == source
|
|
380
|
+
]
|
|
381
|
+
if not meta:
|
|
382
|
+
logging.info("No data available for %s", product)
|
|
383
|
+
return None
|
|
384
|
+
if len(meta) > 1:
|
|
385
|
+
logging.info(
|
|
386
|
+
"Multiple files for %s ... taking the first but some logic needed", product
|
|
387
|
+
)
|
|
388
|
+
meta = [meta[0]]
|
|
389
|
+
suffix = "geophysical" if "geophysical" in meta[0].product.type else "instrument"
|
|
390
|
+
folder = _create_output_folder(suffix, args)
|
|
391
|
+
return _download_product_file(meta, folder, client)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _fetch_model(args: argparse.Namespace, client: APIClient) -> str | None:
|
|
395
|
+
files = client.files(product_id="model", date=args.date, site_id=args.site)
|
|
396
|
+
if not files:
|
|
397
|
+
logging.info("No model data available for this date")
|
|
398
|
+
return None
|
|
399
|
+
folder = _create_output_folder("instrument", args)
|
|
400
|
+
return _download_product_file(files, folder, client)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _fetch_raw(
|
|
404
|
+
metadata: list[RawMetadata], args: argparse.Namespace, client: APIClient
|
|
405
|
+
) -> list[Path]:
|
|
406
|
+
pid = _shorten_pid(metadata[0].instrument.pid)
|
|
407
|
+
instrument = f"{metadata[0].instrument.instrument_id}_{pid}"
|
|
408
|
+
folder = _create_input_folder(instrument, args)
|
|
409
|
+
return client.download(metadata, output_directory=folder)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _download_product_file(
|
|
413
|
+
meta: list[ProductMetadata], folder: Path, client: APIClient
|
|
414
|
+
) -> str:
|
|
415
|
+
if len(meta) > 1:
|
|
416
|
+
msg = "Multiple product files found"
|
|
417
|
+
raise ValueError(msg)
|
|
418
|
+
filepath = folder / meta[0].filename
|
|
419
|
+
if filepath.exists():
|
|
420
|
+
logging.info("Existing file found: %s", filepath)
|
|
421
|
+
return str(filepath)
|
|
422
|
+
logging.info("Downloading file: %s", filepath)
|
|
423
|
+
return str(client.download(meta, output_directory=folder)[0])
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def _shorten_pid(pid: str) -> str:
|
|
427
|
+
return pid.split(".")[-1][:8]
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _check_input(files: list) -> None:
|
|
431
|
+
if len(files) > 1:
|
|
432
|
+
msg = "Multiple input files found"
|
|
433
|
+
raise ValueError(msg)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _plot(
|
|
437
|
+
filepath: PathLike | str | None, product: str, args: argparse.Namespace
|
|
438
|
+
) -> None:
|
|
439
|
+
if filepath is None or (not args.plot and not args.show):
|
|
440
|
+
return
|
|
441
|
+
res = requests.get(f"{cloudnet_api_url}products/variables", timeout=60)
|
|
442
|
+
res.raise_for_status()
|
|
443
|
+
variables = next(var["variables"] for var in res.json() if var["id"] == product)
|
|
444
|
+
variables = [var["id"].split("-")[-1] for var in variables]
|
|
445
|
+
image_name = str(filepath).replace(".nc", ".png") if args.plot else None
|
|
446
|
+
try:
|
|
447
|
+
generate_figure(
|
|
448
|
+
filepath,
|
|
449
|
+
variables,
|
|
450
|
+
show=args.show,
|
|
451
|
+
output_filename=image_name,
|
|
452
|
+
)
|
|
453
|
+
except PlottingError as e:
|
|
454
|
+
logging.info("Failed to plot %s: %s", product, e)
|
|
455
|
+
if args.plot:
|
|
456
|
+
logging.info("Plotted %s: %s", product, image_name)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _process_cat_product(product: str, categorize_file: str) -> str:
|
|
460
|
+
output_file = categorize_file.replace("categorize", product)
|
|
461
|
+
module = importlib.import_module("cloudnetpy.products")
|
|
462
|
+
getattr(module, f"generate_{product}")(categorize_file, output_file)
|
|
463
|
+
logging.info("Processed %s: %s", product, output_file)
|
|
464
|
+
return output_file
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _process_mwrpy_product(
|
|
468
|
+
product: str, mwr_l1c_file: str, args: argparse.Namespace
|
|
469
|
+
) -> str:
|
|
470
|
+
filename = f"{args.date}_{args.site}_{product}.nc"
|
|
471
|
+
output_file = _create_output_folder("geophysical", args) / filename
|
|
472
|
+
module = importlib.import_module("cloudnetpy.products")
|
|
473
|
+
getattr(module, f"generate_{product.replace('-', '_')}")(mwr_l1c_file, output_file)
|
|
474
|
+
logging.info("Processed %s: %s", product, output_file)
|
|
475
|
+
return str(output_file)
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def _parse_products(product_argument: str, client: APIClient) -> list[str]:
|
|
479
|
+
products = product_argument.split(",")
|
|
480
|
+
valid_options = [p.id for p in client.products()]
|
|
481
|
+
valid_products = []
|
|
482
|
+
for product in products:
|
|
483
|
+
prod, _ = _parse_instrument(product)
|
|
484
|
+
if prod in valid_options:
|
|
485
|
+
valid_products.append(product)
|
|
486
|
+
return valid_products
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def main() -> None:
|
|
490
|
+
client = APIClient()
|
|
491
|
+
parser = argparse.ArgumentParser(
|
|
492
|
+
description="Command line interface for running CloudnetPy."
|
|
493
|
+
)
|
|
494
|
+
parser.add_argument(
|
|
495
|
+
"-s",
|
|
496
|
+
"--site",
|
|
497
|
+
type=str,
|
|
498
|
+
help="Site",
|
|
499
|
+
required=True,
|
|
500
|
+
choices=[site.id for site in client.sites()],
|
|
501
|
+
metavar="SITE",
|
|
502
|
+
)
|
|
503
|
+
parser.add_argument(
|
|
504
|
+
"-d", "--date", type=str, help="Date in YYYY-MM-DD", required=True
|
|
505
|
+
)
|
|
506
|
+
parser.add_argument(
|
|
507
|
+
"-p",
|
|
508
|
+
"--products",
|
|
509
|
+
type=lambda arg: _parse_products(arg, client),
|
|
510
|
+
help=(
|
|
511
|
+
"Products to process, e.g. 'radar' or 'classification'. If the site "
|
|
512
|
+
"has many instruments, you can specify the instrument in brackets, "
|
|
513
|
+
"e.g. radar[mira-35]."
|
|
514
|
+
),
|
|
515
|
+
required=True,
|
|
516
|
+
)
|
|
517
|
+
parser.add_argument("--input", type=Path, help="Input path", default="input/")
|
|
518
|
+
parser.add_argument("--output", type=Path, help="Output path", default="output/")
|
|
519
|
+
parser.add_argument(
|
|
520
|
+
"--plot",
|
|
521
|
+
help="Plot the processed data",
|
|
522
|
+
default=False,
|
|
523
|
+
action=argparse.BooleanOptionalAction,
|
|
524
|
+
)
|
|
525
|
+
parser.add_argument(
|
|
526
|
+
"--show",
|
|
527
|
+
help="Show plotted image",
|
|
528
|
+
default=False,
|
|
529
|
+
action=argparse.BooleanOptionalAction,
|
|
530
|
+
)
|
|
531
|
+
parser.add_argument(
|
|
532
|
+
"--dl",
|
|
533
|
+
help="Download raw data only",
|
|
534
|
+
default=False,
|
|
535
|
+
action=argparse.BooleanOptionalAction,
|
|
536
|
+
)
|
|
537
|
+
args = parser.parse_args()
|
|
538
|
+
|
|
539
|
+
logger = logging.getLogger()
|
|
540
|
+
logger.setLevel(logging.INFO)
|
|
541
|
+
handler = logging.StreamHandler()
|
|
542
|
+
formatter = logging.Formatter("%(levelname)s: %(message)s")
|
|
543
|
+
handler.setFormatter(formatter)
|
|
544
|
+
logger.handlers = [handler]
|
|
545
|
+
|
|
546
|
+
with TemporaryDirectory() as tmpdir:
|
|
547
|
+
run(args, tmpdir, client)
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def md5sum(filename: str | PathLike, *, is_base64: bool = False) -> str:
|
|
551
|
+
"""Calculates hash of file using md5."""
|
|
552
|
+
return _calc_hash_sum(filename, "md5", is_base64=is_base64)
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def _calc_hash_sum(
|
|
556
|
+
filename: str | PathLike, method: Literal["sha256", "md5"], *, is_base64: bool
|
|
557
|
+
) -> str:
|
|
558
|
+
hash_sum = getattr(hashlib, method)()
|
|
559
|
+
with open(filename, "rb") as f:
|
|
560
|
+
for byte_block in iter(lambda: f.read(4096), b""):
|
|
561
|
+
hash_sum.update(byte_block)
|
|
562
|
+
if is_base64:
|
|
563
|
+
return base64.encodebytes(hash_sum.digest()).decode("utf-8").strip()
|
|
564
|
+
return hash_sum.hexdigest()
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def _unzip_gz_file(path_in: Path) -> Path:
|
|
568
|
+
if path_in.suffix != ".gz":
|
|
569
|
+
return path_in
|
|
570
|
+
path_out = path_in.with_suffix("")
|
|
571
|
+
logging.debug("Decompressing %s to %s", path_in, path_out)
|
|
572
|
+
with gzip.open(path_in, "rb") as file_in, open(path_out, "wb") as file_out:
|
|
573
|
+
shutil.copyfileobj(file_in, file_out)
|
|
574
|
+
return path_out
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
if __name__ == "__main__":
|
|
578
|
+
main()
|