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,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