cloudnetpy 1.95.7__tar.gz → 1.96.1__tar.gz
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-1.95.7/cloudnetpy.egg-info → cloudnetpy-1.96.1}/PKG-INFO +2 -2
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/cli.py +3 -1
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/disdronator/lpm.py +13 -2
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/disdronator/parsivel.py +4 -1
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/disdronator/process.py +4 -1
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/disdronator/rd80.py +10 -1
- cloudnetpy-1.96.1/cloudnetpy/disdronator/utils.py +69 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/disdrometer.py +3 -3
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/mmcr.py +136 -3
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/output.py +12 -1
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/plotting/plot_meta.py +44 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/plotting/plotting.py +39 -2
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/products/__init__.py +1 -0
- cloudnetpy-1.96.1/cloudnetpy/products/cod.py +491 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/products/lwc.py +10 -2
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/version.py +2 -2
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1/cloudnetpy.egg-info}/PKG-INFO +2 -2
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy.egg-info/SOURCES.txt +1 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy.egg-info/requires.txt +1 -1
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/pyproject.toml +1 -1
- cloudnetpy-1.95.7/cloudnetpy/disdronator/utils.py +0 -38
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/LICENSE +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/MANIFEST.in +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/README.md +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/__init__.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/arm.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/categorize/__init__.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/categorize/atmos_utils.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/categorize/attenuation.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/categorize/attenuations/__init__.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/categorize/attenuations/gas_attenuation.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/categorize/attenuations/liquid_attenuation.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/categorize/attenuations/melting_attenuation.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/categorize/attenuations/rain_attenuation.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/categorize/categorize.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/categorize/classify.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/categorize/containers.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/categorize/disdrometer.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/categorize/droplet.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/categorize/falling.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/categorize/freezing.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/categorize/insects.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/categorize/lidar.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/categorize/melting.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/categorize/model.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/categorize/mwr.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/categorize/radar.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/cloudnetarray.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/concat_lib.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/constants.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/datasource.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/disdronator/__init__.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/exceptions.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/__init__.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/arm_ceilo.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/arm_ld.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/arm_utils.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/basta.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/bowtie.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/ceilo.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/ceilometer.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/cl61d.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/cloudnet_instrument.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/copernicus.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/da10.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/dealias.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/fd12p.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/galileo.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/hatpro.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/instruments.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/kazr.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/lufft.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/mira.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/mrr.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/mwr3c.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/mwrlos.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/nc_lidar.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/nc_radar.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/pollyxt.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/radiometrics.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/rain_e_h3.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/rpg.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/rpg_reader.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/toa5.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/vaisala.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/weather_radar.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/weather_station.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/metadata.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/__init__.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/file_handler.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/metadata.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/model_metadata.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/plotting/__init__.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/plotting/plot_meta.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/plotting/plot_tools.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/plotting/plotting.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/products/__init__.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/products/advance_methods.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/products/grid_methods.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/products/model_products.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/products/observation_products.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/products/product_resampling.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/products/tools.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/statistics/__init__.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/statistics/statistical_methods.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/tests/__init__.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/tests/e2e/__init__.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/tests/e2e/conftest.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/tests/e2e/process_cf/__init__.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/tests/e2e/process_cf/main.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/tests/e2e/process_cf/tests.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/tests/e2e/process_iwc/__init__.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/tests/e2e/process_iwc/main.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/tests/e2e/process_iwc/tests.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/tests/e2e/process_lwc/__init__.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/tests/e2e/process_lwc/main.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/tests/e2e/process_lwc/tests.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/tests/unit/__init__.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/tests/unit/conftest.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/tests/unit/test_advance_methods.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/tests/unit/test_grid_methods.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/tests/unit/test_model_products.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/tests/unit/test_observation_products.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/tests/unit/test_plot_tools.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/tests/unit/test_plotting.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/tests/unit/test_statistical_methods.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/tests/unit/test_tools.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/utils.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/plotting/__init__.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/products/classification.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/products/der.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/products/drizzle.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/products/drizzle_error.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/products/drizzle_tools.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/products/epsilon_lidar.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/products/epsilon_radar.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/products/ier.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/products/iwc.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/products/iwc_weather_radar.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/products/mie_lu_tables.nc +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/products/mwr_tools.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/products/product_tools.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/py.typed +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/utils.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy.egg-info/dependency_links.txt +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy.egg-info/entry_points.txt +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy.egg-info/top_level.txt +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/docs/source/conf.py +0 -0
- {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cloudnetpy
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.96.1
|
|
4
4
|
Summary: Python package for Cloudnet processing
|
|
5
5
|
Author: Simo Tukiainen
|
|
6
6
|
License: MIT License
|
|
@@ -38,7 +38,7 @@ Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
|
|
|
38
38
|
Requires-Python: >=3.10
|
|
39
39
|
Description-Content-Type: text/markdown
|
|
40
40
|
License-File: LICENSE
|
|
41
|
-
Requires-Dist: atmoslib>=2.
|
|
41
|
+
Requires-Dist: atmoslib>=2.4.2
|
|
42
42
|
Requires-Dist: ceilopyter>=0.2.0
|
|
43
43
|
Requires-Dist: cloudnet-api-client
|
|
44
44
|
Requires-Dist: doppy>=0.5.0
|
|
@@ -62,9 +62,11 @@ def run(args: argparse.Namespace, tmpdir: str, client: APIClient) -> None:
|
|
|
62
62
|
process_categorize = "categorize" in args.products
|
|
63
63
|
cat_filepath = None
|
|
64
64
|
if is_arm and cat_products and not process_categorize:
|
|
65
|
-
#
|
|
65
|
+
# Prefer a local file, then the portal; process only if neither exists
|
|
66
66
|
if not ARM_PRODUCTS & set(args.products):
|
|
67
67
|
cat_filepath = _find_existing_categorize(args)
|
|
68
|
+
if cat_filepath is None:
|
|
69
|
+
cat_filepath = _fetch_product(args, "categorize", client)
|
|
68
70
|
process_categorize = cat_filepath is None
|
|
69
71
|
|
|
70
72
|
# Instrument based products
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import datetime
|
|
2
|
+
import logging
|
|
2
3
|
from collections import defaultdict
|
|
3
4
|
from os import PathLike
|
|
4
5
|
from typing import TypeAlias
|
|
@@ -109,9 +110,18 @@ def read_lpm(filename: str | PathLike) -> tuple[npt.NDArray, dict[int, npt.NDArr
|
|
|
109
110
|
|
|
110
111
|
|
|
111
112
|
def read_lpm_l1(
|
|
112
|
-
time: npt.NDArray,
|
|
113
|
+
time: npt.NDArray,
|
|
114
|
+
l0: dict[int, npt.NDArray],
|
|
115
|
+
au: int | None = None,
|
|
116
|
+
altitude: float | None = None,
|
|
113
117
|
) -> DisdroL1:
|
|
114
|
-
|
|
118
|
+
if au is None:
|
|
119
|
+
area_nom = 4560.0
|
|
120
|
+
logging.warning(
|
|
121
|
+
"No AU parameter given, assuming nominal sampling area: %d mm2", area_nom
|
|
122
|
+
)
|
|
123
|
+
else:
|
|
124
|
+
area_nom = 4600.0 * 1000.0 / au
|
|
115
125
|
area_eff = area_nom * (1 - D / (2 * LASER_WIDTH))
|
|
116
126
|
data_raw = np.stack([l0[i] for i in range(81, 521)], axis=1).reshape(
|
|
117
127
|
(len(time), len(D), len(V))
|
|
@@ -128,6 +138,7 @@ def read_lpm_l1(
|
|
|
128
138
|
area_nom=area_nom,
|
|
129
139
|
area_eff=area_eff,
|
|
130
140
|
data_raw=data_raw,
|
|
141
|
+
altitude=altitude,
|
|
131
142
|
)
|
|
132
143
|
|
|
133
144
|
|
|
@@ -522,7 +522,9 @@ def read_parsivel(
|
|
|
522
522
|
return np.array(time), convert_to_numpy(data, {}, INT_KEYS, FLOAT_KEYS)
|
|
523
523
|
|
|
524
524
|
|
|
525
|
-
def read_parsivel_l1(
|
|
525
|
+
def read_parsivel_l1(
|
|
526
|
+
time: npt.NDArray, l0: dict[int, npt.NDArray], altitude: float | None = None
|
|
527
|
+
) -> DisdroL1:
|
|
526
528
|
if 93 not in l0:
|
|
527
529
|
msg = "No raw data"
|
|
528
530
|
raise ValueError(msg)
|
|
@@ -544,6 +546,7 @@ def read_parsivel_l1(time: npt.NDArray, l0: dict[int, npt.NDArray]) -> DisdroL1:
|
|
|
544
546
|
area_nom=AREA_NOM,
|
|
545
547
|
area_eff=AREA_EFF,
|
|
546
548
|
data_raw=data_raw,
|
|
549
|
+
altitude=altitude,
|
|
547
550
|
)
|
|
548
551
|
|
|
549
552
|
|
|
@@ -20,6 +20,7 @@ class DisdroL1:
|
|
|
20
20
|
area_nom: float
|
|
21
21
|
area_eff: npt.NDArray | None
|
|
22
22
|
data_raw: npt.NDArray
|
|
23
|
+
altitude: float | None
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
@dataclass
|
|
@@ -43,6 +44,7 @@ class DisdroL2:
|
|
|
43
44
|
radar_refl: npt.NDArray
|
|
44
45
|
energy_flux: npt.NDArray
|
|
45
46
|
visibility: npt.NDArray
|
|
47
|
+
altitude: float | None
|
|
46
48
|
|
|
47
49
|
|
|
48
50
|
def process_l2(l1: DisdroL1) -> DisdroL2:
|
|
@@ -112,7 +114,7 @@ def process_l2(l1: DisdroL1) -> DisdroL2:
|
|
|
112
114
|
fall_velocity = ma.divide(
|
|
113
115
|
np.sum(l1.velocity * spec, axis=2), np.sum(spec, axis=2)
|
|
114
116
|
)
|
|
115
|
-
is_rain = make_rain_mask(l1.diameter, l1.velocity)
|
|
117
|
+
is_rain = make_rain_mask(l1.diameter, l1.velocity, altitude=l1.altitude)
|
|
116
118
|
spec_rain = np.copy(spec)
|
|
117
119
|
spec_rain[:, ~is_rain] = 0
|
|
118
120
|
rain_amount = (
|
|
@@ -190,4 +192,5 @@ def process_l2(l1: DisdroL1) -> DisdroL2:
|
|
|
190
192
|
radar_refl=radar_refl_db,
|
|
191
193
|
energy_flux=energy_flux,
|
|
192
194
|
visibility=visibility,
|
|
195
|
+
altitude=l1.altitude,
|
|
193
196
|
)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import datetime
|
|
2
|
+
import logging
|
|
2
3
|
from os import PathLike
|
|
3
4
|
|
|
4
5
|
import numpy as np
|
|
@@ -35,7 +36,14 @@ def read_rd80(filename: str | PathLike) -> tuple[npt.NDArray, dict[str, npt.NDAr
|
|
|
35
36
|
return np.array(time), convert_to_numpy(data)
|
|
36
37
|
|
|
37
38
|
|
|
38
|
-
def read_rd80_l1(
|
|
39
|
+
def read_rd80_l1(
|
|
40
|
+
time: npt.NDArray, l0: dict[str, npt.NDArray], altitude: float | None = None
|
|
41
|
+
) -> DisdroL1:
|
|
42
|
+
if altitude is not None:
|
|
43
|
+
logging.warning(
|
|
44
|
+
"Altitude given but the correction for non-sea-level "
|
|
45
|
+
"conditions is not implemented for RD-80."
|
|
46
|
+
)
|
|
39
47
|
return DisdroL1(
|
|
40
48
|
diameter=D,
|
|
41
49
|
diameter_spread=D_SPREAD,
|
|
@@ -48,6 +56,7 @@ def read_rd80_l1(time: npt.NDArray, l0: dict[str, npt.NDArray]) -> DisdroL1:
|
|
|
48
56
|
area_nom=AREA_NOM,
|
|
49
57
|
area_eff=None,
|
|
50
58
|
data_raw=l0["n"],
|
|
59
|
+
altitude=altitude,
|
|
51
60
|
)
|
|
52
61
|
|
|
53
62
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from collections.abc import Container
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import numpy.typing as npt
|
|
6
|
+
from atmoslib import isa_air_density
|
|
7
|
+
from atmoslib.constants import RHO_STD
|
|
8
|
+
from numpy import ma
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def convert_to_numpy(
|
|
12
|
+
data: dict,
|
|
13
|
+
fill_values: dict | None = None,
|
|
14
|
+
int_keys: Container | None = None,
|
|
15
|
+
float_keys: Container | None = None,
|
|
16
|
+
) -> dict:
|
|
17
|
+
output = {}
|
|
18
|
+
for key, value in data.items():
|
|
19
|
+
arr = np.array(value)
|
|
20
|
+
if fill_values is not None and key in fill_values:
|
|
21
|
+
arr = ma.masked_where(arr == fill_values[key], arr)
|
|
22
|
+
if int_keys is not None and key in int_keys:
|
|
23
|
+
new_arr = arr.astype(np.int32)
|
|
24
|
+
if not np.all(arr == new_arr):
|
|
25
|
+
msg = "Cannot convert non-integer float to integer"
|
|
26
|
+
raise ValueError(msg)
|
|
27
|
+
arr = new_arr
|
|
28
|
+
elif float_keys is not None and key in float_keys:
|
|
29
|
+
arr = arr.astype(np.float32)
|
|
30
|
+
output[key] = arr
|
|
31
|
+
return output
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def make_rain_mask(
|
|
35
|
+
diameter: npt.NDArray[np.floating],
|
|
36
|
+
velocity: npt.NDArray[np.floating],
|
|
37
|
+
threshold: float = 0.5,
|
|
38
|
+
altitude: float | None = None,
|
|
39
|
+
) -> npt.NDArray[np.bool]:
|
|
40
|
+
"""Make mask for rain bins based on theoretical diameter-velocity relation.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
diameter: Diameter (mm)
|
|
44
|
+
velocity: Velocity (m s-1)
|
|
45
|
+
threshold: Maximum error to theoretical relation (1)
|
|
46
|
+
altitude: Site altitude for optional altitude correction (m)
|
|
47
|
+
|
|
48
|
+
References:
|
|
49
|
+
Atlas, D., Srivastava, R. C., & Sekhon, R. S. (1973). Doppler Radar
|
|
50
|
+
Characteristics of Precipitation at Vertical Incidence. Reviews of
|
|
51
|
+
Geophysics and Space Physics, 11(1), 1-35.
|
|
52
|
+
https://doi.org/10.1029/RG011i001p00001
|
|
53
|
+
|
|
54
|
+
Beard, K. V. (1985). Simple Altitude Adjustments to Raindrop Velocities
|
|
55
|
+
for Doppler Radar Analysis. Journal of Atmospheric and Oceanic
|
|
56
|
+
Technology, 2(4), 468-471.
|
|
57
|
+
https://doi.org/10.1175/1520-0426(1985)002%3C0468:SAATRV%3E2.0.CO;2
|
|
58
|
+
"""
|
|
59
|
+
v = 9.65 - 10.3 * np.exp(-0.6 * diameter) # Eq. 24, Atlas et al. (1973)
|
|
60
|
+
if altitude is None:
|
|
61
|
+
logging.warning(
|
|
62
|
+
"No altitude given, no correction applied for non-sea-level conditions."
|
|
63
|
+
)
|
|
64
|
+
else:
|
|
65
|
+
rho = isa_air_density(altitude)
|
|
66
|
+
m = 0.375 + 0.025 * diameter # Eq. 3, Beard (1985)
|
|
67
|
+
v = (RHO_STD / rho) ** m * v # Eq. 1, Beard (1985)
|
|
68
|
+
e = np.abs((velocity - v[:, np.newaxis]) / v[:, np.newaxis])
|
|
69
|
+
return e < threshold
|
|
@@ -71,7 +71,7 @@ def parsivel2nc(
|
|
|
71
71
|
field_separator=field_separator,
|
|
72
72
|
decimal_separator=decimal_separator,
|
|
73
73
|
),
|
|
74
|
-
read_parsivel_l1,
|
|
74
|
+
functools.partial(read_parsivel_l1, altitude=site_meta.get("altitude")),
|
|
75
75
|
ATTRIBUTES,
|
|
76
76
|
disdrometer_file,
|
|
77
77
|
output_file,
|
|
@@ -117,7 +117,7 @@ def thies2nc(
|
|
|
117
117
|
return _process_disdrometer(
|
|
118
118
|
Thies,
|
|
119
119
|
read_lpm,
|
|
120
|
-
functools.partial(read_lpm_l1, au=au),
|
|
120
|
+
functools.partial(read_lpm_l1, au=au, altitude=site_meta.get("altitude")),
|
|
121
121
|
ATTRIBUTES,
|
|
122
122
|
disdrometer_file,
|
|
123
123
|
output_file,
|
|
@@ -160,7 +160,7 @@ def rd802nc(
|
|
|
160
160
|
return _process_disdrometer(
|
|
161
161
|
Rd80,
|
|
162
162
|
read_rd80,
|
|
163
|
-
read_rd80_l1,
|
|
163
|
+
functools.partial(read_rd80_l1, altitude=site_meta.get("altitude")),
|
|
164
164
|
RD80_ATTRIBUTES,
|
|
165
165
|
input_file,
|
|
166
166
|
output_file,
|
|
@@ -7,6 +7,7 @@ from uuid import UUID
|
|
|
7
7
|
|
|
8
8
|
import netCDF4
|
|
9
9
|
import numpy as np
|
|
10
|
+
from numpy import ma
|
|
10
11
|
|
|
11
12
|
from cloudnetpy import output, utils
|
|
12
13
|
from cloudnetpy.exceptions import ValidTimeStampError
|
|
@@ -19,7 +20,19 @@ from cloudnetpy.instruments.dealias import (
|
|
|
19
20
|
)
|
|
20
21
|
from cloudnetpy.instruments.instruments import MMCR
|
|
21
22
|
from cloudnetpy.instruments.nc_radar import NcRadar
|
|
22
|
-
from cloudnetpy.metadata import COMMON_ATTRIBUTES
|
|
23
|
+
from cloudnetpy.metadata import COMMON_ATTRIBUTES, MetaData
|
|
24
|
+
|
|
25
|
+
CDR_SNR_LIMIT = -5.0 # dB, co-polar SNR required for a valid CDR pixel
|
|
26
|
+
# Conservative fallback: the highest floor observed at SGP (2005). The
|
|
27
|
+
# floor drifted from -11 dB in early 2005 to -16...-19 dB from late 2005 on.
|
|
28
|
+
# Over-subtracting only weakens the depolarization signal, whereas
|
|
29
|
+
# under-subtracting turns rain and cloud into false insects.
|
|
30
|
+
CDR_FLOOR_DEFAULT = -11.0 # dB, cross-talk floor if it cannot be estimated
|
|
31
|
+
CDR_FLOOR_MARGIN = 1.5 # dB, added to the floor before subtraction
|
|
32
|
+
CDR_FLOOR_RANGE = 4000 # m, pixels above this are used to estimate the floor
|
|
33
|
+
CDR_FLOOR_MIN_PIXELS = 200
|
|
34
|
+
CDR_MAX_TIME_DIFF = 15 # s, max distance to the nearest dual-pol profile
|
|
35
|
+
LDR_MIN = -35.0 # dB
|
|
23
36
|
|
|
24
37
|
|
|
25
38
|
def mmcr2nc(
|
|
@@ -36,8 +49,10 @@ def mmcr2nc(
|
|
|
36
49
|
resolutions and sensitivities. Only profiles from a single mode are used.
|
|
37
50
|
Doppler velocities are folded at the Nyquist velocity of the mode, which
|
|
38
51
|
is only about 5 m/s in the default general (GE) mode, and are dealiased
|
|
39
|
-
using velocity continuity.
|
|
40
|
-
|
|
52
|
+
using velocity continuity. Circular depolarization ratio is measured
|
|
53
|
+
only in the dual-polarization mode (about one profile per 25 s). It is
|
|
54
|
+
mapped to the selected mode by nearest time, corrected for the antenna
|
|
55
|
+
cross-talk floor and stored as `ldr`.
|
|
41
56
|
|
|
42
57
|
Args:
|
|
43
58
|
raw_file: Daily ARM `mmcrmom` netCDF file, e.g.
|
|
@@ -81,6 +96,7 @@ def mmcr2nc(
|
|
|
81
96
|
mmcr.init_data(keymap)
|
|
82
97
|
mmcr.init_mode(mode)
|
|
83
98
|
mmcr.screen_mode()
|
|
99
|
+
mmcr.add_depolarization()
|
|
84
100
|
if date is not None:
|
|
85
101
|
mmcr.check_date(date)
|
|
86
102
|
mmcr.sort_timestamps()
|
|
@@ -95,6 +111,8 @@ def mmcr2nc(
|
|
|
95
111
|
mmcr.add_height()
|
|
96
112
|
mmcr.test_if_all_masked()
|
|
97
113
|
attributes = output.add_time_attribute(ATTRIBUTES, mmcr.date)
|
|
114
|
+
if mmcr.cdr_floor is not None:
|
|
115
|
+
attributes["ldr"] = _ldr_attributes(mmcr.cdr_floor)
|
|
98
116
|
output.update_attributes(mmcr.data, attributes)
|
|
99
117
|
output.save_level1b(mmcr, output_file, uuid)
|
|
100
118
|
return uuid
|
|
@@ -114,6 +132,7 @@ class Mmcr(NcRadar):
|
|
|
114
132
|
self.instrument = MMCR
|
|
115
133
|
self.date = utils.get_epoch(self.dataset["time"].units).date()
|
|
116
134
|
self.mode_index: int = 0
|
|
135
|
+
self.cdr_floor: float | None = None
|
|
117
136
|
self._add_geolocation_from_file()
|
|
118
137
|
|
|
119
138
|
def init_mode(self, mode: str) -> None:
|
|
@@ -164,6 +183,87 @@ class Mmcr(NcRadar):
|
|
|
164
183
|
# MMCR is a fixed vertically pointing radar
|
|
165
184
|
self.append_data(0.0, "zenith_angle")
|
|
166
185
|
|
|
186
|
+
def add_depolarization(self) -> None:
|
|
187
|
+
"""Adds cross-talk corrected circular depolarization ratio as ldr.
|
|
188
|
+
|
|
189
|
+
CDR is available only in the dual-polarization mode. Each profile of
|
|
190
|
+
the selected mode gets the CDR of the nearest dual-pol profile in
|
|
191
|
+
time. The antenna cross-talk floor, estimated as the median CDR of
|
|
192
|
+
high-altitude pixels, is subtracted in linear units.
|
|
193
|
+
"""
|
|
194
|
+
if "CircularDepolarizationRatio" not in self.dataset.variables:
|
|
195
|
+
return
|
|
196
|
+
try:
|
|
197
|
+
ind_co, ind_cross = self._find_dualpol_modes()
|
|
198
|
+
except ValueError:
|
|
199
|
+
logging.info("No dual-polarization mode found, ldr not available")
|
|
200
|
+
return
|
|
201
|
+
mode_num = np.array(self.dataset["ModeNum"][:])
|
|
202
|
+
time_raw = np.array(self.dataset["time"][:])
|
|
203
|
+
co_ind = np.where(mode_num == ind_co)[0]
|
|
204
|
+
cross_ind = np.where(mode_num == ind_cross)[0]
|
|
205
|
+
if len(co_ind) == 0 or len(cross_ind) == 0:
|
|
206
|
+
logging.info("No dual-polarization profiles found, ldr not available")
|
|
207
|
+
return
|
|
208
|
+
# Pair each cross-pol profile with the co-pol profile of the same pulse
|
|
209
|
+
pair_ind = co_ind[_nearest_index(time_raw[co_ind], time_raw[cross_ind])]
|
|
210
|
+
n_heights = int(self.dataset["NumHeights"][ind_cross])
|
|
211
|
+
cdr = ma.masked_invalid(
|
|
212
|
+
self.dataset["CircularDepolarizationRatio"][cross_ind, :n_heights]
|
|
213
|
+
)
|
|
214
|
+
snr = ma.masked_invalid(
|
|
215
|
+
self.dataset["SignalToNoiseRatio"][pair_ind, :n_heights]
|
|
216
|
+
)
|
|
217
|
+
cdr[ma.filled(snr < CDR_SNR_LIMIT, fill_value=True)] = ma.masked
|
|
218
|
+
altitude = float(self.dataset["alt"][:])
|
|
219
|
+
range_cross = (
|
|
220
|
+
np.array(self.dataset["heights"][ind_cross, :n_heights]) - altitude
|
|
221
|
+
)
|
|
222
|
+
self.cdr_floor = self._estimate_cdr_floor(cdr, range_cross)
|
|
223
|
+
ldr = _remove_cdr_floor(cdr, self.cdr_floor)
|
|
224
|
+
# Map to the profiles and range gates of the selected mode
|
|
225
|
+
time_sel = np.array(self.data["time"][:]) * 3600
|
|
226
|
+
time_ind = _nearest_index(time_raw[cross_ind], time_sel)
|
|
227
|
+
range_ind = _nearest_index(range_cross, self.data["range"][:])
|
|
228
|
+
ldr = ldr[time_ind][:, range_ind]
|
|
229
|
+
too_far = np.abs(time_raw[cross_ind][time_ind] - time_sel) > CDR_MAX_TIME_DIFF
|
|
230
|
+
ldr[too_far, :] = ma.masked
|
|
231
|
+
self.append_data(ldr, "ldr")
|
|
232
|
+
|
|
233
|
+
def _estimate_cdr_floor(
|
|
234
|
+
self, cdr: ma.MaskedArray, range_cross: np.ndarray
|
|
235
|
+
) -> float:
|
|
236
|
+
is_high = ~ma.getmaskarray(cdr) & (range_cross[np.newaxis, :] > CDR_FLOOR_RANGE)
|
|
237
|
+
if np.count_nonzero(is_high) < CDR_FLOOR_MIN_PIXELS:
|
|
238
|
+
logging.warning(
|
|
239
|
+
"Not enough high-altitude pixels to estimate CDR floor, "
|
|
240
|
+
"using conservative default %s dB",
|
|
241
|
+
CDR_FLOOR_DEFAULT,
|
|
242
|
+
)
|
|
243
|
+
return CDR_FLOOR_DEFAULT
|
|
244
|
+
floor = float(np.median(cdr[is_high].compressed()))
|
|
245
|
+
logging.info("Estimated CDR cross-talk floor: %.1f dB", floor)
|
|
246
|
+
return floor
|
|
247
|
+
|
|
248
|
+
def _find_dualpol_modes(self) -> tuple[int, int]:
|
|
249
|
+
"""Returns indices of the co- and cross-polar dual-pol receiver modes.
|
|
250
|
+
|
|
251
|
+
Mode names vary between files (e.g. "PO_Receiver0" in 2005,
|
|
252
|
+
"DualPol_Receiver0" in 2008), so the receiver metadata is used.
|
|
253
|
+
"""
|
|
254
|
+
for key in ("NumReceivers", "ReceiverNumber"):
|
|
255
|
+
if key not in self.dataset.variables:
|
|
256
|
+
msg = f"Variable {key} not found"
|
|
257
|
+
raise ValueError(msg)
|
|
258
|
+
n_receivers = ma.filled(self.dataset["NumReceivers"][:], 0)
|
|
259
|
+
receiver = ma.filled(self.dataset["ReceiverNumber"][:], 0)
|
|
260
|
+
co = np.where((n_receivers == 2) & (receiver == 1))[0]
|
|
261
|
+
cross = np.where((n_receivers == 2) & (receiver == 2))[0]
|
|
262
|
+
if len(co) != 1 or len(cross) != 1:
|
|
263
|
+
msg = "Dual-polarization modes not found"
|
|
264
|
+
raise ValueError(msg)
|
|
265
|
+
return int(co[0]), int(cross[0])
|
|
266
|
+
|
|
167
267
|
def _find_mode_index(self, mode: str) -> int:
|
|
168
268
|
var = self.dataset["ModeDescription"]
|
|
169
269
|
var.set_auto_mask(False)
|
|
@@ -180,6 +280,39 @@ class Mmcr(NcRadar):
|
|
|
180
280
|
self.site_meta = read_geolocation(self.dataset, self.site_meta)
|
|
181
281
|
|
|
182
282
|
|
|
283
|
+
def _nearest_index(reference: np.ndarray, query: np.ndarray) -> np.ndarray:
|
|
284
|
+
"""Returns index of the nearest `reference` value for each `query` value."""
|
|
285
|
+
if len(reference) < 2:
|
|
286
|
+
return np.zeros(len(query), dtype=int)
|
|
287
|
+
order = np.argsort(reference)
|
|
288
|
+
sorted_ref = reference[order]
|
|
289
|
+
pos = np.searchsorted(sorted_ref, query)
|
|
290
|
+
pos = np.clip(pos, 1, len(sorted_ref) - 1)
|
|
291
|
+
use_left = np.abs(query - sorted_ref[pos - 1]) <= np.abs(sorted_ref[pos] - query)
|
|
292
|
+
return order[np.where(use_left, pos - 1, pos)]
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _remove_cdr_floor(cdr: ma.MaskedArray, floor: float) -> ma.MaskedArray:
|
|
296
|
+
"""Subtracts the cross-talk floor in linear units, clipping at LDR_MIN."""
|
|
297
|
+
linear = 10 ** (cdr / 10) - 10 ** ((floor + CDR_FLOOR_MARGIN) / 10)
|
|
298
|
+
ldr = ma.masked_where(linear <= 0, linear)
|
|
299
|
+
ldr = 10 * ma.log10(ldr)
|
|
300
|
+
ldr = ldr.filled(LDR_MIN)
|
|
301
|
+
ldr = np.maximum(ldr, LDR_MIN)
|
|
302
|
+
return ma.masked_where(ma.getmaskarray(cdr), ldr)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _ldr_attributes(floor: float) -> MetaData:
|
|
306
|
+
return COMMON_ATTRIBUTES["ldr"]._replace(
|
|
307
|
+
comment=(
|
|
308
|
+
"Circular depolarization ratio from the dual-polarization mode, "
|
|
309
|
+
"mapped to the selected mode by nearest time. The antenna cross-talk "
|
|
310
|
+
f"floor of {floor:.1f} dB has been subtracted in linear units, "
|
|
311
|
+
f"and values are clipped at {LDR_MIN} dB."
|
|
312
|
+
),
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
183
316
|
ATTRIBUTES = {
|
|
184
317
|
"correction_bits": CORRECTION_BITS_ATTRIBUTES,
|
|
185
318
|
"v": DEALIASED_V_ATTRIBUTES,
|
|
@@ -181,7 +181,7 @@ def get_references(identifier: str | None = None, extra: list | None = None) ->
|
|
|
181
181
|
match identifier:
|
|
182
182
|
case "der":
|
|
183
183
|
references += (
|
|
184
|
-
", https://doi.org/10.1175/1520-0426(2002)019
|
|
184
|
+
", https://doi.org/10.1175/1520-0426(2002)019%3C0835:TROSCD%3E2.0.CO;2"
|
|
185
185
|
)
|
|
186
186
|
case "ier":
|
|
187
187
|
references += (
|
|
@@ -195,6 +195,14 @@ def get_references(identifier: str | None = None, extra: list | None = None) ->
|
|
|
195
195
|
references += ", https://doi.org/10.1175/JAM2340.1"
|
|
196
196
|
case "drizzle":
|
|
197
197
|
references += ", https://doi.org/10.1175/JAM-2181.1"
|
|
198
|
+
case "cod":
|
|
199
|
+
references += (
|
|
200
|
+
", https://doi.org/10.1175/BAMS-88-6-883"
|
|
201
|
+
", https://doi.org/10.1175/1520-0426(2002)019%3C0835:TROSCD%3E2.0.CO;2"
|
|
202
|
+
", https://doi.org/10.1175/JAM2340.1"
|
|
203
|
+
", https://doi.org/10.1175/JAM2543.1"
|
|
204
|
+
", https://doi.org/10.5194/amt-13-5335-2020"
|
|
205
|
+
)
|
|
198
206
|
case "epsilon-radar":
|
|
199
207
|
references += (
|
|
200
208
|
", https://doi.org/10.5194/amt-13-5335-2020"
|
|
@@ -472,6 +480,7 @@ def _get_identifier(short_id: str) -> str:
|
|
|
472
480
|
"classification",
|
|
473
481
|
"der",
|
|
474
482
|
"ier",
|
|
483
|
+
"cod",
|
|
475
484
|
"classification-voodoo",
|
|
476
485
|
"epsilon-radar",
|
|
477
486
|
)
|
|
@@ -486,6 +495,8 @@ def _get_identifier(short_id: str) -> str:
|
|
|
486
495
|
return "ice effective radius"
|
|
487
496
|
if short_id == "der":
|
|
488
497
|
return "droplet effective radius"
|
|
498
|
+
if short_id == "cod":
|
|
499
|
+
return "cloud optical depth"
|
|
489
500
|
if short_id == "epsilon-radar":
|
|
490
501
|
return "dissipation rate of turbulent kinetic energy"
|
|
491
502
|
return short_id
|
|
@@ -170,6 +170,15 @@ _CLABEL = {
|
|
|
170
170
|
("Unquantifiable", _COLORS["seaweed_roll"]),
|
|
171
171
|
("Undetected", _COLORS["skyblue"]),
|
|
172
172
|
),
|
|
173
|
+
"extinction_retrieval_status": (
|
|
174
|
+
("_No cloud", _COLORS["white"]),
|
|
175
|
+
("Liquid", _COLORS["lightblue"]),
|
|
176
|
+
("Liquid, assumed radius", _COLORS["yellow"]),
|
|
177
|
+
("Ice", _COLORS["lightsteel"]),
|
|
178
|
+
("Ice, corrected atten.", _COLORS["skyblue"]),
|
|
179
|
+
("Liquid & ice", _COLORS["darkpurple"]),
|
|
180
|
+
("No retrieval", _COLORS["seaweed_roll"]),
|
|
181
|
+
),
|
|
173
182
|
"dominant_hydrometeor_type": (
|
|
174
183
|
("_Clear sky", _COLORS["white"]),
|
|
175
184
|
("Ice", _COLORS["lightgray"]),
|
|
@@ -684,5 +693,40 @@ ATTRIBUTES = {
|
|
|
684
693
|
"dominant_hydrometeor_type": PlotMeta(
|
|
685
694
|
clabel=_CLABEL["dominant_hydrometeor_type"],
|
|
686
695
|
),
|
|
696
|
+
"extinction_liquid": PlotMeta(
|
|
697
|
+
cmap="Blues",
|
|
698
|
+
plot_range=(1e-4, 1e-1),
|
|
699
|
+
log_scale=True,
|
|
700
|
+
),
|
|
701
|
+
"extinction_ice": PlotMeta(
|
|
702
|
+
plot_range=(1e-5, 1e-2),
|
|
703
|
+
log_scale=True,
|
|
704
|
+
),
|
|
705
|
+
"extinction_retrieval_status": PlotMeta(
|
|
706
|
+
clabel=_CLABEL["extinction_retrieval_status"],
|
|
707
|
+
),
|
|
708
|
+
"extinction_liquid_error": PlotMeta(
|
|
709
|
+
cmap="RdYlGn_r",
|
|
710
|
+
plot_range=(0, 5),
|
|
711
|
+
),
|
|
712
|
+
"extinction_ice_error": PlotMeta(
|
|
713
|
+
cmap="RdYlGn_r",
|
|
714
|
+
plot_range=(0, 5),
|
|
715
|
+
),
|
|
716
|
+
"optical_depth_error": PlotMeta(
|
|
717
|
+
zero_line=True,
|
|
718
|
+
),
|
|
719
|
+
"optical_depth": PlotMeta(
|
|
720
|
+
plot_range=(0.02, 1000),
|
|
721
|
+
log_scale=True,
|
|
722
|
+
),
|
|
723
|
+
"optical_depth_liquid": PlotMeta(
|
|
724
|
+
plot_range=(0.02, 1000),
|
|
725
|
+
log_scale=True,
|
|
726
|
+
),
|
|
727
|
+
"optical_depth_ice": PlotMeta(
|
|
728
|
+
plot_range=(0.02, 1000),
|
|
729
|
+
log_scale=True,
|
|
730
|
+
),
|
|
687
731
|
},
|
|
688
732
|
}
|
|
@@ -904,6 +904,9 @@ class Plot1D(Plot):
|
|
|
904
904
|
units = self._convert_units()
|
|
905
905
|
if self._plot_meta.mask_zeros:
|
|
906
906
|
self._mask_zeros()
|
|
907
|
+
is_zero = ~ma.getmaskarray(self._data) & (ma.filled(self._data, 1) == 0)
|
|
908
|
+
if self._is_log:
|
|
909
|
+
self._mask_non_positive()
|
|
907
910
|
self._mark_gaps(figure_data)
|
|
908
911
|
self._ax.plot(
|
|
909
912
|
figure_data.time_including_gaps,
|
|
@@ -914,14 +917,34 @@ class Plot1D(Plot):
|
|
|
914
917
|
)
|
|
915
918
|
if self._plot_meta.moving_average:
|
|
916
919
|
self._plot_moving_average(figure_data, hacky_freq_ind)
|
|
917
|
-
if self._plot_meta.zero_line:
|
|
920
|
+
if self._plot_meta.zero_line and not self._is_log:
|
|
918
921
|
self._ax.axhline(0, color="black", alpha=0.5, label="_nolegend_")
|
|
919
922
|
self._fill_between_data_gaps(figure_data)
|
|
920
|
-
|
|
923
|
+
if self._is_log:
|
|
924
|
+
self._ax.set_yscale("log")
|
|
925
|
+
y_limits = self._get_y_limits()
|
|
926
|
+
self.sub_plot.set_yax(ylabel=units, y_limits=y_limits)
|
|
927
|
+
if self._is_log and np.any(is_zero):
|
|
928
|
+
self._plot_zeros(figure_data.time[is_zero], y_limits[0])
|
|
921
929
|
pos = self._ax.get_position()
|
|
922
930
|
self._ax.set_position((pos.x0, pos.y0, pos.width * 0.965, pos.height))
|
|
923
931
|
self._plot_flags(figure_data)
|
|
924
932
|
|
|
933
|
+
def _plot_zeros(self, time: ndarray, y_min: float) -> None:
|
|
934
|
+
"""Marks zero values along the bottom of a logarithmic axis."""
|
|
935
|
+
is_cloud_variable = self.sub_plot.variable.name.startswith("optical_depth")
|
|
936
|
+
self._ax.plot(
|
|
937
|
+
time,
|
|
938
|
+
np.full(len(time), y_min * 1.3),
|
|
939
|
+
color="lightgrey",
|
|
940
|
+
marker=".",
|
|
941
|
+
lw=0,
|
|
942
|
+
markersize=3,
|
|
943
|
+
label="Clear sky" if is_cloud_variable else "Zero",
|
|
944
|
+
zorder=_get_zorder("data"),
|
|
945
|
+
)
|
|
946
|
+
self._ax.legend(markerscale=3, numpoints=1, frameon=False)
|
|
947
|
+
|
|
925
948
|
def _plot_flags(self, figure_data: FigureData) -> None:
|
|
926
949
|
if figure_data.is_mwrpy_product():
|
|
927
950
|
flags = self._read_flagged_data(figure_data)
|
|
@@ -1021,9 +1044,18 @@ class Plot1D(Plot):
|
|
|
1021
1044
|
frameon=False,
|
|
1022
1045
|
)
|
|
1023
1046
|
|
|
1047
|
+
def _mask_non_positive(self) -> None:
|
|
1048
|
+
self._data = ma.masked_less_equal(self._data, 0)
|
|
1049
|
+
self._data_orig = ma.masked_less_equal(self._data_orig, 0)
|
|
1050
|
+
|
|
1024
1051
|
def _get_y_limits(self) -> tuple[float, float]:
|
|
1025
1052
|
percent_gap = 0.05
|
|
1026
1053
|
fallback = (-percent_gap, percent_gap)
|
|
1054
|
+
if self._is_log:
|
|
1055
|
+
if self._plot_meta.plot_range is not None:
|
|
1056
|
+
return self._plot_meta.plot_range
|
|
1057
|
+
valid = self._data[~ma.getmaskarray(self._data)]
|
|
1058
|
+
return (valid.min() / 2, valid.max() * 2) if valid.size else (0.1, 10)
|
|
1027
1059
|
if ma.all(self._data.mask):
|
|
1028
1060
|
return fallback
|
|
1029
1061
|
min_data = self._data.min()
|
|
@@ -1066,6 +1098,9 @@ class Plot1D(Plot):
|
|
|
1066
1098
|
) -> None:
|
|
1067
1099
|
time = figure_data.time.copy()
|
|
1068
1100
|
data = self._data_orig.copy()
|
|
1101
|
+
if self._is_log:
|
|
1102
|
+
# Average in log space so large values do not dominate
|
|
1103
|
+
data = ma.log10(ma.masked_less_equal(data, 0))
|
|
1069
1104
|
|
|
1070
1105
|
if figure_data.is_mwrpy_product() or self.sub_plot.variable.name in (
|
|
1071
1106
|
"tb",
|
|
@@ -1097,6 +1132,8 @@ class Plot1D(Plot):
|
|
|
1097
1132
|
)
|
|
1098
1133
|
else:
|
|
1099
1134
|
sma = self._calculate_moving_average(data1, time1, window=5)
|
|
1135
|
+
if self._is_log:
|
|
1136
|
+
sma = 10**sma
|
|
1100
1137
|
gap_time = _get_max_gap_in_minutes(figure_data)
|
|
1101
1138
|
gaps = self._find_time_gap_indices(time1, max_gap_min=gap_time) + 1
|
|
1102
1139
|
|