cloudnetpy 1.88.2__tar.gz → 1.89.0__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.88.2/cloudnetpy.egg-info → cloudnetpy-1.89.0}/PKG-INFO +1 -1
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/classify.py +22 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/model.py +22 -1
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/radar.py +66 -63
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/cli.py +69 -10
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/mira.py +22 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/nc_radar.py +13 -13
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/plotting/plot_meta.py +2 -2
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/plotting/plotting.py +2 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/plotting/plot_meta.py +9 -5
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/plotting/plotting.py +28 -12
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/products/classification.py +5 -1
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/utils.py +14 -2
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/version.py +2 -2
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0/cloudnetpy.egg-info}/PKG-INFO +1 -1
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/LICENSE +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/MANIFEST.in +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/README.md +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/__init__.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/__init__.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/atmos_utils.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/attenuation.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/attenuations/__init__.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/attenuations/gas_attenuation.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/attenuations/liquid_attenuation.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/attenuations/melting_attenuation.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/attenuations/rain_attenuation.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/categorize.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/containers.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/disdrometer.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/droplet.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/falling.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/freezing.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/insects.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/itu.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/lidar.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/melting.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/mwr.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/cloudnetarray.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/concat_lib.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/constants.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/datasource.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/disdronator/__init__.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/disdronator/lpm.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/disdronator/parsivel.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/disdronator/rd80.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/disdronator/utils.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/exceptions.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/__init__.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/basta.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/bowtie.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/ceilo.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/ceilometer.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/cl61d.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/cloudnet_instrument.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/copernicus.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/da10.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/disdrometer/__init__.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/disdrometer/common.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/disdrometer/parsivel.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/disdrometer/rd80.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/disdrometer/thies.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/fd12p.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/galileo.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/hatpro.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/instruments.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/lufft.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/mrr.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/nc_lidar.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/pollyxt.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/radiometrics.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/rain_e_h3.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/rpg.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/rpg_reader.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/toa5.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/vaisala.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/weather_station.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/metadata.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/__init__.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/file_handler.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/metadata.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/model_metadata.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/plotting/__init__.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/plotting/plot_tools.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/products/__init__.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/products/advance_methods.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/products/grid_methods.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/products/model_products.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/products/observation_products.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/products/product_resampling.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/products/tools.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/statistics/__init__.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/statistics/statistical_methods.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/__init__.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/e2e/__init__.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/e2e/conftest.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/e2e/process_cf/__init__.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/e2e/process_cf/main.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/e2e/process_cf/tests.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/e2e/process_iwc/__init__.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/e2e/process_iwc/main.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/e2e/process_iwc/tests.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/e2e/process_lwc/__init__.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/e2e/process_lwc/main.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/e2e/process_lwc/tests.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/unit/__init__.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/unit/conftest.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/unit/test_advance_methods.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/unit/test_grid_methods.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/unit/test_model_products.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/unit/test_observation_products.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/unit/test_plot_tools.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/unit/test_plotting.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/unit/test_statistical_methods.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/unit/test_tools.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/utils.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/output.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/plotting/__init__.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/products/__init__.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/products/der.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/products/drizzle.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/products/drizzle_error.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/products/drizzle_tools.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/products/epsilon.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/products/ier.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/products/iwc.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/products/lwc.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/products/mie_lu_tables.nc +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/products/mwr_tools.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/products/product_tools.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/py.typed +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy.egg-info/SOURCES.txt +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy.egg-info/dependency_links.txt +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy.egg-info/entry_points.txt +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy.egg-info/requires.txt +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy.egg-info/top_level.txt +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/docs/source/conf.py +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/pyproject.toml +0 -0
- {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/setup.cfg +0 -0
|
@@ -91,6 +91,7 @@ def classify_measurements(data: Observations) -> ClassificationResult:
|
|
|
91
91
|
bits.aerosol = _find_aerosols(obs, bits)
|
|
92
92
|
bits.aerosol[filtered_ice] = False
|
|
93
93
|
_fix_super_cold_liquid(obs, bits)
|
|
94
|
+
_reclassify_ice_with_high_ldr(obs, bits)
|
|
94
95
|
|
|
95
96
|
return ClassificationResult(
|
|
96
97
|
category_bits=bits,
|
|
@@ -122,6 +123,27 @@ def fetch_quality(
|
|
|
122
123
|
)
|
|
123
124
|
|
|
124
125
|
|
|
126
|
+
def _reclassify_ice_with_high_ldr(obs: ClassData, bits: CategoryBits) -> None:
|
|
127
|
+
"""Reclassifies ice pixels with high LDR as insects.
|
|
128
|
+
|
|
129
|
+
Ice particles typically have LDR below -13 dB. Higher values in the
|
|
130
|
+
freezing region (excluding the melting layer) indicate insects, which
|
|
131
|
+
can be present in temperatures down to about -10 C.
|
|
132
|
+
"""
|
|
133
|
+
if not hasattr(obs, "ldr"):
|
|
134
|
+
return
|
|
135
|
+
ldr_limit = -13
|
|
136
|
+
temp_limit = T0 - 10
|
|
137
|
+
is_ice = bits.falling & bits.freezing & ~bits.droplet & ~bits.melting
|
|
138
|
+
high_ldr = ~ma.getmaskarray(obs.ldr) & (obs.ldr > ldr_limit)
|
|
139
|
+
warm_enough = obs.tw > temp_limit
|
|
140
|
+
reclassify = is_ice & high_ldr & warm_enough
|
|
141
|
+
bits.insect[reclassify] = True
|
|
142
|
+
bits.falling[reclassify] = False
|
|
143
|
+
has_lidar = ~obs.beta.mask
|
|
144
|
+
bits.aerosol[reclassify & has_lidar] = True
|
|
145
|
+
|
|
146
|
+
|
|
125
147
|
def _fix_super_cold_liquid(obs: ClassData, bits: CategoryBits) -> None:
|
|
126
148
|
"""Supercooled liquid droplets do not exist in atmosphere below around -38 C."""
|
|
127
149
|
t_limit = T0 - 38
|
|
@@ -16,6 +16,7 @@ from cloudnetpy.categorize.itu import (
|
|
|
16
16
|
calc_saturation_vapor_pressure,
|
|
17
17
|
)
|
|
18
18
|
from cloudnetpy.cloudnetarray import CloudnetArray
|
|
19
|
+
from cloudnetpy.constants import G
|
|
19
20
|
from cloudnetpy.datasource import DataSource
|
|
20
21
|
from cloudnetpy.exceptions import ModelDataError
|
|
21
22
|
|
|
@@ -115,6 +116,7 @@ class Model(DataSource):
|
|
|
115
116
|
array,
|
|
116
117
|
time_grid,
|
|
117
118
|
half_height if "atten" in key else height_grid,
|
|
119
|
+
extrapolate=True,
|
|
118
120
|
)
|
|
119
121
|
self.height = height_grid
|
|
120
122
|
return utils.find_masked_profiles_indices(self.data_dense["temperature"])
|
|
@@ -144,7 +146,26 @@ class Model(DataSource):
|
|
|
144
146
|
except KeyError as err:
|
|
145
147
|
msg = "No 'height' variable in the model file."
|
|
146
148
|
raise ModelDataError(msg) from err
|
|
147
|
-
|
|
149
|
+
surface_altitude = self._get_model_surface_altitude(alt_site)
|
|
150
|
+
return self.to_m(model_heights) + surface_altitude
|
|
151
|
+
|
|
152
|
+
def _get_model_surface_altitude(self, alt_site: float) -> float | npt.NDArray:
|
|
153
|
+
"""Returns model surface altitude from geopotential if available.
|
|
154
|
+
|
|
155
|
+
For sites in complex terrain (e.g. mountains), the model grid cell
|
|
156
|
+
surface height can differ significantly from the actual site altitude.
|
|
157
|
+
Using the model's own surface height ensures that thermodynamic fields
|
|
158
|
+
are placed at their physically correct absolute heights.
|
|
159
|
+
|
|
160
|
+
Note: Model surface altitude might be higher than the site altitude.
|
|
161
|
+
"""
|
|
162
|
+
if "sfc_height" in self.dataset.variables:
|
|
163
|
+
sfc_height = self.dataset.variables["sfc_height"][:]
|
|
164
|
+
return sfc_height[:, np.newaxis]
|
|
165
|
+
if "sfc_geopotential" in self.dataset.variables:
|
|
166
|
+
geopotential = self.dataset.variables["sfc_geopotential"][:]
|
|
167
|
+
return geopotential[:, np.newaxis] / G
|
|
168
|
+
return alt_site
|
|
148
169
|
|
|
149
170
|
def calc_attenuations(self, frequency: float) -> None:
|
|
150
171
|
temperature = self.getvar("temperature")
|
|
@@ -123,26 +123,12 @@ class Radar(DataSource):
|
|
|
123
123
|
if variable not in self.data:
|
|
124
124
|
return
|
|
125
125
|
data = ma.copy(self.data[variable][:])
|
|
126
|
-
|
|
127
|
-
n_profiles_with_data = np.count_nonzero(n_points_in_profiles)
|
|
126
|
+
n_profiles_with_data = np.count_nonzero(ma.count(data, axis=1))
|
|
128
127
|
if n_profiles_with_data < 300:
|
|
129
128
|
return
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
min_coverage=0.5,
|
|
134
|
-
z_limit=10,
|
|
135
|
-
distance=4,
|
|
136
|
-
n_blocks=100,
|
|
137
|
-
)
|
|
138
|
-
n_horizontal = self._filter(
|
|
139
|
-
data,
|
|
140
|
-
axis=0,
|
|
141
|
-
min_coverage=0.3,
|
|
142
|
-
z_limit=-30,
|
|
143
|
-
distance=3,
|
|
144
|
-
n_blocks=20,
|
|
145
|
-
)
|
|
129
|
+
|
|
130
|
+
n_vertical = self._filter_stripes_axis(data, variable, axis=1)
|
|
131
|
+
n_horizontal = self._filter_stripes_axis(data, variable, axis=0)
|
|
146
132
|
if n_vertical > 0 or n_horizontal > 0:
|
|
147
133
|
logging.debug(
|
|
148
134
|
"Filtered %s vertical and %s horizontal stripes "
|
|
@@ -152,57 +138,74 @@ class Radar(DataSource):
|
|
|
152
138
|
variable,
|
|
153
139
|
)
|
|
154
140
|
|
|
155
|
-
def
|
|
141
|
+
def _filter_stripes_axis(
|
|
156
142
|
self,
|
|
157
143
|
data: npt.NDArray,
|
|
144
|
+
variable: str,
|
|
158
145
|
axis: int,
|
|
159
|
-
min_coverage: float,
|
|
160
|
-
z_limit: float,
|
|
161
|
-
distance: float,
|
|
162
|
-
n_blocks: int,
|
|
163
146
|
) -> int:
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
for block_number in range(n_blocks):
|
|
175
|
-
data_block = data[block_indices, :]
|
|
176
|
-
n_values = ma.count(data_block, axis=1)
|
|
177
|
-
try:
|
|
178
|
-
q1 = np.quantile(n_values, 0.25)
|
|
179
|
-
q3 = np.quantile(n_values, 0.75)
|
|
180
|
-
except IndexError:
|
|
181
|
-
continue
|
|
182
|
-
|
|
183
|
-
if q1 == q3:
|
|
184
|
-
continue
|
|
185
|
-
|
|
186
|
-
threshold = distance * (q3 - q1) + q3
|
|
187
|
-
|
|
188
|
-
indices = np.where(
|
|
189
|
-
(n_values > threshold) & (n_values > (min_coverage * data.shape[1])),
|
|
190
|
-
)[0]
|
|
191
|
-
true_ind = [int(x) for x in (block_number * len_block + indices)]
|
|
192
|
-
n_removed = len(indices)
|
|
193
|
-
|
|
194
|
-
if n_removed > 5:
|
|
147
|
+
n_values = ma.count(data, axis=axis)
|
|
148
|
+
n = len(n_values)
|
|
149
|
+
margin = 5
|
|
150
|
+
|
|
151
|
+
spike_indices: set[int] = set()
|
|
152
|
+
for i in range(margin, n - margin):
|
|
153
|
+
window = [n_values[i + j] for j in range(-margin, margin + 1) if j != 0]
|
|
154
|
+
q1, q3 = np.percentile(window, [25, 75])
|
|
155
|
+
iqr = q3 - q1
|
|
156
|
+
if iqr == 0 and q3 > 0:
|
|
195
157
|
continue
|
|
158
|
+
threshold = q3 + 3 * max(iqr, 1)
|
|
159
|
+
if n_values[i] > max(threshold, 20):
|
|
160
|
+
spike_indices.add(i)
|
|
161
|
+
|
|
162
|
+
for ind in sorted(spike_indices):
|
|
163
|
+
below = ind - 1
|
|
164
|
+
while below >= 0 and below in spike_indices:
|
|
165
|
+
below -= 1
|
|
166
|
+
above = ind + 1
|
|
167
|
+
while above < n and above in spike_indices:
|
|
168
|
+
above += 1
|
|
169
|
+
self._mask_stripe_pixels(variable, ind, axis, below, above, n)
|
|
170
|
+
|
|
171
|
+
return len(spike_indices)
|
|
172
|
+
|
|
173
|
+
def _mask_stripe_pixels(
|
|
174
|
+
self,
|
|
175
|
+
variable: str,
|
|
176
|
+
ind: int,
|
|
177
|
+
axis: int,
|
|
178
|
+
below: int,
|
|
179
|
+
above: int,
|
|
180
|
+
dim_size: int,
|
|
181
|
+
) -> None:
|
|
182
|
+
self._mask_isolated("v", "Z", ind, axis, below, above, dim_size)
|
|
183
|
+
if variable != "v" and variable in self.data:
|
|
184
|
+
self._mask_isolated(variable, variable, ind, axis, below, above, dim_size)
|
|
196
185
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
186
|
+
def _mask_isolated(
|
|
187
|
+
self,
|
|
188
|
+
target: str,
|
|
189
|
+
source: str,
|
|
190
|
+
ind: int,
|
|
191
|
+
axis: int,
|
|
192
|
+
below: int,
|
|
193
|
+
above: int,
|
|
194
|
+
dim_size: int,
|
|
195
|
+
) -> None:
|
|
196
|
+
mask = ma.getmaskarray(self.data[source][:])
|
|
197
|
+
has = ~mask[:, ind] if axis == 0 else ~mask[ind, :]
|
|
198
|
+
lo = (mask[:, below] if axis == 0 else mask[below, :]) if below >= 0 else True
|
|
199
|
+
hi = (
|
|
200
|
+
(mask[:, above] if axis == 0 else mask[above, :])
|
|
201
|
+
if above < dim_size
|
|
202
|
+
else True
|
|
203
|
+
)
|
|
204
|
+
isolated = has & lo & hi
|
|
205
|
+
if axis == 0:
|
|
206
|
+
self.data[target][:][isolated, ind] = ma.masked
|
|
207
|
+
else:
|
|
208
|
+
self.data[target][:][ind, isolated] = ma.masked
|
|
206
209
|
|
|
207
210
|
def correct_atten(self, attenuations: RadarAttenuation) -> None:
|
|
208
211
|
"""Corrects radar echo for liquid and gas attenuation.
|
|
@@ -153,6 +153,9 @@ def _process_instrument_product(
|
|
|
153
153
|
fun: Callable
|
|
154
154
|
match (product, instrument.instrument_id):
|
|
155
155
|
case ("radar", _id) if "mira" in _id:
|
|
156
|
+
site_meta["range_correction_factor"] = calibration.get(
|
|
157
|
+
"range_correction_factor"
|
|
158
|
+
)
|
|
156
159
|
fun = instruments.mira2nc
|
|
157
160
|
case ("radar", _id) if "rpg" in _id:
|
|
158
161
|
fun = instruments.rpg2nc
|
|
@@ -388,16 +391,18 @@ def _fetch_product(
|
|
|
388
391
|
meta = [meta[0]]
|
|
389
392
|
suffix = "geophysical" if "geophysical" in meta[0].product.type else "instrument"
|
|
390
393
|
folder = _create_output_folder(suffix, args)
|
|
391
|
-
return _download_product_file(meta, folder, client)
|
|
394
|
+
return _download_product_file(meta, folder, client, force=args.force_download)
|
|
392
395
|
|
|
393
396
|
|
|
394
397
|
def _fetch_model(args: argparse.Namespace, client: APIClient) -> str | None:
|
|
395
|
-
files = client.files(
|
|
398
|
+
files = client.files(
|
|
399
|
+
product_id="model", model_id=args.model, date=args.date, site_id=args.site
|
|
400
|
+
)
|
|
396
401
|
if not files:
|
|
397
402
|
logging.info("No model data available for this date")
|
|
398
403
|
return None
|
|
399
404
|
folder = _create_output_folder("instrument", args)
|
|
400
|
-
return _download_product_file(files, folder, client)
|
|
405
|
+
return _download_product_file(files, folder, client, force=args.force_download)
|
|
401
406
|
|
|
402
407
|
|
|
403
408
|
def _fetch_raw(
|
|
@@ -406,19 +411,32 @@ def _fetch_raw(
|
|
|
406
411
|
pid = _shorten_pid(metadata[0].instrument.pid)
|
|
407
412
|
instrument = f"{metadata[0].instrument.instrument_id}_{pid}"
|
|
408
413
|
folder = _create_input_folder(instrument, args)
|
|
414
|
+
if args.force_download:
|
|
415
|
+
for meta in metadata:
|
|
416
|
+
filepath = folder / meta.download_url.split("/")[-1]
|
|
417
|
+
if filepath.exists():
|
|
418
|
+
logging.info("Removing existing file: %s", filepath)
|
|
419
|
+
filepath.unlink()
|
|
409
420
|
return client.download(metadata, output_directory=folder)
|
|
410
421
|
|
|
411
422
|
|
|
412
423
|
def _download_product_file(
|
|
413
|
-
meta: list[ProductMetadata],
|
|
424
|
+
meta: list[ProductMetadata],
|
|
425
|
+
folder: Path,
|
|
426
|
+
client: APIClient,
|
|
427
|
+
*,
|
|
428
|
+
force: bool = False,
|
|
414
429
|
) -> str:
|
|
415
430
|
if len(meta) > 1:
|
|
416
431
|
msg = "Multiple product files found"
|
|
417
432
|
raise ValueError(msg)
|
|
418
433
|
filepath = folder / meta[0].filename
|
|
419
|
-
if filepath.exists():
|
|
434
|
+
if filepath.exists() and not force:
|
|
420
435
|
logging.info("Existing file found: %s", filepath)
|
|
421
436
|
return str(filepath)
|
|
437
|
+
if filepath.exists():
|
|
438
|
+
logging.info("Removing existing file: %s", filepath)
|
|
439
|
+
filepath.unlink()
|
|
422
440
|
logging.info("Downloading file: %s", filepath)
|
|
423
441
|
return str(client.download(meta, output_directory=folder)[0])
|
|
424
442
|
|
|
@@ -438,10 +456,13 @@ def _plot(
|
|
|
438
456
|
) -> None:
|
|
439
457
|
if filepath is None or (not args.plot and not args.show):
|
|
440
458
|
return
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
459
|
+
if args.variables is not None:
|
|
460
|
+
variables = args.variables.split(",")
|
|
461
|
+
else:
|
|
462
|
+
res = requests.get(f"{cloudnet_api_url}products/variables", timeout=60)
|
|
463
|
+
res.raise_for_status()
|
|
464
|
+
variables = next(var["variables"] for var in res.json() if var["id"] == product)
|
|
465
|
+
variables = [var["id"].split("-")[-1] for var in variables]
|
|
445
466
|
image_name = str(filepath).replace(".nc", ".png") if args.plot else None
|
|
446
467
|
try:
|
|
447
468
|
generate_figure(
|
|
@@ -486,6 +507,21 @@ def _parse_products(product_argument: str, client: APIClient) -> list[str]:
|
|
|
486
507
|
return valid_products
|
|
487
508
|
|
|
488
509
|
|
|
510
|
+
def _parse_model(model_id: str, client: APIClient) -> str:
|
|
511
|
+
models = client.models()
|
|
512
|
+
found = False
|
|
513
|
+
for model in models:
|
|
514
|
+
if model_id == model.source_model_id and model.id != model.source_model_id:
|
|
515
|
+
msg = f"Use {model.id} or similar instead of {model_id}"
|
|
516
|
+
raise argparse.ArgumentTypeError(msg)
|
|
517
|
+
if model_id == model.id:
|
|
518
|
+
found = True
|
|
519
|
+
if not found:
|
|
520
|
+
msg = f"Invalid model: {model_id}"
|
|
521
|
+
raise argparse.ArgumentTypeError(msg)
|
|
522
|
+
return model_id
|
|
523
|
+
|
|
524
|
+
|
|
489
525
|
def main() -> None:
|
|
490
526
|
client = APIClient()
|
|
491
527
|
parser = argparse.ArgumentParser(
|
|
@@ -501,7 +537,11 @@ def main() -> None:
|
|
|
501
537
|
metavar="SITE",
|
|
502
538
|
)
|
|
503
539
|
parser.add_argument(
|
|
504
|
-
"-d",
|
|
540
|
+
"-d",
|
|
541
|
+
"--date",
|
|
542
|
+
type=str,
|
|
543
|
+
help="Date in YYYY-MM-DD (default: today)",
|
|
544
|
+
default=datetime.datetime.now(tz=datetime.timezone.utc).date().isoformat(),
|
|
505
545
|
)
|
|
506
546
|
parser.add_argument(
|
|
507
547
|
"-p",
|
|
@@ -514,6 +554,12 @@ def main() -> None:
|
|
|
514
554
|
),
|
|
515
555
|
required=True,
|
|
516
556
|
)
|
|
557
|
+
parser.add_argument(
|
|
558
|
+
"-m",
|
|
559
|
+
"--model",
|
|
560
|
+
type=lambda arg: _parse_model(arg, client),
|
|
561
|
+
help="Model to use in categorize.",
|
|
562
|
+
)
|
|
517
563
|
parser.add_argument("--input", type=Path, help="Input path", default="input/")
|
|
518
564
|
parser.add_argument("--output", type=Path, help="Output path", default="output/")
|
|
519
565
|
parser.add_argument(
|
|
@@ -534,6 +580,19 @@ def main() -> None:
|
|
|
534
580
|
default=False,
|
|
535
581
|
action=argparse.BooleanOptionalAction,
|
|
536
582
|
)
|
|
583
|
+
parser.add_argument(
|
|
584
|
+
"--force-download",
|
|
585
|
+
help="Force re-download of input files even if they exist locally",
|
|
586
|
+
default=False,
|
|
587
|
+
action=argparse.BooleanOptionalAction,
|
|
588
|
+
)
|
|
589
|
+
parser.add_argument(
|
|
590
|
+
"-v",
|
|
591
|
+
"--variables",
|
|
592
|
+
type=str,
|
|
593
|
+
help="Variables to plot (comma-separated), e.g. 'target_classification'",
|
|
594
|
+
default=None,
|
|
595
|
+
)
|
|
537
596
|
args = parser.parse_args()
|
|
538
597
|
|
|
539
598
|
logger = logging.getLogger()
|
|
@@ -156,6 +156,7 @@ class Mira(NcRadar):
|
|
|
156
156
|
if "tpow" not in self.data:
|
|
157
157
|
logging.warning("Variable tpow is missing")
|
|
158
158
|
return
|
|
159
|
+
self._correct_fzk_tpow()
|
|
159
160
|
tpow = self.data["tpow"][:]
|
|
160
161
|
# Threshold for abnormally low power e.g. Limassol 2024-10-20. Average
|
|
161
162
|
# power should 30 to 60 W according to MIRA-35 data sheet. Based on a
|
|
@@ -169,6 +170,27 @@ class Mira(NcRadar):
|
|
|
169
170
|
)
|
|
170
171
|
self.screen_time_indices(~is_low)
|
|
171
172
|
|
|
173
|
+
def _correct_fzk_tpow(self) -> None:
|
|
174
|
+
"""Corrects tpow for old FZK-serial instruments missing the FZK100 fix.
|
|
175
|
+
|
|
176
|
+
Some MIRA-35 instruments with serial number 'fzk' had a firmware bug
|
|
177
|
+
where tpow was reported 100x too small. Files produced after the
|
|
178
|
+
firmware fix contain 'FZK100' in the 'hrd' global attribute. Files
|
|
179
|
+
without this field need tpow multiplied by 100.
|
|
180
|
+
"""
|
|
181
|
+
hrd = getattr(self.dataset, "hrd", "")
|
|
182
|
+
if "FZK100" in hrd:
|
|
183
|
+
return
|
|
184
|
+
sn_lines = [line for line in hrd.split("\n") if line.strip().startswith("SN:")]
|
|
185
|
+
if not sn_lines:
|
|
186
|
+
return
|
|
187
|
+
sn = sn_lines[0].split(":", 1)[1].strip()
|
|
188
|
+
if sn != "fzk":
|
|
189
|
+
return
|
|
190
|
+
logging.info("Correcting tpow by factor 100 for old FZK instrument firmware")
|
|
191
|
+
self.data["tpow"].data[:] *= 100
|
|
192
|
+
self.data["tpow"].correction_factor = 100
|
|
193
|
+
|
|
172
194
|
def screen_invalid_ldr(self) -> None:
|
|
173
195
|
"""Masks LDR in MIRA STSR mode data.
|
|
174
196
|
Is there a better way to identify this mode?
|
|
@@ -53,8 +53,13 @@ class NcRadar(DataSource, CloudnetInstrument):
|
|
|
53
53
|
range_instru = np.array(
|
|
54
54
|
self.getvar("range", "height"),
|
|
55
55
|
) # "height" in old BASTA files
|
|
56
|
+
range_correction_factor = self.site_meta.get("range_correction_factor")
|
|
57
|
+
if range_correction_factor is not None:
|
|
58
|
+
range_instru = range_instru * range_correction_factor
|
|
56
59
|
time = np.array(self.time)
|
|
57
60
|
self.append_data(range_instru, "range")
|
|
61
|
+
if range_correction_factor is not None:
|
|
62
|
+
self.data["range"].correction_factor = range_correction_factor
|
|
58
63
|
self.append_data(time, "time", dtype=time_dtype)
|
|
59
64
|
|
|
60
65
|
def screen_by_snr(self, snr_limit: float) -> None:
|
|
@@ -120,23 +125,18 @@ class NcRadar(DataSource, CloudnetInstrument):
|
|
|
120
125
|
raise ValidTimeStampError(msg)
|
|
121
126
|
|
|
122
127
|
elevation_diff = ma.diff(elevation, prepend=elevation[1])
|
|
123
|
-
azimuth_diff = ma.diff(azimuth, prepend=azimuth[1])
|
|
124
|
-
|
|
125
|
-
# if azimuth_diff is large, it can be due to wrapping around 360 degrees
|
|
126
|
-
azimuth_diff[azimuth_diff > 180] -= 360
|
|
127
|
-
azimuth_diff[azimuth_diff < -180] += 360
|
|
128
|
-
|
|
129
128
|
is_stable = np.abs(elevation - 90) < elevation_threshold
|
|
130
129
|
is_stable &= np.abs(elevation_diff) < elevation_diff_threshold
|
|
131
|
-
is_stable &= np.abs(azimuth_diff) < azimuth_diff_threshold
|
|
132
130
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
131
|
+
if not ma.getmaskarray(azimuth).all():
|
|
132
|
+
azimuth_diff = ma.diff(azimuth, prepend=azimuth[1])
|
|
133
|
+
azimuth_diff[azimuth_diff > 180] -= 360
|
|
134
|
+
azimuth_diff[azimuth_diff < -180] += 360
|
|
135
|
+
is_stable &= np.abs(azimuth_diff) < azimuth_diff_threshold
|
|
137
136
|
|
|
138
|
-
|
|
139
|
-
|
|
137
|
+
# Masked values indicate missing scanning info
|
|
138
|
+
# (assume vertically pointing)
|
|
139
|
+
is_stable = np.array(ma.filled(is_stable, fill_value=True))
|
|
140
140
|
n_removed = np.count_nonzero(~is_stable)
|
|
141
141
|
|
|
142
142
|
if n_removed >= len(elevation) - 1:
|
|
@@ -348,6 +348,8 @@ def plot_colormesh(
|
|
|
348
348
|
ax: Axes, data: npt.NDArray, axes: tuple, variable_info: PlotMeta
|
|
349
349
|
) -> None:
|
|
350
350
|
vmin, vmax = variable_info.plot_range
|
|
351
|
+
if variable_info.clabel == "\u00b0C":
|
|
352
|
+
data = data - 273.15
|
|
351
353
|
if variable_info.plot_scale == "logarithmic":
|
|
352
354
|
data, vmin, vmax = lin2log(data, vmin, vmax)
|
|
353
355
|
cmap = plt.get_cmap(variable_info.cbar, 22)
|
|
@@ -19,6 +19,7 @@ class PlotMeta(NamedTuple):
|
|
|
19
19
|
moving_average: Whether to plot a moving average in a 1d plot.
|
|
20
20
|
contour: Whether to plot contours on top of a filled colormap.
|
|
21
21
|
zero_line: Whether to plot a zero line in a 1d plot.
|
|
22
|
+
contour_levels: Specific contour levels to draw on 2D plots.
|
|
22
23
|
mask_zeros: Whether to mask zero values in the plot.
|
|
23
24
|
time_smoothing_duration: The duration of the time smoothing window
|
|
24
25
|
(in 2d plots) in minutes.
|
|
@@ -31,6 +32,7 @@ class PlotMeta(NamedTuple):
|
|
|
31
32
|
moving_average: bool = True
|
|
32
33
|
contour: bool = False
|
|
33
34
|
zero_line: bool = False
|
|
35
|
+
contour_levels: tuple[float, ...] | None = None
|
|
34
36
|
mask_zeros: bool = False
|
|
35
37
|
time_smoothing_duration: int = 0
|
|
36
38
|
|
|
@@ -185,7 +187,7 @@ ATTRIBUTES = {
|
|
|
185
187
|
"mwr": {
|
|
186
188
|
"temperature": PlotMeta(
|
|
187
189
|
cmap="coolwarm",
|
|
188
|
-
plot_range=(
|
|
190
|
+
plot_range=(-50, 50),
|
|
189
191
|
contour=True,
|
|
190
192
|
time_smoothing_duration=_MWR_SINGLE_SMOOTHING,
|
|
191
193
|
),
|
|
@@ -204,7 +206,7 @@ ATTRIBUTES = {
|
|
|
204
206
|
"mwr-single": {
|
|
205
207
|
"temperature": PlotMeta(
|
|
206
208
|
cmap="coolwarm",
|
|
207
|
-
plot_range=(
|
|
209
|
+
plot_range=(-50, 50),
|
|
208
210
|
contour=True,
|
|
209
211
|
time_smoothing_duration=_MWR_SINGLE_SMOOTHING,
|
|
210
212
|
),
|
|
@@ -235,7 +237,7 @@ ATTRIBUTES = {
|
|
|
235
237
|
"mwr-multi": {
|
|
236
238
|
"temperature": PlotMeta(
|
|
237
239
|
cmap="coolwarm",
|
|
238
|
-
plot_range=(
|
|
240
|
+
plot_range=(-50, 50),
|
|
239
241
|
contour=True,
|
|
240
242
|
time_smoothing_duration=_MWR_MULTI_SMOOTHING,
|
|
241
243
|
),
|
|
@@ -406,7 +408,8 @@ ATTRIBUTES = {
|
|
|
406
408
|
),
|
|
407
409
|
"temperature": PlotMeta(
|
|
408
410
|
cmap="RdBu_r",
|
|
409
|
-
plot_range=(
|
|
411
|
+
plot_range=(-50, 50),
|
|
412
|
+
contour_levels=(-38, -10, 0),
|
|
410
413
|
),
|
|
411
414
|
"cloud_fraction": PlotMeta(
|
|
412
415
|
cmap="Blues",
|
|
@@ -414,7 +417,8 @@ ATTRIBUTES = {
|
|
|
414
417
|
),
|
|
415
418
|
"Tw": PlotMeta(
|
|
416
419
|
cmap="RdBu_r",
|
|
417
|
-
plot_range=(
|
|
420
|
+
plot_range=(-50, 50),
|
|
421
|
+
contour_levels=(-38, -10, 0),
|
|
418
422
|
),
|
|
419
423
|
"specific_humidity": PlotMeta(
|
|
420
424
|
plot_range=(1e-5, 1e-2),
|
|
@@ -16,6 +16,7 @@ from matplotlib.axes import Axes
|
|
|
16
16
|
from matplotlib.colorbar import Colorbar
|
|
17
17
|
from matplotlib.colorizer import ColorizingArtist
|
|
18
18
|
from matplotlib.colors import ListedColormap
|
|
19
|
+
from matplotlib.contour import QuadContourSet
|
|
19
20
|
from matplotlib.pyplot import Figure
|
|
20
21
|
from matplotlib.ticker import AutoMinorLocator
|
|
21
22
|
from matplotlib.transforms import Affine2D, Bbox
|
|
@@ -402,6 +403,7 @@ class Plot:
|
|
|
402
403
|
|
|
403
404
|
def _convert_units(self) -> str:
|
|
404
405
|
multiply, add = "multiply", "add"
|
|
406
|
+
k_to_c = (add, -273.15, "\u00b0C")
|
|
405
407
|
units_conversion = {
|
|
406
408
|
"rainfall_rate": (multiply, 3600000, "mm h$^{-1}$"),
|
|
407
409
|
"snowfall_rate": (multiply, 3600000, "mm h$^{-1}$"),
|
|
@@ -411,7 +413,16 @@ class Plot:
|
|
|
411
413
|
"rainfall_amount": (multiply, 1000, "mm"),
|
|
412
414
|
"snowfall_amount": (multiply, 1000, "mm"),
|
|
413
415
|
"precipitation_amount": (multiply, 1000, "mm"),
|
|
414
|
-
"air_temperature":
|
|
416
|
+
"air_temperature": k_to_c,
|
|
417
|
+
"temperature": k_to_c,
|
|
418
|
+
"Tw": k_to_c,
|
|
419
|
+
"T_sensor": k_to_c,
|
|
420
|
+
"T_ambient": k_to_c,
|
|
421
|
+
"T_interior": k_to_c,
|
|
422
|
+
"T_laser_driver": k_to_c,
|
|
423
|
+
"transmitter_temperature": k_to_c,
|
|
424
|
+
"receiver_temperature": k_to_c,
|
|
425
|
+
"pc_temperature": k_to_c,
|
|
415
426
|
"r_accum_RT": (multiply, 1000, "mm"),
|
|
416
427
|
"r_accum_NRT": (multiply, 1000, "mm"),
|
|
417
428
|
"cloud_top_height_agl": (multiply, con.M_TO_KM, "Height (km AGL)"),
|
|
@@ -773,32 +784,37 @@ class Plot2D(Plot):
|
|
|
773
784
|
linewidths=0.5,
|
|
774
785
|
)
|
|
775
786
|
|
|
776
|
-
if self.
|
|
777
|
-
self._plot_contour(
|
|
787
|
+
if self._plot_meta.contour_levels is not None:
|
|
788
|
+
cs = self._plot_contour(
|
|
778
789
|
figure_data,
|
|
779
790
|
alt,
|
|
780
|
-
levels=np.array(
|
|
791
|
+
levels=np.array(self._plot_meta.contour_levels),
|
|
781
792
|
colors="gray",
|
|
782
793
|
linewidths=1.25,
|
|
783
794
|
linestyles="dashed",
|
|
784
795
|
)
|
|
796
|
+
if cs is not None:
|
|
797
|
+
self._ax.clabel(cs, fmt="%g \u00b0C", fontsize=8)
|
|
785
798
|
|
|
786
799
|
def _plot_contour(
|
|
787
800
|
self,
|
|
788
801
|
figure_data: FigureData,
|
|
789
802
|
alt: npt.NDArray,
|
|
790
803
|
**options, # noqa: ANN003
|
|
791
|
-
) -> None:
|
|
804
|
+
) -> QuadContourSet | None:
|
|
792
805
|
time_length = len(figure_data.time_including_gaps)
|
|
793
806
|
step = max(1, time_length // 200)
|
|
794
807
|
ind_time = np.arange(0, time_length, step)
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
808
|
+
try:
|
|
809
|
+
return self._ax.contour(
|
|
810
|
+
figure_data.time_including_gaps[ind_time],
|
|
811
|
+
alt,
|
|
812
|
+
self._data[ind_time, :].T,
|
|
813
|
+
**options,
|
|
814
|
+
zorder=_get_zorder("contour"),
|
|
815
|
+
)
|
|
816
|
+
except ValueError:
|
|
817
|
+
return None
|
|
802
818
|
|
|
803
819
|
def _screen_data_by_max_y(self, figure_data: FigureData) -> ndarray:
|
|
804
820
|
if figure_data.height is None:
|
|
@@ -317,7 +317,11 @@ def _find_cloud_mask(classification: npt.NDArray) -> npt.NDArray:
|
|
|
317
317
|
Target.ICE_AND_SUPERCOOLED,
|
|
318
318
|
]:
|
|
319
319
|
cloud_mask[classification == value] = 1
|
|
320
|
-
return
|
|
320
|
+
return utils.remove_small_objects(
|
|
321
|
+
cloud_mask.astype(bool),
|
|
322
|
+
max_size=20,
|
|
323
|
+
connectivity=1,
|
|
324
|
+
).astype(int)
|
|
321
325
|
|
|
322
326
|
|
|
323
327
|
COMMENTS = {
|
|
@@ -354,6 +354,8 @@ def interpolate_2d_mask(
|
|
|
354
354
|
z: ma.MaskedArray,
|
|
355
355
|
x_new: npt.NDArray,
|
|
356
356
|
y_new: npt.NDArray,
|
|
357
|
+
*,
|
|
358
|
+
extrapolate: bool = False,
|
|
357
359
|
) -> ma.MaskedArray:
|
|
358
360
|
"""2D linear interpolation preserving the mask.
|
|
359
361
|
|
|
@@ -363,13 +365,14 @@ def interpolate_2d_mask(
|
|
|
363
365
|
z: 2D masked array, data values.
|
|
364
366
|
x_new: 1D array, new x-coordinates.
|
|
365
367
|
y_new: 1D array, new y-coordinates.
|
|
368
|
+
extrapolate: If True, linearly extrapolate along the y-axis for points
|
|
369
|
+
outside the original range. Default is False (NaN / masked).
|
|
366
370
|
|
|
367
371
|
Returns:
|
|
368
372
|
Interpolated 2D masked array.
|
|
369
373
|
|
|
370
374
|
Notes:
|
|
371
|
-
|
|
372
|
-
interpolation. Input data may contain nan-values.
|
|
375
|
+
Uses linear interpolation. Input data may contain nan-values.
|
|
373
376
|
|
|
374
377
|
"""
|
|
375
378
|
z = ma.array(ma.masked_invalid(z, copy=True))
|
|
@@ -386,6 +389,15 @@ def interpolate_2d_mask(
|
|
|
386
389
|
(xx_new, yy_new),
|
|
387
390
|
method="linear",
|
|
388
391
|
)
|
|
392
|
+
if extrapolate:
|
|
393
|
+
y_new_arr = np.asarray(y_new)
|
|
394
|
+
for i in range(data.shape[0]):
|
|
395
|
+
row = data[i, :]
|
|
396
|
+
nans = np.isnan(row)
|
|
397
|
+
if nans.any() and np.sum(~nans) >= 2:
|
|
398
|
+
valid = ~nans
|
|
399
|
+
f = interp1d(y_new_arr[valid], row[valid], fill_value="extrapolate")
|
|
400
|
+
data[i, nans] = f(y_new_arr[nans])
|
|
389
401
|
# Preserve mask:
|
|
390
402
|
mask_fun = RectBivariateSpline(x, y, ma.getmaskarray(z), kx=1, ky=1)
|
|
391
403
|
mask = mask_fun(x_new, y_new)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/attenuations/gas_attenuation.py
RENAMED
|
File without changes
|
{cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/attenuations/liquid_attenuation.py
RENAMED
|
File without changes
|
{cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/attenuations/melting_attenuation.py
RENAMED
|
File without changes
|
{cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/attenuations/rain_attenuation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/products/advance_methods.py
RENAMED
|
File without changes
|
{cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/products/grid_methods.py
RENAMED
|
File without changes
|
{cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/products/model_products.py
RENAMED
|
File without changes
|
{cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/products/observation_products.py
RENAMED
|
File without changes
|
{cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/products/product_resampling.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/e2e/process_cf/__init__.py
RENAMED
|
File without changes
|
{cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/e2e/process_cf/main.py
RENAMED
|
File without changes
|
{cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/e2e/process_cf/tests.py
RENAMED
|
File without changes
|
|
File without changes
|
{cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/e2e/process_iwc/main.py
RENAMED
|
File without changes
|
{cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/e2e/process_iwc/tests.py
RENAMED
|
File without changes
|
|
File without changes
|
{cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/e2e/process_lwc/main.py
RENAMED
|
File without changes
|
{cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/e2e/process_lwc/tests.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/unit/test_grid_methods.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/unit/test_plot_tools.py
RENAMED
|
File without changes
|
{cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/unit/test_plotting.py
RENAMED
|
File without changes
|
|
File without changes
|
{cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/unit/test_tools.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|