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,739 @@
|
|
|
1
|
+
"""Physics-informed vertical interpolation scheme for sparse ocean profiles.
|
|
2
|
+
|
|
3
|
+
This module implements the core algorithm used in the RAPID array, which
|
|
4
|
+
reconstructs full-depth temperature and salinity profiles using climatological
|
|
5
|
+
vertical gradients.
|
|
6
|
+
|
|
7
|
+
Functions
|
|
8
|
+
---------
|
|
9
|
+
- spacing : Generate evenly spaced pressure levels from start to end.
|
|
10
|
+
- save_climatology : Save climatology fields as a NetCDF dataset.
|
|
11
|
+
- smooth_climatology : Apply running mean smoothing to climatology fields.
|
|
12
|
+
- interpolate_internal : Interpolate between two sparse profile points using gradient fields.
|
|
13
|
+
- extrapolate_boundary : Extrapolate into unsampled regions near profile boundaries.
|
|
14
|
+
|
|
15
|
+
References
|
|
16
|
+
----------
|
|
17
|
+
- Vertical interpolation based on climatological gradients (Johns et al., 2001).
|
|
18
|
+
- Adapted and translated from Matlab routines by T. Kanzow (2000).
|
|
19
|
+
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import pathlib
|
|
23
|
+
from datetime import datetime
|
|
24
|
+
from typing import Tuple, Union
|
|
25
|
+
|
|
26
|
+
import gsw
|
|
27
|
+
import numpy as np
|
|
28
|
+
import pandas as pd
|
|
29
|
+
import xarray as xr
|
|
30
|
+
from scipy.integrate import cumulative_trapezoid
|
|
31
|
+
from scipy.interpolate import interp1d
|
|
32
|
+
|
|
33
|
+
from oceanarray import utilities
|
|
34
|
+
from oceanarray.logger import log_info
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def spacing(p_start: float, p_end: float, step: float) -> np.ndarray:
|
|
38
|
+
"""Create an array of pressures from `p_start` to `p_end` using approximately uniform spacing.
|
|
39
|
+
|
|
40
|
+
This helper function generates a 1D pressure array using either increasing or decreasing
|
|
41
|
+
steps depending on the ordering of `p_start` and `p_end`. It ensures that the end point
|
|
42
|
+
is included.
|
|
43
|
+
|
|
44
|
+
Parameters
|
|
45
|
+
----------
|
|
46
|
+
p_start : float
|
|
47
|
+
Starting pressure [dbar].
|
|
48
|
+
p_end : float
|
|
49
|
+
Ending pressure [dbar].
|
|
50
|
+
step : float
|
|
51
|
+
Approximate pressure increment [dbar].
|
|
52
|
+
|
|
53
|
+
Returns
|
|
54
|
+
-------
|
|
55
|
+
np.ndarray
|
|
56
|
+
1D array of pressure values from `p_start` to `p_end` (inclusive).
|
|
57
|
+
|
|
58
|
+
Notes
|
|
59
|
+
-----
|
|
60
|
+
This function is used internally by the vertical interpolation routines to construct
|
|
61
|
+
pressure grids between adjacent observations.
|
|
62
|
+
|
|
63
|
+
See Also
|
|
64
|
+
--------
|
|
65
|
+
verticalnn.rapid_interp.interpolate_internal : Uses this function to build vertical grids.
|
|
66
|
+
|
|
67
|
+
"""
|
|
68
|
+
if p_start < p_end:
|
|
69
|
+
return np.arange(p_start, p_end + step, step)
|
|
70
|
+
else:
|
|
71
|
+
return np.arange(p_start, p_end - step, -step)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def save_climatology(
|
|
75
|
+
clim_ds: xr.Dataset, output_path: Union[str, pathlib.Path]
|
|
76
|
+
) -> None:
|
|
77
|
+
"""Save a climatology dataset containing vertical gradients to a NetCDF file.
|
|
78
|
+
|
|
79
|
+
This function writes the input dataset—typically containing monthly fields
|
|
80
|
+
of dT/dP and dS/dP as a function of temperature—to disk, adding metadata.
|
|
81
|
+
|
|
82
|
+
Parameters
|
|
83
|
+
----------
|
|
84
|
+
clim_ds : xr.Dataset
|
|
85
|
+
Dataset containing climatological temperature and salinity gradients.
|
|
86
|
+
Should include variables like 'dTdp', 'dSdp', and coordinate 'TEMP', 'month'.
|
|
87
|
+
output_path : str or pathlib.Path
|
|
88
|
+
Destination file path for the NetCDF file.
|
|
89
|
+
|
|
90
|
+
Notes
|
|
91
|
+
-----
|
|
92
|
+
Adds metadata to the saved dataset:
|
|
93
|
+
- `description`: Short description of contents.
|
|
94
|
+
- `generated_by`: Marks source module (`verticalnn.rapid_interp`).
|
|
95
|
+
- `created_on`: ISO-formatted timestamp of when the file was written.
|
|
96
|
+
|
|
97
|
+
See Also
|
|
98
|
+
--------
|
|
99
|
+
verticalnn.rapid_interp.smooth_climatology : Smooths this dataset along the temperature axis.
|
|
100
|
+
verticalnn.rapid_interp.build_climatology : Creates the dataset to be saved.
|
|
101
|
+
|
|
102
|
+
"""
|
|
103
|
+
output_path = pathlib.Path(output_path)
|
|
104
|
+
|
|
105
|
+
# Make sure parent folder exists
|
|
106
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
|
|
108
|
+
# Add some metadata
|
|
109
|
+
clim_ds.attrs.update(
|
|
110
|
+
{
|
|
111
|
+
"description": "Monthly climatology of dT/dP and dS/dP vs temperature.",
|
|
112
|
+
"generated_by": "verticalnn rapid_interp module",
|
|
113
|
+
"created_on": datetime.now().isoformat(timespec="seconds"),
|
|
114
|
+
}
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Save
|
|
118
|
+
clim_ds.to_netcdf(output_path)
|
|
119
|
+
|
|
120
|
+
log_info(f"✅ Saved climatology to {output_path}")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def smooth_climatology(clim_ds: xr.Dataset, window: int = 3) -> xr.Dataset:
|
|
124
|
+
"""Apply rolling smoothing to vertical gradient climatology along the temperature axis.
|
|
125
|
+
|
|
126
|
+
This function smooths the dT/dP and dS/dP climatology fields using a centered
|
|
127
|
+
moving average along the temperature bins, applied independently for each month.
|
|
128
|
+
|
|
129
|
+
Parameters
|
|
130
|
+
----------
|
|
131
|
+
clim_ds : xr.Dataset
|
|
132
|
+
Input climatology dataset with variables 'dTdp' and 'dSdp' on (month, TEMP) grid.
|
|
133
|
+
window : int, optional
|
|
134
|
+
Rolling window size (number of temperature bins), by default 3.
|
|
135
|
+
|
|
136
|
+
Returns
|
|
137
|
+
-------
|
|
138
|
+
xr.Dataset
|
|
139
|
+
Smoothed climatology dataset with the same coordinates and attributes.
|
|
140
|
+
|
|
141
|
+
Notes
|
|
142
|
+
-----
|
|
143
|
+
Rolling averages are computed with `center=True` and `min_periods=1`, preserving edge values.
|
|
144
|
+
The smoothing is purely horizontal (along TEMP), not temporal.
|
|
145
|
+
|
|
146
|
+
See Also
|
|
147
|
+
--------
|
|
148
|
+
verticalnn.rapid_interp.save_climatology : saves smoothed output to disk.
|
|
149
|
+
|
|
150
|
+
"""
|
|
151
|
+
dTdp_smoothed = (
|
|
152
|
+
clim_ds["dTdp"].rolling(TEMP=window, center=True, min_periods=1).mean()
|
|
153
|
+
)
|
|
154
|
+
dSdp_smoothed = (
|
|
155
|
+
clim_ds["dSdp"].rolling(TEMP=window, center=True, min_periods=1).mean()
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
clim_ds_smoothed = xr.Dataset(
|
|
159
|
+
{
|
|
160
|
+
"dTdp": dTdp_smoothed,
|
|
161
|
+
"dSdp": dSdp_smoothed,
|
|
162
|
+
},
|
|
163
|
+
coords=clim_ds.coords,
|
|
164
|
+
attrs=clim_ds.attrs,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
return clim_ds_smoothed
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def build_climatology(
|
|
171
|
+
ds: xr.Dataset,
|
|
172
|
+
standard_pressures: np.ndarray,
|
|
173
|
+
temp_key: str = "CT",
|
|
174
|
+
salt_key: str = "SA",
|
|
175
|
+
pres_key: str = "PRESSURE",
|
|
176
|
+
time_key: str = "TIME",
|
|
177
|
+
min_profiles_per_bin: int = 5,
|
|
178
|
+
temp_bins: np.ndarray | None = None,
|
|
179
|
+
) -> xr.Dataset:
|
|
180
|
+
"""Build a seasonal climatology of dT/dP and dS/dP as a function of temperature.
|
|
181
|
+
|
|
182
|
+
This function processes hydrographic profiles to compute monthly climatologies
|
|
183
|
+
of temperature and salinity vertical gradients. It bins gradients by temperature
|
|
184
|
+
and month, and returns an xarray.Dataset with `dTdp` and `dSdp` fields indexed by
|
|
185
|
+
temperature and time of year.
|
|
186
|
+
|
|
187
|
+
Parameters
|
|
188
|
+
----------
|
|
189
|
+
ds : xr.Dataset
|
|
190
|
+
Dataset containing temperature, salinity, pressure, and time variables.
|
|
191
|
+
standard_pressures : np.ndarray
|
|
192
|
+
Vertical pressure grid [dbar] to interpolate profiles before gradient calculation.
|
|
193
|
+
temp_key : str, optional
|
|
194
|
+
Variable name for Conservative Temperature, by default "CT".
|
|
195
|
+
salt_key : str, optional
|
|
196
|
+
Variable name for Absolute Salinity, by default "SA".
|
|
197
|
+
pres_key : str, optional
|
|
198
|
+
Variable name for Pressure, by default "PRESSURE".
|
|
199
|
+
time_key : str, optional
|
|
200
|
+
Variable name for time coordinate, by default "TIME".
|
|
201
|
+
min_profiles_per_bin : int, optional
|
|
202
|
+
Minimum number of valid profiles per (month, temp) bin to accept, by default 5.
|
|
203
|
+
temp_bins : np.ndarray, optional
|
|
204
|
+
Temperature bin edges for grouping (°C). If None, defaults to -2 to 35.5°C in 0.5°C bins.
|
|
205
|
+
|
|
206
|
+
Returns
|
|
207
|
+
-------
|
|
208
|
+
xr.Dataset
|
|
209
|
+
Climatology dataset with 'dTdp' and 'dSdp' indexed by ('month', 'TEMP').
|
|
210
|
+
|
|
211
|
+
Notes
|
|
212
|
+
-----
|
|
213
|
+
- Interpolates all input profiles to `standard_pressures` before calculating gradients.
|
|
214
|
+
- Based on the physics-informed interpolation approach described in Johns et al. (2001).
|
|
215
|
+
- Original implementation adapted from Matlab code by T. Kanzow (2000).
|
|
216
|
+
|
|
217
|
+
See Also
|
|
218
|
+
--------
|
|
219
|
+
verticalnn.rapid_interp.smooth_climatology : Applies smoothing to output climatology.
|
|
220
|
+
verticalnn.rapid_interp.save_climatology : Saves climatology dataset to disk.
|
|
221
|
+
verticalnn.rapid_interp.interpolate_profiles : Uses this climatology for profile interpolation.
|
|
222
|
+
|
|
223
|
+
"""
|
|
224
|
+
if temp_bins is None:
|
|
225
|
+
temp_bins = np.arange(-2, 35.5, 0.5) # typical ocean range
|
|
226
|
+
|
|
227
|
+
# Step 1: Downsample each profile to standard pressures
|
|
228
|
+
pres_key, time_key, pres_dim, time_dim = utilities.get_dims(ds)
|
|
229
|
+
profile_dim = time_dim
|
|
230
|
+
ntime = ds.sizes[profile_dim]
|
|
231
|
+
|
|
232
|
+
# Reshape pressure array to match the number of profiles
|
|
233
|
+
if ds[pres_key].ndim == 2:
|
|
234
|
+
pressure_array = ds[pres_key].values
|
|
235
|
+
else:
|
|
236
|
+
pressure_array = np.tile(ds[pres_key].values, (ntime, 1))
|
|
237
|
+
|
|
238
|
+
temp_std = np.full((ntime, len(standard_pressures)), np.nan)
|
|
239
|
+
salt_std = np.full((ntime, len(standard_pressures)), np.nan)
|
|
240
|
+
|
|
241
|
+
for i in range(ntime):
|
|
242
|
+
t_prof = ds[temp_key].isel({profile_dim: i}).values
|
|
243
|
+
s_prof = ds[salt_key].isel({profile_dim: i}).values
|
|
244
|
+
p_prof = pressure_array[i, :]
|
|
245
|
+
|
|
246
|
+
mask = ~np.isnan(t_prof) & ~np.isnan(p_prof)
|
|
247
|
+
|
|
248
|
+
if np.sum(mask) < 2:
|
|
249
|
+
continue # skip bad profiles
|
|
250
|
+
|
|
251
|
+
temp_std[i, :] = np.interp(
|
|
252
|
+
standard_pressures, p_prof[mask], t_prof[mask], left=np.nan, right=np.nan
|
|
253
|
+
)
|
|
254
|
+
salt_std[i, :] = np.interp(
|
|
255
|
+
standard_pressures, p_prof[mask], s_prof[mask], left=np.nan, right=np.nan
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
# Step 2: Assign months
|
|
259
|
+
months = pd.to_datetime(ds[time_key].values).month # array of months (1–12)
|
|
260
|
+
|
|
261
|
+
# Step 3: Compute gradients dT/dP and dS/dP
|
|
262
|
+
dTdp = np.gradient(temp_std, standard_pressures, axis=1)
|
|
263
|
+
dSdp = np.gradient(salt_std, standard_pressures, axis=1)
|
|
264
|
+
|
|
265
|
+
# Step 4: Flatten arrays
|
|
266
|
+
flat_temp = temp_std.flatten()
|
|
267
|
+
flat_dTdp = dTdp.flatten()
|
|
268
|
+
flat_dSdp = dSdp.flatten()
|
|
269
|
+
flat_month = np.repeat(months, len(standard_pressures))
|
|
270
|
+
|
|
271
|
+
# Step 5: Bin by temperature and month
|
|
272
|
+
temp_bin_centers = 0.5 * (temp_bins[:-1] + temp_bins[1:])
|
|
273
|
+
n_temp_bins = len(temp_bin_centers)
|
|
274
|
+
|
|
275
|
+
dTdp_clim = np.full((12, n_temp_bins), np.nan)
|
|
276
|
+
dSdp_clim = np.full((12, n_temp_bins), np.nan)
|
|
277
|
+
|
|
278
|
+
for month in range(1, 13):
|
|
279
|
+
mask_month = flat_month == month
|
|
280
|
+
|
|
281
|
+
for j, (tmin, tmax) in enumerate(zip(temp_bins[:-1], temp_bins[1:])):
|
|
282
|
+
mask_temp = (flat_temp >= tmin) & (flat_temp < tmax)
|
|
283
|
+
|
|
284
|
+
combined_mask = mask_month & mask_temp
|
|
285
|
+
|
|
286
|
+
if np.sum(combined_mask) >= min_profiles_per_bin:
|
|
287
|
+
dTdp_clim[month - 1, j] = np.nanmean(flat_dTdp[combined_mask])
|
|
288
|
+
dSdp_clim[month - 1, j] = np.nanmean(flat_dSdp[combined_mask])
|
|
289
|
+
|
|
290
|
+
# Step 6: Build climatology xarray.Dataset
|
|
291
|
+
clim_ds = xr.Dataset(
|
|
292
|
+
{
|
|
293
|
+
"dTdp": (("month", "TEMP"), dTdp_clim),
|
|
294
|
+
"dSdp": (("month", "TEMP"), dSdp_clim),
|
|
295
|
+
},
|
|
296
|
+
coords={
|
|
297
|
+
"month": np.arange(1, 13),
|
|
298
|
+
"TEMP": temp_bin_centers,
|
|
299
|
+
},
|
|
300
|
+
attrs={
|
|
301
|
+
"method": "seasonal climatology built from downsampled CTD profiles",
|
|
302
|
+
"temp_bin_width": temp_bins[1] - temp_bins[0],
|
|
303
|
+
},
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
return clim_ds
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def extrapolate_boundary(
|
|
310
|
+
T: float,
|
|
311
|
+
S: float,
|
|
312
|
+
P: float,
|
|
313
|
+
p_bound: float,
|
|
314
|
+
dtdp_func,
|
|
315
|
+
dsdp_func,
|
|
316
|
+
int_step: float = 20.0,
|
|
317
|
+
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
318
|
+
"""Extrapolate temperature and salinity profiles from a boundary point using climatological gradients.
|
|
319
|
+
|
|
320
|
+
This function integrates vertical gradients of temperature (dT/dP) and salinity (dS/dP)
|
|
321
|
+
downward or upward from a single observed point to reach a specified pressure boundary.
|
|
322
|
+
|
|
323
|
+
Parameters
|
|
324
|
+
----------
|
|
325
|
+
T : float
|
|
326
|
+
Starting temperature [°C].
|
|
327
|
+
S : float
|
|
328
|
+
Starting salinity [g/kg].
|
|
329
|
+
P : float
|
|
330
|
+
Starting pressure [dbar].
|
|
331
|
+
p_bound : float
|
|
332
|
+
Target pressure to extrapolate to [dbar].
|
|
333
|
+
dtdp_func : function
|
|
334
|
+
Callable that returns dT/dP as a function of T.
|
|
335
|
+
dsdp_func : function
|
|
336
|
+
Callable that returns dS/dP as a function of T.
|
|
337
|
+
int_step : float, optional
|
|
338
|
+
Integration step size [dbar], by default 20.0.
|
|
339
|
+
|
|
340
|
+
Returns
|
|
341
|
+
-------
|
|
342
|
+
T_profile : np.ndarray
|
|
343
|
+
Extrapolated temperature profile.
|
|
344
|
+
S_profile : np.ndarray
|
|
345
|
+
Extrapolated salinity profile.
|
|
346
|
+
P_profile : np.ndarray
|
|
347
|
+
Corresponding pressure levels.
|
|
348
|
+
|
|
349
|
+
Notes
|
|
350
|
+
-----
|
|
351
|
+
This function integrates stepwise from the boundary point toward the pressure bound.
|
|
352
|
+
The step direction is automatically inferred from the sign of `(p_bound - P)`.
|
|
353
|
+
|
|
354
|
+
Based on the original Matlab routine `t_bound0.m` by T. Kanzow (2000).
|
|
355
|
+
Method described in Johns et al. (2001).
|
|
356
|
+
|
|
357
|
+
See Also
|
|
358
|
+
--------
|
|
359
|
+
verticalnn.rapid_interp.interpolate_internal : Used for interpolation between data points.
|
|
360
|
+
verticalnn.rapid_interp.interpolate_profiles : Combines extrapolation and interpolation.
|
|
361
|
+
|
|
362
|
+
"""
|
|
363
|
+
# Handle degenerate case early
|
|
364
|
+
if P == p_bound:
|
|
365
|
+
return np.array([T]), np.array([S]), np.array([P])
|
|
366
|
+
|
|
367
|
+
# Ensure consistent stepping direction
|
|
368
|
+
step = np.sign(p_bound - P) * abs(int_step)
|
|
369
|
+
|
|
370
|
+
# Create pressure steps from P to p_bound (inclusive)
|
|
371
|
+
inc = np.arange(P, p_bound, step)
|
|
372
|
+
if inc.size == 0 or inc[-1] != p_bound:
|
|
373
|
+
inc = np.append(inc, p_bound)
|
|
374
|
+
|
|
375
|
+
# Evaluate gradients at each pressure step
|
|
376
|
+
T_values = np.full_like(inc, np.nan)
|
|
377
|
+
T_values[0] = T
|
|
378
|
+
|
|
379
|
+
for i in range(1, len(inc)):
|
|
380
|
+
T_values[i] = T_values[i - 1] + dtdp_func(T_values[i - 1]) * (
|
|
381
|
+
inc[i] - inc[i - 1]
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
# Now compute gradients based on T_values
|
|
385
|
+
dTdp_vals = dtdp_func(T_values)
|
|
386
|
+
dSdp_vals = dsdp_func(T_values)
|
|
387
|
+
|
|
388
|
+
# Integrate gradients
|
|
389
|
+
T_profile = T + cumulative_trapezoid(dTdp_vals, inc, initial=0)
|
|
390
|
+
S_profile = S + cumulative_trapezoid(dSdp_vals, inc, initial=0)
|
|
391
|
+
P_profile = inc
|
|
392
|
+
|
|
393
|
+
# Flatten to 1D arrays
|
|
394
|
+
T_profile = T_profile.flatten()
|
|
395
|
+
S_profile = S_profile.flatten()
|
|
396
|
+
P_profile = P_profile.flatten()
|
|
397
|
+
|
|
398
|
+
# Ensure output is sorted increasing in pressure
|
|
399
|
+
if P_profile[0] > P_profile[-1]:
|
|
400
|
+
T_profile = np.flip(T_profile)
|
|
401
|
+
S_profile = np.flip(S_profile)
|
|
402
|
+
P_profile = np.flip(P_profile)
|
|
403
|
+
|
|
404
|
+
return T_profile, S_profile, P_profile
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def interpolate_internal(
|
|
408
|
+
T: np.ndarray,
|
|
409
|
+
S: np.ndarray,
|
|
410
|
+
P: np.ndarray,
|
|
411
|
+
dtdp_func,
|
|
412
|
+
dsdp_func,
|
|
413
|
+
int_step: float = 20.0,
|
|
414
|
+
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
415
|
+
"""Interpolate between observed data points using climatological vertical gradients.
|
|
416
|
+
|
|
417
|
+
This function fills gaps between observed pressure levels by integrating temperature
|
|
418
|
+
and salinity gradients from both the upper and lower sensors and blending the results.
|
|
419
|
+
It implements the logic of the original `t_int0.m` routine by Kanzow (2000).
|
|
420
|
+
|
|
421
|
+
Parameters
|
|
422
|
+
----------
|
|
423
|
+
T : np.ndarray
|
|
424
|
+
Observed temperature values [°C].
|
|
425
|
+
S : np.ndarray
|
|
426
|
+
Observed salinity values [g/kg].
|
|
427
|
+
P : np.ndarray
|
|
428
|
+
Observed pressure values [dbar].
|
|
429
|
+
dtdp_func : callable
|
|
430
|
+
Function that returns dT/dP given temperature.
|
|
431
|
+
dsdp_func : callable
|
|
432
|
+
Function that returns dS/dP given temperature.
|
|
433
|
+
int_step : float, optional
|
|
434
|
+
Pressure increment for integration [dbar], by default 20.0.
|
|
435
|
+
|
|
436
|
+
Returns
|
|
437
|
+
-------
|
|
438
|
+
np.ndarray
|
|
439
|
+
Interpolated temperature values between observations.
|
|
440
|
+
np.ndarray
|
|
441
|
+
Interpolated salinity values between observations.
|
|
442
|
+
np.ndarray
|
|
443
|
+
Corresponding pressure values.
|
|
444
|
+
|
|
445
|
+
Notes
|
|
446
|
+
-----
|
|
447
|
+
This function performs dual integration (from above and below) and blends the two
|
|
448
|
+
estimates using linear weighting. Duplicate pressure values are removed in the final output.
|
|
449
|
+
- Adapted from original Matlab function `t_int0.m`.
|
|
450
|
+
- Author: T. Kanzow, 4 April 2000.
|
|
451
|
+
- Part of the vertical interpolation scheme described in Johns et al. (2001):
|
|
452
|
+
"The Kuroshio east of Taiwan: Moored transport observations from the WOCE PCM-1 Array."
|
|
453
|
+
This version translated to Python and adapted for TEOS-10 Conservative Temperature and Absolute Salinity.
|
|
454
|
+
|
|
455
|
+
See Also
|
|
456
|
+
--------
|
|
457
|
+
verticalnn.rapid_interp.extrapolate_boundary : For extrapolation above or below the data range.
|
|
458
|
+
verticalnn.rapid_interp.interpolate_profiles : Applies this interpolation to all profiles in a dataset.
|
|
459
|
+
|
|
460
|
+
"""
|
|
461
|
+
innerT = []
|
|
462
|
+
innerS = []
|
|
463
|
+
innerP = []
|
|
464
|
+
|
|
465
|
+
ni = len(T) - 1 # number of separate gaps to fill
|
|
466
|
+
|
|
467
|
+
for i in range(ni):
|
|
468
|
+
p_start = P[i]
|
|
469
|
+
p_end = P[i + 1]
|
|
470
|
+
t_start = T[i]
|
|
471
|
+
t_end = T[i + 1]
|
|
472
|
+
s_start = S[i]
|
|
473
|
+
s_end = S[i + 1]
|
|
474
|
+
|
|
475
|
+
# Build pressure grid between points
|
|
476
|
+
inc = np.arange(p_start, p_end, np.sign(p_end - p_start) * int_step)
|
|
477
|
+
inc = np.append(inc, p_end)
|
|
478
|
+
|
|
479
|
+
if len(inc) < 2:
|
|
480
|
+
continue
|
|
481
|
+
|
|
482
|
+
dinc_up = np.diff(inc)
|
|
483
|
+
dinc_down = np.diff(inc[::-1])
|
|
484
|
+
|
|
485
|
+
# Upward integration (from top sensor)
|
|
486
|
+
tgrad1 = (
|
|
487
|
+
dtdp_func(t_start)
|
|
488
|
+
if np.isscalar(t_start)
|
|
489
|
+
else dtdp_func(np.array([t_start]))[0]
|
|
490
|
+
)
|
|
491
|
+
sgrad1 = (
|
|
492
|
+
dsdp_func(t_start)
|
|
493
|
+
if np.isscalar(t_start)
|
|
494
|
+
else dsdp_func(np.array([t_start]))[0]
|
|
495
|
+
)
|
|
496
|
+
T1 = t_start + cumulative_trapezoid(np.full_like(inc, tgrad1), inc, initial=0)
|
|
497
|
+
S1 = s_start + cumulative_trapezoid(np.full_like(inc, sgrad1), inc, initial=0)
|
|
498
|
+
|
|
499
|
+
# Downward integration (from bottom sensor)
|
|
500
|
+
tgrad2 = (
|
|
501
|
+
dtdp_func(t_end) if np.isscalar(t_end) else dtdp_func(np.array([t_end]))[0]
|
|
502
|
+
)
|
|
503
|
+
sgrad2 = (
|
|
504
|
+
dsdp_func(t_end) if np.isscalar(t_end) else dsdp_func(np.array([t_end]))[0]
|
|
505
|
+
)
|
|
506
|
+
T2 = t_end + cumulative_trapezoid(
|
|
507
|
+
np.full_like(inc[::-1], tgrad2), inc[::-1], initial=0
|
|
508
|
+
)
|
|
509
|
+
S2 = s_end + cumulative_trapezoid(
|
|
510
|
+
np.full_like(inc[::-1], sgrad2), inc[::-1], initial=0
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
T2 = T2[::-1]
|
|
514
|
+
S2 = S2[::-1]
|
|
515
|
+
|
|
516
|
+
# Weights
|
|
517
|
+
w1 = 1 - np.abs(inc - p_start) / np.abs(p_end - p_start)
|
|
518
|
+
w2 = 1 - np.abs(inc - p_end) / np.abs(p_end - p_start)
|
|
519
|
+
|
|
520
|
+
# Weighted average
|
|
521
|
+
T_comb = w1 * T1 + w2 * T2
|
|
522
|
+
S_comb = w1 * S1 + w2 * S2
|
|
523
|
+
|
|
524
|
+
innerT.append(T_comb)
|
|
525
|
+
innerS.append(S_comb)
|
|
526
|
+
innerP.append(inc)
|
|
527
|
+
|
|
528
|
+
if not innerT:
|
|
529
|
+
return np.array([]), np.array([]), np.array([])
|
|
530
|
+
|
|
531
|
+
innerT = np.concatenate(innerT)
|
|
532
|
+
innerS = np.concatenate(innerS)
|
|
533
|
+
innerP = np.concatenate(innerP)
|
|
534
|
+
|
|
535
|
+
# Remove duplicate pressures
|
|
536
|
+
_, unique_idx = np.unique(innerP, return_index=True)
|
|
537
|
+
innerT = innerT[unique_idx]
|
|
538
|
+
innerS = innerS[unique_idx]
|
|
539
|
+
innerP = innerP[unique_idx]
|
|
540
|
+
|
|
541
|
+
return innerT, innerS, innerP
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def interpolate_profiles(
|
|
545
|
+
ds: xr.Dataset,
|
|
546
|
+
clim_ds: xr.Dataset,
|
|
547
|
+
temp_key: str = "CT",
|
|
548
|
+
salt_key: str = "SA",
|
|
549
|
+
pres_key: str = "PRES",
|
|
550
|
+
time_key: str = "TIME",
|
|
551
|
+
p_grid: np.ndarray | None = None,
|
|
552
|
+
int_step: float = 20.0,
|
|
553
|
+
extrapolate: bool = True,
|
|
554
|
+
) -> xr.Dataset:
|
|
555
|
+
"""Interpolate sparse moored profiles onto a regular vertical grid using climatological gradients.
|
|
556
|
+
|
|
557
|
+
This function performs both interpolation between observed levels and extrapolation
|
|
558
|
+
beyond observed bounds using seasonal climatologies of dT/dP and dS/dP, following
|
|
559
|
+
the physics-informed method described by Johns et al. (2001) and originally implemented
|
|
560
|
+
in Matlab by T. Kanzow (2000).
|
|
561
|
+
|
|
562
|
+
Parameters
|
|
563
|
+
----------
|
|
564
|
+
ds : xr.Dataset
|
|
565
|
+
Input dataset containing sparse temperature, salinity, and pressure profiles.
|
|
566
|
+
clim_ds : xr.Dataset
|
|
567
|
+
Climatology dataset with vertical gradients (variables: 'dTdp', 'dSdp') indexed by TEMP and month.
|
|
568
|
+
temp_key : str, optional
|
|
569
|
+
Variable name for Conservative Temperature, by default "CT".
|
|
570
|
+
salt_key : str, optional
|
|
571
|
+
Variable name for Absolute Salinity, by default "SA".
|
|
572
|
+
pres_key : str, optional
|
|
573
|
+
Variable name for pressure, by default "PRES".
|
|
574
|
+
time_key : str, optional
|
|
575
|
+
Variable name for profile time coordinate, by default "TIME".
|
|
576
|
+
p_grid : np.ndarray, optional
|
|
577
|
+
Target vertical grid for output profiles [dbar], by default `np.arange(0, 5000, 20)`.
|
|
578
|
+
int_step : float, optional
|
|
579
|
+
Step size for vertical integration [dbar], by default 20.
|
|
580
|
+
extrapolate : bool, optional
|
|
581
|
+
Whether to extrapolate above and below observed pressures, by default True.
|
|
582
|
+
|
|
583
|
+
Returns
|
|
584
|
+
-------
|
|
585
|
+
xr.Dataset
|
|
586
|
+
Interpolated dataset with gridded CT, SA, and SIGMA0 on the pressure grid.
|
|
587
|
+
|
|
588
|
+
Notes
|
|
589
|
+
-----
|
|
590
|
+
- Uses temperature-dependent vertical gradients from `clim_ds`.
|
|
591
|
+
- Combines logic of `con_tprof0.m`, `t_int0.m`, and `t_bound0.m` (Kanzow 2000).
|
|
592
|
+
- Final dataset includes derived potential density (SIGMA0) using TEOS-10.
|
|
593
|
+
|
|
594
|
+
See Also
|
|
595
|
+
--------
|
|
596
|
+
verticalnn.rapid_interp.build_climatology : Generates the dT/dP and dS/dP climatology.
|
|
597
|
+
verticalnn.rapid_interp.interpolate_internal : Fills gaps between sensors.
|
|
598
|
+
verticalnn.rapid_interp.extrapolate_boundary : Fills above/below observed range.
|
|
599
|
+
|
|
600
|
+
"""
|
|
601
|
+
# Check if the climatology dataset has the required variables
|
|
602
|
+
utilities.check_necessary_variables(clim_ds, vars=["dTdp", "dSdp"])
|
|
603
|
+
utilities.check_necessary_variables(
|
|
604
|
+
ds, vars=[temp_key, salt_key, "LATITUDE", "LONGITUDE"]
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
pres_key, time_key1, pres_dim, time_dim = utilities.get_dims(ds)
|
|
608
|
+
if time_key1 != time_key:
|
|
609
|
+
raise ValueError(f"Time key mismatch: {time_key1} != {time_key}")
|
|
610
|
+
|
|
611
|
+
if p_grid is None:
|
|
612
|
+
p_grid = np.arange(0, 5000, 20)
|
|
613
|
+
|
|
614
|
+
profile_dim = time_dim
|
|
615
|
+
ntime = ds.sizes[profile_dim]
|
|
616
|
+
|
|
617
|
+
t_con = np.full((ntime, len(p_grid)), np.nan)
|
|
618
|
+
s_con = np.full((ntime, len(p_grid)), np.nan)
|
|
619
|
+
|
|
620
|
+
TEMP = clim_ds["TEMP"].values
|
|
621
|
+
|
|
622
|
+
# Reshape pressure array to match the number of profiles
|
|
623
|
+
if ds[pres_key].ndim == 2:
|
|
624
|
+
pressure_array = ds[pres_key].values
|
|
625
|
+
else:
|
|
626
|
+
pressure_array = np.tile(ds[pres_key].values, (ntime, 1))
|
|
627
|
+
|
|
628
|
+
for ti in range(ntime):
|
|
629
|
+
t_prof = ds[temp_key].isel({time_dim: ti}).values
|
|
630
|
+
s_prof = ds[salt_key].isel({time_dim: ti}).values
|
|
631
|
+
p_prof = pressure_array[ti, :]
|
|
632
|
+
|
|
633
|
+
mask = ~np.isnan(t_prof) & ~np.isnan(p_prof)
|
|
634
|
+
|
|
635
|
+
if np.sum(mask) < 2:
|
|
636
|
+
continue # Not enough data to interpolate
|
|
637
|
+
|
|
638
|
+
t_good = t_prof[mask]
|
|
639
|
+
s_good = s_prof[mask]
|
|
640
|
+
p_good = p_prof[mask]
|
|
641
|
+
|
|
642
|
+
# --- Select climatology for the month ---
|
|
643
|
+
month_i = pd.to_datetime(ds[time_key].isel({time_dim: ti}).values).month
|
|
644
|
+
|
|
645
|
+
cTg_month = clim_ds["dTdp"].sel(month=month_i).values
|
|
646
|
+
cSg_month = clim_ds["dSdp"].sel(month=month_i).values
|
|
647
|
+
|
|
648
|
+
dtdp_func = interp1d(
|
|
649
|
+
TEMP,
|
|
650
|
+
cTg_month,
|
|
651
|
+
bounds_error=False,
|
|
652
|
+
fill_value=(cTg_month[0], cTg_month[-1]),
|
|
653
|
+
)
|
|
654
|
+
dsdp_func = interp1d(
|
|
655
|
+
TEMP,
|
|
656
|
+
cSg_month,
|
|
657
|
+
bounds_error=False,
|
|
658
|
+
fill_value=(cSg_month[0], cSg_month[-1]),
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
segments = []
|
|
662
|
+
|
|
663
|
+
# --- Extrapolate upward if needed
|
|
664
|
+
if extrapolate and p_good[0] > p_grid.min():
|
|
665
|
+
upT, upS, upP = extrapolate_boundary(
|
|
666
|
+
T=t_good[0],
|
|
667
|
+
S=s_good[0],
|
|
668
|
+
P=p_good[0],
|
|
669
|
+
p_bound=p_grid.min(),
|
|
670
|
+
dtdp_func=dtdp_func,
|
|
671
|
+
dsdp_func=dsdp_func,
|
|
672
|
+
int_step=int_step,
|
|
673
|
+
)
|
|
674
|
+
mask_up = upP < p_good[0]
|
|
675
|
+
segments.append((upT[mask_up], upS[mask_up], upP[mask_up]))
|
|
676
|
+
|
|
677
|
+
# --- Interpolate internal points
|
|
678
|
+
innerT, innerS, innerP = interpolate_internal(
|
|
679
|
+
t_good,
|
|
680
|
+
s_good,
|
|
681
|
+
p_good,
|
|
682
|
+
dtdp_func=dtdp_func,
|
|
683
|
+
dsdp_func=dsdp_func,
|
|
684
|
+
int_step=int_step,
|
|
685
|
+
)
|
|
686
|
+
segments.append((innerT, innerS, innerP))
|
|
687
|
+
|
|
688
|
+
# --- Extrapolate downward if needed
|
|
689
|
+
if extrapolate and p_good[-1] < p_grid.max():
|
|
690
|
+
lowT, lowS, lowP = extrapolate_boundary(
|
|
691
|
+
T=t_good[-1],
|
|
692
|
+
S=s_good[-1],
|
|
693
|
+
P=p_good[-1],
|
|
694
|
+
p_bound=p_grid.max(),
|
|
695
|
+
dtdp_func=dtdp_func,
|
|
696
|
+
dsdp_func=dsdp_func,
|
|
697
|
+
int_step=int_step,
|
|
698
|
+
)
|
|
699
|
+
mask_low = lowP > p_good[-1]
|
|
700
|
+
segments.append((lowT[mask_low], lowS[mask_low], lowP[mask_low]))
|
|
701
|
+
|
|
702
|
+
# --- Stitch segments together
|
|
703
|
+
full_T = np.concatenate([seg[0] for seg in segments])
|
|
704
|
+
full_S = np.concatenate([seg[1] for seg in segments])
|
|
705
|
+
full_P = np.concatenate([seg[2] for seg in segments])
|
|
706
|
+
|
|
707
|
+
# Sort by pressure
|
|
708
|
+
sort_idx = np.argsort(full_P)
|
|
709
|
+
full_T = full_T[sort_idx]
|
|
710
|
+
full_S = full_S[sort_idx]
|
|
711
|
+
full_P = full_P[sort_idx]
|
|
712
|
+
|
|
713
|
+
# Interpolate onto regular p_grid
|
|
714
|
+
t_interp = np.interp(p_grid, full_P, full_T, left=np.nan, right=np.nan)
|
|
715
|
+
s_interp = np.interp(p_grid, full_P, full_S, left=np.nan, right=np.nan)
|
|
716
|
+
|
|
717
|
+
t_con[ti, :] = t_interp
|
|
718
|
+
s_con[ti, :] = s_interp
|
|
719
|
+
|
|
720
|
+
sigma_con = gsw.sigma0(s_con, t_con)
|
|
721
|
+
|
|
722
|
+
out_ds = xr.Dataset(
|
|
723
|
+
{
|
|
724
|
+
temp_key: ((time_dim, pres_dim), t_con),
|
|
725
|
+
salt_key: ((time_dim, pres_dim), s_con),
|
|
726
|
+
"SIGMA0": ((time_dim, pres_dim), sigma_con),
|
|
727
|
+
},
|
|
728
|
+
coords={
|
|
729
|
+
pres_key: (pres_dim, p_grid),
|
|
730
|
+
time_key: (time_dim, ds[time_key].values),
|
|
731
|
+
"LATITUDE": (time_dim, ds["LATITUDE"].values),
|
|
732
|
+
"LONGITUDE": (time_dim, ds["LONGITUDE"].values),
|
|
733
|
+
},
|
|
734
|
+
attrs={
|
|
735
|
+
"method": "vertical interpolation using climatological gradients with seasonal adjustment"
|
|
736
|
+
},
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
return out_ds
|