cloudnetpy 1.91.2__tar.gz → 1.92.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.91.2/cloudnetpy.egg-info → cloudnetpy-1.92.0}/PKG-INFO +1 -1
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/categorize/model.py +1 -20
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/cli.py +27 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/output.py +47 -9
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/plotting/plot_meta.py +5 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/products/__init__.py +2 -0
- cloudnetpy-1.91.2/cloudnetpy/products/epsilon.py → cloudnetpy-1.92.0/cloudnetpy/products/epsilon_lidar.py +2 -2
- cloudnetpy-1.92.0/cloudnetpy/products/epsilon_radar.py +373 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/utils.py +22 -1
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/version.py +2 -2
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0/cloudnetpy.egg-info}/PKG-INFO +1 -1
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy.egg-info/SOURCES.txt +2 -1
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/LICENSE +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/MANIFEST.in +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/README.md +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/__init__.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/categorize/__init__.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/categorize/atmos_utils.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/categorize/attenuation.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/categorize/attenuations/__init__.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/categorize/attenuations/gas_attenuation.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/categorize/attenuations/liquid_attenuation.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/categorize/attenuations/melting_attenuation.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/categorize/attenuations/rain_attenuation.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/categorize/categorize.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/categorize/classify.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/categorize/containers.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/categorize/disdrometer.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/categorize/droplet.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/categorize/falling.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/categorize/freezing.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/categorize/insects.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/categorize/lidar.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/categorize/melting.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/categorize/mwr.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/categorize/radar.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/cloudnetarray.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/concat_lib.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/constants.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/datasource.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/disdronator/__init__.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/disdronator/lpm.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/disdronator/parsivel.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/disdronator/rd80.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/disdronator/utils.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/exceptions.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/__init__.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/basta.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/bowtie.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/ceilo.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/ceilometer.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/cl61d.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/cloudnet_instrument.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/copernicus.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/da10.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/disdrometer/__init__.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/disdrometer/common.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/disdrometer/parsivel.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/disdrometer/rd80.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/disdrometer/thies.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/fd12p.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/galileo.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/hatpro.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/instruments.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/lufft.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/mira.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/mrr.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/nc_lidar.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/nc_radar.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/pollyxt.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/radiometrics.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/rain_e_h3.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/rpg.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/rpg_reader.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/toa5.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/vaisala.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/weather_radar.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/weather_station.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/metadata.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/__init__.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/file_handler.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/metadata.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/model_metadata.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/plotting/__init__.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/plotting/plot_meta.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/plotting/plot_tools.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/plotting/plotting.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/products/__init__.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/products/advance_methods.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/products/grid_methods.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/products/model_products.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/products/observation_products.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/products/product_resampling.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/products/tools.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/statistics/__init__.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/statistics/statistical_methods.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/__init__.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/e2e/__init__.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/e2e/conftest.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/e2e/process_cf/__init__.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/e2e/process_cf/main.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/e2e/process_cf/tests.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/e2e/process_iwc/__init__.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/e2e/process_iwc/main.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/e2e/process_iwc/tests.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/e2e/process_lwc/__init__.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/e2e/process_lwc/main.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/e2e/process_lwc/tests.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/unit/__init__.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/unit/conftest.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/unit/test_advance_methods.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/unit/test_grid_methods.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/unit/test_model_products.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/unit/test_observation_products.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/unit/test_plot_tools.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/unit/test_plotting.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/unit/test_statistical_methods.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/unit/test_tools.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/utils.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/plotting/__init__.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/plotting/plotting.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/products/classification.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/products/der.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/products/drizzle.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/products/drizzle_error.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/products/drizzle_tools.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/products/ier.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/products/iwc.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/products/lwc.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/products/mie_lu_tables.nc +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/products/mwr_tools.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/products/product_tools.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/py.typed +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy.egg-info/dependency_links.txt +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy.egg-info/entry_points.txt +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy.egg-info/requires.txt +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy.egg-info/top_level.txt +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/docs/source/conf.py +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/pyproject.toml +0 -0
- {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/setup.cfg +0 -0
|
@@ -11,7 +11,6 @@ from scipy.interpolate import interp1d
|
|
|
11
11
|
|
|
12
12
|
from cloudnetpy import utils
|
|
13
13
|
from cloudnetpy.cloudnetarray import CloudnetArray
|
|
14
|
-
from cloudnetpy.constants import G
|
|
15
14
|
from cloudnetpy.datasource import DataSource
|
|
16
15
|
from cloudnetpy.exceptions import ModelDataError
|
|
17
16
|
|
|
@@ -146,27 +145,9 @@ class Model(DataSource):
|
|
|
146
145
|
except KeyError as err:
|
|
147
146
|
msg = "No 'height' variable in the model file."
|
|
148
147
|
raise ModelDataError(msg) from err
|
|
149
|
-
surface_altitude = self.
|
|
148
|
+
surface_altitude = utils.get_model_surface_altitude(self.dataset, alt_site)
|
|
150
149
|
return self.to_m(model_heights) + surface_altitude
|
|
151
150
|
|
|
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
|
|
169
|
-
|
|
170
151
|
def calc_attenuations(self, frequency: float) -> None:
|
|
171
152
|
temperature = self.getvar("temperature")
|
|
172
153
|
pressure = self.getvar("pressure")
|
|
@@ -90,6 +90,33 @@ def run(args: argparse.Namespace, tmpdir: str, client: APIClient) -> None:
|
|
|
90
90
|
l2_filename = _process_mwrpy_product(product, mwrpy_filepath, args)
|
|
91
91
|
_plot(l2_filename, product, args)
|
|
92
92
|
|
|
93
|
+
# Radar + model based products (e.g. epsilon-radar)
|
|
94
|
+
if "epsilon-radar" in args.products:
|
|
95
|
+
epsilon_filepath = _process_epsilon_radar(cat_files, args, client)
|
|
96
|
+
if epsilon_filepath is not None:
|
|
97
|
+
_plot(epsilon_filepath, "epsilon-radar", args)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _process_epsilon_radar(
|
|
101
|
+
cat_files: dict, args: argparse.Namespace, client: APIClient
|
|
102
|
+
) -> str | None:
|
|
103
|
+
radar_filepath = cat_files.get("radar") or _fetch_product(args, "radar", client)
|
|
104
|
+
if radar_filepath is None:
|
|
105
|
+
logging.info("No radar data available for epsilon-radar")
|
|
106
|
+
return None
|
|
107
|
+
model_filepath = _fetch_model(args, client)
|
|
108
|
+
if model_filepath is None:
|
|
109
|
+
logging.info("No model data available for epsilon-radar")
|
|
110
|
+
return None
|
|
111
|
+
filename = f"{args.date.replace('-', '')}_{args.site}_epsilon-radar.nc"
|
|
112
|
+
output_file = _create_output_folder("geophysical", args) / filename
|
|
113
|
+
products = importlib.import_module("cloudnetpy.products")
|
|
114
|
+
products.generate_epsilon_from_radar(
|
|
115
|
+
str(radar_filepath), model_filepath, str(output_file)
|
|
116
|
+
)
|
|
117
|
+
logging.info("Processed epsilon-radar: %s", output_file)
|
|
118
|
+
return str(output_file)
|
|
119
|
+
|
|
93
120
|
|
|
94
121
|
def _process_categorize(
|
|
95
122
|
input_files: dict,
|
|
@@ -74,6 +74,7 @@ def save_product_file(
|
|
|
74
74
|
file_name: str | PathLike,
|
|
75
75
|
uuid: UUID,
|
|
76
76
|
copy_from_cat: tuple = (),
|
|
77
|
+
extra_sources: tuple[DataSource, ...] = (),
|
|
77
78
|
) -> None:
|
|
78
79
|
"""Saves a standard Cloudnet product file.
|
|
79
80
|
|
|
@@ -83,13 +84,18 @@ def save_product_file(
|
|
|
83
84
|
file_name: Name of the output file to be generated.
|
|
84
85
|
uuid: Set specific UUID for the file.
|
|
85
86
|
copy_from_cat: Variables to be copied from the categorize file.
|
|
87
|
+
extra_sources: Additional input DataSources whose ``file_uuid`` should
|
|
88
|
+
also be listed in ``source_file_uuids`` (e.g. a model file used
|
|
89
|
+
alongside the primary L1b input).
|
|
86
90
|
|
|
87
91
|
"""
|
|
88
92
|
human_readable_file_type = _get_identifier(short_id)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
"height"
|
|
92
|
-
|
|
93
|
+
height_size = (
|
|
94
|
+
len(obj.data["height"][:])
|
|
95
|
+
if "height" in obj.data
|
|
96
|
+
else len(obj.dataset.variables["height"])
|
|
97
|
+
)
|
|
98
|
+
dimensions = {"time": len(obj.time), "height": height_size}
|
|
93
99
|
with init_file(file_name, dimensions, obj.data, uuid) as nc:
|
|
94
100
|
nc.cloudnet_file_type = short_id
|
|
95
101
|
vars_from_source = (
|
|
@@ -105,7 +111,7 @@ def save_product_file(
|
|
|
105
111
|
f"{human_readable_file_type.capitalize()} products from"
|
|
106
112
|
f" {obj.dataset.location}"
|
|
107
113
|
)
|
|
108
|
-
nc.source_file_uuids = get_source_uuids([nc, obj])
|
|
114
|
+
nc.source_file_uuids = get_source_uuids([nc, obj, *extra_sources])
|
|
109
115
|
copy_global(
|
|
110
116
|
obj.dataset,
|
|
111
117
|
nc,
|
|
@@ -116,13 +122,28 @@ def save_product_file(
|
|
|
116
122
|
"year",
|
|
117
123
|
"source",
|
|
118
124
|
"source_instrument_pids",
|
|
125
|
+
"instrument_pid",
|
|
119
126
|
"voodoonet_version",
|
|
120
127
|
),
|
|
121
128
|
)
|
|
122
|
-
|
|
129
|
+
_append_extra_sources(nc, extra_sources)
|
|
130
|
+
merge_history(nc, human_readable_file_type, obj, extra_sources=extra_sources)
|
|
123
131
|
nc.references = get_references(short_id)
|
|
124
132
|
|
|
125
133
|
|
|
134
|
+
def _append_extra_sources(
|
|
135
|
+
nc: netCDF4.Dataset, extra_sources: tuple[DataSource, ...]
|
|
136
|
+
) -> None:
|
|
137
|
+
"""Merges ``source`` strings from auxiliary input files into ``nc.source``."""
|
|
138
|
+
if not extra_sources:
|
|
139
|
+
return
|
|
140
|
+
existing = nc.source.split("\n") if "source" in nc.ncattrs() else []
|
|
141
|
+
extras = [src.dataset.source for src in extra_sources if src.dataset.source]
|
|
142
|
+
merged = list(dict.fromkeys([*existing, *extras]))
|
|
143
|
+
if merged:
|
|
144
|
+
nc.source = "\n".join(merged)
|
|
145
|
+
|
|
146
|
+
|
|
126
147
|
def get_l1b_source(instrument: Instrument) -> str:
|
|
127
148
|
"""Returns level 1b file source."""
|
|
128
149
|
parts = [
|
|
@@ -173,6 +194,11 @@ def get_references(identifier: str | None = None, extra: list | None = None) ->
|
|
|
173
194
|
references += ", https://doi.org/10.1175/JAM2340.1"
|
|
174
195
|
case "drizzle":
|
|
175
196
|
references += ", https://doi.org/10.1175/JAM-2181.1"
|
|
197
|
+
case "epsilon-radar":
|
|
198
|
+
references += (
|
|
199
|
+
", https://doi.org/10.5194/amt-13-5335-2020"
|
|
200
|
+
", https://doi.org/10.1002/2015JD024543"
|
|
201
|
+
)
|
|
176
202
|
if extra is not None:
|
|
177
203
|
for reference in extra:
|
|
178
204
|
references += f", {reference}"
|
|
@@ -203,14 +229,21 @@ def get_source_uuids(data: Observations | list[netCDF4.Dataset | DataSource]) ->
|
|
|
203
229
|
|
|
204
230
|
|
|
205
231
|
def merge_history(
|
|
206
|
-
nc: netCDF4.Dataset,
|
|
232
|
+
nc: netCDF4.Dataset,
|
|
233
|
+
file_type: str,
|
|
234
|
+
data: Observations | DataSource,
|
|
235
|
+
extra_sources: tuple[DataSource, ...] = (),
|
|
207
236
|
) -> None:
|
|
208
237
|
"""Merges history fields from one or several files and creates a new record."""
|
|
209
238
|
|
|
210
239
|
def extract_history(obj: DataSource | Observations) -> list[str]:
|
|
211
240
|
if hasattr(obj, "dataset") and hasattr(obj.dataset, "history"):
|
|
212
241
|
history = obj.dataset.history
|
|
213
|
-
|
|
242
|
+
is_model_file = (
|
|
243
|
+
isinstance(obj, Model)
|
|
244
|
+
or getattr(obj.dataset, "cloudnet_file_type", "") == "model"
|
|
245
|
+
)
|
|
246
|
+
if is_model_file:
|
|
214
247
|
return [history.split("\n")[-1]]
|
|
215
248
|
return history.split("\n")
|
|
216
249
|
return []
|
|
@@ -221,6 +254,8 @@ def merge_history(
|
|
|
221
254
|
elif isinstance(data, Observations):
|
|
222
255
|
for field in fields(data):
|
|
223
256
|
histories.extend(extract_history(getattr(data, field.name)))
|
|
257
|
+
for src in extra_sources:
|
|
258
|
+
histories.extend(extract_history(src))
|
|
224
259
|
|
|
225
260
|
# Remove duplicates
|
|
226
261
|
histories = list(dict.fromkeys(histories))
|
|
@@ -298,7 +333,7 @@ def copy_variables(
|
|
|
298
333
|
|
|
299
334
|
"""
|
|
300
335
|
for key in keys:
|
|
301
|
-
if key in source.variables:
|
|
336
|
+
if key in source.variables and key not in target.variables:
|
|
302
337
|
fill_value = getattr(source.variables[key], "_FillValue", False)
|
|
303
338
|
variable = source.variables[key]
|
|
304
339
|
var_out = target.createVariable(
|
|
@@ -436,6 +471,7 @@ def _get_identifier(short_id: str) -> str:
|
|
|
436
471
|
"der",
|
|
437
472
|
"ier",
|
|
438
473
|
"classification-voodoo",
|
|
474
|
+
"epsilon-radar",
|
|
439
475
|
)
|
|
440
476
|
if short_id not in valid_ids:
|
|
441
477
|
msg = f"Invalid file identifier: {short_id}"
|
|
@@ -448,6 +484,8 @@ def _get_identifier(short_id: str) -> str:
|
|
|
448
484
|
return "ice effective radius"
|
|
449
485
|
if short_id == "der":
|
|
450
486
|
return "droplet effective radius"
|
|
487
|
+
if short_id == "epsilon-radar":
|
|
488
|
+
return "dissipation rate of turbulent kinetic energy"
|
|
451
489
|
return short_id
|
|
452
490
|
|
|
453
491
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from .classification import generate_classification
|
|
2
2
|
from .der import generate_der
|
|
3
3
|
from .drizzle import generate_drizzle
|
|
4
|
+
from .epsilon_lidar import generate_epsilon_from_lidar
|
|
5
|
+
from .epsilon_radar import generate_epsilon_from_radar
|
|
4
6
|
from .ier import generate_ier
|
|
5
7
|
from .iwc import generate_iwc
|
|
6
8
|
from .lwc import generate_lwc
|
|
@@ -12,8 +12,8 @@ from doppy.product.turbulence import HorizontalWind, Options, Turbulence, Vertic
|
|
|
12
12
|
from scipy.interpolate import LinearNDInterpolator, NearestNDInterpolator
|
|
13
13
|
|
|
14
14
|
import cloudnetpy
|
|
15
|
+
from cloudnetpy import output
|
|
15
16
|
from cloudnetpy.exceptions import ValidTimeStampError
|
|
16
|
-
from cloudnetpy.output import copy_variables
|
|
17
17
|
from cloudnetpy.utils import get_time, get_uuid
|
|
18
18
|
|
|
19
19
|
|
|
@@ -95,7 +95,7 @@ def generate_epsilon_from_lidar(
|
|
|
95
95
|
netCDF4.Dataset(doppler_lidar_file, "r") as nc_src_stare,
|
|
96
96
|
netCDF4.Dataset(doppler_lidar_wind_file, "r") as nc_src_wind,
|
|
97
97
|
):
|
|
98
|
-
copy_variables(
|
|
98
|
+
output.copy_variables(
|
|
99
99
|
nc_src_stare, nc_out, ("latitude", "longitude", "altitude", "source")
|
|
100
100
|
)
|
|
101
101
|
nc_out.source_file_uuids = f"{nc_src_stare.file_uuid}, {nc_src_wind.file_uuid}"
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"""Module for creating Cloudnet eddy dissipation rate product, based on the
|
|
2
|
+
pipeline of Griesche et al. (2020) with the inertial-subrange slope-acceptance
|
|
3
|
+
criterion of Borque et al. (2016).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from os import PathLike
|
|
8
|
+
from uuid import UUID
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import numpy.typing as npt
|
|
12
|
+
from numpy import ma
|
|
13
|
+
from scipy.interpolate import CubicSpline, RectBivariateSpline
|
|
14
|
+
|
|
15
|
+
from cloudnetpy import output, utils
|
|
16
|
+
from cloudnetpy.datasource import DataSource
|
|
17
|
+
from cloudnetpy.metadata import COMMON_ATTRIBUTES, MetaData
|
|
18
|
+
from cloudnetpy.utils import get_uuid
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def generate_epsilon_from_radar(
|
|
22
|
+
radar_file: str | PathLike,
|
|
23
|
+
model_file: str | PathLike,
|
|
24
|
+
output_file: str | PathLike,
|
|
25
|
+
uuid: str | UUID | None = None,
|
|
26
|
+
) -> UUID:
|
|
27
|
+
"""Generates Cloudnet radar-based dissipation rate of turbulent kinetic
|
|
28
|
+
energy product.
|
|
29
|
+
|
|
30
|
+
Based on the pipeline of Griesche et al. (2020) with the inertial-subrange
|
|
31
|
+
slope-acceptance criterion of Borque et al. (2016).
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
radar_file: Cloud radar L1b file name (provides Doppler velocity).
|
|
35
|
+
model_file: Cloudnet model file name (provides horizontal wind).
|
|
36
|
+
output_file: Output file name.
|
|
37
|
+
uuid: Set specific UUID for the file.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
UUID of the generated file.
|
|
41
|
+
|
|
42
|
+
Examples:
|
|
43
|
+
>>> from cloudnetpy.products import generate_epsilon_from_radar
|
|
44
|
+
>>> generate_epsilon_from_radar('radar.nc', 'ecmwf.nc', 'epsilon.nc')
|
|
45
|
+
|
|
46
|
+
References:
|
|
47
|
+
Griesche, H. J., Seifert, P., Ansmann, A., Baars, H., Barrientos
|
|
48
|
+
Velasco, C., Bühl, J., Engelmann, R., Radenz, M., Zhenping, Y., and
|
|
49
|
+
Macke, A. (2020): Application of the shipborne remote sensing
|
|
50
|
+
supersite OCEANET for profiling of Arctic aerosols and clouds during
|
|
51
|
+
Polarstern cruise PS106, Atmos. Meas. Tech., 13, 5335-5358,
|
|
52
|
+
https://doi.org/10.5194/amt-13-5335-2020.
|
|
53
|
+
|
|
54
|
+
Borque, P., Luke, E., and Kollias, P. (2016): On the unified
|
|
55
|
+
estimation of turbulence eddy dissipation rate using Doppler cloud
|
|
56
|
+
radars and lidars, J. Geophys. Res. Atmos., 120, 5972-5989,
|
|
57
|
+
https://doi.org/10.1002/2015JD024543.
|
|
58
|
+
"""
|
|
59
|
+
uuid = get_uuid(uuid)
|
|
60
|
+
with (
|
|
61
|
+
EpsilonRadarSource(radar_file) as epsilon_source,
|
|
62
|
+
DataSource(model_file) as model_source,
|
|
63
|
+
):
|
|
64
|
+
if epsilon_source.altitude is None:
|
|
65
|
+
msg = "Radar file is missing 'altitude' attribute."
|
|
66
|
+
raise ValueError(msg)
|
|
67
|
+
wind_interp = _get_wind_interpolator(
|
|
68
|
+
model_source, alt_site=float(epsilon_source.altitude)
|
|
69
|
+
)
|
|
70
|
+
epsilon_source.append_epsilon(wind_interp)
|
|
71
|
+
epsilon_source.append_grid_variables()
|
|
72
|
+
date = epsilon_source.get_date()
|
|
73
|
+
attributes = output.add_time_attribute(EPSILON_RADAR_ATTRIBUTES, date)
|
|
74
|
+
output.update_attributes(epsilon_source.data, attributes)
|
|
75
|
+
output.save_product_file(
|
|
76
|
+
"epsilon-radar",
|
|
77
|
+
epsilon_source,
|
|
78
|
+
output_file,
|
|
79
|
+
uuid,
|
|
80
|
+
extra_sources=(model_source,),
|
|
81
|
+
)
|
|
82
|
+
return uuid
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _get_wind_interpolator(
|
|
86
|
+
model: DataSource, alt_site: float
|
|
87
|
+
) -> Callable[[npt.NDArray, npt.NDArray], npt.NDArray]:
|
|
88
|
+
"""Returns a bilinear interpolator for horizontal wind speed on
|
|
89
|
+
(time, height_amsl) where height is meters above MSL.
|
|
90
|
+
|
|
91
|
+
Model heights are above the model's own surface; shift to absolute MSL so
|
|
92
|
+
they share the radar's height frame. In complex terrain the model grid
|
|
93
|
+
cell surface can differ from the site altitude, so prefer the model's own
|
|
94
|
+
surface field when available.
|
|
95
|
+
"""
|
|
96
|
+
uwind = model.getvar("uwind")
|
|
97
|
+
vwind = model.getvar("vwind")
|
|
98
|
+
surface_altitude = utils.get_model_surface_altitude(model.dataset, alt_site)
|
|
99
|
+
heights = model.to_m(model.dataset.variables["height"]) + surface_altitude
|
|
100
|
+
wind_speed = np.hypot(np.asarray(uwind), np.asarray(vwind))
|
|
101
|
+
mean_height = np.asarray(ma.mean(heights, axis=0))
|
|
102
|
+
common = np.empty((wind_speed.shape[0], mean_height.size))
|
|
103
|
+
for i in range(wind_speed.shape[0]):
|
|
104
|
+
common[i] = np.interp(mean_height, np.asarray(heights[i]), wind_speed[i])
|
|
105
|
+
return RectBivariateSpline(model.time, mean_height, common, kx=1, ky=1)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class EpsilonRadarSource(DataSource):
|
|
109
|
+
"""Reads radar Doppler velocity and computes turbulent kinetic energy
|
|
110
|
+
dissipation rate (epsilon) on the product grid.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
height: npt.NDArray
|
|
114
|
+
|
|
115
|
+
def append_epsilon(
|
|
116
|
+
self, wind_interp: Callable[[npt.NDArray, npt.NDArray], npt.NDArray]
|
|
117
|
+
) -> None:
|
|
118
|
+
"""Estimate the dissipation rate (epsilon) on the 30 s product grid."""
|
|
119
|
+
radar_time = np.asarray(self.getvar("time"))
|
|
120
|
+
self._radar_time = radar_time
|
|
121
|
+
height = self.height
|
|
122
|
+
radar_dt_sec = float(np.round(np.median(np.diff(radar_time)) * 3600.0))
|
|
123
|
+
|
|
124
|
+
v = self.getvar("v")
|
|
125
|
+
if isinstance(v, ma.MaskedArray):
|
|
126
|
+
v = v.filled(np.nan)
|
|
127
|
+
v = np.asarray(v, dtype=np.float64)
|
|
128
|
+
|
|
129
|
+
product_time = utils.time_grid(time_step=PRODUCT_TIME_STEP_SEC)
|
|
130
|
+
self.time = product_time
|
|
131
|
+
wind_at_product = wind_interp(product_time, height)
|
|
132
|
+
|
|
133
|
+
n_time = product_time.size
|
|
134
|
+
n_height = height.size
|
|
135
|
+
epsilon = np.full((n_time, n_height), EPSILON_INVALID, dtype=np.float64)
|
|
136
|
+
epsilon_error = np.full((n_time, n_height), EPSILON_INVALID, dtype=np.float64)
|
|
137
|
+
|
|
138
|
+
starts = np.searchsorted(radar_time, product_time, side="right")
|
|
139
|
+
stops = np.searchsorted(
|
|
140
|
+
radar_time, product_time + AVERAGING_TIME_HR, side="right"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
for time_idx in range(n_time):
|
|
144
|
+
after, stop = int(starts[time_idx]), int(stops[time_idx])
|
|
145
|
+
if stop <= after:
|
|
146
|
+
continue
|
|
147
|
+
time_window = radar_time[after:stop]
|
|
148
|
+
min_valid = int(MIN_VALID_FRACTION * time_window.size)
|
|
149
|
+
|
|
150
|
+
for height_idx in range(n_height):
|
|
151
|
+
vel = v[after:stop, height_idx]
|
|
152
|
+
nan_mask = np.isnan(vel)
|
|
153
|
+
if nan_mask[:2].all() or nan_mask[-2:].all():
|
|
154
|
+
continue
|
|
155
|
+
if (vel.size - int(nan_mask.sum())) < min_valid:
|
|
156
|
+
continue
|
|
157
|
+
if nan_mask.any():
|
|
158
|
+
valid = ~nan_mask
|
|
159
|
+
vel = CubicSpline(time_window[valid], vel[valid])(time_window)
|
|
160
|
+
|
|
161
|
+
wind_speed = float(wind_at_product[time_idx, height_idx])
|
|
162
|
+
if not np.isfinite(wind_speed) or wind_speed <= 0:
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
freq_sp, power_sp = _periodogram(vel, radar_dt_sec, wind_speed)
|
|
166
|
+
if freq_sp.size < 2:
|
|
167
|
+
continue
|
|
168
|
+
|
|
169
|
+
result = _epsilon_from_spectrum(freq_sp, power_sp)
|
|
170
|
+
if result is not None:
|
|
171
|
+
eps, eps_err = result
|
|
172
|
+
epsilon[time_idx, height_idx] = eps
|
|
173
|
+
epsilon_error[time_idx, height_idx] = eps_err
|
|
174
|
+
|
|
175
|
+
self.append_data(
|
|
176
|
+
ma.masked_where(epsilon == EPSILON_INVALID, epsilon), "epsilon"
|
|
177
|
+
)
|
|
178
|
+
self.append_data(
|
|
179
|
+
ma.masked_where(
|
|
180
|
+
(epsilon_error == EPSILON_INVALID) | ~np.isfinite(epsilon_error),
|
|
181
|
+
epsilon_error,
|
|
182
|
+
),
|
|
183
|
+
"epsilon_error",
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
def append_grid_variables(self) -> None:
|
|
187
|
+
"""Adds time/height/altitude/latitude/longitude on the product grid.
|
|
188
|
+
|
|
189
|
+
altitude/latitude/longitude are always written as 1-D arrays on the
|
|
190
|
+
product time grid: scalars from stationary radars are broadcast,
|
|
191
|
+
time-varying fields from moving platforms are rebinned.
|
|
192
|
+
"""
|
|
193
|
+
self.append_data(self.time, "time", dtype="f8")
|
|
194
|
+
self.append_data(np.asarray(self.height, dtype=np.float32), "height")
|
|
195
|
+
for key in ("altitude", "latitude", "longitude"):
|
|
196
|
+
if key not in self.dataset.variables:
|
|
197
|
+
continue
|
|
198
|
+
src = np.asarray(self.dataset.variables[key][:])
|
|
199
|
+
if src.ndim == 0:
|
|
200
|
+
values = np.full(self.time.size, float(src), dtype=np.float32)
|
|
201
|
+
else:
|
|
202
|
+
values = utils.rebin_1d(self._radar_time, src, self.time)
|
|
203
|
+
self.append_data(values, key)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _periodogram(
|
|
207
|
+
vel: npt.NDArray, delta_t_sec: float, adv_vel: float
|
|
208
|
+
) -> tuple[npt.NDArray, npt.NDArray]:
|
|
209
|
+
"""Compute angular wavenumber spectrum from a Doppler velocity series."""
|
|
210
|
+
n = vel.size
|
|
211
|
+
delta_x = delta_t_sec * adv_vel
|
|
212
|
+
fft_result = np.fft.rfft(vel)[1:]
|
|
213
|
+
power_sp = (delta_x / n) * np.abs(fft_result) ** 2 / np.pi
|
|
214
|
+
freq_sp = np.fft.rfftfreq(n, d=delta_x)[1:] * 2.0 * np.pi
|
|
215
|
+
keep = power_sp > 0
|
|
216
|
+
return freq_sp[keep], power_sp[keep]
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _epsilon_from_spectrum(
|
|
220
|
+
freq_sp: npt.NDArray,
|
|
221
|
+
power_sp: npt.NDArray,
|
|
222
|
+
) -> tuple[float, float] | None:
|
|
223
|
+
"""Vectorised slope fit on log-log spectrum across all frequency bands.
|
|
224
|
+
|
|
225
|
+
Uses cumulative sums over a sorted spectrum so each band's least-squares
|
|
226
|
+
fit reduces to a constant-time index lookup. Returns ``(mean, std)`` of
|
|
227
|
+
epsilon across accepted bands -- the band-spread used by Griesche et al.
|
|
228
|
+
(2020) as the random retrieval uncertainty -- or ``None`` if no band
|
|
229
|
+
passes the slope filter. ``std`` is NaN when only a single band passes.
|
|
230
|
+
"""
|
|
231
|
+
log_f = np.log10(freq_sp)
|
|
232
|
+
log_p = np.log10(power_sp)
|
|
233
|
+
|
|
234
|
+
csx = np.concatenate(([0.0], np.cumsum(log_f)))
|
|
235
|
+
csy = np.concatenate(([0.0], np.cumsum(log_p)))
|
|
236
|
+
csxx = np.concatenate(([0.0], np.cumsum(log_f * log_f)))
|
|
237
|
+
csxy = np.concatenate(([0.0], np.cumsum(log_f * log_p)))
|
|
238
|
+
|
|
239
|
+
lo = np.searchsorted(freq_sp, _FMIN_ARR, side="right")
|
|
240
|
+
hi = np.searchsorted(freq_sp, _FMAX_ARR, side="left")
|
|
241
|
+
n = hi - lo
|
|
242
|
+
|
|
243
|
+
sx = csx[hi] - csx[lo]
|
|
244
|
+
sy = csy[hi] - csy[lo]
|
|
245
|
+
sxx = csxx[hi] - csxx[lo]
|
|
246
|
+
sxy = csxy[hi] - csxy[lo]
|
|
247
|
+
|
|
248
|
+
with np.errstate(invalid="ignore", divide="ignore"):
|
|
249
|
+
slope = (n * sxy - sx * sy) / (n * sxx - sx * sx)
|
|
250
|
+
intercept = (sy - slope * sx) / n
|
|
251
|
+
|
|
252
|
+
valid = (
|
|
253
|
+
(n >= 2)
|
|
254
|
+
& np.isfinite(slope)
|
|
255
|
+
& (slope > THRESHOLD_SLOPE_MIN)
|
|
256
|
+
& (slope < THRESHOLD_SLOPE_MAX)
|
|
257
|
+
)
|
|
258
|
+
if not valid.any():
|
|
259
|
+
return None
|
|
260
|
+
epsilon = (10.0 ** intercept[valid] / KOLMOGOROV_CONSTANT) ** 1.5
|
|
261
|
+
std = float(np.std(epsilon, ddof=1)) if epsilon.size >= 2 else float("nan")
|
|
262
|
+
return float(np.mean(epsilon)), std
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
KOLMOGOROV_CONSTANT = 0.5
|
|
266
|
+
THRESHOLD_SLOPE_MIN = -2.0 # -5/3 - 20% (Borque, 2016)
|
|
267
|
+
THRESHOLD_SLOPE_MAX = -1.33 # -5/3 + 20% (Borque, 2016)
|
|
268
|
+
AVERAGING_TIME_HR = 5.0 / 60.0
|
|
269
|
+
PRODUCT_TIME_STEP_SEC = 30
|
|
270
|
+
MIN_VALID_FRACTION = 0.9
|
|
271
|
+
EPSILON_INVALID = -999.0
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
COMMENTS = {
|
|
275
|
+
"general": (
|
|
276
|
+
"This dataset contains the dissipation rate of turbulent kinetic\n"
|
|
277
|
+
"energy calculated using the pipeline of Griesche et al. (2020)\n"
|
|
278
|
+
"with the inertial-subrange slope-acceptance criterion of Borque\n"
|
|
279
|
+
"et al. (2016). The turbulent energy spectrum is derived as the\n"
|
|
280
|
+
"power spectrum of cloud radar Doppler velocity over 5-minute\n"
|
|
281
|
+
"windows and converted to a wavenumber spectrum using model\n"
|
|
282
|
+
"horizontal wind via Taylor's hypothesis. The dissipation rate is\n"
|
|
283
|
+
"recovered from the intercept of a log-log fit across multiple\n"
|
|
284
|
+
"frequency bands; bands whose slope falls outside -5/3 +/- 20% are\n"
|
|
285
|
+
"rejected and the accepted bands are averaged.\n"
|
|
286
|
+
),
|
|
287
|
+
"epsilon": (
|
|
288
|
+
"This variable was calculated for profiles where 5 minutes of\n"
|
|
289
|
+
"continuous cloud radar Doppler velocity was available. With less\n"
|
|
290
|
+
"than 10% missing data, the missing values were filled by cubic\n"
|
|
291
|
+
"spline interpolation.\n"
|
|
292
|
+
),
|
|
293
|
+
"epsilon_error": (
|
|
294
|
+
"Random uncertainty estimated as the standard deviation of epsilon\n"
|
|
295
|
+
"across the frequency bands accepted by the inertial-subrange\n"
|
|
296
|
+
"slope criterion (Griesche et al. 2020). It captures the scatter\n"
|
|
297
|
+
"introduced by the multi-band fit but does not include systematic\n"
|
|
298
|
+
"contributions from horizontal wind uncertainty or the Kolmogorov\n"
|
|
299
|
+
"constant. The variable is masked where only a single band passed\n"
|
|
300
|
+
"the slope filter and no spread can be estimated.\n"
|
|
301
|
+
),
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
EPSILON_RADAR_ATTRIBUTES = {
|
|
306
|
+
"comment": COMMENTS["general"],
|
|
307
|
+
"epsilon": MetaData(
|
|
308
|
+
long_name="Dissipation rate of turbulent kinetic energy",
|
|
309
|
+
units="m2 s-3",
|
|
310
|
+
ancillary_variables="epsilon_error",
|
|
311
|
+
comment=COMMENTS["epsilon"],
|
|
312
|
+
dimensions=("time", "height"),
|
|
313
|
+
),
|
|
314
|
+
"epsilon_error": MetaData(
|
|
315
|
+
long_name="Absolute error in dissipation rate of turbulent kinetic energy",
|
|
316
|
+
units="m2 s-3",
|
|
317
|
+
comment=COMMENTS["epsilon_error"],
|
|
318
|
+
dimensions=("time", "height"),
|
|
319
|
+
),
|
|
320
|
+
"height": COMMON_ATTRIBUTES["height"]._replace(dimensions=("height",)),
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# Frequency-range pairs (rad/m) used to fit the inertial subrange slope.
|
|
325
|
+
# Adapted from Griesche et al. 2020 with a multi-decade extension.
|
|
326
|
+
freq_array = np.array(
|
|
327
|
+
[
|
|
328
|
+
[5e-3, 1.5e-2],
|
|
329
|
+
[5e-3, 3e-2],
|
|
330
|
+
[5e-3, 5e-2],
|
|
331
|
+
[8e-3, 2e-2],
|
|
332
|
+
[8e-3, 4e-2],
|
|
333
|
+
[8e-3, 6e-2],
|
|
334
|
+
[2e-2, 7e-2],
|
|
335
|
+
[2e-2, 1e-1],
|
|
336
|
+
[2e-2, 1.5e-1],
|
|
337
|
+
[3e-2, 1e-1],
|
|
338
|
+
[3e-2, 2e-1],
|
|
339
|
+
[3e-2, 3e-1],
|
|
340
|
+
[5e-2, 1.5e-1],
|
|
341
|
+
[5e-2, 3e-1],
|
|
342
|
+
[5e-2, 5e-1],
|
|
343
|
+
[8e-2, 2e-1],
|
|
344
|
+
[8e-2, 4e-1],
|
|
345
|
+
[8e-2, 6e-1],
|
|
346
|
+
[2e-1, 7e-1],
|
|
347
|
+
[2e-1, 1e0],
|
|
348
|
+
[2e-1, 1.5e0],
|
|
349
|
+
[3e-1, 1e0],
|
|
350
|
+
[3e-1, 2e0],
|
|
351
|
+
[3e-1, 3e0],
|
|
352
|
+
[5e-1, 1.5e0],
|
|
353
|
+
[5e-1, 3e0],
|
|
354
|
+
[5e-1, 5e0],
|
|
355
|
+
[8e-1, 2e0],
|
|
356
|
+
[8e-1, 4e0],
|
|
357
|
+
[8e-1, 6e0],
|
|
358
|
+
[2e0, 7e0],
|
|
359
|
+
[2e0, 1e1],
|
|
360
|
+
[2e0, 1.5e1],
|
|
361
|
+
[3e0, 1e1],
|
|
362
|
+
[3e0, 2e1],
|
|
363
|
+
[3e0, 3e1],
|
|
364
|
+
[5e0, 1.5e1],
|
|
365
|
+
[5e0, 3e1],
|
|
366
|
+
[5e0, 5e1],
|
|
367
|
+
[8e0, 2e1],
|
|
368
|
+
[8e0, 4e1],
|
|
369
|
+
[8e0, 6e1],
|
|
370
|
+
]
|
|
371
|
+
)
|
|
372
|
+
_FMIN_ARR = freq_array[:, 0]
|
|
373
|
+
_FMAX_ARR = freq_array[:, 1]
|
|
@@ -25,7 +25,7 @@ from scipy.interpolate import (
|
|
|
25
25
|
)
|
|
26
26
|
|
|
27
27
|
from cloudnetpy.cloudnetarray import CloudnetArray
|
|
28
|
-
from cloudnetpy.constants import SEC_IN_DAY, SEC_IN_HOUR, SEC_IN_MINUTE
|
|
28
|
+
from cloudnetpy.constants import SEC_IN_DAY, SEC_IN_HOUR, SEC_IN_MINUTE, G
|
|
29
29
|
from cloudnetpy.exceptions import ValidTimeStampError
|
|
30
30
|
|
|
31
31
|
|
|
@@ -875,6 +875,27 @@ def is_empty_line(line: str) -> bool:
|
|
|
875
875
|
return line in ("\n", "\r\n")
|
|
876
876
|
|
|
877
877
|
|
|
878
|
+
def get_model_surface_altitude(
|
|
879
|
+
dataset: netCDF4.Dataset, alt_site: float
|
|
880
|
+
) -> float | npt.NDArray:
|
|
881
|
+
"""Returns model surface altitude (m), preferring the model's own field.
|
|
882
|
+
|
|
883
|
+
For sites in complex terrain (e.g. mountains), the model grid cell
|
|
884
|
+
surface height can differ significantly from the actual site altitude.
|
|
885
|
+
Using the model's own surface height ensures that thermodynamic and
|
|
886
|
+
wind fields are placed at their physically correct absolute heights.
|
|
887
|
+
|
|
888
|
+
Returns a (n_time, 1) array when the model provides a per-timestep
|
|
889
|
+
surface field, otherwise the scalar ``alt_site`` fallback. Both shapes
|
|
890
|
+
broadcast against (n_time, n_height) model arrays.
|
|
891
|
+
"""
|
|
892
|
+
if "sfc_height" in dataset.variables:
|
|
893
|
+
return dataset.variables["sfc_height"][:][:, np.newaxis]
|
|
894
|
+
if "sfc_geopotential" in dataset.variables:
|
|
895
|
+
return dataset.variables["sfc_geopotential"][:][:, np.newaxis] / G
|
|
896
|
+
return alt_site
|
|
897
|
+
|
|
898
|
+
|
|
878
899
|
def is_timestamp(timestamp: str) -> bool:
|
|
879
900
|
"""Tests if the input string is formatted as -yyyy-mm-dd hh:mm:ss."""
|
|
880
901
|
reg_exp = re.compile(r"-\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}")
|
|
@@ -127,7 +127,8 @@ cloudnetpy/products/der.py
|
|
|
127
127
|
cloudnetpy/products/drizzle.py
|
|
128
128
|
cloudnetpy/products/drizzle_error.py
|
|
129
129
|
cloudnetpy/products/drizzle_tools.py
|
|
130
|
-
cloudnetpy/products/
|
|
130
|
+
cloudnetpy/products/epsilon_lidar.py
|
|
131
|
+
cloudnetpy/products/epsilon_radar.py
|
|
131
132
|
cloudnetpy/products/ier.py
|
|
132
133
|
cloudnetpy/products/iwc.py
|
|
133
134
|
cloudnetpy/products/lwc.py
|
|
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.91.2 → cloudnetpy-1.92.0}/cloudnetpy/categorize/attenuations/gas_attenuation.py
RENAMED
|
File without changes
|
{cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/categorize/attenuations/liquid_attenuation.py
RENAMED
|
File without changes
|
{cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/categorize/attenuations/melting_attenuation.py
RENAMED
|
File without changes
|
{cloudnetpy-1.91.2 → cloudnetpy-1.92.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/products/advance_methods.py
RENAMED
|
File without changes
|
{cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/products/grid_methods.py
RENAMED
|
File without changes
|
{cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/products/model_products.py
RENAMED
|
File without changes
|
{cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/products/observation_products.py
RENAMED
|
File without changes
|
{cloudnetpy-1.91.2 → cloudnetpy-1.92.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.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/e2e/process_cf/__init__.py
RENAMED
|
File without changes
|
{cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/e2e/process_cf/main.py
RENAMED
|
File without changes
|
{cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/e2e/process_cf/tests.py
RENAMED
|
File without changes
|
|
File without changes
|
{cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/e2e/process_iwc/main.py
RENAMED
|
File without changes
|
{cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/e2e/process_iwc/tests.py
RENAMED
|
File without changes
|
|
File without changes
|
{cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/e2e/process_lwc/main.py
RENAMED
|
File without changes
|
{cloudnetpy-1.91.2 → cloudnetpy-1.92.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.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/unit/test_grid_methods.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/unit/test_plot_tools.py
RENAMED
|
File without changes
|
{cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/unit/test_plotting.py
RENAMED
|
File without changes
|
|
File without changes
|
{cloudnetpy-1.91.2 → cloudnetpy-1.92.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
|