cloudnetpy 1.49.9__py3-none-any.whl → 1.87.3__py3-none-any.whl

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 (116) hide show
  1. cloudnetpy/categorize/__init__.py +1 -2
  2. cloudnetpy/categorize/atmos_utils.py +297 -67
  3. cloudnetpy/categorize/attenuation.py +31 -0
  4. cloudnetpy/categorize/attenuations/__init__.py +37 -0
  5. cloudnetpy/categorize/attenuations/gas_attenuation.py +30 -0
  6. cloudnetpy/categorize/attenuations/liquid_attenuation.py +84 -0
  7. cloudnetpy/categorize/attenuations/melting_attenuation.py +78 -0
  8. cloudnetpy/categorize/attenuations/rain_attenuation.py +84 -0
  9. cloudnetpy/categorize/categorize.py +332 -156
  10. cloudnetpy/categorize/classify.py +127 -125
  11. cloudnetpy/categorize/containers.py +107 -76
  12. cloudnetpy/categorize/disdrometer.py +40 -0
  13. cloudnetpy/categorize/droplet.py +23 -21
  14. cloudnetpy/categorize/falling.py +53 -24
  15. cloudnetpy/categorize/freezing.py +25 -12
  16. cloudnetpy/categorize/insects.py +35 -23
  17. cloudnetpy/categorize/itu.py +243 -0
  18. cloudnetpy/categorize/lidar.py +36 -41
  19. cloudnetpy/categorize/melting.py +34 -26
  20. cloudnetpy/categorize/model.py +84 -37
  21. cloudnetpy/categorize/mwr.py +18 -14
  22. cloudnetpy/categorize/radar.py +215 -102
  23. cloudnetpy/cli.py +578 -0
  24. cloudnetpy/cloudnetarray.py +43 -89
  25. cloudnetpy/concat_lib.py +218 -78
  26. cloudnetpy/constants.py +28 -10
  27. cloudnetpy/datasource.py +61 -86
  28. cloudnetpy/exceptions.py +49 -20
  29. cloudnetpy/instruments/__init__.py +5 -0
  30. cloudnetpy/instruments/basta.py +29 -12
  31. cloudnetpy/instruments/bowtie.py +135 -0
  32. cloudnetpy/instruments/ceilo.py +138 -115
  33. cloudnetpy/instruments/ceilometer.py +164 -80
  34. cloudnetpy/instruments/cl61d.py +21 -5
  35. cloudnetpy/instruments/cloudnet_instrument.py +74 -36
  36. cloudnetpy/instruments/copernicus.py +108 -30
  37. cloudnetpy/instruments/da10.py +54 -0
  38. cloudnetpy/instruments/disdrometer/common.py +126 -223
  39. cloudnetpy/instruments/disdrometer/parsivel.py +453 -94
  40. cloudnetpy/instruments/disdrometer/thies.py +254 -87
  41. cloudnetpy/instruments/fd12p.py +201 -0
  42. cloudnetpy/instruments/galileo.py +65 -23
  43. cloudnetpy/instruments/hatpro.py +123 -49
  44. cloudnetpy/instruments/instruments.py +113 -1
  45. cloudnetpy/instruments/lufft.py +39 -17
  46. cloudnetpy/instruments/mira.py +268 -61
  47. cloudnetpy/instruments/mrr.py +187 -0
  48. cloudnetpy/instruments/nc_lidar.py +19 -8
  49. cloudnetpy/instruments/nc_radar.py +109 -55
  50. cloudnetpy/instruments/pollyxt.py +135 -51
  51. cloudnetpy/instruments/radiometrics.py +313 -59
  52. cloudnetpy/instruments/rain_e_h3.py +171 -0
  53. cloudnetpy/instruments/rpg.py +321 -189
  54. cloudnetpy/instruments/rpg_reader.py +74 -40
  55. cloudnetpy/instruments/toa5.py +49 -0
  56. cloudnetpy/instruments/vaisala.py +95 -343
  57. cloudnetpy/instruments/weather_station.py +774 -105
  58. cloudnetpy/metadata.py +90 -19
  59. cloudnetpy/model_evaluation/file_handler.py +55 -52
  60. cloudnetpy/model_evaluation/metadata.py +46 -20
  61. cloudnetpy/model_evaluation/model_metadata.py +1 -1
  62. cloudnetpy/model_evaluation/plotting/plot_tools.py +32 -37
  63. cloudnetpy/model_evaluation/plotting/plotting.py +327 -117
  64. cloudnetpy/model_evaluation/products/advance_methods.py +92 -83
  65. cloudnetpy/model_evaluation/products/grid_methods.py +88 -63
  66. cloudnetpy/model_evaluation/products/model_products.py +43 -35
  67. cloudnetpy/model_evaluation/products/observation_products.py +41 -35
  68. cloudnetpy/model_evaluation/products/product_resampling.py +17 -7
  69. cloudnetpy/model_evaluation/products/tools.py +29 -20
  70. cloudnetpy/model_evaluation/statistics/statistical_methods.py +30 -20
  71. cloudnetpy/model_evaluation/tests/e2e/conftest.py +3 -3
  72. cloudnetpy/model_evaluation/tests/e2e/process_cf/main.py +9 -5
  73. cloudnetpy/model_evaluation/tests/e2e/process_cf/tests.py +15 -14
  74. cloudnetpy/model_evaluation/tests/e2e/process_iwc/main.py +9 -5
  75. cloudnetpy/model_evaluation/tests/e2e/process_iwc/tests.py +15 -14
  76. cloudnetpy/model_evaluation/tests/e2e/process_lwc/main.py +9 -5
  77. cloudnetpy/model_evaluation/tests/e2e/process_lwc/tests.py +15 -14
  78. cloudnetpy/model_evaluation/tests/unit/conftest.py +42 -41
  79. cloudnetpy/model_evaluation/tests/unit/test_advance_methods.py +41 -48
  80. cloudnetpy/model_evaluation/tests/unit/test_grid_methods.py +216 -194
  81. cloudnetpy/model_evaluation/tests/unit/test_model_products.py +23 -21
  82. cloudnetpy/model_evaluation/tests/unit/test_observation_products.py +37 -38
  83. cloudnetpy/model_evaluation/tests/unit/test_plot_tools.py +43 -40
  84. cloudnetpy/model_evaluation/tests/unit/test_plotting.py +30 -36
  85. cloudnetpy/model_evaluation/tests/unit/test_statistical_methods.py +68 -31
  86. cloudnetpy/model_evaluation/tests/unit/test_tools.py +33 -26
  87. cloudnetpy/model_evaluation/utils.py +2 -1
  88. cloudnetpy/output.py +170 -111
  89. cloudnetpy/plotting/__init__.py +2 -1
  90. cloudnetpy/plotting/plot_meta.py +562 -822
  91. cloudnetpy/plotting/plotting.py +1142 -704
  92. cloudnetpy/products/__init__.py +1 -0
  93. cloudnetpy/products/classification.py +370 -88
  94. cloudnetpy/products/der.py +85 -55
  95. cloudnetpy/products/drizzle.py +77 -34
  96. cloudnetpy/products/drizzle_error.py +15 -11
  97. cloudnetpy/products/drizzle_tools.py +79 -59
  98. cloudnetpy/products/epsilon.py +211 -0
  99. cloudnetpy/products/ier.py +27 -50
  100. cloudnetpy/products/iwc.py +55 -48
  101. cloudnetpy/products/lwc.py +96 -70
  102. cloudnetpy/products/mwr_tools.py +186 -0
  103. cloudnetpy/products/product_tools.py +170 -128
  104. cloudnetpy/utils.py +455 -240
  105. cloudnetpy/version.py +2 -2
  106. {cloudnetpy-1.49.9.dist-info → cloudnetpy-1.87.3.dist-info}/METADATA +44 -40
  107. cloudnetpy-1.87.3.dist-info/RECORD +127 -0
  108. {cloudnetpy-1.49.9.dist-info → cloudnetpy-1.87.3.dist-info}/WHEEL +1 -1
  109. cloudnetpy-1.87.3.dist-info/entry_points.txt +2 -0
  110. docs/source/conf.py +2 -2
  111. cloudnetpy/categorize/atmos.py +0 -361
  112. cloudnetpy/products/mwr_multi.py +0 -68
  113. cloudnetpy/products/mwr_single.py +0 -75
  114. cloudnetpy-1.49.9.dist-info/RECORD +0 -112
  115. {cloudnetpy-1.49.9.dist-info → cloudnetpy-1.87.3.dist-info/licenses}/LICENSE +0 -0
  116. {cloudnetpy-1.49.9.dist-info → cloudnetpy-1.87.3.dist-info}/top_level.txt +0 -0
cloudnetpy/datasource.py CHANGED
@@ -1,11 +1,16 @@
1
- """Datasource module, containing the :class:`DataSource class.`"""
1
+ """Datasource module, containing the :class:`DataSource` class."""
2
+
3
+ import datetime
2
4
  import logging
3
5
  import os
4
- from datetime import datetime
5
- from typing import Callable
6
+ from collections.abc import Callable
7
+ from os import PathLike
8
+ from types import TracebackType
6
9
 
7
10
  import netCDF4
8
11
  import numpy as np
12
+ import numpy.typing as npt
13
+ from typing_extensions import Self
9
14
 
10
15
  from cloudnetpy import utils
11
16
  from cloudnetpy.cloudnetarray import CloudnetArray
@@ -44,19 +49,25 @@ class DataSource:
44
49
  radar_frequency: float
45
50
  data_dense: dict
46
51
  data_sparse: dict
47
- type: str
52
+ source_type: str
48
53
 
49
- def __init__(self, full_path: str, radar: bool = False):
54
+ def __init__(self, full_path: PathLike | str, *, radar: bool = False) -> None:
50
55
  self.filename = os.path.basename(full_path)
51
56
  self.dataset = netCDF4.Dataset(full_path)
52
57
  self.source = getattr(self.dataset, "source", "")
53
- self.time: np.ndarray = self._init_time()
58
+ self.instrument_pid = getattr(self.dataset, "instrument_pid", "")
59
+ self.time: npt.NDArray = self._init_time()
54
60
  self.altitude = self._init_altitude()
55
61
  self.height = self._init_height()
62
+ self.height_agl = (
63
+ self.height - self.altitude
64
+ if self.height is not None and self.altitude is not None
65
+ else None
66
+ )
56
67
  self.data: dict = {}
57
68
  self._is_radar = radar
58
69
 
59
- def getvar(self, *args) -> np.ndarray:
70
+ def getvar(self, *args: str) -> npt.NDArray:
60
71
  """Returns data array from the source file variables.
61
72
 
62
73
  Returns just the data (and no attributes) from the original
@@ -69,21 +80,23 @@ class DataSource:
69
80
  ndarray: The actual data.
70
81
 
71
82
  Raises:
72
- RuntimeError: The variable is not found.
83
+ KeyError: The variable is not found.
73
84
 
74
85
  """
75
86
  for arg in args:
76
87
  if arg in self.dataset.variables:
77
88
  return self.dataset.variables[arg][:]
78
- raise RuntimeError("Missing variable in the input file.")
89
+ msg = f"Missing variable {args[0]} in the input file."
90
+ raise KeyError(msg)
79
91
 
80
92
  def append_data(
81
93
  self,
82
- variable: netCDF4.Variable | np.ndarray | float | int,
94
+ variable: netCDF4.Variable | npt.NDArray | float,
83
95
  key: str,
84
96
  name: str | None = None,
85
97
  units: str | None = None,
86
- ):
98
+ dtype: str | None = None,
99
+ ) -> None:
87
100
  """Adds new CloudnetVariable or RadarVariable into `data` attribute.
88
101
 
89
102
  Args:
@@ -92,123 +105,85 @@ class DataSource:
92
105
  attribute (dictionary).
93
106
  name: CloudnetArray.name attribute. Default value is *key*.
94
107
  units: CloudnetArray.units attribute.
108
+ dtype: CloudnetArray.data_type attribute.
95
109
 
96
110
  """
97
- self.data[key] = CloudnetArray(variable, name or key, units)
111
+ self.data[key] = CloudnetArray(variable, name or key, units, data_type=dtype)
98
112
 
99
- def get_date(self) -> list:
113
+ def get_date(self) -> datetime.date:
100
114
  """Returns date components.
101
115
 
102
116
  Returns:
103
- list: Date components [YYYY, MM, DD].
117
+ date object
104
118
 
105
119
  Raises:
106
120
  RuntimeError: Not found or invalid date.
107
121
 
108
122
  """
109
123
  try:
110
- year = str(self.dataset.year)
111
- month = str(self.dataset.month).zfill(2)
112
- day = str(self.dataset.day).zfill(2)
113
- datetime.strptime(f"{year}{month}{day}", "%Y%m%d")
124
+ year = int(self.dataset.year)
125
+ month = int(self.dataset.month)
126
+ day = int(self.dataset.day)
127
+ return datetime.date(year, month, day)
114
128
  except (AttributeError, ValueError) as read_error:
115
- raise RuntimeError(
116
- "Missing or invalid date in global attributes."
117
- ) from read_error
118
- return [year, month, day]
129
+ msg = "Missing or invalid date in global attributes."
130
+ raise RuntimeError(msg) from read_error
119
131
 
120
132
  def close(self) -> None:
121
133
  """Closes the open file."""
122
134
  self.dataset.close()
123
135
 
124
136
  @staticmethod
125
- def km2m(var: netCDF4.Variable) -> np.ndarray:
137
+ def to_m(var: netCDF4.Variable) -> npt.NDArray:
126
138
  """Converts km to m."""
127
139
  alt = var[:]
128
140
  if var.units == "km":
129
141
  alt *= 1000
142
+ elif var.units not in ("m", "meters"):
143
+ msg = f"Unexpected unit: {var.units}"
144
+ raise ValueError(msg)
130
145
  return alt
131
146
 
132
- @staticmethod
133
- def m2km(var: netCDF4.Variable) -> np.ndarray:
134
- """Converts m to km."""
135
- alt = var[:]
136
- if var.units == "m":
137
- alt /= 1000
138
- return alt
139
-
140
- def _init_time(self) -> np.ndarray:
147
+ def _init_time(self) -> npt.NDArray:
141
148
  time = self.getvar("time")
142
149
  if len(time) == 0:
143
- raise ValidTimeStampError("Empty time vector")
150
+ msg = "Empty time vector"
151
+ raise ValidTimeStampError(msg)
144
152
  if max(time) > 25:
145
- logging.warning("Assuming time as seconds, converting to fraction hour")
153
+ logging.debug("Assuming time as seconds, converting to fraction hour")
146
154
  time = utils.seconds2hours(time)
147
155
  return time
148
156
 
149
157
  def _init_altitude(self) -> float | None:
150
158
  """Returns altitude of the instrument (m)."""
151
159
  if "altitude" in self.dataset.variables:
152
- altitude_above_sea = self.km2m(self.dataset.variables["altitude"])
153
- return float(np.mean(altitude_above_sea))
160
+ var = self.dataset.variables["altitude"]
161
+ if utils.is_all_masked(var[:]):
162
+ return None
163
+ altitude_above_sea = self.to_m(var)
164
+ return float(
165
+ altitude_above_sea
166
+ if utils.isscalar(altitude_above_sea)
167
+ else np.mean(altitude_above_sea),
168
+ )
154
169
  return None
155
170
 
156
- def _init_height(self) -> np.ndarray | None:
171
+ def _init_height(self) -> npt.NDArray | None:
157
172
  """Returns height array above mean sea level (m)."""
158
173
  if "height" in self.dataset.variables:
159
- return self.km2m(self.dataset.variables["height"])
174
+ return self.to_m(self.dataset.variables["height"])
160
175
  if "range" in self.dataset.variables and self.altitude is not None:
161
- range_instrument = self.km2m(self.dataset.variables["range"])
176
+ range_instrument = self.to_m(self.dataset.variables["range"])
162
177
  return np.array(range_instrument + self.altitude)
163
178
  return None
164
179
 
165
- def _variables_to_cloudnet_arrays(self, keys: tuple) -> None:
166
- """Transforms netCDF4-variables into CloudnetArrays.
167
-
168
- Args:
169
- keys: netCDF4-variables to be converted. The results
170
- are saved in *self.data* dictionary with *fields*
171
- strings as keys.
172
-
173
- Notes:
174
- The attributes of the variables are not copied. Just the data.
175
-
176
- """
177
- for key in keys:
178
- self.append_data(self.dataset.variables[key], key)
179
-
180
- def _unknown_variable_to_cloudnet_array(
181
- self,
182
- possible_names: tuple,
183
- key: str,
184
- units: str | None = None,
185
- ignore_mask: bool = False,
186
- ):
187
- """Transforms single netCDF4 variable into CloudnetArray.
188
-
189
- Args:
190
- possible_names: Tuple of strings containing the possible
191
- names of the variable in the input NetCDF file.
192
- key: Key for self.data dictionary and name-attribute
193
- for the saved CloudnetArray object.
194
- units: Units attribute for the CloudnetArray object.
195
- ignore_mask: If true, always writes an ordinary numpy array.
196
-
197
- Raises:
198
- RuntimeError: No variable found.
199
-
200
- """
201
- for name in possible_names:
202
- if name in self.dataset.variables:
203
- array = self.dataset.variables[name]
204
- if ignore_mask is True:
205
- array = np.array(array)
206
- self.append_data(array, key, units=units)
207
- return
208
- raise RuntimeError("Missing variable in the input file.")
209
-
210
- def __enter__(self):
180
+ def __enter__(self) -> Self:
211
181
  return self
212
182
 
213
- def __exit__(self, exc_type, exc_val, exc_tb):
183
+ def __exit__(
184
+ self,
185
+ exc_type: type[BaseException] | None,
186
+ exc_val: BaseException | None,
187
+ exc_tb: TracebackType | None,
188
+ ) -> None:
214
189
  self.close()
cloudnetpy/exceptions.py CHANGED
@@ -1,38 +1,67 @@
1
- class InconsistentDataError(Exception):
1
+ class CloudnetException(Exception):
2
+ """Base class for exceptions in this module."""
3
+
4
+
5
+ class InconsistentDataError(CloudnetException):
6
+ """Internal exception class."""
7
+
8
+ def __init__(self, msg: str) -> None:
9
+ super().__init__(msg)
10
+
11
+
12
+ class DisdrometerDataError(CloudnetException):
13
+ """Internal exception class."""
14
+
15
+ def __init__(self, msg: str) -> None:
16
+ super().__init__(msg)
17
+
18
+
19
+ class RadarDataError(CloudnetException):
20
+ """Internal exception class."""
21
+
22
+ def __init__(self, msg: str) -> None:
23
+ super().__init__(msg)
24
+
25
+
26
+ class LidarDataError(CloudnetException):
27
+ """Internal exception class."""
28
+
29
+ def __init__(self, msg: str) -> None:
30
+ super().__init__(msg)
31
+
32
+
33
+ class PlottingError(CloudnetException):
2
34
  """Internal exception class."""
3
35
 
4
- def __init__(self, msg: str = ""):
5
- self.message = msg
6
- super().__init__(self.message)
36
+ def __init__(self, msg: str) -> None:
37
+ super().__init__(msg)
7
38
 
8
39
 
9
- class DisdrometerDataError(Exception):
40
+ class ModelDataError(CloudnetException):
10
41
  """Internal exception class."""
11
42
 
12
- def __init__(self, msg: str = ""):
13
- self.message = msg
14
- super().__init__(self.message)
43
+ def __init__(
44
+ self, msg: str = "Invalid model file: not enough proper profiles"
45
+ ) -> None:
46
+ super().__init__(msg)
15
47
 
16
48
 
17
- class WeatherStationDataError(Exception):
49
+ class ValidTimeStampError(CloudnetException):
18
50
  """Internal exception class."""
19
51
 
20
- def __init__(self, msg: str = ""):
21
- self.message = msg
22
- super().__init__(self.message)
52
+ def __init__(self, msg: str = "No valid timestamps found") -> None:
53
+ super().__init__(msg)
23
54
 
