cloudnetpy-qc 1.25.0__tar.gz → 1.25.2__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.
- {cloudnetpy_qc-1.25.0/cloudnetpy_qc.egg-info → cloudnetpy_qc-1.25.2}/PKG-INFO +1 -1
- {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2}/cloudnetpy_qc/quality.py +57 -6
- {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2}/cloudnetpy_qc/utils.py +48 -0
- {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2}/cloudnetpy_qc/version.py +1 -1
- {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2/cloudnetpy_qc.egg-info}/PKG-INFO +1 -1
- {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2}/pyproject.toml +1 -1
- {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2}/tests/test_qc.py +1 -0
- {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2}/LICENSE +0 -0
- {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2}/MANIFEST.in +0 -0
- {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2}/README.md +0 -0
- {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2}/cloudnetpy_qc/__init__.py +0 -0
- {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2}/cloudnetpy_qc/coverage.py +0 -0
- {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2}/cloudnetpy_qc/data/area-type-table.xml +0 -0
- {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2}/cloudnetpy_qc/data/cf-standard-name-table.xml +0 -0
- {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2}/cloudnetpy_qc/data/data_quality_config.ini +0 -0
- {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2}/cloudnetpy_qc/data/standardized-region-list.xml +0 -0
- {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2}/cloudnetpy_qc/py.typed +0 -0
- {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2}/cloudnetpy_qc/variables.py +0 -0
- {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2}/cloudnetpy_qc.egg-info/SOURCES.txt +0 -0
- {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2}/cloudnetpy_qc.egg-info/dependency_links.txt +0 -0
- {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2}/cloudnetpy_qc.egg-info/requires.txt +0 -0
- {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2}/cloudnetpy_qc.egg-info/top_level.txt +0 -0
- {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2}/setup.cfg +0 -0
- {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2}/tests/test_utils.py +0 -0
|
@@ -14,6 +14,7 @@ from typing import NamedTuple, TypedDict
|
|
|
14
14
|
import netCDF4
|
|
15
15
|
import numpy as np
|
|
16
16
|
import scipy.stats
|
|
17
|
+
from cftime import num2pydate
|
|
17
18
|
from numpy import ma
|
|
18
19
|
from requests import RequestException
|
|
19
20
|
|
|
@@ -75,8 +76,9 @@ class FileReport(NamedTuple):
|
|
|
75
76
|
|
|
76
77
|
|
|
77
78
|
class SiteMeta(TypedDict):
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
time: np.ndarray | None
|
|
80
|
+
latitude: float | np.ndarray | None
|
|
81
|
+
longitude: float | np.ndarray | None
|
|
80
82
|
altitude: float | None
|
|
81
83
|
|
|
82
84
|
|
|
@@ -114,6 +116,7 @@ def run_tests(
|
|
|
114
116
|
test_instance._add_error(
|
|
115
117
|
f"Failed to run test: {err} ({type(err).__name__})"
|
|
116
118
|
)
|
|
119
|
+
logging.exception("Failed to run test:")
|
|
117
120
|
test_reports.append(test_instance.report)
|
|
118
121
|
if test_instance.coverage is not None:
|
|
119
122
|
coverage = test_instance.coverage
|
|
@@ -799,14 +802,31 @@ class TestCoordinates(Test):
|
|
|
799
802
|
self._add_error(f"Variable '{key}' is missing")
|
|
800
803
|
|
|
801
804
|
if "latitude" in self.nc.variables and "longitude" in self.nc.variables:
|
|
802
|
-
site_lat = self.site_meta["latitude"]
|
|
803
|
-
site_lon = self.site_meta["longitude"]
|
|
805
|
+
site_lat = np.atleast_1d(self.site_meta["latitude"])
|
|
806
|
+
site_lon = np.atleast_1d(self.site_meta["longitude"])
|
|
804
807
|
file_lat = np.atleast_1d(self.nc["latitude"][:])
|
|
805
808
|
file_lon = np.atleast_1d(self.nc["longitude"][:])
|
|
806
809
|
file_lon[file_lon > 180] -= 360
|
|
807
|
-
|
|
810
|
+
|
|
811
|
+
if (
|
|
812
|
+
self.site_meta["time"] is not None
|
|
813
|
+
and file_lat.size > 1
|
|
814
|
+
and file_lon.size > 1
|
|
815
|
+
):
|
|
816
|
+
site_time = self._read_site_time()
|
|
817
|
+
file_time = self._read_file_time()
|
|
818
|
+
idx = utils.find_closest(file_time, site_time)
|
|
819
|
+
file_lat = file_lat[idx]
|
|
820
|
+
file_lon = file_lon[idx]
|
|
821
|
+
else:
|
|
822
|
+
file_lat, file_lon = utils.average_coordinate(file_lat, file_lon)
|
|
823
|
+
site_lat, site_lon = utils.average_coordinate(site_lat, site_lon)
|
|
824
|
+
|
|
825
|
+
dist = np.atleast_1d(
|
|
826
|
+
utils.haversine(site_lat, site_lon, file_lat, file_lon)
|
|
827
|
+
)
|
|
808
828
|
i = np.argmax(dist)
|
|
809
|
-
max_dist =
|
|
829
|
+
max_dist = self._calc_max_dist(site_lat, site_lon)
|
|
810
830
|
if dist[i] > max_dist:
|
|
811
831
|
self._add_error(
|
|
812
832
|
f"Variables 'latitude' and 'longitude' do not match "
|
|
@@ -828,6 +848,37 @@ class TestCoordinates(Test):
|
|
|
828
848
|
f"but received {round(file_alt[i])}\u00a0m"
|
|
829
849
|
)
|
|
830
850
|
|
|
851
|
+
def _read_site_time(self):
|
|
852
|
+
for dt in self.site_meta["time"]:
|
|
853
|
+
if (
|
|
854
|
+
not isinstance(dt, datetime.datetime)
|
|
855
|
+
or dt.tzinfo is None
|
|
856
|
+
or dt.tzinfo.utcoffset(dt) is None
|
|
857
|
+
):
|
|
858
|
+
raise ValueError("Naive datetimes are not supported")
|
|
859
|
+
naive_dt = [
|
|
860
|
+
dt.astimezone(datetime.timezone.utc).replace(tzinfo=None)
|
|
861
|
+
for dt in self.site_meta["time"]
|
|
862
|
+
]
|
|
863
|
+
return np.array(naive_dt, dtype="datetime64[s]")
|
|
864
|
+
|
|
865
|
+
def _read_file_time(self):
|
|
866
|
+
naive_dt = num2pydate(
|
|
867
|
+
self.nc["time"][:], self.nc["time"].units, self.nc["time"].calendar
|
|
868
|
+
)
|
|
869
|
+
return np.array(naive_dt, dtype="datetime64[s]")
|
|
870
|
+
|
|
871
|
+
def _calc_max_dist(self, latitude, longitude):
|
|
872
|
+
if self.nc.cloudnet_file_type == "model":
|
|
873
|
+
angle = 1 # Model resolution should be at least 1 degrees.
|
|
874
|
+
half_angle = angle / 2
|
|
875
|
+
min_lat = np.maximum(-90, latitude - half_angle)
|
|
876
|
+
max_lat = np.minimum(90, latitude + half_angle)
|
|
877
|
+
min_lon = np.maximum(-180, longitude - half_angle)
|
|
878
|
+
max_lon = np.minimum(180, longitude + half_angle)
|
|
879
|
+
return utils.haversine(min_lat, min_lon, max_lat, max_lon)
|
|
880
|
+
return 10
|
|
881
|
+
|
|
831
882
|
|
|
832
883
|
# ------------------------------#
|
|
833
884
|
# ------ Error / Warning ------ #
|
|
@@ -141,3 +141,51 @@ def haversine(
|
|
|
141
141
|
|
|
142
142
|
a = np.sin(dlat / 2) ** 2 + np.cos(lat1) * np.cos(lat2) * np.sin(dlon / 2) ** 2
|
|
143
143
|
return 2 * r * np.arcsin(np.sqrt(a))
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def find_closest(x: npt.NDArray, x_new: npt.NDArray) -> npt.NDArray[np.intp]:
|
|
147
|
+
"""Find the closest values between two arrays.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
x: Sorted array.
|
|
151
|
+
x_new: Sorted array.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Indices into `x` which correspond to the closest values in `x_new`.
|
|
155
|
+
|
|
156
|
+
Example:
|
|
157
|
+
>>> x = np.array([0.9, 1.2, 2.0, 2.1])
|
|
158
|
+
>>> x_new = np.array([1, 2])
|
|
159
|
+
>>> find_closest(x, x_new)
|
|
160
|
+
array([0, 2])
|
|
161
|
+
"""
|
|
162
|
+
idx = np.searchsorted(x, x_new)
|
|
163
|
+
idx_left = np.clip(idx - 1, 0, len(x) - 1)
|
|
164
|
+
idx_right = np.clip(idx, 0, len(x) - 1)
|
|
165
|
+
diff_left = np.abs(x_new - x[idx_left])
|
|
166
|
+
diff_right = np.abs(x_new - x[idx_right])
|
|
167
|
+
return np.where(diff_left < diff_right, idx_left, idx_right)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def average_coordinate(
|
|
171
|
+
latitude: npt.NDArray, longitude: npt.NDArray
|
|
172
|
+
) -> tuple[float, float]:
|
|
173
|
+
"""Calculate average position from given coordinates.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
latitude: Array of latitudes.
|
|
177
|
+
longitude: Array of longitudes.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Tuple of average latitude and longitude.
|
|
181
|
+
"""
|
|
182
|
+
if latitude.size == longitude.size == 1:
|
|
183
|
+
return latitude[0], longitude[0]
|
|
184
|
+
latitude = np.radians(latitude)
|
|
185
|
+
longitude = np.radians(longitude)
|
|
186
|
+
x = np.mean(np.cos(latitude) * np.cos(longitude))
|
|
187
|
+
y = np.mean(np.cos(latitude) * np.sin(longitude))
|
|
188
|
+
z = np.mean(np.sin(latitude))
|
|
189
|
+
avg_lat = np.degrees(np.atan2(z, np.sqrt(x * x + y * y)))
|
|
190
|
+
avg_lon = np.degrees(np.atan2(y, x))
|
|
191
|
+
return avg_lat, avg_lon
|
|
@@ -30,7 +30,7 @@ Repository = "https://github.com/actris-cloudnet/cloudnetpy-qc"
|
|
|
30
30
|
Changelog = "https://github.com/actris-cloudnet/cloudnetpy-qc/blob/main/CHANGELOG.md"
|
|
31
31
|
|
|
32
32
|
[[tool.mypy.overrides]]
|
|
33
|
-
module = ["cfchecker.*", "scipy.*"]
|
|
33
|
+
module = ["cfchecker.*", "cftime.*", "scipy.*"]
|
|
34
34
|
ignore_missing_imports = true
|
|
35
35
|
|
|
36
36
|
[tool.release-version]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2}/cloudnetpy_qc/data/standardized-region-list.xml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|