microstructure-tpw 0.3.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.
- microstructure_tpw-0.3.0.dist-info/METADATA +234 -0
- microstructure_tpw-0.3.0.dist-info/RECORD +119 -0
- microstructure_tpw-0.3.0.dist-info/WHEEL +5 -0
- microstructure_tpw-0.3.0.dist-info/entry_points.txt +7 -0
- microstructure_tpw-0.3.0.dist-info/licenses/LICENSE +674 -0
- microstructure_tpw-0.3.0.dist-info/top_level.txt +1 -0
- odas_tpw/__init__.py +18 -0
- odas_tpw/chi/__init__.py +48 -0
- odas_tpw/chi/batchelor.py +189 -0
- odas_tpw/chi/chi.py +857 -0
- odas_tpw/chi/fp07.py +649 -0
- odas_tpw/chi/l2_chi.py +153 -0
- odas_tpw/chi/l3_chi.py +388 -0
- odas_tpw/chi/l4_chi.py +391 -0
- odas_tpw/config_base.py +423 -0
- odas_tpw/perturb/__init__.py +18 -0
- odas_tpw/perturb/adcp.py +274 -0
- odas_tpw/perturb/atomic_io.py +47 -0
- odas_tpw/perturb/binning.py +761 -0
- odas_tpw/perturb/cli.py +591 -0
- odas_tpw/perturb/combo.py +346 -0
- odas_tpw/perturb/config.py +1046 -0
- odas_tpw/perturb/ctd.py +424 -0
- odas_tpw/perturb/diag/__init__.py +12 -0
- odas_tpw/perturb/diag/_common.py +140 -0
- odas_tpw/perturb/diag/chi.py +42 -0
- odas_tpw/perturb/diag/cli.py +67 -0
- odas_tpw/perturb/diag/data.py +662 -0
- odas_tpw/perturb/diag/epsilon.py +45 -0
- odas_tpw/perturb/diag/inspector.py +342 -0
- odas_tpw/perturb/diag/mixing.py +94 -0
- odas_tpw/perturb/diag/render.py +543 -0
- odas_tpw/perturb/discover.py +40 -0
- odas_tpw/perturb/fp07_cal.py +354 -0
- odas_tpw/perturb/gps.py +458 -0
- odas_tpw/perturb/hotel.py +572 -0
- odas_tpw/perturb/logging_setup.py +181 -0
- odas_tpw/perturb/merge.py +320 -0
- odas_tpw/perturb/mk_sections.py +310 -0
- odas_tpw/perturb/netcdf_schema.py +520 -0
- odas_tpw/perturb/pipeline.py +2654 -0
- odas_tpw/perturb/plot/__init__.py +15 -0
- odas_tpw/perturb/plot/cli.py +107 -0
- odas_tpw/perturb/plot/diagnostics.py +195 -0
- odas_tpw/perturb/plot/eps_chi.py +524 -0
- odas_tpw/perturb/plot/figure.py +679 -0
- odas_tpw/perturb/plot/gamma_scaling.py +1097 -0
- odas_tpw/perturb/plot/grid.py +112 -0
- odas_tpw/perturb/plot/layout.py +361 -0
- odas_tpw/perturb/plot/overview.py +547 -0
- odas_tpw/perturb/plot/presets/eps-chi.yaml +16 -0
- odas_tpw/perturb/plot/presets/profiles.yaml +32 -0
- odas_tpw/perturb/plot/presets/scalar.yaml +21 -0
- odas_tpw/perturb/plot/profiles.py +652 -0
- odas_tpw/perturb/plot/scalar.py +391 -0
- odas_tpw/perturb/plot/sections.py +506 -0
- odas_tpw/perturb/plot/xaxis.py +396 -0
- odas_tpw/perturb/qc_gate.py +177 -0
- odas_tpw/perturb/qc_rules.py +373 -0
- odas_tpw/perturb/resolve.py +306 -0
- odas_tpw/perturb/seawater.py +76 -0
- odas_tpw/perturb/speed.py +14 -0
- odas_tpw/perturb/trim.py +313 -0
- odas_tpw/processing/__init__.py +62 -0
- odas_tpw/processing/bottom.py +215 -0
- odas_tpw/processing/chi_combine.py +300 -0
- odas_tpw/processing/ct_align.py +172 -0
- odas_tpw/processing/epsilon_combine.py +210 -0
- odas_tpw/processing/mixing.py +630 -0
- odas_tpw/processing/thorpe.py +458 -0
- odas_tpw/processing/top_trim.py +274 -0
- odas_tpw/pyturb/__init__.py +3 -0
- odas_tpw/pyturb/__main__.py +5 -0
- odas_tpw/pyturb/_compat.py +204 -0
- odas_tpw/pyturb/_profind.py +124 -0
- odas_tpw/pyturb/bin.py +113 -0
- odas_tpw/pyturb/cli.py +175 -0
- odas_tpw/pyturb/eps.py +353 -0
- odas_tpw/pyturb/merge.py +101 -0
- odas_tpw/pyturb/p2nc.py +76 -0
- odas_tpw/rsi/__init__.py +31 -0
- odas_tpw/rsi/adapter.py +314 -0
- odas_tpw/rsi/bench.py +904 -0
- odas_tpw/rsi/binning.py +193 -0
- odas_tpw/rsi/channels.py +376 -0
- odas_tpw/rsi/chi_io.py +980 -0
- odas_tpw/rsi/cli.py +1411 -0
- odas_tpw/rsi/combine.py +120 -0
- odas_tpw/rsi/config.py +126 -0
- odas_tpw/rsi/config_patch.py +790 -0
- odas_tpw/rsi/convert.py +651 -0
- odas_tpw/rsi/deconvolve.py +119 -0
- odas_tpw/rsi/diss_look.py +343 -0
- odas_tpw/rsi/dissipation.py +669 -0
- odas_tpw/rsi/helpers.py +527 -0
- odas_tpw/rsi/mixing_look.py +509 -0
- odas_tpw/rsi/p_file.py +771 -0
- odas_tpw/rsi/pipeline.py +924 -0
- odas_tpw/rsi/profile.py +624 -0
- odas_tpw/rsi/quick_look.py +758 -0
- odas_tpw/rsi/sensor_inventory.py +684 -0
- odas_tpw/rsi/shear_noise.py +128 -0
- odas_tpw/rsi/speed.py +221 -0
- odas_tpw/rsi/vehicle.py +74 -0
- odas_tpw/rsi/viewer_base.py +1071 -0
- odas_tpw/rsi/window.py +459 -0
- odas_tpw/scor160/__init__.py +36 -0
- odas_tpw/scor160/cli.py +242 -0
- odas_tpw/scor160/compare.py +484 -0
- odas_tpw/scor160/despike.py +227 -0
- odas_tpw/scor160/goodman.py +288 -0
- odas_tpw/scor160/io.py +690 -0
- odas_tpw/scor160/l2.py +311 -0
- odas_tpw/scor160/l3.py +279 -0
- odas_tpw/scor160/l4.py +850 -0
- odas_tpw/scor160/nasmyth.py +208 -0
- odas_tpw/scor160/ocean.py +287 -0
- odas_tpw/scor160/profile.py +231 -0
- odas_tpw/scor160/spectral.py +482 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Mar-2026, Claude and Pat Welch, pat@mousebrains.com
|
|
2
|
+
"""Electronics noise model for shear probe channels.
|
|
3
|
+
|
|
4
|
+
Port of noise_shearchannel.m from the ODAS MATLAB library (v4.5.1).
|
|
5
|
+
|
|
6
|
+
The shear probe circuit consists of:
|
|
7
|
+
1. Charge-transfer amplifier (LTC6240, R1=1 GΩ, C1=1.5 nF)
|
|
8
|
+
2. Differentiator (R2, C2, R3, C3)
|
|
9
|
+
3. Anti-aliasing filter (two cascaded 4th-order Butterworth)
|
|
10
|
+
4. ADC sampler
|
|
11
|
+
|
|
12
|
+
References
|
|
13
|
+
----------
|
|
14
|
+
RSI Technical Note 042: Noise in Shear Measurements.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
import numpy.typing as npt
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def noise_shearchannel(
|
|
22
|
+
F: npt.ArrayLike,
|
|
23
|
+
*,
|
|
24
|
+
R1: float = 1e9,
|
|
25
|
+
C1: float = 1.5e-9,
|
|
26
|
+
R2: float = 499,
|
|
27
|
+
C2: float = 0.94e-6,
|
|
28
|
+
R3: float = 1e6,
|
|
29
|
+
C3: float = 470e-12,
|
|
30
|
+
CP: float = 0,
|
|
31
|
+
E_1: float = 9e-9,
|
|
32
|
+
fc: float = 50,
|
|
33
|
+
I_1: float = 0.56e-15,
|
|
34
|
+
f_AA: float = 110,
|
|
35
|
+
fs: float = 512,
|
|
36
|
+
VFS: float = 4.096,
|
|
37
|
+
Bits: int = 16,
|
|
38
|
+
gamma_RSI: float = 2.5,
|
|
39
|
+
T_K: float = 295,
|
|
40
|
+
K_B: float = 1.382e-23,
|
|
41
|
+
) -> np.ndarray:
|
|
42
|
+
"""Compute the output noise spectrum of the shear probe channel.
|
|
43
|
+
|
|
44
|
+
Parameters
|
|
45
|
+
----------
|
|
46
|
+
F : array_like
|
|
47
|
+
Frequency vector [Hz].
|
|
48
|
+
R1 : float
|
|
49
|
+
First-stage feedback resistance [Ohm]. Default: 1 GΩ.
|
|
50
|
+
C1 : float
|
|
51
|
+
First-stage charge-transfer capacitance [F]. Default: 1.5 nF.
|
|
52
|
+
R2 : float
|
|
53
|
+
Differentiator input resistance [Ohm]. Default: 499 Ω.
|
|
54
|
+
C2 : float
|
|
55
|
+
Differentiator capacitance [F]. Default: 0.94 µF.
|
|
56
|
+
R3 : float
|
|
57
|
+
Differentiator feedback resistance [Ohm]. Default: 1 MΩ.
|
|
58
|
+
C3 : float
|
|
59
|
+
Differentiator feedback capacitance [F]. Default: 470 pF.
|
|
60
|
+
CP : float
|
|
61
|
+
Probe capacitance [F]. Default: 0 (no probe). Set to ~1 nF with probe.
|
|
62
|
+
E_1 : float
|
|
63
|
+
First-stage amplifier voltage noise [V/√Hz]. Default: 9e-9.
|
|
64
|
+
fc : float
|
|
65
|
+
First-stage flicker noise knee frequency [Hz]. Default: 50.
|
|
66
|
+
I_1 : float
|
|
67
|
+
First-stage amplifier current noise [A/√Hz]. Default: 0.56e-15.
|
|
68
|
+
f_AA : float
|
|
69
|
+
Anti-aliasing filter cutoff [Hz]. Default: 110.
|
|
70
|
+
fs : float
|
|
71
|
+
Sampling rate [Hz]. Default: 512.
|
|
72
|
+
VFS : float
|
|
73
|
+
ADC full-scale voltage [V]. Default: 4.096.
|
|
74
|
+
Bits : int
|
|
75
|
+
ADC resolution [bits]. Default: 16.
|
|
76
|
+
gamma_RSI : float
|
|
77
|
+
ADC noise factor (RSI sampler excess). Default: 2.5.
|
|
78
|
+
T_K : float
|
|
79
|
+
Operating temperature [K]. Default: 295.
|
|
80
|
+
K_B : float
|
|
81
|
+
Boltzmann constant [J/K]. Default: 1.382e-23.
|
|
82
|
+
|
|
83
|
+
Returns
|
|
84
|
+
-------
|
|
85
|
+
noise : ndarray
|
|
86
|
+
Noise spectrum [counts² / Hz].
|
|
87
|
+
"""
|
|
88
|
+
F = np.asarray(F, dtype=np.float64)
|
|
89
|
+
omega = 2 * np.pi * F
|
|
90
|
+
|
|
91
|
+
delta_s = VFS / 2**Bits # ADC step size
|
|
92
|
+
fN = fs / 2 # Nyquist frequency
|
|
93
|
+
|
|
94
|
+
# --- Stage 1: Charge-transfer amplifier (LTC6240) ---
|
|
95
|
+
# Voltage noise (1/f model from spec sheet)
|
|
96
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
97
|
+
V_V1 = E_1**2 * (fc / F) * np.sqrt(1 + (F / fc) ** 2)
|
|
98
|
+
V_V1 = np.where(np.isfinite(V_V1), V_V1, 0.0)
|
|
99
|
+
|
|
100
|
+
# Current noise (frequency-independent up to 300 Hz)
|
|
101
|
+
V_I1 = I_1**2 * R1**2 / (1 + (omega * R1 * C1) ** 2)
|
|
102
|
+
|
|
103
|
+
# Johnson (thermal) noise from R1
|
|
104
|
+
V_R1 = 4 * K_B * T_K * R1 / (1 + (omega * R1 * C1) ** 2)
|
|
105
|
+
|
|
106
|
+
# Noise gain of first stage (includes probe capacitance)
|
|
107
|
+
G_1 = (1 + (omega * R1 * (CP + C1)) ** 2) / (1 + (omega * R1 * C1) ** 2)
|
|
108
|
+
|
|
109
|
+
Noise_1 = G_1 * (V_V1 + V_I1) + V_R1
|
|
110
|
+
|
|
111
|
+
# --- Stage 2: Differentiator ---
|
|
112
|
+
G_2 = (omega * R3 * C2) ** 2 / (1 + (omega * R2 * C2) ** 2) / (1 + (omega * R3 * C3) ** 2)
|
|
113
|
+
|
|
114
|
+
# Note: MATLAB uses (Noise_1 + V_V1) not (Noise_1 + V_V2) —
|
|
115
|
+
# same first-stage voltage noise applied as second-stage input noise
|
|
116
|
+
Noise_2 = (Noise_1 + V_V1) * G_2
|
|
117
|
+
|
|
118
|
+
# --- Anti-aliasing: two cascaded 4th-order Butterworth ---
|
|
119
|
+
G_AA = 1.0 / (1 + (F / f_AA) ** 8) ** 2
|
|
120
|
+
Noise_3 = Noise_2 * G_AA
|
|
121
|
+
|
|
122
|
+
# --- Sampling: ADC quantization noise ---
|
|
123
|
+
Noise_4 = Noise_3 + gamma_RSI * delta_s**2 / (12 * fN)
|
|
124
|
+
|
|
125
|
+
# Convert to counts²/Hz
|
|
126
|
+
noise_counts = Noise_4 / delta_s**2
|
|
127
|
+
|
|
128
|
+
return np.asarray(noise_counts)
|
odas_tpw/rsi/speed.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# May-2026, Pat Welch, pat@mousebrains.com
|
|
2
|
+
"""Configurable through-water speed computation from a loaded ``.p`` file.
|
|
3
|
+
|
|
4
|
+
Shared by both the ``rsi`` and ``perturb`` pipelines (``perturb.speed``
|
|
5
|
+
re-exports ``compute_speed_for_pfile``). Run *after* the ``.p`` file is loaded
|
|
6
|
+
and any hotel data has been merged into ``pf.channels``, so
|
|
7
|
+
``compute_speed_for_pfile`` sees the union of both sources.
|
|
8
|
+
|
|
9
|
+
Methods
|
|
10
|
+
-------
|
|
11
|
+
``"pressure"``
|
|
12
|
+
ODAS-style smoothed ``|dP/dt|`` at slow rate, interpolated and
|
|
13
|
+
Butterworth-smoothed onto fast rate. This is the historical default
|
|
14
|
+
(correct for VMP, returns vertical speed for a glider).
|
|
15
|
+
|
|
16
|
+
``"em"``
|
|
17
|
+
Use the ``U_EM`` channel from the ``.p`` file directly — the
|
|
18
|
+
MicroRider's electromagnetic flowmeter measurement of along-axis
|
|
19
|
+
flow. Errors out if ``U_EM`` is missing.
|
|
20
|
+
|
|
21
|
+
``"flight"``
|
|
22
|
+
Construct an inviscid flight-model along-axis speed from the MR's
|
|
23
|
+
own pressure and inclinometers:
|
|
24
|
+
|
|
25
|
+
U_along = |W| / sin(|pitch| - aoa)
|
|
26
|
+
|
|
27
|
+
where ``aoa`` is the angle of attack (``aoa_deg``, default 3 deg
|
|
28
|
+
matching ODAS Slocum trim). The pitch axis is auto-picked from the
|
|
29
|
+
inclinometer axis with the larger swing -- Slocum mountings some-
|
|
30
|
+
times have ``Incl_X`` carrying pitch, sometimes ``Incl_Y``.
|
|
31
|
+
|
|
32
|
+
``"constant"``
|
|
33
|
+
Use the scalar in ``speed.value`` for every fast-rate sample.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import warnings
|
|
39
|
+
from typing import Any
|
|
40
|
+
|
|
41
|
+
import numpy as np
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def compute_speed_for_pfile(
|
|
45
|
+
pf: Any,
|
|
46
|
+
speed_cfg: dict | None,
|
|
47
|
+
vehicle: str | None = None,
|
|
48
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
49
|
+
"""Return (speed_fast, W_slow) from configured method, in m/s and dbar/s.
|
|
50
|
+
|
|
51
|
+
Parameters
|
|
52
|
+
----------
|
|
53
|
+
pf : PFile
|
|
54
|
+
Has ``channels`` (already merged with hotel), ``t_fast``,
|
|
55
|
+
``t_slow``, ``fs_fast``, ``fs_slow``.
|
|
56
|
+
speed_cfg : dict
|
|
57
|
+
Merged ``speed:`` section from the perturb config.
|
|
58
|
+
vehicle : str | None
|
|
59
|
+
Vehicle string from instrument_info (``"slocum_glider"``,
|
|
60
|
+
``"vmp"`` etc.). Auto-resolves ``tau`` and, for the ``pressure``
|
|
61
|
+
method, triggers a bias warning on glide/horizontal platforms
|
|
62
|
+
(where ``|dP/dt|`` is not the through-water speed).
|
|
63
|
+
|
|
64
|
+
Returns
|
|
65
|
+
-------
|
|
66
|
+
speed_fast : (n_fast,) float64, m/s, >= ``speed_cutout``.
|
|
67
|
+
For the pressure method this is numerically |dP/dt| in
|
|
68
|
+
dbar/s treated as m/s (the ODAS convention). 1 dbar is
|
|
69
|
+
~0.99 m (equivalently 1 m ~ 1.006-1.009 dbar depending on
|
|
70
|
+
latitude/density), so |dP/dt| in dbar/s slightly OVER-states
|
|
71
|
+
the true speed in m/s, by <1%. Through epsilon's strong
|
|
72
|
+
inverse speed leverage (~U^-4 in the shear conversion and
|
|
73
|
+
wavenumber transform) that biases the reported epsilon
|
|
74
|
+
slightly LOW.
|
|
75
|
+
W_slow : (n_slow,) float64, dbar/s. Always the smoothed |dP/dt|
|
|
76
|
+
-- independent of method, useful for QC/binning.
|
|
77
|
+
"""
|
|
78
|
+
from odas_tpw.rsi.vehicle import resolve_direction, resolve_tau
|
|
79
|
+
from odas_tpw.scor160.profile import compute_speed_fast as _ode_speed
|
|
80
|
+
from odas_tpw.scor160.profile import smooth_fall_rate
|
|
81
|
+
|
|
82
|
+
cfg = speed_cfg or {}
|
|
83
|
+
method = cfg.get("method", "pressure")
|
|
84
|
+
speed_cutout = float(cfg.get("speed_cutout", 0.05))
|
|
85
|
+
tau = float(cfg.get("tau") or resolve_tau(vehicle or ""))
|
|
86
|
+
|
|
87
|
+
P_slow = np.asarray(pf.channels["P"], dtype=np.float64)
|
|
88
|
+
fs_slow = float(pf.fs_slow)
|
|
89
|
+
fs_fast = float(pf.fs_fast)
|
|
90
|
+
t_fast = np.asarray(pf.t_fast, dtype=np.float64)
|
|
91
|
+
t_slow = np.asarray(pf.t_slow, dtype=np.float64)
|
|
92
|
+
|
|
93
|
+
# W_slow always: smoothed |dP/dt|, regardless of method.
|
|
94
|
+
W_slow = smooth_fall_rate(P_slow, fs_slow, tau=tau)
|
|
95
|
+
|
|
96
|
+
if method == "pressure":
|
|
97
|
+
# For a glider / horizontal platform, |dP/dt| is the VERTICAL speed and
|
|
98
|
+
# underestimates the along-path flow past the sensors; epsilon has ~U^4
|
|
99
|
+
# leverage, so this strongly biases epsilon high. The rsi path warns
|
|
100
|
+
# about this in helpers.prepare_profiles, but perturb precomputes speed
|
|
101
|
+
# here and so never reaches that warning — emit the equivalent here so
|
|
102
|
+
# the perturb path is not silent (audit r2-3). VMP (down/up) is unaffected.
|
|
103
|
+
direction = resolve_direction("auto", vehicle or "")
|
|
104
|
+
if direction in ("glide", "horizontal"):
|
|
105
|
+
warnings.warn(
|
|
106
|
+
f"Vehicle direction '{direction}' but speed.method='pressure' "
|
|
107
|
+
"computes speed from |dP/dt| (vertical); set speed.method="
|
|
108
|
+
"'flight' (glider) or 'em' (horizontal), or speed.value — "
|
|
109
|
+
"epsilon scales as ~U^4 and will be strongly biased",
|
|
110
|
+
stacklevel=2,
|
|
111
|
+
)
|
|
112
|
+
speed_fast, _ = _ode_speed(
|
|
113
|
+
P_slow, t_fast, t_slow, fs_fast, fs_slow,
|
|
114
|
+
tau=tau, speed_min=speed_cutout,
|
|
115
|
+
)
|
|
116
|
+
return speed_fast, W_slow
|
|
117
|
+
|
|
118
|
+
if method == "constant":
|
|
119
|
+
v = cfg.get("value")
|
|
120
|
+
if v is None:
|
|
121
|
+
raise ValueError("speed.method='constant' but speed.value is null")
|
|
122
|
+
speed_fast = np.full(len(t_fast), max(abs(float(v)), speed_cutout))
|
|
123
|
+
return speed_fast, W_slow
|
|
124
|
+
|
|
125
|
+
if method == "em":
|
|
126
|
+
if "U_EM" not in pf.channels:
|
|
127
|
+
raise ValueError(
|
|
128
|
+
"speed.method='em' but channel U_EM is missing from the "
|
|
129
|
+
".p file. Use 'flight' or 'pressure' instead."
|
|
130
|
+
)
|
|
131
|
+
U_em_slow = np.abs(np.asarray(pf.channels["U_EM"], dtype=np.float64))
|
|
132
|
+
return _slow_to_fast(U_em_slow, t_fast, t_slow, fs_fast, fs_slow,
|
|
133
|
+
tau=tau, speed_min=speed_cutout), W_slow
|
|
134
|
+
|
|
135
|
+
if method == "flight":
|
|
136
|
+
aoa_deg = float(cfg.get("aoa_deg", 3.0))
|
|
137
|
+
min_pitch_deg = float(cfg.get("min_pitch_deg", 5.0))
|
|
138
|
+
aq = cfg.get("amplitude_quantile") or (1.0, 99.0)
|
|
139
|
+
speed_slow = _flight_model_slow(
|
|
140
|
+
W_slow, pf, aoa_deg=aoa_deg, min_pitch_deg=min_pitch_deg,
|
|
141
|
+
amplitude_quantile=(float(aq[0]), float(aq[1])),
|
|
142
|
+
)
|
|
143
|
+
return _slow_to_fast(speed_slow, t_fast, t_slow, fs_fast, fs_slow,
|
|
144
|
+
tau=tau, speed_min=speed_cutout), W_slow
|
|
145
|
+
|
|
146
|
+
raise ValueError(
|
|
147
|
+
f"Unknown speed.method={method!r}. "
|
|
148
|
+
"Expected: pressure | em | flight | constant."
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _slow_to_fast(
|
|
153
|
+
arr_slow: np.ndarray,
|
|
154
|
+
t_fast: np.ndarray,
|
|
155
|
+
t_slow: np.ndarray,
|
|
156
|
+
fs_fast: float,
|
|
157
|
+
fs_slow: float,
|
|
158
|
+
tau: float,
|
|
159
|
+
speed_min: float,
|
|
160
|
+
) -> np.ndarray:
|
|
161
|
+
"""Match scor160 speed pipeline: linear interp + Butterworth smoothing."""
|
|
162
|
+
from scipy.signal import butter, filtfilt
|
|
163
|
+
|
|
164
|
+
# NaN-safe interp by filling on the slow grid first.
|
|
165
|
+
finite = np.isfinite(arr_slow)
|
|
166
|
+
if not finite.any():
|
|
167
|
+
return np.full(len(t_fast), speed_min)
|
|
168
|
+
arr_slow = np.where(
|
|
169
|
+
finite, arr_slow,
|
|
170
|
+
np.interp(t_slow, t_slow[finite], arr_slow[finite]),
|
|
171
|
+
)
|
|
172
|
+
arr_fast = np.interp(t_fast, t_slow, arr_slow)
|
|
173
|
+
f_c = 0.68 / tau
|
|
174
|
+
b, a = butter(1, f_c / (fs_fast / 2.0))
|
|
175
|
+
arr_fast = np.asarray(filtfilt(b, a, arr_fast))
|
|
176
|
+
return np.asarray(np.maximum(arr_fast, speed_min), dtype=np.float64)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _flight_model_slow(
|
|
180
|
+
W_slow: np.ndarray,
|
|
181
|
+
pf: Any,
|
|
182
|
+
aoa_deg: float,
|
|
183
|
+
min_pitch_deg: float,
|
|
184
|
+
amplitude_quantile: tuple[float, float] = (1.0, 99.0),
|
|
185
|
+
) -> np.ndarray:
|
|
186
|
+
"""U_along = |W| / sin(glide angle), at slow rate.
|
|
187
|
+
|
|
188
|
+
Steady-flight kinematics (Merckelbach et al. 2010,
|
|
189
|
+
doi:10.1175/2009JTECHO710.1): W = U * sin(gamma) with glide-path
|
|
190
|
+
angle gamma = |pitch| - angle of attack, so U = |W| / sin(gamma).
|
|
191
|
+
Roll does not enter the along-path relation (it rotates the body
|
|
192
|
+
about the flight axis without changing the vertical velocity
|
|
193
|
+
component); an earlier version multiplied by cos(roll), which
|
|
194
|
+
inflated U by 1/cos(roll).
|
|
195
|
+
|
|
196
|
+
Pitch axis auto-picked from whichever of ``Incl_X``/``Incl_Y`` has
|
|
197
|
+
the larger percentile-spread amplitude (default 99-1) -- Slocum
|
|
198
|
+
mountings vary, and percentiles keep brief outlier spikes from
|
|
199
|
+
masquerading as the high-amplitude flight axis.
|
|
200
|
+
"""
|
|
201
|
+
iX = np.asarray(pf.channels.get("Incl_X"), dtype=np.float64) \
|
|
202
|
+
if "Incl_X" in pf.channels else None
|
|
203
|
+
iY = np.asarray(pf.channels.get("Incl_Y"), dtype=np.float64) \
|
|
204
|
+
if "Incl_Y" in pf.channels else None
|
|
205
|
+
if iX is None or iY is None:
|
|
206
|
+
raise ValueError(
|
|
207
|
+
"speed.method='flight' needs Incl_X and Incl_Y channels in the "
|
|
208
|
+
".p file. Use 'pressure' or 'em' instead."
|
|
209
|
+
)
|
|
210
|
+
lo_q, hi_q = float(amplitude_quantile[0]), float(amplitude_quantile[1])
|
|
211
|
+
iX_rng = float(np.nanpercentile(iX, hi_q) - np.nanpercentile(iX, lo_q))
|
|
212
|
+
iY_rng = float(np.nanpercentile(iY, hi_q) - np.nanpercentile(iY, lo_q))
|
|
213
|
+
pitch_deg = iX if iX_rng >= iY_rng else iY
|
|
214
|
+
|
|
215
|
+
pitch = np.deg2rad(pitch_deg)
|
|
216
|
+
aoa = np.deg2rad(aoa_deg)
|
|
217
|
+
eff = np.maximum(np.abs(pitch) - aoa, 0.0)
|
|
218
|
+
sin_path = np.sin(eff)
|
|
219
|
+
sin_floor = np.sin(np.deg2rad(min_pitch_deg))
|
|
220
|
+
sin_path = np.where(sin_path > sin_floor, sin_path, np.nan)
|
|
221
|
+
return np.asarray(np.abs(W_slow) / sin_path, dtype=np.float64)
|
odas_tpw/rsi/vehicle.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Mar-2026, Claude and Pat Welch, pat@mousebrains.com
|
|
2
|
+
"""Vehicle attributes for RSI instruments.
|
|
3
|
+
|
|
4
|
+
Maps vehicle types to their default profile direction and smoothing
|
|
5
|
+
time constant (tau), matching ODAS ``default_vehicle_attributes.ini``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
# (direction, tau) per vehicle — matches ODAS default_vehicle_attributes.ini
|
|
11
|
+
VEHICLE_ATTRIBUTES: dict[str, tuple[str, float]] = {
|
|
12
|
+
"vmp": ("down", 1.5),
|
|
13
|
+
"rvmp": ("up", 1.5),
|
|
14
|
+
"xmp": ("down", 1.5),
|
|
15
|
+
"argo_float": ("up", 60.0),
|
|
16
|
+
"slocum_glider": ("glide", 3.0),
|
|
17
|
+
"sea_glider": ("glide", 5.0),
|
|
18
|
+
"sea_explorer": ("glide", 3.0),
|
|
19
|
+
"auv": ("horizontal", 10.0),
|
|
20
|
+
"auv_emc": ("horizontal", 10.0),
|
|
21
|
+
"nemo": ("horizontal", 60.0),
|
|
22
|
+
"micro_squid": ("horizontal", 1.5),
|
|
23
|
+
"stand": ("horizontal", 1.5),
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
DEFAULT_DIRECTION = "down"
|
|
27
|
+
DEFAULT_TAU = 1.5
|
|
28
|
+
|
|
29
|
+
_EXPLICIT_DIRECTIONS = {"up", "down", "glide", "horizontal"}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def resolve_direction(direction: str, vehicle: str) -> str:
|
|
33
|
+
"""Resolve profile direction from user setting and vehicle type.
|
|
34
|
+
|
|
35
|
+
Parameters
|
|
36
|
+
----------
|
|
37
|
+
direction : str
|
|
38
|
+
User-specified direction. ``"auto"`` looks up *vehicle* in
|
|
39
|
+
:data:`VEHICLE_ATTRIBUTES`; any explicit value (``"up"``,
|
|
40
|
+
``"down"``, ``"glide"``, ``"horizontal"``) passes through unchanged.
|
|
41
|
+
vehicle : str
|
|
42
|
+
Vehicle identifier from the instrument config (lower-case).
|
|
43
|
+
|
|
44
|
+
Returns
|
|
45
|
+
-------
|
|
46
|
+
str
|
|
47
|
+
Resolved direction.
|
|
48
|
+
"""
|
|
49
|
+
if direction.lower() in _EXPLICIT_DIRECTIONS:
|
|
50
|
+
return direction.lower()
|
|
51
|
+
# "auto" or unrecognised → look up vehicle
|
|
52
|
+
attrs = VEHICLE_ATTRIBUTES.get(vehicle.lower())
|
|
53
|
+
if attrs is not None:
|
|
54
|
+
return attrs[0]
|
|
55
|
+
return DEFAULT_DIRECTION
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def resolve_tau(vehicle: str) -> float:
|
|
59
|
+
"""Return the smoothing time constant for a vehicle.
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
vehicle : str
|
|
64
|
+
Vehicle identifier (lower-case).
|
|
65
|
+
|
|
66
|
+
Returns
|
|
67
|
+
-------
|
|
68
|
+
float
|
|
69
|
+
Tau [s].
|
|
70
|
+
"""
|
|
71
|
+
attrs = VEHICLE_ATTRIBUTES.get(vehicle.lower())
|
|
72
|
+
if attrs is not None:
|
|
73
|
+
return attrs[1]
|
|
74
|
+
return DEFAULT_TAU
|