cloudnetpy 1.86.0__tar.gz → 1.86.2__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 (133) hide show
  1. {cloudnetpy-1.86.0/cloudnetpy.egg-info → cloudnetpy-1.86.2}/PKG-INFO +1 -1
  2. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/categorize/radar.py +0 -1
  3. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/instruments/bowtie.py +5 -0
  4. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/instruments/mira.py +11 -2
  5. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/instruments/rpg.py +125 -88
  6. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/output.py +5 -2
  7. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/utils.py +38 -3
  8. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/version.py +1 -1
  9. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2/cloudnetpy.egg-info}/PKG-INFO +1 -1
  10. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/LICENSE +0 -0
  11. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/MANIFEST.in +0 -0
  12. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/README.md +0 -0
  13. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/__init__.py +0 -0
  14. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/categorize/__init__.py +0 -0
  15. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/categorize/atmos_utils.py +0 -0
  16. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/categorize/attenuation.py +0 -0
  17. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/categorize/attenuations/__init__.py +0 -0
  18. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/categorize/attenuations/gas_attenuation.py +0 -0
  19. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/categorize/attenuations/liquid_attenuation.py +0 -0
  20. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/categorize/attenuations/melting_attenuation.py +0 -0
  21. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/categorize/attenuations/rain_attenuation.py +0 -0
  22. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/categorize/categorize.py +0 -0
  23. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/categorize/classify.py +0 -0
  24. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/categorize/containers.py +0 -0
  25. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/categorize/disdrometer.py +0 -0
  26. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/categorize/droplet.py +0 -0
  27. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/categorize/falling.py +0 -0
  28. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/categorize/freezing.py +0 -0
  29. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/categorize/insects.py +0 -0
  30. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/categorize/itu.py +0 -0
  31. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/categorize/lidar.py +0 -0
  32. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/categorize/melting.py +0 -0
  33. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/categorize/model.py +0 -0
  34. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/categorize/mwr.py +0 -0
  35. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/cli.py +0 -0
  36. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/cloudnetarray.py +0 -0
  37. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/concat_lib.py +0 -0
  38. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/constants.py +0 -0
  39. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/datasource.py +0 -0
  40. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/exceptions.py +0 -0
  41. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/instruments/__init__.py +0 -0
  42. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/instruments/basta.py +0 -0
  43. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/instruments/ceilo.py +0 -0
  44. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/instruments/ceilometer.py +0 -0
  45. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/instruments/cl61d.py +0 -0
  46. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/instruments/cloudnet_instrument.py +0 -0
  47. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/instruments/copernicus.py +0 -0
  48. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/instruments/da10.py +0 -0
  49. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/instruments/disdrometer/__init__.py +0 -0
  50. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/instruments/disdrometer/common.py +0 -0
  51. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/instruments/disdrometer/parsivel.py +0 -0
  52. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/instruments/disdrometer/thies.py +0 -0
  53. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/instruments/fd12p.py +0 -0
  54. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/instruments/galileo.py +0 -0
  55. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/instruments/hatpro.py +0 -0
  56. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/instruments/instruments.py +0 -0
  57. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/instruments/lufft.py +0 -0
  58. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/instruments/mrr.py +0 -0
  59. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/instruments/nc_lidar.py +0 -0
  60. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/instruments/nc_radar.py +0 -0
  61. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/instruments/pollyxt.py +0 -0
  62. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/instruments/radiometrics.py +0 -0
  63. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/instruments/rain_e_h3.py +0 -0
  64. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/instruments/rpg_reader.py +0 -0
  65. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/instruments/toa5.py +0 -0
  66. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/instruments/vaisala.py +0 -0
  67. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/instruments/weather_station.py +0 -0
  68. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/metadata.py +0 -0
  69. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/model_evaluation/__init__.py +0 -0
  70. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/model_evaluation/file_handler.py +0 -0
  71. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/model_evaluation/metadata.py +0 -0
  72. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/model_evaluation/model_metadata.py +0 -0
  73. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/model_evaluation/plotting/__init__.py +0 -0
  74. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/model_evaluation/plotting/plot_meta.py +0 -0
  75. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/model_evaluation/plotting/plot_tools.py +0 -0
  76. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/model_evaluation/plotting/plotting.py +0 -0
  77. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/model_evaluation/products/__init__.py +0 -0
  78. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/model_evaluation/products/advance_methods.py +0 -0
  79. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/model_evaluation/products/grid_methods.py +0 -0
  80. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/model_evaluation/products/model_products.py +0 -0
  81. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/model_evaluation/products/observation_products.py +0 -0
  82. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/model_evaluation/products/product_resampling.py +0 -0
  83. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/model_evaluation/products/tools.py +0 -0
  84. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/model_evaluation/statistics/__init__.py +0 -0
  85. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/model_evaluation/statistics/statistical_methods.py +0 -0
  86. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/model_evaluation/tests/__init__.py +0 -0
  87. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/model_evaluation/tests/e2e/__init__.py +0 -0
  88. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/model_evaluation/tests/e2e/conftest.py +0 -0
  89. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/model_evaluation/tests/e2e/process_cf/__init__.py +0 -0
  90. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/model_evaluation/tests/e2e/process_cf/main.py +0 -0
  91. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/model_evaluation/tests/e2e/process_cf/tests.py +0 -0
  92. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/model_evaluation/tests/e2e/process_iwc/__init__.py +0 -0
  93. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/model_evaluation/tests/e2e/process_iwc/main.py +0 -0
  94. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/model_evaluation/tests/e2e/process_iwc/tests.py +0 -0
  95. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/model_evaluation/tests/e2e/process_lwc/__init__.py +0 -0
  96. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/model_evaluation/tests/e2e/process_lwc/main.py +0 -0
  97. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/model_evaluation/tests/e2e/process_lwc/tests.py +0 -0
  98. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/model_evaluation/tests/unit/__init__.py +0 -0
  99. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/model_evaluation/tests/unit/conftest.py +0 -0
  100. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/model_evaluation/tests/unit/test_advance_methods.py +0 -0
  101. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/model_evaluation/tests/unit/test_grid_methods.py +0 -0
  102. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/model_evaluation/tests/unit/test_model_products.py +0 -0
  103. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/model_evaluation/tests/unit/test_observation_products.py +0 -0
  104. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/model_evaluation/tests/unit/test_plot_tools.py +0 -0
  105. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/model_evaluation/tests/unit/test_plotting.py +0 -0
  106. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/model_evaluation/tests/unit/test_statistical_methods.py +0 -0
  107. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/model_evaluation/tests/unit/test_tools.py +0 -0
  108. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/model_evaluation/utils.py +0 -0
  109. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/plotting/__init__.py +0 -0
  110. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/plotting/plot_meta.py +0 -0
  111. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/plotting/plotting.py +0 -0
  112. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/products/__init__.py +0 -0
  113. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/products/classification.py +0 -0
  114. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/products/der.py +0 -0
  115. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/products/drizzle.py +0 -0
  116. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/products/drizzle_error.py +0 -0
  117. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/products/drizzle_tools.py +0 -0
  118. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/products/epsilon.py +0 -0
  119. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/products/ier.py +0 -0
  120. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/products/iwc.py +0 -0
  121. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/products/lwc.py +0 -0
  122. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/products/mie_lu_tables.nc +0 -0
  123. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/products/mwr_tools.py +0 -0
  124. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/products/product_tools.py +0 -0
  125. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy/py.typed +0 -0
  126. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy.egg-info/SOURCES.txt +0 -0
  127. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy.egg-info/dependency_links.txt +0 -0
  128. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy.egg-info/entry_points.txt +0 -0
  129. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy.egg-info/requires.txt +0 -0
  130. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/cloudnetpy.egg-info/top_level.txt +0 -0
  131. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/docs/source/conf.py +0 -0
  132. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/pyproject.toml +0 -0
  133. {cloudnetpy-1.86.0 → cloudnetpy-1.86.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudnetpy
3
- Version: 1.86.0
3
+ Version: 1.86.2
4
4
  Summary: Python package for Cloudnet processing
5
5
  Author: Simo Tukiainen
6
6
  License: MIT License
@@ -401,7 +401,6 @@ class Radar(DataSource):
401
401
 
402
402
  if fvel.shape == (n_time, n_height):
403
403
  # Folding velocity is already expanded in radar file
404
- # Not yet in current files
405
404
  return fvel
406
405
  if utils.isscalar(fvel):
407
406
  # e.g. MIRA
@@ -127,4 +127,9 @@ ATTRIBUTES = RPG_ATTRIBUTES | {
127
127
  ),
128
128
  dimensions=("time", "range"),
129
129
  ),
