oceanarray 0.1.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.
- oceanarray/__init__.py +20 -0
- oceanarray/_version.py +1 -0
- oceanarray/cli.py +1343 -0
- oceanarray/clock_offset.py +675 -0
- oceanarray/config/OS1_sensor_attrs.yaml +49 -0
- oceanarray/config/OS1_var_names.yaml +5 -0
- oceanarray/config/OS1_vocab_attrs.yaml +84 -0
- oceanarray/config/legacy/project_RAPID.yaml +69 -0
- oceanarray/config/legacy/rodb_keys.txt +206 -0
- oceanarray/config/legacy/rodb_keys.yaml +1012 -0
- oceanarray/config/logging.yaml +28 -0
- oceanarray/legacy/__init__.py +47 -0
- oceanarray/legacy/caldip.py +753 -0
- oceanarray/legacy/convertOS.py +527 -0
- oceanarray/legacy/find_deployment.py +489 -0
- oceanarray/legacy/mooring_rodb.py +429 -0
- oceanarray/legacy/process_rodb.py +369 -0
- oceanarray/legacy/rodb.py +330 -0
- oceanarray/logger.py +192 -0
- oceanarray/logsheet/__init__.py +60 -0
- oceanarray/logsheet/_columns.py +97 -0
- oceanarray/logsheet/_config.py +245 -0
- oceanarray/logsheet/_firmware.py +43 -0
- oceanarray/logsheet/_latex.py +128 -0
- oceanarray/logsheet/_render.py +126 -0
- oceanarray/logsheet/builders/__init__.py +13 -0
- oceanarray/logsheet/builders/caldip_download.py +195 -0
- oceanarray/logsheet/builders/caldip_setup.py +188 -0
- oceanarray/logsheet/builders/deployment_setup.py +218 -0
- oceanarray/logsheet/builders/mooring_download.py +243 -0
- oceanarray/logsheet/builders/recovery.py +157 -0
- oceanarray/logsheet/data/column_defs.yaml +686 -0
- oceanarray/logsheet/data/latex_templates/_base_instrument.tex.j2 +42 -0
- oceanarray/logsheet/data/latex_templates/caldip_download.tex.j2 +47 -0
- oceanarray/logsheet/data/latex_templates/caldip_setup.tex.j2 +70 -0
- oceanarray/logsheet/data/latex_templates/deployment_setup.tex.j2 +97 -0
- oceanarray/logsheet/data/latex_templates/mooring_download.tex.j2 +54 -0
- oceanarray/logsheet/data/latex_templates/recovery.tex.j2 +103 -0
- oceanarray/mooring_level.py +1368 -0
- oceanarray/oceanarray.mplstyle +18 -0
- oceanarray/parameters.py +270 -0
- oceanarray/plotter.py +942 -0
- oceanarray/plotters/__init__.py +97 -0
- oceanarray/plotters/_animation.py +261 -0
- oceanarray/plotters/_current.py +705 -0
- oceanarray/plotters/_diagnostic.py +667 -0
- oceanarray/plotters/_helpers.py +57 -0
- oceanarray/plotters/_primitives.py +101 -0
- oceanarray/plotters/_section.py +16 -0
- oceanarray/plotters/_timeseries.py +11 -0
- oceanarray/rapid_interp.py +739 -0
- oceanarray/read_rbr_hex.py +541 -0
- oceanarray/readers.py +303 -0
- oceanarray/report/__init__.py +5 -0
- oceanarray/report/_array.py +386 -0
- oceanarray/report/_grid.py +539 -0
- oceanarray/report/_html_helpers.py +1004 -0
- oceanarray/report/_instrument.py +853 -0
- oceanarray/report/_mooring.py +1385 -0
- oceanarray/report/_plots.py +4284 -0
- oceanarray/report/_stack.py +941 -0
- oceanarray/stage1.py +1959 -0
- oceanarray/stage2.py +1160 -0
- oceanarray/stage3.py +2494 -0
- oceanarray/time_gridding.py +958 -0
- oceanarray/tools.py +400 -0
- oceanarray/transports.py +1 -0
- oceanarray/utilities.py +538 -0
- oceanarray/validation.py +389 -0
- oceanarray/writers.py +99 -0
- oceanarray-0.1.0.dist-info/METADATA +503 -0
- oceanarray-0.1.0.dist-info/RECORD +76 -0
- oceanarray-0.1.0.dist-info/WHEEL +5 -0
- oceanarray-0.1.0.dist-info/entry_points.txt +2 -0
- oceanarray-0.1.0.dist-info/licenses/LICENSE +21 -0
- oceanarray-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Optional, Union
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import xarray as xr
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
from .. import utilities # for any shared helpers like date parsing
|
|
10
|
+
from ..utilities import iso8601_duration_from_seconds # or wherever you store it
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def convert_rodb_to_oceansites(
|
|
14
|
+
ds: xr.Dataset,
|
|
15
|
+
metadata_txt: Union[str, Path],
|
|
16
|
+
var_map_yaml: Union[str, Path],
|
|
17
|
+
vocab_yaml: Union[str, Path],
|
|
18
|
+
sensor_yaml: Optional[Union[str, Path]] = None,
|
|
19
|
+
project_yaml: Optional[Union[str, Path]] = None,
|
|
20
|
+
) -> xr.Dataset:
|
|
21
|
+
"""Convert a dataset loaded from RODB format into OceanSITES-compliant format.
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
ds : xarray.Dataset
|
|
26
|
+
Original dataset with RODB-style variables and TIME coordinate.
|
|
27
|
+
metadata_txt : str or Path
|
|
28
|
+
Path to RODB metadata text file (header block with key=value lines).
|
|
29
|
+
var_map_yaml : str or Path
|
|
30
|
+
YAML file mapping original variable names to OceanSITES names.
|
|
31
|
+
vocab_yaml : str or Path
|
|
32
|
+
YAML file providing OceanSITES variable attributes.
|
|
33
|
+
sensor_yaml : str or Path, optional
|
|
34
|
+
YAML file with instrument metadata and variable-instrument mapping.
|
|
35
|
+
project_yaml : str or Path, optional
|
|
36
|
+
YAML file with project-level global attributes.
|
|
37
|
+
|
|
38
|
+
Returns
|
|
39
|
+
-------
|
|
40
|
+
xarray.Dataset
|
|
41
|
+
OceanSITES-compliant dataset.
|
|
42
|
+
|
|
43
|
+
"""
|
|
44
|
+
metadata, _ = parse_rodb_metadata(metadata_txt)
|
|
45
|
+
var_map = load_yaml(var_map_yaml)
|
|
46
|
+
vocab_attrs = load_yaml(vocab_yaml)
|
|
47
|
+
|
|
48
|
+
# If present, remove variables YY, MM, DD, HH and coordinate N_MEASUREMENTS
|
|
49
|
+
for var in ["YY", "MM", "DD", "HH", "N_MEASUREMENTS"]:
|
|
50
|
+
if var in ds.data_vars:
|
|
51
|
+
ds = ds.drop_vars(var)
|
|
52
|
+
if var in ds.coords:
|
|
53
|
+
ds = ds.drop_vars(var)
|
|
54
|
+
|
|
55
|
+
# Step 1: Rename variables using mapping
|
|
56
|
+
ds = ds.rename(var_map)
|
|
57
|
+
|
|
58
|
+
# Step 2: Add coordinate variables for LATITUDE, LONGITUDE, DEPTH
|
|
59
|
+
ds = add_fixed_coordinates(ds, metadata)
|
|
60
|
+
|
|
61
|
+
# Step 3: Add variable attributes from vocab
|
|
62
|
+
ds = add_variable_attributes(ds, vocab_attrs)
|
|
63
|
+
|
|
64
|
+
# Step 4: Add global attributes
|
|
65
|
+
ds = add_global_attributes(ds, metadata)
|
|
66
|
+
|
|
67
|
+
# Step 5: Format TIME variable
|
|
68
|
+
ds = format_time_variable(ds)
|
|
69
|
+
|
|
70
|
+
# Step 6: Add instrument metadata (optional)
|
|
71
|
+
if sensor_yaml is not None:
|
|
72
|
+
sensor_dict = load_yaml(sensor_yaml)
|
|
73
|
+
ds = add_instrument_metadata(ds, sensor_dict, metadata)
|
|
74
|
+
else:
|
|
75
|
+
# Fallback: assign default TOOL0018 for all known variables
|
|
76
|
+
serial = metadata.get("SERIALNUMBER", "unknown")
|
|
77
|
+
start = f"{metadata['START_DATE']}T{metadata['START_TIME']}:00Z"
|
|
78
|
+
end = f"{metadata['END_DATE']}T{metadata['END_TIME']}:00Z"
|
|
79
|
+
sensor_dict = {
|
|
80
|
+
"instrument_1": {
|
|
81
|
+
"sensor_model": "SBE 37 MicroCat SMP-CT",
|
|
82
|
+
"sensor_manufacturer": "Sea-Bird",
|
|
83
|
+
"sensor_serial_number": serial,
|
|
84
|
+
"sensor_SeaVoX_L22_code": "SDN:L22::TOOL0018",
|
|
85
|
+
"sensor_reference": "https://www.seabird.com/sbe37",
|
|
86
|
+
"sensor_data_start_date": start,
|
|
87
|
+
"sensor_data_end_date": end,
|
|
88
|
+
"comment": "Default instrument (SMP-CT) assigned by fallback.",
|
|
89
|
+
},
|
|
90
|
+
"variables": {
|
|
91
|
+
var: "instrument_1" for var in ds.data_vars if var not in ds.coords
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
ds = add_instrument_metadata(ds, sensor_dict, metadata)
|
|
95
|
+
|
|
96
|
+
# Step 7: Add project attributes (optional)
|
|
97
|
+
project_attrs = load_yaml(project_yaml) if project_yaml is not None else {}
|
|
98
|
+
ds = add_project_attributes(ds, project_attrs, metadata)
|
|
99
|
+
|
|
100
|
+
# Rename "mooring" attribute to "internal_mooring_identifier" if present
|
|
101
|
+
if "mooring" in ds.attrs:
|
|
102
|
+
ds.attrs["internal_identifier"] = ds.attrs.pop("mooring")
|
|
103
|
+
ds = sort_global_attributes(ds)
|
|
104
|
+
|
|
105
|
+
lat = float(ds["LATITUDE"].values.flat[0])
|
|
106
|
+
lon = float(ds["LONGITUDE"].values.flat[0])
|
|
107
|
+
|
|
108
|
+
ds = ds.squeeze(dim=["LATITUDE", "LONGITUDE"], drop=True)
|
|
109
|
+
ds = ds.assign_coords(LATITUDE=("TIME", [lat] * ds.sizes["TIME"]))
|
|
110
|
+
ds = ds.assign_coords(LONGITUDE=("TIME", [lon] * ds.sizes["TIME"]))
|
|
111
|
+
|
|
112
|
+
return ds
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def load_yaml(path):
|
|
116
|
+
with open(path, "r") as f:
|
|
117
|
+
return yaml.safe_load(f)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def parse_rodb_metadata(txt_path):
|
|
121
|
+
"""Parse RODB metadata from a .raw or .use file header.
|
|
122
|
+
|
|
123
|
+
Parameters
|
|
124
|
+
----------
|
|
125
|
+
txt_path : str or Path
|
|
126
|
+
Path to the RODB file.
|
|
127
|
+
|
|
128
|
+
Returns
|
|
129
|
+
-------
|
|
130
|
+
tuple
|
|
131
|
+
(header_dict, data_start_index)
|
|
132
|
+
|
|
133
|
+
"""
|
|
134
|
+
txt_path = Path(txt_path)
|
|
135
|
+
with open(txt_path, "r") as f:
|
|
136
|
+
lines = f.readlines()
|
|
137
|
+
|
|
138
|
+
header = {}
|
|
139
|
+
data_start_index = 0
|
|
140
|
+
for i, line in enumerate(lines):
|
|
141
|
+
if not line.strip():
|
|
142
|
+
continue
|
|
143
|
+
if "=" in line:
|
|
144
|
+
key, val = map(str.strip, line.split("=", 1))
|
|
145
|
+
header[key.upper()] = val
|
|
146
|
+
else:
|
|
147
|
+
data_start_index = i
|
|
148
|
+
break
|
|
149
|
+
return header, data_start_index
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def add_fixed_coordinates(ds, metadata):
|
|
153
|
+
"""Add fixed spatial coordinates LATITUDE, LONGITUDE, and DEPTH from metadata."""
|
|
154
|
+
|
|
155
|
+
def dms_to_decimal(coord_str):
|
|
156
|
+
parts = coord_str.split()
|
|
157
|
+
deg = float(parts[0])
|
|
158
|
+
min_ = float(parts[1])
|
|
159
|
+
hemi = parts[2].upper()
|
|
160
|
+
decimal = deg + min_ / 60
|
|
161
|
+
if hemi in ["S", "W"]:
|
|
162
|
+
decimal *= -1
|
|
163
|
+
return decimal
|
|
164
|
+
|
|
165
|
+
lat = dms_to_decimal(metadata["LATITUDE"])
|
|
166
|
+
lon = dms_to_decimal(metadata["LONGITUDE"])
|
|
167
|
+
depth = float(metadata["INSTRDEPTH"])
|
|
168
|
+
|
|
169
|
+
ds = ds.expand_dims({"DEPTH": [depth], "LATITUDE": [lat], "LONGITUDE": [lon]})
|
|
170
|
+
|
|
171
|
+
ds = ds.assign_coords(
|
|
172
|
+
{
|
|
173
|
+
"DEPTH": (
|
|
174
|
+
"DEPTH",
|
|
175
|
+
[depth],
|
|
176
|
+
{
|
|
177
|
+
"units": "m",
|
|
178
|
+
"standard_name": "depth",
|
|
179
|
+
"long_name": "Instrument depth",
|
|
180
|
+
"positive": "down",
|
|
181
|
+
"axis": "Z",
|
|
182
|
+
},
|
|
183
|
+
),
|
|
184
|
+
"LATITUDE": (
|
|
185
|
+
"LATITUDE",
|
|
186
|
+
[lat],
|
|
187
|
+
{
|
|
188
|
+
"units": "degrees_north",
|
|
189
|
+
"standard_name": "latitude",
|
|
190
|
+
"long_name": "Latitude of instrument",
|
|
191
|
+
"axis": "Y",
|
|
192
|
+
},
|
|
193
|
+
),
|
|
194
|
+
"LONGITUDE": (
|
|
195
|
+
"LONGITUDE",
|
|
196
|
+
[lon],
|
|
197
|
+
{
|
|
198
|
+
"units": "degrees_east",
|
|
199
|
+
"standard_name": "longitude",
|
|
200
|
+
"long_name": "Longitude of instrument",
|
|
201
|
+
"axis": "X",
|
|
202
|
+
},
|
|
203
|
+
),
|
|
204
|
+
}
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Reorder coordinates on all variables to TIME, DEPTH, LATITUDE, LONGITUDE order if present
|
|
208
|
+
coord_order = [
|
|
209
|
+
c for c in ["TIME", "DEPTH", "LATITUDE", "LONGITUDE"] if c in ds.dims
|
|
210
|
+
]
|
|
211
|
+
for var in ds.data_vars:
|
|
212
|
+
dims = list(ds[var].dims)
|
|
213
|
+
# Only reorder if all coord_order dims are present in variable
|
|
214
|
+
if all(dim in dims for dim in coord_order):
|
|
215
|
+
# Place coord_order dims first, then any remaining dims
|
|
216
|
+
new_order = coord_order + [d for d in dims if d not in coord_order]
|
|
217
|
+
ds[var] = ds[var].transpose(*new_order)
|
|
218
|
+
|
|
219
|
+
return ds
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def add_variable_attributes(ds, vocab_attrs):
|
|
223
|
+
for var in ds.data_vars:
|
|
224
|
+
if var in vocab_attrs:
|
|
225
|
+
ds[var].attrs.update(vocab_attrs[var])
|
|
226
|
+
return ds
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def add_global_attributes(ds, metadata):
|
|
230
|
+
"""Add OceanSITES-compliant global attributes to the dataset."""
|
|
231
|
+
start = f"{metadata['START_DATE']}T{metadata['START_TIME']}:00Z"
|
|
232
|
+
end = f"{metadata['END_DATE']}T{metadata['END_TIME']}:00Z"
|
|
233
|
+
start = start.replace("/", "-")
|
|
234
|
+
end = end.replace("/", "-")
|
|
235
|
+
utilities.is_iso8601_utc(start)
|
|
236
|
+
utilities.is_iso8601_utc(end)
|
|
237
|
+
|
|
238
|
+
lat = metadata["LATITUDE"]
|
|
239
|
+
lon = metadata["LONGITUDE"]
|
|
240
|
+
instr_depth = float(metadata["INSTRDEPTH"])
|
|
241
|
+
|
|
242
|
+
ds.attrs.update(
|
|
243
|
+
{
|
|
244
|
+
"title": f"Time series from {metadata['MOORING']}, instrument {metadata['SERIALNUMBER']}",
|
|
245
|
+
"summary": "CTD mooring time series processed to OceanSITES standards.",
|
|
246
|
+
"Conventions": "CF-1.7, ACDD-1.3, OceanSITES-1.4",
|
|
247
|
+
"deployment_code": metadata["MOORING"],
|
|
248
|
+
"time_coverage_start": start,
|
|
249
|
+
"time_coverage_end": end,
|
|
250
|
+
"geospatial_lat_min": lat,
|
|
251
|
+
"geospatial_lat_max": lat,
|
|
252
|
+
"geospatial_lon_min": lon,
|
|
253
|
+
"geospatial_lon_max": lon,
|
|
254
|
+
"geospatial_vertical_min": instr_depth,
|
|
255
|
+
"geospatial_vertical_max": instr_depth,
|
|
256
|
+
"history": "converted to OceanSITES by convert_rodb_to_oceansites",
|
|
257
|
+
}
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
# remove if in the attributes ["start_time", "end_time"]
|
|
261
|
+
if "start_time" in ds.attrs:
|
|
262
|
+
del ds.attrs["start_time"]
|
|
263
|
+
if "end_time" in ds.attrs:
|
|
264
|
+
del ds.attrs["end_time"]
|
|
265
|
+
|
|
266
|
+
return ds
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def format_time_variable(ds):
|
|
270
|
+
# Ensure TIME is CF-compliant: units, calendar, etc.
|
|
271
|
+
ds["TIME"].attrs.update(
|
|
272
|
+
{
|
|
273
|
+
"standard_name": "time",
|
|
274
|
+
"long_name": "Time of measurement",
|
|
275
|
+
"units": "seconds since 1970-01-01T00:00:00Z",
|
|
276
|
+
"calendar": "gregorian",
|
|
277
|
+
"axis": "T",
|
|
278
|
+
}
|
|
279
|
+
)
|
|
280
|
+
return ds
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def add_instrument_metadata(ds, sensor_dict, metadata):
|
|
284
|
+
"""Add centralized instrument metadata and link variables via the 'instrument' attribute.
|
|
285
|
+
|
|
286
|
+
Parameters
|
|
287
|
+
----------
|
|
288
|
+
ds : xarray.Dataset
|
|
289
|
+
The dataset to modify.
|
|
290
|
+
sensor_dict : dict
|
|
291
|
+
Dictionary with 'instrument_<N>' keys and/or TOOL### templates, plus 'variables'.
|
|
292
|
+
metadata : dict
|
|
293
|
+
Header metadata to extract instrument-specific fields like serial and dates.
|
|
294
|
+
|
|
295
|
+
"""
|
|
296
|
+
variables = sensor_dict.get("variables", {})
|
|
297
|
+
templates = {k: v for k, v in sensor_dict.items() if k.startswith("TOOL")}
|
|
298
|
+
|
|
299
|
+
sensor_type = "CTD" # for RAPID core measurements (TEMP, CNDC, PRES)
|
|
300
|
+
serial = metadata.get("SERIALNUMBER", "unknown")
|
|
301
|
+
inst_depth = float(metadata.get("INSTRDEPTH", 0))
|
|
302
|
+
start = f"{metadata['START_DATE']}T{metadata['START_TIME']}:00Z"
|
|
303
|
+
end = f"{metadata['END_DATE']}T{metadata['END_TIME']}:00Z"
|
|
304
|
+
|
|
305
|
+
for var, _ in variables.items():
|
|
306
|
+
if var in ds.data_vars:
|
|
307
|
+
sensor_name = f"SENSOR_{sensor_type}_{serial}"
|
|
308
|
+
ds[var].attrs["instrument"] = sensor_name
|
|
309
|
+
|
|
310
|
+
sensor_name = f"SENSOR_{sensor_type}_{serial}"
|
|
311
|
+
if sensor_name not in ds.data_vars:
|
|
312
|
+
# Fallback: determine base template
|
|
313
|
+
template_key = "TOOL0018"
|
|
314
|
+
for attr in ds[var].attrs.values():
|
|
315
|
+
if isinstance(attr, str) and attr.startswith("SDN:L22::TOOL"):
|
|
316
|
+
template_key = attr.split("::")[-1]
|
|
317
|
+
break
|
|
318
|
+
base = templates.get(template_key, templates.get("TOOL0018", {})).copy()
|
|
319
|
+
|
|
320
|
+
# Fill dynamic fields
|
|
321
|
+
base.setdefault("sensor_serial_number", serial)
|
|
322
|
+
base.setdefault("sensor_data_start_date", start)
|
|
323
|
+
base.setdefault("sensor_data_end_date", end)
|
|
324
|
+
|
|
325
|
+
# If inst_depth is an integer (i.e., no values smaller than 1), convert to int
|
|
326
|
+
if isinstance(inst_depth, (int, float)) and inst_depth.is_integer():
|
|
327
|
+
inst_depth = int(inst_depth)
|
|
328
|
+
|
|
329
|
+
# Attach as scalar variable with attributes only (no dims, no coords)
|
|
330
|
+
ds[sensor_name] = xr.DataArray(inst_depth, attrs=base)
|
|
331
|
+
|
|
332
|
+
return ds
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def infer_data_mode(source_filename=None, history=None):
|
|
336
|
+
"""Infer data_mode (D=delayed, R=real-time, P=provisional) from filename or processing history.
|
|
337
|
+
|
|
338
|
+
Parameters
|
|
339
|
+
----------
|
|
340
|
+
source_filename : str or Path, optional
|
|
341
|
+
Original filename (e.g., 'example.raw', 'data.microcat').
|
|
342
|
+
history : str, optional
|
|
343
|
+
Global attribute describing the file's processing history.
|
|
344
|
+
|
|
345
|
+
Returns
|
|
346
|
+
-------
|
|
347
|
+
str
|
|
348
|
+
One of "D", "R", or "P". Defaults to "D" (delayed mode) if no clear match found.
|
|
349
|
+
|
|
350
|
+
"""
|
|
351
|
+
# Priority 1: filename extension
|
|
352
|
+
if source_filename:
|
|
353
|
+
fname = str(source_filename).lower()
|
|
354
|
+
if fname.endswith((".raw", ".use")):
|
|
355
|
+
return "P"
|
|
356
|
+
elif fname.endswith(".microcat"):
|
|
357
|
+
return "D"
|
|
358
|
+
|
|
359
|
+
# Priority 2: clues from history
|
|
360
|
+
if history:
|
|
361
|
+
text = history.lower()
|
|
362
|
+
if "realtime" in text or "real-time" in text:
|
|
363
|
+
return "R"
|
|
364
|
+
elif "prelim" in text:
|
|
365
|
+
return "P"
|
|
366
|
+
elif "delayed" in text:
|
|
367
|
+
return "D"
|
|
368
|
+
|
|
369
|
+
# Fallback
|
|
370
|
+
return "P" # Default to preliminary mode if no clues found
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def add_project_attributes(ds, project_attrs=None, metadata=None):
|
|
374
|
+
"""Merge project-level attributes into dataset, with derived and dynamic values."""
|
|
375
|
+
if project_attrs is None:
|
|
376
|
+
project_attrs = {}
|
|
377
|
+
if metadata is None:
|
|
378
|
+
metadata = {}
|
|
379
|
+
attrs = project_attrs.copy()
|
|
380
|
+
|
|
381
|
+
# Platform and deployment from mooring name
|
|
382
|
+
mooring = metadata.get("MOORING", "unknown")
|
|
383
|
+
if "_" in mooring:
|
|
384
|
+
platform_code, deployment_code = mooring.split("_", 1)
|
|
385
|
+
else:
|
|
386
|
+
platform_code = mooring
|
|
387
|
+
deployment_code = "unknown"
|
|
388
|
+
|
|
389
|
+
attrs.setdefault("platform_code", platform_code)
|
|
390
|
+
attrs.setdefault("deployment_code", deployment_code)
|
|
391
|
+
attrs.setdefault(
|
|
392
|
+
"title",
|
|
393
|
+
f"Time series from {mooring}, instrument {metadata.get('SERIALNUMBER', '')}",
|
|
394
|
+
)
|
|
395
|
+
attrs.setdefault(
|
|
396
|
+
"summary", "CTD mooring time series processed to OceanSITES standards."
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
# Determine serial and data mode
|
|
400
|
+
serial = metadata.get("SERIALNUMBER", "unknown")
|
|
401
|
+
if "data_mode" not in attrs:
|
|
402
|
+
datamode = infer_data_mode(
|
|
403
|
+
source_filename=ds.attrs.get("source_file", None),
|
|
404
|
+
history=ds.attrs.get("history", None),
|
|
405
|
+
)
|
|
406
|
+
attrs["data_mode"] = datamode
|
|
407
|
+
else:
|
|
408
|
+
datamode = attrs["data_mode"]
|
|
409
|
+
|
|
410
|
+
# Generate unique ID
|
|
411
|
+
attrs["id"] = f"OS_{platform_code}_{deployment_code}_{serial}_{datamode}"
|
|
412
|
+
|
|
413
|
+
# Duration and resolution from TIME coordinate
|
|
414
|
+
time = ds["TIME"].values
|
|
415
|
+
if len(time) > 1:
|
|
416
|
+
duration_days = (time[-1] - time[0]) / np.timedelta64(1, "D")
|
|
417
|
+
mean_spacing = np.median(np.diff(time)) / np.timedelta64(1, "s")
|
|
418
|
+
attrs.setdefault(
|
|
419
|
+
"time_coverage_resolution", iso8601_duration_from_seconds(mean_spacing)
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
# Add date_created / date_modified
|
|
423
|
+
now = datetime.datetime.utcnow().isoformat(timespec="seconds") + "Z"
|
|
424
|
+
if "date_created" not in ds.attrs:
|
|
425
|
+
attrs["date_created"] = now
|
|
426
|
+
else:
|
|
427
|
+
attrs["date_modified"] = now
|
|
428
|
+
|
|
429
|
+
attrs.setdefault("QC_indicator", "unknown")
|
|
430
|
+
|
|
431
|
+
# Merge into dataset attributes
|
|
432
|
+
ds.attrs.update(attrs)
|
|
433
|
+
|
|
434
|
+
return ds
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
OCEANSITES_ATTR_ORDER = [
|
|
438
|
+
# Identifiers and project-level
|
|
439
|
+
"site_code",
|
|
440
|
+
"platform_code",
|
|
441
|
+
"deployment_code",
|
|
442
|
+
"data_mode",
|
|
443
|
+
"title",
|
|
444
|
+
"theme",
|
|
445
|
+
"summary",
|
|
446
|
+
"naming_authority",
|
|
447
|
+
"id",
|
|
448
|
+
"internal_identifier",
|
|
449
|
+
"wmo_platform_code",
|
|
450
|
+
"source",
|
|
451
|
+
"source_vocabulary",
|
|
452
|
+
"principal_investigator",
|
|
453
|
+
"principal_investigator_email",
|
|
454
|
+
"principal_investigator_id",
|
|
455
|
+
"creator_name",
|
|
456
|
+
"creator_email",
|
|
457
|
+
"creator_url",
|
|
458
|
+
"creator_institution",
|
|
459
|
+
"creator_id",
|
|
460
|
+
"creator_type",
|
|
461
|
+
"array",
|
|
462
|
+
"network",
|
|
463
|
+
"keywords",
|
|
464
|
+
"keywords_vocabulary",
|
|
465
|
+
"comment",
|
|
466
|
+
# Geospatial and temporal
|
|
467
|
+
"sea_area",
|
|
468
|
+
"sea_area_vocabulary",
|
|
469
|
+
"geospatial_lat_min",
|
|
470
|
+
"geospatial_lat_max",
|
|
471
|
+
"geospatial_lon_min",
|
|
472
|
+
"geospatial_lon_max",
|
|
473
|
+
"geospatial_vertical_min",
|
|
474
|
+
"geospatial_vertical_max",
|
|
475
|
+
"geospatial_vertical_units",
|
|
476
|
+
"geospatial_vertical_positive",
|
|
477
|
+
"time_coverage_start",
|
|
478
|
+
"time_coverage_end",
|
|
479
|
+
"time_coverage_duration",
|
|
480
|
+
"time_coverage_resolution",
|
|
481
|
+
# Platform and feature metadata
|
|
482
|
+
"cdm_data_type",
|
|
483
|
+
"featureType",
|
|
484
|
+
"platform_deployment_date",
|
|
485
|
+
"platform_deployment_ship_name",
|
|
486
|
+
"platform_deployment_cruise_name",
|
|
487
|
+
"platform_recovery_date",
|
|
488
|
+
"platform_recovery_ship_name",
|
|
489
|
+
"platform_recovery_cruise_name",
|
|
490
|
+
# File format and publisher
|
|
491
|
+
"data_type",
|
|
492
|
+
"format_version",
|
|
493
|
+
"Conventions",
|
|
494
|
+
"publisher_name",
|
|
495
|
+
"publisher_email",
|
|
496
|
+
"publisher_id",
|
|
497
|
+
"publisher_institution",
|
|
498
|
+
"publisher_institution_vocabulary",
|
|
499
|
+
# Links and credits
|
|
500
|
+
"references",
|
|
501
|
+
"update_interval",
|
|
502
|
+
"license",
|
|
503
|
+
"citation",
|
|
504
|
+
"acknowledgement",
|
|
505
|
+
# History and QA
|
|
506
|
+
"date_created",
|
|
507
|
+
"date_modified",
|
|
508
|
+
"history",
|
|
509
|
+
"processing_level",
|
|
510
|
+
"QC_indicator",
|
|
511
|
+
# Contributor info
|
|
512
|
+
"contributor_name",
|
|
513
|
+
"contributor_email",
|
|
514
|
+
"contributor_id",
|
|
515
|
+
"contributor_role",
|
|
516
|
+
"contributor_role_vocabulary",
|
|
517
|
+
]
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def sort_global_attributes(ds, attr_order=None):
|
|
521
|
+
if attr_order is None:
|
|
522
|
+
attr_order = OCEANSITES_ATTR_ORDER
|
|
523
|
+
|
|
524
|
+
ordered = {k: ds.attrs[k] for k in attr_order if k in ds.attrs}
|
|
525
|
+
remaining = {k: v for k, v in ds.attrs.items() if k not in ordered}
|
|
526
|
+
ds.attrs = {**ordered, **remaining}
|
|
527
|
+
return ds
|