cloudnetpy 1.65.7__tar.gz → 1.66.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 (129) hide show
  1. {cloudnetpy-1.65.7/cloudnetpy.egg-info → cloudnetpy-1.66.0}/PKG-INFO +1 -1
  2. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/categorize/__init__.py +0 -1
  3. cloudnetpy-1.66.0/cloudnetpy/categorize/atmos_utils.py +342 -0
  4. cloudnetpy-1.66.0/cloudnetpy/categorize/attenuation.py +31 -0
  5. cloudnetpy-1.66.0/cloudnetpy/categorize/attenuations/__init__.py +37 -0
  6. cloudnetpy-1.66.0/cloudnetpy/categorize/attenuations/gas_attenuation.py +30 -0
  7. cloudnetpy-1.66.0/cloudnetpy/categorize/attenuations/liquid_attenuation.py +80 -0
  8. cloudnetpy-1.66.0/cloudnetpy/categorize/attenuations/melting_attenuation.py +75 -0
  9. cloudnetpy-1.66.0/cloudnetpy/categorize/attenuations/rain_attenuation.py +84 -0
  10. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/categorize/categorize.py +140 -81
  11. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/categorize/classify.py +92 -128
  12. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/categorize/containers.py +45 -31
  13. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/categorize/droplet.py +2 -2
  14. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/categorize/falling.py +3 -3
  15. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/categorize/freezing.py +2 -2
  16. cloudnetpy-1.66.0/cloudnetpy/categorize/itu.py +243 -0
  17. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/categorize/melting.py +0 -3
  18. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/categorize/model.py +31 -14
  19. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/categorize/radar.py +28 -12
  20. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/constants.py +3 -6
  21. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/model_evaluation/file_handler.py +2 -2
  22. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/model_evaluation/products/observation_products.py +8 -8
  23. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/model_evaluation/tests/unit/test_grid_methods.py +5 -2
  24. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/model_evaluation/tests/unit/test_observation_products.py +11 -11
  25. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/output.py +46 -26
  26. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/plotting/plot_meta.py +8 -2
  27. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/plotting/plotting.py +31 -8
  28. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/products/classification.py +39 -34
  29. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/products/der.py +15 -13
  30. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/products/drizzle_tools.py +22 -21
  31. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/products/ier.py +8 -45
  32. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/products/iwc.py +7 -22
  33. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/products/lwc.py +14 -15
  34. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/products/mwr_tools.py +15 -2
  35. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/products/product_tools.py +121 -119
  36. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/utils.py +4 -0
  37. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/version.py +2 -2
  38. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0/cloudnetpy.egg-info}/PKG-INFO +1 -1
  39. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy.egg-info/SOURCES.txt +7 -1
  40. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/pyproject.toml +1 -3
  41. cloudnetpy-1.65.7/cloudnetpy/categorize/atmos.py +0 -376
  42. cloudnetpy-1.65.7/cloudnetpy/categorize/atmos_utils.py +0 -123
  43. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/LICENSE +0 -0
  44. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/MANIFEST.in +0 -0
  45. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/README.md +0 -0
  46. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/__init__.py +0 -0
  47. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/categorize/disdrometer.py +0 -0
  48. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/categorize/insects.py +0 -0
  49. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/categorize/lidar.py +0 -0
  50. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/categorize/mwr.py +0 -0
  51. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/cloudnetarray.py +0 -0
  52. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/concat_lib.py +0 -0
  53. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/datasource.py +0 -0
  54. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/exceptions.py +0 -0
  55. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/instruments/__init__.py +0 -0
  56. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/instruments/basta.py +0 -0
  57. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/instruments/campbell_scientific.py +0 -0
  58. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/instruments/ceilo.py +0 -0
  59. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/instruments/ceilometer.py +0 -0
  60. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/instruments/cl61d.py +0 -0
  61. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/instruments/cloudnet_instrument.py +0 -0
  62. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/instruments/copernicus.py +0 -0
  63. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/instruments/disdrometer/__init__.py +0 -0
  64. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/instruments/disdrometer/common.py +0 -0
  65. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/instruments/disdrometer/parsivel.py +0 -0
  66. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/instruments/disdrometer/thies.py +0 -0
  67. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/instruments/galileo.py +0 -0
  68. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/instruments/hatpro.py +0 -0
  69. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/instruments/instruments.py +0 -0
  70. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/instruments/lufft.py +0 -0
  71. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/instruments/mira.py +0 -0
  72. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/instruments/mrr.py +0 -0
  73. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/instruments/nc_lidar.py +0 -0
  74. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/instruments/nc_radar.py +0 -0
  75. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/instruments/pollyxt.py +0 -0
  76. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/instruments/radiometrics.py +0 -0
  77. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/instruments/rpg.py +0 -0
  78. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/instruments/rpg_reader.py +0 -0
  79. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/instruments/toa5.py +0 -0
  80. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/instruments/vaisala.py +0 -0
  81. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/instruments/weather_station.py +0 -0
  82. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/metadata.py +0 -0
  83. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/model_evaluation/__init__.py +0 -0
  84. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/model_evaluation/metadata.py +0 -0
  85. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/model_evaluation/model_metadata.py +0 -0
  86. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/model_evaluation/plotting/__init__.py +0 -0
  87. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/model_evaluation/plotting/plot_meta.py +0 -0
  88. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/model_evaluation/plotting/plot_tools.py +0 -0
  89. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/model_evaluation/plotting/plotting.py +0 -0
  90. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/model_evaluation/products/__init__.py +0 -0
  91. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/model_evaluation/products/advance_methods.py +0 -0
  92. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/model_evaluation/products/grid_methods.py +0 -0
  93. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/model_evaluation/products/model_products.py +0 -0
  94. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/model_evaluation/products/product_resampling.py +0 -0
  95. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/model_evaluation/products/tools.py +0 -0
  96. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/model_evaluation/statistics/__init__.py +0 -0
  97. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/model_evaluation/statistics/statistical_methods.py +0 -0
  98. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/model_evaluation/tests/__init__.py +0 -0
  99. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/model_evaluation/tests/e2e/__init__.py +0 -0
  100. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/model_evaluation/tests/e2e/conftest.py +0 -0
  101. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/model_evaluation/tests/e2e/process_cf/__init__.py +0 -0
  102. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/model_evaluation/tests/e2e/process_cf/main.py +0 -0
  103. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/model_evaluation/tests/e2e/process_cf/tests.py +0 -0
  104. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/model_evaluation/tests/e2e/process_iwc/__init__.py +0 -0
  105. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/model_evaluation/tests/e2e/process_iwc/main.py +0 -0
  106. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/model_evaluation/tests/e2e/process_iwc/tests.py +0 -0
  107. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/model_evaluation/tests/e2e/process_lwc/__init__.py +0 -0
  108. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/model_evaluation/tests/e2e/process_lwc/main.py +0 -0
  109. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/model_evaluation/tests/e2e/process_lwc/tests.py +0 -0
  110. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/model_evaluation/tests/unit/__init__.py +0 -0
  111. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/model_evaluation/tests/unit/conftest.py +0 -0
  112. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/model_evaluation/tests/unit/test_advance_methods.py +0 -0
  113. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/model_evaluation/tests/unit/test_model_products.py +0 -0
  114. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/model_evaluation/tests/unit/test_plot_tools.py +0 -0
  115. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/model_evaluation/tests/unit/test_plotting.py +0 -0
  116. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/model_evaluation/tests/unit/test_statistical_methods.py +0 -0
  117. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/model_evaluation/tests/unit/test_tools.py +0 -0
  118. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/model_evaluation/utils.py +0 -0
  119. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/plotting/__init__.py +0 -0
  120. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/products/__init__.py +0 -0
  121. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/products/drizzle.py +0 -0
  122. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/products/drizzle_error.py +0 -0
  123. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/products/mie_lu_tables.nc +0 -0
  124. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy/py.typed +0 -0
  125. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy.egg-info/dependency_links.txt +0 -0
  126. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy.egg-info/requires.txt +0 -0
  127. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/cloudnetpy.egg-info/top_level.txt +0 -0
  128. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/docs/source/conf.py +0 -0
  129. {cloudnetpy-1.65.7 → cloudnetpy-1.66.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cloudnetpy
3
- Version: 1.65.7
3
+ Version: 1.66.0
4
4
  Summary: Python package for Cloudnet processing
5
5
  Author: Simo Tukiainen
6
6
  License: MIT License
@@ -1,2 +1 @@
1
1
  from .categorize import generate_categorize
2
- from .radar import Radar
@@ -0,0 +1,342 @@
1
+ import logging
2
+
3
+ import numpy as np
4
+ import numpy.typing as npt
5
+ import scipy.constants
6
+ from numpy import ma
7
+
8
+ import cloudnetpy.constants as con
9
+ from cloudnetpy import utils
10
+
11
+
12
+ def calc_wet_bulb_temperature(model_data: dict) -> np.ndarray:
13
+ """Calculate wet-bulb temperature iteratively.
14
+
15
+ Args:
16
+ model_data: Model variables `temperature`, `pressure`, `q`.
17
+
18
+ Returns:
19
+ Wet-bulb temperature (K).
20
+
21
+ References:
22
+ Al-Ismaili, A. M., & Al-Azri, N. A. (2016). Simple Iterative Approach to
23
+ Calculate Wet-Bulb Temperature for Estimating Evaporative Cooling
24
+ Efficiency. Int. J. Agric. Innovations Res., 4, 1013-1018.
25
+ """
26
+ specific_humidity = model_data["q"]
27
+ pressure = model_data["pressure"]
28
+ td = k2c(model_data["temperature"])
29
+ vp = calc_vapor_pressure(pressure, specific_humidity)
30
+ W = calc_mixing_ratio(vp, pressure)
31
+ L_v_0 = 2501e3 # Latent heat of vaporization at 0degC (J kg-1)
32
+
33
+ def f(tw):
34
+ svp = calc_saturation_vapor_pressure(c2k(tw))
35
+ W_s = calc_mixing_ratio(svp, pressure)
36
+ C_p_w = 0.0265 * tw**2 - 1.7688 * tw + 4205.6 # Eq. 6 (J kg-1 C-1)
37
+ C_p_wv = 0.0016 * td**2 + 0.1546 * td + 1858.7 # Eq. 7 (J kg-1 C-1)
38
+ C_p_da = 0.0667 * ((td + tw) / 2) + 1005 # Eq. 8 (J kg-1 C-1)
39
+ a = (L_v_0 - (C_p_w - C_p_wv) * tw) * W_s - C_p_da * (td - tw)
40
+ b = L_v_0 + C_p_wv * td - C_p_w * tw
41
+ return a / b - W
42
+
43
+ min_err = 1e-6 * np.maximum(np.abs(td), 1)
44
+ delta = 1e-8
45
+ tw = td
46
+ max_iter = 20
47
+ for _ in range(max_iter):
48
+ f_tw = f(tw)
49
+ if np.all(np.abs(f_tw) < min_err):
50
+ break
51
+ df_tw = (f(tw + delta) - f_tw) / delta
52
+ tw = tw - f_tw / df_tw
53
+ else:
54
+ msg = (
55
+ "Wet-bulb temperature didn't converge after %d iterations: "
56
+ "error min %g, max %g, mean %g, median %g"
57
+ )
58
+ logging.warning(
59
+ msg, max_iter, np.min(f_tw), np.max(f_tw), np.mean(f_tw), np.median(f_tw)
60
+ )
61
+
62
+ return c2k(tw)
63
+
64
+
65
+ def calc_vapor_pressure(
66
+ pressure: npt.NDArray, specific_humidity: npt.NDArray
67
+ ) -> npt.NDArray:
68
+ """Calculate vapor pressure of water based on pressure and specific
69
+ humidity.
70
+
71
+ Args:
72
+ pressure: Pressure (Pa)
73
+ specific_humidity: Specific humidity (1)
74
+
75
+ Returns:
76
+ Vapor pressure (Pa)
77
+
78
+ References:
79
+ Cai, J. (2019). Humidity Measures.
80
+ https://cran.r-project.org/web/packages/humidity/vignettes/humidity-measures.html
81
+ """
82
+ return (
83
+ specific_humidity
84
+ * pressure
85
+ / (con.MW_RATIO + (1 - con.MW_RATIO) * specific_humidity)
86
+ )
87
+
88
+
89
+ def c2k(temp: np.ndarray) -> np.ndarray:
90
+ """Converts Celsius to Kelvins."""
91
+ return ma.array(temp) + 273.15
92
+
93
+
94
+ def k2c(temp: np.ndarray) -> np.ndarray:
95
+ """Converts Kelvins to Celsius."""
96
+ return ma.array(temp) - 273.15
97
+
98
+
99
+ def find_cloud_bases(array: np.ndarray) -> np.ndarray:
100
+ """Finds bases of clouds.
101
+
102
+ Args:
103
+ array: 2D boolean array denoting clouds or some other similar field.
104
+
105
+ Returns:
106
+ Boolean array indicating bases of the individual clouds.
107
+
108
+ """
109
+ zeros = np.zeros(array.shape[0])
110
+ array_padded = np.insert(array, 0, zeros, axis=1).astype(int)
111
+ return np.diff(array_padded, axis=1) == 1
112
+
113
+
114
+ def find_cloud_tops(array: np.ndarray) -> np.ndarray:
115
+ """Finds tops of clouds.
116
+
117
+ Args:
118
+ array: 2D boolean array denoting clouds or some other similar field.
119
+
120
+ Returns:
121
+ Boolean array indicating tops of the individual clouds.
122
+
123
+ """
124
+ array_flipped = np.fliplr(array)
125
+ bases_of_flipped = find_cloud_bases(array_flipped)
126
+ return np.fliplr(bases_of_flipped)
127
+
128
+
129
+ def find_lowest_cloud_bases(
130
+ cloud_mask: np.ndarray,
131
+ height: np.ndarray,
132
+ ) -> ma.MaskedArray:
133
+ """Finds altitudes of cloud bases."""
134
+ cloud_heights = cloud_mask * height
135
+ return _find_lowest_heights(cloud_heights)
136
+
137
+
138
+ def find_highest_cloud_tops(
139
+ cloud_mask: np.ndarray,
140
+ height: np.ndarray,
141
+ ) -> ma.MaskedArray:
142
+ """Finds altitudes of cloud tops."""
143
+ cloud_heights = cloud_mask * height
144
+ cloud_heights_flipped = np.fliplr(cloud_heights)
145
+ return _find_lowest_heights(cloud_heights_flipped)
146
+
147
+
148
+ def _find_lowest_heights(cloud_heights: np.ndarray) -> ma.MaskedArray:
149
+ inds = (cloud_heights != 0).argmax(axis=1)
150
+ heights = np.array([cloud_heights[i, ind] for i, ind in enumerate(inds)])
151
+ return ma.masked_equal(heights, 0.0)
152
+
153
+
154
+ def fill_clouds_with_lwc_dz(
155
+ temperature: np.ndarray, pressure: np.ndarray, is_liquid: np.ndarray
156
+ ) -> np.ndarray:
157
+ """Fills liquid clouds with lwc change rate at the cloud bases.
158
+
159
+ Args:
160
+ temperature: 2D temperature array (K).
161
+ pressure: 2D pressure array (Pa).
162
+ is_liquid: Boolean array indicating presence of liquid clouds.
163
+
164
+ Returns:
165
+ Liquid water content change rate (kg m-3 m-1), so that for each cloud the base
166
+ value is filled for the whole cloud.
167
+
168
+ """
169
+ lwc_dz = get_lwc_change_rate_at_bases(temperature, pressure, is_liquid)
170
+ lwc_dz_filled = ma.zeros(lwc_dz.shape)
171
+ lwc_dz_filled[is_liquid] = utils.ffill(lwc_dz[is_liquid])
172
+ return lwc_dz_filled
173
+
174
+
175
+ def get_lwc_change_rate_at_bases(
176
+ temperature: np.ndarray,
177
+ pressure: np.ndarray,
178
+ is_liquid: np.ndarray,
179
+ ) -> np.ndarray:
180
+ """Finds LWC change rate in liquid cloud bases.
181
+
182
+ Args:
183
+ temperature: 2D temperature array (K).
184
+ pressure: 2D pressure array (Pa).
185
+ is_liquid: Boolean array indicating presence of liquid clouds.
186
+
187
+ Returns:
188
+ Liquid water content change rate at cloud bases (kg m-3 m-1).
189
+
190
+ """
191
+ liquid_bases = find_cloud_bases(is_liquid)
192
+ lwc_dz = ma.zeros(liquid_bases.shape)
193
+ lwc_dz[liquid_bases] = calc_lwc_change_rate(
194
+ temperature[liquid_bases],
195
+ pressure[liquid_bases],
196
+ )
197
+
198
+ return lwc_dz
199
+
200
+
201
+ def calc_lwc_change_rate(temperature: np.ndarray, pressure: np.ndarray) -> np.ndarray:
202
+ """Returns rate of change of condensable water (LWC).
203
+
204
+ Calculates the theoretical adiabatic rate of increase of LWC
205
+ with height, given the cloud base temperature and pressure.
206
+
207
+ Args:
208
+ temperature: Temperature of cloud base (K).
209
+ pressure: Pressure of cloud base (Pa).
210
+
211
+ Returns:
212
+ dlwc/dz (kg m-3 m-1)
213
+
214
+ References:
215
+ Brenguier, 1991, https://doi.org/10.1175/1520-0469(1991)048<0264:POTCPA>2.0.CO;2
216
+
217
+ """
218
+ svp = calc_saturation_vapor_pressure(temperature)
219
+ svp_mixing_ratio = calc_mixing_ratio(svp, pressure)
220
+ air_density = calc_air_density(pressure, temperature, svp_mixing_ratio)
221
+
222
+ e = 0.622
223
+ Cp = 1004 # J kg-1 K-1
224
+ Lv = 2.45e6 # J kg-1 = Pa m3 kg-1
225
+ qs = svp_mixing_ratio # kg kg-1
226
+ pa = air_density # kg m-3
227
+ es = svp # Pa
228
+ P = pressure # Pa
229
+ T = temperature # K
230
+
231
+ # See Appendix B in Brenguier (1991) for the derivation of the following equation
232
+ dqs_dp = (
233
+ -(1 - (Cp * T) / (e * Lv))
234
+ * (((Cp * T) / (e * Lv)) + ((Lv * qs * pa) / (P - es))) ** -1
235
+ * (e * es)
236
+ * (P - es) ** -2
237
+ )
238
+
239
+ # Using hydrostatic equation to convert dqs_dp to dqs_dz
240
+ dqs_dz = dqs_dp * air_density * -scipy.constants.g
241
+
242
+ return dqs_dz * air_density
243
+
244
+
245
+ def calc_saturation_vapor_pressure(temperature: np.ndarray) -> np.ndarray:
246
+ """Goff-Gratch formula for saturation vapor pressure over water adopted by WMO.
247
+
248
+ Args:
249
+ temperature: Temperature (K).
250
+
251
+ Returns:
252
+ Saturation vapor pressure (Pa).
253
+
254
+ """
255
+ ratio = con.T0 / temperature
256
+ inv_ratio = ratio**-1
257
+ return (
258
+ 10
259
+ ** (
260
+ 10.79574 * (1 - ratio)
261
+ - 5.028 * np.log10(inv_ratio)
262
+ + 1.50475e-4 * (1 - (10 ** (-8.2969 * (inv_ratio - 1))))
263
+ + 0.42873e-3 * (10 ** (4.76955 * (1 - ratio)) - 1)
264
+ + 0.78614
265
+ )
266
+ ) * con.HPA_TO_PA
267
+
268
+
269
+ def calc_mixing_ratio(vapor_pressure: np.ndarray, pressure: np.ndarray) -> np.ndarray:
270
+ """Calculates mixing ratio from partial vapor pressure and pressure.
271
+
272
+ Args:
273
+ vapor_pressure: Partial pressure of water vapor (Pa).
274
+ pressure: Atmospheric pressure (Pa).
275
+
276
+ Returns:
277
+ Mixing ratio (kg kg-1).
278
+
279
+ """
280
+ return con.MW_RATIO * vapor_pressure / (pressure - vapor_pressure)
281
+
282
+
283
+ def calc_air_density(
284
+ pressure: np.ndarray,
285
+ temperature: np.ndarray,
286
+ svp_mixing_ratio: np.ndarray,
287
+ ) -> np.ndarray:
288
+ """Calculates air density (kg m-3).
289
+
290
+ Args:
291
+ pressure: Pressure (Pa).
292
+ temperature: Temperature (K).
293
+ svp_mixing_ratio: Saturation vapor pressure mixing ratio (kg kg-1).
294
+
295
+ Returns:
296
+ Air density (kg m-3).
297
+
298
+ """
299
+ return pressure / (con.RS * temperature * (0.6 * svp_mixing_ratio + 1))
300
+
301
+
302
+ def calc_adiabatic_lwc(lwc_dz: np.ndarray, height: np.ndarray) -> np.ndarray:
303
+ """Calculates adiabatic liquid water content (kg m-3).
304
+
305
+ Args:
306
+ lwc_dz: Liquid water content change rate (kg m-3 m-1) calculated at the
307
+ base of each cloud and filled to that cloud.
308
+ height: Height vector (m).
309
+
310
+ Returns:
311
+ Liquid water content (kg m-3).
312
+
313
+ """
314
+ is_cloud = lwc_dz != 0
315
+ cloud_indices = utils.cumsumr(is_cloud, axis=1)
316
+ dz = utils.path_lengths_from_ground(height) * np.ones_like(lwc_dz)
317
+ dz[cloud_indices < 1] = 0
318
+ return utils.cumsumr(dz, axis=1) * lwc_dz
319
+
320
+
321
+ def normalize_lwc_by_lwp(
322
+ lwc_adiabatic: np.ndarray, lwp: np.ndarray, height: np.ndarray
323
+ ) -> np.ndarray:
324
+ """Finds LWC that would produce measured LWP.
325
+
326
+ Calculates LWP-weighted, normalized LWC. This is the measured
327
+ LWP distributed to liquid cloud pixels according to their
328
+ theoretical proportion.
329
+
330
+ Args:
331
+ lwc_adiabatic: Theoretical 2D liquid water content (kg m-3).
332
+ lwp: 1D liquid water path (kg m-2).
333
+ height: Height vector (m).
334
+
335
+ Returns:
336
+ 2D LWP-weighted, scaled LWC (kg m-3) that would produce the observed LWP.
337
+
338
+ """
339
+ path_lengths = utils.path_lengths_from_ground(height)
340
+ theoretical_lwp = ma.sum(lwc_adiabatic * path_lengths, axis=1)
341
+ scaling_factors = lwp / theoretical_lwp
342
+ return lwc_adiabatic * utils.transpose(scaling_factors)
@@ -0,0 +1,31 @@
1
+ from numpy import ma
2
+
3
+ from cloudnetpy.categorize.attenuations import (
4
+ RadarAttenuation,
5
+ gas_attenuation,
6
+ liquid_attenuation,
7
+ melting_attenuation,
8
+ rain_attenuation,
9
+ )
10
+ from cloudnetpy.categorize.containers import ClassificationResult, Observations
11
+
12
+
13
+ def get_attenuations(
14
+ data: Observations, classification: ClassificationResult
15
+ ) -> RadarAttenuation:
16
+ rain = rain_attenuation.calc_rain_attenuation(data, classification)
17
+ gas = gas_attenuation.calc_gas_attenuation(data, classification)
18
+ liquid = liquid_attenuation.LiquidAttenuation(data, classification).attenuation
19
+ melting = melting_attenuation.calc_melting_attenuation(data, classification)
20
+
21
+ liquid.amount[rain.attenuated] = ma.masked
22
+ liquid.error[rain.attenuated] = ma.masked
23
+ liquid.attenuated[rain.attenuated] = False
24
+ liquid.uncorrected[rain.attenuated] = False
25
+
26
+ return RadarAttenuation(
27
+ gas=gas,
28
+ liquid=liquid,
29
+ rain=rain,
30
+ melting=melting,
31
+ )
@@ -0,0 +1,37 @@
1
+ from dataclasses import dataclass
2
+ from typing import Annotated
3
+
4
+ import numpy as np
5
+ from numpy import ma
6
+ from numpy.typing import NDArray
7
+
8
+ from cloudnetpy import constants as con
9
+ from cloudnetpy.utils import path_lengths_from_ground
10
+
11
+
12
+ @dataclass
13
+ class Attenuation:
14
+ amount: Annotated[ma.MaskedArray, "float32"]
15
+ error: Annotated[ma.MaskedArray, "float32"]
16
+ attenuated: NDArray[np.bool_]
17
+ uncorrected: NDArray[np.bool_]
18
+
19
+
20
+ @dataclass
21
+ class RadarAttenuation:
22
+ gas: Attenuation
23
+ liquid: Attenuation
24
+ rain: Attenuation
25
+ melting: Attenuation
26
+
27
+
28
+ def calc_two_way_attenuation(
29
+ height: np.ndarray, specific_attenuation: ma.MaskedArray
30
+ ) -> ma.MaskedArray:
31
+ """Calculates two-way attenuation (dB) for given specific attenuation
32
+ (dB km-1) and height (m).
33
+ """
34
+ path_lengths = path_lengths_from_ground(height) * con.M_TO_KM # km
35
+ one_way_attenuation = specific_attenuation * path_lengths
36
+ accumulated_attenuation = ma.cumsum(one_way_attenuation, axis=1)
37
+ return accumulated_attenuation * con.TWO_WAY
@@ -0,0 +1,30 @@
1
+ import numpy as np
2
+
3
+ from cloudnetpy.categorize.attenuations import (
4
+ Attenuation,
5
+ calc_two_way_attenuation,
6
+ )
7
+ from cloudnetpy.categorize.containers import ClassificationResult, Observations
8
+
9
+
10
+ def calc_gas_attenuation(
11
+ data: Observations, classification: ClassificationResult
12
+ ) -> Attenuation:
13
+ model_data = data.model.data_dense
14
+
15
+ specific_attenuation = model_data["specific_gas_atten"].copy()
16
+ saturated_attenuation = model_data["specific_saturated_gas_atten"]
17
+
18
+ liquid_in_pixel = classification.category_bits.droplet
19
+ specific_attenuation[liquid_in_pixel] = saturated_attenuation[liquid_in_pixel]
20
+
21
+ two_way_attenuation = calc_two_way_attenuation(
22
+ data.radar.height, specific_attenuation
23
+ )
24
+
25
+ return Attenuation(
26
+ amount=two_way_attenuation,
27
+ error=two_way_attenuation * 0.1,
28
+ attenuated=np.ones_like(two_way_attenuation, dtype=bool),
29
+ uncorrected=np.zeros_like(two_way_attenuation, dtype=bool),
30
+ )
@@ -0,0 +1,80 @@
1
+ import numpy as np
2
+ from numpy import ma
3
+
4
+ import cloudnetpy.constants as con
5
+ from cloudnetpy import utils
6
+ from cloudnetpy.categorize import atmos_utils
7
+ from cloudnetpy.categorize.attenuations import Attenuation, calc_two_way_attenuation
8
+ from cloudnetpy.categorize.containers import ClassificationResult, Observations
9
+
10
+
11
+ class LiquidAttenuation:
12
+ """Class for calculating liquid attenuation.
13
+
14
+ References:
15
+ Hogan, Robin & Connor, Ewan. (2004). Facilitating cloud radar and lidar
16
+ algorithms: the Cloudnet Instrument Synergy/Target Categorization product.
17
+ """
18
+
19
+ def __init__(self, data: Observations, classification: ClassificationResult):
20
+ self._model = data.model.data_dense
21
+ self._liquid_in_pixel = classification.category_bits.droplet
22
+ self._height = data.radar.height
23
+
24
+ if data.mwr is not None:
25
+ lwp = data.mwr.data["lwp"][:]
26
+ lwp_error = data.mwr.data["lwp_error"][:]
27
+ else:
28
+ lwp = ma.masked_all(data.radar.time.size)
29
+ lwp_error = ma.masked_all(data.radar.time.size)
30
+
31
+ lwc_dz = atmos_utils.fill_clouds_with_lwc_dz(
32
+ self._model["temperature"], self._model["pressure"], self._liquid_in_pixel
33
+ )
34
+
35
+ two_way_attenuation = self._calc_liquid_atten(lwp, lwc_dz)
36
+ two_way_attenuation_error = self._calc_liquid_atten_err(lwp_error, lwc_dz)
37
+
38
+ attenuated = utils.ffill(self._liquid_in_pixel)
39
+
40
+ two_way_attenuation[~attenuated] = ma.masked
41
+ two_way_attenuation_error[~attenuated] = ma.masked
42
+
43
+ uncorrected = attenuated & two_way_attenuation.mask
44
+
45
+ self.attenuation = Attenuation(
46
+ amount=two_way_attenuation,
47
+ error=two_way_attenuation_error,
48
+ attenuated=attenuated,
49
+ uncorrected=uncorrected,
50
+ )
51
+
52
+ def _calc_liquid_atten(
53
+ self, lwp: ma.MaskedArray, lwc_dz: np.ndarray
54
+ ) -> ma.MaskedArray:
55
+ """Finds radar liquid attenuation."""
56
+ lwp = lwp.copy()
57
+ lwp[lwp < 0] = 0
58
+ lwc_adiabatic = atmos_utils.calc_adiabatic_lwc(lwc_dz, self._height)
59
+ lwc_scaled = atmos_utils.normalize_lwc_by_lwp(lwc_adiabatic, lwp, self._height)
60
+ return self._calc_two_way_attenuation(lwc_scaled)
61
+
62
+ def _calc_liquid_atten_err(
63
+ self, lwp_error: ma.MaskedArray, lwc_dz: np.ndarray
64
+ ) -> ma.MaskedArray:
65
+ """Finds radar liquid attenuation error."""
66
+ lwc_err_scaled = atmos_utils.normalize_lwc_by_lwp(
67
+ lwc_dz, lwp_error, self._height
68
+ )
69
+ return self._calc_two_way_attenuation(lwc_err_scaled)
70
+
71
+ def _calc_two_way_attenuation(self, lwc_scaled: np.ndarray) -> ma.MaskedArray:
72
+ """Calculates liquid attenuation (dB).
73
+
74
+ Args:
75
+ lwc_scaled: Liquid water content (kg m-3).
76
+
77
+ """
78
+ specific_attenuation_rate = self._model["specific_liquid_atten"]
79
+ specific_attenuation = specific_attenuation_rate * lwc_scaled * con.KG_TO_G
80
+ return calc_two_way_attenuation(self._height, specific_attenuation)
@@ -0,0 +1,75 @@
1
+ import numpy as np
2
+ from numpy import ma
3
+
4
+ import cloudnetpy.constants as con
5
+ from cloudnetpy import utils
6
+ from cloudnetpy.categorize.attenuations import (
7
+ Attenuation,
8
+ )
9
+ from cloudnetpy.categorize.containers import ClassificationResult, Observations
10
+
11
+
12
+ def calc_melting_attenuation(
13
+ data: Observations, classification: ClassificationResult
14
+ ) -> Attenuation:
15
+ shape = classification.category_bits.melting.shape
16
+ is_rain = classification.is_rain
17
+
18
+ affected_region = classification.category_bits.freezing.copy()
19
+
20
+ if data.disdrometer is None:
21
+ affected_region[~is_rain, :] = False
22
+ above_melting = utils.ffill(classification.category_bits.melting)
23
+ affected_region[~above_melting] = False
24
+ return Attenuation(
25
+ amount=ma.masked_all(shape),
26
+ error=ma.masked_all(shape),
27
+ attenuated=affected_region,
28
+ uncorrected=affected_region,
29
+ )
30
+
31
+ rainfall_rate = data.disdrometer.data["rainfall_rate"][:]
32
+ frequency = data.radar.radar_frequency
33
+
34
+ attenuation_array = _calc_melting_attenuation(rainfall_rate, frequency)
35
+
36
+ amount = affected_region * utils.transpose(attenuation_array)
37
+
38
+ affected_region[amount == 0] = False
39
+
40
+ amount[amount == 0] = ma.masked
41
+
42
+ band = utils.get_wl_band(data.radar.radar_frequency)
43
+ error_factor = 0.2 if band == 0 else 0.1
44
+
45
+ error = amount * error_factor
46
+ error[~affected_region] = ma.masked
47
+
48
+ return Attenuation(
49
+ amount=amount,
50
+ error=error,
51
+ attenuated=affected_region,
52
+ uncorrected=affected_region & amount.mask,
53
+ )
54
+
55
+
56
+ def _calc_melting_attenuation(
57
+ rainfall_rate: np.ndarray, frequency: float
58
+ ) -> np.ndarray:
59
+ """Calculates total attenuation due to melting layer (dB).
60
+
61
+ References:
62
+ Li, H., & Moisseev, D. (2019). Melting layer attenuation
63
+ at Ka- and W-bands as derived from multifrequency radar
64
+ Doppler spectra observations. Journal of Geophysical
65
+ Research: Atmospheres, 124, 9520–9533. https://doi.org/10.1029/2019JD030316
66
+
67
+ """
68
+ if frequency > 34 and frequency < 37:
69
+ a, b = 0.97, 0.61
70
+ elif frequency > 93 and frequency < 96:
71
+ a, b = 2.9, 0.42
72
+ else:
73
+ msg = "Radar frequency not supported"
74
+ raise ValueError(msg)
75
+ return a * (rainfall_rate * con.M_S_TO_MM_H) ** b
@@ -0,0 +1,84 @@
1
+ import numpy as np
2
+ from numpy import ma
3
+ from numpy.typing import NDArray
4
+
5
+ import cloudnetpy.constants as con
6
+ from cloudnetpy import utils
7
+ from cloudnetpy.categorize.attenuations import (
8
+ Attenuation,
9
+ calc_two_way_attenuation,
10
+ )
11
+ from cloudnetpy.categorize.containers import ClassificationResult, Observations
12
+
13
+
14
+ def calc_rain_attenuation(
15
+ data: Observations, classification: ClassificationResult
16
+ ) -> Attenuation:
17
+ affected_region, inducing_region = _find_regions(classification)
18
+ shape = affected_region.shape
19
+
20
+ if data.disdrometer is None:
21
+ return Attenuation(
22
+ amount=ma.masked_all(shape),
23
+ error=ma.masked_all(shape),
24
+ attenuated=affected_region,
25
+ uncorrected=affected_region,
26
+ )
27
+
28
+ rainfall_rate = data.disdrometer.data["rainfall_rate"][:].copy()
29
+ rainfall_rate[classification.is_rain == 0] = ma.masked
30
+ frequency = data.radar.radar_frequency
31
+
32
+ specific_attenuation_array = _calc_rain_specific_attenuation(
33
+ rainfall_rate, frequency
34
+ )
35
+
36
+ specific_attenuation = utils.transpose(specific_attenuation_array) * ma.ones(shape)
37
+
38
+ two_way_attenuation = calc_two_way_attenuation(
39
+ data.radar.height, specific_attenuation
40
+ )
41
+
42
+ two_way_attenuation[~inducing_region] = 0
43
+ two_way_attenuation = ma.array(utils.ffill(two_way_attenuation.data))
44
+ two_way_attenuation[two_way_attenuation == 0] = ma.masked
45
+
46
+ return Attenuation(
47
+ amount=two_way_attenuation,
48
+ error=two_way_attenuation * 0.2,
49
+ attenuated=affected_region,
50
+ uncorrected=np.zeros_like(affected_region, dtype=bool),
51
+ )
52
+
53
+
54
+ def _find_regions(
55
+ classification: ClassificationResult,
56
+ ) -> tuple[NDArray[np.bool_], NDArray[np.bool_]]:
57
+ """Finds regions where rain attenuation is present and can be corrected or not."""
58
+ warm_region = ~classification.category_bits.freezing
59
+ is_rain = utils.transpose(classification.is_rain).astype(bool)
60
+ affected_region = np.ones_like(warm_region, dtype=bool) * is_rain
61
+ inducing_region = warm_region * is_rain
62
+ return affected_region, inducing_region
63
+
64
+
65
+ def _calc_rain_specific_attenuation(
66
+ rainfall_rate: np.ndarray, frequency: float
67
+ ) -> np.ndarray:
68
+ """Calculates specific attenuation due to rain (dB km-1).
69
+
70
+ References:
71
+ Crane, R. (1980). Prediction of Attenuation by Rain.
72
+ IEEE Transactions on Communications, 28(9), 1717–1733.
73
+ doi:10.1109/tcom.1980.1094844
74
+ """
75
+ if frequency > 8 and frequency < 12:
76
+ alpha, beta = 0.0125, 1.18
77
+ if frequency > 34 and frequency < 37:
78
+ alpha, beta = 0.242, 1.04
79
+ elif frequency > 93 and frequency < 96:
80
+ alpha, beta = 0.95, 0.72
81
+ else:
82
+ msg = "Radar frequency not supported"
83
+ raise ValueError(msg)
84
+ return alpha * (rainfall_rate * con.M_S_TO_MM_H) ** beta