cloudnetpy 1.65.8__py3-none-any.whl → 1.66.1__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 (42) hide show
  1. cloudnetpy/categorize/__init__.py +0 -1
  2. cloudnetpy/categorize/atmos_utils.py +278 -59
  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 +80 -0
  7. cloudnetpy/categorize/attenuations/melting_attenuation.py +75 -0
  8. cloudnetpy/categorize/attenuations/rain_attenuation.py +84 -0
  9. cloudnetpy/categorize/categorize.py +140 -81
  10. cloudnetpy/categorize/classify.py +92 -128
  11. cloudnetpy/categorize/containers.py +45 -31
  12. cloudnetpy/categorize/droplet.py +2 -2
  13. cloudnetpy/categorize/falling.py +3 -3
  14. cloudnetpy/categorize/freezing.py +2 -2
  15. cloudnetpy/categorize/itu.py +243 -0
  16. cloudnetpy/categorize/melting.py +0 -3
  17. cloudnetpy/categorize/model.py +31 -14
  18. cloudnetpy/categorize/radar.py +28 -12
  19. cloudnetpy/constants.py +3 -6
  20. cloudnetpy/model_evaluation/file_handler.py +2 -2
  21. cloudnetpy/model_evaluation/products/observation_products.py +8 -8
  22. cloudnetpy/model_evaluation/tests/unit/test_grid_methods.py +5 -2
  23. cloudnetpy/model_evaluation/tests/unit/test_observation_products.py +11 -11
  24. cloudnetpy/output.py +46 -26
  25. cloudnetpy/plotting/plot_meta.py +8 -2
  26. cloudnetpy/plotting/plotting.py +37 -7
  27. cloudnetpy/products/classification.py +39 -34
  28. cloudnetpy/products/der.py +15 -13
  29. cloudnetpy/products/drizzle_tools.py +22 -21
  30. cloudnetpy/products/ier.py +8 -45
  31. cloudnetpy/products/iwc.py +7 -22
  32. cloudnetpy/products/lwc.py +14 -15
  33. cloudnetpy/products/mwr_tools.py +15 -2
  34. cloudnetpy/products/product_tools.py +121 -119
  35. cloudnetpy/utils.py +4 -0
  36. cloudnetpy/version.py +2 -2
  37. {cloudnetpy-1.65.8.dist-info → cloudnetpy-1.66.1.dist-info}/METADATA +1 -1
  38. {cloudnetpy-1.65.8.dist-info → cloudnetpy-1.66.1.dist-info}/RECORD +41 -35
  39. {cloudnetpy-1.65.8.dist-info → cloudnetpy-1.66.1.dist-info}/WHEEL +1 -1
  40. cloudnetpy/categorize/atmos.py +0 -376
  41. {cloudnetpy-1.65.8.dist-info → cloudnetpy-1.66.1.dist-info}/LICENSE +0 -0
  42. {cloudnetpy-1.65.8.dist-info → cloudnetpy-1.66.1.dist-info}/top_level.txt +0 -0
@@ -43,8 +43,7 @@ def generate_iwc(
43
43
  product = "iwc"
44
44
  with IwcSource(categorize_file, product) as iwc_source:
45
45
  ice_classification = IceClassification(categorize_file)
46
- iwc_source.append_main_variable_including_rain(ice_classification)
47
- iwc_source.append_main_variable(ice_classification)
46
+ iwc_source.append_icy_data(ice_classification)
48
47
  iwc_source.append_bias()
49
48
  iwc_source.append_sensitivity()
50
49
  lwp_prior, bias = iwc_source.append_error(ice_classification)
@@ -92,7 +91,7 @@ class IwcSource(IceSource):
92
91
  retrieval_uncertainty,
93
92
  error_uncorrected,
94
93
  )
