dsgbr 0.1.0__tar.gz
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.
- dsgbr-0.1.0/LICENSE +28 -0
- dsgbr-0.1.0/PKG-INFO +252 -0
- dsgbr-0.1.0/README.md +192 -0
- dsgbr-0.1.0/pyproject.toml +123 -0
- dsgbr-0.1.0/setup.cfg +4 -0
- dsgbr-0.1.0/src/dsgbr/DSGBR.py +29 -0
- dsgbr-0.1.0/src/dsgbr/__init__.py +35 -0
- dsgbr-0.1.0/src/dsgbr/_compat.py +68 -0
- dsgbr-0.1.0/src/dsgbr/_config.py +255 -0
- dsgbr-0.1.0/src/dsgbr/_detector.py +466 -0
- dsgbr-0.1.0/src/dsgbr/_selection.py +185 -0
- dsgbr-0.1.0/src/dsgbr/py.typed +0 -0
- dsgbr-0.1.0/src/dsgbr.egg-info/PKG-INFO +252 -0
- dsgbr-0.1.0/src/dsgbr.egg-info/SOURCES.txt +22 -0
- dsgbr-0.1.0/src/dsgbr.egg-info/dependency_links.txt +1 -0
- dsgbr-0.1.0/src/dsgbr.egg-info/requires.txt +37 -0
- dsgbr-0.1.0/src/dsgbr.egg-info/top_level.txt +1 -0
- dsgbr-0.1.0/tests/test_compat.py +94 -0
- dsgbr-0.1.0/tests/test_config.py +139 -0
- dsgbr-0.1.0/tests/test_coverage_gaps.py +260 -0
- dsgbr-0.1.0/tests/test_detector.py +259 -0
- dsgbr-0.1.0/tests/test_edge_cases.py +91 -0
- dsgbr-0.1.0/tests/test_integration.py +68 -0
- dsgbr-0.1.0/tests/test_selection.py +112 -0
dsgbr-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Ricardo Frantz
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
dsgbr-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dsgbr
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Dual Savitzky–Golay Baseline Ratio (DSGBR) spectral peak detector
|
|
5
|
+
Author: Ricardo Frantz
|
|
6
|
+
Maintainer: Ricardo Frantz
|
|
7
|
+
License-Expression: BSD-3-Clause
|
|
8
|
+
Project-URL: Homepage, https://github.com/ricardofrantz/dsgbr
|
|
9
|
+
Project-URL: Source, https://github.com/ricardofrantz/dsgbr
|
|
10
|
+
Project-URL: Tracker, https://github.com/ricardofrantz/dsgbr/issues
|
|
11
|
+
Project-URL: Documentation, https://github.com/ricardofrantz/dsgbr#readme
|
|
12
|
+
Keywords: spectral-analysis,peak-detection,signal-processing,savitzky-golay,power-spectral-density,fluid-dynamics
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Education
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: numpy>=1.24
|
|
28
|
+
Requires-Dist: scipy>=1.10
|
|
29
|
+
Provides-Extra: plotting
|
|
30
|
+
Requires-Dist: matplotlib>=3.7; extra == "plotting"
|
|
31
|
+
Provides-Extra: tests
|
|
32
|
+
Requires-Dist: coverage[toml]>=7.5.3; extra == "tests"
|
|
33
|
+
Requires-Dist: hypothesis>=6.100; extra == "tests"
|
|
34
|
+
Requires-Dist: pytest-cov>=5.0.0; extra == "tests"
|
|
35
|
+
Requires-Dist: pytest>=8.2.2; extra == "tests"
|
|
36
|
+
Provides-Extra: tests-extra
|
|
37
|
+
Requires-Dist: pytest-randomly==3.16.0; extra == "tests-extra"
|
|
38
|
+
Requires-Dist: pytest-rerunfailures==15.1; extra == "tests-extra"
|
|
39
|
+
Requires-Dist: pytest-xdist==3.8.0; extra == "tests-extra"
|
|
40
|
+
Provides-Extra: qa
|
|
41
|
+
Requires-Dist: codespell>=2.4.1; extra == "qa"
|
|
42
|
+
Requires-Dist: mypy>=1.11.0; extra == "qa"
|
|
43
|
+
Requires-Dist: pre-commit>=3.8.0; extra == "qa"
|
|
44
|
+
Requires-Dist: ruff>=0.7.0; extra == "qa"
|
|
45
|
+
Provides-Extra: dev
|
|
46
|
+
Requires-Dist: build; extra == "dev"
|
|
47
|
+
Requires-Dist: coverage[toml]>=7.5.3; extra == "dev"
|
|
48
|
+
Requires-Dist: codespell>=2.4.1; extra == "dev"
|
|
49
|
+
Requires-Dist: hypothesis>=6.100; extra == "dev"
|
|
50
|
+
Requires-Dist: mypy>=1.11.0; extra == "dev"
|
|
51
|
+
Requires-Dist: pre-commit>=3.8.0; extra == "dev"
|
|
52
|
+
Requires-Dist: pytest>=8.2.2; extra == "dev"
|
|
53
|
+
Requires-Dist: pytest-cov>=5.0.0; extra == "dev"
|
|
54
|
+
Requires-Dist: pytest-randomly==3.16.0; extra == "dev"
|
|
55
|
+
Requires-Dist: pytest-rerunfailures==15.1; extra == "dev"
|
|
56
|
+
Requires-Dist: pytest-xdist==3.8.0; extra == "dev"
|
|
57
|
+
Requires-Dist: ruff>=0.7.0; extra == "dev"
|
|
58
|
+
Requires-Dist: twine; extra == "dev"
|
|
59
|
+
Dynamic: license-file
|
|
60
|
+
|
|
61
|
+
# DSGBR
|
|
62
|
+
|
|
63
|
+
[](https://github.com/ricardofrantz/dsgbr/actions/workflows/ci.yml)
|
|
64
|
+
[](https://codecov.io/gh/ricardofrantz/dsgbr)
|
|
65
|
+
[](https://pypi.org/project/dsgbr/)
|
|
66
|
+
[](https://pypi.org/project/dsgbr/)
|
|
67
|
+
[](LICENSE)
|
|
68
|
+
|
|
69
|
+
**Dual Savitzky-Golay Baseline Ratio (DSGBR)** is a spectral peak detector
|
|
70
|
+
for frequency-domain signals. It was designed for robust detection in dense,
|
|
71
|
+
noisy power spectra common in fluid dynamics, vibration analysis, and other
|
|
72
|
+
experimental sciences.
|
|
73
|
+
|
|
74
|
+
## Algorithm
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
PSD ──► SEARCH (short-scale SG smooth)
|
|
78
|
+
│
|
|
79
|
+
▼
|
|
80
|
+
BASELINE (long-scale SG smooth)
|
|
81
|
+
│
|
|
82
|
+
▼
|
|
83
|
+
RATIO = SEARCH / BASELINE
|
|
84
|
+
│
|
|
85
|
+
▼
|
|
86
|
+
peaks where RATIO ≥ threshold
|
|
87
|
+
│
|
|
88
|
+
├──► spacing rules
|
|
89
|
+
├──► ULF guardrail
|
|
90
|
+
└──► band selection (if > max_peaks)
|
|
91
|
+
│
|
|
92
|
+
▼
|
|
93
|
+
(peak_frequencies, peak_heights)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The detector builds a short-scale **SEARCH** signal and a longer-scale
|
|
97
|
+
**BASELINE** signal using Savitzky-Golay filtering. A peak is accepted
|
|
98
|
+
when `SEARCH / BASELINE` exceeds a configurable ratio threshold, subject
|
|
99
|
+
to spacing constraints and an ultra-low-frequency guardrail.
|
|
100
|
+
|
|
101
|
+
## Install
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
pip install dsgbr
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
For development:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
git clone https://github.com/ricardofrantz/dsgbr.git
|
|
111
|
+
cd dsgbr
|
|
112
|
+
uv pip install -e ".[dev]"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Quick start
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
import numpy as np
|
|
119
|
+
from dsgbr import dsgbr_detector
|
|
120
|
+
|
|
121
|
+
# Synthetic PSD with known peaks
|
|
122
|
+
frequencies = np.linspace(0.001, 1.0, 2048)
|
|
123
|
+
psd = np.ones_like(frequencies)
|
|
124
|
+
psd[400] = 12.0 # inject a peak
|
|
125
|
+
psd[1200] = 8.0 # inject another
|
|
126
|
+
|
|
127
|
+
peak_f, peak_h = dsgbr_detector(
|
|
128
|
+
frequencies, psd,
|
|
129
|
+
case_info={"ratio_threshold": 1.5, "baseline_window": 61},
|
|
130
|
+
)
|
|
131
|
+
print(f"Detected {peak_f.size} peaks at f = {peak_f}")
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Configuration
|
|
135
|
+
|
|
136
|
+
All parameters are set through `DetectionConfig` or passed as a dictionary
|
|
137
|
+
via the `case_info` argument. Short aliases (RT, SW, BWF, etc.) are
|
|
138
|
+
supported for concise configuration.
|
|
139
|
+
|
|
140
|
+
| Parameter | Alias | Default | Description |
|
|
141
|
+
|-----------|-------|---------|-------------|
|
|
142
|
+
| `ratio_threshold` | RT | 1.8 | Min SEARCH/BASELINE ratio for acceptance |
|
|
143
|
+
| `smooth_window` | SW | 3 | Savitzky-Golay window for SEARCH (odd, >= 3) |
|
|
144
|
+
| `baseline_window_frac` | BWF | 0.001 | Baseline window as fraction of data length |
|
|
145
|
+
| `distance_low` | DL | 2 | Min bin separation below `switch_frequency` |
|
|
146
|
+
| `distance_high` | DH | 1 | Min bin separation above `switch_frequency` |
|
|
147
|
+
| `switch_frequency` | SF | 0.02 | Frequency threshold for spacing rules |
|
|
148
|
+
| `max_peaks` | MP | 25 | Maximum peaks returned |
|
|
149
|
+
| `smooth_polyorder` | — | 2 | Polynomial order for SG filter |
|
|
150
|
+
| `smooth_on_log` | — | True | Smooth log10(PSD) instead of linear |
|
|
151
|
+
| `baseline_window` | — | None | Fixed baseline window (overrides BWF) |
|
|
152
|
+
| `baseline_on_log` | — | True | Baseline smoothing in log domain |
|
|
153
|
+
| `band_strategy` | — | proportional | Band allocation: proportional or equal |
|
|
154
|
+
| `n_bands` | — | 10 | Number of logarithmic frequency bands |
|
|
155
|
+
| `ulf_fmax` | — | 0.001 | ULF band upper frequency limit |
|
|
156
|
+
| `ulf_min_q` | — | 9.0 | Minimum Q-factor for ULF peaks |
|
|
157
|
+
| `ulf_max_points` | — | 5 | Maximum ULF peaks to retain |
|
|
158
|
+
|
|
159
|
+
## Advanced usage
|
|
160
|
+
|
|
161
|
+
### Support series for visualization
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
from dsgbr import compute_support_series
|
|
165
|
+
|
|
166
|
+
support = compute_support_series(frequencies, psd, case_info={"RT": 2.0})
|
|
167
|
+
|
|
168
|
+
# Plot SEARCH vs BASELINE overlay
|
|
169
|
+
import matplotlib.pyplot as plt
|
|
170
|
+
plt.semilogy(frequencies, support["search_series"], label="SEARCH")
|
|
171
|
+
plt.semilogy(frequencies, support["baseline_series"], label="BASELINE")
|
|
172
|
+
plt.semilogy(frequencies, support["rthreshold"], "--", label="Threshold")
|
|
173
|
+
plt.legend()
|
|
174
|
+
plt.show()
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Band-balanced peak selection
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
from dsgbr import select_peaks_by_frequency_bands
|
|
181
|
+
|
|
182
|
+
# Reduce 100 peaks to 15, spread across frequency bands
|
|
183
|
+
sel_f, sel_h = select_peaks_by_frequency_bands(
|
|
184
|
+
peak_frequencies, peak_heights,
|
|
185
|
+
max_peaks=15, strategy="proportional", n_bands=8,
|
|
186
|
+
)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Configuration via dataclass
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
from dsgbr import DetectionConfig
|
|
193
|
+
|
|
194
|
+
cfg = DetectionConfig(ratio_threshold=2.5, smooth_window=7, max_peaks=10)
|
|
195
|
+
print(cfg.to_metadata())
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## API reference
|
|
199
|
+
|
|
200
|
+
| Function / Class | Description |
|
|
201
|
+
|-----------------|-------------|
|
|
202
|
+
| `dsgbr_detector(f, psd, *, case_info, return_support)` | Main detection pipeline |
|
|
203
|
+
| `compute_support_series(f, psd, case_info)` | Return intermediate arrays for visualization |
|
|
204
|
+
| `select_peaks_by_frequency_bands(f, h, *, max_peaks, strategy, n_bands)` | Band-balanced down-selection |
|
|
205
|
+
| `find_nearest_frequency(target, frequencies, heights)` | Closest detected frequency lookup |
|
|
206
|
+
| `DetectionConfig` | Frozen dataclass with 17 parameters |
|
|
207
|
+
| `detect_peaks_case_adaptive(...)` | Deprecated alias for `dsgbr_detector` |
|
|
208
|
+
| `DSGBR_PARAM_ALIASES` | Short-to-long parameter name mapping |
|
|
209
|
+
|
|
210
|
+
## Examples
|
|
211
|
+
|
|
212
|
+
See [`examples/`](examples/) for runnable scripts:
|
|
213
|
+
|
|
214
|
+
- **`basic_usage.py`** — minimal detection example
|
|
215
|
+
- **`parameter_tuning.py`** — sweep ratio_threshold, compare peak counts
|
|
216
|
+
- **`visualization.py`** — SEARCH/BASELINE overlay plot
|
|
217
|
+
|
|
218
|
+
## How it works
|
|
219
|
+
|
|
220
|
+
DSGBR applies two Savitzky-Golay passes at different scales to separate
|
|
221
|
+
sharp spectral peaks from the slowly varying baseline. The ratio between
|
|
222
|
+
these two series naturally highlights peaks above the local background,
|
|
223
|
+
making the detector robust to spectral slope and broadband noise. For a
|
|
224
|
+
detailed description, see [`docs/algorithm.md`](docs/algorithm.md).
|
|
225
|
+
|
|
226
|
+
## Citation
|
|
227
|
+
|
|
228
|
+
If you use DSGBR in your research, please cite:
|
|
229
|
+
|
|
230
|
+
```bibtex
|
|
231
|
+
@software{dsgbr2026,
|
|
232
|
+
author = {Frantz, Ricardo},
|
|
233
|
+
title = {{DSGBR}: Dual Savitzky--Golay Baseline Ratio spectral peak detector},
|
|
234
|
+
year = {2026},
|
|
235
|
+
url = {https://github.com/ricardofrantz/dsgbr},
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## License
|
|
240
|
+
|
|
241
|
+
BSD 3-Clause. See [LICENSE](LICENSE).
|
|
242
|
+
|
|
243
|
+
## Contributing
|
|
244
|
+
|
|
245
|
+
Contributions are welcome. Please open an issue to discuss changes before
|
|
246
|
+
submitting a pull request. Run the full QA suite before submitting:
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
uv pip install -e ".[dev]"
|
|
250
|
+
pre-commit run --all-files
|
|
251
|
+
pytest --cov=dsgbr
|
|
252
|
+
```
|
dsgbr-0.1.0/README.md
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# DSGBR
|
|
2
|
+
|
|
3
|
+
[](https://github.com/ricardofrantz/dsgbr/actions/workflows/ci.yml)
|
|
4
|
+
[](https://codecov.io/gh/ricardofrantz/dsgbr)
|
|
5
|
+
[](https://pypi.org/project/dsgbr/)
|
|
6
|
+
[](https://pypi.org/project/dsgbr/)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
**Dual Savitzky-Golay Baseline Ratio (DSGBR)** is a spectral peak detector
|
|
10
|
+
for frequency-domain signals. It was designed for robust detection in dense,
|
|
11
|
+
noisy power spectra common in fluid dynamics, vibration analysis, and other
|
|
12
|
+
experimental sciences.
|
|
13
|
+
|
|
14
|
+
## Algorithm
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
PSD ──► SEARCH (short-scale SG smooth)
|
|
18
|
+
│
|
|
19
|
+
▼
|
|
20
|
+
BASELINE (long-scale SG smooth)
|
|
21
|
+
│
|
|
22
|
+
▼
|
|
23
|
+
RATIO = SEARCH / BASELINE
|
|
24
|
+
│
|
|
25
|
+
▼
|
|
26
|
+
peaks where RATIO ≥ threshold
|
|
27
|
+
│
|
|
28
|
+
├──► spacing rules
|
|
29
|
+
├──► ULF guardrail
|
|
30
|
+
└──► band selection (if > max_peaks)
|
|
31
|
+
│
|
|
32
|
+
▼
|
|
33
|
+
(peak_frequencies, peak_heights)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The detector builds a short-scale **SEARCH** signal and a longer-scale
|
|
37
|
+
**BASELINE** signal using Savitzky-Golay filtering. A peak is accepted
|
|
38
|
+
when `SEARCH / BASELINE` exceeds a configurable ratio threshold, subject
|
|
39
|
+
to spacing constraints and an ultra-low-frequency guardrail.
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install dsgbr
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
For development:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
git clone https://github.com/ricardofrantz/dsgbr.git
|
|
51
|
+
cd dsgbr
|
|
52
|
+
uv pip install -e ".[dev]"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Quick start
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
import numpy as np
|
|
59
|
+
from dsgbr import dsgbr_detector
|
|
60
|
+
|
|
61
|
+
# Synthetic PSD with known peaks
|
|
62
|
+
frequencies = np.linspace(0.001, 1.0, 2048)
|
|
63
|
+
psd = np.ones_like(frequencies)
|
|
64
|
+
psd[400] = 12.0 # inject a peak
|
|
65
|
+
psd[1200] = 8.0 # inject another
|
|
66
|
+
|
|
67
|
+
peak_f, peak_h = dsgbr_detector(
|
|
68
|
+
frequencies, psd,
|
|
69
|
+
case_info={"ratio_threshold": 1.5, "baseline_window": 61},
|
|
70
|
+
)
|
|
71
|
+
print(f"Detected {peak_f.size} peaks at f = {peak_f}")
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Configuration
|
|
75
|
+
|
|
76
|
+
All parameters are set through `DetectionConfig` or passed as a dictionary
|
|
77
|
+
via the `case_info` argument. Short aliases (RT, SW, BWF, etc.) are
|
|
78
|
+
supported for concise configuration.
|
|
79
|
+
|
|
80
|
+
| Parameter | Alias | Default | Description |
|
|
81
|
+
|-----------|-------|---------|-------------|
|
|
82
|
+
| `ratio_threshold` | RT | 1.8 | Min SEARCH/BASELINE ratio for acceptance |
|
|
83
|
+
| `smooth_window` | SW | 3 | Savitzky-Golay window for SEARCH (odd, >= 3) |
|
|
84
|
+
| `baseline_window_frac` | BWF | 0.001 | Baseline window as fraction of data length |
|
|
85
|
+
| `distance_low` | DL | 2 | Min bin separation below `switch_frequency` |
|
|
86
|
+
| `distance_high` | DH | 1 | Min bin separation above `switch_frequency` |
|
|
87
|
+
| `switch_frequency` | SF | 0.02 | Frequency threshold for spacing rules |
|
|
88
|
+
| `max_peaks` | MP | 25 | Maximum peaks returned |
|
|
89
|
+
| `smooth_polyorder` | — | 2 | Polynomial order for SG filter |
|
|
90
|
+
| `smooth_on_log` | — | True | Smooth log10(PSD) instead of linear |
|
|
91
|
+
| `baseline_window` | — | None | Fixed baseline window (overrides BWF) |
|
|
92
|
+
| `baseline_on_log` | — | True | Baseline smoothing in log domain |
|
|
93
|
+
| `band_strategy` | — | proportional | Band allocation: proportional or equal |
|
|
94
|
+
| `n_bands` | — | 10 | Number of logarithmic frequency bands |
|
|
95
|
+
| `ulf_fmax` | — | 0.001 | ULF band upper frequency limit |
|
|
96
|
+
| `ulf_min_q` | — | 9.0 | Minimum Q-factor for ULF peaks |
|
|
97
|
+
| `ulf_max_points` | — | 5 | Maximum ULF peaks to retain |
|
|
98
|
+
|
|
99
|
+
## Advanced usage
|
|
100
|
+
|
|
101
|
+
### Support series for visualization
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from dsgbr import compute_support_series
|
|
105
|
+
|
|
106
|
+
support = compute_support_series(frequencies, psd, case_info={"RT": 2.0})
|
|
107
|
+
|
|
108
|
+
# Plot SEARCH vs BASELINE overlay
|
|
109
|
+
import matplotlib.pyplot as plt
|
|
110
|
+
plt.semilogy(frequencies, support["search_series"], label="SEARCH")
|
|
111
|
+
plt.semilogy(frequencies, support["baseline_series"], label="BASELINE")
|
|
112
|
+
plt.semilogy(frequencies, support["rthreshold"], "--", label="Threshold")
|
|
113
|
+
plt.legend()
|
|
114
|
+
plt.show()
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Band-balanced peak selection
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from dsgbr import select_peaks_by_frequency_bands
|
|
121
|
+
|
|
122
|
+
# Reduce 100 peaks to 15, spread across frequency bands
|
|
123
|
+
sel_f, sel_h = select_peaks_by_frequency_bands(
|
|
124
|
+
peak_frequencies, peak_heights,
|
|
125
|
+
max_peaks=15, strategy="proportional", n_bands=8,
|
|
126
|
+
)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Configuration via dataclass
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from dsgbr import DetectionConfig
|
|
133
|
+
|
|
134
|
+
cfg = DetectionConfig(ratio_threshold=2.5, smooth_window=7, max_peaks=10)
|
|
135
|
+
print(cfg.to_metadata())
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## API reference
|
|
139
|
+
|
|
140
|
+
| Function / Class | Description |
|
|
141
|
+
|-----------------|-------------|
|
|
142
|
+
| `dsgbr_detector(f, psd, *, case_info, return_support)` | Main detection pipeline |
|
|
143
|
+
| `compute_support_series(f, psd, case_info)` | Return intermediate arrays for visualization |
|
|
144
|
+
| `select_peaks_by_frequency_bands(f, h, *, max_peaks, strategy, n_bands)` | Band-balanced down-selection |
|
|
145
|
+
| `find_nearest_frequency(target, frequencies, heights)` | Closest detected frequency lookup |
|
|
146
|
+
| `DetectionConfig` | Frozen dataclass with 17 parameters |
|
|
147
|
+
| `detect_peaks_case_adaptive(...)` | Deprecated alias for `dsgbr_detector` |
|
|
148
|
+
| `DSGBR_PARAM_ALIASES` | Short-to-long parameter name mapping |
|
|
149
|
+
|
|
150
|
+
## Examples
|
|
151
|
+
|
|
152
|
+
See [`examples/`](examples/) for runnable scripts:
|
|
153
|
+
|
|
154
|
+
- **`basic_usage.py`** — minimal detection example
|
|
155
|
+
- **`parameter_tuning.py`** — sweep ratio_threshold, compare peak counts
|
|
156
|
+
- **`visualization.py`** — SEARCH/BASELINE overlay plot
|
|
157
|
+
|
|
158
|
+
## How it works
|
|
159
|
+
|
|
160
|
+
DSGBR applies two Savitzky-Golay passes at different scales to separate
|
|
161
|
+
sharp spectral peaks from the slowly varying baseline. The ratio between
|
|
162
|
+
these two series naturally highlights peaks above the local background,
|
|
163
|
+
making the detector robust to spectral slope and broadband noise. For a
|
|
164
|
+
detailed description, see [`docs/algorithm.md`](docs/algorithm.md).
|
|
165
|
+
|
|
166
|
+
## Citation
|
|
167
|
+
|
|
168
|
+
If you use DSGBR in your research, please cite:
|
|
169
|
+
|
|
170
|
+
```bibtex
|
|
171
|
+
@software{dsgbr2026,
|
|
172
|
+
author = {Frantz, Ricardo},
|
|
173
|
+
title = {{DSGBR}: Dual Savitzky--Golay Baseline Ratio spectral peak detector},
|
|
174
|
+
year = {2026},
|
|
175
|
+
url = {https://github.com/ricardofrantz/dsgbr},
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## License
|
|
180
|
+
|
|
181
|
+
BSD 3-Clause. See [LICENSE](LICENSE).
|
|
182
|
+
|
|
183
|
+
## Contributing
|
|
184
|
+
|
|
185
|
+
Contributions are welcome. Please open an issue to discuss changes before
|
|
186
|
+
submitting a pull request. Run the full QA suite before submitting:
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
uv pip install -e ".[dev]"
|
|
190
|
+
pre-commit run --all-files
|
|
191
|
+
pytest --cov=dsgbr
|
|
192
|
+
```
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "dsgbr"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Dual Savitzky–Golay Baseline Ratio (DSGBR) spectral peak detector"
|
|
9
|
+
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "BSD-3-Clause"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Ricardo Frantz" }]
|
|
14
|
+
maintainers = [{ name = "Ricardo Frantz" }]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"numpy>=1.24",
|
|
17
|
+
"scipy>=1.10",
|
|
18
|
+
]
|
|
19
|
+
keywords = [
|
|
20
|
+
"spectral-analysis",
|
|
21
|
+
"peak-detection",
|
|
22
|
+
"signal-processing",
|
|
23
|
+
"savitzky-golay",
|
|
24
|
+
"power-spectral-density",
|
|
25
|
+
"fluid-dynamics",
|
|
26
|
+
]
|
|
27
|
+
classifiers = [
|
|
28
|
+
"Development Status :: 4 - Beta",
|
|
29
|
+
"Intended Audience :: Education",
|
|
30
|
+
"Intended Audience :: Science/Research",
|
|
31
|
+
"Operating System :: OS Independent",
|
|
32
|
+
"Programming Language :: Python :: 3",
|
|
33
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
34
|
+
"Programming Language :: Python :: 3.10",
|
|
35
|
+
"Programming Language :: Python :: 3.11",
|
|
36
|
+
"Programming Language :: Python :: 3.12",
|
|
37
|
+
"Topic :: Scientific/Engineering",
|
|
38
|
+
"Typing :: Typed",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[project.optional-dependencies]
|
|
42
|
+
plotting = [
|
|
43
|
+
"matplotlib>=3.7",
|
|
44
|
+
]
|
|
45
|
+
tests = [
|
|
46
|
+
"coverage[toml]>=7.5.3",
|
|
47
|
+
"hypothesis>=6.100",
|
|
48
|
+
"pytest-cov>=5.0.0",
|
|
49
|
+
"pytest>=8.2.2",
|
|
50
|
+
]
|
|
51
|
+
tests-extra = [
|
|
52
|
+
"pytest-randomly==3.16.0",
|
|
53
|
+
"pytest-rerunfailures==15.1",
|
|
54
|
+
"pytest-xdist==3.8.0",
|
|
55
|
+
]
|
|
56
|
+
qa = [
|
|
57
|
+
"codespell>=2.4.1",
|
|
58
|
+
"mypy>=1.11.0",
|
|
59
|
+
"pre-commit>=3.8.0",
|
|
60
|
+
"ruff>=0.7.0",
|
|
61
|
+
]
|
|
62
|
+
dev = [
|
|
63
|
+
"build",
|
|
64
|
+
"coverage[toml]>=7.5.3",
|
|
65
|
+
"codespell>=2.4.1",
|
|
66
|
+
"hypothesis>=6.100",
|
|
67
|
+
"mypy>=1.11.0",
|
|
68
|
+
"pre-commit>=3.8.0",
|
|
69
|
+
"pytest>=8.2.2",
|
|
70
|
+
"pytest-cov>=5.0.0",
|
|
71
|
+
"pytest-randomly==3.16.0",
|
|
72
|
+
"pytest-rerunfailures==15.1",
|
|
73
|
+
"pytest-xdist==3.8.0",
|
|
74
|
+
"ruff>=0.7.0",
|
|
75
|
+
"twine",
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
[project.urls]
|
|
79
|
+
Homepage = "https://github.com/ricardofrantz/dsgbr"
|
|
80
|
+
Source = "https://github.com/ricardofrantz/dsgbr"
|
|
81
|
+
Tracker = "https://github.com/ricardofrantz/dsgbr/issues"
|
|
82
|
+
Documentation = "https://github.com/ricardofrantz/dsgbr#readme"
|
|
83
|
+
|
|
84
|
+
[tool.setuptools.packages.find]
|
|
85
|
+
where = ["src"]
|
|
86
|
+
|
|
87
|
+
[tool.ruff]
|
|
88
|
+
line-length = 100
|
|
89
|
+
target-version = "py310"
|
|
90
|
+
extend-exclude = ["docs/conf.py"]
|
|
91
|
+
|
|
92
|
+
[tool.ruff.lint]
|
|
93
|
+
select = ["E", "F", "I", "B", "UP", "SIM", "RUF", "D"]
|
|
94
|
+
ignore = ["E501", "D100", "D104"]
|
|
95
|
+
|
|
96
|
+
[tool.ruff.lint.pydocstyle]
|
|
97
|
+
convention = "numpy"
|
|
98
|
+
|
|
99
|
+
[tool.ruff.lint.extend-per-file-ignores]
|
|
100
|
+
"__init__.py" = ["F401"]
|
|
101
|
+
"tests/*" = ["D"]
|
|
102
|
+
"examples/*" = ["D"]
|
|
103
|
+
|
|
104
|
+
[tool.mypy]
|
|
105
|
+
pretty = true
|
|
106
|
+
strict = true
|
|
107
|
+
python_version = "3.10"
|
|
108
|
+
exclude = ["docs/conf.py"]
|
|
109
|
+
|
|
110
|
+
[tool.codespell]
|
|
111
|
+
skip = "docs/conf.py"
|
|
112
|
+
check-filenames = true
|
|
113
|
+
|
|
114
|
+
[tool.coverage.run]
|
|
115
|
+
branch = true
|
|
116
|
+
relative_files = true
|
|
117
|
+
source = ["dsgbr"]
|
|
118
|
+
omit = ["*/__pycache__/*"]
|
|
119
|
+
|
|
120
|
+
[tool.coverage.report]
|
|
121
|
+
fail_under = 90
|
|
122
|
+
precision = 2
|
|
123
|
+
show_missing = true
|
dsgbr-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Backward-compatibility shim for ``from dsgbr.DSGBR import ...``.
|
|
2
|
+
|
|
3
|
+
This module re-exports the full public API so that existing call sites
|
|
4
|
+
(e.g. ``from dsgbr.DSGBR import dsgbr_detector``) continue to work
|
|
5
|
+
unchanged after the package restructure.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dsgbr._compat import (
|
|
9
|
+
DSGBR_PARAM_ALIASES,
|
|
10
|
+
DSGBRDetectionConfig,
|
|
11
|
+
detect_peaks_case_adaptive,
|
|
12
|
+
)
|
|
13
|
+
from dsgbr._config import DetectionConfig
|
|
14
|
+
from dsgbr._detector import compute_support_series, dsgbr_detector
|
|
15
|
+
from dsgbr._selection import (
|
|
16
|
+
find_nearest_frequency,
|
|
17
|
+
select_peaks_by_frequency_bands,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"DSGBR_PARAM_ALIASES",
|
|
22
|
+
"DSGBRDetectionConfig",
|
|
23
|
+
"DetectionConfig",
|
|
24
|
+
"compute_support_series",
|
|
25
|
+
"detect_peaks_case_adaptive",
|
|
26
|
+
"dsgbr_detector",
|
|
27
|
+
"find_nearest_frequency",
|
|
28
|
+
"select_peaks_by_frequency_bands",
|
|
29
|
+
]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Dual Savitzky-Golay Baseline Ratio (DSGBR) spectral peak detector.
|
|
2
|
+
|
|
3
|
+
The DSGBR detector applies two Savitzky-Golay smoothing passes -- one to build
|
|
4
|
+
the SEARCH series, another to obtain a broader BASELINE -- and accepts peaks
|
|
5
|
+
where the SEARCH/BASELINE ratio exceeds a configurable threshold.
|
|
6
|
+
|
|
7
|
+
Public API
|
|
8
|
+
----------
|
|
9
|
+
.. autosummary::
|
|
10
|
+
DetectionConfig
|
|
11
|
+
dsgbr_detector
|
|
12
|
+
compute_support_series
|
|
13
|
+
select_peaks_by_frequency_bands
|
|
14
|
+
find_nearest_frequency
|
|
15
|
+
detect_peaks_case_adaptive
|
|
16
|
+
DSGBRDetectionConfig
|
|
17
|
+
DSGBR_PARAM_ALIASES
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from dsgbr._compat import DSGBR_PARAM_ALIASES, DSGBRDetectionConfig, detect_peaks_case_adaptive
|
|
21
|
+
from dsgbr._config import DetectionConfig
|
|
22
|
+
from dsgbr._detector import compute_support_series, dsgbr_detector
|
|
23
|
+
from dsgbr._selection import find_nearest_frequency, select_peaks_by_frequency_bands
|
|
24
|
+
|
|
25
|
+
__version__ = "0.1.0"
|
|
26
|
+
__all__ = [
|
|
27
|
+
"DSGBR_PARAM_ALIASES",
|
|
28
|
+
"DSGBRDetectionConfig",
|
|
29
|
+
"DetectionConfig",
|
|
30
|
+
"compute_support_series",
|
|
31
|
+
"detect_peaks_case_adaptive",
|
|
32
|
+
"dsgbr_detector",
|
|
33
|
+
"find_nearest_frequency",
|
|
34
|
+
"select_peaks_by_frequency_bands",
|
|
35
|
+
]
|