cloudnetpy 1.65.7__py3-none-any.whl → 1.66.0__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 +31 -8
  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.7.dist-info → cloudnetpy-1.66.0.dist-info}/METADATA +1 -1
  38. {cloudnetpy-1.65.7.dist-info → cloudnetpy-1.66.0.dist-info}/RECORD +41 -35
  39. {cloudnetpy-1.65.7.dist-info → cloudnetpy-1.66.0.dist-info}/WHEEL +1 -1
  40. cloudnetpy/categorize/atmos.py +0 -376
  41. {cloudnetpy-1.65.7.dist-info → cloudnetpy-1.66.0.dist-info}/LICENSE +0 -0
  42. {cloudnetpy-1.65.7.dist-info → cloudnetpy-1.66.0.dist-info}/top_level.txt +0 -0
@@ -1,19 +1,28 @@
1
- """Module containing low-level functions to classify gridded
2
- radar / lidar measurements.
3
- """
4
-
5
1
  import numpy as np
6
2
  import skimage
7
3
  from numpy import ma
4
+ from numpy.typing import NDArray
8
5
 
9
- import cloudnetpy.categorize.atmos
10
6
  from cloudnetpy import utils
11
- from cloudnetpy.categorize import droplet, falling, freezing, insects, melting
12
- from cloudnetpy.categorize.containers import ClassData, ClassificationResult
7
+ from cloudnetpy.categorize import (
8
+ atmos_utils,
9
+ droplet,
10
+ falling,
11
+ freezing,
12
+ insects,
13
+ melting,
14
+ )
15
+ from cloudnetpy.categorize.attenuations import RadarAttenuation
16
+ from cloudnetpy.categorize.containers import (
17
+ ClassData,
18
+ ClassificationResult,
19
+ Observations,
20
+ )
13
21
  from cloudnetpy.constants import T0
22
+ from cloudnetpy.products.product_tools import CategoryBits, QualityBits
14
23
 
15
24
 
16
- def classify_measurements(data: dict) -> ClassificationResult:
25
+ def classify_measurements(data: Observations) -> ClassificationResult:
17
26
  """Classifies radar/lidar observations.
18
27
 
19
28
  This function classifies atmospheric scatterers from the input data.
@@ -21,8 +30,7 @@ def classify_measurements(data: dict) -> ClassificationResult:
21
30
  time / height grid before calling this function.
22
31
 
23
32
  Args:
24
- data: Containing :class:`Radar`, :class:`Lidar`, :class:`Model`
25
- and :class:`Mwr` instances.
33
+ data: A :class:`Observations` instance.
26
34
 
27
35
  Returns:
28
36
  A :class:`ClassificationResult` instance.
@@ -37,19 +45,20 @@ def classify_measurements(data: dict) -> ClassificationResult:
37
45
  implementation compared to the original Cloudnet methodology.
38
46
  Especially methods classifying insects, melting layer and liquid droplets.
39
47
 
40
- Explanation of bits:
41
- - bit 0: Liquid droplets
42
- - bit 1: Falling hydrometeors
43
- - bit 2: Freezing region
44
- - bit 3: Melting layer
45
- - bit 4: Aerosols
46
- - bit 5: Insects
47
-
48
48
  """
49
+ bits = CategoryBits(
50
+ droplet=np.array([], dtype=bool),
51
+ falling=np.array([], dtype=bool),
52
+ freezing=np.array([], dtype=bool),
53
+ melting=np.array([], dtype=bool),
54
+ aerosol=np.array([], dtype=bool),
55
+ insect=np.array([], dtype=bool),
56
+ )
57
+
49
58
  obs = ClassData(data)
50
- bits: list[np.ndarray] = [np.array([])] * 6
51
- bits[3] = melting.find_melting_layer(obs)
52
- bits[2] = freezing.find_freezing_region(obs, bits[3])
59
+
60
+ bits.melting = melting.find_melting_layer(obs)
61
+ bits.freezing = freezing.find_freezing_region(obs, bits.melting)
53
62
  liquid_from_lidar = droplet.find_liquid(obs)
54
63
  if obs.lv0_files is not None and len(obs.lv0_files) > 0:
