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.
Files changed (76) hide show
  1. oceanarray/__init__.py +20 -0
  2. oceanarray/_version.py +1 -0
  3. oceanarray/cli.py +1343 -0
  4. oceanarray/clock_offset.py +675 -0
  5. oceanarray/config/OS1_sensor_attrs.yaml +49 -0
  6. oceanarray/config/OS1_var_names.yaml +5 -0
  7. oceanarray/config/OS1_vocab_attrs.yaml +84 -0
  8. oceanarray/config/legacy/project_RAPID.yaml +69 -0
  9. oceanarray/config/legacy/rodb_keys.txt +206 -0
  10. oceanarray/config/legacy/rodb_keys.yaml +1012 -0
  11. oceanarray/config/logging.yaml +28 -0
  12. oceanarray/legacy/__init__.py +47 -0
  13. oceanarray/legacy/caldip.py +753 -0
  14. oceanarray/legacy/convertOS.py +527 -0
  15. oceanarray/legacy/find_deployment.py +489 -0
  16. oceanarray/legacy/mooring_rodb.py +429 -0
  17. oceanarray/legacy/process_rodb.py +369 -0
  18. oceanarray/legacy/rodb.py +330 -0
  19. oceanarray/logger.py +192 -0
  20. oceanarray/logsheet/__init__.py +60 -0
  21. oceanarray/logsheet/_columns.py +97 -0
  22. oceanarray/logsheet/_config.py +245 -0
  23. oceanarray/logsheet/_firmware.py +43 -0
  24. oceanarray/logsheet/_latex.py +128 -0
  25. oceanarray/logsheet/_render.py +126 -0
  26. oceanarray/logsheet/builders/__init__.py +13 -0
  27. oceanarray/logsheet/builders/caldip_download.py +195 -0
  28. oceanarray/logsheet/builders/caldip_setup.py +188 -0
  29. oceanarray/logsheet/builders/deployment_setup.py +218 -0
  30. oceanarray/logsheet/builders/mooring_download.py +243 -0
  31. oceanarray/logsheet/builders/recovery.py +157 -0
  32. oceanarray/logsheet/data/column_defs.yaml +686 -0
  33. oceanarray/logsheet/data/latex_templates/_base_instrument.tex.j2 +42 -0
  34. oceanarray/logsheet/data/latex_templates/caldip_download.tex.j2 +47 -0
  35. oceanarray/logsheet/data/latex_templates/caldip_setup.tex.j2 +70 -0
  36. oceanarray/logsheet/data/latex_templates/deployment_setup.tex.j2 +97 -0
  37. oceanarray/logsheet/data/latex_templates/mooring_download.tex.j2 +54 -0
  38. oceanarray/logsheet/data/latex_templates/recovery.tex.j2 +103 -0
  39. oceanarray/mooring_level.py +1368 -0
  40. oceanarray/oceanarray.mplstyle +18 -0
  41. oceanarray/parameters.py +270 -0
  42. oceanarray/plotter.py +942 -0
  43. oceanarray/plotters/__init__.py +97 -0
  44. oceanarray/plotters/_animation.py +261 -0
  45. oceanarray/plotters/_current.py +705 -0
  46. oceanarray/plotters/_diagnostic.py +667 -0
  47. oceanarray/plotters/_helpers.py +57 -0
  48. oceanarray/plotters/_primitives.py +101 -0
  49. oceanarray/plotters/_section.py +16 -0
  50. oceanarray/plotters/_timeseries.py +11 -0
  51. oceanarray/rapid_interp.py +739 -0
  52. oceanarray/read_rbr_hex.py +541 -0
  53. oceanarray/readers.py +303 -0
  54. oceanarray/report/__init__.py +5 -0
  55. oceanarray/report/_array.py +386 -0
  56. oceanarray/report/_grid.py +539 -0
  57. oceanarray/report/_html_helpers.py +1004 -0
  58. oceanarray/report/_instrument.py +853 -0
  59. oceanarray/report/_mooring.py +1385 -0
  60. oceanarray/report/_plots.py +4284 -0
  61. oceanarray/report/_stack.py +941 -0
  62. oceanarray/stage1.py +1959 -0
  63. oceanarray/stage2.py +1160 -0
  64. oceanarray/stage3.py +2494 -0
  65. oceanarray/time_gridding.py +958 -0
  66. oceanarray/tools.py +400 -0
  67. oceanarray/transports.py +1 -0
  68. oceanarray/utilities.py +538 -0
  69. oceanarray/validation.py +389 -0
  70. oceanarray/writers.py +99 -0
  71. oceanarray-0.1.0.dist-info/METADATA +503 -0
  72. oceanarray-0.1.0.dist-info/RECORD +76 -0
  73. oceanarray-0.1.0.dist-info/WHEEL +5 -0
  74. oceanarray-0.1.0.dist-info/entry_points.txt +2 -0
  75. oceanarray-0.1.0.dist-info/licenses/LICENSE +21 -0
  76. oceanarray-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,97 @@
1
+ """Oceanarray plotting package.
2
+
3
+ Three-tier architecture
4
+ -----------------------
5
+ Tier 1 _primitives.py Data-agnostic plot primitives (array-in / Figure-out).
6
+ Tier 2 _current.py etc. Domain wrappers (xr.Dataset-in / Figure-out).
7
+ Tier 3 report/_plots.py Thin report wrappers (path-in / base64-out).
8
+
9
+ Separate modules (not part of the Tier pipeline)
10
+ -------------------------------------------------
11
+ _animation.py Animated GIF output via matplotlib.animation + pillow.
12
+ Intentionally isolated so plotly/_interactive.py can be
13
+ added alongside it later without coupling to the static stack.
14
+
15
+ Public API
16
+ ----------
17
+ Available now (pre-OdB):
18
+
19
+ plot_temperature_trajectory Lagrangian trajectory coloured by temperature
20
+ plot_speed_boxplot Current speed boxplot with percentile statistics
21
+ plot_hodograph Two-panel hodograph coloured by time
22
+ animate_hodograph Animated GIF hodograph (requires pillow)
23
+
24
+ Legacy functions (from plotter.py — available via backward-compat shim):
25
+
26
+ plot_qartod_summary, plot_climatology, scatter_profile_vs_PRES,
27
+ pcolor_timeseries_by_depth, plot_timeseries_by_depth, plot_trim_windows,
28
+ plot_microcat_raw, plot_aquadopp_raw, plot_microcat,
29
+ show_variables, show_attributes, plot_mooring_timeseries, plot_grid
30
+
31
+ As each legacy function is migrated into the appropriate sub-module, remove
32
+ it from plotter.py and from the shim below. See the migration checklist at
33
+ .claude/plotters_update-20260718.md.
34
+
35
+ Future
36
+ ------
37
+ Post-OdB: fill in _timeseries.py, _section.py, _diagnostic.py, _helpers.py;
38
+ implement remaining Tier-1 primitives; delete plotter.py once empty.
39
+ May eventually be split into a separate oceanvis package.
40
+ """
41
+
42
+ # ---------------------------------------------------------------------------
43
+ # New functions (pre-OdB)
44
+ # ---------------------------------------------------------------------------
45
+ from oceanarray.plotters._current import ( # noqa: F401
46
+ plot_temperature_trajectory,
47
+ plot_speed_boxplot,
48
+ plot_multi_aquadopp_trajectories,
49
+ plot_aquadopp_speed_profile,
50
+ plot_hodograph,
51
+ )
52
+ from oceanarray.plotters._animation import animate_hodograph # noqa: F401
53
+
54
+ # ---------------------------------------------------------------------------
55
+ # Backward-compat shim — re-exports from legacy plotter.py
56
+ # Remove each entry here once the function has been migrated to a sub-module
57
+ # and the corresponding function deleted from plotter.py.
58
+ # ---------------------------------------------------------------------------
59
+ from oceanarray.plotter import ( # noqa: F401
60
+ plot_qartod_summary,
61
+ plot_climatology,
62
+ scatter_profile_vs_PRES,
63
+ pcolor_timeseries_by_depth,
64
+ plot_timeseries_by_depth,
65
+ plot_trim_windows,
66
+ plot_microcat_raw,
67
+ plot_aquadopp_raw,
68
+ plot_microcat,
69
+ show_variables,
70
+ show_attributes,
71
+ plot_mooring_timeseries,
72
+ plot_grid,
73
+ )
74
+
75
+ __all__ = [
76
+ # New (pre-OdB)
77
+ "plot_temperature_trajectory",
78
+ "plot_speed_boxplot",
79
+ "plot_multi_aquadopp_trajectories",
80
+ "plot_aquadopp_speed_profile",
81
+ "plot_hodograph",
82
+ "animate_hodograph",
83
+ # Legacy shim
84
+ "plot_qartod_summary",
85
+ "plot_climatology",
86
+ "scatter_profile_vs_PRES",
87
+ "pcolor_timeseries_by_depth",
88
+ "plot_timeseries_by_depth",
89
+ "plot_trim_windows",
90
+ "plot_microcat_raw",
91
+ "plot_aquadopp_raw",
92
+ "plot_microcat",
93
+ "show_variables",
94
+ "show_attributes",
95
+ "plot_mooring_timeseries",
96
+ "plot_grid",
97
+ ]
@@ -0,0 +1,261 @@
1
+ """Animated plot functions for the oceanarray plotters package.
2
+
3
+ This module is intentionally separate from the static (Tier-1/2/3) pipeline.
4
+ It relies on ``matplotlib.animation`` and requires the ``pillow`` package as a
5
+ GIF writer. ``scipy`` is required for the Tukey smoothing window.
6
+
7
+ Future interactive equivalents (plotly, bokeh) should live in a companion
8
+ ``_interactive.py`` module so the animation and interactive layers stay
9
+ clearly distinct.
10
+
11
+ Tier classification: Tier-2 domain functions (xr.Dataset-in / file-out).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from pathlib import Path
17
+ from typing import Optional, Union
18
+
19
+ import matplotlib.colors as mcolors
20
+ import matplotlib.pyplot as plt
21
+ import numpy as np
22
+ import xarray as xr
23
+
24
+ from oceanarray.plotters._helpers import tukey_smooth
25
+ from oceanarray.utilities import _nice_colorbar_bounds
26
+
27
+
28
+ def animate_hodograph(
29
+ ds: xr.Dataset,
30
+ output_path: Union[str, Path],
31
+ u_var: str = "east_velocity",
32
+ v_var: str = "north_velocity",
33
+ lp_days: float = 4.0,
34
+ smooth_hours: float = 3.0,
35
+ frame_hours: float = 6.0,
36
+ fps: int = 20,
37
+ dpi: int = 100,
38
+ ) -> Optional[Path]:
39
+ """Write an animated GIF of the smoothed hodograph drawing itself through time.
40
+
41
+ Each frame advances by *frame_hours* of real deployment time and reveals
42
+ the smoothed velocity trajectory accumulated up to that moment, so the
43
+ viewer watches rotary/eddy structure build up through the record.
44
+
45
+ Signal processing (applied once to the full record before animating):
46
+
47
+ * **Raw panel** — *smooth_hours*-Tukey filter of the raw east/north signal.
48
+ * **Eddy panel** — *lp_days*-day rolling-mean removed, then *smooth_hours*-
49
+ Tukey filter to suppress instrument noise.
50
+
51
+ A timestamp header on each frame shows the real date and time.
52
+
53
+ Requires ``pillow`` (``pip install pillow``) as the GIF writer and
54
+ ``scipy`` for the Tukey window.
55
+
56
+ Parameters
57
+ ----------
58
+ ds : xr.Dataset
59
+ Per-instrument dataset with east/north velocity variables and a
60
+ ``time`` dimension.
61
+ output_path : str or Path
62
+ Destination file path; ``.gif`` extension recommended.
63
+ u_var : str
64
+ Eastward velocity variable name (m s⁻¹).
65
+ v_var : str
66
+ Northward velocity variable name (m s⁻¹).
67
+ lp_days : float
68
+ Low-pass window length in days for the eddy-component panel.
69
+ smooth_hours : float
70
+ Tukey smoothing window in hours applied to both panels.
71
+ frame_hours : float
72
+ Time step between frames in hours (default 6 h → one frame per
73
+ quarter-day of deployment).
74
+ fps : int
75
+ Frames per second in the output GIF.
76
+ dpi : int
77
+ Resolution of each frame in dots per inch.
78
+
79
+ Returns
80
+ -------
81
+ Path
82
+ The resolved output path on success.
83
+ None
84
+ If *u_var* or *v_var* are absent from *ds*, or if the ``pillow``
85
+ writer is unavailable.
86
+
87
+ """
88
+ import pandas as pd
89
+ from matplotlib.animation import FuncAnimation
90
+
91
+ output_path = Path(output_path)
92
+
93
+ if u_var not in ds.data_vars or v_var not in ds.data_vars:
94
+ return None
95
+
96
+ instr_id = ds.attrs.get("id", "")
97
+ east_raw = ds[u_var].values.astype(float).ravel()
98
+ north_raw = ds[v_var].values.astype(float).ravel()
99
+ t = ds["time"].values
100
+ units = ds[u_var].attrs.get("units", "m s⁻¹")
101
+
102
+ # Sample interval (seconds)
103
+ dt_s = (
104
+ float(np.median(np.diff(t) / np.timedelta64(1, "s"))) if len(t) > 1 else 3600.0
105
+ )
106
+
107
+ # Tukey smoothing window length (samples)
108
+ smooth_n = max(3, int(round(smooth_hours * 3600.0 / dt_s)))
109
+
110
+ # LP window for eddy removal (rolling mean)
111
+ lp_n = max(3, int(round(lp_days * 86400.0 / dt_s)))
112
+
113
+ # Raw panel: Tukey-smoothed raw signal
114
+ e_raw_sm = tukey_smooth(east_raw, smooth_n)
115
+ n_raw_sm = tukey_smooth(north_raw, smooth_n)
116
+
117
+ # Eddy panel: LP mean removed, then Tukey smoothed
118
+ e_lp = (
119
+ pd.Series(east_raw)
120
+ .rolling(window=lp_n, min_periods=1, center=True)
121
+ .mean()
122
+ .values
123
+ )
124
+ n_lp = (
125
+ pd.Series(north_raw)
126
+ .rolling(window=lp_n, min_periods=1, center=True)
127
+ .mean()
128
+ .values
129
+ )
130
+ e_eddy_sm = tukey_smooth(east_raw - e_lp, smooth_n)
131
+ n_eddy_sm = tukey_smooth(north_raw - n_lp, smooth_n)
132
+
133
+ # Frame indices — one frame per frame_hours of deployment time
134
+ step_n = max(1, int(round(frame_hours * 3600.0 / dt_s)))
135
+ N = len(east_raw)
136
+ frame_ends = np.arange(step_n, N + step_n, step_n, dtype=int)
137
+ frame_ends = frame_ends[frame_ends <= N]
138
+ if len(frame_ends) == 0 or frame_ends[-1] < N:
139
+ frame_ends = np.append(frame_ends, N)
140
+ n_frames = len(frame_ends)
141
+
142
+ # Time colour: permanent fraction 0→1 per sample
143
+ t_frac = np.linspace(0.0, 1.0, N)
144
+ bounds = _nice_colorbar_bounds(0.0, 1.0, n=10)
145
+ norm = mcolors.BoundaryNorm(bounds, ncolors=256)
146
+ cmap = plt.get_cmap("viridis")
147
+
148
+ # Fixed symmetric axis limits from full smoothed data
149
+ def _sym_lim(e: np.ndarray, n: np.ndarray) -> float:
150
+ vals = np.concatenate([e[np.isfinite(e)], n[np.isfinite(n)]])
151
+ return float(np.nanmax(np.abs(vals))) * 1.08 if vals.size > 0 else 1.0
152
+
153
+ raw_lim = _sym_lim(e_raw_sm, n_raw_sm)
154
+ eddy_lim = _sym_lim(e_eddy_sm, n_eddy_sm)
155
+
156
+ # ------------------------------------------------------------------ Figure
157
+ fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
158
+
159
+ # Fix the layout explicitly so the colorbar axes position is stable across
160
+ # ax.cla() calls (tight_layout after colorbar creation causes misalignment).
161
+ fig.subplots_adjust(left=0.07, right=0.86, top=0.88, bottom=0.11, wspace=0.35)
162
+
163
+ # Colorbar on a fixed axes to the right of ax2 — position in figure coords
164
+ cbar_ax = fig.add_axes([0.88, 0.13, 0.02, 0.70])
165
+ sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm)
166
+ sm.set_array([])
167
+ cb = fig.colorbar(
168
+ sm, cax=cbar_ax, label="Time (0 = start, 1 = end of record)", ticks=bounds
169
+ )
170
+ cb.ax.tick_params(labelsize=8)
171
+
172
+ # Static instrument label + dynamic timestamp in the headroom above panels
173
+ if instr_id:
174
+ fig.text(0.45, 0.99, instr_id, ha="center", va="top", fontsize=9, color="#555")
175
+ timestamp_text = fig.text(
176
+ 0.45, 0.94, "", ha="center", va="top", fontsize=11, fontweight="bold"
177
+ )
178
+
179
+ def _setup_ax(ax: plt.Axes, lim: float, title: str) -> None:
180
+ ax.set_xlim(-lim, lim)
181
+ ax.set_ylim(-lim, lim)
182
+ ax.set_aspect("equal")
183
+ ax.axhline(0, color="#bbb", lw=0.7, zorder=0)
184
+ ax.axvline(0, color="#bbb", lw=0.7, zorder=0)
185
+ ax.set_xlabel(f"East ({units})")
186
+ ax.set_ylabel(f"North ({units})")
187
+ ax.set_title(title, fontsize=10)
188
+ ax.grid(True, linestyle="--", linewidth=0.4, alpha=0.4)
189
+
190
+ def _draw_frame(frame_idx: int) -> None:
191
+ k = int(frame_ends[frame_idx])
192
+
193
+ # Timestamp from the last sample in this frame
194
+ time_str = pd.Timestamp(t[k - 1]).strftime("%Y-%m-%d %H:%M UTC")
195
+ timestamp_text.set_text(time_str)
196
+
197
+ panels = [
198
+ (ax1, e_raw_sm, n_raw_sm, raw_lim, f"Raw ({smooth_hours:.0f}-h smoothed)"),
199
+ (
200
+ ax2,
201
+ e_eddy_sm,
202
+ n_eddy_sm,
203
+ eddy_lim,
204
+ f"Eddy ({lp_days:.0f}-day LP removed, {smooth_hours:.0f}-h smoothed)",
205
+ ),
206
+ ]
207
+ for ax, e, n, lim, title in panels:
208
+ ax.cla()
209
+ _setup_ax(ax, lim, title)
210
+
211
+ mask = np.isfinite(e[:k]) & np.isfinite(n[:k])
212
+ if not mask.any():
213
+ continue
214
+
215
+ ax.scatter(
216
+ e[:k][mask],
217
+ n[:k][mask],
218
+ c=t_frac[:k][mask],
219
+ cmap=cmap,
220
+ norm=norm,
221
+ s=4,
222
+ alpha=0.8,
223
+ linewidths=0,
224
+ marker="o",
225
+ rasterized=True,
226
+ )
227
+ # Permanent start marker; moving end marker
228
+ idx = np.where(mask)[0]
229
+ ax.scatter(
230
+ e[:k][idx[0]],
231
+ n[:k][idx[0]],
232
+ s=55,
233
+ c="lime",
234
+ zorder=5,
235
+ marker="o",
236
+ edgecolors="black",
237
+ linewidths=0.6,
238
+ )
239
+ ax.scatter(
240
+ e[:k][idx[-1]],
241
+ n[:k][idx[-1]],
242
+ s=65,
243
+ c="red",
244
+ zorder=5,
245
+ marker="s",
246
+ edgecolors="black",
247
+ linewidths=0.6,
248
+ )
249
+
250
+ anim = FuncAnimation(
251
+ fig, _draw_frame, frames=n_frames, interval=1000 // fps, blit=False
252
+ )
253
+
254
+ try:
255
+ anim.save(str(output_path), writer="pillow", fps=fps, dpi=dpi)
256
+ except Exception: # noqa: BLE001 — broad catch at I/O boundary; caller checks return value
257
+ plt.close(fig)
258
+ return None
259
+
260
+ plt.close(fig)
261
+ return output_path