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.
Files changed (119) hide show
  1. microstructure_tpw-0.3.0.dist-info/METADATA +234 -0
  2. microstructure_tpw-0.3.0.dist-info/RECORD +119 -0
  3. microstructure_tpw-0.3.0.dist-info/WHEEL +5 -0
  4. microstructure_tpw-0.3.0.dist-info/entry_points.txt +7 -0
  5. microstructure_tpw-0.3.0.dist-info/licenses/LICENSE +674 -0
  6. microstructure_tpw-0.3.0.dist-info/top_level.txt +1 -0
  7. odas_tpw/__init__.py +18 -0
  8. odas_tpw/chi/__init__.py +48 -0
  9. odas_tpw/chi/batchelor.py +189 -0
  10. odas_tpw/chi/chi.py +857 -0
  11. odas_tpw/chi/fp07.py +649 -0
  12. odas_tpw/chi/l2_chi.py +153 -0
  13. odas_tpw/chi/l3_chi.py +388 -0
  14. odas_tpw/chi/l4_chi.py +391 -0
  15. odas_tpw/config_base.py +423 -0
  16. odas_tpw/perturb/__init__.py +18 -0
  17. odas_tpw/perturb/adcp.py +274 -0
  18. odas_tpw/perturb/atomic_io.py +47 -0
  19. odas_tpw/perturb/binning.py +761 -0
  20. odas_tpw/perturb/cli.py +591 -0
  21. odas_tpw/perturb/combo.py +346 -0
  22. odas_tpw/perturb/config.py +1046 -0
  23. odas_tpw/perturb/ctd.py +424 -0
  24. odas_tpw/perturb/diag/__init__.py +12 -0
  25. odas_tpw/perturb/diag/_common.py +140 -0
  26. odas_tpw/perturb/diag/chi.py +42 -0
  27. odas_tpw/perturb/diag/cli.py +67 -0
  28. odas_tpw/perturb/diag/data.py +662 -0
  29. odas_tpw/perturb/diag/epsilon.py +45 -0
  30. odas_tpw/perturb/diag/inspector.py +342 -0
  31. odas_tpw/perturb/diag/mixing.py +94 -0
  32. odas_tpw/perturb/diag/render.py +543 -0
  33. odas_tpw/perturb/discover.py +40 -0
  34. odas_tpw/perturb/fp07_cal.py +354 -0
  35. odas_tpw/perturb/gps.py +458 -0
  36. odas_tpw/perturb/hotel.py +572 -0
  37. odas_tpw/perturb/logging_setup.py +181 -0
  38. odas_tpw/perturb/merge.py +320 -0
  39. odas_tpw/perturb/mk_sections.py +310 -0
  40. odas_tpw/perturb/netcdf_schema.py +520 -0
  41. odas_tpw/perturb/pipeline.py +2654 -0
  42. odas_tpw/perturb/plot/__init__.py +15 -0
  43. odas_tpw/perturb/plot/cli.py +107 -0
  44. odas_tpw/perturb/plot/diagnostics.py +195 -0
  45. odas_tpw/perturb/plot/eps_chi.py +524 -0
  46. odas_tpw/perturb/plot/figure.py +679 -0
  47. odas_tpw/perturb/plot/gamma_scaling.py +1097 -0
  48. odas_tpw/perturb/plot/grid.py +112 -0
  49. odas_tpw/perturb/plot/layout.py +361 -0
  50. odas_tpw/perturb/plot/overview.py +547 -0
  51. odas_tpw/perturb/plot/presets/eps-chi.yaml +16 -0
  52. odas_tpw/perturb/plot/presets/profiles.yaml +32 -0
  53. odas_tpw/perturb/plot/presets/scalar.yaml +21 -0
  54. odas_tpw/perturb/plot/profiles.py +652 -0
  55. odas_tpw/perturb/plot/scalar.py +391 -0
  56. odas_tpw/perturb/plot/sections.py +506 -0
  57. odas_tpw/perturb/plot/xaxis.py +396 -0
  58. odas_tpw/perturb/qc_gate.py +177 -0
  59. odas_tpw/perturb/qc_rules.py +373 -0
  60. odas_tpw/perturb/resolve.py +306 -0
  61. odas_tpw/perturb/seawater.py +76 -0
  62. odas_tpw/perturb/speed.py +14 -0
  63. odas_tpw/perturb/trim.py +313 -0
  64. odas_tpw/processing/__init__.py +62 -0
  65. odas_tpw/processing/bottom.py +215 -0
  66. odas_tpw/processing/chi_combine.py +300 -0
  67. odas_tpw/processing/ct_align.py +172 -0
  68. odas_tpw/processing/epsilon_combine.py +210 -0
  69. odas_tpw/processing/mixing.py +630 -0
  70. odas_tpw/processing/thorpe.py +458 -0
  71. odas_tpw/processing/top_trim.py +274 -0
  72. odas_tpw/pyturb/__init__.py +3 -0
  73. odas_tpw/pyturb/__main__.py +5 -0
  74. odas_tpw/pyturb/_compat.py +204 -0
  75. odas_tpw/pyturb/_profind.py +124 -0
  76. odas_tpw/pyturb/bin.py +113 -0
  77. odas_tpw/pyturb/cli.py +175 -0
  78. odas_tpw/pyturb/eps.py +353 -0
  79. odas_tpw/pyturb/merge.py +101 -0
  80. odas_tpw/pyturb/p2nc.py +76 -0
  81. odas_tpw/rsi/__init__.py +31 -0
  82. odas_tpw/rsi/adapter.py +314 -0
  83. odas_tpw/rsi/bench.py +904 -0
  84. odas_tpw/rsi/binning.py +193 -0
  85. odas_tpw/rsi/channels.py +376 -0
  86. odas_tpw/rsi/chi_io.py +980 -0
  87. odas_tpw/rsi/cli.py +1411 -0
  88. odas_tpw/rsi/combine.py +120 -0
  89. odas_tpw/rsi/config.py +126 -0
  90. odas_tpw/rsi/config_patch.py +790 -0
  91. odas_tpw/rsi/convert.py +651 -0
  92. odas_tpw/rsi/deconvolve.py +119 -0
  93. odas_tpw/rsi/diss_look.py +343 -0
  94. odas_tpw/rsi/dissipation.py +669 -0
  95. odas_tpw/rsi/helpers.py +527 -0
  96. odas_tpw/rsi/mixing_look.py +509 -0
  97. odas_tpw/rsi/p_file.py +771 -0
  98. odas_tpw/rsi/pipeline.py +924 -0
  99. odas_tpw/rsi/profile.py +624 -0
  100. odas_tpw/rsi/quick_look.py +758 -0
  101. odas_tpw/rsi/sensor_inventory.py +684 -0
  102. odas_tpw/rsi/shear_noise.py +128 -0
  103. odas_tpw/rsi/speed.py +221 -0
  104. odas_tpw/rsi/vehicle.py +74 -0
  105. odas_tpw/rsi/viewer_base.py +1071 -0
  106. odas_tpw/rsi/window.py +459 -0
  107. odas_tpw/scor160/__init__.py +36 -0
  108. odas_tpw/scor160/cli.py +242 -0
  109. odas_tpw/scor160/compare.py +484 -0
  110. odas_tpw/scor160/despike.py +227 -0
  111. odas_tpw/scor160/goodman.py +288 -0
  112. odas_tpw/scor160/io.py +690 -0
  113. odas_tpw/scor160/l2.py +311 -0
  114. odas_tpw/scor160/l3.py +279 -0
  115. odas_tpw/scor160/l4.py +850 -0
  116. odas_tpw/scor160/nasmyth.py +208 -0
  117. odas_tpw/scor160/ocean.py +287 -0
  118. odas_tpw/scor160/profile.py +231 -0
  119. 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)
@@ -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