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.
Files changed (140) hide show
  1. {cloudnetpy-1.91.2/cloudnetpy.egg-info → cloudnetpy-1.92.0}/PKG-INFO +1 -1
  2. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/categorize/model.py +1 -20
  3. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/cli.py +27 -0
  4. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/output.py +47 -9
  5. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/plotting/plot_meta.py +5 -0
  6. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/products/__init__.py +2 -0
  7. cloudnetpy-1.91.2/cloudnetpy/products/epsilon.py → cloudnetpy-1.92.0/cloudnetpy/products/epsilon_lidar.py +2 -2
  8. cloudnetpy-1.92.0/cloudnetpy/products/epsilon_radar.py +373 -0
  9. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/utils.py +22 -1
  10. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/version.py +2 -2
  11. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0/cloudnetpy.egg-info}/PKG-INFO +1 -1
  12. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy.egg-info/SOURCES.txt +2 -1
  13. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/LICENSE +0 -0
  14. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/MANIFEST.in +0 -0
  15. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/README.md +0 -0
  16. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/__init__.py +0 -0
  17. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/categorize/__init__.py +0 -0
  18. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/categorize/atmos_utils.py +0 -0
  19. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/categorize/attenuation.py +0 -0
  20. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/categorize/attenuations/__init__.py +0 -0
  21. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/categorize/attenuations/gas_attenuation.py +0 -0
  22. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/categorize/attenuations/liquid_attenuation.py +0 -0
  23. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/categorize/attenuations/melting_attenuation.py +0 -0
  24. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/categorize/attenuations/rain_attenuation.py +0 -0
  25. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/categorize/categorize.py +0 -0
  26. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/categorize/classify.py +0 -0
  27. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/categorize/containers.py +0 -0
  28. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/categorize/disdrometer.py +0 -0
  29. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/categorize/droplet.py +0 -0
  30. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/categorize/falling.py +0 -0
  31. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/categorize/freezing.py +0 -0
  32. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/categorize/insects.py +0 -0
  33. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/categorize/lidar.py +0 -0
  34. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/categorize/melting.py +0 -0
  35. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/categorize/mwr.py +0 -0
  36. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/categorize/radar.py +0 -0
  37. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/cloudnetarray.py +0 -0
  38. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/concat_lib.py +0 -0
  39. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/constants.py +0 -0
  40. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/datasource.py +0 -0
  41. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/disdronator/__init__.py +0 -0
  42. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/disdronator/lpm.py +0 -0
  43. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/disdronator/parsivel.py +0 -0
  44. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/disdronator/rd80.py +0 -0
  45. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/disdronator/utils.py +0 -0
  46. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/exceptions.py +0 -0
  47. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/__init__.py +0 -0
  48. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/basta.py +0 -0
  49. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/bowtie.py +0 -0
  50. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/ceilo.py +0 -0
  51. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/ceilometer.py +0 -0
  52. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/cl61d.py +0 -0
  53. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/cloudnet_instrument.py +0 -0
  54. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/copernicus.py +0 -0
  55. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/da10.py +0 -0
  56. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/disdrometer/__init__.py +0 -0
  57. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/disdrometer/common.py +0 -0
  58. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/disdrometer/parsivel.py +0 -0
  59. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/disdrometer/rd80.py +0 -0
  60. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/disdrometer/thies.py +0 -0
  61. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/fd12p.py +0 -0
  62. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/galileo.py +0 -0
  63. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/hatpro.py +0 -0
  64. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/instruments.py +0 -0
  65. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/lufft.py +0 -0
  66. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/mira.py +0 -0
  67. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/mrr.py +0 -0
  68. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/nc_lidar.py +0 -0
  69. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/nc_radar.py +0 -0
  70. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/pollyxt.py +0 -0
  71. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/radiometrics.py +0 -0
  72. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/rain_e_h3.py +0 -0
  73. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/rpg.py +0 -0
  74. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/rpg_reader.py +0 -0
  75. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/toa5.py +0 -0
  76. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/vaisala.py +0 -0
  77. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/weather_radar.py +0 -0
  78. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/instruments/weather_station.py +0 -0
  79. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/metadata.py +0 -0
  80. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/__init__.py +0 -0
  81. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/file_handler.py +0 -0
  82. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/metadata.py +0 -0
  83. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/model_metadata.py +0 -0
  84. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/plotting/__init__.py +0 -0
  85. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/plotting/plot_meta.py +0 -0
  86. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/plotting/plot_tools.py +0 -0
  87. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/plotting/plotting.py +0 -0
  88. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/products/__init__.py +0 -0
  89. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/products/advance_methods.py +0 -0
  90. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/products/grid_methods.py +0 -0
  91. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/products/model_products.py +0 -0
  92. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/products/observation_products.py +0 -0
  93. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/products/product_resampling.py +0 -0
  94. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/products/tools.py +0 -0
  95. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/statistics/__init__.py +0 -0
  96. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/statistics/statistical_methods.py +0 -0
  97. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/__init__.py +0 -0
  98. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/e2e/__init__.py +0 -0
  99. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/e2e/conftest.py +0 -0
  100. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/e2e/process_cf/__init__.py +0 -0
  101. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/e2e/process_cf/main.py +0 -0
  102. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/e2e/process_cf/tests.py +0 -0
  103. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/e2e/process_iwc/__init__.py +0 -0
  104. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/e2e/process_iwc/main.py +0 -0
  105. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/e2e/process_iwc/tests.py +0 -0
  106. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/e2e/process_lwc/__init__.py +0 -0
  107. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/e2e/process_lwc/main.py +0 -0
  108. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/e2e/process_lwc/tests.py +0 -0
  109. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/unit/__init__.py +0 -0
  110. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/unit/conftest.py +0 -0
  111. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/unit/test_advance_methods.py +0 -0
  112. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/unit/test_grid_methods.py +0 -0
  113. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/unit/test_model_products.py +0 -0
  114. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/unit/test_observation_products.py +0 -0
  115. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/unit/test_plot_tools.py +0 -0
  116. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/unit/test_plotting.py +0 -0
  117. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/unit/test_statistical_methods.py +0 -0
  118. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/tests/unit/test_tools.py +0 -0
  119. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/model_evaluation/utils.py +0 -0
  120. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/plotting/__init__.py +0 -0
  121. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/plotting/plotting.py +0 -0
  122. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/products/classification.py +0 -0
  123. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/products/der.py +0 -0
  124. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/products/drizzle.py +0 -0
  125. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/products/drizzle_error.py +0 -0
  126. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/products/drizzle_tools.py +0 -0
  127. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/products/ier.py +0 -0
  128. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/products/iwc.py +0 -0
  129. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/products/lwc.py +0 -0
  130. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/products/mie_lu_tables.nc +0 -0
  131. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/products/mwr_tools.py +0 -0
  132. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/products/product_tools.py +0 -0
  133. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy/py.typed +0 -0
  134. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy.egg-info/dependency_links.txt +0 -0
  135. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy.egg-info/entry_points.txt +0 -0
  136. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy.egg-info/requires.txt +0 -0
  137. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/cloudnetpy.egg-info/top_level.txt +0 -0
  138. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/docs/source/conf.py +0 -0
  139. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/pyproject.toml +0 -0
  140. {cloudnetpy-1.91.2 → cloudnetpy-1.92.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudnetpy
3
- Version: 1.91.2
3
+ Version: 1.92.0
4
4
  Summary: Python package for Cloudnet processing
5
5
  Author: Simo Tukiainen
6
6
  License: MIT License
@@ -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._get_model_surface_altitude(alt_site)
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
- dimensions = {
90
- "time": len(obj.time),
91
- "height": len(obj.dataset.variables["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
- merge_history(nc, human_readable_file_type, obj)
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, file_type: str, data: Observations | DataSource
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
- if isinstance(obj, Model):
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
 
@@ -667,5 +667,10 @@ ATTRIBUTES = {
667
667
  plot_range=(1e-7, 1e-1),
668
668
  log_scale=True,
669
669
  ),
670
+ "epsilon_error": PlotMeta(
671
+ cmap="inferno",
672
+ plot_range=(1e-7, 1e-1),
673
+ log_scale=True,
674
+ ),
670
675
  },
671
676
  }
@@ -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}")
@@ -1,4 +1,4 @@
1
1
  MAJOR = 1
2
- MINOR = 91
3
- PATCH = 2
2
+ MINOR = 92
3
+ PATCH = 0
4
4
  __version__ = f"{MAJOR}.{MINOR}.{PATCH}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudnetpy
3
- Version: 1.91.2
3
+ Version: 1.92.0
4
4
  Summary: Python package for Cloudnet processing
5
5
  Author: Simo Tukiainen
6
6
  License: MIT License
@@ -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/epsilon.py
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