cloudnetpy 1.88.3__tar.gz → 1.89.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.88.3/cloudnetpy.egg-info → cloudnetpy-1.89.1}/PKG-INFO +1 -1
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/categorize/classify.py +101 -16
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/categorize/model.py +22 -1
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/categorize/radar.py +66 -63
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/cli.py +136 -22
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/instruments/mira.py +22 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/instruments/nc_radar.py +13 -13
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/model_evaluation/plotting/plot_meta.py +2 -2
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/model_evaluation/plotting/plotting.py +2 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/plotting/plot_meta.py +9 -9
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/plotting/plotting.py +28 -12
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/products/classification.py +5 -1
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/utils.py +14 -2
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/version.py +2 -2
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1/cloudnetpy.egg-info}/PKG-INFO +1 -1
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/LICENSE +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/MANIFEST.in +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/README.md +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/__init__.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/categorize/__init__.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/categorize/atmos_utils.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/categorize/attenuation.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/categorize/attenuations/__init__.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/categorize/attenuations/gas_attenuation.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/categorize/attenuations/liquid_attenuation.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/categorize/attenuations/melting_attenuation.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/categorize/attenuations/rain_attenuation.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/categorize/categorize.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/categorize/containers.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/categorize/disdrometer.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/categorize/droplet.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/categorize/falling.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/categorize/freezing.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/categorize/insects.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/categorize/itu.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/categorize/lidar.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/categorize/melting.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/categorize/mwr.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/cloudnetarray.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/concat_lib.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/constants.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/datasource.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/disdronator/__init__.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/disdronator/lpm.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/disdronator/parsivel.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/disdronator/rd80.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/disdronator/utils.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/exceptions.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/instruments/__init__.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/instruments/basta.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/instruments/bowtie.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/instruments/ceilo.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/instruments/ceilometer.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/instruments/cl61d.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/instruments/cloudnet_instrument.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/instruments/copernicus.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/instruments/da10.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/instruments/disdrometer/__init__.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/instruments/disdrometer/common.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/instruments/disdrometer/parsivel.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/instruments/disdrometer/rd80.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/instruments/disdrometer/thies.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/instruments/fd12p.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/instruments/galileo.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/instruments/hatpro.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/instruments/instruments.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/instruments/lufft.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/instruments/mrr.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/instruments/nc_lidar.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/instruments/pollyxt.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/instruments/radiometrics.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/instruments/rain_e_h3.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/instruments/rpg.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/instruments/rpg_reader.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/instruments/toa5.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/instruments/vaisala.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/instruments/weather_station.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/metadata.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/model_evaluation/__init__.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/model_evaluation/file_handler.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/model_evaluation/metadata.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/model_evaluation/model_metadata.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/model_evaluation/plotting/__init__.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/model_evaluation/plotting/plot_tools.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/model_evaluation/products/__init__.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/model_evaluation/products/advance_methods.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/model_evaluation/products/grid_methods.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/model_evaluation/products/model_products.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/model_evaluation/products/observation_products.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/model_evaluation/products/product_resampling.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/model_evaluation/products/tools.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/model_evaluation/statistics/__init__.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/model_evaluation/statistics/statistical_methods.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/model_evaluation/tests/__init__.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/model_evaluation/tests/e2e/__init__.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/model_evaluation/tests/e2e/conftest.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/model_evaluation/tests/e2e/process_cf/__init__.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/model_evaluation/tests/e2e/process_cf/main.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/model_evaluation/tests/e2e/process_cf/tests.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/model_evaluation/tests/e2e/process_iwc/__init__.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/model_evaluation/tests/e2e/process_iwc/main.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/model_evaluation/tests/e2e/process_iwc/tests.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/model_evaluation/tests/e2e/process_lwc/__init__.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/model_evaluation/tests/e2e/process_lwc/main.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/model_evaluation/tests/e2e/process_lwc/tests.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/model_evaluation/tests/unit/__init__.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/model_evaluation/tests/unit/conftest.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/model_evaluation/tests/unit/test_advance_methods.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/model_evaluation/tests/unit/test_grid_methods.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/model_evaluation/tests/unit/test_model_products.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/model_evaluation/tests/unit/test_observation_products.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/model_evaluation/tests/unit/test_plot_tools.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/model_evaluation/tests/unit/test_plotting.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/model_evaluation/tests/unit/test_statistical_methods.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/model_evaluation/tests/unit/test_tools.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/model_evaluation/utils.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/output.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/plotting/__init__.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/products/__init__.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/products/der.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/products/drizzle.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/products/drizzle_error.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/products/drizzle_tools.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/products/epsilon.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/products/ier.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/products/iwc.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/products/lwc.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/products/mie_lu_tables.nc +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/products/mwr_tools.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/products/product_tools.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy/py.typed +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy.egg-info/SOURCES.txt +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy.egg-info/dependency_links.txt +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy.egg-info/entry_points.txt +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy.egg-info/requires.txt +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/cloudnetpy.egg-info/top_level.txt +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/docs/source/conf.py +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/pyproject.toml +0 -0
- {cloudnetpy-1.88.3 → cloudnetpy-1.89.1}/setup.cfg +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import numpy as np
|
|
2
2
|
import numpy.typing as npt
|
|
3
3
|
from numpy import ma
|
|
4
|
+
from scipy import ndimage
|
|
4
5
|
|
|
5
6
|
from cloudnetpy import utils
|
|
6
7
|
from cloudnetpy.categorize import (
|
|
@@ -84,18 +85,21 @@ def classify_measurements(data: Observations) -> ClassificationResult:
|
|
|
84
85
|
bits.droplet = droplet.correct_liquid_top(obs, is_liquid, bits.freezing, limit=500)
|
|
85
86
|
bits.insect, insect_prob = insects.find_insects(obs, bits.melting, bits.droplet)
|
|
86
87
|
bits.falling = falling.find_falling_hydrometeors(obs, bits.droplet, bits.insect)
|
|
87
|
-
filtered_ice = _filter_falling(bits)
|
|
88
88
|
for _ in range(5):
|
|
89
89
|
_fix_undetected_melting_layer(bits)
|
|
90
90
|
_filter_insects(bits)
|
|
91
91
|
bits.aerosol = _find_aerosols(obs, bits)
|
|
92
|
-
bits.aerosol[filtered_ice] = False
|
|
93
92
|
_fix_super_cold_liquid(obs, bits)
|
|
93
|
+
_reclassify_ice_with_high_ldr(obs, bits)
|
|
94
|
+
_filter_insects_in_falling(bits)
|
|
95
|
+
_filter_falling(bits, obs)
|
|
96
|
+
|
|
97
|
+
is_clutter = _remove_clutter_in_liquid(obs.is_clutter, bits)
|
|
94
98
|
|
|
95
99
|
return ClassificationResult(
|
|
96
100
|
category_bits=bits,
|
|
97
101
|
is_rain=obs.is_rain,
|
|
98
|
-
is_clutter=
|
|
102
|
+
is_clutter=is_clutter,
|
|
99
103
|
insect_prob=insect_prob,
|
|
100
104
|
liquid_prob=liquid_prob,
|
|
101
105
|
)
|
|
@@ -122,6 +126,64 @@ def fetch_quality(
|
|
|
122
126
|
)
|
|
123
127
|
|
|
124
128
|
|
|
129
|
+
def _reclassify_ice_with_high_ldr(obs: ClassData, bits: CategoryBits) -> None:
|
|
130
|
+
"""Reclassifies ice pixels with high LDR as insects.
|
|
131
|
+
|
|
132
|
+
Ice particles typically have LDR below -13 dB. Higher values in the
|
|
133
|
+
freezing region (excluding the melting layer) indicate insects, which
|
|
134
|
+
can be present in temperatures down to about -10 C.
|
|
135
|
+
"""
|
|
136
|
+
if not hasattr(obs, "ldr"):
|
|
137
|
+
return
|
|
138
|
+
ldr_limit = -13
|
|
139
|
+
temp_limit = T0 - 10
|
|
140
|
+
is_ice = bits.falling & bits.freezing & ~bits.droplet & ~bits.melting
|
|
141
|
+
high_ldr = ~ma.getmaskarray(obs.ldr) & (obs.ldr > ldr_limit)
|
|
142
|
+
warm_enough = obs.tw > temp_limit
|
|
143
|
+
above_melting = utils.ffill(bits.melting)
|
|
144
|
+
reclassify = is_ice & high_ldr & warm_enough & ~above_melting
|
|
145
|
+
bits.insect[reclassify] = True
|
|
146
|
+
bits.falling[reclassify] = False
|
|
147
|
+
has_lidar = ~obs.beta.mask
|
|
148
|
+
bits.aerosol[reclassify & has_lidar] = True
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _filter_insects_in_falling(
|
|
152
|
+
bits: CategoryBits,
|
|
153
|
+
min_falling_size: int = 100,
|
|
154
|
+
dilation: int = 2,
|
|
155
|
+
) -> None:
|
|
156
|
+
"""Reclassifies insect pixels in freezing regions near large falling ice.
|
|
157
|
+
|
|
158
|
+
Insect pixels in freezing temperatures near sufficiently large falling
|
|
159
|
+
hydrometeor regions are almost certainly false positives. Real insects
|
|
160
|
+
cannot survive in freezing conditions, so the freezing constraint
|
|
161
|
+
protects legitimate warm-region insects from being reclassified.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
bits: A :class:`CategoryBits` instance.
|
|
165
|
+
min_falling_size: Minimum size of nearby falling regions required
|
|
166
|
+
to trigger reclassification.
|
|
167
|
+
dilation: Number of pixels to dilate around falling regions.
|
|
168
|
+
"""
|
|
169
|
+
structure = ndimage.generate_binary_structure(2, 1)
|
|
170
|
+
falling_labels, n_falling = ndimage.label(bits.falling, structure=structure)
|
|
171
|
+
if n_falling == 0:
|
|
172
|
+
return
|
|
173
|
+
falling_ids = np.arange(1, n_falling + 1)
|
|
174
|
+
falling_sizes = ndimage.sum(bits.falling, falling_labels, falling_ids)
|
|
175
|
+
large_falling = np.isin(
|
|
176
|
+
falling_labels, falling_ids[falling_sizes >= min_falling_size]
|
|
177
|
+
)
|
|
178
|
+
near_large_falling = ndimage.binary_dilation(
|
|
179
|
+
large_falling, structure, iterations=dilation
|
|
180
|
+
)
|
|
181
|
+
reclassify = bits.insect & bits.freezing & near_large_falling
|
|
182
|
+
bits.falling[reclassify] = True
|
|
183
|
+
bits.insect[reclassify] = False
|
|
184
|
+
bits.aerosol[reclassify] = False
|
|
185
|
+
|
|
186
|
+
|
|
125
187
|
def _fix_super_cold_liquid(obs: ClassData, bits: CategoryBits) -> None:
|
|
126
188
|
"""Supercooled liquid droplets do not exist in atmosphere below around -38 C."""
|
|
127
189
|
t_limit = T0 - 38
|
|
@@ -224,21 +286,44 @@ def _filter_insects(bits: CategoryBits) -> None:
|
|
|
224
286
|
bits.insect = is_insects
|
|
225
287
|
|
|
226
288
|
|
|
227
|
-
def
|
|
289
|
+
def _remove_clutter_in_liquid(
|
|
290
|
+
is_clutter: npt.NDArray,
|
|
291
|
+
bits: CategoryBits,
|
|
292
|
+
) -> npt.NDArray:
|
|
293
|
+
"""Removes clutter that is inside liquid or falling hydrometeor layers.
|
|
294
|
+
|
|
295
|
+
Clutter detection based on near-zero velocity can produce false positives
|
|
296
|
+
inside cloud and drizzle layers. This function removes clutter flags from
|
|
297
|
+
pixels that are adjacent to droplet or falling hydrometeor pixels, and
|
|
298
|
+
reclassifies them as falling hydrometeors.
|
|
299
|
+
"""
|
|
300
|
+
is_clutter = is_clutter.copy()
|
|
301
|
+
liquid_or_falling = bits.droplet | bits.falling
|
|
302
|
+
structure = ndimage.generate_binary_structure(2, 1)
|
|
303
|
+
near_liquid = ndimage.binary_dilation(liquid_or_falling, structure)
|
|
304
|
+
false_clutter = is_clutter & near_liquid
|
|
305
|
+
is_clutter[false_clutter] = False
|
|
306
|
+
bits.falling[false_clutter] = True
|
|
307
|
+
return is_clutter
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _filter_falling(bits: CategoryBits, obs: ClassData) -> None:
|
|
228
311
|
# filter falling ice speckle noise
|
|
312
|
+
is_beta = ~obs.beta.mask
|
|
229
313
|
is_freezing = bits.freezing
|
|
230
314
|
is_falling = bits.falling
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
315
|
+
filtered_out = is_falling & ~np.asarray(
|
|
316
|
+
utils.remove_small_objects(
|
|
317
|
+
is_falling,
|
|
318
|
+
max_size=10,
|
|
319
|
+
connectivity=1,
|
|
320
|
+
),
|
|
321
|
+
dtype=bool,
|
|
235
322
|
)
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
323
|
+
is_falling[filtered_out] = False
|
|
324
|
+
# In warm conditions, these are likely insects
|
|
325
|
+
bits.insect[filtered_out & ~is_freezing] = True
|
|
326
|
+
bits.aerosol[filtered_out & ~is_freezing & is_beta] = True
|
|
327
|
+
# In cold conditions, classify as aerosol if lidar signal is present
|
|
328
|
+
bits.aerosol[filtered_out & is_freezing & is_beta] = True
|
|
242
329
|
bits.falling = is_falling
|
|
243
|
-
bits.insect[insect_ind] = True
|
|
244
|
-
return ice_ind
|
|
@@ -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.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
import base64
|
|
3
|
+
import concurrent.futures
|
|
3
4
|
import datetime
|
|
4
5
|
import gzip
|
|
5
6
|
import hashlib
|
|
@@ -88,21 +89,51 @@ def _process_categorize(
|
|
|
88
89
|
input_files: dict, args: argparse.Namespace, client: APIClient
|
|
89
90
|
) -> str | None:
|
|
90
91
|
cat_filepath = _create_categorize_filepath(args)
|
|
92
|
+
instrument_prefs = _parse_instrument_preferences(args.instrument)
|
|
93
|
+
|
|
94
|
+
def fetch_instrument(product: str) -> tuple[str, str | None]:
|
|
95
|
+
source = instrument_prefs.get(product)
|
|
96
|
+
filepath = _fetch_product(args, product, client, source=source)
|
|
97
|
+
if filepath is None and source:
|
|
98
|
+
logging.info(
|
|
99
|
+
"Preferred instrument '%s' not found for %s, using available",
|
|
100
|
+
source,
|
|
101
|
+
product,
|
|
102
|
+
)
|
|
103
|
+
filepath = _fetch_product(args, product, client)
|
|
104
|
+
return product, filepath
|
|
91
105
|
|
|
92
|
-
|
|
93
|
-
|
|
106
|
+
def fetch_mwr() -> tuple[str, str | None]:
|
|
107
|
+
try:
|
|
108
|
+
return "mwr", _fetch_mwr(args, client)
|
|
109
|
+
except (CloudnetAPIError, KeyError, ValueError):
|
|
110
|
+
logging.info("LWP data not available, continuing without it")
|
|
111
|
+
return "mwr", None
|
|
112
|
+
|
|
113
|
+
def fetch_model() -> tuple[str, str | None]:
|
|
114
|
+
return "model", _fetch_model(args, client)
|
|
115
|
+
|
|
116
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
117
|
+
futures = [executor.submit(fetch_model)]
|
|
118
|
+
for product in ("radar", "lidar", "disdrometer"):
|
|
119
|
+
if product not in input_files:
|
|
120
|
+
futures.append(executor.submit(fetch_instrument, product))
|
|
121
|
+
|
|
122
|
+
for future in concurrent.futures.as_completed(futures):
|
|
123
|
+
product, filepath = future.result()
|
|
124
|
+
if filepath:
|
|
125
|
+
input_files[product] = filepath
|
|
126
|
+
|
|
127
|
+
# MWR runs after other downloads because its fallback chain may
|
|
128
|
+
# try to download the same radar file fetched above.
|
|
129
|
+
mwr_key, mwr_path = fetch_mwr()
|
|
130
|
+
if mwr_path:
|
|
131
|
+
input_files[mwr_key] = mwr_path
|
|
132
|
+
|
|
133
|
+
if "model" not in input_files:
|
|
94
134
|
logging.info("No model data available for this date.")
|
|
95
135
|
return None
|
|
96
136
|
|
|
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
137
|
try:
|
|
107
138
|
logging.info("Processing categorize...")
|
|
108
139
|
generate_categorize(cast("CategorizeInput", input_files), cat_filepath)
|
|
@@ -153,6 +184,9 @@ def _process_instrument_product(
|
|
|
153
184
|
fun: Callable
|
|
154
185
|
match (product, instrument.instrument_id):
|
|
155
186
|
case ("radar", _id) if "mira" in _id:
|
|
187
|
+
site_meta["range_correction_factor"] = calibration.get(
|
|
188
|
+
"range_correction_factor"
|
|
189
|
+
)
|
|
156
190
|
fun = instruments.mira2nc
|
|
157
191
|
case ("radar", _id) if "rpg" in _id:
|
|
158
192
|
fun = instruments.rpg2nc
|
|
@@ -335,6 +369,20 @@ def _get_product_sources(
|
|
|
335
369
|
return source_products
|
|
336
370
|
|
|
337
371
|
|
|
372
|
+
def _parse_instrument_preferences(args: list[str] | None) -> dict[str, str]:
|
|
373
|
+
"""Parses instrument preferences like 'radar:mira-35' into a dict."""
|
|
374
|
+
if not args:
|
|
375
|
+
return {}
|
|
376
|
+
prefs = {}
|
|
377
|
+
for arg in args:
|
|
378
|
+
if ":" not in arg:
|
|
379
|
+
msg = f"Invalid instrument format '{arg}', expected 'product:instrument_id'"
|
|
380
|
+
raise argparse.ArgumentTypeError(msg)
|
|
381
|
+
product, instrument_id = arg.split(":", 1)
|
|
382
|
+
prefs[product] = instrument_id
|
|
383
|
+
return prefs
|
|
384
|
+
|
|
385
|
+
|
|
338
386
|
def _parse_instrument(s: str) -> tuple[str, str | None]:
|
|
339
387
|
if "[" in s and s.endswith("]"):
|
|
340
388
|
name = s[: s.index("[")]
|
|
@@ -379,7 +427,6 @@ def _fetch_product(
|
|
|
379
427
|
if m.instrument is not None and m.instrument.instrument_id == source
|
|
380
428
|
]
|
|
381
429
|
if not meta:
|
|
382
|
-
logging.info("No data available for %s", product)
|
|
383
430
|
return None
|
|
384
431
|
if len(meta) > 1:
|
|
385
432
|
logging.info(
|
|
@@ -388,16 +435,18 @@ def _fetch_product(
|
|
|
388
435
|
meta = [meta[0]]
|
|
389
436
|
suffix = "geophysical" if "geophysical" in meta[0].product.type else "instrument"
|
|
390
437
|
folder = _create_output_folder(suffix, args)
|
|
391
|
-
return _download_product_file(meta, folder, client)
|
|
438
|
+
return _download_product_file(meta, folder, client, force=args.force_download)
|
|
392
439
|
|
|
393
440
|
|
|
394
441
|
def _fetch_model(args: argparse.Namespace, client: APIClient) -> str | None:
|
|
395
|
-
files = client.files(
|
|
442
|
+
files = client.files(
|
|
443
|
+
product_id="model", model_id=args.model, date=args.date, site_id=args.site
|
|
444
|
+
)
|
|
396
445
|
if not files:
|
|
397
446
|
logging.info("No model data available for this date")
|
|
398
447
|
return None
|
|
399
448
|
folder = _create_output_folder("instrument", args)
|
|
400
|
-
return _download_product_file(files, folder, client)
|
|
449
|
+
return _download_product_file(files, folder, client, force=args.force_download)
|
|
401
450
|
|
|
402
451
|
|
|
403
452
|
def _fetch_raw(
|
|
@@ -406,19 +455,32 @@ def _fetch_raw(
|
|
|
406
455
|
pid = _shorten_pid(metadata[0].instrument.pid)
|
|
407
456
|
instrument = f"{metadata[0].instrument.instrument_id}_{pid}"
|
|
408
457
|
folder = _create_input_folder(instrument, args)
|
|
458
|
+
if args.force_download:
|
|
459
|
+
for meta in metadata:
|
|
460
|
+
filepath = folder / meta.download_url.split("/")[-1]
|
|
461
|
+
if filepath.exists():
|
|
462
|
+
logging.info("Removing existing file: %s", filepath)
|
|
463
|
+
filepath.unlink()
|
|
409
464
|
return client.download(metadata, output_directory=folder)
|
|
410
465
|
|
|
411
466
|
|
|
412
467
|
def _download_product_file(
|
|
413
|
-
meta: list[ProductMetadata],
|
|
468
|
+
meta: list[ProductMetadata],
|
|
469
|
+
folder: Path,
|
|
470
|
+
client: APIClient,
|
|
471
|
+
*,
|
|
472
|
+
force: bool = False,
|
|
414
473
|
) -> str:
|
|
415
474
|
if len(meta) > 1:
|
|
416
475
|
msg = "Multiple product files found"
|
|
417
476
|
raise ValueError(msg)
|
|
418
477
|
filepath = folder / meta[0].filename
|
|
419
|
-
if filepath.exists():
|
|
478
|
+
if filepath.exists() and not force:
|
|
420
479
|
logging.info("Existing file found: %s", filepath)
|
|
421
480
|
return str(filepath)
|
|
481
|
+
if filepath.exists():
|
|
482
|
+
logging.info("Removing existing file: %s", filepath)
|
|
483
|
+
filepath.unlink()
|
|
422
484
|
logging.info("Downloading file: %s", filepath)
|
|
423
485
|
return str(client.download(meta, output_directory=folder)[0])
|
|
424
486
|
|
|
@@ -438,10 +500,13 @@ def _plot(
|
|
|
438
500
|
) -> None:
|
|
439
501
|
if filepath is None or (not args.plot and not args.show):
|
|
440
502
|
return
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
503
|
+
if args.variables is not None:
|
|
504
|
+
variables = args.variables.split(",")
|
|
505
|
+
else:
|
|
506
|
+
res = requests.get(f"{cloudnet_api_url}products/variables", timeout=60)
|
|
507
|
+
res.raise_for_status()
|
|
508
|
+
variables = next(var["variables"] for var in res.json() if var["id"] == product)
|
|
509
|
+
variables = [var["id"].split("-")[-1] for var in variables]
|
|
445
510
|
image_name = str(filepath).replace(".nc", ".png") if args.plot else None
|
|
446
511
|
try:
|
|
447
512
|
generate_figure(
|
|
@@ -486,6 +551,21 @@ def _parse_products(product_argument: str, client: APIClient) -> list[str]:
|
|
|
486
551
|
return valid_products
|
|
487
552
|
|
|
488
553
|
|
|
554
|
+
def _parse_model(model_id: str, client: APIClient) -> str:
|
|
555
|
+
models = client.models()
|
|
556
|
+
found = False
|
|
557
|
+
for model in models:
|
|
558
|
+
if model_id == model.source_model_id and model.id != model.source_model_id:
|
|
559
|
+
msg = f"Use {model.id} or similar instead of {model_id}"
|
|
560
|
+
raise argparse.ArgumentTypeError(msg)
|
|
561
|
+
if model_id == model.id:
|
|
562
|
+
found = True
|
|
563
|
+
if not found:
|
|
564
|
+
msg = f"Invalid model: {model_id}"
|
|
565
|
+
raise argparse.ArgumentTypeError(msg)
|
|
566
|
+
return model_id
|
|
567
|
+
|
|
568
|
+
|
|
489
569
|
def main() -> None:
|
|
490
570
|
client = APIClient()
|
|
491
571
|
parser = argparse.ArgumentParser(
|
|
@@ -501,7 +581,11 @@ def main() -> None:
|
|
|
501
581
|
metavar="SITE",
|
|
502
582
|
)
|
|
503
583
|
parser.add_argument(
|
|
504
|
-
"-d",
|
|
584
|
+
"-d",
|
|
585
|
+
"--date",
|
|
586
|
+
type=str,
|
|
587
|
+
help="Date in YYYY-MM-DD (default: today)",
|
|
588
|
+
default=datetime.datetime.now(tz=datetime.timezone.utc).date().isoformat(),
|
|
505
589
|
)
|
|
506
590
|
parser.add_argument(
|
|
507
591
|
"-p",
|
|
@@ -514,6 +598,23 @@ def main() -> None:
|
|
|
514
598
|
),
|
|
515
599
|
required=True,
|
|
516
600
|
)
|
|
601
|
+
parser.add_argument(
|
|
602
|
+
"-m",
|
|
603
|
+
"--model",
|
|
604
|
+
type=lambda arg: _parse_model(arg, client),
|
|
605
|
+
help="Model to use in categorize.",
|
|
606
|
+
)
|
|
607
|
+
parser.add_argument(
|
|
608
|
+
"-i",
|
|
609
|
+
"--instrument",
|
|
610
|
+
type=str,
|
|
611
|
+
action="append",
|
|
612
|
+
help=(
|
|
613
|
+
"Preferred instrument for categorize input, e.g. "
|
|
614
|
+
"'radar:mira-35' or 'lidar:cl61d'. Can be specified multiple times."
|
|
615
|
+
),
|
|
616
|
+
default=None,
|
|
617
|
+
)
|
|
517
618
|
parser.add_argument("--input", type=Path, help="Input path", default="input/")
|
|
518
619
|
parser.add_argument("--output", type=Path, help="Output path", default="output/")
|
|
519
620
|
parser.add_argument(
|
|
@@ -534,6 +635,19 @@ def main() -> None:
|
|
|
534
635
|
default=False,
|
|
535
636
|
action=argparse.BooleanOptionalAction,
|
|
536
637
|
)
|
|
638
|
+
parser.add_argument(
|
|
639
|
+
"--force-download",
|
|
640
|
+
help="Force re-download of input files even if they exist locally",
|
|
641
|
+
default=False,
|
|
642
|
+
action=argparse.BooleanOptionalAction,
|
|
643
|
+
)
|
|
644
|
+
parser.add_argument(
|
|
645
|
+
"-v",
|
|
646
|
+
"--variables",
|
|
647
|
+
type=str,
|
|
648
|
+
help="Variables to plot (comma-separated), e.g. 'target_classification'",
|
|
649
|
+
default=None,
|
|
650
|
+
)
|
|
537
651
|
args = parser.parse_args()
|
|
538
652
|
|
|
539
653
|
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?
|