55
64
  if "rpg-fmcw-94" not in obs.radar_type.lower():
@@ -68,24 +77,24 @@ def classify_measurements(data: dict) -> ClassificationResult:
68
77
  liquid_from_radar,
69
78
  liquid_from_lidar,
70
79
  )
71
- liquid_from_radar[~bits[2]] = 0
80
+ liquid_from_radar[~bits.freezing] = 0
72
81
  is_liquid = liquid_from_radar | liquid_from_lidar
73
82
  else:
74
83
  is_liquid = liquid_from_lidar
75
84
  liquid_prob = None
76
- bits[0] = droplet.correct_liquid_top(obs, is_liquid, bits[2], limit=500)
77
- bits[5], insect_prob = insects.find_insects(obs, bits[3], bits[0])
78
- bits[1] = falling.find_falling_hydrometeors(obs, bits[0], bits[5])
79
- bits, filtered_ice = _filter_falling(bits)
85
+ bits.droplet = droplet.correct_liquid_top(obs, is_liquid, bits.freezing, limit=500)
86
+ bits.insect, insect_prob = insects.find_insects(obs, bits.melting, bits.droplet)
87
+ bits.falling = falling.find_falling_hydrometeors(obs, bits.droplet, bits.insect)
88
+ filtered_ice = _filter_falling(bits)
80
89
  for _ in range(5):
81
- bits[3] = _fix_undetected_melting_layer(bits)
82
- bits = _filter_insects(bits)
83
- bits[4] = _find_aerosols(obs, bits[1], bits[0])
84
- bits[4][filtered_ice] = False
85
- bits = _fix_super_cold_liquid(obs, bits)
90
+ _fix_undetected_melting_layer(bits)
91
+ _filter_insects(bits)
92
+ bits.aerosol = _find_aerosols(obs, bits)
93
+ bits.aerosol[filtered_ice] = False
94
+ _fix_super_cold_liquid(obs, bits)
86
95
 
87
96
  return ClassificationResult(
88
- category_bits=_bits_to_integer(bits),
97
+ category_bits=bits,
89
98
  is_rain=obs.is_rain,
90
99
  is_clutter=obs.is_clutter,
91
100
  insect_prob=insect_prob,
@@ -93,112 +102,86 @@ def classify_measurements(data: dict) -> ClassificationResult:
93
102
  )
94
103
 
95
104
 
96
- def _fix_super_cold_liquid(obs: ClassData, bits: list) -> list:
105
+ def fetch_quality(
106
+ data: Observations,
107
+ classification: ClassificationResult,
108
+ attenuations: RadarAttenuation,
109
+ ) -> QualityBits:
110
+ return QualityBits(
111
+ radar=~data.radar.data["Z"][:].mask,
112
+ lidar=~data.lidar.data["beta"][:].mask,
113
+ clutter=classification.is_clutter,
114
+ molecular=np.zeros(data.radar.data["Z"][:].shape, dtype=bool),
115
+ attenuated_liquid=attenuations.liquid.attenuated,
116
+ corrected_liquid=attenuations.liquid.attenuated
117
+ & ~attenuations.liquid.uncorrected,
118
+ attenuated_rain=attenuations.rain.attenuated,
119
+ corrected_rain=attenuations.rain.attenuated & ~attenuations.rain.uncorrected,
120
+ attenuated_melting=attenuations.melting.attenuated,
121
+ corrected_melting=attenuations.melting.attenuated
122
+ & ~attenuations.melting.uncorrected,
123
+ )
124
+
125
+
126
+ def _fix_super_cold_liquid(obs: ClassData, bits: CategoryBits) -> None:
97
127
  """Supercooled liquid droplets do not exist in atmosphere below around -38 C."""
98
128
  t_limit = T0 - 38
99
- super_cold_liquid = np.where((obs.tw < t_limit) & bits[0])
100
- bits[0][super_cold_liquid] = False
101
- bits[1][super_cold_liquid] = True
102
- return bits
129
+ super_cold_liquid = np.where((obs.tw < t_limit) & bits.droplet)
130
+ bits.droplet[super_cold_liquid] = False
131
+ bits.falling[super_cold_liquid] = True
103
132
 
104
133
 
