pypromice 1.3.2__tar.gz → 1.3.4__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.

Files changed (62) hide show
  1. {pypromice-1.3.2/src/pypromice.egg-info → pypromice-1.3.4}/PKG-INFO +2 -1
  2. {pypromice-1.3.2 → pypromice-1.3.4}/setup.py +7 -4
  3. pypromice-1.3.4/src/pypromice/postprocess/bufr_to_csv.py +11 -0
  4. pypromice-1.3.4/src/pypromice/postprocess/bufr_utilities.py +489 -0
  5. pypromice-1.3.4/src/pypromice/postprocess/get_bufr.py +629 -0
  6. pypromice-1.3.4/src/pypromice/postprocess/positions_seed.csv +5 -0
  7. pypromice-1.3.4/src/pypromice/postprocess/real_time_utilities.py +241 -0
  8. pypromice-1.3.4/src/pypromice/postprocess/station_configurations.toml +762 -0
  9. {pypromice-1.3.2 → pypromice-1.3.4}/src/pypromice/process/L0toL1.py +44 -4
  10. {pypromice-1.3.2 → pypromice-1.3.4}/src/pypromice/process/value_clipping.py +6 -13
  11. {pypromice-1.3.2 → pypromice-1.3.4}/src/pypromice/process/variables.csv +13 -15
  12. {pypromice-1.3.2 → pypromice-1.3.4}/src/pypromice/qc/github_data_issues.py +61 -89
  13. {pypromice-1.3.2 → pypromice-1.3.4/src/pypromice.egg-info}/PKG-INFO +2 -1
  14. {pypromice-1.3.2 → pypromice-1.3.4}/src/pypromice.egg-info/SOURCES.txt +5 -2
  15. {pypromice-1.3.2 → pypromice-1.3.4}/src/pypromice.egg-info/entry_points.txt +1 -1
  16. {pypromice-1.3.2 → pypromice-1.3.4}/src/pypromice.egg-info/requires.txt +1 -0
  17. pypromice-1.3.2/src/pypromice/postprocess/csv2bufr.py +0 -508
  18. pypromice-1.3.2/src/pypromice/postprocess/get_bufr.py +0 -291
  19. pypromice-1.3.2/src/pypromice/postprocess/wmo_config.py +0 -179
  20. {pypromice-1.3.2 → pypromice-1.3.4}/LICENSE.txt +0 -0
  21. {pypromice-1.3.2 → pypromice-1.3.4}/MANIFEST.in +0 -0
  22. {pypromice-1.3.2 → pypromice-1.3.4}/README.md +0 -0
  23. {pypromice-1.3.2 → pypromice-1.3.4}/setup.cfg +0 -0
  24. {pypromice-1.3.2 → pypromice-1.3.4}/src/pypromice/__init__.py +0 -0
  25. {pypromice-1.3.2 → pypromice-1.3.4}/src/pypromice/get/__init__.py +0 -0
  26. {pypromice-1.3.2 → pypromice-1.3.4}/src/pypromice/get/get.py +0 -0
  27. {pypromice-1.3.2 → pypromice-1.3.4}/src/pypromice/get/get_promice_data.py +0 -0
  28. {pypromice-1.3.2 → pypromice-1.3.4}/src/pypromice/postprocess/__init__.py +0 -0
  29. {pypromice-1.3.2 → pypromice-1.3.4}/src/pypromice/process/L1toL2.py +0 -0
  30. {pypromice-1.3.2 → pypromice-1.3.4}/src/pypromice/process/L2toL3.py +0 -0
  31. {pypromice-1.3.2 → pypromice-1.3.4}/src/pypromice/process/__init__.py +0 -0
  32. {pypromice-1.3.2 → pypromice-1.3.4}/src/pypromice/process/aws.py +0 -0
  33. {pypromice-1.3.2 → pypromice-1.3.4}/src/pypromice/process/get_l3.py +0 -0
  34. {pypromice-1.3.2 → pypromice-1.3.4}/src/pypromice/process/join_l3.py +0 -0
  35. {pypromice-1.3.2 → pypromice-1.3.4}/src/pypromice/process/metadata.csv +0 -0
  36. {pypromice-1.3.2 → pypromice-1.3.4}/src/pypromice/qc/__init__.py +0 -0
  37. {pypromice-1.3.2 → pypromice-1.3.4}/src/pypromice/qc/percentiles/__init__.py +0 -0
  38. {pypromice-1.3.2 → pypromice-1.3.4}/src/pypromice/qc/percentiles/compute_thresholds.py +0 -0
  39. {pypromice-1.3.2 → pypromice-1.3.4}/src/pypromice/qc/percentiles/outlier_detector.py +0 -0
  40. {pypromice-1.3.2 → pypromice-1.3.4}/src/pypromice/qc/percentiles/thresholds.csv +0 -0
  41. {pypromice-1.3.2 → pypromice-1.3.4}/src/pypromice/qc/persistence.py +0 -0
  42. {pypromice-1.3.2 → pypromice-1.3.4}/src/pypromice/qc/persistence_test.py +0 -0
  43. {pypromice-1.3.2 → pypromice-1.3.4}/src/pypromice/test/test_config1.toml +0 -0
  44. {pypromice-1.3.2 → pypromice-1.3.4}/src/pypromice/test/test_config2.toml +0 -0
  45. {pypromice-1.3.2 → pypromice-1.3.4}/src/pypromice/test/test_email +0 -0
  46. {pypromice-1.3.2 → pypromice-1.3.4}/src/pypromice/test/test_payload_formats.csv +0 -0
  47. {pypromice-1.3.2 → pypromice-1.3.4}/src/pypromice/test/test_payload_types.csv +0 -0
  48. {pypromice-1.3.2 → pypromice-1.3.4}/src/pypromice/test/test_percentile.py +0 -0
  49. {pypromice-1.3.2 → pypromice-1.3.4}/src/pypromice/test/test_raw1.txt +0 -0
  50. {pypromice-1.3.2 → pypromice-1.3.4}/src/pypromice/test/test_raw_DataTable2.txt +0 -0
  51. {pypromice-1.3.2 → pypromice-1.3.4}/src/pypromice/test/test_raw_SlimTableMem1.txt +0 -0
  52. {pypromice-1.3.2 → pypromice-1.3.4}/src/pypromice/test/test_raw_transmitted1.txt +0 -0
  53. {pypromice-1.3.2 → pypromice-1.3.4}/src/pypromice/test/test_raw_transmitted2.txt +0 -0
  54. {pypromice-1.3.2 → pypromice-1.3.4}/src/pypromice/tx/__init__.py +0 -0
  55. {pypromice-1.3.2 → pypromice-1.3.4}/src/pypromice/tx/get_l0tx.py +0 -0
  56. {pypromice-1.3.2 → pypromice-1.3.4}/src/pypromice/tx/get_msg.py +0 -0
  57. {pypromice-1.3.2 → pypromice-1.3.4}/src/pypromice/tx/get_watsontx.py +0 -0
  58. {pypromice-1.3.2 → pypromice-1.3.4}/src/pypromice/tx/payload_formats.csv +0 -0
  59. {pypromice-1.3.2 → pypromice-1.3.4}/src/pypromice/tx/payload_types.csv +0 -0
  60. {pypromice-1.3.2 → pypromice-1.3.4}/src/pypromice/tx/tx.py +0 -0
  61. {pypromice-1.3.2 → pypromice-1.3.4}/src/pypromice.egg-info/dependency_links.txt +0 -0
  62. {pypromice-1.3.2 → pypromice-1.3.4}/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.2
3
+ Version: 1.3.4
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
  [![PyPI version](https://badge.fury.io/py/pypromice.svg)](https://badge.fury.io/py/pypromice) [![Anaconda-Server Badge](https://anaconda.org/conda-forge/pypromice/badges/version.svg)](https://anaconda.org/conda-forge/pypromice) [![Anaconda-Server Badge](https://anaconda.org/conda-forge/pypromice/badges/platforms.svg)](https://anaconda.org/conda-forge/pypromice) [![](<https://img.shields.io/badge/Dataverse DOI-10.22008/FK2/3TSBF0-orange>)](https://www.doi.org/10.22008/FK2/3TSBF0) [![DOI](https://joss.theoj.org/papers/10.21105/joss.05298/status.svg)](https://doi.org/10.21105/joss.05298) [![Documentation Status](https://readthedocs.org/projects/pypromice/badge/?version=latest)](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.2",
8
+ version="1.3.4",
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={"pypromice.qc.percentiles": ["thresholds.csv"]},
34
- 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'],
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: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")