pypromice 1.3.6__py3-none-any.whl → 1.4.0__py3-none-any.whl
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/postprocess/bufr_to_csv.py +6 -1
- pypromice/postprocess/bufr_utilities.py +91 -18
- pypromice/postprocess/create_bufr_files.py +178 -0
- pypromice/postprocess/get_bufr.py +248 -397
- pypromice/postprocess/make_metadata_csv.py +214 -0
- pypromice/postprocess/real_time_utilities.py +41 -11
- pypromice/process/L0toL1.py +12 -5
- pypromice/process/L1toL2.py +69 -14
- pypromice/process/L2toL3.py +1033 -186
- pypromice/process/aws.py +130 -808
- pypromice/process/get_l2.py +90 -0
- pypromice/process/get_l2tol3.py +111 -0
- pypromice/process/join_l2.py +112 -0
- pypromice/process/join_l3.py +551 -120
- pypromice/process/load.py +161 -0
- pypromice/process/resample.py +128 -0
- pypromice/process/utilities.py +68 -0
- pypromice/process/write.py +503 -0
- pypromice/qc/github_data_issues.py +10 -16
- pypromice/qc/persistence.py +52 -30
- pypromice/resources/__init__.py +28 -0
- pypromice/{process/metadata.csv → resources/file_attributes.csv} +0 -2
- pypromice/resources/variable_aliases_GC-Net.csv +78 -0
- pypromice/resources/variables.csv +106 -0
- pypromice/station_configuration.py +118 -0
- pypromice/tx/get_l0tx.py +7 -4
- pypromice/tx/payload_formats.csv +1 -0
- pypromice/tx/tx.py +27 -6
- pypromice/utilities/__init__.py +0 -0
- pypromice/utilities/git.py +61 -0
- {pypromice-1.3.6.dist-info → pypromice-1.4.0.dist-info}/METADATA +3 -3
- pypromice-1.4.0.dist-info/RECORD +53 -0
- {pypromice-1.3.6.dist-info → pypromice-1.4.0.dist-info}/WHEEL +1 -1
- pypromice-1.4.0.dist-info/entry_points.txt +13 -0
- pypromice/postprocess/station_configurations.toml +0 -762
- pypromice/process/get_l3.py +0 -46
- pypromice/process/variables.csv +0 -92
- pypromice/qc/persistence_test.py +0 -150
- pypromice/test/test_config1.toml +0 -69
- pypromice/test/test_config2.toml +0 -54
- pypromice/test/test_email +0 -75
- pypromice/test/test_payload_formats.csv +0 -4
- pypromice/test/test_payload_types.csv +0 -7
- pypromice/test/test_percentile.py +0 -229
- pypromice/test/test_raw1.txt +0 -4468
- pypromice/test/test_raw_DataTable2.txt +0 -11167
- pypromice/test/test_raw_SlimTableMem1.txt +0 -1155
- pypromice/test/test_raw_transmitted1.txt +0 -15411
- pypromice/test/test_raw_transmitted2.txt +0 -28
- pypromice-1.3.6.dist-info/RECORD +0 -53
- pypromice-1.3.6.dist-info/entry_points.txt +0 -8
- {pypromice-1.3.6.dist-info → pypromice-1.4.0.dist-info}/LICENSE.txt +0 -0
- {pypromice-1.3.6.dist-info → pypromice-1.4.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Module containing all the functions needed to prepare and AWS data
|
|
5
|
+
"""
|
|
6
|
+
import datetime
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
import pandas as pd
|
|
13
|
+
from pypromice.process.resample import resample_dataset
|
|
14
|
+
import pypromice.resources
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def prepare_and_write(
|
|
20
|
+
dataset, output_path: Path | str, vars_df=None, meta_dict=None, time="60min", resample=True
|
|
21
|
+
):
|
|
22
|
+
"""Prepare data with resampling, formating and metadata population; then
|
|
23
|
+
write data to .nc and .csv hourly and daily files
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
dataset : xarray.Dataset
|
|
28
|
+
Dataset to write to file
|
|
29
|
+
output_path : Path|str
|
|
30
|
+
Output directory
|
|
31
|
+
vars_df : pandas.DataFrame
|
|
32
|
+
Variables look-up table dataframe
|
|
33
|
+
meta_dict : dictionary
|
|
34
|
+
Metadata dictionary to write to dataset
|
|
35
|
+
time : str
|
|
36
|
+
Resampling interval for output dataset
|
|
37
|
+
"""
|
|
38
|
+
# Resample dataset
|
|
39
|
+
if isinstance(output_path, str):
|
|
40
|
+
output_path = Path(output_path)
|
|
41
|
+
|
|
42
|
+
if resample:
|
|
43
|
+
d2 = resample_dataset(dataset, time)
|
|
44
|
+
logger.info("Resampling to " + str(time))
|
|
45
|
+
if len(d2.time) == 1:
|
|
46
|
+
logger.warning(
|
|
47
|
+
"Output of resample has length 1. Not enough data to calculate daily/monthly average."
|
|
48
|
+
)
|
|
49
|
+
return None
|
|
50
|
+
else:
|
|
51
|
+
d2 = dataset.copy()
|
|
52
|
+
|
|
53
|
+
# Reformat time
|
|
54
|
+
d2 = reformat_time(d2)
|
|
55
|
+
|
|
56
|
+
# finding station/site name
|
|
57
|
+
if "station_id" in d2.attrs.keys():
|
|
58
|
+
name = d2.attrs["station_id"]
|
|
59
|
+
else:
|
|
60
|
+
name = d2.attrs["site_id"]
|
|
61
|
+
|
|
62
|
+
# Reformat longitude (to negative values)
|
|
63
|
+
if "gps_lon" in d2.keys():
|
|
64
|
+
d2 = reformat_lon(d2)
|
|
65
|
+
else:
|
|
66
|
+
logger.info("%s does not have gps_lon" % name)
|
|
67
|
+
|
|
68
|
+
# Add variable attributes and metadata
|
|
69
|
+
if vars_df is None:
|
|
70
|
+
vars_df = pypromice.resources.load_variables()
|
|
71
|
+
if meta_dict is None:
|
|
72
|
+
meta_dict = pypromice.resources.load_metadata()
|
|
73
|
+
|
|
74
|
+
d2 = addVars(d2, vars_df)
|
|
75
|
+
d2 = addMeta(d2, meta_dict)
|
|
76
|
+
|
|
77
|
+
# Round all values to specified decimals places
|
|
78
|
+
d2 = roundValues(d2, vars_df)
|
|
79
|
+
|
|
80
|
+
# Get variable names to write out
|
|
81
|
+
if "site_id" in d2.attrs.keys():
|
|
82
|
+
remove_nan_fields = True
|
|
83
|
+
else:
|
|
84
|
+
remove_nan_fields = False
|
|
85
|
+
col_names = getColNames(vars_df, d2, remove_nan_fields=remove_nan_fields)
|
|
86
|
+
|
|
87
|
+
# Define filename based on resample rate
|
|
88
|
+
t = int(pd.Timedelta((d2["time"][1] - d2["time"][0]).values).total_seconds())
|
|
89
|
+
|
|
90
|
+
# Create out directory
|
|
91
|
+
output_dir = output_path / name
|
|
92
|
+
output_dir.mkdir(exist_ok=True, parents=True)
|
|
93
|
+
|
|
94
|
+
if t == 600:
|
|
95
|
+
out_csv = output_dir / f"{name}_10min.csv"
|
|
96
|
+
out_nc = output_dir / f"{name}_10min.nc"
|
|
97
|
+
elif t == 3600:
|
|
98
|
+
out_csv = output_dir / f"{name}_hour.csv"
|
|
99
|
+
out_nc = output_dir / f"{name}_hour.nc"
|
|
100
|
+
elif t == 86400:
|
|
101
|
+
# removing instantaneous values from daily and monthly files
|
|
102
|
+
for v in col_names:
|
|
103
|
+
if ("_i" in v) and ("_i_" not in v):
|
|
104
|
+
col_names.remove(v)
|
|
105
|
+
out_csv = output_dir / f"{name}_day.csv"
|
|
106
|
+
out_nc = output_dir / f"{name}_day.nc"
|
|
107
|
+
else:
|
|
108
|
+
# removing instantaneous values from daily and monthly files
|
|
109
|
+
for v in col_names:
|
|
110
|
+
if ("_i" in v) and ("_i_" not in v):
|
|
111
|
+
col_names.remove(v)
|
|
112
|
+
out_csv = output_dir / f"{name}_month.csv"
|
|
113
|
+
out_nc = output_dir / f"{name}_month.nc"
|
|
114
|
+
|
|
115
|
+
# Write to csv file
|
|
116
|
+
logger.info("Writing to files...")
|
|
117
|
+
writeCSV(out_csv, d2, col_names)
|
|
118
|
+
|
|
119
|
+
# Write to netcdf file
|
|
120
|
+
writeNC(out_nc, d2, col_names)
|
|
121
|
+
logger.info(f"Written to {out_csv}")
|
|
122
|
+
logger.info(f"Written to {out_nc}")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def writeAll(outpath, station_id, l3_h, l3_d, l3_m, csv_order=None):
|
|
126
|
+
"""Write L3 hourly, daily and monthly datasets to .nc and .csv
|
|
127
|
+
files
|
|
128
|
+
|
|
129
|
+
Parameters
|
|
130
|
+
----------
|
|
131
|
+
outpath : str
|
|
132
|
+
Output file path
|
|
133
|
+
station_id : str
|
|
134
|
+
Station name
|
|
135
|
+
l3_h : xr.Dataset
|
|
136
|
+
L3 hourly data
|
|
137
|
+
l3_d : xr.Dataset
|
|
138
|
+
L3 daily data
|
|
139
|
+
l3_m : xr.Dataset
|
|
140
|
+
L3 monthly data
|
|
141
|
+
csv_order : list, optional
|
|
142
|
+
List order of variables
|
|
143
|
+
"""
|
|
144
|
+
if not os.path.isdir(outpath):
|
|
145
|
+
os.mkdir(outpath)
|
|
146
|
+
outfile_h = os.path.join(outpath, station_id + "_hour")
|
|
147
|
+
outfile_d = os.path.join(outpath, station_id + "_day")
|
|
148
|
+
outfile_m = os.path.join(outpath, station_id + "_month")
|
|
149
|
+
for o, l in zip([outfile_h, outfile_d, outfile_m], [l3_h, l3_d, l3_m]):
|
|
150
|
+
writeCSV(o + ".csv", l, csv_order)
|
|
151
|
+
writeNC(o + ".nc", l)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def writeCSV(outfile, Lx, csv_order):
|
|
155
|
+
"""Write data product to CSV file
|
|
156
|
+
|
|
157
|
+
Parameters
|
|
158
|
+
----------
|
|
159
|
+
outfile : str
|
|
160
|
+
Output file path
|
|
161
|
+
Lx : xr.Dataset
|
|
162
|
+
Dataset to write to file
|
|
163
|
+
csv_order : list
|
|
164
|
+
List order of variables
|
|
165
|
+
"""
|
|
166
|
+
Lcsv = Lx.to_dataframe().dropna(how="all")
|
|
167
|
+
if csv_order is not None:
|
|
168
|
+
names = [c for c in csv_order if c in list(Lcsv.columns)]
|
|
169
|
+
Lcsv = Lcsv[names]
|
|
170
|
+
Lcsv.to_csv(outfile)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def writeNC(outfile, Lx, col_names=None):
|
|
174
|
+
"""Write data product to NetCDF file
|
|
175
|
+
|
|
176
|
+
Parameters
|
|
177
|
+
----------
|
|
178
|
+
outfile : str
|
|
179
|
+
Output file path
|
|
180
|
+
Lx : xr.Dataset
|
|
181
|
+
Dataset to write to file
|
|
182
|
+
"""
|
|
183
|
+
if os.path.isfile(outfile):
|
|
184
|
+
os.remove(outfile)
|
|
185
|
+
if col_names is not None:
|
|
186
|
+
names = [c for c in col_names if c in list(Lx.keys())]
|
|
187
|
+
else:
|
|
188
|
+
names = list(Lx.keys())
|
|
189
|
+
|
|
190
|
+
Lx[names].to_netcdf(outfile, mode="w", format="NETCDF4", compute=True)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def getColNames(vars_df, ds, remove_nan_fields=False):
|
|
194
|
+
"""
|
|
195
|
+
Get variable names for a given dataset with respect to its type and processing level
|
|
196
|
+
|
|
197
|
+
The dataset must have the the following attributes:
|
|
198
|
+
* level
|
|
199
|
+
* number_of_booms when the processing level is <= 2
|
|
200
|
+
|
|
201
|
+
This is mainly for exporting purposes.
|
|
202
|
+
|
|
203
|
+
Parameters
|
|
204
|
+
-------
|
|
205
|
+
list
|
|
206
|
+
Variable names
|
|
207
|
+
"""
|
|
208
|
+
# selecting variable list based on level
|
|
209
|
+
vars_df = vars_df.loc[vars_df[ds.attrs["level"]] == 1]
|
|
210
|
+
|
|
211
|
+
# selecting variable list based on geometry
|
|
212
|
+
if ds.attrs["level"] in ["L0", "L1", "L2"]:
|
|
213
|
+
if ds.attrs["number_of_booms"] == 1:
|
|
214
|
+
vars_df = vars_df.loc[vars_df["station_type"].isin(["one-boom", "all"])]
|
|
215
|
+
elif ds.attrs["number_of_booms"] == 2:
|
|
216
|
+
vars_df = vars_df.loc[vars_df["station_type"].isin(["two-boom", "all"])]
|
|
217
|
+
|
|
218
|
+
var_list = list(vars_df.index)
|
|
219
|
+
if remove_nan_fields:
|
|
220
|
+
for v in var_list:
|
|
221
|
+
if v not in ds.keys():
|
|
222
|
+
var_list.remove(v)
|
|
223
|
+
continue
|
|
224
|
+
if ds[v].isnull().all():
|
|
225
|
+
var_list.remove(v)
|
|
226
|
+
return var_list
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def addVars(ds, variables):
|
|
230
|
+
"""Add variable attributes from file to dataset
|
|
231
|
+
|
|
232
|
+
Parameters
|
|
233
|
+
----------
|
|
234
|
+
ds : xarray.Dataset
|
|
235
|
+
Dataset to add variable attributes to
|
|
236
|
+
variables : pandas.DataFrame
|
|
237
|
+
Variables lookup table file
|
|
238
|
+
|
|
239
|
+
Returns
|
|
240
|
+
-------
|
|
241
|
+
ds : xarray.Dataset
|
|
242
|
+
Dataset with metadata
|
|
243
|
+
"""
|
|
244
|
+
for k in ds.keys():
|
|
245
|
+
if k not in variables.index:
|
|
246
|
+
continue
|
|
247
|
+
ds[k].attrs["standard_name"] = variables.loc[k]["standard_name"]
|
|
248
|
+
ds[k].attrs["long_name"] = variables.loc[k]["long_name"]
|
|
249
|
+
ds[k].attrs["units"] = variables.loc[k]["units"]
|
|
250
|
+
ds[k].attrs["coverage_content_type"] = variables.loc[k]["coverage_content_type"]
|
|
251
|
+
ds[k].attrs["coordinates"] = variables.loc[k]["coordinates"]
|
|
252
|
+
return ds
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def addMeta(ds, meta):
|
|
256
|
+
"""Add metadata attributes from file to dataset
|
|
257
|
+
|
|
258
|
+
Parameters
|
|
259
|
+
----------
|
|
260
|
+
ds : xarray.Dataset
|
|
261
|
+
Dataset to add metadata attributes to
|
|
262
|
+
meta : dict
|
|
263
|
+
Metadata file
|
|
264
|
+
|
|
265
|
+
Returns
|
|
266
|
+
-------
|
|
267
|
+
ds : xarray.Dataset
|
|
268
|
+
Dataset with metadata
|
|
269
|
+
"""
|
|
270
|
+
|
|
271
|
+
# a static latitude, longitude and altitude is saved as attribute along its origin
|
|
272
|
+
var_alias = {"lat": "latitude", "lon": "longitude", "alt": "altitude"}
|
|
273
|
+
for v in ["lat", "lon", "alt"]:
|
|
274
|
+
# saving the reference latitude/longitude/altitude
|
|
275
|
+
original_value = np.nan
|
|
276
|
+
if var_alias[v] in ds.attrs.keys():
|
|
277
|
+
original_value = ds.attrs[var_alias[v]]
|
|
278
|
+
if v in ds.keys():
|
|
279
|
+
# if possible, replacing it with average coordinates based on the extra/interpolated coords
|
|
280
|
+
ds.attrs[var_alias[v]] = ds[v].mean().item()
|
|
281
|
+
ds.attrs[var_alias[v] + "_origin"] = (
|
|
282
|
+
"average of gap-filled postprocessed " + v
|
|
283
|
+
)
|
|
284
|
+
elif "gps_" + v in ds.keys():
|
|
285
|
+
# if possible, replacing it with average coordinates based on the measured coords (can be gappy)
|
|
286
|
+
ds.attrs[var_alias[v]] = ds["gps_" + v].mean().item()
|
|
287
|
+
ds.attrs[var_alias[v] + "_origin"] = (
|
|
288
|
+
"average of GPS-measured " + v + ", potentially including gaps"
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
if np.isnan(ds.attrs[var_alias[v]]):
|
|
292
|
+
# if no better data was available to update the coordinate, then we
|
|
293
|
+
# re-use the original value
|
|
294
|
+
ds.attrs[var_alias[v]] = original_value
|
|
295
|
+
ds.attrs[var_alias[v] + "_origin"] = "reference value, origin unknown"
|
|
296
|
+
|
|
297
|
+
# Attribute convention for data discovery
|
|
298
|
+
# https://wiki.esipfed.org/Attribute_Convention_for_Data_Discovery_1-3
|
|
299
|
+
|
|
300
|
+
# Determine the temporal resolution
|
|
301
|
+
sample_rate = "unknown_sample_rate"
|
|
302
|
+
if len(ds["time"]) > 1:
|
|
303
|
+
time_diff = pd.Timedelta((ds["time"][1] - ds["time"][0]).values)
|
|
304
|
+
if time_diff == pd.Timedelta("10min"):
|
|
305
|
+
sample_rate = "10min"
|
|
306
|
+
elif time_diff == pd.Timedelta("1h"):
|
|
307
|
+
sample_rate = "hourly"
|
|
308
|
+
elif time_diff == pd.Timedelta("1D"):
|
|
309
|
+
sample_rate = "daily"
|
|
310
|
+
elif 28 <= time_diff.days <= 31:
|
|
311
|
+
sample_rate = "monthly"
|
|
312
|
+
|
|
313
|
+
if "station_id" in ds.attrs.keys():
|
|
314
|
+
id_components = [
|
|
315
|
+
"dk",
|
|
316
|
+
"geus",
|
|
317
|
+
"promice",
|
|
318
|
+
"station",
|
|
319
|
+
ds.attrs["station_id"],
|
|
320
|
+
ds.attrs["level"],
|
|
321
|
+
sample_rate,
|
|
322
|
+
]
|
|
323
|
+
ds.attrs["id"] = ".".join(id_components)
|
|
324
|
+
else:
|
|
325
|
+
id_components = [
|
|
326
|
+
"dk",
|
|
327
|
+
"geus",
|
|
328
|
+
"promice",
|
|
329
|
+
"site",
|
|
330
|
+
ds.attrs["site_id"],
|
|
331
|
+
ds.attrs["level"],
|
|
332
|
+
sample_rate,
|
|
333
|
+
]
|
|
334
|
+
ds.attrs["id"] = ".".join(id_components)
|
|
335
|
+
|
|
336
|
+
ds.attrs["history"] = "Generated on " + datetime.datetime.utcnow().isoformat()
|
|
337
|
+
ds.attrs["date_created"] = str(datetime.datetime.now().isoformat())
|
|
338
|
+
ds.attrs["date_modified"] = ds.attrs["date_created"]
|
|
339
|
+
ds.attrs["date_issued"] = ds.attrs["date_created"]
|
|
340
|
+
ds.attrs["date_metadata_modified"] = ds.attrs["date_created"]
|
|
341
|
+
ds.attrs["processing_level"] = ds.attrs["level"].replace("L", "Level ")
|
|
342
|
+
|
|
343
|
+
id = ds.attrs.get('station_id', ds.attrs.get('site_id'))
|
|
344
|
+
title_string_format = "AWS measurements from {id} processed to {processing_level}. {sample_rate} average."
|
|
345
|
+
ds.attrs["title"] = title_string_format.format(
|
|
346
|
+
id=id,
|
|
347
|
+
processing_level=ds.attrs["processing_level"].lower(),
|
|
348
|
+
sample_rate=sample_rate.capitalize(),
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
if "lat" in ds.keys():
|
|
352
|
+
lat_min = ds["lat"].min().values
|
|
353
|
+
lat_max = ds["lat"].max().values
|
|
354
|
+
elif "gps_lat" in ds.keys():
|
|
355
|
+
lat_min = ds["gps_lat"].min().values
|
|
356
|
+
lat_max = ds["gps_lat"].max().values
|
|
357
|
+
elif "latitude" in ds.attrs.keys():
|
|
358
|
+
lat_min = ds.attrs["latitude"]
|
|
359
|
+
lat_max = ds.attrs["latitude"]
|
|
360
|
+
else:
|
|
361
|
+
lat_min = np.nan
|
|
362
|
+
lat_max = np.nan
|
|
363
|
+
|
|
364
|
+
if "lon" in ds.keys():
|
|
365
|
+
lon_min = ds["lon"].min().values
|
|
366
|
+
lon_max = ds["lon"].max().values
|
|
367
|
+
elif "gps_lon" in ds.keys():
|
|
368
|
+
lon_min = ds["gps_lon"].min().values
|
|
369
|
+
lon_max = ds["gps_lon"].max().values
|
|
370
|
+
elif "longitude" in ds.attrs.keys():
|
|
371
|
+
lon_min = ds.attrs["longitude"]
|
|
372
|
+
lon_max = ds.attrs["longitude"]
|
|
373
|
+
else:
|
|
374
|
+
lon_min = np.nan
|
|
375
|
+
lon_max = np.nan
|
|
376
|
+
|
|
377
|
+
if "alt" in ds.keys():
|
|
378
|
+
alt_min = ds["alt"].min().values
|
|
379
|
+
alt_max = ds["alt"].max().values
|
|
380
|
+
elif "gps_alt" in ds.keys():
|
|
381
|
+
alt_min = ds["gps_alt"].min().values
|
|
382
|
+
alt_max = ds["gps_alt"].max().values
|
|
383
|
+
elif "altitude" in ds.attrs.keys():
|
|
384
|
+
alt_min = ds.attrs["altitude"]
|
|
385
|
+
alt_max = ds.attrs["altitude"]
|
|
386
|
+
else:
|
|
387
|
+
alt_min = np.nan
|
|
388
|
+
alt_max = np.nan
|
|
389
|
+
|
|
390
|
+
ds.attrs["geospatial_bounds"] = (
|
|
391
|
+
"POLYGON(("
|
|
392
|
+
+ f"{lat_min} {lon_min}, "
|
|
393
|
+
+ f"{lat_min} {lon_max}, "
|
|
394
|
+
+ f"{lat_max} {lon_max}, "
|
|
395
|
+
+ f"{lat_max} {lon_min}, "
|
|
396
|
+
+ f"{lat_min} {lon_min}))"
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
ds.attrs["geospatial_lat_min"] = str(lat_min)
|
|
400
|
+
ds.attrs["geospatial_lat_max"] = str(lat_max)
|
|
401
|
+
ds.attrs["geospatial_lon_min"] = str(lon_min)
|
|
402
|
+
ds.attrs["geospatial_lon_max"] = str(lon_max)
|
|
403
|
+
ds.attrs["geospatial_vertical_min"] = str(alt_min)
|
|
404
|
+
ds.attrs["geospatial_vertical_max"] = str(alt_max)
|
|
405
|
+
|
|
406
|
+
ds.attrs["geospatial_vertical_positive"] = "up"
|
|
407
|
+
ds.attrs["time_coverage_start"] = str(ds["time"][0].values)
|
|
408
|
+
ds.attrs["time_coverage_end"] = str(ds["time"][-1].values)
|
|
409
|
+
|
|
410
|
+
# https://www.digi.com/resources/documentation/digidocs/90001437-13/reference/r_iso_8601_duration_format.htm
|
|
411
|
+
try:
|
|
412
|
+
ds.attrs["time_coverage_duration"] = str(
|
|
413
|
+
pd.Timedelta((ds["time"][-1] - ds["time"][0]).values).isoformat()
|
|
414
|
+
)
|
|
415
|
+
ds.attrs["time_coverage_resolution"] = str(
|
|
416
|
+
pd.Timedelta((ds["time"][1] - ds["time"][0]).values).isoformat()
|
|
417
|
+
)
|
|
418
|
+
except:
|
|
419
|
+
ds.attrs["time_coverage_duration"] = str(pd.Timedelta(0).isoformat())
|
|
420
|
+
ds.attrs["time_coverage_resolution"] = str(pd.Timedelta(0).isoformat())
|
|
421
|
+
|
|
422
|
+
# Note: int64 dtype (long int) is incompatible with OPeNDAP access via THREDDS for NetCDF files
|
|
423
|
+
# See https://stackoverflow.com/questions/48895227/output-int32-time-dimension-in-netcdf-using-xarray
|
|
424
|
+
ds.time.encoding["dtype"] = "i4" # 32-bit signed integer
|
|
425
|
+
# ds.time.encoding["calendar"] = 'proleptic_gregorian' # this is default
|
|
426
|
+
|
|
427
|
+
# Load metadata attributes and add to Dataset
|
|
428
|
+
[_addAttr(ds, key, value) for key, value in meta.items()]
|
|
429
|
+
|
|
430
|
+
# Check attribute formating
|
|
431
|
+
for k, v in ds.attrs.items():
|
|
432
|
+
if not isinstance(v, str) or not isinstance(v, int):
|
|
433
|
+
ds.attrs[k] = str(v)
|
|
434
|
+
return ds
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _addAttr(ds, key, value):
|
|
438
|
+
"""Add attribute to xarray dataset
|
|
439
|
+
|
|
440
|
+
ds : xr.Dataset
|
|
441
|
+
Dataset to add attribute to
|
|
442
|
+
key : str
|
|
443
|
+
Attribute name, with "." denoting variable attributes
|
|
444
|
+
value : str/int
|
|
445
|
+
Value for attribute"""
|
|
446
|
+
if len(key.split(".")) == 2:
|
|
447
|
+
try:
|
|
448
|
+
ds[key.split(".")[0]].attrs[key.split(".")[1]] = str(value)
|
|
449
|
+
except:
|
|
450
|
+
pass
|
|
451
|
+
# logger.info(f'Unable to add metadata to {key.split(".")[0]}')
|
|
452
|
+
else:
|
|
453
|
+
ds.attrs[key] = value
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def roundValues(ds, df, col="max_decimals"):
|
|
457
|
+
"""Round all variable values in data array based on pre-defined rounding
|
|
458
|
+
value in variables look-up table DataFrame
|
|
459
|
+
|
|
460
|
+
Parameters
|
|
461
|
+
----------
|
|
462
|
+
ds : xr.Dataset
|
|
463
|
+
Dataset to round values in
|
|
464
|
+
df : pd.Dataframe
|
|
465
|
+
Variable look-up table with rounding values
|
|
466
|
+
col : str
|
|
467
|
+
Column in variable look-up table that contains rounding values. The
|
|
468
|
+
default is "max_decimals"
|
|
469
|
+
"""
|
|
470
|
+
df = df[col]
|
|
471
|
+
df = df.dropna(how="all")
|
|
472
|
+
for var in df.index:
|
|
473
|
+
if var not in list(ds.variables):
|
|
474
|
+
continue
|
|
475
|
+
if df[var] is not np.nan:
|
|
476
|
+
ds[var] = ds[var].round(decimals=int(df[var]))
|
|
477
|
+
return ds
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def reformat_time(dataset):
|
|
481
|
+
"""Re-format time"""
|
|
482
|
+
t = dataset["time"].values
|
|
483
|
+
dataset["time"] = list(t)
|
|
484
|
+
return dataset
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def reformat_lon(dataset, exempt=["UWN", "Roof_GEUS", "Roof_PROMICE"]):
|
|
488
|
+
"""Switch gps_lon to negative values (degrees_east). We do this here, and
|
|
489
|
+
NOT in addMeta, otherwise we switch back to positive when calling getMeta
|
|
490
|
+
in joinL2"""
|
|
491
|
+
if "station_id" in dataset.attrs.keys():
|
|
492
|
+
id = dataset.attrs["station_id"]
|
|
493
|
+
else:
|
|
494
|
+
id = dataset.attrs["site_id"]
|
|
495
|
+
|
|
496
|
+
if id not in exempt:
|
|
497
|
+
if "gps_lon" not in dataset.keys():
|
|
498
|
+
return dataset
|
|
499
|
+
dataset["gps_lon"] = np.abs(dataset["gps_lon"]) * -1
|
|
500
|
+
if "lon" not in dataset.keys():
|
|
501
|
+
return dataset
|
|
502
|
+
dataset["lon"] = np.abs(dataset["lon"]) * -1
|
|
503
|
+
return dataset
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import os
|
|
3
|
-
import urllib.request
|
|
4
|
-
from urllib.error import HTTPError, URLError
|
|
5
3
|
|
|
6
4
|
import numpy as np
|
|
7
5
|
import pandas as pd
|
|
@@ -16,8 +14,7 @@ __all__ = [
|
|
|
16
14
|
logger = logging.getLogger(__name__)
|
|
17
15
|
|
|
18
16
|
|
|
19
|
-
def flagNAN(ds_in,
|
|
20
|
-
flag_dir='../PROMICE-AWS-data-issues/flags'):
|
|
17
|
+
def flagNAN(ds_in, flag_dir):
|
|
21
18
|
'''Read flagged data from .csv file. For each variable, and downstream
|
|
22
19
|
dependents, flag as invalid (or other) if set in the flag .csv
|
|
23
20
|
|
|
@@ -65,17 +62,15 @@ def flagNAN(ds_in,
|
|
|
65
62
|
|
|
66
63
|
for v in varlist:
|
|
67
64
|
if v in list(ds.keys()):
|
|
68
|
-
logger.
|
|
65
|
+
logger.debug(f'---> flagging {t0} {t1} {v}')
|
|
69
66
|
ds[v] = ds[v].where((ds['time'] < t0) | (ds['time'] > t1))
|
|
70
67
|
else:
|
|
71
|
-
logger.
|
|
68
|
+
logger.debug(f'---> could not flag {v} not in dataset')
|
|
72
69
|
|
|
73
70
|
return ds
|
|
74
71
|
|
|
75
72
|
|
|
76
|
-
def adjustTime(ds,
|
|
77
|
-
adj_dir='../PROMICE-AWS-data-issues/adjustments/',
|
|
78
|
-
var_list=[], skip_var=[]):
|
|
73
|
+
def adjustTime(ds, adj_dir, var_list=[], skip_var=[]):
|
|
79
74
|
'''Read adjustment data from .csv file. Only applies the "time_shift" adjustment
|
|
80
75
|
|
|
81
76
|
Parameters
|
|
@@ -134,9 +129,7 @@ def adjustTime(ds,
|
|
|
134
129
|
return ds_out
|
|
135
130
|
|
|
136
131
|
|
|
137
|
-
def adjustData(ds,
|
|
138
|
-
adj_dir='../PROMICE-AWS-data-issues/adjustments/',
|
|
139
|
-
var_list=[], skip_var=[]):
|
|
132
|
+
def adjustData(ds, adj_dir, var_list=[], skip_var=[]):
|
|
140
133
|
'''Read adjustment data from .csv file. For each variable, and downstream
|
|
141
134
|
dependents, adjust data accordingly if set in the adjustment .csv
|
|
142
135
|
|
|
@@ -206,13 +199,14 @@ def adjustData(ds,
|
|
|
206
199
|
t1 = pd.to_datetime(t1, utc=True).tz_localize(None)
|
|
207
200
|
|
|
208
201
|
index_slice = dict(time=slice(t0, t1))
|
|
209
|
-
|
|
210
202
|
if len(ds_out[var].loc[index_slice].time.time) == 0:
|
|
203
|
+
logger.info(f'---> {t0} {t1} {var} {func} {val}')
|
|
211
204
|
logger.info("Time range does not intersect with dataset")
|
|
212
205
|
continue
|
|
213
206
|
|
|
214
|
-
|
|
215
|
-
|
|
207
|
+
else:
|
|
208
|
+
logger.debug(f'---> {t0} {t1} {var} {func} {val}')
|
|
209
|
+
|
|
216
210
|
if func == "add":
|
|
217
211
|
ds_out[var].loc[index_slice] = ds_out[var].loc[index_slice].values + val
|
|
218
212
|
# flagging adjusted values
|
|
@@ -314,7 +308,7 @@ def _getDF(flag_file):
|
|
|
314
308
|
).dropna(how='all', axis='rows')
|
|
315
309
|
else:
|
|
316
310
|
df=None
|
|
317
|
-
logger.info(f"No {flag_file
|
|
311
|
+
logger.info(f"No {flag_file} file to read.")
|
|
318
312
|
return df
|
|
319
313
|
|
|
320
314
|
|