specmod 0.2.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 (62) hide show
  1. specmod/__init__.py +17 -0
  2. specmod/_vendor/__init__.py +21 -0
  3. specmod/_vendor/qiinv.py +243 -0
  4. specmod/acquire.py +358 -0
  5. specmod/api.py +480 -0
  6. specmod/cli.py +139 -0
  7. specmod/config/__init__.py +44 -0
  8. specmod/config/layers.py +168 -0
  9. specmod/config/provenance.py +77 -0
  10. specmod/config/sections.py +385 -0
  11. specmod/config/serialize.py +58 -0
  12. specmod/core/__init__.py +41 -0
  13. specmod/core/bandwidth.py +187 -0
  14. specmod/core/collection.py +549 -0
  15. specmod/core/noise.py +478 -0
  16. specmod/core/scalogram.py +234 -0
  17. specmod/core/spectrum.py +326 -0
  18. specmod/core/units.py +116 -0
  19. specmod/datasets.py +316 -0
  20. specmod/distance.py +190 -0
  21. specmod/exceptions.py +58 -0
  22. specmod/fitting/__init__.py +58 -0
  23. specmod/fitting/base.py +50 -0
  24. specmod/fitting/event.py +284 -0
  25. specmod/fitting/guess.py +170 -0
  26. specmod/fitting/spectrum.py +330 -0
  27. specmod/io.py +241 -0
  28. specmod/magnitude.py +312 -0
  29. specmod/picks/__init__.py +182 -0
  30. specmod/picks/base.py +250 -0
  31. specmod/picks/delimited.py +224 -0
  32. specmod/picks/events.py +157 -0
  33. specmod/picks/resolution.py +149 -0
  34. specmod/picks/snuffler.py +92 -0
  35. specmod/pipeline.py +280 -0
  36. specmod/plotting.py +203 -0
  37. specmod/preprocess.py +554 -0
  38. specmod/smoothing/__init__.py +50 -0
  39. specmod/smoothing/base.py +56 -0
  40. specmod/smoothing/konno_ohmachi.py +83 -0
  41. specmod/smoothing/log_bins.py +171 -0
  42. specmod/sources/__init__.py +65 -0
  43. specmod/sources/attenuation.py +110 -0
  44. specmod/sources/composite.py +135 -0
  45. specmod/sources/motion.py +40 -0
  46. specmod/sources/source.py +147 -0
  47. specmod/spreading.py +209 -0
  48. specmod/staged.py +523 -0
  49. specmod/tables.py +110 -0
  50. specmod/transforms/__init__.py +50 -0
  51. specmod/transforms/base.py +242 -0
  52. specmod/transforms/cwt.py +219 -0
  53. specmod/transforms/fft.py +157 -0
  54. specmod/transforms/multitaper.py +357 -0
  55. specmod/transforms/prieto.py +272 -0
  56. specmod/transforms/quadratic.py +221 -0
  57. specmod/utils.py +305 -0
  58. specmod-0.2.0.dist-info/METADATA +294 -0
  59. specmod-0.2.0.dist-info/RECORD +62 -0
  60. specmod-0.2.0.dist-info/WHEEL +4 -0
  61. specmod-0.2.0.dist-info/entry_points.txt +2 -0
  62. specmod-0.2.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,41 @@