105
134
  def _remove_false_radar_liquid(
106
135
  liquid_from_radar: np.ndarray,
107
136
  liquid_from_lidar: np.ndarray,
108
- ) -> np.ndarray:
137
+ ) -> NDArray[np.bool_]:
109
138
  """Removes radar-liquid below lidar-detected liquid bases."""
110
- lidar_liquid_bases = cloudnetpy.categorize.atmos.find_cloud_bases(liquid_from_lidar)
139
+ lidar_liquid_bases = atmos_utils.find_cloud_bases(liquid_from_lidar)
111
140
  for prof, base in zip(*np.where(lidar_liquid_bases), strict=True):
112
141
  liquid_from_radar[prof, 0:base] = 0
113
142
  return liquid_from_radar
114
143
 
115
144
 
116
- def fetch_quality(
117
- data: dict,
118
- classification: ClassificationResult,
119
- attenuations: dict,
120
- ) -> dict:
121
- """Returns Cloudnet quality bits.
122
-
123
- Args:
124
- data: Containing :class:`Radar` and :class:`Lidar` instances.
125
- classification: A :class:`ClassificationResult` instance.
126
- attenuations: Dictionary containing keys `liquid_corrected`,
127
- `liquid_uncorrected`.
128
-
129
- Returns:
130
- Dictionary containing `quality_bits`, an integer array with the bits:
131
-
132
- - bit 0: Pixel contains radar data
133
- - bit 1: Pixel contains lidar data
134
- - bit 2: Pixel contaminated by radar clutter
135
- - bit 3: Molecular scattering present (currently not implemented!)
136
- - bit 4: Pixel was affected by liquid attenuation
137
- - bit 5: Liquid attenuation was corrected
138
- - bit 6: Data gap in radar or lidar data
139
-
140
- """
141
- bits: list[np.ndarray] = [np.ndarray([])] * 7
142
- radar_echo = data["radar"].data["Z"][:]
143
- bits[0] = ~radar_echo.mask
144
- bits[1] = ~data["lidar"].data["beta"][:].mask
145
- bits[2] = classification.is_clutter
146
- bits[4] = attenuations["liquid_corrected"] | attenuations["liquid_uncorrected"]
147
- bits[5] = attenuations["liquid_corrected"]
148
- qbits = _bits_to_integer(bits)
149
- return {"quality_bits": qbits}
150
-
151
-
152
145
  def _find_aerosols(
153
146
  obs: ClassData,
154
- is_falling: np.ndarray,
155
- is_liquid: np.ndarray,
156
- ) -> np.ndarray:
147
+ bits: CategoryBits,
148
+ ) -> NDArray[np.bool_]:
157
149
  """Estimates aerosols from lidar backscattering.
158
150
 
159
151
  Aerosols are lidar signals that are: a) not falling, b) not liquid droplets.
160
152
 
161
153
  Args:
162
154
  obs: A :class:`ClassData` instance.
163
- is_falling: 2-D boolean array of falling hydrometeors.
164
- is_liquid: 2-D boolean array of liquid droplets.
155
+ bits: A :class:`CategoryBits instance.
165
156
 
166
157
  Returns:
167
158
  2-D boolean array containing aerosols.
168
159
 
169
160
  """
170
161
  is_beta = ~obs.beta.mask
171
- return is_beta & ~is_falling & ~is_liquid
162
+ return is_beta & ~bits.falling & ~bits.droplet
172
163
 
173
164
 
174
- def _fix_undetected_melting_layer(bits: list) -> np.ndarray:
175
- melting_layer = bits[3]
176
- drizzle_and_falling = _find_drizzle_and_falling(*bits[:3])
165
+ def _fix_undetected_melting_layer(bits: CategoryBits) -> None:
166
+ drizzle_and_falling = _find_drizzle_and_falling(bits)
177
167
  transition = ma.diff(drizzle_and_falling, axis=1) == -1
178
- melting_layer[:, 1:][transition] = True
179
- return melting_layer
168
+ bits.melting[:, 1:][transition] = True
180
169
 
181
170
 
