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
oceanarray/tools.py
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
"""Science utility functions for oceanographic data analysis."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
import gsw
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
import xarray as xr
|
|
9
|
+
from scipy.signal import find_peaks
|
|
10
|
+
|
|
11
|
+
from oceanarray import utilities
|
|
12
|
+
|
|
13
|
+
# Initialize logging
|
|
14
|
+
_log = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def lag_correlation(x, y, max_lag, min_overlap=10):
|
|
18
|
+
"""Pearson correlation at integer lags in [-max_lag, max_lag]."""
|
|
19
|
+
x = np.asarray(x, float)
|
|
20
|
+
y = np.asarray(y, float)
|
|
21
|
+
if x.shape != y.shape:
|
|
22
|
+
raise ValueError("x and y must have same length (subsample both).") # noqa: TRY003
|
|
23
|
+
corrs = np.full(2 * max_lag + 1, np.nan)
|
|
24
|
+
for k, lag in enumerate(range(-max_lag, max_lag + 1)):
|
|
25
|
+
if lag < 0:
|
|
26
|
+
xs = x[:lag] # up to last |lag|
|
|
27
|
+
ys = y[-lag:] # from |lag| to end
|
|
28
|
+
elif lag > 0:
|
|
29
|
+
xs = x[lag:] # from lag to end
|
|
30
|
+
ys = y[:-lag] # up to n-lag
|
|
31
|
+
else:
|
|
32
|
+
xs, ys = x, y
|
|
33
|
+
|
|
34
|
+
m = ~np.isnan(xs) & ~np.isnan(ys)
|
|
35
|
+
if m.sum() >= min_overlap:
|
|
36
|
+
xc = xs[m] - np.nanmean(xs[m])
|
|
37
|
+
yc = ys[m] - np.nanmean(ys[m])
|
|
38
|
+
denom = np.nanstd(xc) * np.nanstd(yc)
|
|
39
|
+
corrs[k] = (np.nanmean(xc * yc) / denom) if denom > 0 else np.nan
|
|
40
|
+
return corrs
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def split_value(data, nbins=30):
|
|
44
|
+
"""Find a histogram-based split value between two data modes."""
|
|
45
|
+
data = data[~np.isnan(data)] # Remove NaNs for histogram
|
|
46
|
+
# Step 1: Create histogram
|
|
47
|
+
counts, bins = np.histogram(data, bins=nbins)
|
|
48
|
+
|
|
49
|
+
# Step 2: Find peaks
|
|
50
|
+
peaks, _ = find_peaks(counts)
|
|
51
|
+
|
|
52
|
+
# Step 3: Find minimum between first two major peaks
|
|
53
|
+
if len(peaks) >= 2:
|
|
54
|
+
i1, i2 = sorted(peaks[:2])
|
|
55
|
+
split_index = np.argmin(counts[i1:i2]) + i1
|
|
56
|
+
splitter = bins[split_index]
|
|
57
|
+
# print("Split value:", splitter)
|
|
58
|
+
return splitter
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def find_cold_entry_exit(
|
|
62
|
+
time, temp, quantile=0.95, dwell_seconds=1800, smooth_window=5
|
|
63
|
+
):
|
|
64
|
+
"""Identify first sustained entry into 'cold' regime and last sustained exit.
|
|
65
|
+
|
|
66
|
+
Parameters
|
|
67
|
+
----------
|
|
68
|
+
time : array-like of datetime64
|
|
69
|
+
Time coordinate array.
|
|
70
|
+
temp : array-like of float
|
|
71
|
+
Temperature values aligned with *time*.
|
|
72
|
+
quantile : float
|
|
73
|
+
Percentile for threshold (e.g. 0.1 ~ 10th percentile).
|
|
74
|
+
dwell_seconds : int
|
|
75
|
+
Minimum time in cold regime for it to count (seconds).
|
|
76
|
+
smooth_window : int
|
|
77
|
+
Rolling median window length (samples).
|
|
78
|
+
|
|
79
|
+
Returns
|
|
80
|
+
-------
|
|
81
|
+
t_start, t_end, threshold : tuple of (Timestamp or None, Timestamp or None, float)
|
|
82
|
+
|
|
83
|
+
"""
|
|
84
|
+
time = pd.to_datetime(time)
|
|
85
|
+
temp = np.asarray(temp, dtype=float)
|
|
86
|
+
thr = np.nanquantile(temp, quantile)
|
|
87
|
+
|
|
88
|
+
# smooth with rolling median
|
|
89
|
+
if smooth_window > 1:
|
|
90
|
+
temp = (
|
|
91
|
+
pd.Series(temp)
|
|
92
|
+
.rolling(smooth_window, center=True, min_periods=1)
|
|
93
|
+
.median()
|
|
94
|
+
.values
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
below = temp <= thr
|
|
98
|
+
if not below.any():
|
|
99
|
+
return None, None, thr
|
|
100
|
+
|
|
101
|
+
# sampling interval (assume near-regular)
|
|
102
|
+
dt = np.nanmedian(np.diff(time.values).astype("timedelta64[s]").astype(float))
|
|
103
|
+
min_len = int(np.ceil(dwell_seconds / dt))
|
|
104
|
+
|
|
105
|
+
# contiguous runs
|
|
106
|
+
idx = np.where(below)[0]
|
|
107
|
+
gaps = np.diff(idx) > 1
|
|
108
|
+
starts = np.r_[idx[0], idx[1:][gaps]]
|
|
109
|
+
ends = np.r_[idx[:-1][gaps], idx[-1]]
|
|
110
|
+
|
|
111
|
+
runs = [(s, e) for s, e in zip(starts, ends) if (e - s + 1) >= min_len]
|
|
112
|
+
if not runs:
|
|
113
|
+
return None, None, thr
|
|
114
|
+
|
|
115
|
+
s0, _ = runs[0]
|
|
116
|
+
_, eL = runs[-1]
|
|
117
|
+
return time[s0], time[eL], thr
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def calc_psal(ds):
|
|
121
|
+
"""Compute Practical Salinity from conductivity, temperature, and pressure.
|
|
122
|
+
|
|
123
|
+
Uses the Gibbs SeaWater (GSW) toolbox: ``gsw.SP_from_C`` applies the
|
|
124
|
+
PSS-78 equation to derive Practical Salinity (dimensionless, roughly PSU)
|
|
125
|
+
from conductivity (mS cm⁻¹), temperature (°C), and pressure (dbar).
|
|
126
|
+
|
|
127
|
+
If ``PSAL`` is already present in *ds* the function is a no-op.
|
|
128
|
+
|
|
129
|
+
Parameters
|
|
130
|
+
----------
|
|
131
|
+
ds : xarray.Dataset
|
|
132
|
+
Dataset containing ``CNDC`` (conductivity, mS cm⁻¹), ``TEMP``
|
|
133
|
+
(temperature, °C), and ``PRES`` (pressure, dbar).
|
|
134
|
+
|
|
135
|
+
Returns
|
|
136
|
+
-------
|
|
137
|
+
xarray.Dataset
|
|
138
|
+
Input dataset with ``PSAL`` added (same dimensions as ``CNDC``),
|
|
139
|
+
or unchanged if ``PSAL`` was already present.
|
|
140
|
+
|
|
141
|
+
"""
|
|
142
|
+
if "PSAL" not in ds:
|
|
143
|
+
SP = gsw.SP_from_C(ds["CNDC"], ds["TEMP"], ds["PRES"])
|
|
144
|
+
ds["PSAL"] = (ds["CNDC"].dims, SP.data)
|
|
145
|
+
else:
|
|
146
|
+
print("ds already contains variable PSAL")
|
|
147
|
+
return ds
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def flag_salinity_outliers(ds, n_std=4):
|
|
151
|
+
"""Flags PSAL values that are more than n_std standard deviations from the mean,
|
|
152
|
+
computed separately for each depth level.
|
|
153
|
+
|
|
154
|
+
Parameters
|
|
155
|
+
----------
|
|
156
|
+
ds : xarray.Dataset
|
|
157
|
+
Dataset containing "PSAL" variable with dimensions including "DEPTH".
|
|
158
|
+
n_std : float, optional
|
|
159
|
+
Number of standard deviations from the mean to define an outlier (default is 4).
|
|
160
|
+
|
|
161
|
+
Returns
|
|
162
|
+
-------
|
|
163
|
+
xarray.DataArray (bool)
|
|
164
|
+
Boolean array with True where salinity is flagged as an outlier.
|
|
165
|
+
|
|
166
|
+
"""
|
|
167
|
+
if "CNDC_QC" in ds:
|
|
168
|
+
mask = ds["CNDC_QC"] == 0
|
|
169
|
+
mean_sal = ds["PSAL"].where(mask).mean(dim="TIME", skipna=True)
|
|
170
|
+
std_sal = ds["PSAL"].where(mask).std(dim="TIME", skipna=True)
|
|
171
|
+
else:
|
|
172
|
+
mean_sal = ds["PSAL"].mean(dim="TIME", skipna=True)
|
|
173
|
+
std_sal = ds["PSAL"].std(dim="TIME", skipna=True)
|
|
174
|
+
|
|
175
|
+
# Broadcast mean and std over time
|
|
176
|
+
lower = mean_sal - n_std * std_sal
|
|
177
|
+
upper = mean_sal + n_std * std_sal
|
|
178
|
+
|
|
179
|
+
flag = (ds["PSAL"] < lower) | (ds["PSAL"] > upper)
|
|
180
|
+
return flag
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def flag_temporal_spikes(ds, var="CNDC", threshold=5):
|
|
184
|
+
"""Flags large absolute differences in time for each depth.
|
|
185
|
+
|
|
186
|
+
threshold: maximum allowed difference in units of the variable
|
|
187
|
+
"""
|
|
188
|
+
diff = np.abs(ds[var].diff("TIME", label="upper"))
|
|
189
|
+
flag = diff > threshold
|
|
190
|
+
# Pad to match original dimensions
|
|
191
|
+
flag = flag.reindex(TIME=ds["TIME"], method="ffill")
|
|
192
|
+
flag = flag.fillna(False)
|
|
193
|
+
return flag.astype(bool)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def flag_vertical_inconsistencies(ds, var="CNDC", threshold=2):
|
|
197
|
+
"""Flags points that are very different from vertical neighbors.
|
|
198
|
+
threshold: max allowed difference between vertically adjacent sensors.
|
|
199
|
+
"""
|
|
200
|
+
# Central difference approximation in depth
|
|
201
|
+
vert_diff = np.abs(ds[var].diff("DEPTH"))
|
|
202
|
+
# Pad to match original dimensions
|
|
203
|
+
vert_diff = vert_diff.reindex(DEPTH=ds["DEPTH"], method="ffill")
|
|
204
|
+
flag = vert_diff > threshold
|
|
205
|
+
return flag
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def run_qc(ds):
|
|
209
|
+
"""Apply a sequence of QC tests and write combined CNDC_QC flag variable."""
|
|
210
|
+
if "PSAL" not in ds:
|
|
211
|
+
ds = calc_psal(ds)
|
|
212
|
+
|
|
213
|
+
sal_flag = flag_salinity_outliers(ds, 6).astype(bool)
|
|
214
|
+
time_flag = flag_temporal_spikes(ds).astype(bool)
|
|
215
|
+
# vert_flag = flag_vertical_inconsistencies(ds).astype(bool)
|
|
216
|
+
|
|
217
|
+
# Combine all
|
|
218
|
+
combined_flag = sal_flag | time_flag
|
|
219
|
+
ds["CNDC_QC"] = (ds["CNDC"].dims, combined_flag.data.astype(int))
|
|
220
|
+
|
|
221
|
+
sal_flag = flag_salinity_outliers(ds, 3).astype(bool)
|
|
222
|
+
combined_flag = sal_flag | time_flag
|
|
223
|
+
ds["CNDC_QC"] = (ds["CNDC"].dims, combined_flag.data.astype(int))
|
|
224
|
+
|
|
225
|
+
return ds
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def downsample_to_sparse(
|
|
229
|
+
temp_profiles, salt_profiles, full_pressures, sparse_pressures
|
|
230
|
+
):
|
|
231
|
+
"""Downsample full T/S profiles to sparse pressure levels.
|
|
232
|
+
|
|
233
|
+
Parameters
|
|
234
|
+
----------
|
|
235
|
+
temp_profiles : np.ndarray
|
|
236
|
+
Full temperature profiles, shape (n_profiles, n_pressures_full).
|
|
237
|
+
salt_profiles : np.ndarray
|
|
238
|
+
Full salinity profiles, shape (n_profiles, n_pressures_full).
|
|
239
|
+
full_pressures : np.ndarray
|
|
240
|
+
Full pressure levels corresponding to temp_profiles and salt_profiles, shape (n_pressures_full,).
|
|
241
|
+
sparse_pressures : np.ndarray
|
|
242
|
+
Target sparse pressure levels to sample, shape (n_pressures_sparse,).
|
|
243
|
+
|
|
244
|
+
Returns
|
|
245
|
+
-------
|
|
246
|
+
sparse_inputs : np.ndarray
|
|
247
|
+
Concatenated sparse temperature and salinity features,
|
|
248
|
+
shape (n_profiles, 2 * n_pressures_sparse).
|
|
249
|
+
(temp_sparse followed by salt_sparse)
|
|
250
|
+
|
|
251
|
+
"""
|
|
252
|
+
n_profiles = temp_profiles.shape[0]
|
|
253
|
+
all_temp = []
|
|
254
|
+
all_salt = []
|
|
255
|
+
|
|
256
|
+
for i in range(n_profiles):
|
|
257
|
+
temp_sparse = np.interp(
|
|
258
|
+
sparse_pressures,
|
|
259
|
+
full_pressures,
|
|
260
|
+
temp_profiles[i],
|
|
261
|
+
left=np.nan,
|
|
262
|
+
right=np.nan,
|
|
263
|
+
)
|
|
264
|
+
salt_sparse = np.interp(
|
|
265
|
+
sparse_pressures,
|
|
266
|
+
full_pressures,
|
|
267
|
+
salt_profiles[i],
|
|
268
|
+
left=np.nan,
|
|
269
|
+
right=np.nan,
|
|
270
|
+
)
|
|
271
|
+
all_temp.append(temp_sparse)
|
|
272
|
+
all_salt.append(salt_sparse)
|
|
273
|
+
|
|
274
|
+
return np.array(all_temp), np.array(all_salt)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def process_dataset(
|
|
278
|
+
ds: xr.Dataset,
|
|
279
|
+
latlim: tuple[float, float] = (26.0, 27.0),
|
|
280
|
+
lonlim: tuple[float, float] = (-77.0, -76.5),
|
|
281
|
+
pgrid: np.ndarray = None,
|
|
282
|
+
) -> tuple[xr.Dataset, xr.Dataset]:
|
|
283
|
+
"""Filter and process a hydrographic dataset for use in training.
|
|
284
|
+
|
|
285
|
+
This function selects a region of interest, extracts and downsamples profiles of
|
|
286
|
+
temperature and salinity onto both standard and sparse pressure grids. It also computes
|
|
287
|
+
potential density anomaly for both resolutions.
|
|
288
|
+
|
|
289
|
+
Parameters
|
|
290
|
+
----------
|
|
291
|
+
ds : xr.Dataset
|
|
292
|
+
Input dataset containing hydrographic data including CT, SA, PRES, and metadata.
|
|
293
|
+
latlim : tuple of float, optional
|
|
294
|
+
Latitude limits for filtering, by default (26.0, 27.0).
|
|
295
|
+
lonlim : tuple of float, optional
|
|
296
|
+
Longitude limits for filtering, by default (-77.0, -76.5).
|
|
297
|
+
pgrid : np.ndarray, optional
|
|
298
|
+
Target pressure levels (dbar) for vertical interpolation. When None,
|
|
299
|
+
a 20 dbar grid is constructed automatically from 0 to the maximum
|
|
300
|
+
observed pressure.
|
|
301
|
+
|
|
302
|
+
Returns
|
|
303
|
+
-------
|
|
304
|
+
ds_standard : xr.Dataset
|
|
305
|
+
Dataset downsampled to standard pressure levels.
|
|
306
|
+
ds_sparse : xr.Dataset
|
|
307
|
+
Dataset downsampled to sparse pressure levels.
|
|
308
|
+
|
|
309
|
+
See Also
|
|
310
|
+
--------
|
|
311
|
+
verticalnn.data_utils.downsample_to_sparse : Used to interpolate to target pressure levels.
|
|
312
|
+
verticalnn.config.STANDARD_PRESSURES : Standard pressure grid.
|
|
313
|
+
verticalnn.config.SPARSE_PRESSURES : Sparse pressure grid.
|
|
314
|
+
|
|
315
|
+
"""
|
|
316
|
+
pres_key, time_key, pres_dim, time_dim = utilities.get_dims(ds)
|
|
317
|
+
|
|
318
|
+
if pgrid is None:
|
|
319
|
+
max_pres = np.nanmax(ds[pres_key].values)
|
|
320
|
+
pgrid = np.arange(0, max_pres + 1, 20)
|
|
321
|
+
|
|
322
|
+
# Extract variables
|
|
323
|
+
TEMP_profiles = ds["TEMP"].values
|
|
324
|
+
PSAL_profiles = ds["PSAL"].values
|
|
325
|
+
CT_profiles = ds["CT"].values
|
|
326
|
+
SA_profiles = ds["SA"].values
|
|
327
|
+
PRES = ds[pres_key].values
|
|
328
|
+
LAT = ds["LATITUDE"].values
|
|
329
|
+
LON = ds["LONGITUDE"].values
|
|
330
|
+
time = ds[time_key].values
|
|
331
|
+
|
|
332
|
+
# Apply region of interest mask
|
|
333
|
+
mask = (
|
|
334
|
+
(LAT >= latlim[0])
|
|
335
|
+
& (LAT <= latlim[1])
|
|
336
|
+
& (LON >= lonlim[0])
|
|
337
|
+
& (LON <= lonlim[1])
|
|
338
|
+
)
|
|
339
|
+
TEMP_profiles = TEMP_profiles[mask]
|
|
340
|
+
PSAL_profiles = PSAL_profiles[mask]
|
|
341
|
+
CT_profiles = CT_profiles[mask]
|
|
342
|
+
SA_profiles = SA_profiles[mask]
|
|
343
|
+
time = time[mask]
|
|
344
|
+
LAT = LAT[mask]
|
|
345
|
+
LON = LON[mask]
|
|
346
|
+
|
|
347
|
+
# Downsample to standard pressures
|
|
348
|
+
CT_standard, SA_standard = downsample_to_sparse(
|
|
349
|
+
CT_profiles, SA_profiles, PRES, pgrid
|
|
350
|
+
)
|
|
351
|
+
# Downsample to standard pressures
|
|
352
|
+
temp_standard, salt_standard = downsample_to_sparse(
|
|
353
|
+
TEMP_profiles, PSAL_profiles, PRES, pgrid
|
|
354
|
+
)
|
|
355
|
+
standard_pressures = pgrid.flatten()
|
|
356
|
+
|
|
357
|
+
# Create ds_standard
|
|
358
|
+
ds_standard = xr.Dataset(
|
|
359
|
+
{
|
|
360
|
+
"CT": ((time_dim, pres_dim), CT_standard),
|
|
361
|
+
"SA": ((time_dim, pres_dim), SA_standard),
|
|
362
|
+
"TEMP": ((time_dim, pres_dim), temp_standard),
|
|
363
|
+
"PSAL": ((time_dim, pres_dim), salt_standard),
|
|
364
|
+
},
|
|
365
|
+
coords={
|
|
366
|
+
time_key: (time_dim, time),
|
|
367
|
+
pres_key: (pres_dim, standard_pressures),
|
|
368
|
+
"LATITUDE": (time_dim, LAT),
|
|
369
|
+
"LONGITUDE": (time_dim, LON),
|
|
370
|
+
},
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
return ds_standard
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def calc_ds_difference(ds1, ds2):
|
|
377
|
+
"""Compute the variable-by-variable difference between two time-matched datasets."""
|
|
378
|
+
if not np.array_equal(ds1["TIME"].values, ds2["TIME"].values):
|
|
379
|
+
raise ValueError("TIME grids do not match between datasets.") # noqa: TRY003
|
|
380
|
+
|
|
381
|
+
# Variables to exclude from differencing
|
|
382
|
+
exclude_vars = {"YY", "MM", "DD", "HH"}
|
|
383
|
+
|
|
384
|
+
# Prepare a dict for new data variables
|
|
385
|
+
diff_data = {}
|
|
386
|
+
|
|
387
|
+
for var in ds1.data_vars:
|
|
388
|
+
# Only difference variables with dimension TIME and not in exclude list
|
|
389
|
+
if "TIME" in ds1[var].dims and var not in exclude_vars:
|
|
390
|
+
diff_data[var] = ds1[var] - ds2[var]
|
|
391
|
+
else:
|
|
392
|
+
# Copy over variables that are not differenced
|
|
393
|
+
diff_data[var] = ds1[var]
|
|
394
|
+
|
|
395
|
+
# Create a new dataset with the same coordinates and attributes
|
|
396
|
+
ds_diff = ds1.copy()
|
|
397
|
+
for var in diff_data:
|
|
398
|
+
ds_diff[var] = diff_data[var]
|
|
399
|
+
|
|
400
|
+
return ds_diff
|
oceanarray/transports.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Array-level transport computations across multiple moorings (work in progress)."""
|