130
+ "nyquist_velocity": MetaData(
131
+ long_name="Nyquist velocity",
132
+ units="m s-1",
133
+ dimensions=("time", "chirp_sequence"),
134
+ ),
130
135
  }
@@ -3,6 +3,7 @@
3
3
  import datetime
4
4
  import logging
5
5
  import os
6
+ import re
6
7
  from collections import OrderedDict
7
8
  from collections.abc import Sequence
8
9
  from os import PathLike
@@ -224,7 +225,7 @@ def _parse_input_files(
224
225
  )
225
226
  raise FileNotFoundError(msg)
226
227
 
227
- filetypes = list({f.split(".")[-1].lower() for f in valid_files})
228
+ filetypes = list({_get_suffix(f) for f in valid_files})
228
229
 
229
230
  if len(filetypes) > 1:
230
231
  err_msg = "Mixed mmclx and znc files. Please use only one filetype."
@@ -241,7 +242,7 @@ def _parse_input_files(
241
242
  )
242
243
  else:
243
244
  input_filename = input_files
244
- keymap = _get_keymap(str(input_filename).split(".")[-1])
245
+ keymap = _get_keymap(_get_suffix(input_filename))
245
246
 
246
247
  return input_filename, keymap
247
248
 
@@ -258,6 +259,13 @@ def _get_ignored_variables(filetype: str) -> list | None:
258
259
  return keymaps.get(filetype.lower(), keymaps.get("mmclx"))
