Skip to content

feat: add IEC 61260-1 nominal frequency labels#46

Merged
jmrplens merged 2 commits intomainfrom
feature/nominal-freq-labels-v2
Mar 8, 2026
Merged

feat: add IEC 61260-1 nominal frequency labels#46
jmrplens merged 2 commits intomainfrom
feature/nominal-freq-labels-v2

Conversation

@jmrplens
Copy link
Owner

@jmrplens jmrplens commented Mar 8, 2026

Summary

Rebased and refined version of #44 (by @ninoblumer) — adds IEC 61260-1 nominal frequency labels to octave/fractional-octave band analysis.

Changes from original PR #44

Features (from #44)

  • nominal=True parameter on OctaveFilterBank.filter() and octavefilter()
  • Returns human-readable IEC 61260-1 labels (e.g. "1k", "31.5", "12.5k") instead of numeric frequencies
  • getansifrequencies() now returns 4-tuple including labels
  • OctaveFilterBank.nominal_freq property for direct label access
  • 16 new tests covering label formatting, integration, edge cases

CI Status

  • ✅ ruff (lint)
  • ✅ mypy (type checking)
  • ✅ pytest (111 tests pass)

Closes #44

Summary by Sourcery

Add optional IEC 61260-1 nominal frequency labels to octave and fractional-octave band analysis outputs.

New Features:

  • Introduce a nominal parameter on OctaveFilterBank.filter and octavefilter to return IEC 61260-1 nominal frequency labels instead of numeric center frequencies.
  • Expose nominal_freq labels on OctaveFilterBank instances for direct access to band labels.
  • Extend getansifrequencies and internal frequency generation to compute and return nominal label strings alongside numeric band data.

Enhancements:

  • Implement IEC 61260-1–compliant helpers for computing and formatting nominal band frequencies, including preferred number snapping and significant-figure rounding.
  • Ensure nominal labels remain aligned with bands after Nyquist-limit trimming by reusing label slices instead of recomputing.

Tests:

  • Add comprehensive tests for IEC 61260-1 rounding and formatting helpers, nominal label generation, and end-to-end use via OctaveFilterBank.filter and octavefilter.
  • Update existing tests to account for getansifrequencies returning an additional labels element.

Summary by CodeRabbit

  • New Features
    • Optional nominal frequency labels (IEC 61260‑1) — request human-readable band labels (e.g., "1k", "500") instead of numeric center frequencies; default unchanged.
  • API Changes
    • Filter and helper calls accept an opt-in nominal flag and return labels when enabled; per‑band signal outputs also support labels.
  • Tests
    • New tests validating nominal label generation, formatting, and behavior across modes.
  • Documentation
    • Docstrings updated to describe the nominal option and its effects.
  • Chores
    • Package version bumped to 1.2.0; CI/analysis exclusions updated.

@sourcery-ai
Copy link

sourcery-ai bot commented Mar 8, 2026

Reviewer's Guide

Adds opt-in IEC 61260-1 nominal frequency label support across the octave filter API, extending frequency generation to return labels, wiring them through OctaveFilterBank and the octavefilter wrapper, and adding helpers/tests for nominal rounding and formatting.

Sequence diagram for octavefilter nominal label flow

sequenceDiagram
    actor User
    participant OctaveFilterFunction as octavefilter
    participant FilterBank as OctaveFilterBank
    participant FreqGen as _genfreqs
    participant ANSI as getansifrequencies

    User->>OctaveFilterFunction: octavefilter(x, fs, ..., nominal=True)
    OctaveFilterFunction->>FilterBank: __init__(limits, fraction, fs)
    FilterBank->>FreqGen: _genfreqs(limits, fraction, fs)
    FreqGen->>ANSI: getansifrequencies(fraction, limits)
    ANSI-->>FreqGen: freq, freq_d, freq_u, labels
    FreqGen-->>FilterBank: freq, freq_d, freq_u, labels
    OctaveFilterFunction->>FilterBank: filter(x, sigbands, mode, detrend, calculate_level, nominal=True)
    FilterBank->>FilterBank: compute SPL bands
    FilterBank->>FilterBank: freq_out = nominal_freq
    FilterBank-->>OctaveFilterFunction: spl, nominal_freq[, sigbands]
    OctaveFilterFunction-->>User: spl, List[str] nominal labels[, sigbands]
Loading

Class diagram for octave filter API with IEC 61260-1 nominal labels

classDiagram
    class OctaveFilterBank {
        +List[float] freq
        +List[float] freq_d
        +List[float] freq_u
        +List[str] nominal_freq
        +__init__(limits: List[float], fraction: float, fs: int, order: int, stateful: bool)
        +filter(x: List[float] | np.ndarray, sigbands: bool, mode: str, detrend: bool, calculate_level: bool, nominal: bool) Tuple
        +_process_bands(x: List[float] | np.ndarray, sigbands: bool, mode: str, detrend: bool, calculate_level: bool) Tuple
    }

    class FrequenciesModule {
        +getansifrequencies(fraction: float, limits: List[float] | None) Tuple[List[float], List[float], List[float], List[str]]
        +_genfreqs(limits: List[float], fraction: float, fs: int) Tuple[List[float], List[float], List[float], List[str]]
        +_iec_e3_round(f: float) float
        +_nominal_freq_for_band(exact_freq: float, fraction: float) float
        +_format_nominal_freq(f: float) str
        +_deleteouters(freq: List[float], freq_d: List[float], freq_u: List[float], fs: int) Tuple[List[float], List[float], List[float]]
        +normalizedfreq(fraction: int) List[float]
    }

    class OctaveFilterFunction {
        +octavefilter(x: List[float] | np.ndarray, fs: int, fraction: float, order: int, limits: List[float] | None, show: bool, sigbands: bool, plot_file: str | None, detrend: bool, filter_type: str, ripple: float, attenuation: float, calibration_factor: float, dbfs: bool, mode: str, nominal: bool) Tuple
    }

    OctaveFilterBank ..> FrequenciesModule : uses _genfreqs
    OctaveFilterFunction ..> OctaveFilterBank : creates
    OctaveFilterFunction ..> OctaveFilterBank : calls filter(nominal)
    FrequenciesModule ..> FrequenciesModule : getansifrequencies calls _nominal_freq_for_band
    FrequenciesModule ..> FrequenciesModule : _nominal_freq_for_band calls normalizedfreq
    FrequenciesModule ..> FrequenciesModule : _nominal_freq_for_band calls _iec_e3_round
Loading

File-Level Changes

Change Details Files
Propagate nominal frequency labels through OctaveFilterBank and its filter API.
  • Extend _genfreqs to return nominal label list alongside center/lower/upper band frequencies and adapt its internal call to getansifrequencies and _deleteouters.
  • Store nominal labels as self.nominal_freq on OctaveFilterBank during initialization and keep num_bands consistent with the number of labels.
  • Add overloads and a nominal parameter to OctaveFilterBank.filter, returning either float frequencies or string labels depending on the flag.
  • Use self.nominal_freq when nominal=True while preserving existing behaviour (float frequencies) when nominal=False, for both sigbands and non-sigbands cases.
src/pyoctaveband/core.py
Extend ANSI/IEC frequency generation to compute and format IEC 61260-1 nominal labels.
  • Change getansifrequencies to return a 4-tuple including nominal label strings and update its docstring accordingly.
  • Introduce _iec_e3_round implementing IEC 61260-1 Annex E.3 significant-figure rounding behaviour.
  • Add _nominal_freq_for_band to map exact mid-band frequencies to IEC nominal values, using normalizedfreq tables for 1/3-octave and E.3 rounding for other fractions.
  • Add _format_nominal_freq to convert nominal numeric values to human-readable labels with k-suffix above 1 kHz.
  • Refactor _genfreqs to reuse labels from getansifrequencies and slice them after Nyquist filtering instead of recomputing.
src/pyoctaveband/frequencies.py
Expose nominal label support through the public octavefilter wrapper and update type hints.
  • Add a nominal parameter (default False) and corresponding Literal[False]/Literal[True] overloads to octavefilter so callers can opt into label strings while preserving existing default behaviour.
  • Update the octavefilter return-type union to cover both float and string frequency lists, with and without sigbands.
  • Thread the nominal flag through to OctaveFilterBank.filter in both sigbands and non-sigbands paths and use type: ignore annotations to satisfy mypy for overload dispatch.
src/pyoctaveband/__init__.py
Update and extend tests to cover nominal label behaviour and the updated frequency API.
  • Adjust existing coverage test to handle the new 4-tuple return from getansifrequencies.
  • Add a dedicated test_nominal_frequencies module to validate _iec_e3_round, _nominal_freq_for_band, and _format_nominal_freq against expected IEC-style rounding and formatting.
  • Add integration tests for getansifrequencies label output, OctaveFilterBank.nominal_freq, and nominal=True behaviour on both OctaveFilterBank.filter and octavefilter, including sigbands and default nominal=False cases.
tests/test_coverage_fix.py
tests/test_nominal_frequencies.py

Possibly linked issues

  • #: PR adds nominal=True and IEC 61260-1 label support exactly as requested in the issue, without changing defaults.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai
Copy link

coderabbitai bot commented Mar 8, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Adds IEC 61260-1 nominal frequency label support: frequency generators now return nominal labels; OctaveFilterBank stores nominal_freq; filter() and top-level octavefilter() accept nominal: bool = False and can return string labels instead of numeric center frequencies when requested.

Changes

Cohort / File(s) Summary
Public API / Entrypoint
src/pyoctaveband/__init__.py
Version bump to 1.2.0; octavefilter overloads and implementation gain nominal parameter; overloads updated to reflect float vs string frequency return types; nominal propagated to filter calls.
Core Filter Implementation
src/pyoctaveband/core.py
OctaveFilterBank now stores nominal_freq; filter() overloads and implementation accept nominal: bool = False and return either numeric centers or nominal string labels; internal calls pass nominal through.
Frequency Generation & Helpers
src/pyoctaveband/frequencies.py
getansifrequencies and _genfreqs now return a 4‑tuple including nominal labels; added helpers _iec_e3_round, _extended_preferred, _nominal_freq_for_band, _format_nominal_freq; labels sliced to match filtered bands.
Tests (updates & new)
tests/test_coverage_fix.py, tests/test_nominal_frequencies.py
Updated coverage test to unpack the 4‑tuple; new comprehensive tests for IEC rounding, nominal mapping, label formatting, nominal_freq attribute, and filter/octavefilter behavior with nominal True/False.
Tooling / Metadata
sonar-project.properties, pyproject.toml
Added sonar exclusions for overload-heavy files; bumped project version in pyproject.toml to 1.2.0.

Sequence Diagram

sequenceDiagram
    participant User
    participant octavefilter as "octavefilter()"
    participant FilterBank as "OctaveFilterBank.filter()"
    participant FreqGen as "FrequencyGeneration"

    User->>octavefilter: call(signal, nominal=True)
    octavefilter->>FilterBank: filter(signal, ..., nominal=True)
    FilterBank->>FreqGen: request bands (compute center, edges, nominal labels)
    FreqGen-->>FilterBank: (centers, lower, upper, nominal_labels)
    FilterBank-->>octavefilter: (ndarray, nominal_labels, sigbands?)
    octavefilter-->>User: return result with nominal labels
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 Hopping through bands both wide and nominal,

I count "1k", "500" with standards so formal,
Floats still leap when exactness you choose,
Ask for labels and IEC sings the muse,
Tiny whiskers applaud the new formal.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.23% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add IEC 61260-1 nominal frequency labels' accurately and concisely describes the main feature being introduced across all modified files.
Linked Issues check ✅ Passed The PR fully implements all objectives from issue #44: IEC 61260-1 nominal frequency helpers, 4-tuple getansifrequencies, OctaveFilterBank.nominal_freq attribute, nominal parameter for filter/octavefilter functions, comprehensive tests, and version bump to 1.2.0.
Out of Scope Changes check ✅ Passed All changes are directly aligned with linked issue #44 requirements: frequency label generation, API extensions, filter enhancements, test coverage, sonar exclusions for overloads, and version updates are all in-scope.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/nominal-freq-labels-v2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the pyoctaveband library by implementing support for IEC 61260-1 nominal frequency labels. This feature provides a more standardized and user-friendly representation of frequency bands in octave and fractional-octave analysis, making the output more intuitive for users. The changes involve extending the API with a new nominal parameter, updating frequency generation logic, and ensuring robust label formatting, all while maintaining high code quality and comprehensive test coverage.

Highlights

  • Nominal Frequency Labels: Introduced a nominal=True parameter for OctaveFilterBank.filter() and octavefilter() functions, enabling the return of human-readable IEC 61260-1 frequency labels (e.g., "1k", "31.5") instead of exact numeric frequencies.
  • API Enhancements: Updated getansifrequencies() to return a 4-tuple including these new nominal labels and added an OctaveFilterBank.nominal_freq property for direct access to the labels.
  • Code Quality and Stability: Resolved six merge conflicts in core.py overloads, fixed linting issues, and addressed mypy type checking errors, ensuring the stability and maintainability of the codebase.
  • Performance Optimization: Eliminated duplicate label computation by reusing labels from getansifrequencies() via slicing within _genfreqs().
  • Documentation and Testing: Added a docstring to _nominal_freq_for_band explaining fallback for non-standard fractions and included 16 new tests covering label formatting, integration, and edge cases for the new feature.
Changelog
  • src/pyoctaveband/init.py
    • Added nominal: Literal[False] = False to existing octavefilter overloads.
    • Introduced new octavefilter overloads for nominal: Literal[True] returning List[str].
    • Updated the main octavefilter function signature to include nominal: bool = False and adjusted return type hints.
    • Modified calls to filter_bank.filter to pass the nominal parameter.
  • src/pyoctaveband/core.py
    • Modified OctaveFilterBank.__init__ to store nominal_freq from _genfreqs.
    • Added nominal: Literal[False] = False to existing filter method overloads.
    • Introduced new filter method overloads for nominal: Literal[True] returning List[str].
    • Updated the main filter method signature to include nominal: bool = False and adjusted return type hints.
    • Modified the filter method's return logic to output self.nominal_freq when nominal is True.
  • src/pyoctaveband/frequencies.py
    • Updated getansifrequencies to return a 4-tuple including List[str] for nominal labels.
    • Added logic within getansifrequencies to generate and return nominal labels.
    • Modified _genfreqs to return a 4-tuple including nominal labels and to slice labels after _deleteouters.
    • Introduced new helper functions: _iec_e3_round, _nominal_freq_for_band, and _format_nominal_freq for calculating and formatting nominal frequencies.
  • tests/test_coverage_fix.py
    • Updated the test_octavefilter_limits_none function to unpack the 4-tuple returned by getansifrequencies.
  • tests/test_nominal_frequencies.py
    • Added a new test file to cover the new nominal frequency functionality, including tests for rounding, nominal frequency determination, formatting, getansifrequencies output, OctaveFilterBank.nominal_freq attribute, and filter and octavefilter functions with nominal=True/False.
Activity
  • This pull request is a rebased and refined version of a previous pull request (feat: add IEC 61260-1 nominal frequency labels (closes #nominal) #44 by @ninoblumer).
  • Six merge conflicts in core.py overloads were resolved during the rebase process.
  • Linting checks using ruff passed successfully.
  • Type checking with mypy passed successfully.
  • All 111 pytest tests passed, including 16 new tests specifically for the nominal frequency feature.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • Changing getansifrequencies from a 3-tuple to a 4-tuple is a breaking API change for any external callers; consider either adding a new helper for labeled frequencies or making the additional labels optional (e.g., via a flag or a small data structure) to preserve the original return signature.
  • In _genfreqs, the logic assumes _deleteouters only drops trailing bands and then slices labels accordingly; if _deleteouters behavior ever changes (e.g., removes bands in the middle), labels will desynchronize—consider having _deleteouters return the indices or operate on a single structure so labels and frequencies stay aligned by construction.
  • The return type annotations for filter/octavefilter are becoming quite complex with large unions; consider introducing a type alias or narrowing overloads (e.g., grouping by nominal/sigbands) to keep the public type surface easier to read and maintain.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Changing `getansifrequencies` from a 3-tuple to a 4-tuple is a breaking API change for any external callers; consider either adding a new helper for labeled frequencies or making the additional labels optional (e.g., via a flag or a small data structure) to preserve the original return signature.
- In `_genfreqs`, the logic assumes `_deleteouters` only drops trailing bands and then slices `labels` accordingly; if `_deleteouters` behavior ever changes (e.g., removes bands in the middle), labels will desynchronize—consider having `_deleteouters` return the indices or operate on a single structure so labels and frequencies stay aligned by construction.
- The return type annotations for `filter`/`octavefilter` are becoming quite complex with large unions; consider introducing a type alias or narrowing overloads (e.g., grouping by `nominal`/`sigbands`) to keep the public type surface easier to read and maintain.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a great new feature for displaying IEC 61260-1 nominal frequency labels. The implementation is solid, with good use of Python's typing features for clarity and correctness. The new functionality is also well-covered by a new suite of tests. I've found a couple of opportunities for minor refactoring to improve maintainability and performance. Overall, this is a high-quality contribution.

Comment on lines +193 to +196
if sigbands:
return filter_bank.filter(x, sigbands=True, mode=mode, detrend=detrend)
return filter_bank.filter(x, sigbands=True, mode=mode, detrend=detrend, nominal=nominal) # type: ignore[call-overload,no-any-return]
else:
return filter_bank.filter(x, sigbands=False, mode=mode, detrend=detrend) No newline at end of file
return filter_bank.filter(x, sigbands=False, mode=mode, detrend=detrend, nominal=nominal) # type: ignore[call-overload,no-any-return]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This if/else block can be simplified into a single return statement by passing the sigbands parameter directly. This reduces code duplication and improves readability.

    return filter_bank.filter(x, sigbands=sigbands, mode=mode, detrend=detrend, nominal=nominal)  # type: ignore[call-overload,no-any-return]

Comment on lines +141 to +153
def _nominal_freq_for_band(exact_freq: float, fraction: float) -> float:
"""Return IEC 61260-1 nominal frequency (float) for an exact mid-band frequency.

For standard fractions (1, 3), snaps to the IEC preferred table via
``normalizedfreq``. For non-standard fractions, falls back to Annex E.3
significant-figure rounding (``_iec_e3_round``).
"""
freq, freq_d, freq_u = getansifrequencies(fraction, limits)
return _deleteouters(freq, freq_d, freq_u, fs)
frac = round(fraction)
if frac in (1, 3):
base = normalizedfreq(frac)
extended: List[float] = [f * (10 ** d) for d in range(-3, 4) for f in base]
return min(extended, key=lambda f: abs(np.log(f / exact_freq)))
return _iec_e3_round(exact_freq)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The extended list of preferred frequencies is re-calculated for every frequency band inside getansifrequencies. This is inefficient. You can cache this list as it only depends on the fraction. Here is a suggestion using a function attribute as a simple cache, which avoids re-computation without needing module-level variables or new imports.

Suggested change
def _nominal_freq_for_band(exact_freq: float, fraction: float) -> float:
"""Return IEC 61260-1 nominal frequency (float) for an exact mid-band frequency.
For standard fractions (1, 3), snaps to the IEC preferred table via
``normalizedfreq``. For non-standard fractions, falls back to Annex E.3
significant-figure rounding (``_iec_e3_round``).
"""
freq, freq_d, freq_u = getansifrequencies(fraction, limits)
return _deleteouters(freq, freq_d, freq_u, fs)
frac = round(fraction)
if frac in (1, 3):
base = normalizedfreq(frac)
extended: List[float] = [f * (10 ** d) for d in range(-3, 4) for f in base]
return min(extended, key=lambda f: abs(np.log(f / exact_freq)))
return _iec_e3_round(exact_freq)
def _nominal_freq_for_band(exact_freq: float, fraction: float) -> float:
"""Return IEC 61260-1 nominal frequency (float) for an exact mid-band frequency.
For standard fractions (1, 3), snaps to the IEC preferred table via
``normalizedfreq``. For non-standard fractions, falls back to Annex E.3
significant-figure rounding (``_iec_e3_round``).
"""
frac = round(fraction)
if frac in (1, 3):
if not hasattr(_nominal_freq_for_band, "_cache"):
_nominal_freq_for_band._cache = {} # type: ignore
if frac not in _nominal_freq_for_band._cache:
base = normalizedfreq(frac)
_nominal_freq_for_band._cache[frac] = [f * (10 ** d) for d in range(-3, 4) for f in base]
extended = _nominal_freq_for_band._cache[frac]
return min(extended, key=lambda f: abs(np.log(f / exact_freq)))
return _iec_e3_round(exact_freq)

@github-actions
Copy link
Contributor

github-actions bot commented Mar 8, 2026

CI Results 🚀

Test Summary

Python Version Tests Failures Coverage Status
macos-latest-3.13 111 0 99.5% ✅ Passed
ubuntu-latest-3.13 111 0 99.5% ✅ Passed
windows-latest-3.13 111 0 99.5% ✅ Passed

Technical Benchmark Summary

📊 View Benchmark Details

PyOctaveBand: Technical Benchmark Report

Generated: 2026-03-08 06:54:00

1. Test Signal Parameters

  • Sample Rate: 96.0 kHz
  • Duration: 10.0 seconds
  • Signal Types: White Noise (Stability) / Pure Sine (Precision)
  • Precision: 64-bit Floating Point

2. Crossover (Linkwitz-Riley)

Crossover

  • Flatness Error: 0.000000 dB (Target < 0.01)

3. Precision & Isolation

Precision

Type Error (dB) Isolation Ripple GD Std (ms)
butter 2.46e-03 31.3 dB 0.2705 dB 2847.826
cheby1 3.38e-03 40.5 dB 0.1000 dB 3551.677
cheby2 3.26e-03 57.8 dB 29.4187 dB 4790.013
ellip 9.41e-03 54.2 dB 0.1000 dB 4700.881
bessel 5.20e-01 32.5 dB 5.9845 dB 1380.212

4. Performance

Performance

Channels Exec Time (s) Speedup
1 0.628 1.00x
2 1.228 1.02x
4 2.444 1.03x
8 4.842 1.04x
16 9.666 1.04x

View Full Artifacts

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/pyoctaveband/__init__.py`:
- Around line 173-175: The docstring return type currently only documents
float-based frequency lists; update the :rtype: to also include the nominal
(List[str]) variants when nominal=True. Specifically, amend the Union so it
includes Tuple[np.ndarray, List[str]] and Tuple[np.ndarray, List[str],
List[np.ndarray]] alongside the existing float-based tuples (referencing the
nominal parameter and the function returning (SPL_array, Frequencies_list) or
(SPL_array, Frequencies_list, signals)). Ensure both 2-tuple and 3-tuple forms
are represented in the :rtype: line.

In `@src/pyoctaveband/frequencies.py`:
- Around line 14-24: The public API of getansifrequencies was changed to return
a 4-tuple which breaks existing callers; update the package/runtime version to
1.2.0 to signal the breaking change and include a clear changelog entry and
release note referencing getansifrequencies so users are aware of the new return
shape, or alternatively restore backward compatibility by providing a wrapper
overload that returns a 3-tuple for older callers and mark it deprecated. Ensure
the version bump (to 1.2.0), changelog entry, and any deprecation comment
reference getansifrequencies and the new 4-tuple return.
- Around line 141-153: The function _nominal_freq_for_band incorrectly uses
round(fraction) so non-integer fractions like 0.6 or 2.6 are coerced into 1 or 3
and routed to the preferred-table lookup; change the logic to only treat
fraction as an integer when it is within a tiny tolerance of an integer (e.g.,
abs(fraction - round(fraction)) < 1e-8) and then cast to int and check if that
int is in (1, 3) before calling normalizedfreq(frac); otherwise fall back to
_iec_e3_round(exact_freq). Ensure you reference _nominal_freq_for_band,
normalizedfreq, and _iec_e3_round when making the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0caf2489-bf83-40bc-85f9-8a66be02d534

📥 Commits

Reviewing files that changed from the base of the PR and between acad961 and 07bbf89.

📒 Files selected for processing (5)
  • src/pyoctaveband/__init__.py
  • src/pyoctaveband/core.py
  • src/pyoctaveband/frequencies.py
  • tests/test_coverage_fix.py
  • tests/test_nominal_frequencies.py

Comment on lines 14 to 24
def getansifrequencies(
fraction: float,
limits: List[float] | None = None,
) -> Tuple[List[float], List[float], List[float]]:
) -> Tuple[List[float], List[float], List[float], List[str]]:
"""
Calculate frequencies according to ANSI/IEC standards.

:param fraction: Bandwidth fraction (e.g., 1, 3).
:param limits: [f_min, f_max] limits.
:return: Tuple of (center_freqs, lower_edges, upper_edges).
:return: Tuple of (center_freqs, lower_edges, upper_edges, nominal_labels).
"""
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Treat the 4-tuple return as a versioned API break.

This changes a public unpacking contract. Please pair it with the planned 1.2.0 runtime/package version bump before release; otherwise existing callers still on 1.1.x will fail with too many values to unpack.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pyoctaveband/frequencies.py` around lines 14 - 24, The public API of
getansifrequencies was changed to return a 4-tuple which breaks existing
callers; update the package/runtime version to 1.2.0 to signal the breaking
change and include a clear changelog entry and release note referencing
getansifrequencies so users are aware of the new return shape, or alternatively
restore backward compatibility by providing a wrapper overload that returns a
3-tuple for older callers and mark it deprecated. Ensure the version bump (to
1.2.0), changelog entry, and any deprecation comment reference
getansifrequencies and the new 4-tuple return.

Co-authored-by: ninoblumer <40544174+ninoblumer@users.noreply.github.com>
@jmrplens jmrplens force-pushed the feature/nominal-freq-labels-v2 branch from 07bbf89 to 4e75c18 Compare March 8, 2026 10:09
@github-actions
Copy link
Contributor

github-actions bot commented Mar 8, 2026

CI Results 🚀

Test Summary

Python Version Tests Failures Coverage Status
macos-latest-3.13 111 0 99.5% ✅ Passed
ubuntu-latest-3.13 111 0 99.5% ✅ Passed
windows-latest-3.13 111 0 99.5% ✅ Passed

Technical Benchmark Summary

📊 View Benchmark Details

PyOctaveBand: Technical Benchmark Report

Generated: 2026-03-08 10:12:38

1. Test Signal Parameters

  • Sample Rate: 96.0 kHz
  • Duration: 10.0 seconds
  • Signal Types: White Noise (Stability) / Pure Sine (Precision)
  • Precision: 64-bit Floating Point

2. Crossover (Linkwitz-Riley)

Crossover

  • Flatness Error: 0.000000 dB (Target < 0.01)

3. Precision & Isolation

Precision

Type Error (dB) Isolation Ripple GD Std (ms)
butter 2.46e-03 31.3 dB 0.2705 dB 2847.826
cheby1 3.38e-03 40.5 dB 0.1000 dB 3551.677
cheby2 3.26e-03 57.8 dB 29.4187 dB 4790.013
ellip 9.41e-03 54.2 dB 0.1000 dB 4700.881
bessel 5.20e-01 32.5 dB 5.9845 dB 1380.212

4. Performance

Performance

Channels Exec Time (s) Speedup
1 0.628 1.00x
2 1.230 1.02x
4 2.441 1.03x
8 4.849 1.04x
16 9.688 1.04x

View Full Artifacts

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
src/pyoctaveband/frequencies.py (1)

156-160: ⚠️ Potential issue | 🟡 Minor

Use an explicit tiny tolerance for standard fractions.

np.isclose with default tolerances still treats values like 1.00001 as equal to 1, so some non-standard fractions can still be routed through the preferred-table lookup instead of the Annex E.3 fallback.

Possible fix
-    frac = round(fraction)
-    if np.isclose(fraction, frac) and frac in (1, 3):
+    frac = round(fraction)
+    if abs(fraction - frac) < 1e-8 and frac in (1, 3):
         extended = _extended_preferred(frac)
         return min(extended, key=lambda f: abs(np.log(f / exact_freq)))

Verify the current behavior with a small read-only check; fraction=1.00001 and fraction=3.00003 should currently print isclose=True, which demonstrates the gap:

#!/bin/bash
python - <<'PY'
import numpy as np

for fraction in [1.0, 1.000001, 1.00001, 0.99999, 3.0, 3.00003]:
    frac = round(fraction)
    print(
        f"fraction={fraction:<8} rounded={frac} "
        f"isclose={np.isclose(fraction, frac)} "
        f"abs_diff={abs(fraction - frac)}"
    )
PY
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pyoctaveband/frequencies.py` around lines 156 - 160, The existing check
using np.isclose(fraction, frac) is too permissive; update the condition in the
block that computes frac = round(fraction) (and checks frac in (1, 3) before
calling _extended_preferred) to use an explicit tiny tolerance, e.g.
np.isclose(fraction, frac, atol=1e-6) (or a similarly small absolute tolerance)
so values like 1.00001/3.00003 don't pass as "close" and will correctly fall
through to _iec_e3_round(exact_freq).
🧹 Nitpick comments (1)
tests/test_coverage_fix.py (1)

142-143: Assert the new 4-tuple contract, not just unpackability.

This only proves the extra value can be unpacked. It will still pass if labels is the wrong length or drifts out of sync with the generated bands, which is the key behavior added here.

Suggested test tightening
     f1, f2, f3, labels = getansifrequencies(1, limits=None)
     assert len(f1) > 0
+    assert len(f1) == len(f2) == len(f3) == len(labels)
+    assert all(isinstance(label, str) for label in labels)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_coverage_fix.py` around lines 142 - 143, The test only verifies
unpacking of getansifrequencies(1, limits=None) into f1, f2, f3, labels but not
the intended 4-tuple contract; update the assertions in
tests/test_coverage_fix.py to validate the new contract by asserting labels has
the expected length and alignment with the returned frequency bands from
getansifrequencies (e.g., ensure labels is a sequence, len(labels) matches the
number of bands implied by f1/f2/f3 or the combined frequency arrays, and that
label indices correspond to the generated bands), referencing getansifrequencies
and the variables f1, f2, f3, labels to locate the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/pyoctaveband/frequencies.py`:
- Around line 156-160: The existing check using np.isclose(fraction, frac) is
too permissive; update the condition in the block that computes frac =
round(fraction) (and checks frac in (1, 3) before calling _extended_preferred)
to use an explicit tiny tolerance, e.g. np.isclose(fraction, frac, atol=1e-6)
(or a similarly small absolute tolerance) so values like 1.00001/3.00003 don't
pass as "close" and will correctly fall through to _iec_e3_round(exact_freq).

---

Nitpick comments:
In `@tests/test_coverage_fix.py`:
- Around line 142-143: The test only verifies unpacking of getansifrequencies(1,
limits=None) into f1, f2, f3, labels but not the intended 4-tuple contract;
update the assertions in tests/test_coverage_fix.py to validate the new contract
by asserting labels has the expected length and alignment with the returned
frequency bands from getansifrequencies (e.g., ensure labels is a sequence,
len(labels) matches the number of bands implied by f1/f2/f3 or the combined
frequency arrays, and that label indices correspond to the generated bands),
referencing getansifrequencies and the variables f1, f2, f3, labels to locate
the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 90990015-4639-4e1e-8ff8-b92328ef8950

📥 Commits

Reviewing files that changed from the base of the PR and between 07bbf89 and 4e75c18.

📒 Files selected for processing (6)
  • sonar-project.properties
  • src/pyoctaveband/__init__.py
  • src/pyoctaveband/core.py
  • src/pyoctaveband/frequencies.py
  • tests/test_coverage_fix.py
  • tests/test_nominal_frequencies.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/test_nominal_frequencies.py

- Bump version to 1.2.0 (breaking: getansifrequencies returns 4-tuple)
- Sync __init__.py __version__ with pyproject.toml
- Simplify sigbands dispatch in __init__.py (single return)
- Update docstring :rtype: with nominal label variants
- Use np.isclose() for fraction matching in frequencies.py
- Cache extended preferred frequency list with @lru_cache
- Add sonar.cpd.exclusions for overload duplication
- Tighten test assertions for 4-tuple contract
@jmrplens jmrplens force-pushed the feature/nominal-freq-labels-v2 branch from 4e75c18 to ac4faa8 Compare March 8, 2026 10:32
@sonarqubecloud
Copy link

sonarqubecloud bot commented Mar 8, 2026

@github-actions
Copy link
Contributor

github-actions bot commented Mar 8, 2026

CI Results 🚀

Test Summary

Python Version Tests Failures Coverage Status
macos-latest-3.13 111 0 99.5% ✅ Passed
ubuntu-latest-3.13 111 0 99.5% ✅ Passed
windows-latest-3.13 111 0 99.5% ✅ Passed

Technical Benchmark Summary

📊 View Benchmark Details

PyOctaveBand: Technical Benchmark Report

Generated: 2026-03-08 10:36:09

1. Test Signal Parameters

  • Sample Rate: 96.0 kHz
  • Duration: 10.0 seconds
  • Signal Types: White Noise (Stability) / Pure Sine (Precision)
  • Precision: 64-bit Floating Point

2. Crossover (Linkwitz-Riley)

Crossover

  • Flatness Error: 0.000000 dB (Target < 0.01)

3. Precision & Isolation

Precision

Type Error (dB) Isolation Ripple GD Std (ms)
butter 2.46e-03 31.3 dB 0.2705 dB 2847.826
cheby1 3.38e-03 40.5 dB 0.1000 dB 3551.677
cheby2 3.26e-03 57.8 dB 29.4187 dB 4790.013
ellip 9.41e-03 54.2 dB 0.1000 dB 4700.881
bessel 5.20e-01 32.5 dB 5.9845 dB 1380.212

4. Performance

Performance

Channels Exec Time (s) Speedup
1 0.630 1.00x
2 1.235 1.02x
4 2.440 1.03x
8 4.850 1.04x
16 9.660 1.04x

View Full Artifacts

@jmrplens jmrplens merged commit eb67e63 into main Mar 8, 2026
14 of 15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant