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.
- specmod/__init__.py +17 -0
- specmod/_vendor/__init__.py +21 -0
- specmod/_vendor/qiinv.py +243 -0
- specmod/acquire.py +358 -0
- specmod/api.py +480 -0
- specmod/cli.py +139 -0
- specmod/config/__init__.py +44 -0
- specmod/config/layers.py +168 -0
- specmod/config/provenance.py +77 -0
- specmod/config/sections.py +385 -0
- specmod/config/serialize.py +58 -0
- specmod/core/__init__.py +41 -0
- specmod/core/bandwidth.py +187 -0
- specmod/core/collection.py +549 -0
- specmod/core/noise.py +478 -0
- specmod/core/scalogram.py +234 -0
- specmod/core/spectrum.py +326 -0
- specmod/core/units.py +116 -0
- specmod/datasets.py +316 -0
- specmod/distance.py +190 -0
- specmod/exceptions.py +58 -0
- specmod/fitting/__init__.py +58 -0
- specmod/fitting/base.py +50 -0
- specmod/fitting/event.py +284 -0
- specmod/fitting/guess.py +170 -0
- specmod/fitting/spectrum.py +330 -0
- specmod/io.py +241 -0
- specmod/magnitude.py +312 -0
- specmod/picks/__init__.py +182 -0
- specmod/picks/base.py +250 -0
- specmod/picks/delimited.py +224 -0
- specmod/picks/events.py +157 -0
- specmod/picks/resolution.py +149 -0
- specmod/picks/snuffler.py +92 -0
- specmod/pipeline.py +280 -0
- specmod/plotting.py +203 -0
- specmod/preprocess.py +554 -0
- specmod/smoothing/__init__.py +50 -0
- specmod/smoothing/base.py +56 -0
- specmod/smoothing/konno_ohmachi.py +83 -0
- specmod/smoothing/log_bins.py +171 -0
- specmod/sources/__init__.py +65 -0
- specmod/sources/attenuation.py +110 -0
- specmod/sources/composite.py +135 -0
- specmod/sources/motion.py +40 -0
- specmod/sources/source.py +147 -0
- specmod/spreading.py +209 -0
- specmod/staged.py +523 -0
- specmod/tables.py +110 -0
- specmod/transforms/__init__.py +50 -0
- specmod/transforms/base.py +242 -0
- specmod/transforms/cwt.py +219 -0
- specmod/transforms/fft.py +157 -0
- specmod/transforms/multitaper.py +357 -0
- specmod/transforms/prieto.py +272 -0
- specmod/transforms/quadratic.py +221 -0
- specmod/utils.py +305 -0
- specmod-0.2.0.dist-info/METADATA +294 -0
- specmod-0.2.0.dist-info/RECORD +62 -0
- specmod-0.2.0.dist-info/WHEEL +4 -0
- specmod-0.2.0.dist-info/entry_points.txt +2 -0
- specmod-0.2.0.dist-info/licenses/LICENSE +21 -0
specmod/core/__init__.py
ADDED
|
@@ -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()
|