cloudnetpy 1.88.2__tar.gz → 1.89.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. {cloudnetpy-1.88.2/cloudnetpy.egg-info → cloudnetpy-1.89.0}/PKG-INFO +1 -1
  2. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/classify.py +22 -0
  3. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/model.py +22 -1
  4. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/radar.py +66 -63
  5. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/cli.py +69 -10
  6. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/mira.py +22 -0
  7. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/nc_radar.py +13 -13
  8. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/plotting/plot_meta.py +2 -2
  9. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/plotting/plotting.py +2 -0
  10. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/plotting/plot_meta.py +9 -5
  11. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/plotting/plotting.py +28 -12
  12. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/products/classification.py +5 -1
  13. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/utils.py +14 -2
  14. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/version.py +2 -2
  15. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0/cloudnetpy.egg-info}/PKG-INFO +1 -1
  16. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/LICENSE +0 -0
  17. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/MANIFEST.in +0 -0
  18. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/README.md +0 -0
  19. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/__init__.py +0 -0
  20. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/__init__.py +0 -0
  21. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/atmos_utils.py +0 -0
  22. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/attenuation.py +0 -0
  23. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/attenuations/__init__.py +0 -0
  24. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/attenuations/gas_attenuation.py +0 -0
  25. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/attenuations/liquid_attenuation.py +0 -0
  26. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/attenuations/melting_attenuation.py +0 -0
  27. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/attenuations/rain_attenuation.py +0 -0
  28. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/categorize.py +0 -0
  29. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/containers.py +0 -0
  30. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/disdrometer.py +0 -0
  31. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/droplet.py +0 -0
  32. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/falling.py +0 -0
  33. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/freezing.py +0 -0
  34. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/insects.py +0 -0
  35. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/itu.py +0 -0
  36. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/lidar.py +0 -0
  37. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/melting.py +0 -0
  38. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/categorize/mwr.py +0 -0
  39. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/cloudnetarray.py +0 -0
  40. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/concat_lib.py +0 -0
  41. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/constants.py +0 -0
  42. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/datasource.py +0 -0
  43. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/disdronator/__init__.py +0 -0
  44. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/disdronator/lpm.py +0 -0
  45. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/disdronator/parsivel.py +0 -0
  46. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/disdronator/rd80.py +0 -0
  47. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/disdronator/utils.py +0 -0
  48. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/exceptions.py +0 -0
  49. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/__init__.py +0 -0
  50. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/basta.py +0 -0
  51. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/bowtie.py +0 -0
  52. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/ceilo.py +0 -0
  53. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/ceilometer.py +0 -0
  54. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/cl61d.py +0 -0
  55. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/cloudnet_instrument.py +0 -0
  56. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/copernicus.py +0 -0
  57. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/da10.py +0 -0
  58. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/disdrometer/__init__.py +0 -0
  59. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/disdrometer/common.py +0 -0
  60. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/disdrometer/parsivel.py +0 -0
  61. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/disdrometer/rd80.py +0 -0
  62. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/disdrometer/thies.py +0 -0
  63. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/fd12p.py +0 -0
  64. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/galileo.py +0 -0
  65. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/hatpro.py +0 -0
  66. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/instruments.py +0 -0
  67. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/lufft.py +0 -0
  68. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/mrr.py +0 -0
  69. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/nc_lidar.py +0 -0
  70. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/pollyxt.py +0 -0
  71. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/radiometrics.py +0 -0
  72. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/rain_e_h3.py +0 -0
  73. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/rpg.py +0 -0
  74. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/rpg_reader.py +0 -0
  75. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/toa5.py +0 -0
  76. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/vaisala.py +0 -0
  77. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/instruments/weather_station.py +0 -0
  78. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/metadata.py +0 -0
  79. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/__init__.py +0 -0
  80. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/file_handler.py +0 -0
  81. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/metadata.py +0 -0
  82. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/model_metadata.py +0 -0
  83. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/plotting/__init__.py +0 -0
  84. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/plotting/plot_tools.py +0 -0
  85. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/products/__init__.py +0 -0
  86. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/products/advance_methods.py +0 -0
  87. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/products/grid_methods.py +0 -0
  88. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/products/model_products.py +0 -0
  89. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/products/observation_products.py +0 -0
  90. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/products/product_resampling.py +0 -0
  91. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/products/tools.py +0 -0
  92. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/statistics/__init__.py +0 -0
  93. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/statistics/statistical_methods.py +0 -0
  94. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/__init__.py +0 -0
  95. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/e2e/__init__.py +0 -0
  96. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/e2e/conftest.py +0 -0
  97. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/e2e/process_cf/__init__.py +0 -0
  98. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/e2e/process_cf/main.py +0 -0
  99. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/e2e/process_cf/tests.py +0 -0
  100. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/e2e/process_iwc/__init__.py +0 -0
  101. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/e2e/process_iwc/main.py +0 -0
  102. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/e2e/process_iwc/tests.py +0 -0
  103. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/e2e/process_lwc/__init__.py +0 -0
  104. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/e2e/process_lwc/main.py +0 -0
  105. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/e2e/process_lwc/tests.py +0 -0
  106. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/unit/__init__.py +0 -0
  107. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/unit/conftest.py +0 -0
  108. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/unit/test_advance_methods.py +0 -0
  109. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/unit/test_grid_methods.py +0 -0
  110. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/unit/test_model_products.py +0 -0
  111. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/unit/test_observation_products.py +0 -0
  112. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/unit/test_plot_tools.py +0 -0
  113. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/unit/test_plotting.py +0 -0
  114. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/unit/test_statistical_methods.py +0 -0
  115. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/tests/unit/test_tools.py +0 -0
  116. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/model_evaluation/utils.py +0 -0
  117. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/output.py +0 -0
  118. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/plotting/__init__.py +0 -0
  119. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/products/__init__.py +0 -0
  120. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/products/der.py +0 -0
  121. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/products/drizzle.py +0 -0
  122. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/products/drizzle_error.py +0 -0
  123. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/products/drizzle_tools.py +0 -0
  124. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/products/epsilon.py +0 -0
  125. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/products/ier.py +0 -0
  126. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/products/iwc.py +0 -0
  127. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/products/lwc.py +0 -0
  128. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/products/mie_lu_tables.nc +0 -0
  129. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/products/mwr_tools.py +0 -0
  130. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/products/product_tools.py +0 -0
  131. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy/py.typed +0 -0
  132. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy.egg-info/SOURCES.txt +0 -0
  133. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy.egg-info/dependency_links.txt +0 -0
  134. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy.egg-info/entry_points.txt +0 -0
  135. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy.egg-info/requires.txt +0 -0
  136. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/cloudnetpy.egg-info/top_level.txt +0 -0
  137. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/docs/source/conf.py +0 -0
  138. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/pyproject.toml +0 -0
  139. {cloudnetpy-1.88.2 → cloudnetpy-1.89.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudnetpy
3
- Version: 1.88.2
3
+ Version: 1.89.0
4
4
  Summary: Python package for Cloudnet processing
5
5
  Author: Simo Tukiainen
6
6
  License: MIT License
@@ -91,6 +91,7 @@ def classify_measurements(data: Observations) -> ClassificationResult:
91
91
  bits.aerosol = _find_aerosols(obs, bits)
92
92
  bits.aerosol[filtered_ice] = False
93
93
  _fix_super_cold_liquid(obs, bits)
94
+ _reclassify_ice_with_high_ldr(obs, bits)
94
95
 
95
96
  return ClassificationResult(
96
97
  category_bits=bits,
@@ -122,6 +123,27 @@ def fetch_quality(
122
123
  )
123
124
 
124
125
 
126
+ def _reclassify_ice_with_high_ldr(obs: ClassData, bits: CategoryBits) -> None:
127
+ """Reclassifies ice pixels with high LDR as insects.
128
+
129
+ Ice particles typically have LDR below -13 dB. Higher values in the
130
+ freezing region (excluding the melting layer) indicate insects, which
131
+ can be present in temperatures down to about -10 C.
132
+ """
133
+ if not hasattr(obs, "ldr"):
134
+ return
135
+ ldr_limit = -13
136
+ temp_limit = T0 - 10
137
+ is_ice = bits.falling & bits.freezing & ~bits.droplet & ~bits.melting
138
+ high_ldr = ~ma.getmaskarray(obs.ldr) & (obs.ldr > ldr_limit)
139
+ warm_enough = obs.tw > temp_limit
140
+ reclassify = is_ice & high_ldr & warm_enough
141
+ bits.insect[reclassify] = True
142
+ bits.falling[reclassify] = False
143
+ has_lidar = ~obs.beta.mask
144
+ bits.aerosol[reclassify & has_lidar] = True
145
+
146
+
125
147
  def _fix_super_cold_liquid(obs: ClassData, bits: CategoryBits) -> None:
126
148
  """Supercooled liquid droplets do not exist in atmosphere below around -38 C."""
127
149
  t_limit = T0 - 38
@@ -16,6 +16,7 @@ from cloudnetpy.categorize.itu import (
16
16
  calc_saturation_vapor_pressure,
17
17
  )
18
18
  from cloudnetpy.cloudnetarray import CloudnetArray
19
+ from cloudnetpy.constants import G
19
20
  from cloudnetpy.datasource import DataSource
20
21
  from cloudnetpy.exceptions import ModelDataError
21
22
 
@@ -115,6 +116,7 @@ class Model(DataSource):
115
116
  array,
116
117
  time_grid,
117
118
  half_height if "atten" in key else height_grid,
119
+ extrapolate=True,
118
120
  )
119
121
  self.height = height_grid
120
122
  return utils.find_masked_profiles_indices(self.data_dense["temperature"])
@@ -144,7 +146,26 @@ class Model(DataSource):
144
146
  except KeyError as err:
145
147
  msg = "No 'height' variable in the model file."
146
148
  raise ModelDataError(msg) from err
147
- return self.to_m(model_heights) + alt_site
149
+ surface_altitude = self._get_model_surface_altitude(alt_site)
150
+ return self.to_m(model_heights) + surface_altitude
151
+
152
+ def _get_model_surface_altitude(self, alt_site: float) -> float | npt.NDArray:
153
+ """Returns model surface altitude from geopotential if available.
154
+
155
+ For sites in complex terrain (e.g. mountains), the model grid cell
156
+ surface height can differ significantly from the actual site altitude.
157
+ Using the model's own surface height ensures that thermodynamic fields
158
+ are placed at their physically correct absolute heights.
159
+
160
+ Note: Model surface altitude might be higher than the site altitude.
161
+ """
162
+ if "sfc_height" in self.dataset.variables:
163
+ sfc_height = self.dataset.variables["sfc_height"][:]
164
+ return sfc_height[:, np.newaxis]
165
+ if "sfc_geopotential" in self.dataset.variables:
166
+ geopotential = self.dataset.variables["sfc_geopotential"][:]
167
+ return geopotential[:, np.newaxis] / G
168
+ return alt_site
148
169
 
149
170
  def calc_attenuations(self, frequency: float) -> None:
150
171
  temperature = self.getvar("temperature")
@@ -123,26 +123,12 @@ class Radar(DataSource):
123
123
  if variable not in self.data:
124
124
  return
125
125
  data = ma.copy(self.data[variable][:])
126
- n_points_in_profiles = ma.count(data, axis=1)
127
- n_profiles_with_data = np.count_nonzero(n_points_in_profiles)
126
+ n_profiles_with_data = np.count_nonzero(ma.count(data, axis=1))
128
127
  if n_profiles_with_data < 300:
129
128
  return
130
- n_vertical = self._filter(
131
- data,
132
- axis=1,
133
- min_coverage=0.5,
134
- z_limit=10,
135
- distance=4,
136
- n_blocks=100,
137
- )
138
- n_horizontal = self._filter(
139
- data,
140
- axis=0,
141
- min_coverage=0.3,
142
- z_limit=-30,
143
- distance=3,
144
- n_blocks=20,
145
- )
129
+
130
+ n_vertical = self._filter_stripes_axis(data, variable, axis=1)
131
+ n_horizontal = self._filter_stripes_axis(data, variable, axis=0)
146
132
  if n_vertical > 0 or n_horizontal > 0:
147
133
  logging.debug(
148
134
  "Filtered %s vertical and %s horizontal stripes "
@@ -152,57 +138,74 @@ class Radar(DataSource):
152
138
  variable,
153
139
  )
154
140
 
155
- def _filter(
141
+ def _filter_stripes_axis(
156
142
  self,
157
143
  data: npt.NDArray,
144
+ variable: str,
158
145
  axis: int,
159
- min_coverage: float,
160
- z_limit: float,
161
- distance: float,
162
- n_blocks: int,
163
146
  ) -> int:
164
- if axis == 0:
165
- data = data.T
166
- echo = self.data["Z"][:].T
167
- else:
168
- echo = self.data["Z"][:]
169
-
170
- len_block = int(np.floor(data.shape[0] / n_blocks))
171
- block_indices = np.arange(len_block)
172
- n_removed_total = 0
173
-
174
- for block_number in range(n_blocks):
175
- data_block = data[block_indices, :]
176
- n_values = ma.count(data_block, axis=1)
177
- try:
178
- q1 = np.quantile(n_values, 0.25)
179
- q3 = np.quantile(n_values, 0.75)
180
- except IndexError:
181
- continue
182
-
183
- if q1 == q3:
184
- continue
185
-
186
- threshold = distance * (q3 - q1) + q3
187
-
188
- indices = np.where(
189
- (n_values > threshold) & (n_values > (min_coverage * data.shape[1])),
190
- )[0]
191
- true_ind = [int(x) for x in (block_number * len_block + indices)]
192
- n_removed = len(indices)
193
-
194
- if n_removed > 5:
147
+ n_values = ma.count(data, axis=axis)
148
+ n = len(n_values)
149
+ margin = 5
150
+
151
+ spike_indices: set[int] = set()
152
+ for i in range(margin, n - margin):
153
+ window = [n_values[i + j] for j in range(-margin, margin + 1) if j != 0]
154
+ q1, q3 = np.percentile(window, [25, 75])
155
+ iqr = q3 - q1
156
+ if iqr == 0 and q3 > 0:
195
157
  continue
158
+ threshold = q3 + 3 * max(iqr, 1)
159
+ if n_values[i] > max(threshold, 20):
160
+ spike_indices.add(i)
161
+
162
+ for ind in sorted(spike_indices):
163
+ below = ind - 1
164
+ while below >= 0 and below in spike_indices:
165
+ below -= 1
166
+ above = ind + 1
167
+ while above < n and above in spike_indices:
168
+ above += 1
169
+ self._mask_stripe_pixels(variable, ind, axis, below, above, n)
170
+
171
+ return len(spike_indices)
172
+
173
+ def _mask_stripe_pixels(
174
+ self,
175
+ variable: str,
176
+ ind: int,
177
+ axis: int,
178
+ below: int,
179
+ above: int,
180
+ dim_size: int,
181
+ ) -> None:
182
+ self._mask_isolated("v", "Z", ind, axis, below, above, dim_size)
183
+ if variable != "v" and variable in self.data:
184
+ self._mask_isolated(variable, variable, ind, axis, below, above, dim_size)
196
185
 
197
- if n_removed > 0:
198
- n_removed_total += n_removed
199
- for ind in true_ind:
200
- ind2 = np.where(echo[ind, :] < z_limit)
201
- bad_indices = (ind, ind2) if axis == 1 else (ind2, ind)
202
- self.data["v"][:][bad_indices] = ma.masked
203
- block_indices += len_block
204
-
205
- return n_removed_total
186
+ def _mask_isolated(
187
+ self,
188
+ target: str,
189
+ source: str,
190
+ ind: int,
191
+ axis: int,
192
+ below: int,
193
+ above: int,
194
+ dim_size: int,
195
+ ) -> None:
196
+ mask = ma.getmaskarray(self.data[source][:])
197
+ has = ~mask[:, ind] if axis == 0 else ~mask[ind, :]
198
+ lo = (mask[:, below] if axis == 0 else mask[below, :]) if below >= 0 else True
199
+ hi = (
200
+ (mask[:, above] if axis == 0 else mask[above, :])
201
+ if above < dim_size
202
+ else True
203
+ )
204
+ isolated = has & lo & hi
205
+ if axis == 0:
206
+ self.data[target][:][isolated, ind] = ma.masked
207
+ else:
208
+ self.data[target][:][ind, isolated] = ma.masked
206
209
 
207
210
  def correct_atten(self, attenuations: RadarAttenuation) -> None:
208
211
  """Corrects radar echo for liquid and gas attenuation.
@@ -153,6 +153,9 @@ def _process_instrument_product(
153
153
  fun: Callable
154
154
  match (product, instrument.instrument_id):
155
155
  case ("radar", _id) if "mira" in _id:
156
+ site_meta["range_correction_factor"] = calibration.get(
157
+ "range_correction_factor"
158
+ )
156
159
  fun = instruments.mira2nc
157
160
  case ("radar", _id) if "rpg" in _id:
158
161
  fun = instruments.rpg2nc
@@ -388,16 +391,18 @@ def _fetch_product(
388
391
  meta = [meta[0]]
389
392
  suffix = "geophysical" if "geophysical" in meta[0].product.type else "instrument"
390
393
  folder = _create_output_folder(suffix, args)
391
- return _download_product_file(meta, folder, client)
394
+ return _download_product_file(meta, folder, client, force=args.force_download)
392
395
 
393
396
 
394
397
  def _fetch_model(args: argparse.Namespace, client: APIClient) -> str | None:
395
- files = client.files(product_id="model", date=args.date, site_id=args.site)
398
+ files = client.files(
399
+ product_id="model", model_id=args.model, date=args.date, site_id=args.site
400
+ )
396
401
  if not files:
397
402
  logging.info("No model data available for this date")
398
403
  return None
399
404
  folder = _create_output_folder("instrument", args)
400
- return _download_product_file(files, folder, client)
405
+ return _download_product_file(files, folder, client, force=args.force_download)
401
406
 
402
407
 
403
408
  def _fetch_raw(
@@ -406,19 +411,32 @@ def _fetch_raw(
406
411
  pid = _shorten_pid(metadata[0].instrument.pid)
407
412
  instrument = f"{metadata[0].instrument.instrument_id}_{pid}"
408
413
  folder = _create_input_folder(instrument, args)
414
+ if args.force_download:
415
+ for meta in metadata:
416
+ filepath = folder / meta.download_url.split("/")[-1]
417
+ if filepath.exists():
418
+ logging.info("Removing existing file: %s", filepath)
419
+ filepath.unlink()
409
420
  return client.download(metadata, output_directory=folder)
410
421
 
411
422
 
412
423
  def _download_product_file(
413
- meta: list[ProductMetadata], folder: Path, client: APIClient
424
+ meta: list[ProductMetadata],
425
+ folder: Path,
426
+ client: APIClient,
427
+ *,
428
+ force: bool = False,
414
429
  ) -> str:
415
430
  if len(meta) > 1:
416
431
  msg = "Multiple product files found"
417
432
  raise ValueError(msg)
418
433
  filepath = folder / meta[0].filename
419
- if filepath.exists():
434
+ if filepath.exists() and not force:
420
435
  logging.info("Existing file found: %s", filepath)
421
436
  return str(filepath)
437
+ if filepath.exists():
438
+ logging.info("Removing existing file: %s", filepath)
439
+ filepath.unlink()
422
440
  logging.info("Downloading file: %s", filepath)
423
441
  return str(client.download(meta, output_directory=folder)[0])
424
442
 
@@ -438,10 +456,13 @@ def _plot(
438
456
  ) -> None:
439
457
  if filepath is None or (not args.plot and not args.show):
440
458
  return
441
- res = requests.get(f"{cloudnet_api_url}products/variables", timeout=60)
442
- res.raise_for_status()
443
- variables = next(var["variables"] for var in res.json() if var["id"] == product)
444
- variables = [var["id"].split("-")[-1] for var in variables]
459
+ if args.variables is not None:
460
+ variables = args.variables.split(",")
461
+ else:
462
+ res = requests.get(f"{cloudnet_api_url}products/variables", timeout=60)
463
+ res.raise_for_status()
464
+ variables = next(var["variables"] for var in res.json() if var["id"] == product)
465
+ variables = [var["id"].split("-")[-1] for var in variables]
445
466
  image_name = str(filepath).replace(".nc", ".png") if args.plot else None
446
467
  try:
447
468
  generate_figure(
@@ -486,6 +507,21 @@ def _parse_products(product_argument: str, client: APIClient) -> list[str]:
486
507
  return valid_products
487
508
 
488
509
 
510
+ def _parse_model(model_id: str, client: APIClient) -> str:
511
+ models = client.models()
512
+ found = False
513
+ for model in models:
514
+ if model_id == model.source_model_id and model.id != model.source_model_id:
515
+ msg = f"Use {model.id} or similar instead of {model_id}"
516
+ raise argparse.ArgumentTypeError(msg)
517
+ if model_id == model.id:
518
+ found = True
519
+ if not found:
520
+ msg = f"Invalid model: {model_id}"
521
+ raise argparse.ArgumentTypeError(msg)
522
+ return model_id
523
+
524
+
489
525
  def main() -> None:
490
526
  client = APIClient()
491
527
  parser = argparse.ArgumentParser(
@@ -501,7 +537,11 @@ def main() -> None:
501
537
  metavar="SITE",
502
538
  )
503
539
  parser.add_argument(
504
- "-d", "--date", type=str, help="Date in YYYY-MM-DD", required=True
540
+ "-d",
541
+ "--date",
542
+ type=str,
543
+ help="Date in YYYY-MM-DD (default: today)",
544
+ default=datetime.datetime.now(tz=datetime.timezone.utc).date().isoformat(),
505
545
  )
506
546
  parser.add_argument(
507
547
  "-p",
@@ -514,6 +554,12 @@ def main() -> None:
514
554
  ),
515
555
  required=True,
516
556
  )
557
+ parser.add_argument(
558
+ "-m",
559
+ "--model",
560
+ type=lambda arg: _parse_model(arg, client),
561
+ help="Model to use in categorize.",
562
+ )
517
563
  parser.add_argument("--input", type=Path, help="Input path", default="input/")
518
564
  parser.add_argument("--output", type=Path, help="Output path", default="output/")
519
565
  parser.add_argument(
@@ -534,6 +580,19 @@ def main() -> None:
534
580
  default=False,
535
581
  action=argparse.BooleanOptionalAction,
536
582
  )
583
+ parser.add_argument(
584
+ "--force-download",
585
+ help="Force re-download of input files even if they exist locally",
586
+ default=False,
587
+ action=argparse.BooleanOptionalAction,
588
+ )
589
+ parser.add_argument(
590
+ "-v",
591
+ "--variables",
592
+ type=str,
593
+ help="Variables to plot (comma-separated), e.g. 'target_classification'",
594
+ default=None,
595
+ )
537
596
  args = parser.parse_args()
538
597
 
539
598
  logger = logging.getLogger()
@@ -156,6 +156,7 @@ class Mira(NcRadar):
156
156
  if "tpow" not in self.data:
157
157
  logging.warning("Variable tpow is missing")
158
158
  return
159
+ self._correct_fzk_tpow()
159
160
  tpow = self.data["tpow"][:]
160
161
  # Threshold for abnormally low power e.g. Limassol 2024-10-20. Average
161
162
  # power should 30 to 60 W according to MIRA-35 data sheet. Based on a
@@ -169,6 +170,27 @@ class Mira(NcRadar):
169
170
  )
170
171
  self.screen_time_indices(~is_low)
171
172
 
173
+ def _correct_fzk_tpow(self) -> None:
174
+ """Corrects tpow for old FZK-serial instruments missing the FZK100 fix.
175
+
176
+ Some MIRA-35 instruments with serial number 'fzk' had a firmware bug
177
+ where tpow was reported 100x too small. Files produced after the
178
+ firmware fix contain 'FZK100' in the 'hrd' global attribute. Files
179
+ without this field need tpow multiplied by 100.
180
+ """
181
+ hrd = getattr(self.dataset, "hrd", "")
182
+ if "FZK100" in hrd:
183
+ return
184
+ sn_lines = [line for line in hrd.split("\n") if line.strip().startswith("SN:")]
185
+ if not sn_lines:
186
+ return
187
+ sn = sn_lines[0].split(":", 1)[1].strip()
188
+ if sn != "fzk":
189
+ return
190
+ logging.info("Correcting tpow by factor 100 for old FZK instrument firmware")
191
+ self.data["tpow"].data[:] *= 100
192
+ self.data["tpow"].correction_factor = 100
193
+
172
194
  def screen_invalid_ldr(self) -> None:
173
195
  """Masks LDR in MIRA STSR mode data.
174
196
  Is there a better way to identify this mode?
@@ -53,8 +53,13 @@ class NcRadar(DataSource, CloudnetInstrument):
53
53
  range_instru = np.array(
54
54
  self.getvar("range", "height"),
55
55
  ) # "height" in old BASTA files
56
+ range_correction_factor = self.site_meta.get("range_correction_factor")
57
+ if range_correction_factor is not None:
58
+ range_instru = range_instru * range_correction_factor
56
59
  time = np.array(self.time)
57
60
  self.append_data(range_instru, "range")
61
+ if range_correction_factor is not None:
62
+ self.data["range"].correction_factor = range_correction_factor
58
63
  self.append_data(time, "time", dtype=time_dtype)
59
64
 
60
65
  def screen_by_snr(self, snr_limit: float) -> None:
@@ -120,23 +125,18 @@ class NcRadar(DataSource, CloudnetInstrument):
120
125
  raise ValidTimeStampError(msg)
121
126
 
122
127
  elevation_diff = ma.diff(elevation, prepend=elevation[1])
123
- azimuth_diff = ma.diff(azimuth, prepend=azimuth[1])
124
-
125
- # if azimuth_diff is large, it can be due to wrapping around 360 degrees
126
- azimuth_diff[azimuth_diff > 180] -= 360
127
- azimuth_diff[azimuth_diff < -180] += 360
128
-
129
128
  is_stable = np.abs(elevation - 90) < elevation_threshold
130
129
  is_stable &= np.abs(elevation_diff) < elevation_diff_threshold
131
- is_stable &= np.abs(azimuth_diff) < azimuth_diff_threshold
132
130
 
133
- # If scanning unit is broken, data are missing
134
- # (assume it's vertically pointing)
135
- missing_info = elevation.mask & azimuth.mask
136
- is_stable[missing_info] = True
131
+ if not ma.getmaskarray(azimuth).all():
132
+ azimuth_diff = ma.diff(azimuth, prepend=azimuth[1])
133
+ azimuth_diff[azimuth_diff > 180] -= 360
134
+ azimuth_diff[azimuth_diff < -180] += 360
135
+ is_stable &= np.abs(azimuth_diff) < azimuth_diff_threshold
137
136
 
138
- if ma.isMaskedArray(is_stable):
139
- is_stable[is_stable.mask] = False
137
+ # Masked values indicate missing scanning info
138
+ # (assume vertically pointing)
139
+ is_stable = np.array(ma.filled(is_stable, fill_value=True))
140
140
  n_removed = np.count_nonzero(~is_stable)
141
141
 
142
142
  if n_removed >= len(elevation) - 1:
@@ -61,8 +61,8 @@ ATTRIBUTES = {
61
61
  "temperature": PlotMeta(
62
62
  name="Model temperature",
63
63
  cbar="RdBu_r",
64
- clabel="K",
65
- plot_range=(223.15, 323.15),
64
+ clabel="\u00b0C",
65
+ plot_range=(-50, 50),
66
66
  plot_scale=_LIN,
67
67
  plot_type="model",
68
68
  ),
@@ -348,6 +348,8 @@ def plot_colormesh(
348
348
  ax: Axes, data: npt.NDArray, axes: tuple, variable_info: PlotMeta
349
349
  ) -> None:
350
350
  vmin, vmax = variable_info.plot_range
351
+ if variable_info.clabel == "\u00b0C":
352
+ data = data - 273.15
351
353
  if variable_info.plot_scale == "logarithmic":
352
354
  data, vmin, vmax = lin2log(data, vmin, vmax)
353
355
  cmap = plt.get_cmap(variable_info.cbar, 22)
@@ -19,6 +19,7 @@ class PlotMeta(NamedTuple):
19
19
  moving_average: Whether to plot a moving average in a 1d plot.
20
20
  contour: Whether to plot contours on top of a filled colormap.
21
21
  zero_line: Whether to plot a zero line in a 1d plot.
22
+ contour_levels: Specific contour levels to draw on 2D plots.
22
23
  mask_zeros: Whether to mask zero values in the plot.
23
24
  time_smoothing_duration: The duration of the time smoothing window
24
25
  (in 2d plots) in minutes.
@@ -31,6 +32,7 @@ class PlotMeta(NamedTuple):
31
32
  moving_average: bool = True
32
33
  contour: bool = False
33
34
  zero_line: bool = False
35
+ contour_levels: tuple[float, ...] | None = None
34
36
  mask_zeros: bool = False
35
37
  time_smoothing_duration: int = 0
36
38
 
@@ -185,7 +187,7 @@ ATTRIBUTES = {
185
187
  "mwr": {
186
188
  "temperature": PlotMeta(
187
189
  cmap="coolwarm",
188
- plot_range=(223.15, 323.15),
190
+ plot_range=(-50, 50),
189
191
  contour=True,
190
192
  time_smoothing_duration=_MWR_SINGLE_SMOOTHING,
191
193
  ),
@@ -204,7 +206,7 @@ ATTRIBUTES = {
204
206
  "mwr-single": {
205
207
  "temperature": PlotMeta(
206
208
  cmap="coolwarm",
207
- plot_range=(223.15, 323.15),
209
+ plot_range=(-50, 50),
208
210
  contour=True,
209
211
  time_smoothing_duration=_MWR_SINGLE_SMOOTHING,
210
212
  ),
@@ -235,7 +237,7 @@ ATTRIBUTES = {
235
237
  "mwr-multi": {
236
238
  "temperature": PlotMeta(
237
239
  cmap="coolwarm",
238
- plot_range=(223.15, 323.15),
240
+ plot_range=(-50, 50),
239
241
  contour=True,
240
242
  time_smoothing_duration=_MWR_MULTI_SMOOTHING,
241
243
  ),
@@ -406,7 +408,8 @@ ATTRIBUTES = {
406
408
  ),
407
409
  "temperature": PlotMeta(
408
410
  cmap="RdBu_r",
409
- plot_range=(223.15, 323.15),
411
+ plot_range=(-50, 50),
412
+ contour_levels=(-38, -10, 0),
410
413
  ),
411
414
  "cloud_fraction": PlotMeta(
412
415
  cmap="Blues",
@@ -414,7 +417,8 @@ ATTRIBUTES = {
414
417
  ),
415
418
  "Tw": PlotMeta(
416
419
  cmap="RdBu_r",
417
- plot_range=(223.15, 323.15),
420
+ plot_range=(-50, 50),
421
+ contour_levels=(-38, -10, 0),
418
422
  ),
419
423
  "specific_humidity": PlotMeta(
420
424
  plot_range=(1e-5, 1e-2),
@@ -16,6 +16,7 @@ from matplotlib.axes import Axes
16
16
  from matplotlib.colorbar import Colorbar
17
17
  from matplotlib.colorizer import ColorizingArtist
18
18
  from matplotlib.colors import ListedColormap
19
+ from matplotlib.contour import QuadContourSet
19
20
  from matplotlib.pyplot import Figure
20
21
  from matplotlib.ticker import AutoMinorLocator
21
22
  from matplotlib.transforms import Affine2D, Bbox
@@ -402,6 +403,7 @@ class Plot:
402
403
 
403
404
  def _convert_units(self) -> str:
404
405
  multiply, add = "multiply", "add"
406
+ k_to_c = (add, -273.15, "\u00b0C")
405
407
  units_conversion = {
406
408
  "rainfall_rate": (multiply, 3600000, "mm h$^{-1}$"),
407
409
  "snowfall_rate": (multiply, 3600000, "mm h$^{-1}$"),
@@ -411,7 +413,16 @@ class Plot:
411
413
  "rainfall_amount": (multiply, 1000, "mm"),
412
414
  "snowfall_amount": (multiply, 1000, "mm"),
413
415
  "precipitation_amount": (multiply, 1000, "mm"),
414
- "air_temperature": (add, -273.15, "\u00b0C"),
416
+ "air_temperature": k_to_c,
417
+ "temperature": k_to_c,
418
+ "Tw": k_to_c,
419
+ "T_sensor": k_to_c,
420
+ "T_ambient": k_to_c,
421
+ "T_interior": k_to_c,
422
+ "T_laser_driver": k_to_c,
423
+ "transmitter_temperature": k_to_c,
424
+ "receiver_temperature": k_to_c,
425
+ "pc_temperature": k_to_c,
415
426
  "r_accum_RT": (multiply, 1000, "mm"),
416
427
  "r_accum_NRT": (multiply, 1000, "mm"),
417
428
  "cloud_top_height_agl": (multiply, con.M_TO_KM, "Height (km AGL)"),
@@ -773,32 +784,37 @@ class Plot2D(Plot):
773
784
  linewidths=0.5,
774
785
  )
775
786
 
776
- if self.sub_plot.variable.name == "Tw":
777
- self._plot_contour(
787
+ if self._plot_meta.contour_levels is not None:
788
+ cs = self._plot_contour(
778
789
  figure_data,
779
790
  alt,
780
- levels=np.array([con.T0]),
791
+ levels=np.array(self._plot_meta.contour_levels),
781
792
  colors="gray",
782
793
  linewidths=1.25,
783
794
  linestyles="dashed",
784
795
  )
796
+ if cs is not None:
797
+ self._ax.clabel(cs, fmt="%g \u00b0C", fontsize=8)
785
798
 
786
799
  def _plot_contour(
787
800
  self,
788
801
  figure_data: FigureData,
789
802
  alt: npt.NDArray,
790
803
  **options, # noqa: ANN003
791
- ) -> None:
804
+ ) -> QuadContourSet | None:
792
805
  time_length = len(figure_data.time_including_gaps)
793
806
  step = max(1, time_length // 200)
794
807
  ind_time = np.arange(0, time_length, step)
795
- self._ax.contour(
796
- figure_data.time_including_gaps[ind_time],
797
- alt,
798
- self._data[ind_time, :].T,
799
- **options,
800
- zorder=_get_zorder("contour"),
801
- )
808
+ try:
809
+ return self._ax.contour(
810
+ figure_data.time_including_gaps[ind_time],
811
+ alt,
812
+ self._data[ind_time, :].T,
813
+ **options,
814
+ zorder=_get_zorder("contour"),
815
+ )
816
+ except ValueError:
817
+ return None
802
818
 
803
819
  def _screen_data_by_max_y(self, figure_data: FigureData) -> ndarray:
804
820
  if figure_data.height is None:
@@ -317,7 +317,11 @@ def _find_cloud_mask(classification: npt.NDArray) -> npt.NDArray:
317
317
  Target.ICE_AND_SUPERCOOLED,
318
318
  ]:
319
319
  cloud_mask[classification == value] = 1
320
- return cloud_mask
320
+ return utils.remove_small_objects(
321
+ cloud_mask.astype(bool),
322
+ max_size=20,
323
+ connectivity=1,
324
+ ).astype(int)
321
325
 
322
326
 
323
327
  COMMENTS = {
@@ -354,6 +354,8 @@ def interpolate_2d_mask(
354
354
  z: ma.MaskedArray,
355
355
  x_new: npt.NDArray,
356
356
  y_new: npt.NDArray,
357
+ *,
358
+ extrapolate: bool = False,
357
359
  ) -> ma.MaskedArray:
358
360
  """2D linear interpolation preserving the mask.
359
361
 
@@ -363,13 +365,14 @@ def interpolate_2d_mask(
363
365
  z: 2D masked array, data values.
364
366
  x_new: 1D array, new x-coordinates.
365
367
  y_new: 1D array, new y-coordinates.
368
+ extrapolate: If True, linearly extrapolate along the y-axis for points
369
+ outside the original range. Default is False (NaN / masked).
366
370
 
367
371
  Returns:
368
372
  Interpolated 2D masked array.
369
373
 
370
374
  Notes:
371
- Points outside the original range will be nans (and masked). Uses linear
372
- interpolation. Input data may contain nan-values.
375
+ Uses linear interpolation. Input data may contain nan-values.
373
376
 
374
377
  """
375
378
  z = ma.array(ma.masked_invalid(z, copy=True))
@@ -386,6 +389,15 @@ def interpolate_2d_mask(
386
389
  (xx_new, yy_new),
387
390
  method="linear",
388
391
  )
392
+ if extrapolate:
393
+ y_new_arr = np.asarray(y_new)
394
+ for i in range(data.shape[0]):
395
+ row = data[i, :]
396
+ nans = np.isnan(row)
397
+ if nans.any() and np.sum(~nans) >= 2:
398
+ valid = ~nans
399
+ f = interp1d(y_new_arr[valid], row[valid], fill_value="extrapolate")
400
+ data[i, nans] = f(y_new_arr[nans])
389
401
  # Preserve mask:
390
402
  mask_fun = RectBivariateSpline(x, y, ma.getmaskarray(z), kx=1, ky=1)
391
403
  mask = mask_fun(x_new, y_new)
@@ -1,4 +1,4 @@
1
1
  MAJOR = 1
2
- MINOR = 88
3
- PATCH = 2
2
+ MINOR = 89
3
+ PATCH = 0
4
4
  __version__ = f"{MAJOR}.{MINOR}.{PATCH}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudnetpy
3
- Version: 1.88.2
3
+ Version: 1.89.0
4
4
  Summary: Python package for Cloudnet processing
5
5
  Author: Simo Tukiainen
6
6
  License: MIT License
File without changes
File without changes
File without changes
File without changes
File without changes