24
55
 
25
- class ModelDataError(Exception):
56
+ class HatproDataError(CloudnetException):
26
57
  """Internal exception class."""
27
58
 
28
- def __init__(self, msg: str = "Invalid model file: not enough proper profiles"):
29
- self.message = msg
30
- super().__init__(self.message)
59
+ def __init__(self, msg: str = "Invalid HATPRO file") -> None:
60
+ super().__init__(msg)
31
61
 
32
62
 
33
- class ValidTimeStampError(Exception):
63
+ class InvalidSourceFileError(CloudnetException):
34
64
  """Internal exception class."""
35
65
 
36
- def __init__(self, msg: str = "No valid timestamps found"):
37
- self.message = msg
38
- super().__init__(self.message)
66
+ def __init__(self, msg: str = "Invalid source file") -> None:
67
+ super().__init__(msg)
@@ -1,11 +1,16 @@
1
1
  from .basta import basta2nc
2
+ from .bowtie import bowtie2nc
2
3
  from .ceilo import ceilo2nc
3
4
  from .copernicus import copernicus2nc
4
5
  from .disdrometer import parsivel2nc, thies2nc
6
+ from .fd12p import fd12p2nc
5
7
  from .galileo import galileo2nc
6
8
  from .hatpro import hatpro2l1c, hatpro2nc
9
+ from .instruments import Instrument
7
10
  from .mira import mira2nc
11
+ from .mrr import mrr2nc
8
12
  from .pollyxt import pollyxt2nc
9
13
  from .radiometrics import radiometrics2nc
14
+ from .rain_e_h3 import rain_e_h32nc
10
15
  from .rpg import rpg2nc
11
16
  from .weather_station import ws2nc
@@ -1,4 +1,9 @@
1
1
  """Module for reading / converting BASTA radar data."""
2
+
3
+ import datetime
4
+ from os import PathLike
5
+ from uuid import UUID
6
+
2
7
  import numpy as np
3
8
 
4
9
  from cloudnetpy import output
@@ -6,15 +11,16 @@ from cloudnetpy.exceptions import ValidTimeStampError
6
11
  from cloudnetpy.instruments import instruments
7
12
  from cloudnetpy.instruments.nc_radar import NcRadar
8
13
  from cloudnetpy.metadata import MetaData
14
+ from cloudnetpy.utils import get_uuid
9
15
 
10
16
 
11
17
  def basta2nc(
12
- basta_file: str,
13
- output_file: str,
18
+ basta_file: str | PathLike,
19
+ output_file: str | PathLike,
14
20
  site_meta: dict,
15
- uuid: str | None = None,
16
- date: str | None = None,
17
- ) -> str:
21
+ uuid: str | UUID | None = None,
22
+ date: str | datetime.date | None = None,
23
+ ) -> UUID:
18
24
  """Converts BASTA cloud radar data into Cloudnet Level 1b netCDF file.
19
25
 
20
26
  This function converts daily BASTA file into a much smaller file that
@@ -41,6 +47,10 @@ def basta2nc(
41
47
  >>> basta2nc('basta_file.nc', 'radar.nc', site_meta)
42
48
 
43
49
  """
