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/rsi/combine.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Combine binned profiles into a single dataset (L6)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import xarray as xr
|
|
7
|
+
|
|
8
|
+
# Sub-bin tolerance (dbar) for collapsing ULP-distinct-but-equal bin centers.
|
|
9
|
+
# Far below any real bin spacing (>= ~0.1 dbar) yet far above float ULP noise
|
|
10
|
+
# (~1e-11 dbar at 100 dbar), so genuine bins stay distinct while accumulation
|
|
11
|
+
# error in per-profile np.arange grids is merged.
|
|
12
|
+
_DEPTH_ATOL = 1.0e-6
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _depth_key(d: float) -> int:
|
|
16
|
+
"""Quantize a depth-bin center onto the shared canonical grid."""
|
|
17
|
+
return round(float(d) / _DEPTH_ATOL)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def combine_profiles(
|
|
21
|
+
binned_datasets: list[xr.Dataset],
|
|
22
|
+
profile_metadata: list[dict] | None = None,
|
|
23
|
+
) -> xr.Dataset:
|
|
24
|
+
"""Combine binned profiles into a multi-profile dataset.
|
|
25
|
+
|
|
26
|
+
Aligns all profiles to a common depth grid (union of bin centers).
|
|
27
|
+
Output dimensions: ``(profile, depth_bin)``. NaN where a profile
|
|
28
|
+
doesn't cover a depth.
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
binned_datasets : list of xr.Dataset
|
|
33
|
+
Per-profile binned datasets (from ``bin_by_depth``).
|
|
34
|
+
profile_metadata : list of dict, optional
|
|
35
|
+
Metadata for each profile (start_time, file, etc.).
|
|
36
|
+
|
|
37
|
+
Returns
|
|
38
|
+
-------
|
|
39
|
+
xr.Dataset with dimensions ``(profile, depth_bin)``.
|
|
40
|
+
"""
|
|
41
|
+
if not binned_datasets:
|
|
42
|
+
return xr.Dataset()
|
|
43
|
+
|
|
44
|
+
# Build common depth grid (union of all bin centers). Each profile builds
|
|
45
|
+
# its own np.arange center grid, so when bin_size is not binary-exact
|
|
46
|
+
# (e.g. 0.1, 0.2) two profiles produce mathematically-equal centers that
|
|
47
|
+
# differ by an ULP (0.35 vs 0.35000000000000003). Key the union on a
|
|
48
|
+
# quantized center (sub-bin tolerance) so those collapse to one column
|
|
49
|
+
# instead of splitting one physical depth into two half-NaN columns.
|
|
50
|
+
canon: dict[int, float] = {}
|
|
51
|
+
for ds in binned_datasets:
|
|
52
|
+
if "depth_bin" in ds.coords:
|
|
53
|
+
for d in ds.coords["depth_bin"].values.tolist():
|
|
54
|
+
canon.setdefault(_depth_key(d), d)
|
|
55
|
+
|
|
56
|
+
if not canon:
|
|
57
|
+
return xr.Dataset()
|
|
58
|
+
|
|
59
|
+
items = sorted(canon.items(), key=lambda kv: kv[1])
|
|
60
|
+
common_depths = np.array([v for _, v in items])
|
|
61
|
+
key_to_idx = {k: i for i, (k, _) in enumerate(items)}
|
|
62
|
+
n_profiles = len(binned_datasets)
|
|
63
|
+
n_depths = len(common_depths)
|
|
64
|
+
|
|
65
|
+
# Collect all variable names
|
|
66
|
+
var_names: set[str] = set()
|
|
67
|
+
for ds in binned_datasets:
|
|
68
|
+
var_names.update(str(v) for v in ds.data_vars)
|
|
69
|
+
|
|
70
|
+
data_vars = {}
|
|
71
|
+
for var in sorted(var_names):
|
|
72
|
+
combined = np.full((n_profiles, n_depths), np.nan)
|
|
73
|
+
var_attrs: dict = {}
|
|
74
|
+
for i, ds in enumerate(binned_datasets):
|
|
75
|
+
if var not in ds.data_vars:
|
|
76
|
+
continue
|
|
77
|
+
if not var_attrs:
|
|
78
|
+
# Carry CF units/long_name/standard_name from the L5 binned
|
|
79
|
+
# source so the L6 combined product is not metadata-free.
|
|
80
|
+
var_attrs = dict(ds[var].attrs)
|
|
81
|
+
ds_depths = ds.coords["depth_bin"].values
|
|
82
|
+
ds_vals = ds[var].values
|
|
83
|
+
for j, d in enumerate(ds_depths):
|
|
84
|
+
idx = key_to_idx.get(_depth_key(d))
|
|
85
|
+
if idx is not None:
|
|
86
|
+
combined[i, idx] = ds_vals[j]
|
|
87
|
+
data_vars[var] = (["profile", "depth_bin"], combined, var_attrs)
|
|
88
|
+
|
|
89
|
+
coords = {
|
|
90
|
+
"profile": np.arange(n_profiles),
|
|
91
|
+
"depth_bin": common_depths,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
result = xr.Dataset(data_vars, coords=coords)
|
|
95
|
+
result.coords["depth_bin"].attrs.update(
|
|
96
|
+
{
|
|
97
|
+
"units": "dbar",
|
|
98
|
+
"long_name": "depth bin center pressure",
|
|
99
|
+
"standard_name": "sea_water_pressure",
|
|
100
|
+
"positive": "down",
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
result.coords["profile"].attrs["long_name"] = "profile number"
|
|
104
|
+
|
|
105
|
+
# CF/ACDD global attrs: the L6 headline product previously advertised no
|
|
106
|
+
# convention conformance (empty .attrs), unlike every other writer, so a
|
|
107
|
+
# CF/ACDD consumer could not validate it. L6 is a set of profiles on a
|
|
108
|
+
# shared depth grid -> CF §9 featureType "profile" (audit: missing
|
|
109
|
+
# Conventions/featureType on combined product).
|
|
110
|
+
result.attrs["Conventions"] = "CF-1.13, ACDD-1.3"
|
|
111
|
+
result.attrs["featureType"] = "profile"
|
|
112
|
+
|
|
113
|
+
# Attach metadata
|
|
114
|
+
if profile_metadata:
|
|
115
|
+
for i, meta in enumerate(profile_metadata):
|
|
116
|
+
for key, val in meta.items():
|
|
117
|
+
attr_name = f"profile_{i}_{key}"
|
|
118
|
+
result.attrs[attr_name] = str(val)
|
|
119
|
+
|
|
120
|
+
return result
|
odas_tpw/rsi/config.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Mar-2026, Claude and Pat Welch, pat@mousebrains.com
|
|
2
|
+
"""YAML configuration file support for rsi-tpw.
|
|
3
|
+
|
|
4
|
+
Provides loading, validation, three-way merge (defaults <- config <- CLI),
|
|
5
|
+
deterministic hashing, and sequential output directory management.
|
|
6
|
+
|
|
7
|
+
All config management logic lives in :mod:`odas_tpw.config_base.ConfigManager`;
|
|
8
|
+
this module defines rsi-specific DEFAULTS, instantiates the manager, and
|
|
9
|
+
re-exports the methods as module-level functions for backward compatibility.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from odas_tpw.config_base import ConfigManager, _normalize_value # noqa: F401
|
|
15
|
+
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
# Canonical defaults — one dict per processing section
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
DEFAULTS: dict[str, dict] = {
|
|
21
|
+
"profiles": {
|
|
22
|
+
"P_min": 0.5,
|
|
23
|
+
"W_min": 0.3,
|
|
24
|
+
"direction": "auto",
|
|
25
|
+
"vehicle": None,
|
|
26
|
+
"min_duration": 7.0,
|
|
27
|
+
},
|
|
28
|
+
"epsilon": {
|
|
29
|
+
"fft_length": 1024,
|
|
30
|
+
"diss_length": None,
|
|
31
|
+
"overlap": None,
|
|
32
|
+
"speed": None,
|
|
33
|
+
"direction": "auto",
|
|
34
|
+
"vehicle": None,
|
|
35
|
+
"goodman": True,
|
|
36
|
+
"f_AA": 98.0,
|
|
37
|
+
"f_limit": None,
|
|
38
|
+
"fit_order": 3,
|
|
39
|
+
"despike_thresh": 8,
|
|
40
|
+
"despike_smooth": 0.5,
|
|
41
|
+
"salinity": None,
|
|
42
|
+
},
|
|
43
|
+
"chi": {
|
|
44
|
+
"fft_length": 1024,
|
|
45
|
+
"diss_length": None,
|
|
46
|
+
"overlap": None,
|
|
47
|
+
"speed": None,
|
|
48
|
+
"direction": "auto",
|
|
49
|
+
"vehicle": None,
|
|
50
|
+
"fp07_model": "single_pole",
|
|
51
|
+
"goodman": True,
|
|
52
|
+
"f_AA": 98.0,
|
|
53
|
+
"fit_method": "iterative",
|
|
54
|
+
"spectrum_model": "kraichnan",
|
|
55
|
+
"salinity": None,
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
_mgr = ConfigManager(DEFAULTS)
|
|
60
|
+
|
|
61
|
+
# Re-export manager methods as module-level functions
|
|
62
|
+
load_config = _mgr.load_config
|
|
63
|
+
validate_config = _mgr.validate_config
|
|
64
|
+
merge_config = _mgr.merge_config
|
|
65
|
+
canonicalize = _mgr.canonicalize
|
|
66
|
+
compute_hash = _mgr.compute_hash
|
|
67
|
+
resolve_output_dir = _mgr.resolve_output_dir
|
|
68
|
+
write_signature = _mgr.write_signature
|
|
69
|
+
write_resolved_config = _mgr.write_resolved_config
|
|
70
|
+
|
|
71
|
+
# Also keep _VALID_SECTIONS for any direct references
|
|
72
|
+
_VALID_SECTIONS = _mgr.valid_sections
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
# Template generation
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
_TEMPLATE = """\
|
|
80
|
+
# rsi-tpw configuration
|
|
81
|
+
# Values shown are the defaults. Uncomment and edit to customize.
|
|
82
|
+
# CLI flags override values in this file.
|
|
83
|
+
|
|
84
|
+
profiles:
|
|
85
|
+
P_min: 0.5 # minimum pressure [dbar]
|
|
86
|
+
W_min: 0.3 # minimum fall rate [dbar/s]
|
|
87
|
+
direction: auto # profile direction: auto, up, down, glide, horizontal
|
|
88
|
+
vehicle: null # vehicle override (null = from .p file)
|
|
89
|
+
min_duration: 7.0 # minimum profile duration [s]
|
|
90
|
+
|
|
91
|
+
epsilon:
|
|
92
|
+
fft_length: 1024 # FFT segment length [samples]
|
|
93
|
+
diss_length: null # dissipation window [samples] (null = 4 * fft_length)
|
|
94
|
+
overlap: null # window overlap [samples] (null = diss_length // 2)
|
|
95
|
+
speed: null # profiling speed [m/s] (null = from dP/dt)
|
|
96
|
+
direction: auto # profile direction: auto, up, down, glide, horizontal
|
|
97
|
+
vehicle: null # vehicle override (null = from .p file)
|
|
98
|
+
goodman: true # Goodman coherent noise removal
|
|
99
|
+
f_AA: 98.0 # anti-aliasing filter cutoff [Hz]
|
|
100
|
+
f_limit: null # upper frequency limit [Hz] (null = f_AA)
|
|
101
|
+
fit_order: 3 # polynomial fit order for Nasmyth integration
|
|
102
|
+
despike_thresh: 8 # despike threshold (rectified-HP / LP-envelope ratio, not MAD)
|
|
103
|
+
despike_smooth: 0.5 # despike envelope low-pass cutoff [Hz]
|
|
104
|
+
salinity: null # salinity [PSU] (null = 35, fixed S)
|
|
105
|
+
|
|
106
|
+
chi:
|
|
107
|
+
fft_length: 1024 # FFT segment length [samples]
|
|
108
|
+
diss_length: null # dissipation window [samples] (null = 4 * fft_length)
|
|
109
|
+
overlap: null # window overlap [samples] (null = diss_length // 2)
|
|
110
|
+
speed: null # profiling speed [m/s] (null = from dP/dt)
|
|
111
|
+
direction: auto # profile direction: auto, up, down, glide, horizontal
|
|
112
|
+
vehicle: null # vehicle override (null = from .p file)
|
|
113
|
+
fp07_model: single_pole # FP07 transfer function: single_pole or double_pole
|
|
114
|
+
goodman: true # Goodman coherent noise removal
|
|
115
|
+
f_AA: 98.0 # anti-aliasing filter cutoff [Hz]
|
|
116
|
+
fit_method: iterative # Method 2 fitting: iterative or mle
|
|
117
|
+
spectrum_model: kraichnan # theoretical spectrum: batchelor or kraichnan
|
|
118
|
+
salinity: null # salinity [PSU] (null = 35, fixed S)
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def generate_template(path: str | Path) -> Path:
|
|
123
|
+
"""Write a fully-commented template configuration file."""
|
|
124
|
+
path = Path(path)
|
|
125
|
+
path.write_text(_TEMPLATE)
|
|
126
|
+
return path
|