Skip to main content

DSGBR banner

PyPI Documentation CI Python License

Dual Savitzky-Golay Baseline Ratio (DSGBR) detects spectral peaks in frequency-domain signals. It is built for dense, noisy power spectra from fluid dynamics, vibration analysis, and related experimental work, where spectra slope over several decades and a fixed prominence threshold either drowns in low-frequency power or misses everything at high frequency.

DSGBR detection example

A ten-harmonic frequency comb in a noisy, sloped synthetic spectrum: all ten harmonics recovered with the default detector parameters, and no detections away from the comb. The weakest harmonics barely clear the noise scatter, and the comb crowds together on the log axis — both regimes where fixed-threshold detectors merge or miss peaks. Generated by examples/readme_figure.py.

Install

pip install dsgbr

For development:

git clone https://github.com/openfluids/dsgbr.git
cd dsgbr
uv pip install -e ".[dev]"

Quick start

import numpy as np
from dsgbr import dsgbr_detector

# Synthetic PSD with known peaks
frequencies = np.linspace(0.001, 1.0, 2048)
psd = np.ones_like(frequencies)
psd[400] = 12.0  # inject a peak
psd[1200] = 8.0  # inject another

peak_f, peak_h = dsgbr_detector(frequencies, psd)
print(f"Detected {peak_f.size} peaks at f = {peak_f}")

The defaults find both peaks. To tune, pass a case_info dictionary with full names or short aliases:

peak_f, peak_h = dsgbr_detector(frequencies, psd, case_info={"RT": 2.5, "SW": 5})

How it works

DSGBR compares the spectrum against a local estimate of its own background, so acceptance is a ratio rather than an absolute height: a peak three times above its surroundings is detected the same way at any frequency, regardless of the slope between them.

flowchart TD
    PSD(["PSD P(f)"]) --> SEARCH["SEARCH
    narrow Savitzky-Golay smooth"]
    PSD --> BASELINE["BASELINE
    wide rolling median"]
    SEARCH --> RATIO{"SEARCH / BASELINE
    ≥ ratio_threshold ?"}
    BASELINE --> RATIO
    RATIO --> SPACING["greedy spacing selection
    (strongest first)"]
    SPACING --> REFINE["refine to raw-PSD maxima"]
    REFINE --> ULF["ultra-low-frequency
    Q-factor guardrail"]
    ULF --> BANDS["band-balanced selection
    (if > max_peaks)"]
    BANDS --> OUT(["peak_frequencies, peak_heights"])

The two series are deliberately different estimators: SEARCH is a narrow Savitzky-Golay smooth that suppresses single-bin noise while keeping peak shapes, and BASELINE is a wide rolling median of the raw PSD, which sits under narrow peaks instead of being dragged up by them. Accepted candidates then pass spacing rules, are repositioned onto the raw PSD, and survive an ultra-low-frequency guardrail that rejects broad leakage bumps near the left edge of the spectrum. The full pipeline, design rationale, and parameter sensitivity study are in docs/algorithm.md.

Validation

Scenario DSGBR F1 tuned find_peaks F1
clean_tones 1.000 ± 0.000 1.000 ± 0.000
dense_lowfreq 0.680 ± 0.253 0.440 ± 0.310
steep_slope 0.967 ± 0.103 0.872 ± 0.149
noisy_welch 0.449 ± 0.249 0.296 ± 0.312
no_peaks 0.000 ± 0.000 0.000 ± 0.000

Fresh run: uv run python -m benchmarks.compare, 20 evaluation realizations per scenario after per-scenario tuning of scipy.signal.find_peaks on 8 training realizations.

DSGBR leads on the sloped, dense, and noisy scenarios above; parity is expected for flat, well-separated peaks such as clean_tones, and both detectors return no F1 credit on no_peaks. See docs/algorithm.md Parameter sensitivity and benchmarks/ for reproduction.

Real measurements

The scenarios above are synthetic, so every property of the signal is one the test author chose. They are complemented by recorded bearing vibration from the CWRU Bearing Data Center, where the truth is not hand-labelled: fault frequencies are fixed multiples of shaft speed set by bearing geometry, and each recording carries its own shaft speed, so the expected peaks are computed rather than asserted.

Recording Fault DSGBR tuned find_peaks
130, outer race BPFO 8/8 harmonics, 25 peaks 8/8 harmonics, 439 peaks
105, inner race BPFI 6/8 harmonics, 25 peaks 6/8 harmonics, 572 peaks

Both detectors see the identical envelope spectrum, and find_peaks is tuned per recording while DSGBR runs at defaults. The recall matches; the difference is that DSGBR reaches it with roughly twenty times fewer detections.

Reproduce with uv run --extra tests python -m benchmarks.real.compare. The recordings are downloaded on demand into ~/.cache/dsgbr/cwru and verified against a recorded checksum -- they are not redistributed here. Set DSGBR_CWRU_DATA_DIR to relocate or reuse an existing copy.

Configuration

All parameters are set through DetectionConfig or passed as a dictionary via the case_info argument. Short aliases (RT, SW, BWF, etc.) are supported for concise configuration.

Parameter Alias Default Description
ratio_threshold RT 3.3 Min SEARCH/BASELINE ratio for acceptance
smooth_window SW 3 Savitzky-Golay window for SEARCH (odd, >= 3)
baseline_window_frac BWF 0.05 Baseline window as fraction of data length
distance_low DL 2 Min bin separation below switch_frequency
distance_high DH 5 Min bin separation above switch_frequency
switch_frequency SF 0.02 Frequency threshold for spacing rules
max_peaks MP 25 Maximum peaks returned
smooth_polyorder — 2 Polynomial order for SG filter
smooth_on_log — True Smooth log10(PSD) instead of linear
baseline_window — None Fixed baseline window (overrides BWF)
baseline_on_log — True Baseline smoothing in log domain
band_strategy — proportional Band allocation: proportional or equal
n_bands — 10 Number of logarithmic frequency bands
ulf_fmax — 0.001 ULF band upper frequency limit
ulf_min_q — 9.0 Minimum Q-factor for ULF peaks
ulf_max_points — 5 Maximum ULF peaks to retain
interpolate_peaks IP False Sub-bin peak positions via parabolic fit

Advanced usage

Support series for visualization

from dsgbr import compute_support_series

support = compute_support_series(frequencies, psd, case_info={"RT": 2.0})

# Plot SEARCH vs BASELINE overlay
import matplotlib.pyplot as plt
plt.semilogy(frequencies, support["search_series"], label="SEARCH")
plt.semilogy(frequencies, support["baseline_series"], label="BASELINE")
plt.semilogy(frequencies, support["rthreshold"], "--", label="Threshold")
plt.legend()
plt.show()

Band-balanced peak selection

from dsgbr import select_peaks_by_frequency_bands

# Reduce 100 peaks to 15, spread across frequency bands
sel_f, sel_h = select_peaks_by_frequency_bands(
    peak_frequencies, peak_heights,
    max_peaks=15, strategy="proportional", n_bands=8,
)

Configuration via dataclass

from dsgbr import DetectionConfig

cfg = DetectionConfig(ratio_threshold=2.5, smooth_window=7, max_peaks=10)
print(cfg.to_metadata())

API reference

Function / Class Description
dsgbr_detector(f, psd, *, case_info, return_support) Main detection pipeline
compute_support_series(f, psd, case_info) Return intermediate arrays for visualization
select_peaks_by_frequency_bands(f, h, *, max_peaks, strategy, n_bands) Band-balanced down-selection
find_nearest_frequency(target, frequencies, heights) Closest detected frequency lookup
DetectionConfig Frozen dataclass with 17 parameters
detect_peaks_case_adaptive(...) Deprecated alias for dsgbr_detector
DSGBR_PARAM_ALIASES Short-to-long parameter name mapping

Examples

See examples/ for runnable scripts:

  • basic_usage.py — minimal detection example
  • parameter_tuning.py — sweep ratio_threshold, compare peak counts
  • visualization.py — SEARCH/BASELINE overlay plot
  • readme_figure.py — regenerate the figure at the top of this page

Citation

If you use DSGBR in your research, please cite:

@software{dsgbr2026,
  author = {Frantz, Ricardo},
  title = {{DSGBR}: Dual Savitzky--Golay Baseline Ratio spectral peak detector},
  year = {2026},
  url = {https://github.com/openfluids/dsgbr},
}

License

This project is licensed under Apache-2.0.

As of v0.5.0, this project is licensed under Apache-2.0. Earlier releases remain available under the BSD 3-Clause license that applied at the time of release.

Originally developed by Ricardo A S Frantz. See LICENSE and NOTICE for license terms and attribution notices.

Contributing

Contributions are welcome. Please open an issue to discuss changes before submitting a pull request. Run the full QA suite before submitting:

uv pip install -e ".[dev]"
pre-commit run --all-files
pytest --cov=dsgbr

Release files for dsgbr 0.7.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for dsgbr 0.7.0
File Size Uploaded
dsgbr-0.7.0.tar.gz 48.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for dsgbr 0.7.0
File Interpreter ABI Platform
dsgbr-0.7.0-py3-none-any.whl Python 3 none any Details

Total release size: 74.2 kB

Release files / dsgbr-0.7.0.tar.gz

Download URL dsgbr-0.7.0.tar.gz
Size 48.2 kB
Tags Source
SHA-256 checksum
How to use checksums
a96e07c1afef57c592437799805153c0223644cc700d03289b157918f6eb6a9c
BLAKE2b-256 checksum
How to use checksums
f8e9d6f3ab41831de1a78316c6edf9b3244fb71278d5aca4480de9819fab2476
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 25, 2026.

Transparency log

Release files / dsgbr-0.7.0-py3-none-any.whl

Download URL dsgbr-0.7.0-py3-none-any.whl
Size 26.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
150cfc11f10ead0238bf94e102b1564106a24ec696ae8e185f0b574ae4542a13
BLAKE2b-256 checksum
How to use checksums
d3f6e322a2caef2c4720406fcd641469cfde87ac3b62d86c663d4fafa325447c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 25, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.7.0 This release

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page