50
+ if isinstance(date, str):
51
+ date = datetime.date.fromisoformat(date)
52
+ uuid = get_uuid(uuid)
53
+
44
54
  keymap = {
45
55
  "reflectivity": "Zh",
46
56
  "velocity": "v",
@@ -54,15 +64,17 @@ def basta2nc(
54
64
  if date is not None:
55
65
  basta.validate_date(date)
56
66
  basta.screen_data(keymap)
57
- basta.add_time_and_range()
67
+ basta.add_time_and_range(time_dtype="f8")
58
68
  basta.add_site_geolocation()
59
69
  basta.add_zenith_angle()
60
70
  basta.add_radar_specific_variables()
61
71
  basta.add_height()
62
72
  basta.sort_timestamps()
73
+ basta.remove_duplicate_timestamps()
74
+ basta.test_if_all_masked()
63
75
  attributes = output.add_time_attribute(ATTRIBUTES, basta.date)
64
76
  output.update_attributes(basta.data, attributes)
65
- uuid = output.save_level1b(basta, output_file, uuid)
77
+ output.save_level1b(basta, output_file, uuid)
66
78
  return uuid
67
79
 
68
80
 
@@ -75,9 +87,9 @@ class Basta(NcRadar):
75
87
 
76
88
  """
77
89
 
78
- def __init__(self, full_path: str, site_meta: dict):
90
+ def __init__(self, full_path: str | PathLike, site_meta: dict) -> None:
79
91
  super().__init__(full_path, site_meta)
80
- self.date: list[str] = self.get_date()
92
+ self.date = self.get_date()
81
93
  self.instrument = instruments.BASTA
82
94
 
83
95
  def screen_data(self, keymap: dict) -> None:
@@ -87,12 +99,14 @@ class Basta(NcRadar):
87
99
  if key in self.data and self.data[key].data.ndim == mask.ndim:
88
100
  self.data[key].mask_indices(np.where(mask != 1))
89
101
 
90
- def validate_date(self, expected_date: str) -> None:
102
+ def validate_date(self, expected_date: datetime.date) -> None:
91
103
  """Validates expected data."""
92
104
  date_units = self.dataset.variables["time"].units
93
- date = date_units.split()[2]
105
+ date_str = date_units.split()[2]
106
+ date = datetime.date.fromisoformat(date_str)
94
107
  if expected_date != date:
95
- raise ValidTimeStampError
108
+ msg = "Time units doesn't match expected date"
109
+ raise ValidTimeStampError(msg)
96
110
 
97
111
  def add_zenith_angle(self) -> None:
98
112
  elevation = self.getvar("elevation")
@@ -105,15 +119,18 @@ ATTRIBUTES = {
105
119
  long_name="Radar pitch angle",
106
120
  units="degree",
107
121
  standard_name="platform_roll",
122
+ dimensions=("time",),
108
123
  ),
109
124
  "radar_yaw": MetaData(
110
125
  long_name="Radar yaw angle",
111
126
  units="degree",
112
127
  standard_name="platform_yaw",
128
+ dimensions=("time",),
113
129
  ),
114
130
  "radar_roll": MetaData(
115
131
  long_name="Radar roll angle",
116
132
  units="degree",
117
133
  standard_name="platform_roll",
134
+ dimensions=("time",),
118
135
  ),
119
136
  }
@@ -0,0 +1,135 @@
1
+ import datetime
2
+ from os import PathLike
3
+ from uuid import UUID
4
+
5
+ import numpy as np
6
+ from numpy import ma
7
+
8
+ from cloudnetpy import output
9
+ from cloudnetpy.constants import G_TO_KG, MM_H_TO_M_S
10
+ from cloudnetpy.exceptions import ValidTimeStampError
11
+ from cloudnetpy.instruments.instruments import FMCW94
12
+ from cloudnetpy.instruments.nc_radar import NcRadar
13
+ from cloudnetpy.instruments.rpg import RPG_ATTRIBUTES
14
+ from cloudnetpy.metadata import MetaData
15
+ from cloudnetpy.utils import bit_field_definition, get_uuid
16
+
17
+
18
+ def bowtie2nc(
19
+ bowtie_file: str | PathLike,
20
+ output_file: str | PathLike,
21
+ site_meta: dict,
22
+ uuid: str | UUID | None = None,
23
+ date: str | datetime.date | None = None,
24
+ ) -> UUID:
25
+ """Converts data from 'BOW-TIE' campaign cloud radar on RV-Meteor into
26
+ Cloudnet Level 1b netCDF file.
27
+
28
+ Args:
29
+ bowtie_file: Input filename.
30
+ output_file: Output filename.
31
+ site_meta: Dictionary containing information about the site. Required key
32
+ value pair is `name`. Optional are `latitude`, `longitude`, `altitude`.
33
+ uuid: Set specific UUID for the file.
34
+ date: Expected date as YYYY-MM-DD of all profiles in the file.
35
+
36
+ Returns:
37
+ UUID of the generated file.
38
+
39
+ Raises:
40
+ ValidTimeStampError: No valid timestamps found.
41
+
42
+ """
43
+ keymap = {
44
+ "Zh": "Zh",
45
+ "v": "v",
46
+ "width": "width",
47
+ "ldr": "ldr",
48
+ "kurt": "kurtosis",
49
+ "Skew": "skewness",
50
+ "SNR": "SNR",
51
+ "time": "time",
52
+ "range": "range",
53
+ "lwp": "lwp",
54
+ "SurfRelHum": "relative_humidity",
55
+ "rain": "rainfall_rate",
56
+ "Nyquist_velocity": "nyquist_velocity",
57
+ "range_offsets": "chirp_start_indices",
58
+ }
59
+
60
+ if isinstance(date, str):
61
+ date = datetime.date.fromisoformat(date)
62
+ uuid = get_uuid(uuid)
63
+
64
+ with Bowtie(bowtie_file, site_meta) as bowtie:
65
+ bowtie.init_data(keymap)
66
+ bowtie.add_time_and_range()
67
+ if date is not None:
68
+ bowtie.check_date(date)
69
+ bowtie.add_radar_specific_variables()
70
+ bowtie.add_site_geolocation()
71
+ bowtie.add_height()
72
+ bowtie.convert_units()
73
+ bowtie.fix_chirp_start_indices()
74
+ bowtie.test_if_all_masked()
75
+ bowtie.add_correction_bits()
76
+ attributes = output.add_time_attribute(ATTRIBUTES, bowtie.date)
77
+ output.update_attributes(bowtie.data, attributes)
78
+ output.save_level1b(bowtie, output_file, uuid)
79
+ return uuid
80
+
81
+
82
+ class Bowtie(NcRadar):
83
+ def __init__(self, full_path: str | PathLike, site_meta: dict) -> None:
84
+ super().__init__(full_path, site_meta)
85
+ self.instrument = FMCW94
86
+ self.date = self.get_date()
87
+
88
+ def convert_units(self) -> None:
89
+ self.data["lwp"].data *= G_TO_KG
90
+ self.data["rainfall_rate"].data *= MM_H_TO_M_S
91
+ self.data["relative_humidity"].data /= 100
92
+
93
+ def fix_chirp_start_indices(self) -> None:
94
+ array = self.data["chirp_start_indices"].data
95
+ self.data["chirp_start_indices"].data = np.array(array, dtype=np.int32)
96
+ self.data["chirp_start_indices"].data_type = "int32"
97
+
98
+ def add_correction_bits(self) -> None:
99
+ bits = ma.ones(self.data["v"].data.shape, dtype=np.uint32)
100
+ bits.mask = self.data["v"].data.mask
101
+ self.append_data(bits, "correction_bits")
102
+
103
+ def check_date(self, date: datetime.date) -> None:
104
+ if self.date != date:
105
+ raise ValidTimeStampError
106
+
107
+
108
+ ATTRIBUTES = RPG_ATTRIBUTES | {
109
+ "v": MetaData(
110
+ long_name="Doppler velocity",
111
+ units="m s-1",
112
+ comment=(
113
+ "This parameter is the radial component of the velocity, with positive\n"
114
+ "velocities are away from the radar. It was corrected for the heave\n"
115
+ "motion of the ship. A rolling average over 3 time steps has been\n"
116
+ "applied to it."
117
+ ),
118
+ dimensions=("time", "range"),
119
+ ),
120
+ "correction_bits": MetaData(
121
+ long_name="Correction bits",
122
+ units="1",
123
+ definition=bit_field_definition({0: """Doppler velocity is dealiased."""}),
124
+ comment=(
125
+ "This parameter is a bit field that indicates which corrections have\n"
126
+ "been applied to radar measurements."
127
+ ),
128
+ dimensions=("time", "range"),
129
+ ),
130
+ "nyquist_velocity": MetaData(
131
+ long_name="Nyquist velocity",
132
+ units="m s-1",
133
+ dimensions=("time", "chirp_sequence"),
134
+ ),
135
+ }