cloudnetpy-qc 1.24.3__tar.gz → 1.25.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 (24) hide show
  1. {cloudnetpy_qc-1.24.3/cloudnetpy_qc.egg-info → cloudnetpy_qc-1.25.0}/PKG-INFO +2 -1
  2. {cloudnetpy_qc-1.24.3 → cloudnetpy_qc-1.25.0}/README.md +1 -0
  3. cloudnetpy_qc-1.25.0/cloudnetpy_qc/coverage.py +66 -0
  4. {cloudnetpy_qc-1.24.3 → cloudnetpy_qc-1.25.0}/cloudnetpy_qc/quality.py +13 -52
  5. {cloudnetpy_qc-1.24.3 → cloudnetpy_qc-1.25.0}/cloudnetpy_qc/version.py +2 -2
  6. {cloudnetpy_qc-1.24.3 → cloudnetpy_qc-1.25.0/cloudnetpy_qc.egg-info}/PKG-INFO +2 -1
  7. {cloudnetpy_qc-1.24.3 → cloudnetpy_qc-1.25.0}/cloudnetpy_qc.egg-info/SOURCES.txt +1 -0
  8. {cloudnetpy_qc-1.24.3 → cloudnetpy_qc-1.25.0}/LICENSE +0 -0
  9. {cloudnetpy_qc-1.24.3 → cloudnetpy_qc-1.25.0}/MANIFEST.in +0 -0
  10. {cloudnetpy_qc-1.24.3 → cloudnetpy_qc-1.25.0}/cloudnetpy_qc/__init__.py +0 -0
  11. {cloudnetpy_qc-1.24.3 → cloudnetpy_qc-1.25.0}/cloudnetpy_qc/data/area-type-table.xml +0 -0
  12. {cloudnetpy_qc-1.24.3 → cloudnetpy_qc-1.25.0}/cloudnetpy_qc/data/cf-standard-name-table.xml +0 -0
  13. {cloudnetpy_qc-1.24.3 → cloudnetpy_qc-1.25.0}/cloudnetpy_qc/data/data_quality_config.ini +0 -0
  14. {cloudnetpy_qc-1.24.3 → cloudnetpy_qc-1.25.0}/cloudnetpy_qc/data/standardized-region-list.xml +0 -0
  15. {cloudnetpy_qc-1.24.3 → cloudnetpy_qc-1.25.0}/cloudnetpy_qc/py.typed +0 -0
  16. {cloudnetpy_qc-1.24.3 → cloudnetpy_qc-1.25.0}/cloudnetpy_qc/utils.py +0 -0
  17. {cloudnetpy_qc-1.24.3 → cloudnetpy_qc-1.25.0}/cloudnetpy_qc/variables.py +0 -0
  18. {cloudnetpy_qc-1.24.3 → cloudnetpy_qc-1.25.0}/cloudnetpy_qc.egg-info/dependency_links.txt +0 -0
  19. {cloudnetpy_qc-1.24.3 → cloudnetpy_qc-1.25.0}/cloudnetpy_qc.egg-info/requires.txt +0 -0
  20. {cloudnetpy_qc-1.24.3 → cloudnetpy_qc-1.25.0}/cloudnetpy_qc.egg-info/top_level.txt +0 -0
  21. {cloudnetpy_qc-1.24.3 → cloudnetpy_qc-1.25.0}/pyproject.toml +0 -0
  22. {cloudnetpy_qc-1.24.3 → cloudnetpy_qc-1.25.0}/setup.cfg +0 -0
  23. {cloudnetpy_qc-1.24.3 → cloudnetpy_qc-1.25.0}/tests/test_qc.py +0 -0
  24. {cloudnetpy_qc-1.24.3 → cloudnetpy_qc-1.25.0}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudnetpy_qc
3
- Version: 1.24.3
3
+ Version: 1.25.0
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
@@ -80,6 +80,7 @@ print(json_object)
80
80
  - `timestamp`: UTC timestamp of the test
81
81
  - `qcVersion`: `cloudnetpy-qc` version
82
82
  - `tests`: `Test[]`
83
+ - `data_coverage`: float
83
84
 
84
85
  ### `Test`
85
86
 
@@ -27,6 +27,7 @@ print(json_object)
27
27
  - `timestamp`: UTC timestamp of the test
28
28
  - `qcVersion`: `cloudnetpy-qc` version
29
29
  - `tests`: `Test[]`
30
+ - `data_coverage`: float
30
31
 
31
32
  ### `Test`
32
33
 
