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/plotter.py
ADDED
|
@@ -0,0 +1,942 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import matplotlib.colors as mcolors
|
|
4
|
+
import matplotlib.dates as mdates
|
|
5
|
+
import matplotlib.pyplot as plt
|
|
6
|
+
import numpy as np
|
|
7
|
+
import xarray as xr
|
|
8
|
+
from matplotlib.colors import LinearSegmentedColormap
|
|
9
|
+
from pandas import DataFrame
|
|
10
|
+
|
|
11
|
+
from .utilities import _nice_colorbar_bounds
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def plot_qartod_summary(ds, var="TEMP", qc_var="QC_ROLLUP"):
|
|
15
|
+
"""Plot QARTOD rollup flags and flagged data points for a given variable.
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
ds : xarray.Dataset
|
|
20
|
+
Dataset containing the variable and the QC flag.
|
|
21
|
+
var : str, optional
|
|
22
|
+
Name of the variable to plot (default is "TEMP").
|
|
23
|
+
qc_var : str, optional
|
|
24
|
+
Name of the QC rollup flag variable (default is "QC_ROLLUP").
|
|
25
|
+
|
|
26
|
+
"""
|
|
27
|
+
style_path = Path(__file__).parent.parent / "oceanarray" / "oceanarray.mplstyle"
|
|
28
|
+
plt.style.use(str(style_path))
|
|
29
|
+
flag = ds[qc_var]
|
|
30
|
+
time = ds["TIME"]
|
|
31
|
+
data = ds[var]
|
|
32
|
+
|
|
33
|
+
fig, axs = plt.subplots(nrows=2, ncols=1, figsize=(12, 6), sharex=True)
|
|
34
|
+
|
|
35
|
+
# Subplot 1: QC flag over time
|
|
36
|
+
axs[0].plot(time, flag, drawstyle="steps-post", color="black")
|
|
37
|
+
axs[0].set_yticks([1, 2, 3, 4])
|
|
38
|
+
axs[0].set_yticklabels(["Good", "Not Eval", "Suspect", "Fail"])
|
|
39
|
+
axs[0].set_title("QARTOD Rollup Flag Over Time")
|
|
40
|
+
axs[0].grid(True)
|
|
41
|
+
|
|
42
|
+
# Subplot 2: Variable with flags
|
|
43
|
+
axs[1].plot(time, data, label=var, color="gray", linewidth=0.8)
|
|
44
|
+
axs[1].scatter(
|
|
45
|
+
time.where(flag == 1), data.where(flag == 1), color="green", s=10, label="Good"
|
|
46
|
+
)
|
|
47
|
+
axs[1].scatter(
|
|
48
|
+
time.where(flag == 3),
|
|
49
|
+
data.where(flag == 3),
|
|
50
|
+
color="orange",
|
|
51
|
+
s=15,
|
|
52
|
+
label="Suspect",
|
|
53
|
+
)
|
|
54
|
+
axs[1].scatter(
|
|
55
|
+
time.where(flag == 4), data.where(flag == 4), color="red", s=15, label="Fail"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
axs[1].legend()
|
|
59
|
+
axs[1].set_title(f"{var} with QARTOD Flags")
|
|
60
|
+
axs[1].set_xlabel("Time")
|
|
61
|
+
axs[1].set_ylabel(var)
|
|
62
|
+
axs[1].grid(True)
|
|
63
|
+
|
|
64
|
+
plt.tight_layout()
|
|
65
|
+
plt.show()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def plot_climatology(
|
|
69
|
+
clim_ds: xr.Dataset,
|
|
70
|
+
var: str = "dTdp",
|
|
71
|
+
clim_ds_smoothed: xr.Dataset | None = None,
|
|
72
|
+
fig=None,
|
|
73
|
+
ax=None,
|
|
74
|
+
):
|
|
75
|
+
"""Plot seasonal climatology of dT/dP or dS/dP, optionally with smoothed version overlaid.
|
|
76
|
+
|
|
77
|
+
Parameters
|
|
78
|
+
----------
|
|
79
|
+
clim_ds : xr.Dataset
|
|
80
|
+
Raw climatology dataset with 'dTdp' and/or 'dSdp'.
|
|
81
|
+
var : str, optional
|
|
82
|
+
Variable to plot ('dTdp' or 'dSdp'), by default 'dTdp'.
|
|
83
|
+
clim_ds_smoothed : xr.Dataset, optional
|
|
84
|
+
Smoothed climatology dataset to overlay, by default None.
|
|
85
|
+
fig : matplotlib.figure.Figure, optional
|
|
86
|
+
Existing figure to plot on. If None, a new figure is created.
|
|
87
|
+
ax : matplotlib.axes.Axes, optional
|
|
88
|
+
Existing axes to plot on. If None, new axes are created.
|
|
89
|
+
|
|
90
|
+
Notes
|
|
91
|
+
-----
|
|
92
|
+
If smoothed climatology is provided, the raw climatology is shown in grey.
|
|
93
|
+
Otherwise, only the provided climatology is shown in color.
|
|
94
|
+
|
|
95
|
+
"""
|
|
96
|
+
style_path = Path(__file__).parent.parent / "oceanarray" / "oceanarray.mplstyle"
|
|
97
|
+
plt.style.use(str(style_path))
|
|
98
|
+
if var not in clim_ds:
|
|
99
|
+
raise ValueError(f"{var} not found in climatology dataset.") # noqa: TRY003
|
|
100
|
+
|
|
101
|
+
if fig is None or ax is None:
|
|
102
|
+
fig, ax = plt.subplots(figsize=(10, 6))
|
|
103
|
+
|
|
104
|
+
months = np.arange(1, 13)
|
|
105
|
+
# Create a colormap for the seasonal cycle
|
|
106
|
+
|
|
107
|
+
# Define colors for the seasonal cycle (cold to warm to cold)
|
|
108
|
+
colors = [
|
|
109
|
+
(0, 0, 1), # Blue (January, cold)
|
|
110
|
+
(0, 0.5, 1), # Light blue (Spring)
|
|
111
|
+
(0, 1, 0), # Green (Early summer)
|
|
112
|
+
(1, 1, 0), # Yellow (Mid-summer, warm)
|
|
113
|
+
(1, 0.5, 0), # Orange (Late summer)
|
|
114
|
+
(1, 0, 0), # Red (July, peak warmth)
|
|
115
|
+
(0.5, 0, 0.5), # Purple (Autumn)
|
|
116
|
+
(0, 0, 1), # Blue (December, cold again)
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
# Create a colormap with 12 discrete colors for each month
|
|
120
|
+
seasonal_colormap = LinearSegmentedColormap.from_list(
|
|
121
|
+
"seasonal_cycle", colors, N=12
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
for month in months:
|
|
125
|
+
TEMP = clim_ds["TEMP"]
|
|
126
|
+
|
|
127
|
+
if clim_ds_smoothed is not None:
|
|
128
|
+
# Plot raw (grey)
|
|
129
|
+
ax.plot(
|
|
130
|
+
TEMP,
|
|
131
|
+
clim_ds[var].sel(month=month),
|
|
132
|
+
color="grey",
|
|
133
|
+
alpha=0.4,
|
|
134
|
+
linewidth=1,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Plot smoothed (color)
|
|
138
|
+
ax.plot(
|
|
139
|
+
TEMP,
|
|
140
|
+
clim_ds_smoothed[var].sel(month=month),
|
|
141
|
+
linewidth=1.5,
|
|
142
|
+
label=f"Month {month}",
|
|
143
|
+
color=seasonal_colormap(month - 1),
|
|
144
|
+
)
|
|
145
|
+
else:
|
|
146
|
+
# Only plot raw (color)
|
|
147
|
+
ax.plot(
|
|
148
|
+
TEMP,
|
|
149
|
+
clim_ds[var].sel(month=month),
|
|
150
|
+
label=f"Month {month}",
|
|
151
|
+
color=seasonal_colormap(month - 1),
|
|
152
|
+
linewidth=1.5,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
ax.set_xlabel("Temperature (°C)")
|
|
156
|
+
ax.set_ylabel(f"{var} (per dbar)")
|
|
157
|
+
ax.set_title(f"Monthly {var} Climatology")
|
|
158
|
+
ax.legend(ncol=3, fontsize=8)
|
|
159
|
+
ax.grid(True)
|
|
160
|
+
plt.tight_layout()
|
|
161
|
+
|
|
162
|
+
return fig, ax
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def scatter_profile_vs_PRES(ds_interp, ds_12h, var="CT", downsample: int = 10):
|
|
166
|
+
"""Scatter plot of *var* vs pressure for two datasets.
|
|
167
|
+
|
|
168
|
+
Parameters
|
|
169
|
+
----------
|
|
170
|
+
ds_interp : xarray.Dataset
|
|
171
|
+
Dataset with a PRES coordinate and a TIME dimension.
|
|
172
|
+
ds_12h : xarray.Dataset
|
|
173
|
+
Second dataset (e.g. 12-hourly gridded) with 1-D PRES and TIME.
|
|
174
|
+
var : str
|
|
175
|
+
Variable to plot on the x-axis.
|
|
176
|
+
downsample : int
|
|
177
|
+
Keep every Nth point before plotting (default 10). With O(100k) points
|
|
178
|
+
the plot is saturated at any marker size; downsampling is needed to see
|
|
179
|
+
individual dots. Set to 1 to plot all points.
|
|
180
|
+
|
|
181
|
+
"""
|
|
182
|
+
from . import parameters as P
|
|
183
|
+
|
|
184
|
+
plt.style.use(str(P.MPLSTYLE))
|
|
185
|
+
t1 = ds_interp[var].values.flatten()[::downsample]
|
|
186
|
+
# For ds_interp, PRES is a 1D array (DEPTH), so tile it for each TIME
|
|
187
|
+
p1 = np.tile(ds_interp["PRES"].values, ds_interp["TIME"].size)[::downsample]
|
|
188
|
+
|
|
189
|
+
t2 = ds_12h[var].values.flatten()[::downsample]
|
|
190
|
+
p2 = ds_12h["PRES"].values.flatten()[::downsample]
|
|
191
|
+
|
|
192
|
+
print(f"Plotting {t1.size:,} points for ds_interp (1-in-{downsample})")
|
|
193
|
+
print(f"Plotting {t2.size:,} points for ds_12h (1-in-{downsample})")
|
|
194
|
+
|
|
195
|
+
plt.figure(figsize=(8, 5))
|
|
196
|
+
plt.scatter(t1, p1, s=20, alpha=0.3, label="ds_interp")
|
|
197
|
+
plt.scatter(t2, p2, s=20, alpha=0.3, color="red", label="ds_12h")
|
|
198
|
+
plt.gca().invert_yaxis()
|
|
199
|
+
plt.xlabel(f"{var} ({ds_interp[var].attrs.get('units', '')})")
|
|
200
|
+
plt.ylabel("Pressure (dbar)")
|
|
201
|
+
plt.title(f"Scatter plot of {var} vs PRES")
|
|
202
|
+
plt.legend()
|
|
203
|
+
plt.tight_layout()
|
|
204
|
+
plt.show()
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def pcolor_timeseries_by_depth(ds_interp, var="SA"):
|
|
208
|
+
style_path = Path(__file__).parent.parent / "oceanarray" / "oceanarray.mplstyle"
|
|
209
|
+
plt.style.use(str(style_path))
|
|
210
|
+
plt.figure(figsize=(12, 6))
|
|
211
|
+
pc = plt.pcolormesh(
|
|
212
|
+
ds_interp["TIME"], ds_interp["PRES"], ds_interp[var].T, shading="auto"
|
|
213
|
+
)
|
|
214
|
+
plt.gca().invert_yaxis()
|
|
215
|
+
# Get variable attributes for labeling
|
|
216
|
+
var_attrs = ds_interp[var].attrs
|
|
217
|
+
long_name = var_attrs.get("long_name", var)
|
|
218
|
+
units = var_attrs.get("units", "")
|
|
219
|
+
plt.colorbar(pc, label=f"{long_name} ({units})" if units else long_name)
|
|
220
|
+
plt.xlabel("Time")
|
|
221
|
+
plt.ylabel(
|
|
222
|
+
ds_interp["PRES"].attrs.get("long_name", "Pressure")
|
|
223
|
+
+ f" ({ds_interp['PRES'].attrs.get('units', 'dbar')})"
|
|
224
|
+
)
|
|
225
|
+
plt.title(f"{long_name} from ds_interp")
|
|
226
|
+
plt.tight_layout()
|
|
227
|
+
plt.show()
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def plot_timeseries_by_depth(ds, var="TEMP"):
|
|
231
|
+
"""Plot individual time series for each depth level.
|
|
232
|
+
|
|
233
|
+
Parameters
|
|
234
|
+
----------
|
|
235
|
+
ds : xarray.Dataset
|
|
236
|
+
Dataset containing the variable to plot.
|
|
237
|
+
var : str
|
|
238
|
+
Variable name (default is "TEMP").
|
|
239
|
+
|
|
240
|
+
"""
|
|
241
|
+
style_path = Path(__file__).parent.parent / "oceanarray" / "oceanarray.mplstyle"
|
|
242
|
+
plt.style.use(str(style_path))
|
|
243
|
+
ds = ds.sortby("DEPTH")
|
|
244
|
+
da = ds[var].squeeze() # remove singleton lat/lon if present
|
|
245
|
+
time = ds["TIME"].values
|
|
246
|
+
depths = ds["DEPTH"].values
|
|
247
|
+
|
|
248
|
+
plt.figure(figsize=(12, 6))
|
|
249
|
+
|
|
250
|
+
for i, depth in enumerate(depths):
|
|
251
|
+
# da[:, i] assumes dimensions are (TIME, DEPTH)
|
|
252
|
+
series = da.isel(DEPTH=i)
|
|
253
|
+
plt.plot(time, series, label=f"{depth:.1f} m")
|
|
254
|
+
|
|
255
|
+
plt.xlabel("Time")
|
|
256
|
+
plt.ylabel(f"{var} [{da.attrs.get('units', 'unknown')}]")
|
|
257
|
+
plt.title(f"{var} time series by depth")
|
|
258
|
+
plt.legend(title="Depth", bbox_to_anchor=(1.05, 1), loc="upper left")
|
|
259
|
+
plt.tight_layout()
|
|
260
|
+
plt.show()
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
_DEFAULT_NN = np.timedelta64(12, "h")
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def plot_trim_windows(ds, dstart, dend, NN=_DEFAULT_NN):
|
|
267
|
+
"""Plot start and end windows for variables T, C, P in the dataset,
|
|
268
|
+
highlighting data before/after dstart/dend.
|
|
269
|
+
|
|
270
|
+
Parameters
|
|
271
|
+
----------
|
|
272
|
+
ds : xarray.Dataset
|
|
273
|
+
Dataset containing variables 'T', 'C', 'P' and 'TIME'.
|
|
274
|
+
dstart : np.datetime64
|
|
275
|
+
Deployment start time.
|
|
276
|
+
dend : np.datetime64
|
|
277
|
+
Deployment end time.
|
|
278
|
+
NN : np.timedelta64, optional
|
|
279
|
+
Window size (default: 12 hours).
|
|
280
|
+
|
|
281
|
+
"""
|
|
282
|
+
style_path = Path(__file__).parent.parent / "oceanarray" / "oceanarray.mplstyle"
|
|
283
|
+
plt.style.use(str(style_path))
|
|
284
|
+
fig, axes = plt.subplots(3, 2, figsize=(9, 8), sharex="col")
|
|
285
|
+
variables = ["T", "C", "P"]
|
|
286
|
+
|
|
287
|
+
for i, var in enumerate(variables):
|
|
288
|
+
data = ds[var].values
|
|
289
|
+
time = ds["TIME"].values
|
|
290
|
+
|
|
291
|
+
valid_idx = np.where(~np.isnan(data))[0]
|
|
292
|
+
first_idx = valid_idx[0]
|
|
293
|
+
last_idx = valid_idx[-1]
|
|
294
|
+
|
|
295
|
+
# Left column: start of record
|
|
296
|
+
start_window_end = dstart + NN
|
|
297
|
+
left_mask = (time >= time[first_idx]) & (time <= start_window_end)
|
|
298
|
+
axes[i, 0].plot(
|
|
299
|
+
time[left_mask],
|
|
300
|
+
data[left_mask],
|
|
301
|
+
marker="o",
|
|
302
|
+
linestyle="-",
|
|
303
|
+
markerfacecolor="none",
|
|
304
|
+
label=f"{var} start",
|
|
305
|
+
markersize=5,
|
|
306
|
+
)
|
|
307
|
+
after_dstart_mask = left_mask & (time >= dstart)
|
|
308
|
+
axes[i, 0].plot(
|
|
309
|
+
time[after_dstart_mask],
|
|
310
|
+
data[after_dstart_mask],
|
|
311
|
+
marker="o",
|
|
312
|
+
color="red",
|
|
313
|
+
label=f"{var} after dstart",
|
|
314
|
+
markersize=5,
|
|
315
|
+
)
|
|
316
|
+
axes[i, 0].axvline(dstart, color="k", linestyle="--", label="dstart")
|
|
317
|
+
axes[i, 0].set_ylabel(var)
|
|
318
|
+
if i == 0:
|
|
319
|
+
axes[i, 0].set_title("Start of Record")
|
|
320
|
+
axes[i, 0].legend()
|
|
321
|
+
|
|
322
|
+
# Right column: end of record
|
|
323
|
+
end_window_start = dend - NN
|
|
324
|
+
right_mask = (time >= end_window_start) & (time <= time[last_idx])
|
|
325
|
+
axes[i, 1].plot(
|
|
326
|
+
time[right_mask],
|
|
327
|
+
data[right_mask],
|
|
328
|
+
marker="o",
|
|
329
|
+
linestyle="-",
|
|
330
|
+
markerfacecolor="none",
|
|
331
|
+
label=f"{var} end",
|
|
332
|
+
markersize=5,
|
|
333
|
+
)
|
|
334
|
+
before_dend_mask = right_mask & (time <= dend)
|
|
335
|
+
axes[i, 1].plot(
|
|
336
|
+
time[before_dend_mask],
|
|
337
|
+
data[before_dend_mask],
|
|
338
|
+
marker="o",
|
|
339
|
+
color="red",
|
|
340
|
+
label=f"{var} before dend",
|
|
341
|
+
markersize=5,
|
|
342
|
+
)
|
|
343
|
+
axes[i, 1].axvline(dend, color="k", linestyle="--", label="dend")
|
|
344
|
+
if i == 0:
|
|
345
|
+
axes[i, 1].set_title("End of Record")
|
|
346
|
+
axes[i, 1].legend()
|
|
347
|
+
for ax in axes[i, :]:
|
|
348
|
+
plt.setp(ax.get_xticklabels(), rotation=30, ha="right")
|
|
349
|
+
|
|
350
|
+
plt.tight_layout()
|
|
351
|
+
return fig, axes
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def plot_microcat_raw(ds, save_path=None):
|
|
355
|
+
"""Plot temperature, conductivity, and pressure from a raw/stage2 microcat NetCDF.
|
|
356
|
+
|
|
357
|
+
Works with seasenselib variable names (temperature, conductivity, pressure).
|
|
358
|
+
"""
|
|
359
|
+
style_path = Path(__file__).parent / "oceanarray.mplstyle"
|
|
360
|
+
plt.style.use(str(style_path))
|
|
361
|
+
|
|
362
|
+
panels = [("temperature", "Temperature [°C]", "tab:red")]
|
|
363
|
+
if "conductivity" in ds.data_vars:
|
|
364
|
+
panels.append(("conductivity", "Conductivity [mS/cm]", "tab:blue"))
|
|
365
|
+
if "pressure" in ds.data_vars:
|
|
366
|
+
panels.append(("pressure", "Pressure [dbar]", "tab:green"))
|
|
367
|
+
|
|
368
|
+
nrows = len(panels)
|
|
369
|
+
fig, axs = plt.subplots(nrows, 1, figsize=(12, 3 * nrows), sharex=True)
|
|
370
|
+
if nrows == 1:
|
|
371
|
+
axs = [axs]
|
|
372
|
+
|
|
373
|
+
for ax, (var, label, color) in zip(axs, panels):
|
|
374
|
+
ax.plot(ds["time"], ds[var], color=color, linewidth=0.5)
|
|
375
|
+
ax.set_ylabel(label)
|
|
376
|
+
axs[-1].set_xlabel("Time")
|
|
377
|
+
|
|
378
|
+
serial = (
|
|
379
|
+
ds["serial_number"].item()
|
|
380
|
+
if "serial_number" in ds
|
|
381
|
+
else ds.attrs.get("serial_number", "?")
|
|
382
|
+
)
|
|
383
|
+
depth = ds["InstrDepth"].item() if "InstrDepth" in ds else "?"
|
|
384
|
+
axs[0].set_title(f"MicroCAT s/n: {serial} | Target depth: {depth} m")
|
|
385
|
+
|
|
386
|
+
locator = mdates.AutoDateLocator()
|
|
387
|
+
formatter = mdates.ConciseDateFormatter(locator)
|
|
388
|
+
axs[-1].xaxis.set_major_locator(locator)
|
|
389
|
+
axs[-1].xaxis.set_major_formatter(formatter)
|
|
390
|
+
plt.tight_layout()
|
|
391
|
+
|
|
392
|
+
if save_path:
|
|
393
|
+
fig.savefig(save_path, dpi=150, bbox_inches="tight")
|
|
394
|
+
|
|
395
|
+
return fig
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def plot_aquadopp_raw(ds, save_path=None):
|
|
399
|
+
"""Plot east velocity, north velocity, and pressure from a raw/stage2 Aquadopp NetCDF."""
|
|
400
|
+
style_path = Path(__file__).parent / "oceanarray.mplstyle"
|
|
401
|
+
plt.style.use(str(style_path))
|
|
402
|
+
|
|
403
|
+
# Pick pressure variable — prefer 'pressure', fall back to 'pressure_1'
|
|
404
|
+
pvar = next((v for v in ("pressure", "pressure_1") if v in ds.data_vars), None)
|
|
405
|
+
|
|
406
|
+
panels = [
|
|
407
|
+
("east_velocity", "East velocity [m/s]", "tab:blue", False),
|
|
408
|
+
("north_velocity", "North velocity [m/s]", "tab:orange", False),
|
|
409
|
+
]
|
|
410
|
+
if pvar:
|
|
411
|
+
panels.append((pvar, "Pressure [dbar]", "tab:green", True))
|
|
412
|
+
for tvar, tlabel in (("Pitch", "Pitch [°]"), ("Roll", "Roll [°]")):
|
|
413
|
+
if tvar in ds.data_vars:
|
|
414
|
+
panels.append((tvar, tlabel, "tab:purple", False))
|
|
415
|
+
|
|
416
|
+
nrows = len(panels)
|
|
417
|
+
fig, axs = plt.subplots(nrows, 1, figsize=(12, 3 * nrows), sharex=True)
|
|
418
|
+
if nrows == 1:
|
|
419
|
+
axs = [axs]
|
|
420
|
+
|
|
421
|
+
for ax, (var, label, color, invert) in zip(axs, panels):
|
|
422
|
+
ax.plot(ds["time"], ds[var], color=color, linewidth=0.5)
|
|
423
|
+
if var in ("east_velocity", "north_velocity"):
|
|
424
|
+
ax.axhline(0, color="k", linewidth=0.5, linestyle="--")
|
|
425
|
+
ax.set_ylabel(label)
|
|
426
|
+
if invert:
|
|
427
|
+
vmin = float(ds[var].min())
|
|
428
|
+
vmax = float(ds[var].max())
|
|
429
|
+
pad = max((vmax - vmin) * 0.1, 0.5)
|
|
430
|
+
ax.set_ylim(vmax + pad, vmin - pad)
|
|
431
|
+
axs[-1].set_xlabel("Time")
|
|
432
|
+
|
|
433
|
+
serial = (
|
|
434
|
+
ds["serial_number"].item()
|
|
435
|
+
if "serial_number" in ds
|
|
436
|
+
else ds.attrs.get("serial_number", "?")
|
|
437
|
+
)
|
|
438
|
+
depth = f"{ds['InstrDepth'].item():.0f} m" if "InstrDepth" in ds else "?"
|
|
439
|
+
axs[0].set_title(f"Aquadopp s/n: {serial} | Target depth: {depth}")
|
|
440
|
+
|
|
441
|
+
locator = mdates.AutoDateLocator()
|
|
442
|
+
axs[-1].xaxis.set_major_locator(locator)
|
|
443
|
+
axs[-1].xaxis.set_major_formatter(mdates.ConciseDateFormatter(locator))
|
|
444
|
+
plt.tight_layout()
|
|
445
|
+
|
|
446
|
+
if save_path:
|
|
447
|
+
fig.savefig(save_path, dpi=150, bbox_inches="tight")
|
|
448
|
+
|
|
449
|
+
return fig
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def plot_microcat(ds):
|
|
453
|
+
style_path = Path(__file__).parent.parent / "oceanarray" / "oceanarray.mplstyle"
|
|
454
|
+
plt.style.use(str(style_path))
|
|
455
|
+
|
|
456
|
+
fig, axs = plt.subplots(3, 1, figsize=(9, 8), sharex=True)
|
|
457
|
+
|
|
458
|
+
# Top panel: Temperature
|
|
459
|
+
axs[0].plot(ds["TIME"], ds["T"], color="tab:red")
|
|
460
|
+
axs[0].set_ylabel("Temperature [deg C]")
|
|
461
|
+
|
|
462
|
+
# Middle panel: Conductivity
|
|
463
|
+
axs[1].plot(ds["TIME"], ds["C"], color="tab:blue")
|
|
464
|
+
axs[1].set_ylabel("Conductivity [mS/cm]")
|
|
465
|
+
|
|
466
|
+
# Bottom panel: Pressure
|
|
467
|
+
axs[2].plot(ds["TIME"], ds["P"], color="tab:green")
|
|
468
|
+
axs[2].set_ylabel("Pressure [dbar]")
|
|
469
|
+
axs[2].set_xlabel("Time")
|
|
470
|
+
|
|
471
|
+
# Title
|
|
472
|
+
serial_number = ds.attrs.get("serial_number", "Unknown")
|
|
473
|
+
instr_depth = ds["InstrDepth"].item() if "InstrDepth" in ds else "Unknown"
|
|
474
|
+
axs[0].set_title(f"MicroCAT s/n: {serial_number}; Target Depth: {instr_depth}")
|
|
475
|
+
|
|
476
|
+
axs[2].xaxis.set_major_formatter(mdates.DateFormatter("%Y.%b"))
|
|
477
|
+
plt.setp(axs[2].get_xticklabels(), rotation=30, ha="right")
|
|
478
|
+
|
|
479
|
+
plt.tight_layout(rect=[0, 0, 1, 0.96])
|
|
480
|
+
return fig
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def show_variables(data):
|
|
484
|
+
"""Processes an xarray Dataset or a netCDF file, extracts variable information,
|
|
485
|
+
and returns a styled DataFrame with details about the variables.
|
|
486
|
+
|
|
487
|
+
Parameters
|
|
488
|
+
----------
|
|
489
|
+
data (str or xr.Dataset): The input data, either a file path to a netCDF file or an xarray Dataset.
|
|
490
|
+
|
|
491
|
+
Returns
|
|
492
|
+
-------
|
|
493
|
+
pandas.io.formats.style.Styler: A styled DataFrame containing the following columns:
|
|
494
|
+
- dims: The dimension of the variable (or "string" if it is a string type).
|
|
495
|
+
- name: The name of the variable.
|
|
496
|
+
- units: The units of the variable (if available).
|
|
497
|
+
- comment: Any additional comments about the variable (if available).
|
|
498
|
+
|
|
499
|
+
"""
|
|
500
|
+
if isinstance(data, str):
|
|
501
|
+
print("information is based on file: {}".format(data))
|
|
502
|
+
dataset = xr.Dataset(data)
|
|
503
|
+
variables = dataset.variables
|
|
504
|
+
elif isinstance(data, xr.Dataset):
|
|
505
|
+
print("information is based on xarray Dataset")
|
|
506
|
+
variables = data.variables
|
|
507
|
+
else:
|
|
508
|
+
raise TypeError("Input data must be a file path (str) or an xarray Dataset") # noqa: TRY003
|
|
509
|
+
|
|
510
|
+
info = {}
|
|
511
|
+
for i, key in enumerate(variables):
|
|
512
|
+
var = variables[key]
|
|
513
|
+
if isinstance(data, str):
|
|
514
|
+
dims = var.dimensions[0] if len(var.dimensions) == 1 else "string"
|
|
515
|
+
units = "" if not hasattr(var, "units") else var.units
|
|
516
|
+
comment = "" if not hasattr(var, "comment") else var.comment
|
|
517
|
+
else:
|
|
518
|
+
dims = var.dims[0] if len(var.dims) == 1 else "string"
|
|
519
|
+
units = var.attrs.get("units", "")
|
|
520
|
+
comment = var.attrs.get("comment", "")
|
|
521
|
+
|
|
522
|
+
info[i] = {
|
|
523
|
+
"name": key,
|
|
524
|
+
"dims": dims,
|
|
525
|
+
"units": units,
|
|
526
|
+
"comment": comment,
|
|
527
|
+
"standard_name": var.attrs.get("standard_name", ""),
|
|
528
|
+
"dtype": str(var.dtype) if isinstance(data, str) else str(var.data.dtype),
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
vars = DataFrame(info).T
|
|
532
|
+
|
|
533
|
+
dim = vars.dims
|
|
534
|
+
dim[dim.str.startswith("str")] = "string"
|
|
535
|
+
vars["dims"] = dim
|
|
536
|
+
|
|
537
|
+
vars = (
|
|
538
|
+
vars.sort_values(["dims", "name"])
|
|
539
|
+
.reset_index(drop=True)
|
|
540
|
+
.loc[:, ["dims", "name", "units", "comment", "standard_name", "dtype"]]
|
|
541
|
+
.set_index("name")
|
|
542
|
+
.style
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
return vars
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def show_attributes(data):
|
|
549
|
+
"""Processes an xarray Dataset or a netCDF file, extracts attribute information,
|
|
550
|
+
and returns a DataFrame with details about the attributes.
|
|
551
|
+
|
|
552
|
+
Parameters
|
|
553
|
+
----------
|
|
554
|
+
data (str or xr.Dataset): The input data, either a file path to a netCDF file or an xarray Dataset.
|
|
555
|
+
|
|
556
|
+
Returns
|
|
557
|
+
-------
|
|
558
|
+
pandas.DataFrame: A DataFrame containing the following columns:
|
|
559
|
+
- Attribute: The name of the attribute.
|
|
560
|
+
- Value: The value of the attribute.
|
|
561
|
+
|
|
562
|
+
"""
|
|
563
|
+
from netCDF4 import Dataset
|
|
564
|
+
|
|
565
|
+
if isinstance(data, str):
|
|
566
|
+
print("information is based on file: {}".format(data))
|
|
567
|
+
rootgrp = Dataset(data, "r", format="NETCDF4")
|
|
568
|
+
attributes = rootgrp.ncattrs()
|
|
569
|
+
|
|
570
|
+
def get_attr(key):
|
|
571
|
+
return getattr(rootgrp, key)
|
|
572
|
+
|
|
573
|
+
elif isinstance(data, xr.Dataset):
|
|
574
|
+
print("information is based on xarray Dataset")
|
|
575
|
+
attributes = data.attrs.keys()
|
|
576
|
+
|
|
577
|
+
def get_attr(key): # type: ignore[no-redef]
|
|
578
|
+
return data.attrs[key]
|
|
579
|
+
|
|
580
|
+
else:
|
|
581
|
+
raise TypeError("Input data must be a file path (str) or an xarray Dataset") # noqa: TRY003
|
|
582
|
+
|
|
583
|
+
info = {}
|
|
584
|
+
for i, key in enumerate(attributes):
|
|
585
|
+
dtype = type(get_attr(key)).__name__
|
|
586
|
+
info[i] = {"Attribute": key, "Value": get_attr(key), "DType": dtype}
|
|
587
|
+
|
|
588
|
+
attrs = DataFrame(info).T
|
|
589
|
+
|
|
590
|
+
return attrs
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
# ---------------------------------------------------------------------------
|
|
594
|
+
# Multi-instrument mooring overview plot
|
|
595
|
+
# ---------------------------------------------------------------------------
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def _instrument_label(nc_path: Path, ds: xr.Dataset) -> str:
|
|
599
|
+
"""Build a short instrument label, e.g. 'MC2942 868m'."""
|
|
600
|
+
from . import parameters as P
|
|
601
|
+
|
|
602
|
+
instrument_dir = nc_path.parent.name
|
|
603
|
+
prefix = P.INSTRUMENT_ABBREV.get(instrument_dir, instrument_dir[:2].upper())
|
|
604
|
+
|
|
605
|
+
serial = ds["serial_number"].item() if "serial_number" in ds else "?"
|
|
606
|
+
# Strip trailing non-alphanumeric chars (e.g. * markers in serial strings)
|
|
607
|
+
import re
|
|
608
|
+
|
|
609
|
+
serial = re.sub(r"[^\w]", "", str(serial))
|
|
610
|
+
|
|
611
|
+
depth = f"{ds['InstrDepth'].item():.0f}m" if "InstrDepth" in ds else ""
|
|
612
|
+
return f"{prefix}{serial} {depth}".strip()
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def plot_mooring_timeseries(
|
|
616
|
+
proc_dir: Path,
|
|
617
|
+
mooring: str,
|
|
618
|
+
var_y: str = "temperature",
|
|
619
|
+
var_color: str = None,
|
|
620
|
+
colormap: str = None,
|
|
621
|
+
downsample_seconds: int = None,
|
|
622
|
+
markersize: float = None,
|
|
623
|
+
save_path: Path = None,
|
|
624
|
+
show: bool = True,
|
|
625
|
+
) -> plt.Figure:
|
|
626
|
+
"""Plot all instruments in a mooring on shared axes.
|
|
627
|
+
|
|
628
|
+
Two modes depending on whether *var_color* is supplied:
|
|
629
|
+
|
|
630
|
+
**Scatter mode** (``var_color`` given)
|
|
631
|
+
x = time, y = ``var_y`` (typically ``pressure``), colour = ``var_color``
|
|
632
|
+
(typically ``temperature``). Instruments without both variables are
|
|
633
|
+
skipped. The y-axis is inverted so depth increases downward.
|
|
634
|
+
|
|
635
|
+
**Line mode** (``var_color`` is None)
|
|
636
|
+
x = time, y = ``var_y``. Each instrument that has the variable is
|
|
637
|
+
plotted as a separate line and included in the legend.
|
|
638
|
+
|
|
639
|
+
Parameters
|
|
640
|
+
----------
|
|
641
|
+
proc_dir : Path
|
|
642
|
+
Path to the mooring's proc directory (e.g. ``<basedir>/proc``).
|
|
643
|
+
mooring : str
|
|
644
|
+
Mooring name, e.g. ``dsG3_1_2026``.
|
|
645
|
+
var_y : str
|
|
646
|
+
Variable to place on the y-axis (default: ``temperature``).
|
|
647
|
+
var_color : str, optional
|
|
648
|
+
Variable to use for scatter-plot colour. If None, a line plot is made.
|
|
649
|
+
colormap : str, optional
|
|
650
|
+
Matplotlib colormap name. Defaults to ``parameters.DEFAULT_COLORMAP``.
|
|
651
|
+
downsample_seconds : int, optional
|
|
652
|
+
Resample interval in seconds. Defaults to ``parameters.DOWNSAMPLE_SECONDS``.
|
|
653
|
+
save_path : Path, optional
|
|
654
|
+
If given, save the figure here instead of displaying it.
|
|
655
|
+
|
|
656
|
+
Returns
|
|
657
|
+
-------
|
|
658
|
+
matplotlib.figure.Figure
|
|
659
|
+
|
|
660
|
+
"""
|
|
661
|
+
from . import parameters as P
|
|
662
|
+
|
|
663
|
+
plt.style.use(str(P.MPLSTYLE))
|
|
664
|
+
cmap_name = colormap or P.DEFAULT_COLORMAP
|
|
665
|
+
dt_s = downsample_seconds or P.DOWNSAMPLE_SECONDS
|
|
666
|
+
|
|
667
|
+
mooring_proc = Path(proc_dir) / mooring
|
|
668
|
+
use_files = sorted(mooring_proc.rglob("*_stage2.nc"))
|
|
669
|
+
|
|
670
|
+
if not use_files:
|
|
671
|
+
raise FileNotFoundError(f"No _stage2.nc files found under {mooring_proc}") # noqa: TRY003
|
|
672
|
+
|
|
673
|
+
scatter_mode = var_color is not None
|
|
674
|
+
|
|
675
|
+
# ------------------------------------------------------------------
|
|
676
|
+
# Load and resample all instruments that have the required variables
|
|
677
|
+
# ------------------------------------------------------------------
|
|
678
|
+
segments = [] # list of (label, time_array, y_array, color_array_or_None)
|
|
679
|
+
|
|
680
|
+
for nc in use_files:
|
|
681
|
+
try:
|
|
682
|
+
ds = xr.open_dataset(nc, decode_timedelta=False)
|
|
683
|
+
except Exception: # noqa: BLE001 — skip unreadable NC; one bad file must not abort plot
|
|
684
|
+
continue
|
|
685
|
+
|
|
686
|
+
has_y = var_y in ds.data_vars
|
|
687
|
+
has_c = (not scatter_mode) or (var_color in ds.data_vars)
|
|
688
|
+
if not (has_y and has_c):
|
|
689
|
+
ds.close()
|
|
690
|
+
continue
|
|
691
|
+
|
|
692
|
+
try:
|
|
693
|
+
keep = [
|
|
694
|
+
v
|
|
695
|
+
for v in ([var_y] + ([var_color] if scatter_mode else []))
|
|
696
|
+
if v in ds.data_vars
|
|
697
|
+
]
|
|
698
|
+
if len(ds["time"]) > 1:
|
|
699
|
+
median_dt = float(
|
|
700
|
+
np.median(
|
|
701
|
+
np.diff(ds["time"].values)
|
|
702
|
+
.astype("timedelta64[s]")
|
|
703
|
+
.astype(float)
|
|
704
|
+
)
|
|
705
|
+
)
|
|
706
|
+
step = max(1, round(dt_s / median_dt))
|
|
707
|
+
else:
|
|
708
|
+
step = 1
|
|
709
|
+
ds_small = ds[keep].isel(time=slice(None, None, step)).load()
|
|
710
|
+
except Exception: # noqa: BLE001 — skip instruments that fail to subsample
|
|
711
|
+
ds.close()
|
|
712
|
+
continue
|
|
713
|
+
|
|
714
|
+
label = _instrument_label(nc, ds)
|
|
715
|
+
ds.close()
|
|
716
|
+
|
|
717
|
+
t = ds_small["time"].values
|
|
718
|
+
y = ds_small[var_y].values
|
|
719
|
+
c = ds_small[var_color].values if scatter_mode else None
|
|
720
|
+
segments.append((label, t, y, c))
|
|
721
|
+
|
|
722
|
+
if not segments:
|
|
723
|
+
raise ValueError(
|
|
724
|
+
f"No instruments have variable '{var_y}'"
|
|
725
|
+
+ (f" and '{var_color}'" if scatter_mode else "")
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
# ------------------------------------------------------------------
|
|
729
|
+
# Compute colour limits from combined 5th–95th percentile
|
|
730
|
+
# ------------------------------------------------------------------
|
|
731
|
+
if scatter_mode:
|
|
732
|
+
all_c = np.concatenate([c for _, _, _, c in segments if c is not None])
|
|
733
|
+
all_c = all_c[np.isfinite(all_c)]
|
|
734
|
+
vmin = float(np.percentile(all_c, P.COLORBAR_PLOW))
|
|
735
|
+
vmax = float(np.percentile(all_c, P.COLORBAR_PHIGH))
|
|
736
|
+
|
|
737
|
+
# ------------------------------------------------------------------
|
|
738
|
+
# Draw
|
|
739
|
+
# ------------------------------------------------------------------
|
|
740
|
+
fig, ax = plt.subplots(figsize=P.FIGURE_SIZE_WIDE)
|
|
741
|
+
|
|
742
|
+
if scatter_mode:
|
|
743
|
+
cmap = plt.get_cmap(cmap_name)
|
|
744
|
+
sc = None
|
|
745
|
+
for label, t, y, c in segments:
|
|
746
|
+
sc = ax.scatter(
|
|
747
|
+
t,
|
|
748
|
+
y,
|
|
749
|
+
c=c,
|
|
750
|
+
cmap=cmap,
|
|
751
|
+
vmin=vmin,
|
|
752
|
+
vmax=vmax,
|
|
753
|
+
s=markersize if markersize is not None else 4,
|
|
754
|
+
alpha=0.6,
|
|
755
|
+
rasterized=True,
|
|
756
|
+
label=label,
|
|
757
|
+
)
|
|
758
|
+
if sc is not None:
|
|
759
|
+
cbar = fig.colorbar(sc, ax=ax, pad=0.02)
|
|
760
|
+
cbar.set_label(var_color.replace("_", " ").title())
|
|
761
|
+
ax.invert_yaxis()
|
|
762
|
+
ax.set_ylabel(var_y.replace("_", " ").title())
|
|
763
|
+
else:
|
|
764
|
+
for label, t, y, _ in segments:
|
|
765
|
+
ax.plot(t, y, linewidth=0.8, label=label)
|
|
766
|
+
ax.set_ylabel(var_y.replace("_", " ").title())
|
|
767
|
+
ax.legend(fontsize=9, loc="best", ncol=2)
|
|
768
|
+
|
|
769
|
+
locator = mdates.AutoDateLocator()
|
|
770
|
+
ax.xaxis.set_major_locator(locator)
|
|
771
|
+
ax.xaxis.set_major_formatter(mdates.ConciseDateFormatter(locator))
|
|
772
|
+
ax.set_xlabel("Time")
|
|
773
|
+
|
|
774
|
+
title_parts = [mooring, f"dt={dt_s}s"]
|
|
775
|
+
if scatter_mode:
|
|
776
|
+
title_parts.append(
|
|
777
|
+
f"colour: {var_color} [{P.COLORBAR_PLOW}–{P.COLORBAR_PHIGH}th pct]"
|
|
778
|
+
)
|
|
779
|
+
ax.set_title(" | ".join(title_parts))
|
|
780
|
+
|
|
781
|
+
plt.tight_layout()
|
|
782
|
+
|
|
783
|
+
if save_path:
|
|
784
|
+
fig.savefig(save_path, bbox_inches="tight")
|
|
785
|
+
if show:
|
|
786
|
+
plt.show()
|
|
787
|
+
if not show or save_path:
|
|
788
|
+
plt.close(fig)
|
|
789
|
+
|
|
790
|
+
return fig
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
def plot_grid(
|
|
794
|
+
grid_path: Path,
|
|
795
|
+
save_dir: Path = None,
|
|
796
|
+
show: bool = False,
|
|
797
|
+
) -> list:
|
|
798
|
+
"""Plot temperature and salinity from a mooring grid file as pcolormesh.
|
|
799
|
+
|
|
800
|
+
Salinity is computed from conductivity and temperature using GSW
|
|
801
|
+
(``gsw.SP_from_C``) if a ``salinity`` variable is not already present.
|
|
802
|
+
|
|
803
|
+
Parameters
|
|
804
|
+
----------
|
|
805
|
+
grid_path : Path
|
|
806
|
+
Path to a ``*_grid.nc`` file produced by ``oceanarray grid``.
|
|
807
|
+
save_dir : Path, optional
|
|
808
|
+
Directory in which to save the figure. Defaults to the directory
|
|
809
|
+
containing *grid_path*.
|
|
810
|
+
show : bool
|
|
811
|
+
If True, call ``plt.show()`` interactively.
|
|
812
|
+
|
|
813
|
+
Returns
|
|
814
|
+
-------
|
|
815
|
+
list of Path
|
|
816
|
+
Paths of the saved PNG files (one per variable plotted).
|
|
817
|
+
|
|
818
|
+
"""
|
|
819
|
+
from . import parameters as P
|
|
820
|
+
|
|
821
|
+
grid_path = Path(grid_path)
|
|
822
|
+
save_dir = Path(save_dir) if save_dir else grid_path.parent
|
|
823
|
+
stem = grid_path.stem # e.g. "dsG3_1_2026_grid"
|
|
824
|
+
|
|
825
|
+
plt.style.use(str(P.MPLSTYLE))
|
|
826
|
+
|
|
827
|
+
ds = xr.open_dataset(grid_path).load()
|
|
828
|
+
time = ds["time"].values
|
|
829
|
+
pressure = ds["pressure"].values
|
|
830
|
+
|
|
831
|
+
def _panel(fig, ax, da, title, units, cmap, style="pcolormesh"):
|
|
832
|
+
"""Draw one T/S panel; style is 'pcolormesh' or 'contourf'."""
|
|
833
|
+
data = da.transpose("pressure", "time").values
|
|
834
|
+
vmin = float(np.nanpercentile(data, P.COLORBAR_PLOW))
|
|
835
|
+
vmax = float(np.nanpercentile(data, P.COLORBAR_PHIGH))
|
|
836
|
+
bounds = _nice_colorbar_bounds(vmin, vmax, n=20)
|
|
837
|
+
norm = mcolors.BoundaryNorm(bounds, ncolors=256)
|
|
838
|
+
if style == "contourf":
|
|
839
|
+
pc = ax.contourf(
|
|
840
|
+
time, pressure, data, levels=bounds, cmap=cmap, extend="both"
|
|
841
|
+
)
|
|
842
|
+
else:
|
|
843
|
+
pc = ax.pcolormesh(
|
|
844
|
+
time, pressure, data, shading="nearest", cmap=cmap, norm=norm
|
|
845
|
+
)
|
|
846
|
+
cb = fig.colorbar(pc, ax=ax, pad=0.02)
|
|
847
|
+
cb.set_label(f"{title} ({units})" if units else title)
|
|
848
|
+
ax.invert_yaxis()
|
|
849
|
+
ax.set_ylabel("Pressure (dbar)")
|
|
850
|
+
locator = mdates.AutoDateLocator()
|
|
851
|
+
ax.xaxis.set_major_locator(locator)
|
|
852
|
+
ax.xaxis.set_major_formatter(mdates.ConciseDateFormatter(locator))
|
|
853
|
+
ax.set_title(f"{title} [{style}]")
|
|
854
|
+
|
|
855
|
+
# ------------------------------------------------------------------ #
|
|
856
|
+
# Derive salinity DataArray if not stored #
|
|
857
|
+
# ------------------------------------------------------------------ #
|
|
858
|
+
sal_da = sal_label = sal_units = None
|
|
859
|
+
if "salinity" in ds:
|
|
860
|
+
sal_da = ds["salinity"]
|
|
861
|
+
sal_label = sal_da.attrs.get("long_name", "Salinity")
|
|
862
|
+
sal_units = sal_da.attrs.get("units", "1")
|
|
863
|
+
elif "conductivity" in ds and "temperature" in ds:
|
|
864
|
+
import gsw
|
|
865
|
+
|
|
866
|
+
T_tp = ds["temperature"].transpose("time", "pressure").values
|
|
867
|
+
C_tp = ds["conductivity"].transpose("time", "pressure").values
|
|
868
|
+
SP_vals = gsw.SP_from_C(C_tp, T_tp, pressure[np.newaxis, :])
|
|
869
|
+
sal_da = xr.DataArray(
|
|
870
|
+
SP_vals,
|
|
871
|
+
dims=("time", "pressure"),
|
|
872
|
+
coords={"time": ds["time"], "pressure": ds["pressure"]},
|
|
873
|
+
)
|
|
874
|
+
sal_label = "Practical Salinity"
|
|
875
|
+
sal_units = "1"
|
|
876
|
+
|
|
877
|
+
saved: list = []
|
|
878
|
+
|
|
879
|
+
# ------------------------------------------------------------------ #
|
|
880
|
+
# Temperature #
|
|
881
|
+
# ------------------------------------------------------------------ #
|
|
882
|
+
if "temperature" in ds:
|
|
883
|
+
T_units = ds["temperature"].attrs.get("units", "degC")
|
|
884
|
+
for style in ("pcolormesh", "contourf"):
|
|
885
|
+
fig, ax = plt.subplots(figsize=(12, 4))
|
|
886
|
+
_panel(
|
|
887
|
+
fig, ax, ds["temperature"], "Temperature", T_units, "RdYlBu_r", style
|
|
888
|
+
)
|
|
889
|
+
ax.set_xlabel("Time")
|
|
890
|
+
fig.suptitle(stem, fontsize=10, y=1.01)
|
|
891
|
+
plt.tight_layout()
|
|
892
|
+
out = save_dir / f"{stem}_temperature_{style}.png"
|
|
893
|
+
fig.savefig(out, bbox_inches="tight", dpi=150)
|
|
894
|
+
saved.append(out)
|
|
895
|
+
if show:
|
|
896
|
+
plt.show()
|
|
897
|
+
plt.close(fig)
|
|
898
|
+
|
|
899
|
+
# ------------------------------------------------------------------ #
|
|
900
|
+
# Salinity #
|
|
901
|
+
# ------------------------------------------------------------------ #
|
|
902
|
+
if sal_da is not None:
|
|
903
|
+
for style in ("pcolormesh", "contourf"):
|
|
904
|
+
fig, ax = plt.subplots(figsize=(12, 4))
|
|
905
|
+
_panel(fig, ax, sal_da, sal_label, sal_units, "YlGnBu_r", style)
|
|
906
|
+
ax.set_xlabel("Time")
|
|
907
|
+
fig.suptitle(stem, fontsize=10, y=1.01)
|
|
908
|
+
plt.tight_layout()
|
|
909
|
+
out = save_dir / f"{stem}_salinity_{style}.png"
|
|
910
|
+
fig.savefig(out, bbox_inches="tight", dpi=150)
|
|
911
|
+
saved.append(out)
|
|
912
|
+
if show:
|
|
913
|
+
plt.show()
|
|
914
|
+
plt.close(fig)
|
|
915
|
+
|
|
916
|
+
# ------------------------------------------------------------------ #
|
|
917
|
+
# Potential density (sigma0, sigma2, …) #
|
|
918
|
+
# ------------------------------------------------------------------ #
|
|
919
|
+
sigma_vars = [
|
|
920
|
+
v
|
|
921
|
+
for v in ds.data_vars
|
|
922
|
+
if v.startswith("sigma") and ds[v].dims == ("time", "pressure")
|
|
923
|
+
]
|
|
924
|
+
for sv in sigma_vars:
|
|
925
|
+
da = ds[sv]
|
|
926
|
+
label = da.attrs.get("long_name", sv)
|
|
927
|
+
units = da.attrs.get("units", "kg m-3")
|
|
928
|
+
for style in ("pcolormesh", "contourf"):
|
|
929
|
+
fig, ax = plt.subplots(figsize=(12, 4))
|
|
930
|
+
_panel(fig, ax, da, label, units, P.DENSITY_COLORMAP, style)
|
|
931
|
+
ax.set_xlabel("Time")
|
|
932
|
+
fig.suptitle(stem, fontsize=10, y=1.01)
|
|
933
|
+
plt.tight_layout()
|
|
934
|
+
out = save_dir / f"{stem}_{sv}_{style}.png"
|
|
935
|
+
fig.savefig(out, bbox_inches="tight", dpi=150)
|
|
936
|
+
saved.append(out)
|
|
937
|
+
if show:
|
|
938
|
+
plt.show()
|
|
939
|
+
plt.close(fig)
|
|
940
|
+
|
|
941
|
+
ds.close()
|
|
942
|
+
return saved
|