95
- iwc_error[(~ice_classification.is_ice | ice_classification.ice_above_rain)] = (
94
+ iwc_error[~ice_classification.is_ice | ice_classification.uncorrected_ice] = (
96
95
  ma.masked
97
96
  )
98
97
  self.append_data(iwc_error, f"{self.product}_error")
@@ -162,12 +161,6 @@ COMMENTS = {
162
161
  "This variable describes whether a retrieval was performed\n"
163
162
  "for each pixel, and its associated quality."
164
163
  ),
165
- "iwc_inc_rain": (
166
- "This variable is the same as iwc but it also contains iwc values\n"
167
- "above rain. The iwc values above rain have been severely affected\n"
168
- "by attenuation and should be used when the effect of attenuation\n"
169
- "is being studied."
170
- ),
171
164
  }
172
165
 
173
166
  DEFINITIONS = {
@@ -175,15 +168,12 @@ DEFINITIONS = {
175
168
  {
176
169
  0: """No ice present.""",
177
170
  1: """Reliable retrieval.""",
178
- 2: """Unreliable retrieval due to uncorrected attenuation from
179
- liquid water below the ice (no liquid water path measurement
180
- available).""",
181
- 3: """Retrieval performed but radar corrected for liquid attenuation
182
- using radiometer liquid water path which is not always
183
- accurate.""",
171
+ 2: """Unreliable retrieval due to uncorrected liquid, rain or
172
+ melting attenuation.""",
173
+ 3: """Retrieval performed with radar corrected for liquid, rain and
174
+ melting attenuation.""",
184
175
  4: """Ice detected only by the lidar.""",
185
- 5: """Ice detected by radar but rain below so no retrieval performed
186
- due to very uncertain attenuation.""",
176
+ 5: """Uncorrected rain attenuation (deprecated).""",
187
177
  6: """Clear sky above rain and wet-bulb temperature less than 0degC:
188
178
  if rain attenuation is strong, ice could be present but
189
179
  undetected.""",
@@ -200,11 +190,6 @@ IWC_ATTRIBUTES = {
200
190
  units="kg m-3",
201
191
  ancillary_variables="iwc_error iwc_sensitivity iwc_bias",
202
192
  ),
203
- "iwc_inc_rain": MetaData(
204
- long_name="Ice water content including rain",
205
- units="kg m-3",
206
- comment=COMMENTS["iwc_inc_rain"],
207
- ),
208
193
  "iwc_error": MetaData(
209
194
  long_name="Random error in ice water content",
210
195
  units="dB",
@@ -6,7 +6,7 @@ import numpy as np
6
6
  from numpy import ma
7
7
 
8
8
  from cloudnetpy import output, utils
9
- from cloudnetpy.categorize import atmos
9
+ from cloudnetpy.categorize import atmos_utils
10
10
  from cloudnetpy.datasource import DataSource
11
11
  from cloudnetpy.exceptions import InvalidSourceFileError
12
12
  from cloudnetpy.metadata import MetaData
@@ -80,7 +80,7 @@ class LwcSource(DataSource):
80
80
  lwp (ndarray): 1D liquid water path.
81
81
  lwp_error (ndarray): 1D error of liquid water path.
82
82
  is_rain (ndarray): 1D array denoting presence of rain.
83
- dheight (float): Median difference in height vector.
83
+ path_lengths (ndarray): 1D array of path lengths.
84
84
  atmosphere (dict): Dictionary containing interpolated fields `temperature`
85
85
  and `pressure`.
86
86
  categorize_bits (CategorizeBits): The :class:`CategorizeBits` instance.
@@ -96,7 +96,7 @@ class LwcSource(DataSource):
96
96
  self.lwp[self.lwp < 0] = 0
97
97
  self.lwp_error = self.getvar("lwp_error")
98
98
  self.is_rain = get_is_rain(categorize_file)
99
- self.dheight = utils.mdiff(self.getvar("height"))
99
+ self.path_lengths = utils.path_lengths_from_ground(self.getvar("height"))
100
100
  self.atmosphere = self._get_atmosphere(categorize_file)
101
101
  self.categorize_bits = CategorizeBits(categorize_file)
102
102
 
@@ -125,7 +125,6 @@ class Lwc:
125
125
 
126
126
  Attributes:
127
127
  lwc_source (LwcSource): The :class:`LwcSource` instance.
128
- dheight (float): Median difference in height vector.
129
128
  is_liquid (ndarray): 2D array denoting liquid.
130
129
  lwc_adiabatic (ndarray): 2D array storing adiabatic lwc.
131
130
  lwc (ndarray): 2D array of liquid water content (scaled with lwp).
@@ -134,7 +133,7 @@ class Lwc:
134
133
 
135
134
  def __init__(self, lwc_source: LwcSource):
136
135
  self.lwc_source = lwc_source
137
- self.dheight = self.lwc_source.dheight
136
+ self.height = lwc_source.getvar("height")
138
137
  self.is_liquid = self._get_liquid()
139
138
  self.lwc_adiabatic = self._init_lwc_adiabatic()
140
139
  self.lwc = self._adiabatic_lwc_to_lwc()
@@ -142,26 +141,26 @@ class Lwc:
142
141
 
143
142
  def _get_liquid(self) -> np.ndarray:
144
143
  category_bits = self.lwc_source.categorize_bits.category_bits
145
- return category_bits["droplet"]
144
+ return category_bits.droplet
146
145
 
147
146
  def _init_lwc_adiabatic(self) -> np.ndarray:
148
147
  """Returns theoretical adiabatic lwc in liquid clouds (kg/m3)."""
149
- lwc_dz = atmos.fill_clouds_with_lwc_dz(
150
- self.lwc_source.atmosphere,
148
+ lwc_dz = atmos_utils.fill_clouds_with_lwc_dz(
149
+ *self.lwc_source.atmosphere,
151
150
  self.is_liquid,
152
151
  )
153
- return atmos.calc_adiabatic_lwc(lwc_dz, self.dheight)
152
+ return atmos_utils.calc_adiabatic_lwc(lwc_dz, self.height)
154
153
 
155
154
  def _adiabatic_lwc_to_lwc(self) -> np.ndarray:
156
155
  """Initialises liquid water content (kg/m3).
157
156
 
158
157
  Calculates LWC for ALL profiles (rain, lwp > theoretical, etc.),
159
158
  """
160
- lwc_scaled = atmos.distribute_lwp_to_liquid_clouds(
159
+ return atmos_utils.normalize_lwc_by_lwp(
161
160
  self.lwc_adiabatic,
162
161
  self.lwc_source.lwp,
162
+ self.height,
163
163
  )
164
- return lwc_scaled / self.dheight
165
164
 
166
165
  def _mask_rain(self) -> None:
167
166
  is_rain = self.lwc_source.is_rain.astype(bool)
@@ -198,7 +197,7 @@ class CloudAdjustor:
198
197
 
199
198
  def _get_echo(self) -> dict:
200
199
  quality_bits = self.lwc_source.categorize_bits.quality_bits
201
- return {key: quality_bits[key] for key in ("radar", "lidar")}
200
+ return {"radar": quality_bits.radar, "lidar": quality_bits.lidar}
202
201
 
203
202
  def _init_status(self) -> ma.MaskedArray:
204
203
  status = ma.zeros(self.is_liquid.shape, dtype=int)
@@ -232,8 +231,8 @@ class CloudAdjustor:
232
231
  distance_from_base += 1
233
232
 
234
233
  def _has_converged(self, ind: int) -> bool:
235
- lwc_sum = ma.sum(self.lwc_adiabatic[ind, :])
236
- return lwc_sum * self.lwc_source.dheight > self.lwc_source.lwp[ind]
234
+ lwc_sum = ma.sum(self.lwc_adiabatic[ind, :] * self.lwc_source.path_lengths)
235
+ return lwc_sum > self.lwc_source.lwp[ind]
237
236
 
238
237
  def _out_of_bound(self, ind: int) -> bool:
239
238
  return ind >= self.lwc.shape[1] - 1
@@ -288,7 +287,7 @@ class CloudAdjustor:
288
287
  In theory, this difference should be always positive. Negative values
289
288
  indicate missing (or too narrow) liquid clouds.
290
289
  """
291
- lwc_sum = ma.sum(self.lwc_adiabatic, axis=1) * self.lwc_source.dheight
290
+ lwc_sum = ma.sum(self.lwc_adiabatic * self.lwc_source.path_lengths, axis=1)
292
291
  return lwc_sum - self.lwc_source.lwp
293
292
 
294
293
  def _mask_rain(self) -> None:
@@ -1,8 +1,10 @@
1
+ import os
1
2
  import tempfile
2
3
  from typing import Literal
3
4
 
4
5
  import netCDF4
5
6
  import numpy as np
7
+ import requests
6
8
  from mwrpy.level2.lev2_collocated import generate_lev2_multi as gen_multi
7
9
  from mwrpy.level2.lev2_collocated import generate_lev2_single as gen_single
8
10
  from mwrpy.level2.write_lev2_nc import MissingInputData
@@ -10,7 +12,6 @@ from mwrpy.version import __version__ as mwrpy_version
10
12
 
11
13
  from cloudnetpy import output, utils
12
14
  from cloudnetpy.exceptions import ValidTimeStampError
13
- from cloudnetpy.products import product_tools
14
15
 
15
16
 
16
17
  def generate_mwr_single(
@@ -59,7 +60,7 @@ def _generate_product(
59
60
  ) -> str:
60
61
  fun = gen_multi if product == "multi" else gen_single
61
62
  with tempfile.TemporaryDirectory() as temp_dir:
62
- coeffs = product_tools.get_read_mwrpy_coeffs(mwr_l1c_file, temp_dir)
63
+ coeffs = _read_mwrpy_coeffs(mwr_l1c_file, temp_dir)
63
64
  try:
64
65
  fun(None, mwr_l1c_file, output_file, coeff_files=coeffs)
65
66
  except MissingInputData as err:
@@ -136,3 +137,15 @@ class Mwr:
136
137
  self.nc_l2.source_file_uuids = self.nc_l1c.file_uuid
137
138
  self.nc_l2.mwrpy_version = mwrpy_version
138
139
  self.nc_l2.instrument_pid = self.nc_l1c.instrument_pid
140
+
141
+
142
+ def _read_mwrpy_coeffs(mwr_l1c_file, folder: str) -> list:
143
+ with netCDF4.Dataset(mwr_l1c_file) as nc:
144
+ links = nc.mwrpy_coefficients.split(", ")
145
+ coeffs = []
146
+ for link in links:
147
+ full_path = os.path.join(folder, link.split("/")[-1])
148
+ with open(full_path, "wb") as f:
149
+ f.write(requests.get(link, timeout=10).content)
150
+ coeffs.append(full_path)
151
+ return coeffs
@@ -1,12 +1,12 @@
1
1
  """General helper classes and functions for all products."""
2
2
 
3
- import os
3
+ from dataclasses import dataclass
4
4
  from typing import NamedTuple
5
5
 
6
6
  import netCDF4
7
7
  import numpy as np
8
- import requests
9
8
  from numpy import ma
9
+ from numpy.typing import NDArray
10
10
 
11
11
  from cloudnetpy import constants, utils
12
12
  from cloudnetpy.categorize import atmos_utils
@@ -23,53 +23,63 @@ class IceCoefficients(NamedTuple):
23
23
  c: float
24
24
 
25
25
 
26
- class CategorizeBits:
27
- """Class holding information about category and quality bits.
26
+ @dataclass
27
+ class CategoryBits:
28
+ droplet: NDArray[np.bool_]
29
+ falling: NDArray[np.bool_]
30
+ freezing: NDArray[np.bool_]
31
+ melting: NDArray[np.bool_]
32
+ aerosol: NDArray[np.bool_]
33
+ insect: NDArray[np.bool_]
28
34
 
29
- Args:
30
- categorize_file (str): Categorize file name.
31
35
 
32
- Attributes:
33
- category_bits (dict): Dictionary containing boolean fields for `droplet`,
34
- `falling`, `cold`, `melting`, `aerosol`, `insect`.
35
-
36
- quality_bits (dict): Dictionary containing boolean fields for `radar`,
37
- `lidar`, `clutter`, `molecular`, `attenuated` and `corrected`.
38
-
39
- """
36
+ @dataclass
37
+ class QualityBits:
38
+ radar: NDArray[np.bool_]
39
+ lidar: NDArray[np.bool_]
40
+ clutter: NDArray[np.bool_]
41
+ molecular: NDArray[np.bool_]
42
+ attenuated_liquid: NDArray[np.bool_]
43
+ corrected_liquid: NDArray[np.bool_]
44
+ attenuated_rain: NDArray[np.bool_]
45
+ corrected_rain: NDArray[np.bool_]
46
+ attenuated_melting: NDArray[np.bool_]
47
+ corrected_melting: NDArray[np.bool_]
40
48
 
41
- category_keys = (
42
- "droplet",
43
- "falling",
44
- "cold",
45
- "melting",
46
- "aerosol",
47
- "insect",
48
- )
49
-
50
- quality_keys = (
51
- "radar",
52
- "lidar",
53
- "clutter",
54
- "molecular",
55
- "attenuated",
56
- "corrected",
57
- )
58
49
 
50
+ class CategorizeBits:
59
51
  def __init__(self, categorize_file: str):
60
52
  self._categorize_file = categorize_file
61
- self.category_bits = self._read_bits("category")
62
- self.quality_bits = self._read_bits("quality")
53
+ self.category_bits = self._read_category_bits()
54
+ self.quality_bits = self._read_quality_bits()
55
+
56
+ def _read_category_bits(self) -> CategoryBits:
57
+ with netCDF4.Dataset(self._categorize_file) as nc:
58
+ bits = nc.variables["category_bits"][:]
59
+ return CategoryBits(
60
+ droplet=utils.isbit(bits, 0),
61
+ falling=utils.isbit(bits, 1),
62
+ freezing=utils.isbit(bits, 2),
63
+ melting=utils.isbit(bits, 3),
64
+ aerosol=utils.isbit(bits, 4),
65
+ insect=utils.isbit(bits, 5),
66
+ )
63
67
 
64
- def _read_bits(self, bit_type: str) -> dict:
65
- """Converts bitfield into dictionary."""
68
+ def _read_quality_bits(self) -> QualityBits:
66
69
  with netCDF4.Dataset(self._categorize_file) as nc:
67
- try:
68
- bitfield = nc.variables[f"{bit_type}_bits"][:]
69
- except KeyError as err:
70
- raise KeyError from err
71
- keys = getattr(CategorizeBits, f"{bit_type}_keys")
72
- return {key: utils.isbit(bitfield, i) for i, key in enumerate(keys)}
70
+ bits = nc.variables["quality_bits"][:]
71
+ return QualityBits(
72
+ radar=utils.isbit(bits, 0),
73
+ lidar=utils.isbit(bits, 1),
74
+ clutter=utils.isbit(bits, 2),
75
+ molecular=utils.isbit(bits, 3),
76
+ attenuated_liquid=utils.isbit(bits, 4),
77
+ corrected_liquid=utils.isbit(bits, 5),
78
+ attenuated_rain=utils.isbit(bits, 6),
79
+ corrected_rain=utils.isbit(bits, 7),
80
+ attenuated_melting=utils.isbit(bits, 8),
81
+ corrected_melting=utils.isbit(bits, 9),
82
+ )
73
83
 
74
84
 
75
85
  class ProductClassification(CategorizeBits):
@@ -96,53 +106,75 @@ class IceClassification(ProductClassification):
96
106
 
97
107
  def __init__(self, categorize_file: str):
98
108
  super().__init__(categorize_file)
109
+ self._is_attenuated = self._find_attenuated()
110
+ self._is_corrected = self._find_corrected()
99
111
  self.is_ice = self._find_ice()
100
112
  self.would_be_ice = self._find_would_be_ice()
101
113
  self.corrected_ice = self._find_corrected_ice()
102
114
  self.uncorrected_ice = self._find_uncorrected_ice()
103
115
  self.ice_above_rain = self._find_ice_above_rain()
104
- self.cold_above_rain = self._find_cold_above_rain()
116
+ self.clear_above_rain = self._find_clear_above_rain()
117
+
118
+ def _find_clear_above_rain(self) -> np.ndarray:
119
+ return (
120
+ utils.transpose(self.is_rain) * ~self.is_ice
121
+ & self.category_bits.freezing
122
+ & ~self.category_bits.melting
123
+ )
124
+
125
+ def _find_attenuated(self) -> np.ndarray:
126
+ return (
127
+ self.quality_bits.attenuated_liquid
128
+ | self.quality_bits.attenuated_rain
129
+ | self.quality_bits.attenuated_melting
130
+ )
131
+
132
+ def _find_corrected(self) -> np.ndarray:
133
+ return (
134
+ self.quality_bits.corrected_liquid
135
+ | self.quality_bits.corrected_rain
136
+ | self.quality_bits.corrected_melting
137
+ )
105
138
 
106
139
  def _find_ice(self) -> np.ndarray:
107
140
  return (
108
- self.category_bits["falling"]
109
- & self.category_bits["cold"]
110
- & ~self.category_bits["melting"]
111
- & ~self.category_bits["insect"]
141
+ self.category_bits.falling
142
+ & self.category_bits.freezing
143
+ & ~self.category_bits.melting
144
+ & ~self.category_bits.insect
112
145
  )
113
146
 
114
147
  def _find_would_be_ice(self) -> np.ndarray:
115
148
  warm_falling = (
116
- self.category_bits["falling"]
117
- & ~self.category_bits["cold"]
118
- & ~self.category_bits["insect"]
149
+ self.category_bits.falling
150
+ & ~self.category_bits.freezing
151
+ & ~self.category_bits.insect
119
152
  )
120
- return warm_falling | self.category_bits["melting"]
153
+ return warm_falling | self.category_bits.melting
121
154
 
122
155
  def _find_corrected_ice(self) -> np.ndarray:
123
- return (
124
- self.is_ice
125
- & self.quality_bits["attenuated"]
126
- & self.quality_bits["corrected"]
127
- )
156
+ return self.is_ice & self._is_attenuated & self._is_corrected
128
157
 
129
158
  def _find_uncorrected_ice(self) -> np.ndarray:
159
+ uncorrected_melting = (
160
+ self.quality_bits.attenuated_melting & ~self.quality_bits.corrected_melting
161
+ )
162
+ uncorrected_rain = (
163
+ self.quality_bits.attenuated_rain & ~self.quality_bits.corrected_rain
164
+ )
165
+ uncorrected_liquid = (
166
+ self.quality_bits.attenuated_liquid & ~self.quality_bits.corrected_liquid
167
+ )
130
168
  return (
131
169
  self.is_ice
132
- & self.quality_bits["attenuated"]
133
- & ~self.quality_bits["corrected"]
170
+ & self._is_attenuated
171
+ & (uncorrected_melting | uncorrected_rain | uncorrected_liquid)
134
172
  )
135
173
 
136
174
  def _find_ice_above_rain(self) -> np.ndarray:
137
175
  is_rain = utils.transpose(self.is_rain)
138
176
  return (self.is_ice * is_rain) == 1
139
177
 
140
- def _find_cold_above_rain(self) -> np.ndarray:
141
- is_cold = self.category_bits["cold"]
142
- is_rain = utils.transpose(self.is_rain)
143
- is_cold_rain = (is_cold * is_rain) == 1
144
- return is_cold_rain & ~self.category_bits["melting"]
145
-
146
178
 
147
179
  class IceSource(DataSource):
148
180
  """Base class for different ice products."""
@@ -150,24 +182,20 @@ class IceSource(DataSource):
150
182
  def __init__(self, categorize_file: str, product: str):
151
183
  super().__init__(categorize_file)
152
184
  self.wl_band = utils.get_wl_band(float(self.getvar("radar_frequency")))
153
- self.temperature = get_temperature(categorize_file)
185
+ self.temperature = _get_temperature(categorize_file)
154
186
  self.product = product
155
187
  self.coefficients = self._get_coefficients()
156
188
 
157
- def append_main_variable_including_rain(
189
+ def append_icy_data(
158
190
  self,
159
191
  ice_classification: IceClassification,
160
192
  ) -> None:
161
193
  """Adds the main variable (including ice above rain)."""
162
- data_including_rain = self._convert_z()
163
- data_including_rain[~ice_classification.is_ice] = ma.masked
164
- self.append_data(data_including_rain, f"{self.product}_inc_rain")
165
-
166
- def append_main_variable(self, ice_classification: IceClassification) -> None:
167
- """Adds the main variable (excluding rain)."""
168
- data = ma.copy(self.data[f"{self.product}_inc_rain"][:])
169
- data[ice_classification.ice_above_rain] = ma.masked
170
- self.append_data(data, self.product)
194
+ data = self._convert_z()
195
+ data[~ice_classification.is_ice | ice_classification.uncorrected_ice] = (
196
+ ma.masked
197
+ )
198
+ self.append_data(data, f"{self.product}")
171
199
 
172
200
  def append_status(self, ice_classification: IceClassification) -> None:
173
201
  """Adds the status of retrieval."""
@@ -176,10 +204,9 @@ class IceSource(DataSource):
176
204
  is_data = ~data.mask
177
205
  retrieval_status[is_data] = 1
178
206
  retrieval_status[is_data & ice_classification.corrected_ice] = 3
179
- retrieval_status[is_data & ice_classification.uncorrected_ice] = 2
180
207
  retrieval_status[~is_data & ice_classification.is_ice] = 4
181
- retrieval_status[ice_classification.cold_above_rain] = 6
182
- retrieval_status[ice_classification.ice_above_rain] = 5
208
+ retrieval_status[ice_classification.uncorrected_ice] = 2
209
+ retrieval_status[ice_classification.clear_above_rain] = 6
183
210
  retrieval_status[ice_classification.would_be_ice & (retrieval_status == 0)] = 7
184
211
  self.append_data(retrieval_status, f"{self.product}_retrieval_status")
185
212
 
@@ -232,16 +259,16 @@ class IceSource(DataSource):
232
259
 
233
260
 
234
261
  def get_is_rain(filename: str) -> np.ndarray:
235
- try:
236
- is_rain = read_nc_field(filename, "rain_detected")
237
- except KeyError:
238
- try:
239
- rainfall_rate = read_nc_field(filename, "rainfall_rate")
240
- except KeyError:
241
- rainfall_rate = read_nc_field(filename, "rain_rate")
242
- is_rain = rainfall_rate != 0
243
- is_rain[is_rain.mask] = True
244
- return np.array(is_rain)
262
+ # TODO: Check that this is correct
263
+ with netCDF4.Dataset(filename) as nc:
264
+ for name in ["rain_detected", "rainfall_rate", "rain_rate"]:
265
+ if name in nc.variables:
266
+ data = nc.variables[name][:]
267
+ data = data != 0
268
+ data[data.mask] = True
269
+ return np.array(data)
270
+ msg = "No rain data found."
271
+ raise ValueError(msg)
245
272
 
246
273
 
247
274
  def read_nc_field(nc_file: str, name: str) -> ma.MaskedArray:
@@ -249,21 +276,6 @@ def read_nc_field(nc_file: str, name: str) -> ma.MaskedArray:
249
276
  return nc.variables[name][:]
250
277
 
251
278
 
252
- def read_nc_fields(nc_file: str, names: list[str]) -> list[ma.MaskedArray]:
253
- """Reads selected variables from a netCDF file.
254
-
255
- Args:
256
- nc_file: netCDF file name.
257
- names: Variables to be read, e.g. ['ldr', 'lwp'].
258
-
259
- Returns:
260
- List of numpy arrays.
261
-
262
- """
263
- with netCDF4.Dataset(nc_file) as nc:
264
- return [nc.variables[name][:] for name in names]
265
-
266
-
267
279
  def interpolate_model(cat_file: str, names: str | list) -> dict[str, np.ndarray]:
268
280
  """Interpolates 2D model field into dense Cloudnet grid.
269
281
 
@@ -278,7 +290,7 @@ def interpolate_model(cat_file: str, names: str | list) -> dict[str, np.ndarray]
278
290
  """
279
291
 
280
292
  def _interp_field(var_name: str) -> np.ndarray:
281
- values = read_nc_fields(
293
+ values = _read_nc_fields(
282
294
  cat_file,
283
295
  ["model_time", "model_height", var_name, "time", "height"],
284
296
  )
@@ -288,22 +300,12 @@ def interpolate_model(cat_file: str, names: str | list) -> dict[str, np.ndarray]
288
300
  return {name: _interp_field(name) for name in names}
289
301
 
290
302
 
291
- def get_temperature(categorize_file: str) -> np.ndarray:
292
- """Returns interpolated temperatures in Celsius."""
293
- atmosphere = interpolate_model(categorize_file, "temperature")
294
- return atmos_utils.k2c(atmosphere["temperature"])
295
-
296
-
297
- def get_mwrpy_coeffs(nc_file: str) -> str:
303
+ def _read_nc_fields(nc_file: str, names: list[str]) -> list[ma.MaskedArray]:
298
304
  with netCDF4.Dataset(nc_file) as nc:
299
- return nc.mwrpy_coefficients.split(", ")
305
+ return [nc.variables[name][:] for name in names]
300
306
 
301
307
 
302
- def get_read_mwrpy_coeffs(mwr_l1c_file, folder: str) -> list:
303
- coeffs = []
304
- for link in get_mwrpy_coeffs(mwr_l1c_file):
305
- full_path = os.path.join(folder, link.split("/")[-1])
306
- with open(full_path, "wb") as f:
307
- f.write(requests.get(link, timeout=10).content)
308
- coeffs.append(full_path)
309
- return coeffs
308
+ def _get_temperature(categorize_file: str) -> np.ndarray:
309
+ """Returns interpolated temperatures in Celsius."""
310
+ atmosphere = interpolate_model(categorize_file, "temperature")
311
+ return atmos_utils.k2c(atmosphere["temperature"])
cloudnetpy/utils.py CHANGED
@@ -1013,3 +1013,7 @@ def status_field_definition(definitions: dict[T, str]) -> str:
1013
1013
 
1014
1014
  def bit_field_definition(definitions: dict[T, str]) -> str:
1015
1015
  return _format_definition("Bit", definitions)
1016
+
1017
+
1018
+ def path_lengths_from_ground(height: np.ndarray) -> np.ndarray:
1019
+ return np.diff(height, prepend=0)
cloudnetpy/version.py CHANGED
@@ -1,4 +1,4 @@
1
1
  MAJOR = 1
2
- MINOR = 65
3
- PATCH = 8
2
+ MINOR = 66
3
+ PATCH = 1
4
4
  __version__ = f"{MAJOR}.{MINOR}.{PATCH}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cloudnetpy
3
- Version: 1.65.8
3
+ Version: 1.66.1
4
4
  Summary: Python package for Cloudnet processing
5
5
  Author: Simo Tukiainen
6
6
  License: MIT License