@@ -0,0 +1,66 @@
1
+ import datetime
2
+
3
+ import netCDF4
4
+ import numpy as np
5
+
6
+ from cloudnetpy_qc.variables import Product
7
+
8
+ RESOLUTIONS = {
9
+ Product.DISDROMETER: datetime.timedelta(minutes=1),
10
+ Product.L3_CF: datetime.timedelta(hours=1),
11
+ Product.L3_IWC: datetime.timedelta(hours=1),
12
+ Product.L3_LWC: datetime.timedelta(hours=1),
13
+ Product.MWR: datetime.timedelta(minutes=5),
14
+ Product.MWR_MULTI: datetime.timedelta(minutes=30),
15
+ Product.MWR_SINGLE: datetime.timedelta(minutes=5),
16
+ Product.WEATHER_STATION: datetime.timedelta(minutes=10),
17
+ Product.RAIN_GAUGE: datetime.timedelta(minutes=1),
18
+ Product.DOPPLER_LIDAR_WIND: datetime.timedelta(hours=1.5),
19
+ }
20
+ DEFAULT_RESOLUTION = datetime.timedelta(seconds=30)
21
+
22
+
23
+ def data_coverage(
24
+ nc: netCDF4.Dataset,
25
+ ) -> tuple[float, datetime.timedelta, datetime.timedelta] | None:
26
+ time = np.array(nc["time"][:])
27
+ time_unit = datetime.timedelta(hours=1)
28
+ try:
29
+ n_time = len(time)
30
+ except (TypeError, ValueError):
31
+ return None
32
+ if n_time < 2:
33
+ return None
34
+ if nc.cloudnet_file_type == "model":
35
+ expected_res = _model_resolution(nc)
36
+ else:
37
+ product = Product(nc.cloudnet_file_type)
38
+ expected_res = RESOLUTIONS.get(product, DEFAULT_RESOLUTION)
39
+ duration = get_duration(nc)
40
+ bins = max(1, duration // expected_res)
41
+ hist, _ = np.histogram(time, bins=bins, range=(0, duration / time_unit))
42
+ coverage = np.count_nonzero(hist > 0) / len(hist)
43
+ actual_res = np.median(np.diff(time)) * time_unit
44
+ return coverage, expected_res, actual_res
45
+
46
+
47
+ def _model_resolution(nc: netCDF4.Dataset) -> datetime.timedelta:
48
+ source = nc.source.lower()
49
+ if "gdas" in source or "ecmwf open" in source:
50
+ return datetime.timedelta(hours=3)
51
+ return datetime.timedelta(hours=1)
52
+
53
+
54
+ def get_duration(nc: netCDF4.Dataset) -> datetime.timedelta:
55
+ now = datetime.datetime.now(tz=datetime.timezone.utc)
56
+ if now.date() == _get_date(nc):
57
+ midnight = now.replace(hour=0, minute=0, second=0, microsecond=0)
58
+ duration = now - midnight
59
+ else:
60
+ duration = datetime.timedelta(days=1)
61
+ return duration
62
+
63
+
64
+ def _get_date(nc: netCDF4.Dataset) -> datetime.date:
65
+ date_in_file = [int(getattr(nc, x)) for x in ("year", "month", "day")]
66
+ return datetime.date(*date_in_file)
@@ -17,6 +17,8 @@ import scipy.stats
17
17
  from numpy import ma
18
18
  from requests import RequestException
19
19
 
20
+ from cloudnetpy_qc.coverage import data_coverage, get_duration
21
+
20
22
  from . import utils
21
23
  from .variables import LEVELS, VARIABLES, Product
22
24
  from .version import __version__
@@ -53,6 +55,7 @@ class FileReport(NamedTuple):
53
55
  timestamp: datetime.datetime
54
56
  qc_version: str
55
57
  tests: list[TestReport]
58
+ data_coverage: float | None
56
59
 
57
60
  def to_dict(self) -> dict:
58
61
  return {
@@ -84,6 +87,7 @@ def run_tests(
84
87
  ignore_tests: list[str] | None = None,
85
88
  ) -> FileReport:
86
89
  filename = Path(filename)
90
+ coverage = None
87
91
  if isinstance(product, str):
88
92
  product = Product(product)
89
93
  with netCDF4.Dataset(filename) as nc:
@@ -111,10 +115,13 @@ def run_tests(
111
115
  f"Failed to run test: {err} ({type(err).__name__})"
112
116
  )
113
117
  test_reports.append(test_instance.report)
118
+ if test_instance.coverage is not None:
119
+ coverage = test_instance.coverage
114
120
  return FileReport(
115
121
  timestamp=datetime.datetime.now(tz=datetime.timezone.utc),
116
122
  qc_version=__version__,
117
123
  tests=test_reports,
124
+ data_coverage=coverage,
118
125
  )
119
126
 
120
127
 
@@ -124,6 +131,7 @@ class Test:
124
131
  name: str
125
132
  description: str
126
133
  products: Iterable[Product] = Product.all()
134
+ coverage: float | None = None
127
135
 
128
136
  def __init__(
129
137
  self, nc: netCDF4.Dataset, filename: Path, product: Product, site_meta: SiteMeta
@@ -190,19 +198,6 @@ class Test:
190
198
  )
191
199
  self._add_warning(msg)
192
200
 
193
- def _get_date(self):
194
- date_in_file = [int(getattr(self.nc, x)) for x in ("year", "month", "day")]
195
- return datetime.date(*date_in_file)
196
-
197
- def _get_duration(self) -> datetime.timedelta:
198
- now = datetime.datetime.now(tz=datetime.timezone.utc)
199
- if now.date() == self._get_date():
200
- midnight = now.replace(hour=0, minute=0, second=0, microsecond=0)
201
- duration = now - midnight
202
- else:
203
- duration = datetime.timedelta(days=1)
204
- return duration
205
-
206
201
 
207
202
  # --------------------#
208
203
  # ------ Infos ------ #
@@ -287,45 +282,12 @@ class TestDataCoverage(Test):
287
282
  name = "Data coverage"
288
283
  description = "Test that file contains enough data."
289
284
 
290
- RESOLUTIONS = {
291
- Product.DISDROMETER: datetime.timedelta(minutes=1),
292
- Product.L3_CF: datetime.timedelta(hours=1),
293
- Product.L3_IWC: datetime.timedelta(hours=1),
294
- Product.L3_LWC: datetime.timedelta(hours=1),
295
- Product.MWR: datetime.timedelta(minutes=5),
296
- Product.MWR_MULTI: datetime.timedelta(minutes=30),
297
- Product.MWR_SINGLE: datetime.timedelta(minutes=5),
298
- Product.WEATHER_STATION: datetime.timedelta(minutes=10),
299
- Product.RAIN_GAUGE: datetime.timedelta(minutes=1),
300
- Product.DOPPLER_LIDAR_WIND: datetime.timedelta(hours=1.5),
301
- }
302
- DEFAULT_RESOLUTION = datetime.timedelta(seconds=30)
303
-
304
- def _model_resolution(self):
305
- source = self.nc.source.lower()
306
- if "gdas" in source or "ecmwf open" in source:
307
- return datetime.timedelta(hours=3)
308
- return datetime.timedelta(hours=1)
309
-
310
285
  def run(self):
311
- time = np.array(self.nc["time"][:])
312
- time_unit = datetime.timedelta(hours=1)
313
- try:
314
- n_time = len(time)
315
- except (TypeError, ValueError):
286
+ coverage, expected_res, actual_res = data_coverage(self.nc)
287
+ if coverage is None:
316
288
  return
317
- if n_time < 2:
318
- return
319
- if self.nc.cloudnet_file_type == "model":
320
- expected_res = self._model_resolution()
321
- else:
322
- expected_res = self.RESOLUTIONS.get(self.product, self.DEFAULT_RESOLUTION)
323
- duration = self._get_duration()
324
- bins = max(1, duration // expected_res)
325
- hist, _bin_edges = np.histogram(
326
- time, bins=bins, range=(0, duration / time_unit)
327
- )
328
- missing = np.count_nonzero(hist == 0) / len(hist) * 100
289
+ self.coverage = coverage
290
+ missing = (1 - coverage) * 100
329
291
  if missing > 20:
330
292
  message = f"{round(missing)}% of day's data is missing."
331
293
  if missing > 60:
@@ -333,7 +295,6 @@ class TestDataCoverage(Test):
333
295
  else:
334
296
  self._add_info(message)
335
297
 
336
- actual_res = np.median(np.diff(time)) * time_unit
337
298
  if actual_res > expected_res * 1.05:
338
299
  self._add_warning(
339
300
  f"Expected a measurement with interval at least {expected_res},"
@@ -789,7 +750,7 @@ class TestModelData(Test):
789
750
  if n_time < 2:
790
751
  return
791
752
 
792
- duration = self._get_duration()
753
+ duration = get_duration(self.nc)
793
754
  should_be_data_until = duration / time_unit
794
755
 
795
756
  for key in ("temperature", "pressure", "q"):
@@ -1,6 +1,6 @@
1
1
  """Cloudnetpy-QC version."""
2
2
 
3
3
  MAJOR = 1
4
- MINOR = 24
5
- PATCH = 3
4
+ MINOR = 25
5
+ PATCH = 0
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.24.3
3
+ Version: 1.25.0
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
@@ -80,6 +80,7 @@ print(json_object)
80
80
  - `timestamp`: UTC timestamp of the test
81
81
  - `qcVersion`: `cloudnetpy-qc` version
82
82
  - `tests`: `Test[]`
83
+ - `data_coverage`: float
83
84
 
84
85
  ### `Test`
85
86
 
@@ -3,6 +3,7 @@ MANIFEST.in
3
3
  README.md
4
4
  pyproject.toml
5
5
  cloudnetpy_qc/__init__.py
6
+ cloudnetpy_qc/coverage.py
6
7
  cloudnetpy_qc/py.typed
7
8
  cloudnetpy_qc/quality.py
8
9
  cloudnetpy_qc/utils.py
File without changes
File without changes