1
+ """Core data types: units, and the containers that carry them."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .bandwidth import (
6
+ BANDWIDTH_SELECTORS,
7
+ BandwidthSelector,
8
+ get_bandwidth_selector,
9
+ )
10
+ from .collection import BinnedSpectrum, SpectrumPair, SpectrumSet
11
+ from .noise import (
12
+ NOISE_MODELS,
13
+ BoostNoise,
14
+ NoiseModel,
15
+ NoNoiseModel,
16
+ RotateNoise,
17
+ get_noise_model,
18
+ )
19
+ from .scalogram import Scalogram, ScalogramQC
20
+ from .spectrum import Spectrum
21
+ from .units import AmplitudeKind, Motion
22
+
23
+ __all__ = [
24
+ "BANDWIDTH_SELECTORS",
25
+ "NOISE_MODELS",
26
+ "AmplitudeKind",
27
+ "BandwidthSelector",
28
+ "BinnedSpectrum",
29
+ "BoostNoise",
30
+ "Motion",
31
+ "NoNoiseModel",
32
+ "NoiseModel",
33
+ "RotateNoise",
34
+ "Scalogram",
35
+ "ScalogramQC",
36
+ "Spectrum",
37
+ "SpectrumPair",
38
+ "SpectrumSet",
39
+ "get_bandwidth_selector",
40
+ "get_noise_model",
41
+ ]
@@ -0,0 +1,187 @@
1
+ """Choosing the frequency band a spectrum is fitted over.
2
+
3
+ The band is the most consequential choice in the pipeline after the transform
4
+ itself: it is what constrains ``Omega``, and therefore ``M0`` and ``Mw``. There
5
+ is more than one defensible way to pick it, so this is a **set** of strategies
6
+ behind one signature — given frequencies, a signal-to-noise ratio and a
7
+ threshold, return the band or ``None`` — resolved through
8
+ :data:`BANDWIDTH_SELECTORS`, the same way :mod:`specmod.core.noise` handles
9
+ noise models and :mod:`specmod.transforms` handles estimators.
10
+
11
+ ``peak``
12
+ The shipped default, and what the legacy ``BW_METHOD = 2`` did. Walks
13
+ outward from the highest signal-to-noise bin until the ratio drops below
14
+ threshold in each direction. Anchoring on the peak is a real modelling
15
+ choice: it says the usable band is the one containing the strongest part
16
+ of the signal, even if a wider passing run exists elsewhere.
17
+
18
+ ``widest``
19
+ The widest contiguous run above threshold, bridging single-bin dips.
20
+ Anchors on nothing, so it finds a wide low-frequency run that ``peak``
21
+ would miss if the peak sits elsewhere.
22
+
23
+ Returning ``None`` on failure is deliberate and differs from both legacy
24
+ methods, which returned a band anyway and set a ``pass_snr`` flag beside it —
25
+ so a caller reading the band without checking the flag got numbers that looked
26
+ like a measurement.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ from dataclasses import dataclass
32
+ from typing import Protocol, runtime_checkable
33
+
34
+ import numpy as np
35
+ from numpy.typing import NDArray
36
+
37
+ __all__ = [
38
+ "BANDWIDTH_SELECTORS",
39
+ "BandwidthSelector",
40
+ "PeakBandwidth",
41
+ "WidestBandwidth",
42
+ "get_bandwidth_selector",
43
+ ]
44
+
45
+
46
+ @runtime_checkable
47
+ class BandwidthSelector(Protocol):
48
+ """Anything that picks a band from a signal-to-noise curve."""
49
+
50
+ @property
51
+ def name(self) -> str:
52
+ """Short identifier, recorded alongside the result."""
53
+ ...
54
+
55
+ def select(
56
+ self,
57
+ freq: NDArray[np.float64],
58
+ snr: NDArray[np.float64],
59
+ threshold: float,
60
+ ) -> tuple[float, float] | None:
61
+ """The usable band, or ``None`` if none survives."""
62
+ ...
63
+
64
+
65
+ @dataclass(frozen=True)
66
+ class PeakBandwidth:
67
+ """Walk outward from the strongest bin until the ratio fails each way.
68
+
69
+ Ported from the legacy ``find_optimal_signal_bandwidth_2``, which is what
70
+ the shipped configuration has always used.
71
+
72
+ .. note::
73
+
74
+ **A latent bug in the original is fixed here, and it changes results.**
75
+ The legacy indexed the bin before the first failure with ``[...][0] - 1``
76
+ on a raw index array. When the failure was the bin *immediately* above
77
+ the peak that index is ``0``, so ``0 - 1`` wrapped to ``-1`` and
78
+ selected the *highest* frequency in the record instead of failing —
79
+ returning a band far wider than the data supports, silently. The same
80
+ wrap could not happen at the low end, where ``+ 1`` is used.
81
+
82
+ Here the walk stops where it should and returns ``None`` when there is
83
+ no room to walk.
84
+ """
85
+
86
+ @property
87
+ def name(self) -> str:
88
+ return "peak"
89
+
90
+ def select(
91
+ self,
92
+ freq: NDArray[np.float64],
93
+ snr: NDArray[np.float64],
94
+ threshold: float,
95
+ ) -> tuple[float, float] | None:
96
+ if freq.size == 0 or snr.size != freq.size:
97
+ return None
98
+
99
+ peak = int(np.argmax(snr))
100
+ if snr[peak] < threshold:
101
+ return None
102
+
103
+ # Walk out from the peak while the ratio holds.
104
+ low = peak
105
+ while low - 1 >= 0 and snr[low - 1] >= threshold:
106
+ low -= 1
107
+ high = peak
108
+ while high + 1 < snr.size and snr[high + 1] >= threshold:
109
+ high += 1
110
+
111
+ if high <= low:
112
+ return None
113
+ return float(freq[low]), float(freq[high])
114
+
115
+
116
+ @dataclass(frozen=True)
117
+ class WidestBandwidth:
118
+ """The widest contiguous run above threshold, bridging short dips.
119
+
120
+ Replaces the legacy ``find_optimal_signal_bandwidth`` (``BW_METHOD = 1``),
121
+ which took percentiles of an integrated sign function with a retry loop.
122
+ Every step of that was discontinuous and they compounded: an edge could
123
+ move 13 bins between machines. It also lagged — on a clean 5-30 Hz passing
124
+ region it put the low edge at 9.41 Hz, and the low edge is what constrains
125
+ ``Omega``. This lands within one bin of the truth.
126
+ """
127
+
128
+ max_gap: int = 1
129
+ min_width: int = 3
130
+
131
+ @property
132
+ def name(self) -> str:
133
+ return "widest"
134
+
135
+ def select(
136
+ self,
137
+ freq: NDArray[np.float64],
138
+ snr: NDArray[np.float64],
139
+ threshold: float,
140
+ ) -> tuple[float, float] | None:
141
+ if freq.size == 0 or snr.size != freq.size:
142
+ return None
143
+
144
+ passing = snr >= threshold
145
+ if not passing.any():
146
+ return None
147
+
148
+ # Bridge short gaps, so one noisy bin does not split a band in two.
149
+ bridged = passing.copy()
150
+ (failing,) = np.where(~passing)
151
+ for i in failing:
152
+ left, right = i - 1, i + self.max_gap
153
+ if left >= 0 and right < passing.size and passing[left] and passing[right]:
154
+ bridged[i : i + self.max_gap] = True
155
+
156
+ edges = np.diff(np.concatenate(([0], bridged.view(np.int8), [0])))
157
+ starts = np.flatnonzero(edges == 1)
158
+ ends = np.flatnonzero(edges == -1)
159
+ if starts.size == 0:
160
+ return None
161
+
162
+ widths = ends - starts
163
+ best = int(np.argmax(widths))
164
+ if widths[best] < self.min_width:
165
+ return None
166
+
167
+ low, high = int(starts[best]), int(ends[best]) - 1
168
+ return float(freq[low]), float(freq[high])
169
+
170
+
171
+ #: Registered selectors, by the name configuration refers to them by.
172
+ BANDWIDTH_SELECTORS: dict[str, type[PeakBandwidth] | type[WidestBandwidth]] = {
173
+ "peak": PeakBandwidth,
174
+ "widest": WidestBandwidth,
175
+ }
176
+
177
+
178
+ def get_bandwidth_selector(name: str) -> BandwidthSelector:
179
+ """Resolve a registered selector by name, with its defaults."""
180
+ try:
181
+ cls = BANDWIDTH_SELECTORS[name]
182
+ except KeyError:
183
+ raise ValueError(
184
+ f"Unknown bandwidth selector {name!r}. "
185
+ f"Available: {sorted(BANDWIDTH_SELECTORS)}."
186
+ ) from None
187
+ return cls()