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
@@ -1,9 +1,12 @@
1
1
  import importlib
2
2
  import logging
3
+ from os import PathLike
3
4
 
4
5
  import numpy as np
6
+ import numpy.typing as npt
7
+ import scipy.special
8
+ import scipy.stats
5
9
  from numpy import ma
6
- from scipy.special import gamma
7
10
 
8
11
  import cloudnetpy.utils as cl_tools
9
12
  from cloudnetpy.datasource import DataSource
@@ -24,9 +27,9 @@ class AdvanceProductMethods(DataSource):
24
27
  def __init__(
25
28
  self,
26
29
  model_obj: ModelManager,
27
- model_file: str,
30
+ model_file: str | PathLike,
28
31
  obs_obj: ObservationManager,
29
- ):
32
+ ) -> None:
30
33
  super().__init__(model_file)
31
34
  self._obs_obj = obs_obj
32
35
  self.product = obs_obj.obs
@@ -38,70 +41,88 @@ class AdvanceProductMethods(DataSource):
38
41
  self._model_height = model_obj.data[model_obj.keys["height"]][:]
39
42
  self.generate_products()
40
43
 
41
- def generate_products(self):
42
- cls = getattr(importlib.import_module(__name__), "AdvanceProductMethods")
44
+ def generate_products(self) -> None:
45
+ cls = importlib.import_module(__name__).AdvanceProductMethods
43
46
  try:
44
47
  name = f"get_advance_{self.product}"
45
48
  getattr(cls, name)(self)
46
49
  except AttributeError as error:
47
- logging.warning(f"No advance method for {self.product}: {error}")
50
+ logging.debug("No advance method for %s: %s", self.product, error)
48
51
 
49
- def get_advance_cf(self):
52
+ def get_advance_cf(self) -> None:
50
53
  self.cf_cirrus_filter()
51
54
 
52
- def cf_cirrus_filter(self):
55
+ def cf_cirrus_filter(self) -> None:
53
56
  cf = self.getvar_from_object("cf")
54
57
  h = self.getvar_from_object("h")
55
58
  temperature = self.getvar("temperature")
56
59
  t_screened = self.remove_extra_levels(temperature - 273.15)
57
- iwc, lwc = [self._model_obj.get_water_continent(var) for var in ["iwc", "lwc"]]
60
+ iwc, lwc = (self._model_obj.get_water_content(var) for var in ["iwc", "lwc"])
58
61
  tZT, tT, tZ, t = self.set_frequency_parameters()
59
62
  z_sen = self.fit_z_sensitivity(h)
60
63
  cf_filtered = self.filter_high_iwc_low_cf(cf, iwc, lwc)
61
64
  cloud_iwc, ice_ind = self.find_ice_in_clouds(cf_filtered, iwc, lwc)
62
65
  variance_iwc = self.iwc_variance(h, ice_ind)
63
- # Looks suspicious, check me:
64
- for i, ind in enumerate(zip(ice_ind[0], ice_ind[-1])):
65
- iwc_dist = self.calculate_iwc_distribution(cloud_iwc[i], variance_iwc[i])
66
+
67
+ for i, ind in enumerate(zip(ice_ind[0], ice_ind[-1], strict=True)):
68
+ try:
69
+ iwc_dist = self.calculate_iwc_distribution(
70
+ cloud_iwc[i], variance_iwc[i]
71
+ )
72
+ except ValueError:
73
+ continue
74
+
66
75
  p_iwc = self.gamma_distribution(iwc_dist, variance_iwc[i], cloud_iwc[i])
76
+
77
+ if np.isinf(p_iwc).any():
78
+ cf_filtered[ind] = ma.masked
79
+ continue
80
+
67
81
  if np.sum(p_iwc) == 0 or p_iwc[-1] > 0.01 * np.sum(p_iwc):
68
- cf_filtered[ind] = np.nan
82
+ cf_filtered[ind] = ma.masked
69
83
  continue
70
- obs_index = self.get_observation_index(
71
- iwc_dist,
72
- tZT,
73
- tT,
74
- tZ,
75
- t,
76
- float(t_screened[ind]),
77
- float(z_sen[ind]),
84
+
85
+ min_iwc = 10 ** (
86
+ tZT * z_sen[ind] * t_screened[ind]
87
+ + tT * t_screened[ind]
88
+ + tZ * z_sen[ind]
89
+ + t
78
90
  )
91
+ obs_index = iwc_dist > min_iwc
79
92
  cf_filtered[ind] = self.filter_cirrus(p_iwc, obs_index, cf_filtered[ind])
93
+
80
94
  cf_filtered[cf_filtered < 0.05] = ma.masked
95
+
81
96
  self._model_obj.append_data(
82
97
  cf_filtered,
83
98
  f"{self._model_obj.model}{self._model_obj.cycle}_cf_cirrus",
84
99
  )
85
100
 
86
- def getvar_from_object(self, arg: str) -> np.ndarray:
101
+ def getvar_from_object(self, arg: str) -> npt.NDArray:
87
102
  v_name = arg if arg == "cf" else self._model_obj.get_model_var_names((arg,))[0]
88
103
  key = f"{self._model_obj.model}{self._model_obj.cycle}_{v_name}"
89
104
  return self._model_obj.data[key][:]
90
105
 
91
- def remove_extra_levels(self, arg: np.ndarray) -> np.ndarray:
106
+ def remove_extra_levels(self, arg: npt.NDArray) -> npt.NDArray:
92
107
  return self._model_obj.cut_off_extra_levels(arg)
93
108
 
94
109
  def set_frequency_parameters(self) -> tuple:
95
- assert self._obs_obj.radar_freq is not None
110
+ if self._obs_obj.radar_freq is None:
111
+ msg = "No radar frequency in observation file"
112
+ raise ValueError(msg)
96
113
  if 30 <= self._obs_obj.radar_freq <= 40:
97
114
  return 0.000242, -0.0186, 0.0699, -1.63
98
115
  if 90 <= float(self._obs_obj.radar_freq) <= 100:
99
116
  return 0.00058, -0.00706, 0.0923, -0.992
100
117
  raise ValueError
101
118
 
102
- def fit_z_sensitivity(self, h: np.ndarray) -> np.ndarray:
103
- assert self._obs_obj.z_sensitivity is not None
104
- assert self._obs_obj.height is not None
119
+ def fit_z_sensitivity(self, h: npt.NDArray) -> npt.NDArray:
120
+ if self._obs_obj.z_sensitivity is None:
121
+ msg = "No z_sensitivity in observation file"
122
+ raise ValueError(msg)
123
+ if self._obs_obj.height is None:
124
+ msg = "No height in observation file"
125
+ raise ValueError(msg)
105
126
  z_sen = [
106
127
  cl_tools.rebin_1d(self._obs_obj.height, self._obs_obj.z_sensitivity, h[i])
107
128
  for i in range(len(h))
@@ -109,46 +130,58 @@ class AdvanceProductMethods(DataSource):
109
130
  return np.asarray(z_sen)
110
131
 
111
132
  def filter_high_iwc_low_cf(
112
- self, cf: np.ndarray, iwc: np.ndarray, lwc: np.ndarray
113
- ) -> np.ndarray:
133
+ self,
134
+ cf: npt.NDArray,
135
+ iwc: npt.NDArray,
136
+ lwc: npt.NDArray,
137
+ ) -> npt.NDArray:
114
138
  cf_filtered = self.mask_weird_indices(cf, iwc, lwc)
115
139
  if np.sum((iwc > 0) & (lwc < iwc / 10) & (cf_filtered > 0)) == 0:
116
- raise ValueError("No ice clouds in a input data")
140
+ msg = "No ice clouds in a input data"
141
+ raise ValueError(msg)
117
142
  return cf_filtered
118
143
 
119
144
  @staticmethod
120
145
  def mask_weird_indices(
121
- cf: np.ndarray, iwc: np.ndarray, lwc: np.ndarray
122
- ) -> np.ndarray:
123
- cf_filtered = np.copy(cf)
146
+ cf: npt.NDArray,
147
+ iwc: npt.NDArray,
148
+ lwc: npt.NDArray,
149
+ ) -> npt.NDArray:
150
+ cf_filtered = ma.copy(cf)
124
151
  weird_ind = (iwc / cf > 0.5e-3) & (cf < 0.001)
125
152
  weird_ind = weird_ind | (iwc == 0) & (lwc == 0) & (cf == 0)
126
153
  cf_filtered[weird_ind] = ma.masked
127
154
  return cf_filtered
128
155
 
129
156
  def find_ice_in_clouds(
130
- self, cf_filtered: np.ndarray, iwc: np.ndarray, lwc: np.ndarray
131
- ) -> tuple[np.ndarray, tuple]:
157
+ self,
158
+ cf_filtered: npt.NDArray,
159
+ iwc: npt.NDArray,
160
+ lwc: npt.NDArray,
161
+ ) -> tuple[npt.NDArray, tuple]:
132
162
  ice_ind = self.get_ice_indices(cf_filtered, iwc, lwc)
133
163
  cloud_iwc = iwc[ice_ind] / cf_filtered[ice_ind] * 1e3
134
164
  return cloud_iwc, ice_ind
135
165
 
136
166
  @staticmethod
137
167
  def get_ice_indices(
138
- cf_filtered: np.ndarray, iwc: np.ndarray, lwc: np.ndarray
168
+ cf_filtered: npt.NDArray,
169
+ iwc: npt.NDArray,
170
+ lwc: npt.NDArray,
139
171
  ) -> tuple:
140
172
  return tuple(np.where((cf_filtered > 0) & (iwc > 0) & (lwc < iwc / 10)))
141
173
 
142
- def iwc_variance(self, height: np.ndarray, ice_ind: tuple) -> np.ndarray:
174
+ def iwc_variance(self, height: npt.NDArray, ice_ind: tuple) -> npt.NDArray:
143
175
  u = self.getvar("uwind")
144
176
  v = self.getvar("vwind")
145
177
  u = self.remove_extra_levels(u)
146
178
  v = self.remove_extra_levels(v)
147
179
  w_shear = self.calculate_wind_shear(self._model_obj.wind, u, v, height)
148
- variance_iwc = self.calculate_variance_iwc(w_shear, ice_ind)
149
- return variance_iwc
180
+ return self.calculate_variance_iwc(w_shear, ice_ind)
150
181
 
151
- def calculate_variance_iwc(self, w_shear: np.ndarray, ice_ind: tuple) -> np.ndarray:
182
+ def calculate_variance_iwc(
183
+ self, w_shear: npt.NDArray, ice_ind: tuple
184
+ ) -> npt.NDArray:
152
185
  return 10 ** (
153
186
  0.3 * np.log10(self._model_obj.resolution_h)
154
187
  - 0.04 * w_shear[ice_ind]
@@ -157,8 +190,11 @@ class AdvanceProductMethods(DataSource):
157
190
 
158
191
  @staticmethod
159
192
  def calculate_wind_shear(
160
- wind, u: np.ndarray, v: np.ndarray, height: np.ndarray
161
- ) -> np.ndarray:
193
+ wind: npt.NDArray,
194
+ u: npt.NDArray,
195
+ v: npt.NDArray,
196
+ height: npt.NDArray,
197
+ ) -> npt.NDArray:
162
198
  grand_winds = []
163
199
  for w in (wind, u, v):
164
200
  grad_w = np.zeros(w.shape)
@@ -179,8 +215,10 @@ class AdvanceProductMethods(DataSource):
179
215
  f_variance_iwc: float,
180
216
  n_std: int = 5,
181
217
  n_dist: int = 250,
182
- ) -> np.ndarray:
183
- finish = cloud_iwc + n_std * (np.sqrt(f_variance_iwc) * cloud_iwc)
218
+ ) -> npt.NDArray:
219
+ finish = cloud_iwc + n_std * (ma.sqrt(f_variance_iwc) * cloud_iwc)
220
+ if isinstance(finish, ma.MaskedArray) and finish.mask.all():
221
+ raise ValueError
184
222
  iwc_dist = np.arange(0, finish, finish / (n_dist - 1))
185
223
  if cloud_iwc < iwc_dist[2]:
186
224
  finish = cloud_iwc * 10
@@ -189,45 +227,16 @@ class AdvanceProductMethods(DataSource):
189
227
 
190
228
  @staticmethod
191
229
  def gamma_distribution(
192
- iwc_dist: np.ndarray, f_variance_iwc: float, cloud_iwc: float
193
- ) -> np.ndarray:
194
- def calculate_gamma_dist():
195
- alpha = 1 / f_variance_iwc
196
- return (
197
- 1
198
- / gamma(alpha)
199
- * (alpha / cloud_iwc) ** alpha
200
- * iwc_dist[i] ** (alpha - 1)
201
- * ma.exp(-(alpha * iwc_dist[i] / cloud_iwc))
202
- )
203
-
204
- p_iwc = np.zeros(iwc_dist.shape)
205
- for i in range(len(iwc_dist)):
206
- p_iwc[i] = calculate_gamma_dist()
207
- return p_iwc
208
-
209
- @staticmethod
210
- def get_observation_index(
211
- iwc_dist: np.ndarray,
212
- tZT: float,
213
- tT: float,
214
- tZ: float,
215
- t: np.ndarray,
216
- temperature: float,
217
- z_sen: float,
218
- ) -> np.ndarray:
219
- def calculate_min_iwc():
220
- min_iwc = 10 ** (
221
- tZT * z_sen * temperature + tT * temperature + tZ * z_sen + t
222
- )
223
- return min_iwc
224
-
225
- iwc_min = calculate_min_iwc()
226
- obs_index = iwc_dist > iwc_min
227
- return obs_index
230
+ iwc_dist: npt.NDArray, f_variance_iwc: float, cloud_iwc: float
231
+ ) -> npt.NDArray:
232
+ alpha = 1 / f_variance_iwc
233
+ theta = cloud_iwc / alpha
234
+ return scipy.stats.gamma.pdf(iwc_dist, a=alpha, scale=theta)
228
235
 
229
236
  @staticmethod
230
237
  def filter_cirrus(
231
- p_iwc: np.ndarray, obs_index: np.ndarray, cf_filtered: np.ndarray
232
- ) -> np.ndarray:
233
- return (np.sum(p_iwc * obs_index) / np.sum(p_iwc)) * cf_filtered
238
+ p_iwc: npt.NDArray,
239
+ obs_index: npt.NDArray,
240
+ cf_filtered: npt.NDArray,
241
+ ) -> npt.NDArray:
242
+ return (ma.sum(p_iwc * obs_index) / ma.sum(p_iwc)) * cf_filtered
@@ -1,4 +1,5 @@
1
1
  import numpy as np
2
+ import numpy.typing as npt
2
3
  from numpy import ma
3
4
 
4
5
  from cloudnetpy import utils
@@ -13,12 +14,13 @@ class ProductGrid:
13
14
  Args:
14
15
  model_obj (object): The :class:'ModelManager' object.
15
16
  obs_obj (object): The :class:'ObservationManager' object.
17
+
16
18
  Notes:
17
19
  Downsampled observation products data is added to a ModelManager
18
20
  object which is used for nc-file creation and writing
19
21
  """
20
22
 
21
- def __init__(self, model_obj: ModelManager, obs_obj: ObservationManager):
23
+ def __init__(self, model_obj: ModelManager, obs_obj: ObservationManager) -> None:
22
24
  self._obs_obj = obs_obj
23
25
  self._date = obs_obj.date
24
26
  self._obs_time = tl.time2datetime(obs_obj.time, self._date)
@@ -28,13 +30,15 @@ class ProductGrid:
28
30
  self._model_time = model_obj.time
29
31
  self._model_height = model_obj.data[model_obj.keys["height"]][:]
30
32
  self._time_adv = tl.calculate_advection_time(
31
- int(model_obj.resolution_h), ma.array(model_obj.wind), 1
33
+ int(model_obj.resolution_h),
34
+ ma.array(model_obj.wind),
35
+ 1,
32
36
  )
33
37
  time_steps = utils.binvec(self._model_time)
34
38
  self._time_steps = tl.time2datetime(time_steps, self._date)
35
39
  self._generate_downsample_product()
36
40
 
37
- def _generate_downsample_product(self):
41
+ def _generate_downsample_product(self) -> None:
38
42
  """Downsampling products are generated with different averaging methods
39
43
  for a selected size of model time-height window.
40
44
  """
@@ -42,7 +46,8 @@ class ProductGrid:
42
46
  model_t = tl.time2datetime(self._model_time, self._date)
43
47
  for i in range(len(self._time_steps) - 1):
44
48
  x_ind = tl.get_1d_indices(
45
- (self._time_steps[i], self._time_steps[i + 1]), self._obs_time
49
+ (self._time_steps[i], self._time_steps[i + 1]),
50
+ self._obs_time,
46
51
  )
47
52
  if self._obs_obj.obs == "iwc":
48
53
  x_ind_no_rain = tl.get_1d_indices(
@@ -53,10 +58,13 @@ class ProductGrid:
53
58
  y_steps = tl.rebin_edges(self._model_height[i])
54
59
  for j in range(len(y_steps) - 1):
55
60
  x_ind_adv = tl.get_adv_indices(
56
- model_t[i], self._time_adv[i, j], self._obs_time
61
+ model_t[i],
62
+ self._time_adv[i, j],
63
+ self._obs_time,
57
64
  )
58
65
  y_ind = tl.get_1d_indices(
59
- (y_steps[j], y_steps[j + 1]), self._obs_height
66
+ (y_steps[j], y_steps[j + 1]),
67
+ self._obs_height,
60
68
  )
61
69
  ind = np.outer(x_ind, y_ind)
62
70
  ind_avd = np.outer(x_ind_adv, y_ind)
@@ -66,7 +74,9 @@ class ProductGrid:
66
74
  continue
67
75
  product_dict = self._regrid_cf(product_dict, i, j, data)
68
76
  data_adv = self._reshape_data_to_window(ind_avd, x_ind_adv, y_ind)
69
- assert data_adv is not None
77
+ if data_adv is None:
78
+ msg = "No data for advection"
79
+ raise RuntimeError(msg)
70
80
  product_adv_dict = self._regrid_cf(product_adv_dict, i, j, data_adv)
71
81
  elif self._obs_obj.obs == "iwc":
72
82
  x_ind_no_rain_adv = tl.get_adv_indices(
@@ -78,19 +88,30 @@ class ProductGrid:
78
88
  ind_no_rain = np.outer(x_ind_no_rain, y_ind)
79
89
  ind_no_rain_adv = np.outer(x_ind_no_rain_adv, y_ind)
80
90
  product_dict = self._regrid_iwc(
81
- product_dict, i, j, ind, ind_no_rain
91
+ product_dict,
92
+ i,
93
+ j,
94
+ ind,
95
+ ind_no_rain,
82
96
  )
83
97
  product_adv_dict = self._regrid_iwc(
84
- product_adv_dict, i, j, ind_avd, ind_no_rain_adv
98
+ product_adv_dict,
99
+ i,
100
+ j,
101
+ ind_avd,
102
+ ind_no_rain_adv,
85
103
  )
86
104
  else:
87
105
  product_dict = self._regrid_product(product_dict, i, j, ind)
88
106
  product_adv_dict = self._regrid_product(
89
- product_adv_dict, i, j, ind_avd
107
+ product_adv_dict,
108
+ i,
109
+ j,
110
+ ind_avd,
90
111
  )
91
112
  self._append_data2object([product_dict, product_adv_dict])
92
113
 
93
- def _get_method_storage(self):
114
+ def _get_method_storage(self) -> tuple[dict, dict]:
94
115
  if self._obs_obj.obs == "cf":
95
116
  return self._cf_method_storage()
96
117
  if self._obs_obj.obs == "iwc":
@@ -99,95 +120,99 @@ class ProductGrid:
99
120
 
100
121
  def _cf_method_storage(self) -> tuple[dict, dict]:
101
122
  cf_dict = {
102
- "cf_V": np.zeros(self._model_height.shape),
103
- "cf_A": np.zeros(self._model_height.shape),
123
+ "cf_V": ma.zeros(self._model_height.shape),
124
+ "cf_A": ma.zeros(self._model_height.shape),
104
125
  }
105
126
  cf_adv_dict = {
106
- "cf_V_adv": np.zeros(self._model_height.shape),
107
- "cf_A_adv": np.zeros(self._model_height.shape),
127
+ "cf_V_adv": ma.zeros(self._model_height.shape),
128
+ "cf_A_adv": ma.zeros(self._model_height.shape),
108
129
  }
109
130
  return cf_dict, cf_adv_dict
110
131
 
111
132
  def _iwc_method_storage(self) -> tuple[dict, dict]:
112
133
  iwc_dict = {
113
- "iwc": np.zeros(self._model_height.shape),
114
- "iwc_att": np.zeros(self._model_height.shape),
115
- "iwc_rain": np.zeros(self._model_height.shape),
134
+ "iwc": ma.zeros(self._model_height.shape),
135
+ "iwc_att": ma.zeros(self._model_height.shape),
136
+ "iwc_rain": ma.zeros(self._model_height.shape),
116
137
  }
117
138
  iwc_adv_dict = {
118
- "iwc_adv": np.zeros(self._model_height.shape),
119
- "iwc_att_adv": np.zeros(self._model_height.shape),
120
- "iwc_rain_adv": np.zeros(self._model_height.shape),
139
+ "iwc_adv": ma.zeros(self._model_height.shape),
140
+ "iwc_att_adv": ma.zeros(self._model_height.shape),
141
+ "iwc_rain_adv": ma.zeros(self._model_height.shape),
121
142
  }
122
143
  return iwc_dict, iwc_adv_dict
123
144
 
124
145
  def _product_method_storage(self) -> tuple[dict, dict]:
125
- product_dict = {f"{self._obs_obj.obs}": np.zeros(self._model_height.shape)}
146
+ product_dict = {f"{self._obs_obj.obs}": ma.zeros(self._model_height.shape)}
126
147
  product_adv_dict = {
127
- f"{self._obs_obj.obs}_adv": np.zeros(self._model_height.shape)
148
+ f"{self._obs_obj.obs}_adv": ma.zeros(self._model_height.shape),
128
149
  }
129
150
  return product_dict, product_adv_dict
130
151
 
131
- @staticmethod
132
- def _regrid_cf(storage: dict, i: int, j: int, data: np.ndarray | None) -> dict:
133
- """Calculates average cloud fraction value to grid point"""
134
- for key, downsample in storage.items():
135
- if data is not None:
136
- downsample[i, j] = np.nanmean(data)
137
- if "_A" in key:
138
- downsample[i, j] = tl.average_column_sum(data)
139
- else:
140
- downsample[i, j] = np.nan
141
- storage[key] = downsample
142
- return storage
143
-
144
152
  def _reshape_data_to_window(
145
- self, ind: np.ndarray, x_ind: np.ndarray, y_ind: np.ndarray
146
- ) -> np.ndarray | None:
147
- """Reshapes True observation values to windows shape"""
153
+ self,
154
+ ind: npt.NDArray,
155
+ x_ind: npt.NDArray,
156
+ y_ind: npt.NDArray,
157
+ ) -> npt.NDArray | None:
158
+ """Reshapes True observation values to windows shape."""
148
159
  window_size = tl.get_obs_window_size(x_ind, y_ind)
149
160
  if window_size is not None:
150
161
  return self._obs_data[ind].reshape(window_size)
151
162
  return None
152
163
 
164
+ @staticmethod
165
+ def _regrid_cf(storage: dict, i: int, j: int, data: npt.NDArray) -> dict:
166
+ """Calculates average cloud fraction value to grid point."""
167
+ data_ma = ma.array(data) if not isinstance(data, ma.MaskedArray) else data
168
+ for key, downsample in storage.items():
169
+ downsample[i, j] = ma.mean(data_ma)
170
+ if "_A" in key and not data_ma.mask.all():
171
+ downsample[i, j] = tl.average_column_sum(data_ma)
172
+ storage[key] = downsample
173
+ return storage
174
+
153
175
  def _regrid_iwc(
154
176
  self,
155
177
  storage: dict,
156
178
  i: int,
157
179
  j: int,
158
- ind_rain: np.ndarray,
159
- ind_no_rain: np.ndarray,
180
+ ind_rain: npt.NDArray,
181
+ ind_no_rain: npt.NDArray,
160
182
  ) -> dict:
161
- """Calculates average iwc value for grid point"""
162
- for key, downsample in storage.items():
163
- if not self._obs_data[ind_no_rain].mask.all():
164
- downsample[i, j] = np.nanmean(self._obs_data[ind_no_rain])
165
- elif "rain" in key and not self._obs_data[ind_rain].mask.all():
166
- downsample[i, j] = np.nanmean(self._obs_data[ind_rain])
167
- else:
168
- downsample[i, j] = np.nan
183
+ """Calculates average iwc value for each grid point."""
184
+ for key, down_sample in storage.items():
185
+ down_sample[i, j] = ma.masked
186
+ if "rain" not in key:
187
+ no_rain_data = self._obs_data[ind_no_rain]
188
+ if ind_no_rain.any() and not no_rain_data.mask.all():
189
+ down_sample[i, j] = ma.mean(no_rain_data)
190
+ if "rain" in key:
191
+ rain_data = self._obs_data[ind_rain]
192
+ if ind_rain.any() and not rain_data.mask.all():
193
+ down_sample[i, j] = ma.mean(rain_data)
169
194
  if "att" in key:
170
- iwc_att = self._obs_obj.data["iwc_att"][:]
171
- if iwc_att[ind_no_rain].mask.all():
172
- downsample[i, j] = np.nan
173
- else:
174
- downsample[i, j] = np.nanmean(iwc_att[ind_no_rain])
175
- storage[key] = downsample
195
+ no_rain_att_data = self._obs_obj.data["iwc_att"][ind_no_rain]
196
+ if ind_no_rain.any() and not no_rain_att_data.mask.all():
197
+ down_sample[i, j] = ma.mean(no_rain_att_data)
198
+ storage[key] = down_sample
176
199
  return storage
177
200
 
178
- def _regrid_product(self, storage: dict, i: int, j: int, ind: np.ndarray) -> dict:
179
- """Calculates average of standard product value to grid point"""
201
+ def _regrid_product(self, storage: dict, i: int, j: int, ind: npt.NDArray) -> dict:
202
+ """Calculates average of standard product value for each grid point."""
180
203
  for key, down_sample in storage.items():
181
- if not self._obs_data[ind].mask.all() and ind.any():
182
- down_sample[i, j] = np.nanmean(self._obs_data[ind])
183
- else:
184
- down_sample[i, j] = np.nan
204
+ obs_data_selected = ma.masked_invalid(self._obs_data[ind])
205
+ down_sample[i, j] = (
206
+ ma.mean(obs_data_selected)
207
+ if not obs_data_selected.mask.all()
208
+ else ma.masked
209
+ )
185
210
  storage[key] = down_sample
186
211
  return storage
187
212
 
188
- def _append_data2object(self, data_storage: list):
213
+ def _append_data2object(self, data_storage: list) -> None:
189
214
  for storage in data_storage:
190
- for key in storage.keys():
215
+ for key in storage:
191
216
  down_sample = storage[key]
192
217
  self.model_obj.append_data(
193
218
  down_sample,