259
260
 
260
261
 
262
+ def _get_suffix(filename: str | PathLike) -> str:
263
+ m = re.search(r"\.(\w+)(\.\d+)?$", str(filename))
264
+ if m is None:
265
+ return ""
266
+ return m[1].lower()
267
+
268
+
261
269
  def _get_keymap(filetype: str) -> dict[str, str]:
262
270
  """Returns a dictionary mapping the variables in the raw data to the processed
263
271
  Cloudnet file.
@@ -284,6 +292,7 @@ def _get_keymap(filetype: str) -> dict[str, str]:
284
292
  ("nave", "nave"),
285
293
  ("prf", "prf"),
286
294
  ("rg0", "rg0"),
295
+ ("tpow", "tpow"),
287
296
  ],
288
297
  ),
289
298
  "mmclx": OrderedDict(
@@ -1,9 +1,7 @@
1
- """This module contains RPG Cloud Radar related functions."""
2
-
3
1
  import datetime
4
2
  import logging
5
3
  import math
6
- from collections.abc import Callable, Sequence
4
+ from collections.abc import Sequence
7
5
  from os import PathLike
8
6
  from uuid import UUID
9
7
 
@@ -15,7 +13,7 @@ from rpgpy import RPGFileError
15
13
  from cloudnetpy import output, utils
16
14
  from cloudnetpy.cloudnetarray import CloudnetArray
17
15
  from cloudnetpy.constants import G_TO_KG, HPA_TO_PA, KM_H_TO_M_S, MM_H_TO_M_S
18
- from cloudnetpy.exceptions import InconsistentDataError, ValidTimeStampError
16
+ from cloudnetpy.exceptions import ValidTimeStampError
19
17
  from cloudnetpy.instruments import instruments
20
18
  from cloudnetpy.instruments.cloudnet_instrument import CloudnetInstrument
21
19
  from cloudnetpy.instruments.instruments import Instrument
@@ -68,9 +66,8 @@ def rpg2nc(
68
66
  l1_files = utils.get_sorted_filenames(path_to_l1_files, ".LV1")
69
67
  fmcw94_objects, valid_files = _get_fmcw94_objects(l1_files, date)
70
68
  one_day_of_data = create_one_day_data_record(fmcw94_objects)
71
- if not valid_files:
72
- return uuid, []
73
- print_info(one_day_of_data)
69
+ one_day_of_data["nyquist_velocity"] = _expand_nyquist(one_day_of_data)
70
+ _print_info(one_day_of_data)
74
71
  fmcw = Fmcw(one_day_of_data, site_meta)
75
72
  fmcw.convert_time_to_fraction_hour()
76
73
  fmcw.mask_invalid_ldr()
@@ -93,7 +90,7 @@ def rpg2nc(
93
90
  return uuid, valid_files
94
91
 
95
92
 
96
- def print_info(data: dict) -> None:
93
+ def _print_info(data: dict) -> None:
97
94
  dual_pol = data["dual_polarization"]
98
95
  if dual_pol == 0:
99
96
  mode = "single polarisation"
@@ -108,57 +105,66 @@ RpgObjects = Sequence[Fmcw94Bin] | Sequence[HatproBinCombined]
108
105
 
109
106
 
110
107
  def create_one_day_data_record(rpg_objects: RpgObjects) -> dict:
111
- """Concatenates all RPG data from one day."""
108
+ """Concatenates all RPG FMCW / HATPRO data from one day."""
112
109
  rpg_raw_data, rpg_header = _stack_rpg_data(rpg_objects)
113
- if len(rpg_objects) > 1:
114
- rpg_header = _reduce_header(rpg_header)
110
+ if "range" in rpg_header:
111
+ rpg_header["range"] = rpg_objects[0].header["range"]
112
+ should_be_constant = [
113
+ "model_number",
114
+ "dual_polarization",
115
+ "antenna_separation",
116
+ "antenna_diameter",
117
+ "antenna_gain",
118
+ "half_power_beam_width",
119
+ "radar_frequency",
120
+ ]
121
+ for key in should_be_constant:
122
+ if key not in rpg_header:
123
+ continue
124
+ unique_values = np.unique(rpg_header[key])
125
+ if len(unique_values) > 1:
126
+ msg = f"More than one value for {key} found: {unique_values}"
127
+ raise ValueError(msg)
128
+ rpg_header[key] = unique_values[0]
129
+
115
130
  rpg_raw_data = _mask_invalid_data(rpg_raw_data)
116
131
  return {**rpg_header, **rpg_raw_data}
117
132
 
118
133
 
119
- def _stack_rpg_data(rpg_objects: RpgObjects) -> tuple[dict, dict]:
120
- """Combines data from hourly RPG objects.
121
-
122
- Notes:
123
- Ignores variable names starting with an underscore.
134
+ def _expand_nyquist(data: dict) -> npt.NDArray:
135
+ """Expands Nyquist velocity from time X chirp => time X range."""
136
+ nyquist_velocity = ma.array(data["nyquist_velocity"])
137
+ chirp_start_indices = ma.array(data["chirp_start_indices"])
138
+ n_time = chirp_start_indices.shape[0]
139
+ n_range = len(data["range"])
140
+ expanded_nyquist = np.empty((n_time, n_range))
141
+ for t in range(n_time):
142
+ starts = chirp_start_indices[t].compressed()
143
+ v_nyq = nyquist_velocity[t].compressed()
144
+ ends = np.r_[starts[1:], n_range]
145
+ seg_lengths = ends - starts
146
+ expanded_nyquist[t, :] = np.repeat(v_nyq, seg_lengths)
147
+ return expanded_nyquist
124
148
 