182
- def _find_drizzle_and_falling(
183
- is_liquid: np.ndarray,
184
- is_falling: np.ndarray,
185
- is_freezing: np.ndarray,
186
- ) -> np.ndarray:
171
+ def _find_drizzle_and_falling(bits: CategoryBits) -> np.ndarray:
187
172
  """Classifies pixels as falling, drizzle and others.
188
173
 
189
174
  Args:
190
- is_liquid: 2D boolean array denoting liquid layers.
191
- is_falling: 2D boolean array denoting falling pixels.
192
- is_freezing: 2D boolean array denoting subzero temperatures.
175
+ bits: A :class:`CategoryBits instance.
193
176
 
194
177
  Returns:
195
178
  2D array where values are 1 (falling, drizzle, supercooled liquids),
196
179
  2 (drizzle), and masked (all others).
197
180
 
198
181
  """
199
- falling_dry = is_falling & ~is_liquid
200
- supercooled_liquids = is_liquid & is_freezing
201
- drizzle = falling_dry & ~is_freezing
182
+ falling_dry = bits.falling & ~bits.droplet
183
+ supercooled_liquids = bits.droplet & bits.freezing
184
+ drizzle = falling_dry & ~bits.freezing
202
185
  drizzle_and_falling = falling_dry.astype(int) + drizzle.astype(int)
203
186
  drizzle_and_falling = ma.copy(drizzle_and_falling)
204
187
  drizzle_and_falling[supercooled_liquids] = 1
