pypromice 1.3.3__tar.gz → 1.3.5__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.
Potentially problematic release.
This version of pypromice might be problematic. Click here for more details.
- {pypromice-1.3.3/src/pypromice.egg-info → pypromice-1.3.5}/PKG-INFO +2 -1
- {pypromice-1.3.3 → pypromice-1.3.5}/setup.py +7 -4
- pypromice-1.3.5/src/pypromice/postprocess/bufr_to_csv.py +11 -0
- pypromice-1.3.5/src/pypromice/postprocess/bufr_utilities.py +489 -0
- pypromice-1.3.5/src/pypromice/postprocess/get_bufr.py +629 -0
- pypromice-1.3.5/src/pypromice/postprocess/positions_seed.csv +5 -0
- pypromice-1.3.5/src/pypromice/postprocess/real_time_utilities.py +241 -0
- pypromice-1.3.5/src/pypromice/postprocess/station_configurations.toml +762 -0
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice/process/L0toL1.py +4 -2
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice/process/L1toL2.py +1 -0
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice/process/value_clipping.py +4 -13
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice/process/variables.csv +13 -15
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice/qc/github_data_issues.py +10 -40
- {pypromice-1.3.3 → pypromice-1.3.5/src/pypromice.egg-info}/PKG-INFO +2 -1
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice.egg-info/SOURCES.txt +5 -2
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice.egg-info/entry_points.txt +1 -1
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice.egg-info/requires.txt +1 -0
- pypromice-1.3.3/src/pypromice/postprocess/csv2bufr.py +0 -508
- pypromice-1.3.3/src/pypromice/postprocess/get_bufr.py +0 -291
- pypromice-1.3.3/src/pypromice/postprocess/wmo_config.py +0 -179
- {pypromice-1.3.3 → pypromice-1.3.5}/LICENSE.txt +0 -0
- {pypromice-1.3.3 → pypromice-1.3.5}/MANIFEST.in +0 -0
- {pypromice-1.3.3 → pypromice-1.3.5}/README.md +0 -0
- {pypromice-1.3.3 → pypromice-1.3.5}/setup.cfg +0 -0
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice/__init__.py +0 -0
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice/get/__init__.py +0 -0
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice/get/get.py +0 -0
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice/get/get_promice_data.py +0 -0
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice/postprocess/__init__.py +0 -0
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice/process/L2toL3.py +0 -0
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice/process/__init__.py +0 -0
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice/process/aws.py +0 -0
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice/process/get_l3.py +0 -0
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice/process/join_l3.py +0 -0
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice/process/metadata.csv +0 -0
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice/qc/__init__.py +0 -0
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice/qc/percentiles/__init__.py +0 -0
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice/qc/percentiles/compute_thresholds.py +0 -0
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice/qc/percentiles/outlier_detector.py +0 -0
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice/qc/percentiles/thresholds.csv +0 -0
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice/qc/persistence.py +0 -0
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice/qc/persistence_test.py +0 -0
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice/test/test_config1.toml +0 -0
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice/test/test_config2.toml +0 -0
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice/test/test_email +0 -0
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice/test/test_payload_formats.csv +0 -0
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice/test/test_payload_types.csv +0 -0
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice/test/test_percentile.py +0 -0
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice/test/test_raw1.txt +0 -0
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice/test/test_raw_DataTable2.txt +0 -0
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice/test/test_raw_SlimTableMem1.txt +0 -0
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice/test/test_raw_transmitted1.txt +0 -0
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice/test/test_raw_transmitted2.txt +0 -0
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice/tx/__init__.py +0 -0
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice/tx/get_l0tx.py +0 -0
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice/tx/get_msg.py +0 -0
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice/tx/get_watsontx.py +0 -0
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice/tx/payload_formats.csv +0 -0
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice/tx/payload_types.csv +0 -0
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice/tx/tx.py +0 -0
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice.egg-info/dependency_links.txt +0 -0
- {pypromice-1.3.3 → pypromice-1.3.5}/src/pypromice.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pypromice
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.5
|
|
4
4
|
Summary: PROMICE/GC-Net data processing toolbox
|
|
5
5
|
Home-page: https://github.com/GEUS-Glaciology-and-Climate/pypromice
|
|
6
6
|
Author: GEUS Glaciology and Climate
|
|
@@ -27,6 +27,7 @@ Requires-Dist: scikit-learn>=1.1.0
|
|
|
27
27
|
Requires-Dist: Bottleneck
|
|
28
28
|
Requires-Dist: netcdf4
|
|
29
29
|
Requires-Dist: pyDataverse
|
|
30
|
+
Requires-Dist: eccodes
|
|
30
31
|
|
|
31
32
|
# pypromice
|
|
32
33
|
[](https://badge.fury.io/py/pypromice) [](https://anaconda.org/conda-forge/pypromice) [](https://anaconda.org/conda-forge/pypromice) [](https://www.doi.org/10.22008/FK2/3TSBF0) [](https://doi.org/10.21105/joss.05298) [](https://pypromice.readthedocs.io/en/latest/?badge=latest)
|
|
@@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
|
|
|
5
5
|
|
|
6
6
|
setuptools.setup(
|
|
7
7
|
name="pypromice",
|
|
8
|
-
version="1.3.
|
|
8
|
+
version="1.3.5",
|
|
9
9
|
author="GEUS Glaciology and Climate",
|
|
10
10
|
description="PROMICE/GC-Net data processing toolbox",
|
|
11
11
|
long_description=long_description,
|
|
@@ -30,8 +30,11 @@ setuptools.setup(
|
|
|
30
30
|
include_package_data = True,
|
|
31
31
|
packages=setuptools.find_packages(where="src"),
|
|
32
32
|
python_requires=">=3.8",
|
|
33
|
-
package_data={
|
|
34
|
-
|
|
33
|
+
package_data={
|
|
34
|
+
"pypromice.qc.percentiles": ["thresholds.csv"],
|
|
35
|
+
"pypromice.postprocess": ["station_configurations.toml", "positions_seed.csv"],
|
|
36
|
+
},
|
|
37
|
+
install_requires=['numpy>=1.23.0', 'pandas>=1.5.0', 'xarray>=2022.6.0', 'toml', 'scipy>=1.9.0', 'scikit-learn>=1.1.0', 'Bottleneck', 'netcdf4', 'pyDataverse', 'eccodes'],
|
|
35
38
|
entry_points={
|
|
36
39
|
'console_scripts': [
|
|
37
40
|
'get_promice_data = pypromice.get.get_promice_data:get_promice_data',
|
|
@@ -39,7 +42,7 @@ setuptools.setup(
|
|
|
39
42
|
'get_l3 = pypromice.process.get_l3:get_l3',
|
|
40
43
|
'join_l3 = pypromice.process.join_l3:join_l3',
|
|
41
44
|
'get_watsontx = pypromice.tx.get_watsontx:get_watsontx',
|
|
42
|
-
'get_bufr = pypromice.postprocess.get_bufr:
|
|
45
|
+
'get_bufr = pypromice.postprocess.get_bufr:main',
|
|
43
46
|
'get_msg = pypromice.tx.get_msg:get_msg'
|
|
44
47
|
],
|
|
45
48
|
},
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from pypromice.postprocess.bufr_utilities import read_bufr_file
|
|
5
|
+
|
|
6
|
+
if __name__ == "__main__":
|
|
7
|
+
parser = argparse.ArgumentParser("BUFR to CSV converter")
|
|
8
|
+
parser.add_argument("path", type=Path)
|
|
9
|
+
args = parser.parse_args()
|
|
10
|
+
|
|
11
|
+
print(read_bufr_file(args.path).to_csv())
|
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions writing and reading BUFR files from AWS data
|
|
3
|
+
|
|
4
|
+
see documentation here:
|
|
5
|
+
https://confluence.ecmwf.int/display/ECC/Documentation
|
|
6
|
+
|
|
7
|
+
BUFR element table for WMO master table version 32
|
|
8
|
+
https://confluence.ecmwf.int/display/ECC/WMO%3D32+element+table
|
|
9
|
+
|
|
10
|
+
"""
|
|
11
|
+
import datetime
|
|
12
|
+
import logging
|
|
13
|
+
import math
|
|
14
|
+
from os import PathLike
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import BinaryIO, Optional
|
|
17
|
+
|
|
18
|
+
import attrs
|
|
19
|
+
import numpy as np
|
|
20
|
+
import pandas as pd
|
|
21
|
+
from eccodes import (
|
|
22
|
+
codes_set,
|
|
23
|
+
codes_write,
|
|
24
|
+
codes_release,
|
|
25
|
+
codes_bufr_new_from_samples,
|
|
26
|
+
CodesInternalError,
|
|
27
|
+
codes_is_defined,
|
|
28
|
+
codes_bufr_new_from_file,
|
|
29
|
+
codes_get,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"BUFRVariables",
|
|
36
|
+
"write_bufr_message",
|
|
37
|
+
"read_bufr_message",
|
|
38
|
+
"read_bufr_file",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def round_converter(decimals: int):
|
|
43
|
+
def round(value: float):
|
|
44
|
+
return np.round(value, decimals=decimals)
|
|
45
|
+
|
|
46
|
+
return round
|
|
47
|
+
|
|
48
|
+
# Enforce precision
|
|
49
|
+
# Note the sensor accuracies listed here:
|
|
50
|
+
# https://essd.copernicus.org/articles/13/3819/2021/#section8
|
|
51
|
+
# In addition to sensor accuracy, WMO requires pressure and heights
|
|
52
|
+
# to be reported at 0.1 precision.
|
|
53
|
+
@attrs.define(eq=False)
|
|
54
|
+
class BUFRVariables:
|
|
55
|
+
"""
|
|
56
|
+
Helper class for storing variables used for BUFR IO.
|
|
57
|
+
|
|
58
|
+
The field names reflect the key names in the BUFR template except:
|
|
59
|
+
|
|
60
|
+
* wmo_id: Stored as either as shipOrMobileLandStationIdentifier or stationNumber depending on the station type
|
|
61
|
+
* station_type: Determine the BUFR template
|
|
62
|
+
* timestamp: Stored separately as year, month, day, hour and minutes
|
|
63
|
+
* heightOfSensorAboveLocalGroundOrDeckOfMarinePlatformTempRH: Corresponds to "#1#heightOfSensorAboveLocalGroundOrDeckOfMarinePlatform" which is height if thermometer and hygrometer relative to ground or deck of marine platform.
|
|
64
|
+
* heightOfSensorAboveLocalGroundOrDeckOfMarinePlatformWSPD: Corresponds to "#7#heightOfSensorAboveLocalGroundOrDeckOfMarinePlatform" which is height if anemometer relative to ground or deck of marine platform.
|
|
65
|
+
|
|
66
|
+
"""
|
|
67
|
+
wmo_id: str
|
|
68
|
+
station_type: str
|
|
69
|
+
timestamp: datetime.datetime
|
|
70
|
+
relativeHumidity: float = attrs.field(converter=round_converter(0))
|
|
71
|
+
airTemperature: float = attrs.field(converter=round_converter(1))
|
|
72
|
+
pressure: float = attrs.field(converter=round_converter(1))
|
|
73
|
+
windDirection: float = attrs.field(converter=round_converter(0))
|
|
74
|
+
windSpeed: float = attrs.field(converter=round_converter(1))
|
|
75
|
+
latitude: float = attrs.field(converter=round_converter(6))
|
|
76
|
+
longitude: float = attrs.field(converter=round_converter(6))
|
|
77
|
+
heightOfStationGroundAboveMeanSeaLevel: float = attrs.field(
|
|
78
|
+
converter=round_converter(2)
|
|
79
|
+
)
|
|
80
|
+
#
|
|
81
|
+
heightOfBarometerAboveMeanSeaLevel: float = attrs.field(
|
|
82
|
+
converter=round_converter(2),
|
|
83
|
+
)
|
|
84
|
+
heightOfSensorAboveLocalGroundOrDeckOfMarinePlatformTempRH: float = attrs.field(
|
|
85
|
+
converter=round_converter(4),
|
|
86
|
+
)
|
|
87
|
+
heightOfSensorAboveLocalGroundOrDeckOfMarinePlatformWSPD: float = attrs.field(
|
|
88
|
+
converter=round_converter(4)
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def as_series(self) -> pd.Series:
|
|
92
|
+
return pd.Series(attrs.asdict(self))
|
|
93
|
+
|
|
94
|
+
def __eq__(self, other: "BUFRVariables"):
|
|
95
|
+
"""Use pandas series equals to allow nan values in comparison."""
|
|
96
|
+
return self.as_series().equals(other.as_series())
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
STATION_CONFIGURATIONS = {
|
|
100
|
+
"mobile": {
|
|
101
|
+
# 'blockNumber': 4, #4 is Greenland, 6 is Denmark; not valid with synopMobil template
|
|
102
|
+
"regionNumber": 6, # 6 is Europe, 7 is MISSING VALUE; not valid with synopLand template
|
|
103
|
+
"centre": 94, # 94 is Copenhagen
|
|
104
|
+
# 'agencyInChargeOfOperatingObservingPlatform': , #nothing for DMI or GEUS in code table
|
|
105
|
+
# 'wmoRegionSubArea': 1,
|
|
106
|
+
# 'stationOrSiteName': , #not valid with synopMobil template
|
|
107
|
+
# 'shortStationName': , #not valid with synopMobil template
|
|
108
|
+
# 'longStationName': , #not valid with synopMobil template
|
|
109
|
+
# 'directionOfMotionOfMovingObservingPlatform': ,
|
|
110
|
+
# 'movingObservingPlatformSpeed': ,
|
|
111
|
+
"stationType": 0, # automatic station
|
|
112
|
+
"instrumentationForWindMeasurement": 8, # certified instruments
|
|
113
|
+
"stationElevationQualityMarkForMobileStations": 1, # Excellent - within 3m; not valid with synopLand template
|
|
114
|
+
},
|
|
115
|
+
"land": {
|
|
116
|
+
"blockNumber": 4, # 4 is Greenland, 6 is Denmark; not valid with synopMobil template
|
|
117
|
+
# 'regionNumber': 6, #6 is Europe, 7 is MISSING VALUE; not valid with synopLand template
|
|
118
|
+
"centre": 94, # 94 is Copenhagen
|
|
119
|
+
# 'agencyInChargeOfOperatingObservingPlatform': , #nothing for DMI or GEUS in code table
|
|
120
|
+
# 'wmoRegionSubArea': 1,
|
|
121
|
+
# 'stationOrSiteName': , #not valid with synopMobil template
|
|
122
|
+
# 'shortStationName': , #not valid with synopMobil template
|
|
123
|
+
# 'longStationName': , #not valid with synopMobil template
|
|
124
|
+
"stationType": 0, # automatic station
|
|
125
|
+
"instrumentationForWindMeasurement": 8, # certified instruments
|
|
126
|
+
# 'stationElevationQualityMarkForMobileStations': 1, #Excellent - within 3m; not valid with synopLand template
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
BUFR_TEMPLATES = {
|
|
131
|
+
"mobile": {
|
|
132
|
+
"unexpandedDescriptors": (307090), # message template, "synopMobil"
|
|
133
|
+
"edition": 4, # latest edition
|
|
134
|
+
"masterTableNumber": 0,
|
|
135
|
+
"masterTablesVersionNumber": 32, # DMI recommends any table version between 28-32
|
|
136
|
+
"localTablesVersionNumber": 0,
|
|
137
|
+
"bufrHeaderCentre": 94, # originating centre 98=ECMWF, 94=DMI
|
|
138
|
+
# 'bufrHeaderSubCentre': 0,
|
|
139
|
+
"updateSequenceNumber": 0, # 0 is original message, incremented by 1 for updates
|
|
140
|
+
"dataCategory": 0, # surface data - land
|
|
141
|
+
"internationalDataSubCategory": 3, # hourly synoptic observations from mobile-land stations (SYNOP MOBIL)
|
|
142
|
+
# 'dataSubCategory': 0,
|
|
143
|
+
"observedData": 1,
|
|
144
|
+
"compressedData": 0,
|
|
145
|
+
},
|
|
146
|
+
"land": {
|
|
147
|
+
"unexpandedDescriptors": (307080), # message template, "synopLand"
|
|
148
|
+
"edition": 4, # latest edition
|
|
149
|
+
"masterTableNumber": 0,
|
|
150
|
+
"masterTablesVersionNumber": 32, # DMI recommends any table version between 28-32
|
|
151
|
+
"localTablesVersionNumber": 0,
|
|
152
|
+
"bufrHeaderCentre": 94, # originating centre 98=ECMWF, 94=DMI
|
|
153
|
+
# 'bufrHeaderSubCentre': 0,
|
|
154
|
+
"updateSequenceNumber": 0, # 0 is original message, incremented by 1 for updates
|
|
155
|
+
"dataCategory": 0, # surface data - land
|
|
156
|
+
"internationalDataSubCategory": 0, # Hourly synoptic observations from fixed-land stations (SYNOP)
|
|
157
|
+
# 'dataSubCategory': 0,
|
|
158
|
+
"observedData": 1,
|
|
159
|
+
"compressedData": 0,
|
|
160
|
+
},
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def write_bufr_message(
|
|
165
|
+
variables: BUFRVariables,
|
|
166
|
+
file: BinaryIO,
|
|
167
|
+
):
|
|
168
|
+
"""Construct and export .bufr message to file from pandas Series.
|
|
169
|
+
|
|
170
|
+
Parameters
|
|
171
|
+
----------
|
|
172
|
+
variables : pandas.Series
|
|
173
|
+
Pandas series of single most recent obset for a station
|
|
174
|
+
file
|
|
175
|
+
Binary writable file object
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
# Create new bufr message to write to
|
|
179
|
+
ibufr = codes_bufr_new_from_samples("BUFR4")
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
# we must pass all the following functions without error.
|
|
183
|
+
# If handled (or unhandled) errors occur, we re-raise and
|
|
184
|
+
# the exceptions below will set remove_file to True.
|
|
185
|
+
set_template(ibufr, variables.timestamp, variables.station_type)
|
|
186
|
+
set_station(ibufr, variables.station_type, variables.wmo_id)
|
|
187
|
+
set_AWS_variables(ibufr, variables)
|
|
188
|
+
|
|
189
|
+
# Encode keys in data section
|
|
190
|
+
codes_set(ibufr, "pack", 1)
|
|
191
|
+
|
|
192
|
+
# Write bufr message to bufr file
|
|
193
|
+
codes_write(ibufr, file)
|
|
194
|
+
|
|
195
|
+
except CodesInternalError as ec:
|
|
196
|
+
logger.exception(f"CodesInternalError in getBUFR", exc_info=ec)
|
|
197
|
+
raise ec
|
|
198
|
+
except Exception as e:
|
|
199
|
+
logger.exception(f"ERROR in getBUFR", exc_info=e)
|
|
200
|
+
raise e
|
|
201
|
+
finally:
|
|
202
|
+
codes_release(ibufr)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def set_template(ibufr, timestamp, station_type: str):
|
|
206
|
+
"""Set BUFR message template.
|
|
207
|
+
|
|
208
|
+
Parameters
|
|
209
|
+
----------
|
|
210
|
+
ibufr : bufr.msg
|
|
211
|
+
Bufr message object
|
|
212
|
+
timestamp : datetime.Datetime
|
|
213
|
+
Timestamp of observation
|
|
214
|
+
config_key : str
|
|
215
|
+
Defines which config dict to use in wmo_config.ibufr_settings, 'mobile' or 'land'
|
|
216
|
+
"""
|
|
217
|
+
template = BUFR_TEMPLATES[station_type]
|
|
218
|
+
|
|
219
|
+
for k, v in template.items():
|
|
220
|
+
if codes_is_defined(ibufr, k) == 1:
|
|
221
|
+
codes_set(ibufr, k, v)
|
|
222
|
+
else:
|
|
223
|
+
logger.warning("-----> setTemplate Key not defined: {}".format(k))
|
|
224
|
+
continue
|
|
225
|
+
|
|
226
|
+
codes_set(ibufr, "typicalYear", timestamp.year)
|
|
227
|
+
codes_set(ibufr, "typicalMonth", timestamp.month)
|
|
228
|
+
codes_set(ibufr, "typicalDay", timestamp.day)
|
|
229
|
+
codes_set(ibufr, "typicalHour", timestamp.hour)
|
|
230
|
+
codes_set(ibufr, "typicalMinute", timestamp.minute)
|
|
231
|
+
# codes_set(ibufr, 'typicalSecond', timestamp.second)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def set_station(ibufr, station_type: str, wmo_id: str):
|
|
235
|
+
"""Set station-specific info to bufr message.
|
|
236
|
+
|
|
237
|
+
Parameters
|
|
238
|
+
----------
|
|
239
|
+
ibufr : bufr.msg
|
|
240
|
+
Bufr message object
|
|
241
|
+
config_key : str
|
|
242
|
+
Defines which config dict to use in wmo_config.ibufr_settings, 'mobile' or 'land'
|
|
243
|
+
"""
|
|
244
|
+
if station_type == "mobile":
|
|
245
|
+
station_config = dict(shipOrMobileLandStationIdentifier=wmo_id)
|
|
246
|
+
elif station_type == "land":
|
|
247
|
+
# StationNumber for land stations are integeres
|
|
248
|
+
wmo_id_int = int(wmo_id)
|
|
249
|
+
station_config = dict(stationNumber=wmo_id_int)
|
|
250
|
+
else:
|
|
251
|
+
raise Exception(f"Unsupported station station type {station_type}")
|
|
252
|
+
station_config.update(STATION_CONFIGURATIONS[station_type])
|
|
253
|
+
|
|
254
|
+
for key, value in station_config.items():
|
|
255
|
+
codes_set(ibufr, key, value)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def set_AWS_variables(
|
|
259
|
+
ibufr,
|
|
260
|
+
variables: BUFRVariables,
|
|
261
|
+
):
|
|
262
|
+
"""Set AWS measurements to bufr message.
|
|
263
|
+
|
|
264
|
+
Parameters
|
|
265
|
+
----------
|
|
266
|
+
ibufr s: bufr.msg
|
|
267
|
+
Bufr message object
|
|
268
|
+
variables
|
|
269
|
+
Dict with AWS variable data
|
|
270
|
+
timestamp : datetime.datetime
|
|
271
|
+
timestamp for this row
|
|
272
|
+
"""
|
|
273
|
+
# Set timestamp fields
|
|
274
|
+
timestamp = variables.timestamp
|
|
275
|
+
set_bufr_value(ibufr, "year", timestamp.year)
|
|
276
|
+
set_bufr_value(ibufr, "month", timestamp.month)
|
|
277
|
+
set_bufr_value(ibufr, "day", timestamp.day)
|
|
278
|
+
set_bufr_value(ibufr, "hour", timestamp.hour)
|
|
279
|
+
set_bufr_value(ibufr, "minute", timestamp.minute)
|
|
280
|
+
|
|
281
|
+
set_bufr_value(ibufr, "relativeHumidity", variables.relativeHumidity)
|
|
282
|
+
set_bufr_value(ibufr, "airTemperature", variables.airTemperature)
|
|
283
|
+
set_bufr_value(ibufr, "pressure", variables.pressure)
|
|
284
|
+
set_bufr_value(ibufr, "windDirection", variables.windDirection)
|
|
285
|
+
set_bufr_value(ibufr, "windSpeed", variables.windSpeed)
|
|
286
|
+
|
|
287
|
+
set_bufr_value(ibufr, "latitude", variables.latitude)
|
|
288
|
+
|
|
289
|
+
# Set position metadata
|
|
290
|
+
set_bufr_value(ibufr, "latitude", variables.latitude)
|
|
291
|
+
set_bufr_value(ibufr, "longitude", variables.longitude)
|
|
292
|
+
set_bufr_value(
|
|
293
|
+
ibufr,
|
|
294
|
+
"heightOfStationGroundAboveMeanSeaLevel",
|
|
295
|
+
variables.heightOfStationGroundAboveMeanSeaLevel,
|
|
296
|
+
) # also height and heightOfStation?
|
|
297
|
+
|
|
298
|
+
# The ## in the codes_set() indicate the position in the BUFR for the parameter.
|
|
299
|
+
# e.g. #10#timePeriod will assign to the 10th occurence of "timePeriod", which corresponds
|
|
300
|
+
# to the wind speed section. Note that both the "synopMobil" and "synopLand" templates
|
|
301
|
+
# appear to have the same positions for all parameters that are set here.
|
|
302
|
+
# View the output BUFR to see section keys with 'bufr_dump filename.bufr'.
|
|
303
|
+
if math.isnan(variables.windSpeed) is False:
|
|
304
|
+
# Set time significance (2=temporally averaged)
|
|
305
|
+
codes_set(ibufr, "#1#timeSignificance", 2)
|
|
306
|
+
# Set monitoring time period (-10=10 minutes)
|
|
307
|
+
codes_set(ibufr, "#10#timePeriod", -10)
|
|
308
|
+
|
|
309
|
+
# Set measurement heights
|
|
310
|
+
set_bufr_value(
|
|
311
|
+
ibufr,
|
|
312
|
+
"#1#heightOfSensorAboveLocalGroundOrDeckOfMarinePlatform",
|
|
313
|
+
variables.heightOfSensorAboveLocalGroundOrDeckOfMarinePlatformTempRH,
|
|
314
|
+
)
|
|
315
|
+
set_bufr_value(
|
|
316
|
+
ibufr,
|
|
317
|
+
"#7#heightOfSensorAboveLocalGroundOrDeckOfMarinePlatform",
|
|
318
|
+
variables.heightOfSensorAboveLocalGroundOrDeckOfMarinePlatformWSPD,
|
|
319
|
+
)
|
|
320
|
+
set_bufr_value(
|
|
321
|
+
ibufr,
|
|
322
|
+
"heightOfBarometerAboveMeanSeaLevel",
|
|
323
|
+
variables.heightOfBarometerAboveMeanSeaLevel,
|
|
324
|
+
) # For pressure
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def set_bufr_value(ibufr, b_name, value):
|
|
328
|
+
"""Set variable in BUFR message
|
|
329
|
+
Called in setAWSvariables() to make sure we aren't passing NaNs
|
|
330
|
+
|
|
331
|
+
Parameters
|
|
332
|
+
----------
|
|
333
|
+
ibufr : bufr.msg
|
|
334
|
+
Active BUFR message
|
|
335
|
+
b_name : str
|
|
336
|
+
BUFR message variable name
|
|
337
|
+
value : int/float
|
|
338
|
+
Value to be assigned to variable
|
|
339
|
+
"""
|
|
340
|
+
if math.isnan(value) is False:
|
|
341
|
+
try:
|
|
342
|
+
codes_set(ibufr, b_name, value)
|
|
343
|
+
except CodesInternalError:
|
|
344
|
+
logger.exception(f"CodesInternalError for {b_name} == {value}")
|
|
345
|
+
raise # throw error back to getBUFR where it is handled
|
|
346
|
+
else:
|
|
347
|
+
logger.info(f"Variable {b_name} is {value}. Skipping")
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def get_bufr_value(msgid: int, key: str) -> float:
|
|
351
|
+
"""
|
|
352
|
+
Read and convert numeric BUFR values and interpret nan based on value.
|
|
353
|
+
|
|
354
|
+
Nan values are skipped in set_bufr_value. This means that they have a default value given by the template.
|
|
355
|
+
|
|
356
|
+
* int: 2147483647 == 2**31 -1
|
|
357
|
+
* float: -1e100
|
|
358
|
+
|
|
359
|
+
Note: windDirection and relativeHumidity are serialized as integer in the BUFR message.
|
|
360
|
+
"""
|
|
361
|
+
value = codes_get(msgid, key)
|
|
362
|
+
|
|
363
|
+
if isinstance(value, int):
|
|
364
|
+
if value > 2**30:
|
|
365
|
+
return np.nan
|
|
366
|
+
return value
|
|
367
|
+
elif isinstance(value, float):
|
|
368
|
+
if value == -1e100:
|
|
369
|
+
return np.nan
|
|
370
|
+
return value
|
|
371
|
+
else:
|
|
372
|
+
raise ValueError(f"Unsupported BUFR value type {type(value)} for key {key}")
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def read_bufr_message(fp: BinaryIO) -> Optional[BUFRVariables]:
|
|
376
|
+
"""
|
|
377
|
+
Read and parse BUFR message from binary IO stream.
|
|
378
|
+
|
|
379
|
+
Extract AWS variables similar to the input to bufr_utilities.write_bufr_message.
|
|
380
|
+
Note: stid is not written to the BUFR file hence it will be set to None in the output.
|
|
381
|
+
|
|
382
|
+
Parameters
|
|
383
|
+
----------
|
|
384
|
+
fp
|
|
385
|
+
Readable binary io stream
|
|
386
|
+
|
|
387
|
+
Returns
|
|
388
|
+
-------
|
|
389
|
+
BUFRVariables
|
|
390
|
+
AWS variables or None if there are no messages in stream
|
|
391
|
+
"""
|
|
392
|
+
ibufr = codes_bufr_new_from_file(fp)
|
|
393
|
+
if ibufr is None:
|
|
394
|
+
return None
|
|
395
|
+
codes_set(ibufr, "unpack", 1)
|
|
396
|
+
|
|
397
|
+
year = codes_get(
|
|
398
|
+
ibufr,
|
|
399
|
+
"year",
|
|
400
|
+
)
|
|
401
|
+
month = codes_get(
|
|
402
|
+
ibufr,
|
|
403
|
+
"month",
|
|
404
|
+
)
|
|
405
|
+
day = codes_get(
|
|
406
|
+
ibufr,
|
|
407
|
+
"day",
|
|
408
|
+
)
|
|
409
|
+
hour = codes_get(
|
|
410
|
+
ibufr,
|
|
411
|
+
"hour",
|
|
412
|
+
)
|
|
413
|
+
minute = codes_get(
|
|
414
|
+
ibufr,
|
|
415
|
+
"minute",
|
|
416
|
+
)
|
|
417
|
+
timestamp = datetime.datetime(
|
|
418
|
+
year=year, month=month, day=day, hour=hour, minute=minute
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
# Determine template
|
|
422
|
+
unexpanded_descriptors = codes_get(ibufr, "unexpandedDescriptors")
|
|
423
|
+
if unexpanded_descriptors == 307090:
|
|
424
|
+
# "synopMobil"
|
|
425
|
+
station_type = "mobile"
|
|
426
|
+
wmo_id = codes_get(ibufr, "shipOrMobileLandStationIdentifier")
|
|
427
|
+
elif unexpanded_descriptors == 307080:
|
|
428
|
+
# "synopLand"
|
|
429
|
+
station_type = "land"
|
|
430
|
+
# Note: stationNumber is an integer
|
|
431
|
+
station_number = codes_get(ibufr, "stationNumber")
|
|
432
|
+
wmo_id = str(station_number)
|
|
433
|
+
else:
|
|
434
|
+
raise ValueError(
|
|
435
|
+
f"Unknown BUFR template unexpandedDescriptors: {unexpanded_descriptors}"
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
variables = BUFRVariables(
|
|
439
|
+
timestamp=timestamp,
|
|
440
|
+
relativeHumidity=get_bufr_value(ibufr, "relativeHumidity"),
|
|
441
|
+
airTemperature=get_bufr_value(ibufr, "airTemperature"),
|
|
442
|
+
pressure=get_bufr_value(ibufr, "pressure"),
|
|
443
|
+
windDirection=get_bufr_value(ibufr, "windDirection"),
|
|
444
|
+
windSpeed=get_bufr_value(ibufr, "windSpeed"),
|
|
445
|
+
latitude=get_bufr_value(ibufr, "latitude"),
|
|
446
|
+
longitude=get_bufr_value(ibufr, "longitude"),
|
|
447
|
+
heightOfStationGroundAboveMeanSeaLevel=get_bufr_value(
|
|
448
|
+
ibufr, "heightOfStationGroundAboveMeanSeaLevel"
|
|
449
|
+
),
|
|
450
|
+
heightOfSensorAboveLocalGroundOrDeckOfMarinePlatformTempRH=get_bufr_value(
|
|
451
|
+
ibufr, "#1#heightOfSensorAboveLocalGroundOrDeckOfMarinePlatform"
|
|
452
|
+
),
|
|
453
|
+
heightOfSensorAboveLocalGroundOrDeckOfMarinePlatformWSPD=get_bufr_value(
|
|
454
|
+
ibufr, "#7#heightOfSensorAboveLocalGroundOrDeckOfMarinePlatform"
|
|
455
|
+
),
|
|
456
|
+
heightOfBarometerAboveMeanSeaLevel=get_bufr_value(
|
|
457
|
+
ibufr, "heightOfBarometerAboveMeanSeaLevel"
|
|
458
|
+
),
|
|
459
|
+
wmo_id=wmo_id,
|
|
460
|
+
station_type=station_type,
|
|
461
|
+
)
|
|
462
|
+
codes_release(ibufr)
|
|
463
|
+
|
|
464
|
+
return variables
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def read_bufr_file(path: PathLike) -> pd.DataFrame:
|
|
468
|
+
"""
|
|
469
|
+
Read aws data from all messages in a bufr file.
|
|
470
|
+
|
|
471
|
+
Parameters
|
|
472
|
+
----------
|
|
473
|
+
path : PathLike
|
|
474
|
+
Path to bufr file
|
|
475
|
+
|
|
476
|
+
Returns
|
|
477
|
+
-------
|
|
478
|
+
pd.DataFrame
|
|
479
|
+
|
|
480
|
+
"""
|
|
481
|
+
path = Path(path)
|
|
482
|
+
lines = []
|
|
483
|
+
with path.open("rb") as fp:
|
|
484
|
+
while True:
|
|
485
|
+
message_vars = read_bufr_message(fp)
|
|
486
|
+
if message_vars is None:
|
|
487
|
+
break
|
|
488
|
+
lines.append(message_vars)
|
|
489
|
+
return pd.DataFrame(lines).rename_axis("message_index")
|