125
- """
126
-
127
- def _stack(source: dict, target: dict, fun: Callable) -> None:
128
- for name, value in source.items():
129
- if not name.startswith("_"):
130
- target[name] = fun((target[name], value)) if name in target else value
131
149
 
150
+ def _stack_rpg_data(rpg_objects: RpgObjects) -> tuple[dict, dict]:
132
151
  data: dict = {}
133
152
  header: dict = {}
134
153
  for rpg in rpg_objects:
135
- _stack(rpg.data, data, ma.concatenate)
136
- _stack(rpg.header, header, ma.vstack)
154
+ for src, dst in ((rpg.data, data), (rpg.header, header)):
155
+ for name, value in src.items():
156
+ if name.startswith("_"):
157
+ continue
158
+ arr = dst.get(name)
159
+ fun = (
160
+ ma.concatenate
161
+ if any(isinstance(x, ma.MaskedArray) for x in (value, arr))
162
+ else np.concatenate
163
+ )
164
+ dst[name] = fun((arr, value)) if arr is not None else value
137
165
  return data, header
138
166
 
139
167
 
140
- def _reduce_header(header: dict) -> dict:
141
- """Removes duplicate header data. Otherwise, we would need n_files dimension."""
142
- reduced_header = {}
143
- for key, data in header.items():
144
- # Handle outliers in latitude and longitude (e.g. Galati 2024-02-11):
145
- if key in ("latitude", "longitude"):
146
- reduced_header[key] = ma.median(data)
147
- continue
148
- first_profile_value = data[0]
149
- is_identical_value = bool(
150
- np.isclose(data, first_profile_value, rtol=1e-2).all(),
151
- )
152
- if is_identical_value is False:
153
- msg = f"Inconsistent header: {key}: {data}"
154
- if key in ("sample_duration", "calibration_interval", "noise_threshold"):
155
- logging.warning(msg)
156
- else:
157
- raise InconsistentDataError(msg)
158
- reduced_header[key] = first_profile_value
159
- return reduced_header
160
-
161
-
162
168
  def _mask_invalid_data(data_in: dict) -> dict:
163
169
  """Masks zeros and other fill values from data."""
164
170
  data = data_in.copy()
@@ -193,33 +199,68 @@ def _get_fmcw94_objects(
193
199
  continue
194
200
  objects.append(obj)
195
201
  valid_files.append(file)
196
- if objects:
197
- objects, valid_files = _remove_files_with_bad_height(objects, valid_files)
198
- if not valid_files:
199
- raise ValidTimeStampError
202
+ if not objects:
203
+ msg = "No valid files found"
204
+ raise ValidTimeStampError(msg)
205
+ objects = _interpolate_to_common_height(objects)
206
+ objects = _pad_chirp_related_fields(objects)
207
+ objects = _expand_time_related_fields(objects)
200
208
  return objects, valid_files
201
209
 
202
210
 
203
- def _remove_files_with_bad_height(objects: list, files: list) -> tuple[list, list]:
204
- lengths = [obj.data["Zh"].shape[1] for obj in objects]
205
- most_common = np.bincount(lengths).argmax()
206
- files = [
207
- file
208
- for file, obj, length in zip(files, objects, lengths, strict=True)
209
- if length == most_common
210
- ]
211
- objects = [
212
- obj
213
- for obj, length in zip(objects, lengths, strict=True)
214
- if length == most_common
215
- ]
216
- n_removed = len(lengths) - len(files)
217
- if n_removed > 0:
218
- logging.warning(
219
- "Removed %s RPG-FMCW-94 files due to inconsistent height vector",
220
- n_removed,
221
- )
222
- return objects, files
211
+ def _interpolate_to_common_height(objects: list[Fmcw94Bin]) -> list[Fmcw94Bin]:
212
+ range_arrays = [obj.header["range"] for obj in objects]
213
+ if all(np.array_equal(range_arrays[0], r) for r in range_arrays[1:]):
214
+ return objects
215
+ # Use range with the highest range gate for interpolation
216
+ target_height = max(range_arrays, key=lambda r: r[-1])
217
+ for obj in objects:
218
+ src_range = obj.header["range"]
219
+ if np.array_equal(src_range, target_height):
220
+ continue
221
+ for key, arr in obj.data.items():
222
+ if arr.ndim == 2 and arr.shape[1] == src_range.size:
223
+ obj.data[key] = utils.interpolate_2D_along_y(
224
+ src_range, arr, target_height
225
+ )
226
+ obj.header["range"] = target_height
227
+ return objects
228
+
229
+
230
+ def _pad_chirp_related_fields(objects: list[Fmcw94Bin]) -> list[Fmcw94Bin]:
231
+ """Pads chirp-related header fields with masked values to have the same length."""
232
+ chirp_lens = [len(obj.header["chirp_start_indices"]) for obj in objects]
233
+ if all(chirp_lens[0] == length for length in chirp_lens[1:]):
234
+ return objects
235
+ max_chirp_len = max(chirp_lens)
236
+ for obj in objects:
237
+ n_chirps = len(obj.header["chirp_start_indices"])
238
+ if n_chirps == max_chirp_len:
239
+ continue
240
+ for key, arr in obj.header.items():
241
+ if not isinstance(arr, str) and arr.ndim == 1 and arr.size == n_chirps:
242
+ pad_len = max_chirp_len - n_chirps
243
+ masked_arr = ma.array(arr, dtype=arr.dtype)
244
+ pad = ma.masked_all(pad_len, dtype=arr.dtype)
245
+ obj.header[key] = ma.concatenate([masked_arr, pad])
246
+ return objects
247
+
248
+
249
+ def _expand_time_related_fields(objects: list[Fmcw94Bin]) -> list[Fmcw94Bin]:
250
+ for obj in objects:
251
+ n_time = obj.data["time"].size
252
+ for key in obj.header:
253
+ if key in ("range", "time") or key.startswith("_"):
254
+ continue
255
+ arr = obj.header[key]
256
+ # Handle outliers in latitude and longitude (e.g. Galati 2024-02-11):
257
+ if key in ("latitude", "longitude"):
258
+ arr = ma.median(arr)
259
+ if utils.isscalar(arr):
260
+ obj.header[key] = np.repeat(arr, n_time)
261
+ else:
262
+ obj.header[key] = np.tile(arr, (n_time, 1))
263
+ return objects
223
264
 
224
265
 
225
266
  def _validate_date(obj: Fmcw94Bin, expected_date: datetime.date) -> None:
@@ -443,9 +484,11 @@ RPG_ATTRIBUTES = {
443
484
  long_name="File code",
444
485
  units="1",
445
486
  comment="Indicates the RPG software version.",
446
- dimensions=None,
487
+ dimensions=("time",),
488
+ ),
489
+ "program_number": MetaData(
490
+ long_name="Program number", units="1", dimensions=("time",)
447
491
  ),
448
- "program_number": MetaData(long_name="Program number", units="1", dimensions=None),
449
492
  "model_number": MetaData(
450
493
  long_name="Model number",
451
494
  units="1",
@@ -469,54 +512,51 @@ RPG_ATTRIBUTES = {
469
512
  dimensions=None,
470
513
  ),
471
514
  "sample_duration": MetaData(
472
- long_name="Sample duration", units="s", dimensions=None
515
+ long_name="Sample duration", units="s", dimensions=("time",)
473
516
  ),
474
517
  "calibration_interval": MetaData(
475
- long_name="Calibration interval in samples", units="1", dimensions=None
518
+ long_name="Calibration interval in samples", units="1", dimensions=("time",)
476
519
  ),
477
520
  "number_of_spectral_samples": MetaData(
478
521
  long_name="Number of spectral samples in each chirp sequence",
479
522
  units="1",
480
- dimensions=("chirp_sequence",),
481
- ),
482
- "nyquist_velocity": MetaData(
483
- long_name="Nyquist velocity", units="m s-1", dimensions=("chirp_sequence",)
523
+ dimensions=("time", "chirp_sequence"),
484
524
  ),
485
525
  "number_of_averaged_chirps": MetaData(
486
526
  long_name="Number of averaged chirps in sequence",
487
527
  units="1",
488
- dimensions=("chirp_sequence",),
528
+ dimensions=("time", "chirp_sequence"),
489
529
  ),
490
530
  "chirp_start_indices": MetaData(
491
531
  long_name="Chirp sequences start indices",
492
532
  units="1",
493
- dimensions=("chirp_sequence",),
533
+ dimensions=("time", "chirp_sequence"),
494
534
  ),
495
535
  "integration_time": MetaData(
496
536
  long_name="Integration time",
497
537
  units="s",
498
538
  comment="Effective integration time of chirp sequence",
499
- dimensions=("chirp_sequence",),
539
+ dimensions=("time", "chirp_sequence"),
500
540
  ),
501
541
  "range_resolution": MetaData(
502
542
  long_name="Vertical resolution of range",
503
543
  units="m",
504
- dimensions=("chirp_sequence",),
544
+ dimensions=("time", "chirp_sequence"),
505
545
  ),
506
546
  "FFT_window": MetaData(
507
547
  long_name="FFT window type",
508
548
  units="1",
509
549
  definition=DEFINITIONS["FFT_window"],
510
- dimensions=None,
550
+ dimensions=("time",),
511
551
  ),
512
552
  "input_voltage_range": MetaData(
513
- long_name="ADC input voltage range (+/-)", units="mV", dimensions=None
553
+ long_name="ADC input voltage range (+/-)", units="mV", dimensions=("time",)
514
554
  ),
515
555
  "noise_threshold": MetaData(
516
556
  long_name="Noise filter threshold factor",
517
557
  units="1",
518
558
  comment="Multiple of the standard deviation of Doppler spectra.",
519
- dimensions=None,
559
+ dimensions=("time",),
520
560
  ),
521
561
  "time_ms": MetaData(long_name="Time ms", units="ms", dimensions=("time",)),
522
562
  "quality_flag": MetaData(
@@ -545,7 +585,4 @@ RPG_ATTRIBUTES = {
545
585
  "pc_temperature": MetaData(
546
586
  long_name="PC temperature", units="K", dimensions=("time",)
547
587
  ),
548
- "correlation_coefficient": MetaData(
549
- long_name="Correlation coefficient", units="1", dimensions=None
550
- ),
551
588
  }
@@ -391,7 +391,6 @@ def _write_vars2nc(nc: netCDF4.Dataset, cloudnet_variables: dict) -> None:
391
391
  else:
392
392
  fill_value = False
393
393
  size = obj.dimensions if obj.dimensions is not None else ()
394
-
395
394
  nc_variable = nc.createVariable(
396
395
  obj.name,
397
396
  obj.data_type,
@@ -399,7 +398,11 @@ def _write_vars2nc(nc: netCDF4.Dataset, cloudnet_variables: dict) -> None:
399
398
  zlib=True,
400
399
  fill_value=fill_value,
401
400
  )
402
- nc_variable[:] = obj.data
401
+ try:
402
+ nc_variable[:] = obj.data
403
+ except IndexError as err:
404
+ msg = f"Unable to write variable {obj.name} to file: {err}"
405
+ raise IndexError(msg) from err
403
406
  for attr in obj.fetch_attributes():
404
407
  setattr(nc_variable, attr, getattr(obj, attr))
405
408
 
@@ -387,7 +387,7 @@ def interpolate_2d_mask(
387
387
  def interpolate_2d_nearest(
388
388
  x: npt.NDArray,
389
389
  y: npt.NDArray,
390
- z: npt.NDArray,
390
+ z: ma.MaskedArray,
391
391
  x_new: npt.NDArray,
392
392
  y_new: npt.NDArray,
393
393
  ) -> ma.MaskedArray:
@@ -419,18 +419,53 @@ def interpolate_2d_nearest(
419
419
  return fun((xx, yy)).T
420
420
 
421
421
 
422
+ def interpolate_2D_along_y(
423
+ y: npt.NDArray,
424
+ z: npt.NDArray | ma.MaskedArray,
425
+ y_new: npt.NDArray,
426
+ ) -> ma.MaskedArray:
427
+ """Fast 1D nearest-neighbor interpolation along y for each x.
428
+
429
+ Args:
430
+ y: 1D numpy array of y-coordinates (length M).
431
+ z: 2D array of shape (N, M).
432
+ y_new: 1D numpy array of new y-coordinates.
433
+
434
+ Returns:
435
+ Masked 2D masked array interpolated along y.
436
+
437
+ Notes:
438
+ Only interpolates along y. Points outside range are masked.
439
+ """
440
+ idx = np.searchsorted(y, y_new, side="left")
441
+ idx = np.clip(idx, 0, len(y) - 1)
442
+ left = np.maximum(idx - 1, 0)
443
+ choose_right = (idx == 0) | (
444
+ (idx < len(y)) & (np.abs(y[idx] - y_new) < np.abs(y_new - y[left]))
445
+ )
446
+ idx[~choose_right] = left[~choose_right]
447
+ z_interp = ma.array(z[:, idx])
448
+ mask = (y_new < y.min()) | (y_new > y.max())
449
+ if z_interp.mask is ma.nomask:
450
+ z_mask = np.zeros(z_interp.shape, dtype=bool)
451
+ else:
452
+ z_mask = z_interp.mask.copy()
453
+ z_mask[:, mask] = True
454
+ return ma.MaskedArray(z_interp, mask=z_mask)
455
+
456
+
422
457
  def interpolate_1d(
423
458
  time: npt.NDArray,
424
459
  y: ma.MaskedArray,
425
460
  time_new: npt.NDArray,
426
461
  max_time: float,
427
462
  method: str = "linear",
428
- ) -> npt.NDArray:
463
+ ) -> ma.MaskedArray:
429
464
  """1D linear interpolation preserving the mask.
430
465
 
431
466
  Args:
432
467
  time: 1D array in fraction hour.
433
- y: 1D masked array, data values.
468
+ y: 1D array, data values.
434
469
  time_new: 1D array, new time coordinates.
435
470
  max_time: Maximum allowed gap in minutes. Values outside this gap will
436
471
  be masked.
@@ -1,4 +1,4 @@
1
1
  MAJOR = 1
2
2
  MINOR = 86
3
- PATCH = 0
3
+ PATCH = 2
4
4
  __version__ = f"{MAJOR}.{MINOR}.{PATCH}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudnetpy
3
- Version: 1.86.0
3
+ Version: 1.86.2
4
4
  Summary: Python package for Cloudnet processing
5
5
  Author: Simo Tukiainen
6
6
  License: MIT License
File without changes
File without changes
File without changes
File without changes
File without changes