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.
- {cloudnetpy_qc-1.24.3/cloudnetpy_qc.egg-info → cloudnetpy_qc-1.25.0}/PKG-INFO +2 -1
- {cloudnetpy_qc-1.24.3 → cloudnetpy_qc-1.25.0}/README.md +1 -0
- cloudnetpy_qc-1.25.0/cloudnetpy_qc/coverage.py +66 -0
- {cloudnetpy_qc-1.24.3 → cloudnetpy_qc-1.25.0}/cloudnetpy_qc/quality.py +13 -52
- {cloudnetpy_qc-1.24.3 → cloudnetpy_qc-1.25.0}/cloudnetpy_qc/version.py +2 -2
- {cloudnetpy_qc-1.24.3 → cloudnetpy_qc-1.25.0/cloudnetpy_qc.egg-info}/PKG-INFO +2 -1
- {cloudnetpy_qc-1.24.3 → cloudnetpy_qc-1.25.0}/cloudnetpy_qc.egg-info/SOURCES.txt +1 -0
- {cloudnetpy_qc-1.24.3 → cloudnetpy_qc-1.25.0}/LICENSE +0 -0
- {cloudnetpy_qc-1.24.3 → cloudnetpy_qc-1.25.0}/MANIFEST.in +0 -0
- {cloudnetpy_qc-1.24.3 → cloudnetpy_qc-1.25.0}/cloudnetpy_qc/__init__.py +0 -0
- {cloudnetpy_qc-1.24.3 → cloudnetpy_qc-1.25.0}/cloudnetpy_qc/data/area-type-table.xml +0 -0
- {cloudnetpy_qc-1.24.3 → cloudnetpy_qc-1.25.0}/cloudnetpy_qc/data/cf-standard-name-table.xml +0 -0
- {cloudnetpy_qc-1.24.3 → cloudnetpy_qc-1.25.0}/cloudnetpy_qc/data/data_quality_config.ini +0 -0
- {cloudnetpy_qc-1.24.3 → cloudnetpy_qc-1.25.0}/cloudnetpy_qc/data/standardized-region-list.xml +0 -0
- {cloudnetpy_qc-1.24.3 → cloudnetpy_qc-1.25.0}/cloudnetpy_qc/py.typed +0 -0
- {cloudnetpy_qc-1.24.3 → cloudnetpy_qc-1.25.0}/cloudnetpy_qc/utils.py +0 -0
- {cloudnetpy_qc-1.24.3 → cloudnetpy_qc-1.25.0}/cloudnetpy_qc/variables.py +0 -0
- {cloudnetpy_qc-1.24.3 → cloudnetpy_qc-1.25.0}/cloudnetpy_qc.egg-info/dependency_links.txt +0 -0
- {cloudnetpy_qc-1.24.3 → cloudnetpy_qc-1.25.0}/cloudnetpy_qc.egg-info/requires.txt +0 -0
- {cloudnetpy_qc-1.24.3 → cloudnetpy_qc-1.25.0}/cloudnetpy_qc.egg-info/top_level.txt +0 -0
- {cloudnetpy_qc-1.24.3 → cloudnetpy_qc-1.25.0}/pyproject.toml +0 -0
- {cloudnetpy_qc-1.24.3 → cloudnetpy_qc-1.25.0}/setup.cfg +0 -0
- {cloudnetpy_qc-1.24.3 → cloudnetpy_qc-1.25.0}/tests/test_qc.py +0 -0
- {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.
|
|
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
|
|
|
@@ -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
|
-
|
|
312
|
-
|
|
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
|
-
|
|
318
|
-
|
|
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.
|
|
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
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cloudnetpy_qc
|
|
3
|
-
Version: 1.
|
|
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
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudnetpy_qc-1.24.3 → cloudnetpy_qc-1.25.0}/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
|
|
File without changes
|
|
File without changes
|