@@ -206,28 +189,10 @@ def _find_drizzle_and_falling(
206
189
  return drizzle_and_falling
207
190
 
208
191
 
209
- def _bits_to_integer(bits: list) -> np.ndarray:
210
- """Creates array of integers from individual boolean arrays.
211
-
212
- Args:
213
- bits: List of bit fields (of similar sizes) to be saved in the resulting
214
- array of integers. bits[0] is saved as bit 0, bits[1] as bit 1, etc.
215
-
216
- Returns:
217
- Array of integers containing the information of the individual boolean arrays.
218
-
219
- """
220
- int_array = np.zeros_like(bits[0], dtype=int)
221
- for n, bit in enumerate(bits):
222
- ind = np.where(bit) # works also if bit is None
223
- int_array[ind] = utils.setbit(int_array[ind].astype(int), n)
224
- return int_array
225
-
226
-
227
- def _filter_insects(bits: list) -> list:
228
- is_melting_layer = bits[3]
229
- is_insects = bits[5]
230
- is_falling = bits[1]
192
+ def _filter_insects(bits: CategoryBits) -> None:
193
+ is_melting_layer = bits.melting
194
+ is_insects = bits.insect
195
+ is_falling = bits.falling
231
196
 
232
197
  # Remove above melting layer
233
198
  above_melting = utils.ffill(is_melting_layer)
@@ -256,15 +221,14 @@ def _filter_insects(bits: list) -> list:
256
221
  is_insects[ind1[ind11], y - 1 : y + 2] = False
257
222
  except IndexError:
258
223
  continue
259
- bits[1] = is_falling
260
- bits[5] = is_insects
261
- return bits
224
+ bits.falling = is_falling
225
+ bits.insect = is_insects
262
226
 
263
227
 
264
- def _filter_falling(bits: list) -> tuple:
228
+ def _filter_falling(bits: CategoryBits) -> tuple:
265
229
  # filter falling ice speckle noise
266
- is_freezing = bits[2]
267
- is_falling = bits[1]
230
+ is_freezing = bits.freezing
231
+ is_falling = bits.falling
268
232
  is_falling_filtered = skimage.morphology.remove_small_objects(
269
233
  is_falling,
270
234
  10,
@@ -276,6 +240,6 @@ def _filter_falling(bits: list) -> tuple:
276
240
  # in warm these are (probably) insects
277
241
  insect_ind = np.where(~is_freezing & is_filtered)
278
242
  is_falling[insect_ind] = False
279
- bits[1] = is_falling
280
- bits[5][insect_ind] = True
281
- return bits, ice_ind
243
+ bits.falling = is_falling
244
+ bits.insect[insect_ind] = True
245
+ return ice_ind
@@ -5,13 +5,28 @@ from numpy import ma
5
5
 
6
6
  from cloudnetpy import utils
7
7
  from cloudnetpy.constants import MM_H_TO_M_S
8
+ from cloudnetpy.products.product_tools import CategoryBits
9
+
10
+ from .disdrometer import Disdrometer
11
+ from .lidar import Lidar
12
+ from .model import Model
13
+ from .mwr import Mwr
14
+ from .radar import Radar
8
15
 
9
16
 
10
17
  @dataclass
11
- class ClassificationResult:
12
- """Result of classification."""
18
+ class Observations:
19
+ radar: Radar
20
+ lidar: Lidar
21
+ model: Model
22
+ mwr: Mwr | None = None
23
+ disdrometer: Disdrometer | None = None
24
+ lv0_files: list[str] | None = None
13
25
 
14
- category_bits: np.ndarray
26
+
27
+ @dataclass
28
+ class ClassificationResult:
29
+ category_bits: CategoryBits
15
30
  is_rain: np.ndarray
16
31
  is_clutter: np.ndarray
17
32
  insect_prob: np.ndarray
@@ -44,30 +59,30 @@ class ClassData:
44
59
 
45
60
  """
46
61
 
47
- def __init__(self, data: dict):
62
+ def __init__(self, data: Observations):
48
63
  self.data = data
49
- self.z = data["radar"].data["Z"][:]
50
- self.v = data["radar"].data["v"][:]
51
- self.v_sigma = data["radar"].data["v_sigma"][:]
64
+ self.z = data.radar.data["Z"][:]
65
+ self.v = data.radar.data["v"][:]
66
+ self.v_sigma = data.radar.data["v_sigma"][:]
52
67
  for key in ("width", "ldr", "sldr"):
53
- if key in data["radar"].data:
54
- setattr(self, key, data["radar"].data[key][:])
55
- self.time = data["radar"].time
56
- self.height = data["radar"].height
57
- self.radar_type = data["radar"].source_type
58
- self.tw = data["model"].data["Tw"][:]
59
- self.model_type = data["model"].source_type
60
- self.beta = data["lidar"].data["beta"][:]
68
+ if key in data.radar.data:
69
+ setattr(self, key, data.radar.data[key][:])
70
+ self.time = data.radar.time
71
+ self.height = data.radar.height
72
+ self.radar_type = data.radar.source_type
73
+ self.tw = data.model.data["Tw"][:]
74
+ self.model_type = data.model.source_type
75
+ self.beta = data.lidar.data["beta"][:]
61
76
  self.lwp = (
62
- data["mwr"].data["lwp"][:]
63
- if data["mwr"] is not None
77
+ data.mwr.data["lwp"][:]
78
+ if data.mwr is not None
64
79
  else ma.masked_all(self.time.shape)
65
80
  )
66
81
  self.is_rain = self._find_profiles_with_rain()
67
82
  self.is_clutter = _find_clutter(self.v, self.is_rain)
68
- self.altitude = data["radar"].altitude
69
- self.lv0_files = data["lv0_files"]
70
- self.date = data["radar"].get_date()
83
+ self.altitude = data.radar.altitude
84
+ self.lv0_files = data.lv0_files
85
+ self.date = data.radar.get_date()
71
86
 
72
87
  def _find_profiles_with_rain(self) -> np.ndarray:
73
88
  is_rain = self._find_rain_from_radar_echo()
@@ -77,7 +92,8 @@ class ClassData:
77
92
  return is_rain
78
93
 
79
94
  def _find_rain_from_radar_echo(self) -> np.ndarray:
80
- gate_number = 3
95
+ first_gate_with_data = np.argmin(self.z.mask.all(axis=0))
96
+ gate_number = first_gate_with_data + 3
81
97
  threshold = {"z": 3, "v": 0, "ldr": -15}
82
98
  z = self.z[:, gate_number]
83
99
  v = self.v[:, gate_number]
@@ -98,19 +114,17 @@ class ClassData:
98
114
  )
99
115
 
100
116
  def _find_rain_from_disdrometer(self) -> ma.MaskedArray:
117
+ if self.data.disdrometer is None:
118
+ return ma.masked_all(self.time.shape, dtype=int)
101
119
  threshold_mm_h = 0.25 # Standard threshold for drizzle -> rain
102
120
  threshold_particles = 30 # This is arbitrary and should be better tested
103
121
  threshold_rate = threshold_mm_h * MM_H_TO_M_S
104
- try:
105
- rainfall_rate = self.data["disdrometer"].data["rainfall_rate"].data
106
- n_particles = self.data["disdrometer"].data["n_particles"].data
107
- is_rain = ma.array(
108
- (rainfall_rate > threshold_rate) & (n_particles > threshold_particles),
109
- dtype=int,
110
- )
111
- except (AttributeError, KeyError):
112
- is_rain = ma.masked_all(self.time.shape, dtype=int)
113
- return is_rain
122
+ rainfall_rate = self.data.disdrometer.data["rainfall_rate"].data
123
+ n_particles = self.data.disdrometer.data["n_particles"].data
124
+ return ma.array(
125
+ (rainfall_rate > threshold_rate) & (n_particles > threshold_particles),
126
+ dtype=int,
127
+ )
114
128
 
115
129
 
116
130
  def _find_clutter(
@@ -4,8 +4,8 @@ import numpy as np
4
4
  import scipy.signal
5
5
  from numpy import ma
6
6
 
7
- import cloudnetpy.categorize.atmos
8
7
  from cloudnetpy import utils
8
+ from cloudnetpy.categorize import atmos_utils
9
9
  from cloudnetpy.categorize.containers import ClassData
10
10
 
11
11
 
@@ -32,7 +32,7 @@ def correct_liquid_top(
32
32
 
33
33
  """
34
34
  is_liquid_corrected = np.copy(is_liquid)
35
- liquid_tops = cloudnetpy.categorize.atmos.find_cloud_tops(is_liquid)
35
+ liquid_tops = atmos_utils.find_cloud_tops(is_liquid)
36
36
  top_above = utils.n_elements(obs.height, limit)
37
37
  for prof, top in zip(*np.where(liquid_tops), strict=True):
38
38
  ind = _find_ind_above_top(is_freezing[prof, top:], top_above)
@@ -3,7 +3,7 @@
3
3
  import numpy as np
4
4
  from numpy import ma
5
5
 
6
- from cloudnetpy.categorize import atmos
6
+ from cloudnetpy.categorize import atmos_utils
7
7
  from cloudnetpy.categorize.containers import ClassData
8
8
  from cloudnetpy.constants import T0
9
9
 
@@ -96,8 +96,8 @@ def _fix_liquid_dominated_radar(
96
96
  """Radar signals inside liquid clouds are NOT ice if Z is
97
97
  increasing in height inside the cloud.
98
98
  """
99
- liquid_bases = atmos.find_cloud_bases(is_liquid)
100
- liquid_tops = atmos.find_cloud_tops(is_liquid)
99
+ liquid_bases = atmos_utils.find_cloud_bases(is_liquid)
100
+ liquid_tops = atmos_utils.find_cloud_tops(is_liquid)
101
101
  base_indices = np.where(liquid_bases)
102
102
  top_indices = np.where(liquid_tops)
103
103
 
@@ -35,7 +35,7 @@ def find_freezing_region(obs: ClassData, melting_layer: np.ndarray) -> np.ndarra
35
35
 
36
36
  """
37
37
  is_freezing = np.zeros(obs.tw.shape, dtype=bool)
38
- t0_alt = _find_t0_alt(obs.tw, obs.height)
38
+ t0_alt = find_t0_alt(obs.tw, obs.height)
39
39
  mean_melting_alt = _find_mean_melting_alt(obs, melting_layer)
40
40
 
41
41
  if _is_all_freezing(mean_melting_alt, t0_alt, obs.height):
@@ -78,7 +78,7 @@ def _find_mean_melting_alt(obs: ClassData, melting_layer: np.ndarray) -> ma.Mask
78
78
  return ma.median(melting_alts, axis=1)
79
79
 
80
80
 
81
- def _find_t0_alt(temperature: np.ndarray, height: np.ndarray) -> np.ndarray:
81
+ def find_t0_alt(temperature: np.ndarray, height: np.ndarray) -> np.ndarray:
82
82
  """Interpolates altitudes where temperature goes below freezing.
83
83
 
84
84
  Args: