oceanarray 0.3.0__py3-none-any.whl → 0.4.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 (102) hide show
  1. oceanarray/__init__.py +13 -0
  2. oceanarray/_version.py +1 -1
  3. oceanarray/analysis/clock.py +11 -12
  4. oceanarray/analysis/hydrographic.py +7 -5
  5. oceanarray/analysis/science.py +15 -15
  6. oceanarray/analysis/spectral.py +11 -11
  7. oceanarray/analysis/temporal.py +2 -2
  8. oceanarray/cli.py +350 -451
  9. oceanarray/config/parameters.py +152 -22
  10. oceanarray/config/report.mplstyle +26 -0
  11. oceanarray/config/report_tokens.py +242 -0
  12. oceanarray/config/validation.py +30 -30
  13. oceanarray/inspect.py +7 -7
  14. oceanarray/logger.py +2 -2
  15. oceanarray/paths.py +83 -3
  16. oceanarray/plotters/__init__.py +50 -56
  17. oceanarray/plotters/_cli_legacy.py +5 -5
  18. oceanarray/plotters/animation.py +3 -4
  19. oceanarray/plotters/current.py +284 -174
  20. oceanarray/plotters/diagnostic.py +305 -159
  21. oceanarray/plotters/helpers.py +174 -30
  22. oceanarray/plotters/hydrography.py +54 -96
  23. oceanarray/plotters/primitives.py +448 -57
  24. oceanarray/plotters/spectrum.py +184 -81
  25. oceanarray/plotters/timeseries.py +157 -54
  26. oceanarray/plotters/ts.py +217 -76
  27. oceanarray/processors/__init__.py +10 -9
  28. oceanarray/processors/caldip.py +309 -1
  29. oceanarray/processors/coordinate.py +54 -43
  30. oceanarray/processors/grid.py +57 -62
  31. oceanarray/processors/helpers.py +29 -17
  32. oceanarray/processors/pressure.py +16 -12
  33. oceanarray/processors/qc.py +26 -25
  34. oceanarray/processors/stack.py +28 -27
  35. oceanarray/processors/stage1.py +103 -73
  36. oceanarray/processors/stage2.py +20 -19
  37. oceanarray/processors/stage3.py +90 -35
  38. oceanarray/reports/__init__.py +6 -0
  39. oceanarray/{report → reports}/_array.py +30 -211
  40. oceanarray/reports/_css.py +267 -0
  41. oceanarray/reports/_encode.py +136 -0
  42. oceanarray/reports/_env.py +101 -0
  43. oceanarray/reports/_figdebug.py +142 -0
  44. oceanarray/reports/_grid.py +650 -0
  45. oceanarray/{report → reports}/_html_helpers.py +140 -53
  46. oceanarray/reports/_instrument.py +656 -0
  47. oceanarray/reports/_manifest.py +449 -0
  48. oceanarray/reports/_mooring.py +862 -0
  49. oceanarray/reports/_pdf.py +415 -0
  50. oceanarray/{report → reports}/_plots.py +285 -348
  51. oceanarray/{report → reports}/_recovery_table.py +22 -169
  52. oceanarray/reports/_report_css.py +23 -0
  53. oceanarray/reports/_slots.py +62 -0
  54. oceanarray/reports/_stack.py +1098 -0
  55. oceanarray/reports/templates/_grid_history.html +10 -0
  56. oceanarray/reports/templates/_grid_nc_globals.html +14 -0
  57. oceanarray/reports/templates/_grid_nc_scalars.html +18 -0
  58. oceanarray/reports/templates/_grid_nc_variables.html +30 -0
  59. oceanarray/reports/templates/_instrument_analog_yaml.html +12 -0
  60. oceanarray/reports/templates/_instrument_files.html +39 -0
  61. oceanarray/reports/templates/_instrument_nc_globals.html +14 -0
  62. oceanarray/reports/templates/_instrument_nc_scalars.html +19 -0
  63. oceanarray/reports/templates/_instrument_nc_variables.html +30 -0
  64. oceanarray/reports/templates/_instrument_qc.html +65 -0
  65. oceanarray/reports/templates/_instrument_rose_note.html +6 -0
  66. oceanarray/reports/templates/_instrument_windows_note.html +36 -0
  67. oceanarray/reports/templates/_macros.html +87 -0
  68. oceanarray/reports/templates/_mooring_calibration.html +51 -0
  69. oceanarray/reports/templates/_mooring_clock_table.html +59 -0
  70. oceanarray/reports/templates/_mooring_diagram.html +5 -0
  71. oceanarray/reports/templates/_mooring_instruments.html +101 -0
  72. oceanarray/reports/templates/_mooring_issues.html +28 -0
  73. oceanarray/reports/templates/_mooring_pipeline.html +103 -0
  74. oceanarray/reports/templates/_mooring_qc.html +72 -0
  75. oceanarray/reports/templates/_mooring_timing.html +156 -0
  76. oceanarray/reports/templates/_stack_instr_table.html +16 -0
  77. oceanarray/reports/templates/_stack_nc_dims.html +11 -0
  78. oceanarray/reports/templates/_stack_roses_note.html +13 -0
  79. oceanarray/reports/templates/array.html +150 -0
  80. oceanarray/reports/templates/base.html +103 -0
  81. oceanarray/reports/templates/grid.html +43 -0
  82. oceanarray/reports/templates/instrument.html +36 -0
  83. oceanarray/reports/templates/mooring.html +91 -0
  84. oceanarray/reports/templates/recovery_table.html +129 -0
  85. oceanarray/reports/templates/stack.html +28 -0
  86. oceanarray/tools/rapid_interp.py +17 -23
  87. oceanarray/tools/readers.py +16 -18
  88. oceanarray/tools/writers.py +1 -1
  89. oceanarray/utilities.py +115 -36
  90. {oceanarray-0.3.0.dist-info → oceanarray-0.4.0.dist-info}/METADATA +29 -10
  91. oceanarray-0.4.0.dist-info/RECORD +114 -0
  92. oceanarray/oceanarray.mplstyle +0 -18
  93. oceanarray/report/__init__.py +0 -5
  94. oceanarray/report/_grid.py +0 -558
  95. oceanarray/report/_instrument.py +0 -866
  96. oceanarray/report/_mooring.py +0 -1514
  97. oceanarray/report/_stack.py +0 -950
  98. oceanarray-0.3.0.dist-info/RECORD +0 -74
  99. {oceanarray-0.3.0.dist-info → oceanarray-0.4.0.dist-info}/WHEEL +0 -0
  100. {oceanarray-0.3.0.dist-info → oceanarray-0.4.0.dist-info}/entry_points.txt +0 -0
  101. {oceanarray-0.3.0.dist-info → oceanarray-0.4.0.dist-info}/licenses/LICENSE +0 -0
  102. {oceanarray-0.3.0.dist-info → oceanarray-0.4.0.dist-info}/top_level.txt +0 -0
oceanarray/__init__.py CHANGED
@@ -4,10 +4,23 @@ This package provides functions for processing, quality control, and analysis
4
4
  of oceanographic time series data from moorings and instruments.
5
5
  """
6
6
 
7
+ try:
8
+ # Written by setuptools-scm at build time (gitignored); the live version.
9
+ from oceanarray._version import __version__
10
+ except ImportError: # pragma: no cover - source tree with no build artefact
11
+ try:
12
+ from importlib.metadata import PackageNotFoundError as _PNF
13
+ from importlib.metadata import version as _v
14
+
15
+ __version__ = _v("oceanarray")
16
+ except _PNF: # pragma: no cover
17
+ __version__ = "0.0.0"
18
+
7
19
  from oceanarray.config import parameters
8
20
  from oceanarray.processors import process
9
21
 
10
22
  __all__ = [
23
+ "__version__",
11
24
  # Subpackages (canonical locations)
12
25
  "analysis",
13
26
  "config",
oceanarray/_version.py CHANGED
@@ -1 +1 @@
1
- __version__ = '0.3.0'
1
+ __version__ = '0.4.0'
@@ -12,7 +12,7 @@ Naming conventions updated from the original module:
12
12
  - file suffix ``_raw`` → ``_stage2``
13
13
  """
14
14
 
15
- import os
15
+ from pathlib import Path
16
16
 
17
17
  import numpy as np
18
18
  import pandas as pd
@@ -53,13 +53,13 @@ def load_mooring_instruments(
53
53
  proc_dir = output_path + mooring_name
54
54
  moor_yaml = proc_dir + "/" + mooring_name + ".mooring.yaml"
55
55
 
56
- with open(moor_yaml, "r") as f:
56
+ with Path(moor_yaml).open() as f:
57
57
  moor_yaml_data = yaml.safe_load(f)
58
58
 
59
59
  # Support both new key 'clamp' and legacy key 'instruments'
60
60
  instrument_list = moor_yaml_data.get("clamp", moor_yaml_data.get("instruments", []))
61
61
  if not instrument_list:
62
- raise KeyError( # noqa: TRY003
62
+ raise KeyError(
63
63
  f"Mooring YAML {moor_yaml} has neither 'clamp' nor 'instruments' key."
64
64
  )
65
65
 
@@ -68,7 +68,7 @@ def load_mooring_instruments(
68
68
  fname = mooring_name + "_" + str(i["serial"]) + file_suffix + ".nc"
69
69
  rawfile = proc_dir + "/" + i["instrument"] + "/" + fname
70
70
 
71
- if os.path.exists(rawfile):
71
+ if Path(rawfile).exists():
72
72
  print(rawfile)
73
73
  ds1 = xr.open_dataset(rawfile)
74
74
 
@@ -123,9 +123,7 @@ def create_common_time_grid(datasets):
123
123
  latest_end = np.datetime64("NaT", "ns")
124
124
 
125
125
  dt_sec = int(np.nanmedian(intervals_min) * 60)
126
- time_grid = np.arange(earliest_start, latest_end, np.timedelta64(dt_sec, "s"))
127
-
128
- return time_grid
126
+ return np.arange(earliest_start, latest_end, np.timedelta64(dt_sec, "s"))
129
127
 
130
128
 
131
129
  def interpolate_datasets_to_grid(datasets, time_grid):
@@ -251,7 +249,7 @@ def combine_interpolated_datasets(datasets_interp):
251
249
  co = 0
252
250
  clock_offsets.append(int(np.rint(co)) if np.isfinite(co) else 0)
253
251
 
254
- combined_ds = xr.Dataset(
252
+ return xr.Dataset(
255
253
  data_vars=combined_data,
256
254
  coords={
257
255
  "time": time_coord,
@@ -263,8 +261,6 @@ def combine_interpolated_datasets(datasets_interp):
263
261
  },
264
262
  )
265
263
 
266
- return combined_ds
267
-
268
264
 
269
265
  def analyze_deployment_timing(combined_ds):
270
266
  """Analyze deployment timing using temperature-based detection of deployment periods.
@@ -319,7 +315,7 @@ def calculate_timing_offsets(combined_ds, bin_width_sec=60):
319
315
  # Find consensus group
320
316
  vals = start_off0[np.isfinite(start_off0)]
321
317
  if vals.size == 0:
322
- raise RuntimeError("No finite start offsets to form consensus.") # noqa: TRY003
318
+ raise RuntimeError("No finite start offsets to form consensus.")
323
319
 
324
320
  vmin, vmax = vals.min(), vals.max()
325
321
  bins = np.arange(vmin - bin_width_sec, vmax + 2 * bin_width_sec, bin_width_sec)
@@ -539,6 +535,7 @@ def print_correlation_summary(combined_ds, correlation_results):
539
535
  correlation_results["lags"],
540
536
  correlation_results["max_correlations"],
541
537
  correlation_results["clock_offsets"],
538
+ strict=False,
542
539
  )
543
540
  ):
544
541
  print(
@@ -626,7 +623,9 @@ def plot_deployment_boundaries(
626
623
  continue
627
624
 
628
625
  # Plot each boundary
629
- for ax, title, idx, time_ref in zip(axes, titles, indices, times_ref):
626
+ for ax, title, idx, time_ref in zip(
627
+ axes, titles, indices, times_ref, strict=False
628
+ ):
630
629
  # Define range around the boundary
631
630
  start_range = max(0, idx - n_samples)
632
631
  end_range = min(len(time_orig), idx + n_samples + 1)
@@ -9,10 +9,10 @@ Pairs with :mod:`oceanarray.plotters.hydrography` for figure output.
9
9
 
10
10
  from __future__ import annotations
11
11
 
12
+ import gsw
12
13
  import numpy as np
13
14
  import pandas as pd
14
15
  import xarray as xr
15
- import gsw
16
16
 
17
17
 
18
18
  def calc_psal(ds: xr.Dataset) -> xr.Dataset:
@@ -99,7 +99,9 @@ def find_cold_entry_exit(
99
99
  starts = np.r_[idx[0], idx[1:][gaps]]
100
100
  ends = np.r_[idx[:-1][gaps], idx[-1]]
101
101
 
102
- runs = [(s, e) for s, e in zip(starts, ends) if (e - s + 1) >= min_len]
102
+ runs = [
103
+ (s, e) for s, e in zip(starts, ends, strict=False) if (e - s + 1) >= min_len
104
+ ]
103
105
  if not runs:
104
106
  return None, None, thr
105
107
 
@@ -111,7 +113,7 @@ def find_cold_entry_exit(
111
113
  def calc_ds_difference(ds1: xr.Dataset, ds2: xr.Dataset) -> xr.Dataset:
112
114
  """Compute the variable-by-variable difference between two time-matched datasets."""
113
115
  if not np.array_equal(ds1["TIME"].values, ds2["TIME"].values):
114
- raise ValueError("TIME grids do not match between datasets.") # noqa: TRY003
116
+ raise ValueError("TIME grids do not match between datasets.")
115
117
 
116
118
  # Variables to exclude from differencing
117
119
  exclude_vars = {"YY", "MM", "DD", "HH"}
@@ -261,11 +263,11 @@ def isopycnal_dataset(
261
263
 
262
264
  """
263
265
  if sigma_var not in ds:
264
- raise ValueError(f"sigma variable '{sigma_var}' not found in dataset") # noqa: TRY003
266
+ raise ValueError(f"sigma variable '{sigma_var}' not found in dataset")
265
267
 
266
268
  da = ds[sigma_var]
267
269
  if "time" not in da.dims or "pressure" not in da.dims:
268
- raise ValueError( # noqa: TRY003
270
+ raise ValueError(
269
271
  f"'{sigma_var}' must have both 'time' and 'pressure' dimensions"
270
272
  )
271
273
  da_tp = da.transpose("time", "pressure")
@@ -57,8 +57,7 @@ def flag_salinity_outliers(ds: xr.Dataset, n_std: float = 4) -> xr.DataArray:
57
57
  lower = mean_sal - n_std * std_sal
58
58
  upper = mean_sal + n_std * std_sal
59
59
 
60
- flag = (ds["PSAL"] < lower) | (ds["PSAL"] > upper)
61
- return flag
60
+ return (ds["PSAL"] < lower) | (ds["PSAL"] > upper)
62
61
 
63
62
 
64
63
  def flag_temporal_spikes(
@@ -87,8 +86,7 @@ def flag_vertical_inconsistencies(
87
86
  vert_diff = np.abs(ds[var].diff("DEPTH"))
88
87
  # Pad to match original dimensions
89
88
  vert_diff = vert_diff.reindex(DEPTH=ds["DEPTH"], method="ffill")
90
- flag = vert_diff > threshold
91
- return flag
89
+ return vert_diff > threshold
92
90
 
93
91
 
94
92
  def run_qc(ds: xr.Dataset) -> xr.Dataset:
@@ -172,10 +170,10 @@ def process_dataset(
172
170
 
173
171
  # Apply region of interest mask
174
172
  mask = (
175
- (LAT >= latlim[0])
176
- & (LAT <= latlim[1])
177
- & (LON >= lonlim[0])
178
- & (LON <= lonlim[1])
173
+ (latlim[0] <= LAT)
174
+ & (latlim[1] >= LAT)
175
+ & (lonlim[0] <= LON)
176
+ & (lonlim[1] >= LON)
179
177
  )
180
178
  TEMP_profiles = TEMP_profiles[mask]
181
179
  PSAL_profiles = PSAL_profiles[mask]
@@ -194,7 +192,7 @@ def process_dataset(
194
192
  standard_pressures = pgrid.flatten()
195
193
 
196
194
  # Create ds_standard
197
- ds_standard = xr.Dataset(
195
+ return xr.Dataset(
198
196
  {
199
197
  "CT": ((time_dim, pres_dim), CT_standard),
200
198
  "SA": ((time_dim, pres_dim), SA_standard),
@@ -209,18 +207,20 @@ def process_dataset(
209
207
  },
210
208
  )
211
209
 
212
- return ds_standard
213
-
214
210
 
215
211
  # ---------------------------------------------------------------------------
216
212
  # Backward-compat re-exports — import directly from the new modules in new code
217
213
  # ---------------------------------------------------------------------------
218
- from .temporal import lag_correlation, split_value, downsample_to_sparse # noqa: F401, E402
219
- from .spectral import compute_cwt, welch_psd, welch_psd_gapaware # noqa: F401, E402
220
214
  from .hydrographic import ( # noqa: F401, E402
215
+ calc_ds_difference,
221
216
  calc_psal,
222
217
  find_cold_entry_exit,
223
- calc_ds_difference,
224
- isopycnal_pressure_series,
225
218
  isopycnal_dataset,
219
+ isopycnal_pressure_series,
220
+ )
221
+ from .spectral import compute_cwt, welch_psd, welch_psd_gapaware # noqa: F401, E402
222
+ from .temporal import ( # noqa: F401, E402
223
+ downsample_to_sparse,
224
+ lag_correlation,
225
+ split_value,
226
226
  )
@@ -61,14 +61,14 @@ def gonella_rotary_spectrum(
61
61
  """
62
62
  from scipy.signal import csd as _scipy_csd
63
63
 
64
- _kw: dict = dict(
65
- fs=fs,
66
- window="hann",
67
- nperseg=nperseg,
68
- noverlap=noverlap,
69
- detrend="linear",
70
- scaling="density",
71
- )
64
+ _kw: dict = {
65
+ "fs": fs,
66
+ "window": "hann",
67
+ "nperseg": nperseg,
68
+ "noverlap": noverlap,
69
+ "detrend": "linear",
70
+ "scaling": "density",
71
+ }
72
72
  f_uu, p_uu = _scipy_welch(u_col, **_kw)
73
73
  _, p_vv = _scipy_welch(v_col, **_kw)
74
74
  _, c_uv = _scipy_csd(u_col, v_col, **_kw)
@@ -139,7 +139,7 @@ def compute_cwt(
139
139
  try:
140
140
  import pycwt
141
141
  except ImportError as exc:
142
- raise ImportError( # noqa: TRY003
142
+ raise ImportError(
143
143
  "pycwt is required for wavelet analysis: pip install pycwt"
144
144
  ) from exc
145
145
 
@@ -205,7 +205,7 @@ def compute_cwt(
205
205
  gap_starts = np.where(edge_diff == 1)[0]
206
206
  gap_ends = np.where(edge_diff == -1)[0]
207
207
  t_all = np.arange(n, dtype=float)
208
- for s, e in zip(gap_starts, gap_ends):
208
+ for s, e in zip(gap_starts, gap_ends, strict=False):
209
209
  eff_coi[s:e] = 0.0 # entire gap column is unreliable
210
210
  # Distance (samples) to the nearer gap boundary, outside the gap
211
211
  dist = np.minimum(
@@ -322,7 +322,7 @@ def welch_psd_gapaware(
322
322
  freq_out: np.ndarray | None = None
323
323
  psd_sum: np.ndarray | None = None
324
324
  total_wins = 0
325
- for s, e in zip(starts, ends):
325
+ for s, e in zip(starts, ends, strict=False):
326
326
  seg = x[s:e]
327
327
  if len(seg) < segment_length:
328
328
  continue
@@ -97,7 +97,7 @@ def lag_correlation(
97
97
  x = np.asarray(x, float)
98
98
  y = np.asarray(y, float)
99
99
  if x.shape != y.shape:
100
- raise ValueError("x and y must have same length (subsample both).") # noqa: TRY003
100
+ raise ValueError("x and y must have same length (subsample both).")
101
101
  corrs = np.full(2 * max_lag + 1, np.nan)
102
102
  for k, lag in enumerate(range(-max_lag, max_lag + 1)):
103
103
  if lag < 0:
@@ -154,7 +154,7 @@ def split_value(data: np.ndarray, nbins: int = 30) -> float:
154
154
 
155
155
  # Step 3: Find minimum between first two major peaks
156
156
  if len(peaks) < 2:
157
- raise ValueError( # noqa: TRY003
157
+ raise ValueError(
158
158
  f"split_value: fewer than 2 histogram peaks found in data "
159
159
  f"(found {len(peaks)}). Data may be unimodal or nbins={nbins} too coarse."
160
160
  )