Skip to content

feat: add IEC 61260-1 nominal frequency labels (closes #nominal)#44

Open
ninoblumer wants to merge 1 commit intojmrplens:mainfrom
ninoblumer:feature/nominal-frequency-labels
Open

feat: add IEC 61260-1 nominal frequency labels (closes #nominal)#44
ninoblumer wants to merge 1 commit intojmrplens:mainfrom
ninoblumer:feature/nominal-frequency-labels

Conversation

@ninoblumer
Copy link

@ninoblumer ninoblumer commented Mar 5, 2026

Summary

  • Adds three private helpers in frequencies.py for IEC 61260-1 Annex E nominal frequency calculation and formatting
  • getansifrequencies() now returns a 4-tuple (center_freqs, lower_edges, upper_edges, nominal_labels)breaking change → version 1.2.0
  • OctaveFilterBank gains a nominal_freq: List[str] attribute
  • filter() and octavefilter() gain nominal: bool = False; when True returns nominal frequency labels as List[str] instead of exact floats
  • Default behaviour is fully backward-compatible; filter design (SOS, freq_d, freq_u) is unaffected

Test plan

  • 16 new tests in tests/test_nominal_frequencies.py cover all new code paths
  • tests/test_coverage_fix.py updated for the 4-tuple API change
  • Full test suite passes (91/91)

Summary by Sourcery

Add IEC 61260-1–based nominal frequency label support and propagate it through the frequency generation and filtering APIs.

New Features:

  • Expose IEC 61260-1 nominal frequency labels alongside exact band frequencies via getansifrequencies and internal frequency generation.
  • Add a nominal_freq attribute to OctaveFilterBank holding nominal frequency labels for each band.
  • Add an optional nominal flag to OctaveFilterBank.filter and octavefilter to return nominal frequency labels instead of exact float center frequencies.

Enhancements:

  • Extend getansifrequencies and internal _genfreqs API to return a 4-tuple including nominal labels, updating callers accordingly.

Tests:

  • Add a dedicated nominal frequency test module covering IEC rounding helpers, label formatting, new getansifrequencies output, OctaveFilterBank.nominal_freq, and nominal=True filter paths.
  • Update existing coverage test to handle the new 4-tuple getansifrequencies API.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added optional nominal parameter to octavefilter function; when enabled, returns standardized IEC 61260-1 nominal frequency labels as strings instead of exact numeric values for improved clarity and compliance with international audio standards.
  • Tests

    • Added comprehensive test coverage for nominal frequency labeling and IEC 61260-1 standard compliance.

Exact mid-band frequencies (e.g. 997.2 Hz, 15.85 Hz) are correct for
filter design but IEC 61260-1 §5.5 + Annex E require nominal values
(1000 Hz, 16 Hz) for band identification and labelling.

Changes:
- Add three private helpers in frequencies.py:
  · _iec_e3_round: Annex E.3 significant-figure rounding
  · _nominal_freq_for_band: snaps exact freq to IEC table (b=1,3)
    or falls back to E.3 rounding for other fractions
  · _format_nominal_freq: formats as "31.5", "500", "1k", "16k"
- getansifrequencies() now returns a 4-tuple (breaking change → 1.2.0):
  (center_freqs, lower_edges, upper_edges, nominal_labels)
- _genfreqs() extended to 4-tuple; recomputes labels post-Nyquist filter
- OctaveFilterBank gains nominal_freq: List[str] attribute
- filter() / octavefilter() gain nominal: bool = False parameter;
  when True returns List[str] labels instead of exact floats
- Default behaviour is fully backward-compatible

Tests: 16 new tests in test_nominal_frequencies.py covering all paths;
updated test_coverage_fix.py for the 4-tuple API change.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@sourcery-ai
Copy link

sourcery-ai bot commented Mar 5, 2026

Reviewer's Guide

Implements IEC 61260-1 nominal frequency label support by extending the frequency-generation pipeline to compute human-readable nominal labels, threading them through the filter bank API as an optional output, and updating public helpers and tests for the new 4-tuple frequency signature.

Sequence diagram for octavefilter with optional nominal labels

sequenceDiagram
    actor User
    participant ClientCode
    participant public_api as octavefilter
    participant OFB as OctaveFilterBank
    participant Freq as frequencies_module

    User->>ClientCode: call_octavefilter(x, fs, fraction, nominal)
    ClientCode->>public_api: octavefilter(x, fs, fraction, limits, sigbands, detrend, calibration_factor, dbfs, mode, nominal)

    public_api->>OFB: __init__(limits, fraction, fs, calibration_factor, dbfs)
    OFB->>Freq: _genfreqs(limits, fraction, fs)
    Freq->>Freq: getansifrequencies(fraction, limits)
    Freq-->>OFB: freq, freq_d, freq_u, labels
    OFB->>OFB: store freq, freq_d, freq_u, nominal_freq

    public_api->>OFB: filter(x, sigbands, mode, detrend, nominal)
    OFB->>OFB: _process_bands(x, detrend)
    alt nominal_is_true
        OFB-->>public_api: spl, nominal_freq, optional_sigbands
    else nominal_is_false
        OFB-->>public_api: spl, freq, optional_sigbands
    end

    public_api-->>ClientCode: return_result_tuple
    ClientCode-->>User: present_SPL_and_frequency_labels
Loading

File-Level Changes

Change Details Files
Extend ANSI/IEC frequency generation to compute and return nominal frequency labels alongside exact band edges.
  • Changed getansifrequencies() to return a 4-tuple including nominal string labels and updated its docstring to describe the new return type.
  • Adjusted _genfreqs() to consume the new getansifrequencies() API, drop out-of-range bands based on fs, and recompute nominal labels for the surviving bands.
  • Introduced IEC 61260-1 helpers _iec_e3_round(), _nominal_freq_for_band(), and _format_nominal_freq() to compute and format nominal mid-band frequencies.
src/pyoctaveband/frequencies.py
Propagate nominal frequency labels through the OctaveFilterBank and octavefilter APIs as an opt-in alternative to numeric center frequencies.
  • Updated OctaveFilterBank to store nominal_freq from _genfreqs() and adjusted _genfreqs() call sites to handle the 4-tuple return type.
  • Extended OctaveFilterBank.filter() overloads and implementation to accept a nominal flag and to switch between exact float frequencies and nominal label strings in its returned tuples.
  • Extended the top-level octavefilter() overloads and implementation to accept and document a nominal flag and to forward it through to OctaveFilterBank.filter().
src/pyoctaveband/core.py
src/pyoctaveband/__init__.py
Update and extend tests to cover the new nominal frequency helpers, the 4-tuple frequency API, and nominal label propagation through public functions.
  • Added tests for IEC rounding, nominal frequency selection, and label formatting plus integration tests for getansifrequencies(), OctaveFilterBank.nominal_freq, and octavefilter(nominal=...).
  • Updated existing coverage tests to accommodate the new 4-tuple signature of getansifrequencies().
tests/test_nominal_frequencies.py
tests/test_coverage_fix.py

Possibly linked issues

  • #nominal: They address the same need: providing IEC 61260-1 nominal frequency labels instead of exact mid-band floats, via opt-in flags.

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

@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 enhances the pyoctaveband library by integrating support for IEC 61260-1 nominal frequency labels. This allows users to retrieve standardized, human-readable string representations of frequencies instead of exact float values, which is particularly useful for acoustic analysis and reporting. The changes involve updating core frequency generation logic and extending the OctaveFilterBank and octavefilter interfaces to expose this new functionality, while maintaining backward compatibility for existing usage patterns.

Highlights

  • IEC 61260-1 Nominal Frequency Support: Introduced new private helper functions in frequencies.py for calculating and formatting IEC 61260-1 Annex E nominal frequency labels.
  • API Change in getansifrequencies(): The getansifrequencies() function now returns a 4-tuple, including nominal frequency labels, which is a breaking change.
  • New nominal_freq Attribute: The OctaveFilterBank class gained a nominal_freq: List[str] attribute to store the calculated nominal frequency labels.
  • Optional Nominal Labels in Filtering Functions: The filter() method of OctaveFilterBank and the top-level octavefilter() function now accept a nominal: bool = False parameter, allowing users to retrieve nominal frequency labels (List[str]) instead of exact float values when set to True.
  • Backward Compatibility: The default behavior of the filtering functions remains backward-compatible, and the underlying filter design (SOS, freq_d, freq_u) is unaffected by these changes.
Changelog
  • src/pyoctaveband/init.py
    • Added nominal: bool = False parameter to octavefilter function signatures.
    • Updated the return type hints for octavefilter to include List[str] when nominal is true.
    • Modified internal calls to filter_bank.filter to pass the new nominal argument.
  • src/pyoctaveband/core.py
    • Modified the OctaveFilterBank constructor to store nominal_freq as an attribute.
    • Extended the filter method with nominal: bool = False parameter and added new overload signatures to support List[str] return type.
    • Updated 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 a list of nominal frequency labels.
    • Modified _genfreqs to also return the nominal frequency labels.
    • Introduced three new private helper functions: _iec_e3_round for IEC rounding rules, _nominal_freq_for_band for calculating nominal frequencies, and _format_nominal_freq for formatting labels.
  • tests/test_coverage_fix.py
    • Adjusted the test case test_octavefilter_limits_none to unpack the 4-tuple returned by getansifrequencies.
  • tests/test_nominal_frequencies.py
    • Added a new test file containing 16 new tests to validate the functionality of _iec_e3_round, _nominal_freq_for_band, _format_nominal_freq, the updated getansifrequencies, the OctaveFilterBank.nominal_freq attribute, and the filter and octavefilter methods with the new nominal parameter.
Activity
  • New test cases were added in tests/test_nominal_frequencies.py to cover all new code paths.
  • An existing test file, tests/test_coverage_fix.py, was updated to reflect API changes.
  • The full test suite passed with 91 out of 91 tests.
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.

@coderabbitai
Copy link

coderabbitai bot commented Mar 5, 2026

📝 Walkthrough

Walkthrough

This pull request extends PyOctaveBand with opt-in IEC 61260-1 nominal frequency labeling. A new nominal parameter is added to the public API (octavefilter and OctaveFilterBank.filter). When enabled, nominal frequency labels are computed and returned as strings instead of exact float frequencies. Supporting helpers and infrastructure are added to the frequencies module to generate and format nominal labels according to IEC standards.

Changes

Cohort / File(s) Summary
Core API Nominal Support
src/pyoctaveband/__init__.py, src/pyoctaveband/core.py
Added nominal: bool = False parameter to octavefilter() and OctaveFilterBank.filter() methods. Overloads and return type annotations updated to reflect List[str] outputs when nominal=True. New public attribute nominal_freq added to OctaveFilterBank via initialization.
Frequency Generation & Formatting
src/pyoctaveband/frequencies.py
Extended getansifrequencies() and _genfreqs() to return nominal frequency labels (4-tuple instead of 3-tuple). Introduced new helper functions: _iec_e3_round() for IEC E3 rounding rules, _nominal_freq_for_band() for nominal frequency computation, and _format_nominal_freq() for human-readable string formatting.
Test Updates
tests/test_coverage_fix.py, tests/test_nominal_frequencies.py
Updated test_coverage_fix.py to unpack 4-tuple from getansifrequencies(). Added comprehensive new test module test_nominal_frequencies.py validating IEC E3 rounding, nominal label formatting, and integration with OctaveFilterBank and octavefilter() APIs.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant octavefilter
    participant OctaveFilterBank
    participant filter_bank
    participant frequencies

    User->>octavefilter: octavefilter(signal, fs, nominal=True)
    octavefilter->>OctaveFilterBank: OctaveFilterBank(fs, limits, fraction)
    OctaveFilterBank->>frequencies: _genfreqs(limits, fraction, fs)
    frequencies->>frequencies: _iec_e3_round() & _nominal_freq_for_band()
    frequencies->>frequencies: _format_nominal_freq() for each band
    frequencies-->>OctaveFilterBank: (freqs, lower, upper, nominal_labels)
    OctaveFilterBank->>OctaveFilterBank: Store nominal_freq attribute
    filter_bank->>filter_bank: filter(signal, nominal=True)
    filter_bank-->>octavefilter: (spl_data, ["20", "25", "31.5", ..., "1k", ...])
    octavefilter-->>User: (spl_array, nominal_labels_list)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Poem

🐰 Hopping through frequencies with nominal cheer,
IEC labels now perfectly clear!
Strings where floats once stood so neat,
"1k" and "31.5" make the math complete!
A bundle of bands, now wearing their names,
PyOctaveBand shines with standards and frames! 🎵

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main feature added: IEC 61260-1 nominal frequency labels support, which aligns with the primary objective of the pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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.

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 found 3 issues, and left some high level feedback:

  • In _genfreqs, you discard the labels returned by getansifrequencies and recompute them from freq, which duplicates logic and risks future divergence; consider passing through the labels from getansifrequencies or factoring the label-generation into a single shared helper.
  • The return type hints for octavefilter in __init__.py still specify List[float] even when nominal=True can produce List[str]; aligning these overloads with the more precise OctaveFilterBank.filter typing would make the public API annotations consistent and easier to use with type checkers.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `_genfreqs`, you discard the `labels` returned by `getansifrequencies` and recompute them from `freq`, which duplicates logic and risks future divergence; consider passing through the labels from `getansifrequencies` or factoring the label-generation into a single shared helper.
- The return type hints for `octavefilter` in `__init__.py` still specify `List[float]` even when `nominal=True` can produce `List[str]`; aligning these overloads with the more precise `OctaveFilterBank.filter` typing would make the public API annotations consistent and easier to use with type checkers.

## Individual Comments

### Comment 1
<location path="src/pyoctaveband/frequencies.py" line_range="14-23" />
<code_context>
 def getansifrequencies(
     fraction: float,
     limits: List[float] | None = None,
</code_context>
<issue_to_address>
**suggestion:** Avoid computing nominal labels twice and consider delegating label generation to a single place

`getansifrequencies` now derives `labels` from the full band set, but `_genfreqs` ignores these and recomputes labels after `_deleteouters`, duplicating logic and risking inconsistency if label generation changes. Consider having `getansifrequencies` return only numeric bands and consolidating label computation in `_genfreqs` for the post-`_deleteouters` subset, or alternatively reusing and slicing the `labels` from `getansifrequencies` in parallel with `freq/freq_d/freq_u`.

Suggested implementation:

```python
def getansifrequencies(
    fraction: float,
    limits: List[float] | None = None,
) -> Tuple[List[float], List[float], List[float]]:
    """
    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).
    """

```

To fully implement the suggestion and avoid duplicate label computation, the following additional changes are required in the same module:

1. **Inside `getansifrequencies` implementation (below the snippet you provided)**:
   - Remove any computation of `nominal_labels` and ensure the function returns only numeric lists:
     ```python
     return center_freqs, lower_edges, upper_edges
     ```
   - Delete any label-related local variables in this function.

2. **In `_genfreqs` (or whatever function is orchestrating band generation and currently recomputing labels)**:
   - Make `_genfreqs` the single source of truth for label creation.
   - After calling `_deleteouters` (or equivalent logic that trims bands based on `limits`), generate `nominal_labels` only for the trimmed `freq` set.
   - Example structure (pseudocode):
     ```python
     freq, freq_d, freq_u = getansifrequencies(fraction, limits)
     freq, freq_d, freq_u = _deleteouters(freq, freq_d, freq_u, limits)

     labels = _make_nominal_labels(freq)  # new helper, or inline logic
     return freq, freq_d, freq_u, labels
     ```

3. **Update all call sites** that:
   - Expect `getansifrequencies` to return four values; adjust them to handle three returned sequences.
   - If any caller previously relied on `nominal_labels` from `getansifrequencies`, switch them to use the labels returned by `_genfreqs` (or whichever high-level API now provides the labels after trimming).

4. **Add a single helper for label generation** (if not already present) to keep logic centralized:
   ```python
   def _make_nominal_labels(center_freqs: Sequence[float]) -> List[str]:
       # existing nominal/rounding rules go here
       ...
   ```

These steps ensure that:
- `getansifrequencies` only deals with numeric band data.
- Labels are computed exactly once, after `_deleteouters`, preventing inconsistencies between full-band and trimmed-band label sets.
</issue_to_address>

### Comment 2
<location path="src/pyoctaveband/frequencies.py" line_range="140-146" />
<code_context>
+    return round(f / step) * step
+
+
+def _nominal_freq_for_band(exact_freq: float, fraction: float) -> float:
+    """Return IEC 61260-1 nominal frequency (float) for an exact mid-band frequency."""
+    frac = round(fraction)
+    if frac in (1, 3):
+        base = normalizedfreq(frac)
+        extended = [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)
+
</code_context>
<issue_to_address>
**question:** Check behaviour of nominal frequency mapping outside 1- and 3-band cases

For fractions other than 1 or 3 this falls back to `_iec_e3_round(exact_freq)`, so only 1/3‑octave bands map to a standardized nominal set. If full IEC 61260-1 compliance is expected across all fractions, consider clarifying this behaviour in the docstring (or revisiting the mapping) so users aren’t surprised by the inconsistency between 1/3 and e.g. 6/12/24 bands.
</issue_to_address>

### Comment 3
<location path="src/pyoctaveband/__init__.py" line_range="55" />
<code_context>
+def octavefilter(
</code_context>
<issue_to_address>
**issue:** Update `octavefilter` overload return types to reflect `nominal=True` string labels

The overloads above still specify `List[float]` as the return type, but `nominal=True` now returns `List[str]`. Since static type checkers rely on the overloads, they should be updated (or split) to distinguish nominal vs non-nominal cases; otherwise calls with `nominal=True` will be typed incorrectly.
</issue_to_address>

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.

Comment on lines 14 to +23
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.

suggestion: Avoid computing nominal labels twice and consider delegating label generation to a single place

getansifrequencies now derives labels from the full band set, but _genfreqs ignores these and recomputes labels after _deleteouters, duplicating logic and risking inconsistency if label generation changes. Consider having getansifrequencies return only numeric bands and consolidating label computation in _genfreqs for the post-_deleteouters subset, or alternatively reusing and slicing the labels from getansifrequencies in parallel with freq/freq_d/freq_u.

Suggested implementation:

def getansifrequencies(
    fraction: float,
    limits: List[float] | None = None,
) -> Tuple[List[float], List[float], List[float]]:
    """
    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).
    """

To fully implement the suggestion and avoid duplicate label computation, the following additional changes are required in the same module:

  1. Inside getansifrequencies implementation (below the snippet you provided):

    • Remove any computation of nominal_labels and ensure the function returns only numeric lists:
      return center_freqs, lower_edges, upper_edges
    • Delete any label-related local variables in this function.
  2. In _genfreqs (or whatever function is orchestrating band generation and currently recomputing labels):

    • Make _genfreqs the single source of truth for label creation.
    • After calling _deleteouters (or equivalent logic that trims bands based on limits), generate nominal_labels only for the trimmed freq set.
    • Example structure (pseudocode):
      freq, freq_d, freq_u = getansifrequencies(fraction, limits)
      freq, freq_d, freq_u = _deleteouters(freq, freq_d, freq_u, limits)
      
      labels = _make_nominal_labels(freq)  # new helper, or inline logic
      return freq, freq_d, freq_u, labels
  3. Update all call sites that:

    • Expect getansifrequencies to return four values; adjust them to handle three returned sequences.
    • If any caller previously relied on nominal_labels from getansifrequencies, switch them to use the labels returned by _genfreqs (or whichever high-level API now provides the labels after trimming).
  4. Add a single helper for label generation (if not already present) to keep logic centralized:

    def _make_nominal_labels(center_freqs: Sequence[float]) -> List[str]:
        # existing nominal/rounding rules go here
        ...

These steps ensure that:

  • getansifrequencies only deals with numeric band data.
  • Labels are computed exactly once, after _deleteouters, preventing inconsistencies between full-band and trimmed-band label sets.

Comment on lines +140 to +146
def _nominal_freq_for_band(exact_freq: float, fraction: float) -> float:
"""Return IEC 61260-1 nominal frequency (float) for an exact mid-band frequency."""
frac = round(fraction)
if frac in (1, 3):
base = normalizedfreq(frac)
extended = [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)))
Copy link

Choose a reason for hiding this comment

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

question: Check behaviour of nominal frequency mapping outside 1- and 3-band cases

For fractions other than 1 or 3 this falls back to _iec_e3_round(exact_freq), so only 1/3‑octave bands map to a standardized nominal set. If full IEC 61260-1 compliance is expected across all fractions, consider clarifying this behaviour in the docstring (or revisiting the mapping) so users aren’t surprised by the inconsistency between 1/3 and e.g. 6/12/24 bands.

calibration_factor: float = 1.0,
dbfs: bool = False,
mode: str = "rms",
nominal: bool = False,
Copy link

Choose a reason for hiding this comment

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

issue: Update octavefilter overload return types to reflect nominal=True string labels

The overloads above still specify List[float] as the return type, but nominal=True now returns List[str]. Since static type checkers rely on the overloads, they should be updated (or split) to distinguish nominal vs non-nominal cases; otherwise calls with nominal=True will be typed incorrectly.

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 significant new functionality by adding support for IEC 61260-1 nominal frequency labels. The changes are well-implemented, including necessary modifications to function signatures, return types, and internal logic across __init__.py, core.py, and frequencies.py. The addition of a dedicated test file (test_nominal_frequencies.py) demonstrates thorough testing of the new features and helper functions. The breaking change to getansifrequencies returning a 4-tuple is clearly documented in the PR description and handled appropriately in existing calls. Overall, this is a valuable enhancement to the library.

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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/pyoctaveband/__init__.py (2)

22-22: ⚠️ Potential issue | 🟡 Minor

Version not bumped for breaking API change.

The PR summary indicates this is a breaking change (getansifrequencies now returns a 4-tuple) and version should be bumped to 1.2.0, but the version remains at 1.1.2.

🔧 Proposed fix
-__version__ = "1.1.2"
+__version__ = "1.2.0"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pyoctaveband/__init__.py` at line 22, The package version constant
__version__ still reads "1.1.2" despite a breaking API change
(getansifrequencies now returns a 4-tuple); update the module-level __version__
in src/pyoctaveband/__init__.py from "1.1.2" to "1.2.0" to reflect the breaking
change and ensure releases and package metadata align with the new API contract.

38-77: ⚠️ Potential issue | 🟠 Major

Incomplete overloads: missing nominal=True variants.

The overloads only cover nominal: bool = False cases, but core.py correctly defines 4 overloads (2×2 matrix for sigbands × nominal). Without matching overloads here, type checkers won't correctly infer List[str] return types when nominal=True.

🔧 Proposed fix: Add missing overloads
 `@overload`
 def octavefilter(
     x: List[float] | np.ndarray,
     fs: int,
     fraction: float = 1,
     order: int = 6,
     limits: List[float] | None = None,
     show: bool = False,
     sigbands: Literal[False] = False,
     plot_file: str | None = None,
     detrend: bool = True,
     filter_type: str = "butter",
     ripple: float = 0.1,
     attenuation: float = 60.0,
     calibration_factor: float = 1.0,
     dbfs: bool = False,
     mode: str = "rms",
-    nominal: bool = False,
+    nominal: Literal[False] = False,
 ) -> Tuple[np.ndarray, List[float]]: ...
 
 
 `@overload`
 def octavefilter(
     x: List[float] | np.ndarray,
     fs: int,
     fraction: float = 1,
     order: int = 6,
     limits: List[float] | None = None,
     show: bool = False,
     sigbands: Literal[True] = True,
     plot_file: str | None = None,
     detrend: bool = True,
     filter_type: str = "butter",
     ripple: float = 0.1,
     attenuation: float = 60.0,
     calibration_factor: float = 1.0,
     dbfs: bool = False,
     mode: str = "rms",
-    nominal: bool = False,
+    nominal: Literal[False] = False,
 ) -> Tuple[np.ndarray, List[float], List[np.ndarray]]: ...
+
+
+@overload
+def octavefilter(
+    x: List[float] | np.ndarray,
+    fs: int,
+    fraction: float = 1,
+    order: int = 6,
+    limits: List[float] | None = None,
+    show: bool = False,
+    sigbands: Literal[False] = False,
+    plot_file: str | None = None,
+    detrend: bool = True,
+    filter_type: str = "butter",
+    ripple: float = 0.1,
+    attenuation: float = 60.0,
+    calibration_factor: float = 1.0,
+    dbfs: bool = False,
+    mode: str = "rms",
+    nominal: Literal[True] = ...,
+) -> Tuple[np.ndarray, List[str]]: ...
+
+
+@overload
+def octavefilter(
+    x: List[float] | np.ndarray,
+    fs: int,
+    fraction: float = 1,
+    order: int = 6,
+    limits: List[float] | None = None,
+    show: bool = False,
+    sigbands: Literal[True] = True,
+    plot_file: str | None = None,
+    detrend: bool = True,
+    filter_type: str = "butter",
+    ripple: float = 0.1,
+    attenuation: float = 60.0,
+    calibration_factor: float = 1.0,
+    dbfs: bool = False,
+    mode: str = "rms",
+    nominal: Literal[True] = ...,
+) -> Tuple[np.ndarray, List[str], List[np.ndarray]]: ...
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pyoctaveband/__init__.py` around lines 38 - 77, The overloads for
octavefilter are missing the variants with nominal=True; add two more `@overload`
signatures mirroring the existing ones but with nominal: Literal[True] = True
and the correct return types: when sigbands is False return Tuple[np.ndarray,
List[float], List[str]] and when sigbands is True return Tuple[np.ndarray,
List[float], List[np.ndarray], List[str]] so type checkers match the 2×2
overloads defined in core.py.
🧹 Nitpick comments (2)
tests/test_nominal_frequencies.py (2)

69-69: Rename ambiguous variable l to label.

The variable name l is flagged by static analysis (E741) because it's visually similar to the digit 1. Use a more descriptive name like label.

♻️ Proposed fix
-    assert all(isinstance(l, str) for l in 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_nominal_frequencies.py` at line 69, Rename the ambiguous loop
variable `l` in the assertion to a clearer name (e.g., `label`) to avoid the
E741 warning and improve readability; locate the assertion that reads `assert
all(isinstance(l, str) for l in labels)` in the test (referencing the `labels`
iterable) and change the generator expression to use `label` instead of `l`.

87-87: Rename ambiguous variable l to label.

Same issue as above - use a clearer variable name.

♻️ Proposed fix
-    assert all(isinstance(l, str) for l in fb.nominal_freq)
+    assert all(isinstance(label, str) for label in fb.nominal_freq)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_nominal_frequencies.py` at line 87, Rename the ambiguous loop
variable "l" to a descriptive name like "label" in the generator expression that
checks types for fb.nominal_freq; specifically update the assertion using
fb.nominal_freq (assert all(isinstance(l, str) for l in fb.nominal_freq)) to use
"label" instead (assert all(isinstance(label, str) for label in
fb.nominal_freq)) so the intent is clearer.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/pyoctaveband/__init__.py`:
- Line 22: The package version constant __version__ still reads "1.1.2" despite
a breaking API change (getansifrequencies now returns a 4-tuple); update the
module-level __version__ in src/pyoctaveband/__init__.py from "1.1.2" to "1.2.0"
to reflect the breaking change and ensure releases and package metadata align
with the new API contract.
- Around line 38-77: The overloads for octavefilter are missing the variants
with nominal=True; add two more `@overload` signatures mirroring the existing ones
but with nominal: Literal[True] = True and the correct return types: when
sigbands is False return Tuple[np.ndarray, List[float], List[str]] and when
sigbands is True return Tuple[np.ndarray, List[float], List[np.ndarray],
List[str]] so type checkers match the 2×2 overloads defined in core.py.

---

Nitpick comments:
In `@tests/test_nominal_frequencies.py`:
- Line 69: Rename the ambiguous loop variable `l` in the assertion to a clearer
name (e.g., `label`) to avoid the E741 warning and improve readability; locate
the assertion that reads `assert all(isinstance(l, str) for l in labels)` in the
test (referencing the `labels` iterable) and change the generator expression to
use `label` instead of `l`.
- Line 87: Rename the ambiguous loop variable "l" to a descriptive name like
"label" in the generator expression that checks types for fb.nominal_freq;
specifically update the assertion using fb.nominal_freq (assert
all(isinstance(l, str) for l in fb.nominal_freq)) to use "label" instead (assert
all(isinstance(label, str) for label in fb.nominal_freq)) so the intent is
clearer.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 64ce515f-37aa-4724-b558-2732635a4b58

📥 Commits

Reviewing files that changed from the base of the PR and between 9189b72 and ecb3ae4.

📒 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant