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.
Files changed (24) hide show
  1. {cloudnetpy_qc-1.25.0/cloudnetpy_qc.egg-info → cloudnetpy_qc-1.25.2}/PKG-INFO +1 -1
  2. {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2}/cloudnetpy_qc/quality.py +57 -6
  3. {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2}/cloudnetpy_qc/utils.py +48 -0
  4. {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2}/cloudnetpy_qc/version.py +1 -1
  5. {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2/cloudnetpy_qc.egg-info}/PKG-INFO +1 -1
  6. {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2}/pyproject.toml +1 -1
  7. {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2}/tests/test_qc.py +1 -0
  8. {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2}/LICENSE +0 -0
  9. {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2}/MANIFEST.in +0 -0
  10. {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2}/README.md +0 -0
  11. {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2}/cloudnetpy_qc/__init__.py +0 -0
  12. {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2}/cloudnetpy_qc/coverage.py +0 -0
  13. {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2}/cloudnetpy_qc/data/area-type-table.xml +0 -0
  14. {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2}/cloudnetpy_qc/data/cf-standard-name-table.xml +0 -0
  15. {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2}/cloudnetpy_qc/data/data_quality_config.ini +0 -0
  16. {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2}/cloudnetpy_qc/data/standardized-region-list.xml +0 -0
  17. {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2}/cloudnetpy_qc/py.typed +0 -0
  18. {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2}/cloudnetpy_qc/variables.py +0 -0
  19. {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2}/cloudnetpy_qc.egg-info/SOURCES.txt +0 -0
  20. {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2}/cloudnetpy_qc.egg-info/dependency_links.txt +0 -0
  21. {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2}/cloudnetpy_qc.egg-info/requires.txt +0 -0
  22. {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2}/cloudnetpy_qc.egg-info/top_level.txt +0 -0
  23. {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2}/setup.cfg +0 -0
  24. {cloudnetpy_qc-1.25.0 → cloudnetpy_qc-1.25.2}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudnetpy_qc
3
- Version: 1.25.0
3
+ Version: 1.25.2
4
4
  Summary: Quality control routines for CloudnetPy products
5
5
  Author-email: Finnish Meteorological Institute <actris-cloudnet@fmi.fi>
6
6
  License: MIT License
@@ -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
- latitude: float | None
79
- longitude: float | None
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
- dist = utils.haversine(site_lat, site_lon, file_lat, file_lon)
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 = 100 if self.nc.cloudnet_file_type == "model" else 10
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
@@ -2,5 +2,5 @@
2
2
 
3
3
  MAJOR = 1
4
4
  MINOR = 25
5
- PATCH = 0
5
+ PATCH = 2
6
6
  __version__ = f"{MAJOR}.{MINOR}.{PATCH}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudnetpy_qc
3
- Version: 1.25.0
3
+ Version: 1.25.2
4
4
  Summary: Quality control routines for CloudnetPy products
5
5
  Author-email: Finnish Meteorological Institute <actris-cloudnet@fmi.fi>
6
6
  License: MIT License
@@ -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]
@@ -93,6 +93,7 @@ class Check:
93
93
  def __init__(self, filename: str, file_type: str | None = None):
94
94
  # Norunda
95
95
  site_meta: quality.SiteMeta = {
96
+ "time": None,
96
97
  "latitude": 60.086,
97
98
  "longitude": 17.479,
98
99
  "altitude": 46.0,
File without changes
File without changes
File without changes