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
odas_tpw/chi/fp07.py
ADDED
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
# Mar-2026, Claude and Pat Welch, pat@mousebrains.com
|
|
2
|
+
"""FP07 thermistor transfer function and electronics noise model.
|
|
3
|
+
|
|
4
|
+
References
|
|
5
|
+
----------
|
|
6
|
+
Lueck, R.G., O. Hertzman, and T.R. Osborn, 1977: The spectral response of
|
|
7
|
+
thermistors. Deep-Sea Res., 24, 951-970.
|
|
8
|
+
Gregg, M.C. and T.B. Meagher, 1980: The dynamic response of glass rod
|
|
9
|
+
thermistors. J. Geophys. Res., 85, 2779-2786.
|
|
10
|
+
Peterson, A.K. and I. Fer, 2014: Dissipation measurements using temperature
|
|
11
|
+
microstructure from an underwater glider. Methods in Oceanography, 10, 44-69.
|
|
12
|
+
RSI Technical Note 040: Noise in Temperature Gradient Measurements.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import warnings
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
import numpy as np
|
|
20
|
+
import numpy.typing as npt
|
|
21
|
+
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
# FP07 transfer functions
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def fp07_transfer(f: npt.ArrayLike, tau0: float) -> np.ndarray:
|
|
28
|
+
"""Single-pole FP07 transfer function |H(f)|^2.
|
|
29
|
+
|
|
30
|
+
H^2 = 1 / (1 + (2*pi*f*tau0)^2)
|
|
31
|
+
|
|
32
|
+
Lueck et al. 1977.
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
f : array_like
|
|
37
|
+
Frequency [Hz].
|
|
38
|
+
tau0 : float
|
|
39
|
+
Time constant [s].
|
|
40
|
+
|
|
41
|
+
Returns
|
|
42
|
+
-------
|
|
43
|
+
H2 : ndarray
|
|
44
|
+
Squared magnitude of transfer function.
|
|
45
|
+
"""
|
|
46
|
+
f = np.asarray(f, dtype=np.float64)
|
|
47
|
+
return 1.0 / (1.0 + (2 * np.pi * f * tau0) ** 2)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def fp07_double_pole(f: npt.ArrayLike, tau0: float) -> np.ndarray:
|
|
51
|
+
"""Double-pole FP07 transfer function |H(f)|^2.
|
|
52
|
+
|
|
53
|
+
H^2 = 1 / (1 + (2*pi*f*tau0)^2)^2
|
|
54
|
+
|
|
55
|
+
Gregg & Meagher 1980. Accounts for glass bead thermal mass
|
|
56
|
+
plus thermal boundary layer.
|
|
57
|
+
|
|
58
|
+
Parameters
|
|
59
|
+
----------
|
|
60
|
+
f : array_like
|
|
61
|
+
Frequency [Hz].
|
|
62
|
+
tau0 : float
|
|
63
|
+
Time constant [s].
|
|
64
|
+
|
|
65
|
+
Returns
|
|
66
|
+
-------
|
|
67
|
+
H2 : ndarray
|
|
68
|
+
"""
|
|
69
|
+
f = np.asarray(f, dtype=np.float64)
|
|
70
|
+
return 1.0 / (1.0 + (2 * np.pi * f * tau0) ** 2) ** 2
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def default_tau_model(fp07_model: str) -> str:
|
|
74
|
+
"""Auto-select FP07 tau model based on transfer function model.
|
|
75
|
+
|
|
76
|
+
``double_pole`` pairs with 'goto' (fixed tau = 3 ms, Goto et al.
|
|
77
|
+
2016); ``single_pole`` pairs with 'lueck' (speed-dependent). Note
|
|
78
|
+
that Peterson & Fer (2014) used a SINGLE-pole response,
|
|
79
|
+
H^2(f) = (1 + (f/fc)^2)^-1, with a speed-dependent cut-off, so
|
|
80
|
+
reproducing them means the ``single_pole`` transfer with the
|
|
81
|
+
'peterson' tau (not the default 'lueck') — see the :func:`fp07_tau`
|
|
82
|
+
notes for how to do that explicitly.
|
|
83
|
+
"""
|
|
84
|
+
# Defect: an unrecognized fp07_model previously fell through to 'lueck'
|
|
85
|
+
# silently, pairing a single-pole tau with a double-pole transfer on a typo.
|
|
86
|
+
if fp07_model == "double_pole":
|
|
87
|
+
return "goto"
|
|
88
|
+
elif fp07_model == "single_pole":
|
|
89
|
+
return "lueck"
|
|
90
|
+
raise ValueError(f"Unknown FP07 model: {fp07_model!r}")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def fp07_tau(speed: npt.ArrayLike, model: str = "lueck") -> np.ndarray | float:
|
|
94
|
+
"""Speed-dependent FP07 time constant [s].
|
|
95
|
+
|
|
96
|
+
Handles both scalar and array inputs via broadcasting.
|
|
97
|
+
|
|
98
|
+
Parameters
|
|
99
|
+
----------
|
|
100
|
+
speed : float or array_like
|
|
101
|
+
Profiling speed [m/s].
|
|
102
|
+
model : str
|
|
103
|
+
'lueck' : tau = 0.01 * (1.0/speed)^0.5 (Lueck et al. 1977)
|
|
104
|
+
'peterson' : tau = 0.012 * speed^(-0.32) — the speed-dependent
|
|
105
|
+
response adopted by Peterson & Fer (2014,
|
|
106
|
+
doi:10.1016/j.mio.2014.05.002), in the tradition of
|
|
107
|
+
Vachon & Lueck (1984) and Gregg & Meagher (1980)
|
|
108
|
+
'goto' : tau = 0.003, speed-independent (Goto et al. 2016,
|
|
109
|
+
doi:10.1175/JTECH-D-15-0220.1, for their double-pole model)
|
|
110
|
+
|
|
111
|
+
Notes
|
|
112
|
+
-----
|
|
113
|
+
The pipeline pairs tau models with transfer-function models via
|
|
114
|
+
:func:`default_tau_model`: ``fp07_model='single_pole'`` uses 'lueck'
|
|
115
|
+
and ``'double_pole'`` uses 'goto'. Be aware that Peterson & Fer
|
|
116
|
+
(2014) used a SINGLE-pole response, H^2(f) = (1 + (f/fc)^2)^-1, with
|
|
117
|
+
a speed-dependent cut-off fc = (2*pi*tau*U^-0.32)^-1, tau = 12 ms
|
|
118
|
+
(their double-pole tau = 8 ms is only a Fig. 15 sensitivity test).
|
|
119
|
+
The default 'lueck' tau therefore does NOT reproduce them. To
|
|
120
|
+
reproduce Peterson & Fer exactly, call the single-pole transfer with
|
|
121
|
+
``fp07_transfer`` and ``fp07_tau(speed, model='peterson')``.
|
|
122
|
+
|
|
123
|
+
Returns
|
|
124
|
+
-------
|
|
125
|
+
tau0 : float or ndarray
|
|
126
|
+
Time constant [s].
|
|
127
|
+
"""
|
|
128
|
+
is_scalar = np.ndim(speed) == 0
|
|
129
|
+
speed = np.asarray(speed, dtype=np.float64)
|
|
130
|
+
# Defect: MATLAB gradT_noise_odas.m validates speed > 0; without it a
|
|
131
|
+
# non-positive speed yields NaN (lueck/peterson) or inf downstream.
|
|
132
|
+
if np.any(speed <= 0):
|
|
133
|
+
raise ValueError(f"FP07 speed must be > 0, got min {np.min(speed)!r}")
|
|
134
|
+
if model == "lueck":
|
|
135
|
+
result = 0.01 * (1.0 / speed) ** 0.5
|
|
136
|
+
elif model == "peterson":
|
|
137
|
+
result = 0.012 * speed ** (-0.32)
|
|
138
|
+
elif model == "goto":
|
|
139
|
+
result = np.full_like(speed, 0.003) if speed.ndim > 0 else 0.003
|
|
140
|
+
else:
|
|
141
|
+
raise ValueError(f"Unknown FP07 tau model: {model!r}")
|
|
142
|
+
return float(result) if is_scalar else result
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def fp07_tau_batch(speeds: npt.ArrayLike, model: str = "lueck") -> np.ndarray:
|
|
146
|
+
"""Vectorized FP07 time constants for multiple speeds.
|
|
147
|
+
|
|
148
|
+
Thin wrapper around :func:`fp07_tau` ensuring array output.
|
|
149
|
+
|
|
150
|
+
Parameters
|
|
151
|
+
----------
|
|
152
|
+
speeds : array_like
|
|
153
|
+
Profiling speeds [m/s], shape ``(n,)``.
|
|
154
|
+
model : str
|
|
155
|
+
Same as :func:`fp07_tau`.
|
|
156
|
+
|
|
157
|
+
Returns
|
|
158
|
+
-------
|
|
159
|
+
tau0 : ndarray, shape ``(n,)``
|
|
160
|
+
"""
|
|
161
|
+
return np.atleast_1d(fp07_tau(speeds, model))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def fp07_transfer_batch(
|
|
165
|
+
F: npt.ArrayLike,
|
|
166
|
+
tau0: npt.ArrayLike,
|
|
167
|
+
model: str = "single_pole",
|
|
168
|
+
) -> np.ndarray:
|
|
169
|
+
"""Vectorized FP07 |H(f)|^2 for multiple time constants.
|
|
170
|
+
|
|
171
|
+
Parameters
|
|
172
|
+
----------
|
|
173
|
+
F : array_like, shape ``(n_freq,)``
|
|
174
|
+
Frequency vector [Hz].
|
|
175
|
+
tau0 : array_like, shape ``(n_est,)``
|
|
176
|
+
Per-window time constants [s].
|
|
177
|
+
model : str
|
|
178
|
+
'single_pole' or 'double_pole'.
|
|
179
|
+
|
|
180
|
+
Returns
|
|
181
|
+
-------
|
|
182
|
+
H2 : ndarray, shape ``(n_est, n_freq)``
|
|
183
|
+
"""
|
|
184
|
+
F = np.asarray(F, dtype=np.float64)
|
|
185
|
+
tau0 = np.asarray(tau0, dtype=np.float64)
|
|
186
|
+
omega_tau_sq = (2 * np.pi * F[:, np.newaxis] * tau0[np.newaxis, :]) ** 2
|
|
187
|
+
# Defect: an unrecognized model string silently fell through to
|
|
188
|
+
# double_pole, mixing physics on a config typo. Validate like fp07_tau.
|
|
189
|
+
if model == "single_pole":
|
|
190
|
+
return (1.0 / (1.0 + omega_tau_sq)).T
|
|
191
|
+
elif model == "double_pole":
|
|
192
|
+
return (1.0 / (1.0 + omega_tau_sq) ** 2).T
|
|
193
|
+
else:
|
|
194
|
+
raise ValueError(f"Unknown FP07 model: {model!r}")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# ---------------------------------------------------------------------------
|
|
198
|
+
# Electronics noise model — ports noise_thermchannel.m + gradT_noise_odas.m
|
|
199
|
+
# ---------------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@dataclass
|
|
203
|
+
class FP07NoiseConfig:
|
|
204
|
+
"""Hardware configuration for FP07 electronics noise model.
|
|
205
|
+
|
|
206
|
+
Consolidates the 17 hardware parameters used by ``noise_thermchannel``
|
|
207
|
+
with RSI defaults. Pass an instance as the ``config`` argument to
|
|
208
|
+
override any subset of defaults.
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
R_0: float = 3000 # nominal thermistor resistance [Ohm]
|
|
212
|
+
gain: float = 6 # first-stage amplifier gain
|
|
213
|
+
f_AA: float = 110 # anti-aliasing filter cutoff [Hz]
|
|
214
|
+
adc_fs: float = 4.096 # ADC full-scale voltage [V]
|
|
215
|
+
adc_bits: int = 16 # ADC resolution [bits]
|
|
216
|
+
E_n: float = 4e-9 # first-stage voltage noise [V/sqrt(Hz)]
|
|
217
|
+
fc: float = 18.7 # first-stage flicker noise knee [Hz]
|
|
218
|
+
E_n2: float = 8e-9 # second-stage voltage noise [V/sqrt(Hz)]
|
|
219
|
+
fc_2: float = 42 # second-stage flicker noise knee [Hz]
|
|
220
|
+
gamma_RSI: float = 3 # ADC noise factor
|
|
221
|
+
T_K: float = 295 # operating temperature [K]
|
|
222
|
+
K_B: float = 1.382e-23 # Boltzmann constant [J/K]
|
|
223
|
+
e_b: float = 0.68 # bridge excitation voltage [V]
|
|
224
|
+
b: float = 1.0 # Steinhart-Hart 'b' coefficient
|
|
225
|
+
beta_1: float = 3000.0 # Steinhart-Hart beta_1 [K]
|
|
226
|
+
T_0: float = 289.3 # Steinhart-Hart reference temperature [K]
|
|
227
|
+
beta_2: float | None = None # Steinhart-Hart beta_2 [K]
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _unpack_noise_config(
|
|
231
|
+
config: FP07NoiseConfig | None,
|
|
232
|
+
*,
|
|
233
|
+
R_0: float = 3000,
|
|
234
|
+
gain: float = 6,
|
|
235
|
+
f_AA: float = 110,
|
|
236
|
+
adc_fs: float = 4.096,
|
|
237
|
+
adc_bits: int = 16,
|
|
238
|
+
E_n: float = 4e-9,
|
|
239
|
+
fc: float = 18.7,
|
|
240
|
+
E_n2: float = 8e-9,
|
|
241
|
+
fc_2: float = 42,
|
|
242
|
+
gamma_RSI: float = 3,
|
|
243
|
+
T_K: float = 295,
|
|
244
|
+
K_B: float = 1.382e-23,
|
|
245
|
+
e_b: float = 0.68,
|
|
246
|
+
b: float = 1.0,
|
|
247
|
+
beta_1: float = 3000.0,
|
|
248
|
+
T_0: float = 289.3,
|
|
249
|
+
beta_2: float | None = None,
|
|
250
|
+
) -> dict:
|
|
251
|
+
"""Resolve hardware parameters from config dataclass or kwargs."""
|
|
252
|
+
if config is not None:
|
|
253
|
+
return {
|
|
254
|
+
"R_0": config.R_0,
|
|
255
|
+
"gain": config.gain,
|
|
256
|
+
"f_AA": config.f_AA,
|
|
257
|
+
"adc_fs": config.adc_fs,
|
|
258
|
+
"adc_bits": config.adc_bits,
|
|
259
|
+
"E_n": config.E_n,
|
|
260
|
+
"fc": config.fc,
|
|
261
|
+
"E_n2": config.E_n2,
|
|
262
|
+
"fc_2": config.fc_2,
|
|
263
|
+
"gamma_RSI": config.gamma_RSI,
|
|
264
|
+
"T_K": config.T_K,
|
|
265
|
+
"K_B": config.K_B,
|
|
266
|
+
"e_b": config.e_b,
|
|
267
|
+
"b": config.b,
|
|
268
|
+
"beta_1": config.beta_1,
|
|
269
|
+
"T_0": config.T_0,
|
|
270
|
+
"beta_2": config.beta_2,
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
"R_0": R_0,
|
|
274
|
+
"gain": gain,
|
|
275
|
+
"f_AA": f_AA,
|
|
276
|
+
"adc_fs": adc_fs,
|
|
277
|
+
"adc_bits": adc_bits,
|
|
278
|
+
"E_n": E_n,
|
|
279
|
+
"fc": fc,
|
|
280
|
+
"E_n2": E_n2,
|
|
281
|
+
"fc_2": fc_2,
|
|
282
|
+
"gamma_RSI": gamma_RSI,
|
|
283
|
+
"T_K": T_K,
|
|
284
|
+
"K_B": K_B,
|
|
285
|
+
"e_b": e_b,
|
|
286
|
+
"b": b,
|
|
287
|
+
"beta_1": beta_1,
|
|
288
|
+
"T_0": T_0,
|
|
289
|
+
"beta_2": beta_2,
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _noise_f_intermediates(
|
|
294
|
+
F: np.ndarray,
|
|
295
|
+
fs: float,
|
|
296
|
+
diff_gain: float,
|
|
297
|
+
p: dict,
|
|
298
|
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray, float]:
|
|
299
|
+
"""Compute F-dependent noise intermediates shared between scalar and batch.
|
|
300
|
+
|
|
301
|
+
Returns (base_f, johnson_gain_f, counts_factor, delta_s).
|
|
302
|
+
"""
|
|
303
|
+
delta_s = p["adc_fs"] / 2 ** p["adc_bits"]
|
|
304
|
+
fN = fs / 2
|
|
305
|
+
|
|
306
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
307
|
+
V1 = 2 * p["E_n"] ** 2 * np.sqrt(1 + (F / p["fc"]) ** 2) / (F / p["fc"])
|
|
308
|
+
V1 = np.where(np.isfinite(V1), V1, V1[np.isfinite(V1)].max() if np.any(np.isfinite(V1)) else 0)
|
|
309
|
+
|
|
310
|
+
G_2 = 1 + (2 * np.pi * diff_gain * F) ** 2
|
|
311
|
+
|
|
312
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
313
|
+
V2 = 2 * p["E_n2"] ** 2 * np.sqrt(1 + (F / p["fc_2"]) ** 2) / (F / p["fc_2"])
|
|
314
|
+
V2 = np.where(np.isfinite(V2), V2, V2[np.isfinite(V2)].max() if np.any(np.isfinite(V2)) else 0)
|
|
315
|
+
|
|
316
|
+
G_AA = 1.0 / (1 + (F / p["f_AA"]) ** 8) ** 2
|
|
317
|
+
adc_floor = p["gamma_RSI"] * delta_s**2 / (12 * fN)
|
|
318
|
+
|
|
319
|
+
w = 2 * np.pi * diff_gain * F
|
|
320
|
+
G_HP = (1 / diff_gain) ** 2 * w**2 / (1 + w**2)
|
|
321
|
+
|
|
322
|
+
base_f = G_AA * G_2 * (p["gain"] ** 2 * V1 + V2) + adc_floor
|
|
323
|
+
johnson_gain_f = G_AA * G_2 * p["gain"] ** 2
|
|
324
|
+
counts_factor = G_HP / delta_s**2
|
|
325
|
+
|
|
326
|
+
return base_f, johnson_gain_f, counts_factor, delta_s
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def noise_thermchannel(
|
|
330
|
+
F: npt.ArrayLike,
|
|
331
|
+
T_mean: float,
|
|
332
|
+
fs: float = 512,
|
|
333
|
+
diff_gain: float = 0.94,
|
|
334
|
+
R_0: float = 3000,
|
|
335
|
+
gain: float = 6,
|
|
336
|
+
f_AA: float = 110,
|
|
337
|
+
adc_fs: float = 4.096,
|
|
338
|
+
adc_bits: int = 16,
|
|
339
|
+
E_n: float = 4e-9,
|
|
340
|
+
fc: float = 18.7,
|
|
341
|
+
E_n2: float = 8e-9,
|
|
342
|
+
fc_2: float = 42,
|
|
343
|
+
gamma_RSI: float = 3,
|
|
344
|
+
T_K: float = 295,
|
|
345
|
+
K_B: float = 1.382e-23,
|
|
346
|
+
e_b: float = 0.68,
|
|
347
|
+
b: float = 1.0,
|
|
348
|
+
beta_1: float = 3000.0,
|
|
349
|
+
T_0: float = 289.3,
|
|
350
|
+
beta_2: float | None = None,
|
|
351
|
+
config: FP07NoiseConfig | None = None,
|
|
352
|
+
) -> np.ndarray:
|
|
353
|
+
"""Electronics noise spectrum for FP07 thermistor [(K/s)^2 / Hz].
|
|
354
|
+
|
|
355
|
+
Returns the noise of the temperature *time derivative* dT/dt. Use
|
|
356
|
+
:func:`gradT_noise` to convert to a spatial-gradient spectrum
|
|
357
|
+
[(K/m)^2 / cpm] using the profiling speed.
|
|
358
|
+
|
|
359
|
+
Computes the noise spectrum including:
|
|
360
|
+
- Johnson noise from thermistor resistance
|
|
361
|
+
- Amplifier voltage noise (two stages with 1/f knee)
|
|
362
|
+
- Pre-emphasis gain
|
|
363
|
+
- Anti-aliasing filter (two-stage 4th-order Butterworth)
|
|
364
|
+
- ADC quantization noise
|
|
365
|
+
- Conversion to physical gradient units via scale factor
|
|
366
|
+
|
|
367
|
+
Ports noise_thermchannel.m and gradT_noise_odas.m from ODAS.
|
|
368
|
+
|
|
369
|
+
Parameters
|
|
370
|
+
----------
|
|
371
|
+
F : array_like
|
|
372
|
+
Frequency vector [Hz].
|
|
373
|
+
T_mean : float
|
|
374
|
+
Mean temperature [deg C] for computing scale factor.
|
|
375
|
+
fs : float
|
|
376
|
+
Sampling rate [Hz].
|
|
377
|
+
diff_gain : float
|
|
378
|
+
Pre-emphasis differentiator gain [s].
|
|
379
|
+
R_0 : float
|
|
380
|
+
Nominal thermistor resistance [Ohm] at operating temperature.
|
|
381
|
+
gain : float
|
|
382
|
+
First-stage amplifier gain.
|
|
383
|
+
f_AA : float
|
|
384
|
+
Anti-aliasing filter cutoff [Hz] (each stage).
|
|
385
|
+
adc_fs : float
|
|
386
|
+
ADC full-scale voltage [V].
|
|
387
|
+
adc_bits : int
|
|
388
|
+
ADC resolution [bits].
|
|
389
|
+
E_n : float
|
|
390
|
+
First-stage amplifier voltage noise [V/sqrt(Hz)].
|
|
391
|
+
fc : float
|
|
392
|
+
First-stage flicker noise knee [Hz].
|
|
393
|
+
E_n2 : float
|
|
394
|
+
Second-stage amplifier voltage noise [V/sqrt(Hz)].
|
|
395
|
+
fc_2 : float
|
|
396
|
+
Second-stage flicker noise knee [Hz].
|
|
397
|
+
gamma_RSI : float
|
|
398
|
+
ADC noise factor (RSI sampler excess).
|
|
399
|
+
T_K : float
|
|
400
|
+
Operating temperature [K].
|
|
401
|
+
K_B : float
|
|
402
|
+
Boltzmann constant [J/K].
|
|
403
|
+
e_b : float
|
|
404
|
+
Bridge excitation voltage [V].
|
|
405
|
+
b : float
|
|
406
|
+
Steinhart-Hart 'b' coefficient.
|
|
407
|
+
beta_1 : float
|
|
408
|
+
Steinhart-Hart beta_1 coefficient [K].
|
|
409
|
+
T_0 : float
|
|
410
|
+
Steinhart-Hart reference temperature [K]. Default 289.3 (~16 deg C).
|
|
411
|
+
beta_2 : float or None
|
|
412
|
+
Steinhart-Hart beta_2 coefficient [K]. When finite, applies
|
|
413
|
+
scale factor correction (MATLAB gradT_noise_odas.m l.335-338).
|
|
414
|
+
|
|
415
|
+
config : FP07NoiseConfig or None
|
|
416
|
+
Hardware configuration dataclass. When provided, its fields
|
|
417
|
+
override the corresponding keyword arguments.
|
|
418
|
+
|
|
419
|
+
Returns
|
|
420
|
+
-------
|
|
421
|
+
noise_f : ndarray
|
|
422
|
+
Temperature time-derivative noise spectrum [(K/s)^2 / Hz].
|
|
423
|
+
"""
|
|
424
|
+
p = _unpack_noise_config(
|
|
425
|
+
config,
|
|
426
|
+
R_0=R_0,
|
|
427
|
+
gain=gain,
|
|
428
|
+
f_AA=f_AA,
|
|
429
|
+
adc_fs=adc_fs,
|
|
430
|
+
adc_bits=adc_bits,
|
|
431
|
+
E_n=E_n,
|
|
432
|
+
fc=fc,
|
|
433
|
+
E_n2=E_n2,
|
|
434
|
+
fc_2=fc_2,
|
|
435
|
+
gamma_RSI=gamma_RSI,
|
|
436
|
+
T_K=T_K,
|
|
437
|
+
K_B=K_B,
|
|
438
|
+
e_b=e_b,
|
|
439
|
+
b=b,
|
|
440
|
+
beta_1=beta_1,
|
|
441
|
+
T_0=T_0,
|
|
442
|
+
beta_2=beta_2,
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
F = np.asarray(F, dtype=np.float64)
|
|
446
|
+
base_f, johnson_gain_f, counts_factor, _delta_s = _noise_f_intermediates(F, fs, diff_gain, p)
|
|
447
|
+
|
|
448
|
+
# T-dependent terms
|
|
449
|
+
T_kelvin = T_mean + 273.15
|
|
450
|
+
R_ratio = np.exp(p["beta_1"] * (1.0 / T_kelvin - 1.0 / p["T_0"]))
|
|
451
|
+
if R_ratio < 0.1:
|
|
452
|
+
warnings.warn(
|
|
453
|
+
f"R_ratio={R_ratio:.4g} < 0.1 (possible broken thermistor at T={T_mean:.1f}°C); "
|
|
454
|
+
"clamped to 1.0",
|
|
455
|
+
stacklevel=2,
|
|
456
|
+
)
|
|
457
|
+
R_ratio = 1.0
|
|
458
|
+
R_actual = R_ratio * p["R_0"]
|
|
459
|
+
phi_R = 4 * p["K_B"] * R_actual * p["T_K"]
|
|
460
|
+
|
|
461
|
+
# Scale factor
|
|
462
|
+
eta = (p["b"] / 2) * 2 ** p["adc_bits"] * p["gain"] * p["e_b"] / p["adc_fs"]
|
|
463
|
+
scale_factor = T_kelvin**2 * (1 + R_ratio) ** 2 / (2 * eta * p["beta_1"] * R_ratio)
|
|
464
|
+
if p["beta_2"] is not None and np.isfinite(p["beta_2"]):
|
|
465
|
+
scale_factor *= 1 + 2 * (p["beta_1"] / p["beta_2"]) * np.log(R_ratio)
|
|
466
|
+
|
|
467
|
+
Noise_counts = (base_f + johnson_gain_f * phi_R) * counts_factor
|
|
468
|
+
return Noise_counts * scale_factor**2
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def gradT_noise(
|
|
472
|
+
F: npt.ArrayLike,
|
|
473
|
+
T_mean: float,
|
|
474
|
+
speed: float,
|
|
475
|
+
fs: float = 512,
|
|
476
|
+
diff_gain: float = 0.94,
|
|
477
|
+
**kwargs: Any,
|
|
478
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
479
|
+
"""Temperature gradient noise spectrum [(K/m)^2 / cpm].
|
|
480
|
+
|
|
481
|
+
Convenience wrapper that converts noise_thermchannel output to
|
|
482
|
+
wavenumber units using profiling speed.
|
|
483
|
+
|
|
484
|
+
Parameters
|
|
485
|
+
----------
|
|
486
|
+
F : array_like
|
|
487
|
+
Frequency vector [Hz].
|
|
488
|
+
T_mean : float
|
|
489
|
+
Mean temperature [deg C].
|
|
490
|
+
speed : float
|
|
491
|
+
Profiling speed [m/s].
|
|
492
|
+
fs : float
|
|
493
|
+
Sampling rate [Hz].
|
|
494
|
+
diff_gain : float
|
|
495
|
+
Pre-emphasis differentiator gain [s].
|
|
496
|
+
**kwargs
|
|
497
|
+
Additional parameters passed to noise_thermchannel.
|
|
498
|
+
|
|
499
|
+
Returns
|
|
500
|
+
-------
|
|
501
|
+
noise_K : ndarray
|
|
502
|
+
Noise spectrum [(K/m)^2 / cpm].
|
|
503
|
+
K : ndarray
|
|
504
|
+
Wavenumber vector [cpm].
|
|
505
|
+
"""
|
|
506
|
+
# Defect: a non-positive speed gives a negative noise PSD (impossible) or
|
|
507
|
+
# inf; MATLAB gradT_noise_odas.m validates speed > 0 (val_speed).
|
|
508
|
+
if not (np.isfinite(speed) and speed > 0):
|
|
509
|
+
raise ValueError(f"FP07 speed must be > 0, got {speed!r}")
|
|
510
|
+
noise_f = noise_thermchannel(F, T_mean, fs=fs, diff_gain=diff_gain, **kwargs)
|
|
511
|
+
F_arr = np.asarray(F, dtype=np.float64)
|
|
512
|
+
K = F_arr / speed
|
|
513
|
+
# noise_f is the time-derivative noise [(K/s)^2/Hz]. Converting to a
|
|
514
|
+
# gradient spectrum divides by speed^2 (dT/dz = (dT/dt)/W), and the
|
|
515
|
+
# per-Hz -> per-cpm change of variable multiplies by speed: net /speed.
|
|
516
|
+
# Matches ODAS gradT_noise_odas.m ("scale_factor ./ speed", squared).
|
|
517
|
+
noise_K = noise_f / speed
|
|
518
|
+
return noise_K, K
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def noise_thermchannel_batch(
|
|
522
|
+
F: npt.ArrayLike,
|
|
523
|
+
T_means: npt.ArrayLike,
|
|
524
|
+
fs: float = 512,
|
|
525
|
+
diff_gain: float = 0.94,
|
|
526
|
+
R_0: float = 3000,
|
|
527
|
+
gain: float = 6,
|
|
528
|
+
f_AA: float = 110,
|
|
529
|
+
adc_fs: float = 4.096,
|
|
530
|
+
adc_bits: int = 16,
|
|
531
|
+
E_n: float = 4e-9,
|
|
532
|
+
fc: float = 18.7,
|
|
533
|
+
E_n2: float = 8e-9,
|
|
534
|
+
fc_2: float = 42,
|
|
535
|
+
gamma_RSI: float = 3,
|
|
536
|
+
T_K: float = 295,
|
|
537
|
+
K_B: float = 1.382e-23,
|
|
538
|
+
e_b: float = 0.68,
|
|
539
|
+
b: float = 1.0,
|
|
540
|
+
beta_1: float = 3000.0,
|
|
541
|
+
T_0: float = 289.3,
|
|
542
|
+
beta_2: float | None = None,
|
|
543
|
+
config: FP07NoiseConfig | None = None,
|
|
544
|
+
) -> np.ndarray:
|
|
545
|
+
"""Vectorized electronics noise for multiple T_mean values.
|
|
546
|
+
|
|
547
|
+
Same physics as :func:`noise_thermchannel` but computes F-dependent
|
|
548
|
+
intermediates once and broadcasts T_means through T-dependent terms
|
|
549
|
+
(Johnson noise and scale factor).
|
|
550
|
+
|
|
551
|
+
Parameters
|
|
552
|
+
----------
|
|
553
|
+
F : array_like
|
|
554
|
+
Frequency vector [Hz], shape ``(n_freq,)``.
|
|
555
|
+
T_means : array_like
|
|
556
|
+
Mean temperatures [deg C], shape ``(n_est,)``.
|
|
557
|
+
Other parameters same as :func:`noise_thermchannel`.
|
|
558
|
+
|
|
559
|
+
Returns
|
|
560
|
+
-------
|
|
561
|
+
noise_f : ndarray
|
|
562
|
+
Temperature time-derivative noise [(K/s)^2 / Hz],
|
|
563
|
+
shape ``(n_est, n_freq)``.
|
|
564
|
+
"""
|
|
565
|
+
p = _unpack_noise_config(
|
|
566
|
+
config,
|
|
567
|
+
R_0=R_0,
|
|
568
|
+
gain=gain,
|
|
569
|
+
f_AA=f_AA,
|
|
570
|
+
adc_fs=adc_fs,
|
|
571
|
+
adc_bits=adc_bits,
|
|
572
|
+
E_n=E_n,
|
|
573
|
+
fc=fc,
|
|
574
|
+
E_n2=E_n2,
|
|
575
|
+
fc_2=fc_2,
|
|
576
|
+
gamma_RSI=gamma_RSI,
|
|
577
|
+
T_K=T_K,
|
|
578
|
+
K_B=K_B,
|
|
579
|
+
e_b=e_b,
|
|
580
|
+
b=b,
|
|
581
|
+
beta_1=beta_1,
|
|
582
|
+
T_0=T_0,
|
|
583
|
+
beta_2=beta_2,
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
F = np.asarray(F, dtype=np.float64)
|
|
587
|
+
T_means = np.asarray(T_means, dtype=np.float64)
|
|
588
|
+
|
|
589
|
+
base_f, johnson_gain_f, counts_factor, _delta_s = _noise_f_intermediates(F, fs, diff_gain, p)
|
|
590
|
+
|
|
591
|
+
# --- T-dependent terms ---
|
|
592
|
+
T_kelvin = T_means + 273.15 # (n_est,)
|
|
593
|
+
R_ratio = np.exp(p["beta_1"] * (1.0 / T_kelvin - 1.0 / p["T_0"])) # (n_est,)
|
|
594
|
+
# Defect: the batch path clamped R_ratio < 0.1 silently while the scalar
|
|
595
|
+
# noise_thermchannel warns; surface the broken-thermistor condition here too.
|
|
596
|
+
n_clamped = int(np.count_nonzero(R_ratio < 0.1))
|
|
597
|
+
if n_clamped:
|
|
598
|
+
warnings.warn(
|
|
599
|
+
f"R_ratio < 0.1 for {n_clamped} of {R_ratio.size} T_means "
|
|
600
|
+
"(possible broken thermistor); clamped to 1.0",
|
|
601
|
+
stacklevel=2,
|
|
602
|
+
)
|
|
603
|
+
R_ratio = np.where(R_ratio < 0.1, 1.0, R_ratio)
|
|
604
|
+
R_actual = R_ratio * p["R_0"]
|
|
605
|
+
phi_R = 4 * p["K_B"] * R_actual * p["T_K"] # (n_est,)
|
|
606
|
+
|
|
607
|
+
# Scale factor
|
|
608
|
+
eta = (p["b"] / 2) * 2 ** p["adc_bits"] * p["gain"] * p["e_b"] / p["adc_fs"]
|
|
609
|
+
scale_factor = T_kelvin**2 * (1 + R_ratio) ** 2 / (2 * eta * p["beta_1"] * R_ratio)
|
|
610
|
+
if p["beta_2"] is not None and np.isfinite(p["beta_2"]):
|
|
611
|
+
scale_factor = scale_factor * (1 + 2 * (p["beta_1"] / p["beta_2"]) * np.log(R_ratio))
|
|
612
|
+
|
|
613
|
+
Noise_counts = (
|
|
614
|
+
base_f[np.newaxis, :] + johnson_gain_f[np.newaxis, :] * phi_R[:, np.newaxis]
|
|
615
|
+
) * counts_factor[np.newaxis, :]
|
|
616
|
+
|
|
617
|
+
return Noise_counts * (scale_factor**2)[:, np.newaxis]
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def gradT_noise_batch(
|
|
621
|
+
F: npt.ArrayLike,
|
|
622
|
+
T_means: npt.ArrayLike,
|
|
623
|
+
speeds: npt.ArrayLike,
|
|
624
|
+
fs: float = 512,
|
|
625
|
+
diff_gain: float = 0.94,
|
|
626
|
+
**kwargs: Any,
|
|
627
|
+
) -> np.ndarray:
|
|
628
|
+
"""Batch temperature gradient noise in wavenumber units.
|
|
629
|
+
|
|
630
|
+
Parameters
|
|
631
|
+
----------
|
|
632
|
+
F : shape ``(n_freq,)``
|
|
633
|
+
T_means : shape ``(n_est,)``
|
|
634
|
+
speeds : shape ``(n_est,)``
|
|
635
|
+
|
|
636
|
+
Returns
|
|
637
|
+
-------
|
|
638
|
+
noise_K : ndarray, shape ``(n_est, n_freq)``
|
|
639
|
+
Noise spectrum [(K/m)^2 / cpm].
|
|
640
|
+
"""
|
|
641
|
+
speeds = np.asarray(speeds, dtype=np.float64)
|
|
642
|
+
# Defect: non-positive speeds give a negative/inf noise PSD; MATLAB
|
|
643
|
+
# gradT_noise_odas.m validates speed > 0 (val_speed).
|
|
644
|
+
if not np.all(np.isfinite(speeds) & (speeds > 0)):
|
|
645
|
+
raise ValueError(f"FP07 speeds must all be > 0, got min {np.min(speeds)!r}")
|
|
646
|
+
noise_f = noise_thermchannel_batch(F, T_means, fs=fs, diff_gain=diff_gain, **kwargs)
|
|
647
|
+
# Time-derivative noise -> gradient noise per cpm: /speed^2 * speed
|
|
648
|
+
# (see gradT_noise).
|
|
649
|
+
return noise_f / speeds[:, np.newaxis]
|