Skip to content

Conversation

@d-chambers
Copy link
Contributor

@d-chambers d-chambers commented Sep 1, 2025

Description

This PR implements the stft by simply wrapping the scipy function.

Todos

  • properly handle units and scaling
  • deprecate Patch.spectrogram
  • Add more examples
  • Test a wider range of window functions and overlaps.

Checklist

I have (if applicable):

  • referenced the GitHub issue this PR closes.
  • documented the new feature with docstrings or appropriate doc page.
  • included a test. See testing guidelines.
  • your name has been added to the contributors page (docs/contributors.md).
  • added the "ready_for_review" tag once the PR is ready to be reviewed.

Summary by CodeRabbit

  • New Features

    • STFT and inverse‑STFT transforms; ability to create attribute‑dropped copies of patches.
  • Documentation

    • Tutorials updated with STFT/ISTFT examples and clarified messaging.
  • Refactor

    • Centralized datetime/timedelta detection for broader dtype coverage.
  • Tests

    • Added STFT/ISTFT round‑trip and error tests; expanded time‑dtype and attr‑drop tests.
  • Chores

    • Bumped dependency constraints, added Python 3.13 to CI, adjusted CI cleanup behavior.
  • Deprecated

    • Spectrogram function marked deprecated (use STFT).

@d-chambers d-chambers added the patch related to Patch class label Sep 1, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 1, 2025

Walkthrough

Adds STFT/ISTFT functions, coords, metadata, and inversion support; exports stft/istft; adds PatchAttrs.drop; centralizes datetime/timedelta dtype detection; deprecates spectrogram; updates docs/tests for STFT; tweaks Patch.iresample aliases; bumps SciPy and env deps; adjusts CI micromamba cleanup and adds Python 3.13.

Changes

Cohort / File(s) Summary
STFT / ISTFT implementation & exports
dascore/transform/fourier.py, dascore/transform/__init__.py
Adds public stft(...) and istft(...) with ShortTimeFFT usage, STFT coord construction, _stft_* metadata on patches, inversion validation and ISTFT coord reconstruction; exports stft and istft.
Patch-level aliasing
dascore/core/patch.py
Introduces local aliases stft and istft inside Patch.iresample alongside existing FFT aliases.
Attrs: drop method & tests
dascore/core/attrs.py, tests/test_core/test_attrs.py
Adds PatchAttrs.drop(*args) returning a new instance omitting specified keys (non-mutating); updates/renames tests to verify drop behavior.
Coords docs & transpose examples
dascore/core/coords.py, dascore/proc/coords.py
Docstring updates: get_sample_count enforcement message now includes computed samples; transpose docs/examples note ... usage and method-style examples.
Time utils refactor & tests
dascore/utils/time.py, tests/test_utils/test_time.py
Adds internal _is_dtype helper used by is_datetime64 / is_timedelta64 to centralize dtype detection (handles numpy/pandas scalars, dtype objects, Series, array-likes); adds dtype-input tests.
Spectrogram deprecation
dascore/transform/spectro.py
Adds @deprecate(info="Use Patch.stft() instead.", since="0.1.11", removed_in="0.2.0") to spectrogram; implementation unchanged.
Tests: STFT / ISTFT
tests/test_transform/test_fourier.py
Adds fixtures and unit-aware tests for STFT/ISTFT covering window types, DFT equivalence, round-trip/near-round-trip behavior, detrend and error cases.
Docs: STFT tutorial & examples
docs/tutorial/transformations.qmd
Replaces prior inverse-DFT example with forward real DFT example and adds STFT/ISTFT tutorial and examples.
Dependency & env bumps
pyproject.toml, environment.yml
Bumps SciPy minimum to >=1.15; updates environment constraints (adds numpy>=1.24, pandas>=2.0, pydantic>2.1, matplotlib>=3.5), adds pint and typing_extensions.
Units API signature
dascore/units.py
Updates invert_quantity return type to `pint.Unit
CI: micromamba action & workflow matrix
.github/actions/mamba-install-dascore/action.yml, .github/workflows/runtests.yml
Changes post-cleanup in micromamba action from 'all' to "shell-init"; adds Python 3.13 to the test_code matrix.

Possibly related PRs


📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 3565ec8 and 50a83ef.

📒 Files selected for processing (1)
  • dascore/transform/fourier.py (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • dascore/transform/fourier.py
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (18)
  • GitHub Check: test_code_min_deps (ubuntu-latest, 3.13)
  • GitHub Check: test_code_min_deps (macos-latest, 3.12)
  • GitHub Check: test_code_min_deps (macos-latest, 3.13)
  • GitHub Check: test_code_min_deps (windows-latest, 3.12)
  • GitHub Check: test_code_min_deps (windows-latest, 3.13)
  • GitHub Check: test_code_min_deps (ubuntu-latest, 3.12)
  • GitHub Check: test_code (windows-latest, 3.10)
  • GitHub Check: test_code (windows-latest, 3.11)
  • GitHub Check: test_code (ubuntu-latest, 3.12)
  • GitHub Check: test_code (windows-latest, 3.13)
  • GitHub Check: test_code (windows-latest, 3.12)
  • GitHub Check: test_code (ubuntu-latest, 3.13)
  • GitHub Check: test_code (macos-latest, 3.10)
  • GitHub Check: test_code (macos-latest, 3.13)
  • GitHub Check: test_code (ubuntu-latest, 3.11)
  • GitHub Check: test_code (ubuntu-latest, 3.10)
  • GitHub Check: test_code (macos-latest, 3.12)
  • GitHub Check: test_build_docs
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch stft

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@codecov
Copy link

codecov bot commented Sep 1, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.88%. Comparing base (b79a338) to head (50a83ef).
⚠️ Report is 2 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master     #523   +/-   ##
=======================================
  Coverage   99.87%   99.88%           
=======================================
  Files         122      122           
  Lines        9938    10027   +89     
=======================================
+ Hits         9926    10015   +89     
  Misses         12       12           
Flag Coverage Δ
unittests 99.88% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Contributor

@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: 4

Caution

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

⚠️ Outside diff range comments (1)
dascore/core/coords.py (1)

775-779: isinstance with PEP 604 unions will raise at runtime on py310.

Use a tuple of types, not int | np.integer, to avoid TypeError on the supported baseline (py310).

-            if not isinstance(value, int | np.integer):
+            if not isinstance(value, (int, np.integer)):
                 msg = "When samples==True values must be integers."
                 raise ParameterError(msg)
🧹 Nitpick comments (14)
tests/test_core/test_attrs.py (1)

261-270: Add assertions for immutability and type of result (nice-to-have).

Also assert the original is unchanged and the returned object is PatchAttrs.

 def test_simple_drop(self):
   """Ensure a single attr can be dropped."""
   attrs = PatchAttrs(bob=1, bill=2, sue="Z")
   new = dict(attrs.drop("bob", "bill"))
+  # original unchanged
+  orig = dict(attrs)
+  assert "bob" in orig and "bill" in orig and "sue" in orig
+  # type preserved
+  out = attrs.drop("bob", "bill")
+  assert isinstance(out, PatchAttrs)
   assert "bob" not in new and "bill" not in new
   assert "sue" in new
dascore/core/coords.py (1)

766-769: Doc improvement is helpful; clarify behavior for non-positive values.

Current doc claims results are “always ≥ 1”, but the implementation can return 0 or negatives (e.g., samples=True with value≤0, or value < min for time-like). Consider clarifying the doc or clamping the return to at least 1.

-Therefore, the output will always be greater or equal to 1.
+If value is positive, the result will be >= 1. (Callers may clamp to >=1 if needed.)

Or enforce it in code:

-        return samples
+        return max(1, samples)
dascore/core/attrs.py (1)

250-256: drop() works; minor simplification and doc tweak (nit).

Small simplification avoids an intermediate set and clarifies that a new instance is returned.

-    def drop(self, *args):
-        """Drop specific keys if they exist."""
-        contents = dict(self)
-        ok_to_keep = set(contents) - set(args)
-        out = {i: v for i, v in contents.items() if i in ok_to_keep}
-        return self.__class__(**out)
+    def drop(self, *args):
+        """Return a new PatchAttrs without the specified keys (if present)."""
+        contents = dict(self)
+        out = {k: v for k, v in contents.items() if k not in set(args)}
+        return self.__class__(**out)
dascore/utils/time.py (2)

405-408: Correct is_datetime64 docstring.

It currently says “timedelta64”; should be “datetime64”.

-def is_datetime64(obj):
-    """Determine if an object represents a timedelta64 dtype or value(s)."""
+def is_datetime64(obj):
+    """Determine if an object represents a datetime64 dtype or value(s)."""

383-413: Optional: treat Python datetime/timedelta scalars as time-like.

Extend _is_dtype to also accept datetime.datetime and datetime.timedelta when inspecting object-dtype arrays. Note that is_datetime64/is_timedelta64 are invoked across several modules—transform/fourier.py, units.py, utils/chunk.py, core/coords.py—and exercised by tests in test_utils/test_time.py and test_core/test_coords.py. Ensure you update those call sites and corresponding tests to cover the new behavior.

dascore/transform/fourier.py (2)

461-462: Consider preserving data_units in STFT attributes.

The STFT transformation should update the data units to reflect the transformation. Currently, data_units is not updated in the attributes.

Consider adding data units transformation similar to how it's handled in the DFT:

new_attrs["data_units"] = _get_data_units_from_dims(patch, [dim], mul)

543-544: Consider using existing shape validation instead of assertion.

The assertion on Line 543 could be replaced with a more informative error if shapes don't match.

-    assert new_data.shape == cm.shape
+    if new_data.shape != cm.shape:
+        msg = f"Data shape {new_data.shape} doesn't match coordinate shape {cm.shape}"
+        raise PatchError(msg)
dascore/proc/coords.py (2)

561-563: Fix typo in docs

“diemsnions” → “dimensions”.

Apply:

-        Can also include ... to indicate diemsnions that should be left
+        Can also include ... to indicate dimensions that should be left
         alone.

566-577: Standardize examples and align with project style

Use the common alias import dascore as dc as used in other docstrings in this file.

Apply:

-    >>> import dascore # import dascore library
-    >>> pa = dascore.get_example_patch() # generate example patch
+    >>> import dascore as dc  # import dascore library
+    >>> pa = dc.get_example_patch()  # generate example patch
tests/test_transform/test_fourier.py (5)

9-16: Prefer unit constant import over ad-hoc “seconds” alias

Import second once at module scope and use it consistently. This removes the custom seconds = get_quantity("seconds").

Apply:

 from __future__ import annotations
@@
 import dascore as dc
-from dascore.exceptions import PatchError
-from dascore.transform.fourier import dft, idft
-from dascore.units import get_quantity
+from dascore.exceptions import PatchError
+from dascore.transform.fourier import dft, idft
+from dascore.units import get_quantity, second
@@
-F_0 = 2
-seconds = get_quantity("seconds")
+F_0 = 2

And update usage below (see next hunk).


---

`66-71`: **Use the imported unit constant**

Replace `seconds` variable with `second`.

Apply:

```diff
 def chirp_stft_patch(chirp_patch):
     """Perform sensible stft on patch."""
-    out = chirp_patch.stft(time=0.5 * seconds)
+    out = chirp_patch.stft(time=0.5 * second)
     return out.update_attrs(history=[])

300-307: Minor consistency: compare to local variable

Use patch2 for the final equality check (already unpacked above).

Apply:

-        assert patch1.equals(chirp_patch, close=True)
+        assert patch1.equals(patch2, close=True)

308-314: Avoid function-local imports in tests

Reuse module-level imports for consistency and speed.

Apply:

-    def test_round_trip_2(self):
+    def test_round_trip_2(self):
         """Another round trip test from the doctests."""
-        import dascore as dc
-        from dascore.units import second
-
         patch = dc.get_example_patch("chirp")
         # Simple stft with 10 second window and 4 seconds overlap
         pa1 = patch.stft(time=10 * second, overlap=4 * second)

318-330: Add a cleanup assertion after istft

Validate that private STFT metadata/coords don’t leak after inverse.

Apply (adds a new test):

         assert pa2.equals(patch, close=True)
 
+    def test_no_private_stft_metadata_after_inverse(self, chirp_stft_patch):
+        """_stft* attrs/coords should be removed after istft."""
+        pa2 = chirp_stft_patch.istft()
+        assert not any(k.startswith("_stft") for k in dict(pa2.attrs).keys())
+        assert "_stft_window" not in pa2.coords.coord_map
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9232dd4 and 7477bc3.

📒 Files selected for processing (11)
  • dascore/core/attrs.py (1 hunks)
  • dascore/core/coords.py (1 hunks)
  • dascore/core/patch.py (1 hunks)
  • dascore/proc/coords.py (1 hunks)
  • dascore/transform/__init__.py (1 hunks)
  • dascore/transform/fourier.py (4 hunks)
  • dascore/utils/time.py (1 hunks)
  • pyproject.toml (1 hunks)
  • tests/test_core/test_attrs.py (1 hunks)
  • tests/test_transform/test_fourier.py (3 hunks)
  • tests/test_utils/test_time.py (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
tests/test_core/test_attrs.py (1)
dascore/core/attrs.py (2)
  • drop_private (257-261)
  • drop (250-255)
dascore/core/patch.py (1)
dascore/transform/fourier.py (2)
  • stft (373-462)
  • istft (498-547)
dascore/transform/__init__.py (1)
dascore/transform/fourier.py (3)
  • dft (113-203)
  • stft (373-462)
  • istft (498-547)
dascore/transform/fourier.py (6)
dascore/core/attrs.py (5)
  • PatchAttrs (65-294)
  • update (234-248)
  • drop (250-255)
  • get (174-179)
  • items (181-183)
dascore/core/coordmanager.py (8)
  • get_coord_manager (1036-1124)
  • get_coord (990-1009)
  • update (230-286)
  • min (1011-1013)
  • step (1019-1021)
  • new (431-449)
  • ndim (758-760)
  • shape (744-750)
dascore/core/coords.py (11)
  • get_coord (1475-1663)
  • index (698-717)
  • update (738-751)
  • update (925-927)
  • values (942-946)
  • values (1201-1216)
  • min (438-440)
  • get_sample_count (753-789)
  • data (606-608)
  • new (592-603)
  • ndim (468-470)
dascore/proc/coords.py (1)
  • get_coord (87-139)
dascore/utils/misc.py (2)
  • broadcast_for_index (136-157)
  • iterate (256-268)
dascore/utils/patch.py (1)
  • get_dim_axis_value (551-635)
tests/test_transform/test_fourier.py (5)
dascore/exceptions.py (1)
  • PatchError (30-31)
dascore/transform/fourier.py (2)
  • stft (373-462)
  • istft (498-547)
dascore/units.py (1)
  • get_quantity (70-94)
tests/conftest.py (2)
  • patch (375-377)
  • random_patch (282-284)
dascore/examples.py (3)
  • chirp (239-293)
  • get_example_patch (635-682)
  • random_patch (29-111)
tests/test_utils/test_time.py (2)
tests/test_core/test_coords.py (1)
  • test_dtype (952-957)
dascore/utils/time.py (2)
  • is_datetime64 (405-407)
  • is_timedelta64 (410-412)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (15)
  • GitHub Check: test_code (windows-latest, 3.10)
  • GitHub Check: test_code (windows-latest, 3.11)
  • GitHub Check: test_code (windows-latest, 3.12)
  • GitHub Check: test_code (ubuntu-latest, 3.12)
  • GitHub Check: test_code (macos-latest, 3.10)
  • GitHub Check: test_code (macos-latest, 3.11)
  • GitHub Check: test_code (macos-latest, 3.12)
  • GitHub Check: test_code (ubuntu-latest, 3.10)
  • GitHub Check: test_code (ubuntu-latest, 3.11)
  • GitHub Check: test_code_min_deps (macos-latest, 3.12)
  • GitHub Check: test_code_min_deps (windows-latest, 3.12)
  • GitHub Check: test_code_min_deps (macos-latest, 3.13)
  • GitHub Check: test_code_min_deps (windows-latest, 3.13)
  • GitHub Check: test_code_min_deps (ubuntu-latest, 3.13)
  • GitHub Check: test_code_min_deps (ubuntu-latest, 3.12)
🔇 Additional comments (8)
tests/test_core/test_attrs.py (1)

253-259: Rename clarifies scope: good change.

The test now explicitly targets private attributes; behavior is validated correctly.

tests/test_utils/test_time.py (2)

402-408: LGTM! Good test coverage for dtype handling.

The new test properly validates that is_datetime64 can handle numpy dtype objects directly, extending coverage beyond just arrays and values.


493-499: LGTM! Consistent test coverage for timedelta dtype handling.

The test properly validates that is_timedelta64 can handle numpy dtype objects, mirroring the datetime64 dtype test implementation.

dascore/transform/fourier.py (3)

341-370: Well-structured STFT coordinate generation.

The coordinate generation logic correctly handles datetime/timedelta conversion and properly stores the STFT window and original coordinate for inverse transformation.


372-425: Comprehensive STFT implementation with proper documentation.

The function has excellent parameter documentation and clear examples covering various use cases. The implementation correctly handles window creation, overlap calculation, and detrending options.


498-547: Robust ISTFT implementation with proper validation.

The inverse STFT correctly validates that the patch has undergone STFT, checks invertibility, and properly trims padding. The coordinate reconstruction logic is sound.

dascore/transform/__init__.py (1)

10-10: LGTM! Proper API exposure for STFT functions.

The new STFT and ISTFT functions are correctly exported alongside the existing DFT functions, making them available in the public API.

dascore/core/patch.py (1)

351-352: Expose stft/istft on Patch — LGTM

Binding transform.stft/istft onto Patch is consistent with other transform shortcuts and enables the method-style API. No issues spotted.

Comment on lines 429 to 434
window_samples = coord.get_sample_count(val, samples=samples, enforce_lt_coord=True)
sampling_rate = 1 / dc.to_float(coord.step)
Copy link
Contributor

@coderabbitai coderabbitai bot Sep 1, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add validation for sampling rate calculation.

When the coordinate step is zero or None, the sampling rate calculation will fail. Consider adding validation.

     coord = patch.get_coord(dim, require_sorted=True)
     window_samples = coord.get_sample_count(val, samples=samples, enforce_lt_coord=True)
+    if coord.step is None or coord.step == 0:
+        msg = f"Coordinate {dim} must be evenly sampled for STFT"
+        raise PatchError(msg)
     sampling_rate = 1 / dc.to_float(coord.step)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
window_samples = coord.get_sample_count(val, samples=samples, enforce_lt_coord=True)
sampling_rate = 1 / dc.to_float(coord.step)
coord = patch.get_coord(dim, require_sorted=True)
window_samples = coord.get_sample_count(val, samples=samples, enforce_lt_coord=True)
if coord.step is None or coord.step == 0:
msg = f"Coordinate {dim} must be evenly sampled for STFT"
raise PatchError(msg)
sampling_rate = 1 / dc.to_float(coord.step)
🤖 Prompt for AI Agents
In dascore/transform/fourier.py around lines 429 to 430, the sampling_rate
calculation doesn't validate coord.step; update the code to first check
coord.step is not None, convert it with dc.to_float into a local variable (e.g.,
step_f), then verify step_f is not zero (and is finite) before computing
sampling_rate = 1 / step_f; if coord.step is None or step_f == 0 or not
math.isfinite(step_f) raise a ValueError with a clear message indicating an
invalid coordinate step so callers get a descriptive error instead of a crash.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

added require_evenly_sampled to get_coord call so we gauruntee step is not None and gt 0.

Copy link
Contributor

Choose a reason for hiding this comment

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

@d-chambers Perfect! Using require_evenly_sampled=True is actually a much cleaner solution than my suggestion. This approach prevents the issue at the source by ensuring the coordinate has a valid step value before any calculations, rather than adding validation after the fact. This is more consistent with the codebase patterns and eliminates the need for additional error handling code.


✏️ Learnings added
Learnt from: d-chambers
PR: DASDAE/dascore#523
File: dascore/transform/fourier.py:433-434
Timestamp: 2025-09-02T16:22:22.829Z
Learning: In dascore STFT implementation, use `require_evenly_sampled=True` on `get_coord` calls instead of manual validation for coord.step to ensure sampling rate calculations are safe.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment on lines +383 to 403
def _is_dtype(obj, numpy_dtype, pandas_dtype) -> bool:
"""
Test if a variety of object types are of numpy or pandas dtype.
Returns True if the object is a numpy type, pandas type,
numpy dtype, pandas dtype, or an array-like of dtype values.
"""
# Handle scalars: np.datetime64, pandas.Timestamp
if isinstance(obj, numpy_dtype | pandas_dtype):
return True
# Handle numpy/pandas datetime64 dtypes directly
if isinstance(obj, (np.dtype | pd.api.extensions.ExtensionDtype)):
return np.issubdtype(obj, numpy_dtype)
# Handle pandas Series
if isinstance(obj, pd.Series):
return np.issubdtype(obj.dtype, numpy_dtype)
# Handle array-like objects (numpy arrays, lists, tuples)
if isinstance(obj, np.ndarray | list | tuple):
return np.issubdtype(np.asarray(obj).dtype, numpy_dtype)
return False

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix isinstance() usage with unions; currently raises TypeError on py310.

Replace A | B with (A, B) in isinstance checks. Same for dtype and array-like checks.

-def _is_dtype(obj, numpy_dtype, pandas_dtype) -> bool:
+def _is_dtype(obj, numpy_dtype, pandas_dtype) -> bool:
@@
-    if isinstance(obj, numpy_dtype | pandas_dtype):
+    if isinstance(obj, (numpy_dtype, pandas_dtype)):
         return True
@@
-    if isinstance(obj, (np.dtype | pd.api.extensions.ExtensionDtype)):
+    if isinstance(obj, (np.dtype, pd.api.extensions.ExtensionDtype)):
         return np.issubdtype(obj, numpy_dtype)
@@
-    if isinstance(obj, pd.Series):
+    if isinstance(obj, pd.Series):
         return np.issubdtype(obj.dtype, numpy_dtype)
@@
-    if isinstance(obj, np.ndarray | list | tuple):
+    if isinstance(obj, (np.ndarray, list, tuple)):
         return np.issubdtype(np.asarray(obj).dtype, numpy_dtype)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _is_dtype(obj, numpy_dtype, pandas_dtype) -> bool:
"""
Test if a variety of object types are of numpy or pandas dtype.
Returns True if the object is a numpy type, pandas type,
numpy dtype, pandas dtype, or an array-like of dtype values.
"""
# Handle scalars: np.datetime64, pandas.Timestamp
if isinstance(obj, numpy_dtype | pandas_dtype):
return True
# Handle numpy/pandas datetime64 dtypes directly
if isinstance(obj, (np.dtype | pd.api.extensions.ExtensionDtype)):
return np.issubdtype(obj, numpy_dtype)
# Handle pandas Series
if isinstance(obj, pd.Series):
return np.issubdtype(obj.dtype, numpy_dtype)
# Handle array-like objects (numpy arrays, lists, tuples)
if isinstance(obj, np.ndarray | list | tuple):
return np.issubdtype(np.asarray(obj).dtype, numpy_dtype)
return False
def _is_dtype(obj, numpy_dtype, pandas_dtype) -> bool:
"""
Test if a variety of object types are of numpy or pandas dtype.
Returns True if the object is a numpy type, pandas type,
numpy dtype, pandas dtype, or an array-like of dtype values.
"""
# Handle scalars: np.datetime64, pandas.Timestamp
if isinstance(obj, (numpy_dtype, pandas_dtype)):
return True
# Handle numpy/pandas datetime64 dtypes directly
if isinstance(obj, (np.dtype, pd.api.extensions.ExtensionDtype)):
return np.issubdtype(obj, numpy_dtype)
# Handle pandas Series
if isinstance(obj, pd.Series):
return np.issubdtype(obj.dtype, numpy_dtype)
# Handle array-like objects (numpy arrays, lists, tuples)
if isinstance(obj, (np.ndarray, list, tuple)):
return np.issubdtype(np.asarray(obj).dtype, numpy_dtype)
return False
🤖 Prompt for AI Agents
In dascore/utils/time.py around lines 383 to 403, the isinstance calls use the
Python 3.10 union operator (A | B), which raises TypeError; replace each A | B
with an equivalent tuple (A, B). Concretely: change isinstance(obj, numpy_dtype
| pandas_dtype) to isinstance(obj, (numpy_dtype, pandas_dtype)); change
isinstance(obj, (np.dtype | pd.api.extensions.ExtensionDtype)) to
isinstance(obj, (np.dtype, pd.api.extensions.ExtensionDtype)); and change
isinstance(obj, np.ndarray | list | tuple) to isinstance(obj, (np.ndarray, list,
tuple)). Ensure all isinstance uses in this block use tuples, not | unions.

Copy link
Contributor

@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: 0

🧹 Nitpick comments (6)
tests/test_transform/test_fourier.py (6)

285-290: Fix typo: “insure” → “ensure”.

-    def test_type(self, chirp_stft_patch, chirp_patch):
-        """Simply insure the correct type was returned."""
+    def test_type(self, chirp_stft_patch, chirp_patch):
+        """Simply ensure the correct type was returned."""

291-296: Add a negative test for mismatched window length vs time samples.
Currently, passing an ndarray window whose length ≠ window_samples could misconfigure mfft/window pairing. Add a test that asserts a clear error or automatic alignment.

Would you like me to draft the test?


301-305: Consider hoisting this fixture to module scope.
Slightly improves discoverability/reuse; behavior remains identical.

-    @pytest.fixture(scope="class")
-    def chirp_round_tripped(self, chirp_stft_patch):
-        """Round trip patch through stft."""
-        return chirp_stft_patch.istft()
+@pytest.fixture(scope="session")
+def chirp_round_tripped(chirp_stft_patch):
+    """Round trip patch through stft."""
+    return chirp_stft_patch.istft()

314-324: Remove redundant inner imports; use module-level imports.
Minor cleanup.

-        import dascore as dc
-        from dascore.units import second
-
-        patch = dc.get_example_patch("chirp")
+        patch = dc.get_example_patch("chirp")
         # Simple stft with 10 second window and 4 seconds overlap
-        pa1 = patch.stft(time=10 * second, overlap=4 * second)
+        pa1 = patch.stft(time=10 * dc.get_unit("s"), overlap=4 * dc.get_unit("s"))

Alternatively, import second once at module top.


325-330: Docstring typo: should be “hasn’t undergone stft”.

-        """A patch that hasn't undergone istft can't be used."""
+        """A patch that hasn't undergone stft can't be used."""

Also, good job asserting the error message substring.


314-324: Optional: Assert STFT/ISTFT metadata cleanup after round-trip.
After istft, _stft* attrs should be gone. Add a quick check to prevent regressions.

         pa2 = pa1.istft()
-        assert pa2.equals(patch, close=True)
+        assert pa2.equals(patch, close=True)
+        assert not any(k.startswith("_stft") for k in dict(pa2.attrs))
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7477bc3 and 8c9bcd3.

📒 Files selected for processing (1)
  • tests/test_transform/test_fourier.py (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
tests/test_transform/test_fourier.py (4)
dascore/exceptions.py (1)
  • PatchError (30-31)
dascore/transform/fourier.py (3)
  • dft (113-203)
  • stft (373-462)
  • istft (498-547)
dascore/units.py (1)
  • get_quantity (70-94)
dascore/examples.py (2)
  • chirp (239-293)
  • get_example_patch (635-682)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (15)
  • GitHub Check: test_code (ubuntu-latest, 3.10)
  • GitHub Check: test_code (macos-latest, 3.12)
  • GitHub Check: test_code (windows-latest, 3.11)
  • GitHub Check: test_code (ubuntu-latest, 3.12)
  • GitHub Check: test_code (windows-latest, 3.12)
  • GitHub Check: test_code (windows-latest, 3.10)
  • GitHub Check: test_code (macos-latest, 3.11)
  • GitHub Check: test_code (ubuntu-latest, 3.11)
  • GitHub Check: test_code (macos-latest, 3.10)
  • GitHub Check: test_code_min_deps (windows-latest, 3.13)
  • GitHub Check: test_code_min_deps (macos-latest, 3.13)
  • GitHub Check: test_code_min_deps (windows-latest, 3.12)
  • GitHub Check: test_code_min_deps (macos-latest, 3.12)
  • GitHub Check: test_code_min_deps (ubuntu-latest, 3.13)
  • GitHub Check: test_code_min_deps (ubuntu-latest, 3.12)
🔇 Additional comments (7)
tests/test_transform/test_fourier.py (7)

10-10: Good: explicit exception type import for assertions.


15-15: Unit alias looks fine.
This exercises the new “seconds” alias path in get_quantity.


59-64: Fixture LGTM.
Using the example chirp keeps tests realistic.


66-71: Clearing history to enable equality checks is appropriate.


73-77: Detrended STFT fixture LGTM.
Covers the non-invertible branch.


331-335: Invertibility guard test looks correct.
Covers detrended path that must raise.


306-313: Remove the .ndim warning—Patch.ndim is a defined property and used throughout the codebase.

Likely an incorrect or invalid review comment.

Copy link
Contributor

@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: 1

♻️ Duplicate comments (2)
dascore/transform/fourier.py (2)

160-161: Fix incorrect cross-reference in See Also section.

The second reference should be to stft, not idft again.

-    - [idft](`dascore.transform.fourier.idft`)
-    - [stft](`dascore.transform.fourier.stft`)
+    - [idft](`dascore.transform.fourier.idft`)
+    - [stft](`dascore.transform.fourier.stft`)

430-434: Add validation for sampling rate calculation.

When the coordinate step is zero or None, the sampling rate calculation will fail. Consider adding validation.

     coord = patch.get_coord(dim, require_evenly_sampled=True)
     window_samples = coord.get_sample_count(val, samples=samples, enforce_lt_coord=True)
+    if coord.step is None or coord.step == 0:
+        msg = f"Coordinate {dim} must be evenly sampled for STFT"
+        raise PatchError(msg)
     sampling_rate = 1 / dc.to_float(coord.step)
🧹 Nitpick comments (2)
dascore/transform/fourier.py (1)

310-311: Fix cross-reference consistency in idft See Also section.

Update the second reference to maintain consistency with the dft function's See Also section.

-    - [dft](`dascore.transform.fourier.dft`)
-    - [istft](`dascore.transform.fourier.istft`)
+    - [dft](`dascore.transform.fourier.dft`)
+    - [istft](`dascore.transform.fourier.istft`)
.github/actions/mamba-install-dascore/action.yml (1)

45-45: post-cleanup: "shell-init" is valid (and is the default); optional to keep.

Docs list none | shell-init | environment | all, with shell-init as the default. Keeping this is fine for GitHub-hosted runners; if you move to self-hosted and want stronger isolation, consider environment or all. (github.com)

Apply if you prefer relying on defaults:

-        post-cleanup: "shell-init"
+        # post-cleanup: default is "shell-init"; override only if you need stronger cleanup
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 8c9bcd3 and 1f5ddb3.

📒 Files selected for processing (4)
  • .github/actions/mamba-install-dascore/action.yml (1 hunks)
  • .github/workflows/runtests.yml (1 hunks)
  • dascore/transform/fourier.py (4 hunks)
  • tests/test_transform/test_fourier.py (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/test_transform/test_fourier.py
🧰 Additional context used
🧬 Code graph analysis (1)
dascore/transform/fourier.py (7)
dascore/core/attrs.py (5)
  • PatchAttrs (65-294)
  • update (234-248)
  • drop (250-255)
  • get (174-179)
  • items (181-183)
dascore/core/coordmanager.py (8)
  • get_coord_manager (1036-1124)
  • get_coord (990-1009)
  • update (230-286)
  • min (1011-1013)
  • step (1019-1021)
  • new (431-449)
  • ndim (758-760)
  • shape (744-750)
dascore/proc/coords.py (1)
  • get_coord (87-139)
dascore/core/coords.py (10)
  • get_coord (1475-1663)
  • index (698-717)
  • update (738-751)
  • update (925-927)
  • values (942-946)
  • values (1201-1216)
  • min (438-440)
  • get_sample_count (753-789)
  • new (592-603)
  • ndim (468-470)
dascore/utils/misc.py (2)
  • broadcast_for_index (136-157)
  • iterate (256-268)
dascore/utils/patch.py (1)
  • get_dim_axis_value (551-635)
dascore/utils/time.py (2)
  • is_datetime64 (405-407)
  • is_timedelta64 (410-412)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (17)
  • GitHub Check: test_code_min_deps (macos-latest, 3.12)
  • GitHub Check: test_code_min_deps (windows-latest, 3.12)
  • GitHub Check: test_code_min_deps (windows-latest, 3.13)
  • GitHub Check: test_code_min_deps (ubuntu-latest, 3.12)
  • GitHub Check: test_code_min_deps (macos-latest, 3.13)
  • GitHub Check: test_code_min_deps (ubuntu-latest, 3.13)
  • GitHub Check: test_code (windows-latest, 3.11)
  • GitHub Check: test_code (windows-latest, 3.10)
  • GitHub Check: test_code (windows-latest, 3.13)
  • GitHub Check: test_code (windows-latest, 3.12)
  • GitHub Check: test_code (macos-latest, 3.12)
  • GitHub Check: test_code (ubuntu-latest, 3.12)
  • GitHub Check: test_code (ubuntu-latest, 3.13)
  • GitHub Check: test_code (macos-latest, 3.11)
  • GitHub Check: test_code (macos-latest, 3.10)
  • GitHub Check: test_code (ubuntu-latest, 3.11)
  • GitHub Check: test_code (ubuntu-latest, 3.10)
🔇 Additional comments (11)
dascore/transform/fourier.py (11)

11-11: LGTM: Import for partial function.

The partial import is appropriately used for the detrending functionality in the stft function.


13-13: LGTM: Import additions for STFT functionality.

The imports are correctly added to support the new STFT implementation: Any for type hints, ShortTimeFFT for the core functionality, and get_window for window functions.

Also applies to: 17-18


20-20: LGTM: Import dascore for internal utilities.

The import dascore as dc provides access to internal utility functions like to_timedelta64 and get_quantity.


21-21: LGTM: Additional imports for STFT support.

All new imports are appropriately used:

  • ndarray for type hints
  • PatchError for error handling
  • Quantity, invert_quantity, percent for unit handling
  • broadcast_for_index for array manipulation
  • get_dim_axis_value for dimension handling
  • Time utilities for datetime/timedelta support

Also applies to: 26-27, 28-28, 32-32, 35-35


341-373: LGTM: Well-structured coordinate management for STFT.

The _get_stft_coords function correctly handles:

  • Dimension transformation using FourierTransformatter
  • Time coordinate creation with proper datetime/timedelta support
  • Frequency coordinate generation with appropriate units
  • Storage of metadata for inverse transform

376-384: LGTM: Comprehensive STFT function signature.

The function signature covers all necessary parameters:

  • Flexible taper window specification
  • Overlap control with units
  • Sample vs. time-based parameterization
  • Detrending option with proper warning about invertibility impact

435-451: LGTM: Comprehensive window and STFT setup.

The implementation correctly:

  • Handles different window specifications (string, array, tuple)
  • Calculates hop from overlap ensuring it's less than window size
  • Sets appropriate FFT mode based on data type
  • Configures ShortTimeFFT with all necessary parameters

452-467: LGTM: STFT execution and metadata storage.

The function correctly:

  • Supports optional detrending with appropriate function selection
  • Executes the STFT on the specified axis
  • Stores all necessary metadata for inverse transform
  • Updates patch attributes while preserving non-STFT information

470-482: LGTM: Robust inverse axes validation.

The _get_inverse_axes function properly validates that the required STFT metadata exists before attempting inverse transformation.


485-500: LGTM: Proper coordinate restoration for inverse STFT.

The _get_istft_coord function correctly:

  • Removes STFT-specific metadata
  • Restores the original time coordinate
  • Reconstructs the proper dimension ordering

503-552: LGTM: Comprehensive inverse STFT implementation.

The istft function properly:

  • Validates invertibility and detrending status
  • Reconstructs the ShortTimeFFT object with stored parameters
  • Performs the inverse transform with correct axis specification
  • Handles data trimming to remove padding effects
  • Cleans up STFT-specific attributes

The implementation is robust and handles all edge cases appropriately.

Copy link
Contributor

@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: 4

♻️ Duplicate comments (1)
dascore/utils/time.py (1)

383-401: Fix isinstance() with unions to support Python ≤3.10.

Using A | B in isinstance raises TypeError on py310. Replace with tuples.

Apply:

-    if isinstance(obj, numpy_dtype | pandas_dtype):
+    if isinstance(obj, (numpy_dtype, pandas_dtype)):
@@
-    if isinstance(obj, (np.dtype | pd.api.extensions.ExtensionDtype)):
+    if isinstance(obj, (np.dtype, pd.api.extensions.ExtensionDtype)):
@@
-    if isinstance(obj, np.ndarray | list | tuple):
+    if isinstance(obj, (np.ndarray, list, tuple)):
🧹 Nitpick comments (4)
dascore/core/attrs.py (1)

250-256: Type the signature and simplify via model_dump(exclude=...)

Leverage pydantic’s exclude to avoid building intermediate sets/dicts and add explicit types/return for clarity.

-    def drop(self, *args):
-        """Drop specific keys if they exist."""
-        contents = dict(self)
-        ok_to_keep = set(contents) - set(args)
-        out = {i: v for i, v in contents.items() if i in ok_to_keep}
-        return self.__class__(**out)
+    def drop(self, *keys: str) -> Self:
+        """Return a new PatchAttrs with the given top-level keys removed (unknown keys are ignored)."""
+        drop_keys = frozenset(keys)
+        new = self.model_dump(exclude=drop_keys)
+        return self.__class__(**new)
dascore/utils/time.py (1)

405-408: Docstring mismatch (datetime vs timedelta).

Update to reflect datetime64.

-def is_datetime64(obj):
-    """Determine if an object represents a timedelta64 dtype or value(s)."""
+def is_datetime64(obj):
+    """Determine if an object represents a datetime64 dtype or value(s)."""
dascore/transform/fourier.py (2)

448-451: Validate fft_mode choice for complex input across SciPy versions.

If "centered" isn’t supported for your SciPy target, prefer "twosided" and apply fftshift yourself, or document the minimum required SciPy.


430-447: Guard hop/window edge cases.

coord.get_sample_count enforces overlap < window; consider asserting hop > 0 to fail fast on pathological inputs.

 hop = window_samples - overlap
+assert hop > 0, "STFT hop must be > 0."
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 1f5ddb3 and 1f28e76.

📒 Files selected for processing (13)
  • .github/actions/mamba-install-dascore/action.yml (1 hunks)
  • .github/workflows/runtests.yml (1 hunks)
  • dascore/core/attrs.py (1 hunks)
  • dascore/core/coords.py (1 hunks)
  • dascore/core/patch.py (1 hunks)
  • dascore/proc/coords.py (1 hunks)
  • dascore/transform/__init__.py (1 hunks)
  • dascore/transform/fourier.py (4 hunks)
  • dascore/utils/time.py (1 hunks)
  • pyproject.toml (0 hunks)
  • tests/test_core/test_attrs.py (1 hunks)
  • tests/test_transform/test_fourier.py (3 hunks)
  • tests/test_utils/test_time.py (2 hunks)
💤 Files with no reviewable changes (1)
  • pyproject.toml
✅ Files skipped from review due to trivial changes (1)
  • dascore/core/coords.py
🚧 Files skipped from review as they are similar to previous changes (8)
  • tests/test_core/test_attrs.py
  • .github/actions/mamba-install-dascore/action.yml
  • dascore/transform/init.py
  • .github/workflows/runtests.yml
  • tests/test_utils/test_time.py
  • dascore/proc/coords.py
  • tests/test_transform/test_fourier.py
  • dascore/core/patch.py
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-02T16:22:22.829Z
Learnt from: d-chambers
PR: DASDAE/dascore#523
File: dascore/transform/fourier.py:433-434
Timestamp: 2025-09-02T16:22:22.829Z
Learning: In dascore STFT implementation, use `require_evenly_sampled=True` on `get_coord` calls instead of manual validation for coord.step to ensure sampling rate calculations are safe.

Applied to files:

  • dascore/transform/fourier.py
🧬 Code graph analysis (1)
dascore/transform/fourier.py (5)
dascore/core/attrs.py (4)
  • PatchAttrs (65-294)
  • drop (250-255)
  • get (174-179)
  • items (181-183)
dascore/core/coordmanager.py (6)
  • get_coord_manager (1036-1124)
  • get_coord (990-1009)
  • min (1011-1013)
  • step (1019-1021)
  • ndim (758-760)
  • shape (744-750)
dascore/core/coords.py (7)
  • get_coord (1475-1663)
  • index (698-717)
  • values (942-946)
  • values (1201-1216)
  • min (438-440)
  • get_sample_count (753-789)
  • ndim (468-470)
dascore/utils/misc.py (2)
  • broadcast_for_index (136-157)
  • iterate (256-268)
dascore/utils/patch.py (1)
  • get_dim_axis_value (553-637)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: test_code (ubuntu-latest, 3.10)
  • GitHub Check: test_code (ubuntu-latest, 3.13)
  • GitHub Check: test_code (windows-latest, 3.11)
  • GitHub Check: test_code (windows-latest, 3.12)
  • GitHub Check: test_code (macos-latest, 3.10)
🔇 Additional comments (2)
dascore/core/attrs.py (1)

250-256: Confirm semantics when dropping "coords" and/or "dims"

Dropping one without the other can leave inconsistent assumptions for helpers like coords_from_dims and dynamic attrs. Either document this explicitly or add a quick guard/repair. Do current tests cover these cases?

dascore/transform/fourier.py (1)

466-467: Drop the update-method concern; PatchAttrs.update is defined and returns a new instance.

Likely an incorrect or invalid review comment.

Comment on lines 485 to 501
def _get_istft_coord(coords, frequency_axis, time_axis):
"""
Get the coordinate manager for the inverse of the short time fourier transform.
"""
dims = coords.dims
# Create new time coordinate.
coord_map = dict(coords.coord_map)
coord_map.pop("_stft_window")
time = coord_map.pop("_stft_old_coord")
coord_map.pop(coords.dims[frequency_axis])
coord_map[dims[time_axis]] = time
# Get new dimensions
new_dims = list(dims)
new_dims[frequency_axis] = dims[time_axis]
new_dims.pop(time_axis)
return get_coord_manager(coords=coord_map, dims=new_dims), time

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

_get_istft_coord returns a (None, coord) tuple instead of a Coord; breaks len(coord) and cm shape.

Use the popped value’s second element and reassign with the proper key.

-    coord_map.pop("_stft_window")
-    time = coord_map.pop("_stft_old_coord")
-    coord_map.pop(coords.dims[frequency_axis])
-    coord_map[dims[time_axis]] = time
+    coord_map.pop("_stft_window")
+    # Unwrap the stored original coordinate.
+    orig_time_coord = coord_map.pop("_stft_old_coord")[1]
+    freq_name = dims[frequency_axis]
+    time_name = dims[time_axis]
+    coord_map.pop(freq_name)
+    coord_map[time_name] = orig_time_coord
@@
-    return get_coord_manager(coords=coord_map, dims=new_dims), time
+    return get_coord_manager(coords=coord_map, dims=new_dims), orig_time_coord
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _get_istft_coord(coords, frequency_axis, time_axis):
"""
Get the coordinate manager for the inverse of the short time fourier transform.
"""
dims = coords.dims
# Create new time coordinate.
coord_map = dict(coords.coord_map)
coord_map.pop("_stft_window")
time = coord_map.pop("_stft_old_coord")
coord_map.pop(coords.dims[frequency_axis])
coord_map[dims[time_axis]] = time
# Get new dimensions
new_dims = list(dims)
new_dims[frequency_axis] = dims[time_axis]
new_dims.pop(time_axis)
return get_coord_manager(coords=coord_map, dims=new_dims), time
def _get_istft_coord(coords, frequency_axis, time_axis):
"""
Get the coordinate manager for the inverse of the short time fourier transform.
"""
dims = coords.dims
# Create new time coordinate.
coord_map = dict(coords.coord_map)
coord_map.pop("_stft_window")
- time = coord_map.pop("_stft_old_coord")
- coord_map.pop(coords.dims[frequency_axis])
# Unwrap the stored original coordinate.
orig_time_coord = coord_map.pop("_stft_old_coord")[1]
freq_name = dims[frequency_axis]
time_name = dims[time_axis]
coord_map.pop(freq_name)
coord_map[time_name] = orig_time_coord
# Get new dimensions
new_dims = list(dims)
new_dims[frequency_axis] = dims[time_axis]
new_dims.pop(time_axis)
return get_coord_manager(coords=coord_map, dims=new_dims), orig_time_coord
🤖 Prompt for AI Agents
In dascore/transform/fourier.py around lines 485 to 501, _get_istft_coord
currently pops "_stft_old_coord" and assigns the popped value directly to
`time`, which yields a (None, coord) tuple instead of the Coord object; this
breaks len(coord) and coord manager shapes. Replace the assignment to use the
popped value's second element (the coord) and reassign it back into coord_map
under the proper time-dimension key so `time` is the actual Coord object and
coord_map contains the correct mapping before calling get_coord_manager.

Copy link
Contributor

@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: 0

♻️ Duplicate comments (3)
dascore/transform/fourier.py (3)

376-376: Decorator likely misapplied; use @patch_function() (call it).

Without calling, stft may not be registered as a Patch method.

-@patch_function
+@patch_function()
 def stft(

531-534: Expose istft as a Patch method.

Add the patch decorator so patch.istft() works.

- def istft(
+@patch_function()
+def istft(
     patch,
 ):

499-515: Fix _get_istft_coord to return a Coord (not a (None, coord) tuple) and reassign with the correct key.

Current code stores and re-injects the tuple; this breaks len(coord) and shape checks later, and causes the scale/divide in istft to fail.

-    coord_map.pop("_stft_window")
-    time = coord_map.pop("_stft_old_coord")
-    coord_map.pop(coords.dims[frequency_axis])
-    coord_map[dims[time_axis]] = time
+    coord_map.pop("_stft_window")
+    # Unwrap the stored original coordinate.
+    orig_time_coord = coord_map.pop("_stft_old_coord")[1]
+    freq_name = dims[frequency_axis]
+    time_name = dims[time_axis]
+    coord_map.pop(freq_name)
+    # Reassign the original time coord under the proper key.
+    coord_map[time_name] = (time_name, orig_time_coord)
@@
-    new_dims[frequency_axis] = dims[time_axis]
-    new_dims.pop(time_axis)
-    return get_coord_manager(coords=coord_map, dims=new_dims), time
+    new_dims[frequency_axis] = dims[time_axis]
+    new_dims.pop(time_axis)
+    return get_coord_manager(coords=coord_map, dims=new_dims), orig_time_coord
🧹 Nitpick comments (5)
dascore/transform/spectro.py (1)

61-66: Place @deprecate outermost to ensure deprecation is visible to users and tooling.

With the current order, patch_function() wraps the deprecated function, which can hide the typing-level deprecation and doc updates. Make deprecate the top decorator.

-@patch_function()
-@deprecate(
-    info="Use Patch.stft() instead.",
-    since="0.1.11",
-    removed_in="0.2.0",
-)
+@deprecate(
+    info="Use Patch.stft() instead.",
+    since="0.1.11",
+    removed_in="0.2.0",
+)
+@patch_function()
 def spectrogram(patch: PatchType, dim: "str" = "time", **kwargs) -> PatchType:
tests/test_transform/test_fourier.py (2)

12-16: Avoid two “second(s)” aliases; use the canonical second.

Drop the custom seconds alias and use the imported second for clarity.

-from dascore.units import get_quantity, second
+from dascore.units import get_quantity, second
@@
-seconds = get_quantity("seconds")
@@
-    out = chirp_patch.stft(time=0.5 * seconds)
+    out = chirp_patch.stft(time=0.5 * second)

Also applies to: 67-71


306-327: Make the DFT<>STFT equivalence assertion robust to zero magnitudes.

Ratio can blow up on zeros. Compare magnitudes directly with tolerances.

-        factor = np.abs(ar1) / np.abs(ar2)
-        assert np.allclose(factor, 1.0)
+        assert np.allclose(np.abs(ar1), np.abs(ar2), rtol=1e-6, atol=1e-12)
dascore/transform/fourier.py (2)

426-431: Fix minor docstring typos.

Small polish for user-facing docs.

-    - The output is scalled the same as [Patch.dft](`dascore.Patch.dft`).
-    - For a given sliding window, Parseval's thereom doesn't hold exactly
-      unless a boxcare window is used.
+    - The output is scaled the same as [Patch.dft](`dascore.Patch.dft`).
+    - For a given sliding window, Parseval's theorem doesn't hold exactly
+      unless a boxcar window is used.

535-541: Tighten ISTFT docstring phrasing.

-    Inverse a short-time fourier transform.
+    Inverse the short-time Fourier transform.
@@
-        A patch return from [stft](`dascore.transform.fourier.stft`).
+        A Patch returned from [stft](`dascore.transform.fourier.stft`).
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 1f28e76 and b51ffe8.

📒 Files selected for processing (5)
  • dascore/core/coords.py (2 hunks)
  • dascore/transform/fourier.py (4 hunks)
  • dascore/transform/spectro.py (2 hunks)
  • pyproject.toml (1 hunks)
  • tests/test_transform/test_fourier.py (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • pyproject.toml
  • dascore/core/coords.py
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-02T16:22:22.829Z
Learnt from: d-chambers
PR: DASDAE/dascore#523
File: dascore/transform/fourier.py:433-434
Timestamp: 2025-09-02T16:22:22.829Z
Learning: In dascore STFT implementation, use `require_evenly_sampled=True` on `get_coord` calls instead of manual validation for coord.step to ensure sampling rate calculations are safe.

Applied to files:

  • dascore/transform/fourier.py
🧬 Code graph analysis (3)
tests/test_transform/test_fourier.py (5)
dascore/exceptions.py (1)
  • PatchError (30-31)
dascore/transform/fourier.py (4)
  • dft (113-203)
  • idft (280-338)
  • stft (377-481)
  • istft (531-576)
dascore/units.py (1)
  • get_quantity (70-94)
dascore/core/patch.py (7)
  • seconds (228-230)
  • channel_count (233-235)
  • Patch (27-384)
  • dims (188-190)
  • data (213-215)
  • attrs (203-205)
  • coords (208-210)
dascore/examples.py (1)
  • chirp (239-293)
dascore/transform/spectro.py (1)
dascore/utils/deprecate.py (1)
  • deprecate (16-78)
dascore/transform/fourier.py (7)
dascore/core/attrs.py (5)
  • PatchAttrs (65-294)
  • update (234-248)
  • drop (250-255)
  • get (174-179)
  • items (181-183)
dascore/core/coordmanager.py (8)
  • get_coord_manager (1036-1124)
  • get_coord (990-1009)
  • update (230-286)
  • min (1011-1013)
  • step (1019-1021)
  • new (431-449)
  • ndim (758-760)
  • shape (744-750)
dascore/core/coords.py (11)
  • get_coord (1475-1663)
  • index (698-717)
  • update (738-751)
  • update (925-927)
  • values (942-946)
  • values (1201-1216)
  • min (438-440)
  • get_sample_count (753-789)
  • data (606-608)
  • new (592-603)
  • ndim (468-470)
dascore/proc/coords.py (1)
  • get_coord (87-139)
dascore/utils/misc.py (2)
  • broadcast_for_index (136-157)
  • iterate (256-268)
dascore/utils/patch.py (3)
  • _get_data_units_from_dims (671-683)
  • _get_dx_or_spacing_and_axes (686-723)
  • get_dim_axis_value (553-637)
dascore/utils/time.py (3)
  • is_datetime64 (405-407)
  • is_timedelta64 (410-412)
  • to_float (327-333)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (17)
  • GitHub Check: test_code_min_deps (ubuntu-latest, 3.12)
  • GitHub Check: test_code_min_deps (ubuntu-latest, 3.13)
  • GitHub Check: test_code_min_deps (windows-latest, 3.12)
  • GitHub Check: test_code_min_deps (macos-latest, 3.12)
  • GitHub Check: test_code_min_deps (windows-latest, 3.13)
  • GitHub Check: test_code (windows-latest, 3.10)
  • GitHub Check: test_code (windows-latest, 3.13)
  • GitHub Check: test_code (windows-latest, 3.12)
  • GitHub Check: test_code (ubuntu-latest, 3.13)
  • GitHub Check: test_code (ubuntu-latest, 3.12)
  • GitHub Check: test_code (windows-latest, 3.11)
  • GitHub Check: test_code (macos-latest, 3.10)
  • GitHub Check: test_code (macos-latest, 3.13)
  • GitHub Check: test_code (ubuntu-latest, 3.11)
  • GitHub Check: test_code (macos-latest, 3.11)
  • GitHub Check: test_code (macos-latest, 3.12)
  • GitHub Check: test_code (ubuntu-latest, 3.10)
🔇 Additional comments (3)
dascore/transform/fourier.py (3)

438-455: Nice: even-sampling enforced at source.

Using require_evenly_sampled=True and get_sample_count(..., enforce_lt_coord=True) prevents invalid sampling rate and overlap values. Good alignment with prior learning.


521-527: No change needed: BaseCoord provides .values (and .data proxies to it).


17-19: ShortTimeFFT compatibility requirement satisfied
pyproject.toml already pins SciPy≥1.15, which exceeds the ≥1.12 requirement for ShortTimeFFT.

@d-chambers d-chambers added the documentation Improvements or additions to documentation label Sep 2, 2025
Copy link
Contributor

@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: 5

♻️ Duplicate comments (3)
dascore/transform/fourier.py (3)

439-444: Good: robust coord handling for STFT.

Using get_dim_axis_value + get_coord(require_evenly_sampled=True) + get_sample_count(enforce_lt_coord=True) is the right pattern here.


376-384: Decorator bug: call patch_function().

Without calling, the decorator likely won’t register stft as a Patch method.

-@patch_function
+@patch_function()
 def stft(

501-517: Bug: _get_istft_coord returns a (None, coord) tuple; must unwrap.

Returning/assigning the tuple breaks coord.step and len(coord) downstream.

     coord_map = dict(coords.coord_map)
     coord_map.pop("_stft_window")
-    time = coord_map.pop("_stft_old_coord")
-    coord_map.pop(coords.dims[frequency_axis])
-    coord_map[dims[time_axis]] = time
+    # Unwrap stored original coordinate.
+    orig_time_coord = coord_map.pop("_stft_old_coord")[1]
+    freq_name = dims[frequency_axis]
+    time_name = dims[time_axis]
+    coord_map.pop(freq_name)
+    coord_map[time_name] = orig_time_coord
@@
-    new_dims = list(dims)
-    new_dims[frequency_axis] = dims[time_axis]
-    new_dims.pop(time_axis)
-    return get_coord_manager(coords=coord_map, dims=new_dims), time
+    new_dims = list(dims)
+    new_dims[frequency_axis] = time_name
+    new_dims.pop(time_axis)
+    return get_coord_manager(coords=coord_map, dims=new_dims), orig_time_coord
🧹 Nitpick comments (5)
dascore/transform/fourier.py (3)

430-433: Fix typos in Notes.

Small doc fixes.

-    - The output is scalled the same as [Patch.dft](`dascore.Patch.dft`).
-    - For a given sliding window, Parseval's thereom doesn't hold exactly
-      unless a boxcare window is used.
+    - The output is scaled the same as [Patch.dft](`dascore.Patch.dft`).
+    - For a given sliding window, Parseval's theorem doesn't hold exactly
+      unless a boxcar window is used.

442-444: Minor: simplify float conversions.

No need to convert twice.

-    step = dc.to_float(coord.step)
-    sampling_rate = 1 / dc.to_float(step)
+    step = to_float(coord.step)
+    sampling_rate = 1 / step

364-371: Optional: use get_coord_tuple_map() for consistency with DFT path.

You’re mixing BaseCoord values with (None, obj) tuples in coord_map. Prefer building from get_coord_tuple_map() for uniformity.

-    coord_map = dict(patch.coords.coord_map)
+    coord_map = patch.coords.get_coord_tuple_map()
@@
-            "_stft_window": (None, window),
-            "_stft_old_coord": (None, patch.get_coord(dim)),
+            "_stft_window": (None, window),
+            "_stft_old_coord": (None, patch.get_coord(dim)),
docs/tutorial/transformations.qmd (2)

51-54: Terminology: “Short-Time Fourier Transform.”

Use the hyphenated form and correct term.

-# Short Time Fourier Transform
+# Short-Time Fourier Transform
@@
-Related to the Discrete Fourier Transform, the [Short Term Fourier Transform]
+Related to the Discrete Fourier Transform, the [Short-Time Fourier Transform]

73-78: Strengthen example: verify round-trip.

Add an assertion to demonstrate invertibility in the tutorial.

 inverse =  transformed.istft()
 
 transformed.abs().select(distance=0, samples=True).squeeze().viz.waterfall()
+assert inverse.equals(patch, close=True)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b51ffe8 and e305681.

📒 Files selected for processing (2)
  • dascore/transform/fourier.py (4 hunks)
  • docs/tutorial/transformations.qmd (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: d-chambers
PR: DASDAE/dascore#523
File: dascore/transform/fourier.py:433-434
Timestamp: 2025-09-02T16:22:22.829Z
Learning: In dascore STFT implementation, use `require_evenly_sampled=True` on `get_coord` calls instead of manual validation for coord.step to ensure sampling rate calculations are safe.
📚 Learning: 2025-09-02T16:22:22.829Z
Learnt from: d-chambers
PR: DASDAE/dascore#523
File: dascore/transform/fourier.py:433-434
Timestamp: 2025-09-02T16:22:22.829Z
Learning: In dascore STFT implementation, use `require_evenly_sampled=True` on `get_coord` calls instead of manual validation for coord.step to ensure sampling rate calculations are safe.

Applied to files:

  • dascore/transform/fourier.py
🧬 Code graph analysis (1)
dascore/transform/fourier.py (7)
dascore/core/attrs.py (2)
  • PatchAttrs (65-294)
  • get (174-179)
dascore/core/coordmanager.py (5)
  • get_coord_manager (1036-1124)
  • get_coord (990-1009)
  • step (1019-1021)
  • new (431-449)
  • shape (744-750)
dascore/proc/coords.py (1)
  • get_coord (87-139)
dascore/core/coords.py (7)
  • get_coord (1475-1663)
  • index (698-717)
  • values (942-946)
  • values (1201-1216)
  • get_sample_count (753-789)
  • data (606-608)
  • new (592-603)
dascore/exceptions.py (1)
  • PatchError (30-31)
dascore/utils/misc.py (2)
  • broadcast_for_index (136-157)
  • iterate (256-268)
dascore/utils/patch.py (3)
  • _get_data_units_from_dims (671-683)
  • _get_dx_or_spacing_and_axes (686-723)
  • get_dim_axis_value (553-637)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (18)
  • GitHub Check: test_build_docs
  • GitHub Check: test_code (macos-latest, 3.13)
  • GitHub Check: test_code (windows-latest, 3.11)
  • GitHub Check: test_code (windows-latest, 3.12)
  • GitHub Check: test_code (macos-latest, 3.11)
  • GitHub Check: test_code (windows-latest, 3.10)
  • GitHub Check: test_code (ubuntu-latest, 3.12)
  • GitHub Check: test_code (ubuntu-latest, 3.10)
  • GitHub Check: test_code (windows-latest, 3.13)
  • GitHub Check: test_code (macos-latest, 3.10)
  • GitHub Check: test_code (ubuntu-latest, 3.13)
  • GitHub Check: test_code (macos-latest, 3.12)
  • GitHub Check: test_code (ubuntu-latest, 3.11)
  • GitHub Check: test_code_min_deps (ubuntu-latest, 3.13)
  • GitHub Check: test_code_min_deps (windows-latest, 3.12)
  • GitHub Check: test_code_min_deps (ubuntu-latest, 3.12)
  • GitHub Check: test_code_min_deps (macos-latest, 3.13)
  • GitHub Check: test_code_min_deps (windows-latest, 3.13)
🔇 Additional comments (1)
dascore/transform/fourier.py (1)

519-529: Confirm window retrieval via coords.

get_coord("_stft_window") assumes CoordManager stores the window as a Coord. If this ever changes, consider storing the window in attrs instead of coords to avoid type ambiguity.

Run a quick check in tests to assert isinstance(patch.get_coord("_stft_window").values, np.ndarray).

@github-actions
Copy link

github-actions bot commented Sep 2, 2025

✅ Documentation built:
👉 Download
Note: You must be logged in to github and a DASDAE member to access the link.

Copy link
Contributor

@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: 1

♻️ Duplicate comments (3)
dascore/transform/fourier.py (3)

376-376: Use the callable decorator form for consistency.

Elsewhere in this module decorators are used as @patch_function(). Do the same here to avoid subtle decorator semantics issues.

-@patch_function
+@patch_function()
 def stft(

448-452: Validate custom window length matches window_samples.

Passing a mismatched ndarray silently changes frequency resolution (zero-padding effect). Fail fast.

     if isinstance(taper_window, ndarray):
-        window = taper_window
+        window = taper_window
+        if len(window) != window_samples:
+            raise PatchError(
+                f"Window length ({len(window)}) must equal window_samples ({window_samples})."
+            )
     else:
         window = get_window(taper_window, window_samples, fftbins=False)

536-539: Expose istft as a Patch method and add typing.

Match dft/idft by using the callable decorator and typing the parameter.

-@patch_function
-def istft(
-    patch,
-):
+@patch_function()
+def istft(
+    patch: PatchType,
+) -> PatchType:
🧹 Nitpick comments (5)
environment.yml (1)

9-9: Consider aligning constraint style for pydantic.

Use >=2.1 rather than >2.1 to avoid excluding the exact 2.1.* series.

-  - pydantic>2.1
+  - pydantic>=2.1
dascore/transform/fourier.py (4)

406-409: Remove undocumented psd mention or implement it.

psd is documented but not in the signature or implementation.

-    psd
-        If True, return the power spectral density (PSD).

Or add psd: bool = False and implement PSD scaling.


504-520: Return tuple ok; prefer tuple for dims and clarify variable.

get_coord_manager accepts tuple dims; return a tuple for immutability and readability.

-    new_dims = list(dims)
+    new_dims = list(dims)
@@
-    return get_coord_manager(coords=coord_map, dims=new_dims), time
+    return get_coord_manager(coords=coord_map, dims=tuple(new_dims)), time

559-567: Clearer error when inversion isn’t possible.

Include reason (detrended vs non-invertible window/fft_mode) to guide users.

-    if detrended or not stft.invertible:
-        msg = f"Inverse stft not possible for patch {patch}."
+    if detrended or not stft.invertible:
+        why = "detrended" if detrended else "non-invertible STFT settings"
+        msg = f"Inverse STFT not possible ({why})."
         raise PatchError(msg)

569-577: Trim axis selection is subtle—looks correct; add a brief comment.

Slicing along frequency_axis is correct because that axis becomes the time axis after ISTFT. Add a one-liner to prevent future regressions.

-    index = broadcast_for_index(
+    # Note: after ISTFT, `frequency_axis` now indexes the restored time axis.
+    index = broadcast_for_index(
         data_untrimmed.ndim, frequency_axis, slice(0, len(coord))
     )
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e305681 and 622acf4.

📒 Files selected for processing (3)
  • dascore/transform/fourier.py (4 hunks)
  • docs/tutorial/transformations.qmd (1 hunks)
  • environment.yml (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/tutorial/transformations.qmd
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: d-chambers
PR: DASDAE/dascore#523
File: dascore/transform/fourier.py:433-434
Timestamp: 2025-09-02T16:22:22.829Z
Learning: In dascore STFT implementation, use `require_evenly_sampled=True` on `get_coord` calls instead of manual validation for coord.step to ensure sampling rate calculations are safe.
📚 Learning: 2025-09-02T16:22:22.829Z
Learnt from: d-chambers
PR: DASDAE/dascore#523
File: dascore/transform/fourier.py:433-434
Timestamp: 2025-09-02T16:22:22.829Z
Learning: In dascore STFT implementation, use `require_evenly_sampled=True` on `get_coord` calls instead of manual validation for coord.step to ensure sampling rate calculations are safe.

Applied to files:

  • dascore/transform/fourier.py
🧬 Code graph analysis (1)
dascore/transform/fourier.py (6)
dascore/core/coordmanager.py (7)
  • get_coord_manager (1036-1124)
  • get_coord (990-1009)
  • update (230-286)
  • min (1011-1013)
  • step (1019-1021)
  • new (431-449)
  • shape (744-750)
dascore/proc/coords.py (1)
  • get_coord (87-139)
dascore/exceptions.py (1)
  • PatchError (30-31)
dascore/utils/misc.py (2)
  • broadcast_for_index (136-157)
  • iterate (256-268)
dascore/utils/patch.py (3)
  • _get_data_units_from_dims (671-683)
  • _get_dx_or_spacing_and_axes (686-723)
  • get_dim_axis_value (553-637)
dascore/utils/time.py (3)
  • is_datetime64 (405-407)
  • is_timedelta64 (410-412)
  • to_float (327-333)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (18)
  • GitHub Check: test_code_min_deps (windows-latest, 3.13)
  • GitHub Check: test_code_min_deps (windows-latest, 3.12)
  • GitHub Check: test_code_min_deps (macos-latest, 3.13)
  • GitHub Check: test_code_min_deps (ubuntu-latest, 3.13)
  • GitHub Check: test_code_min_deps (ubuntu-latest, 3.12)
  • GitHub Check: test_code (windows-latest, 3.12)
  • GitHub Check: test_code (ubuntu-latest, 3.13)
  • GitHub Check: test_code (macos-latest, 3.10)
  • GitHub Check: test_code (windows-latest, 3.10)
  • GitHub Check: test_code (macos-latest, 3.13)
  • GitHub Check: test_code (macos-latest, 3.11)
  • GitHub Check: test_code (windows-latest, 3.13)
  • GitHub Check: test_code (windows-latest, 3.11)
  • GitHub Check: test_code (ubuntu-latest, 3.11)
  • GitHub Check: test_code (ubuntu-latest, 3.12)
  • GitHub Check: test_code (macos-latest, 3.12)
  • GitHub Check: test_code (ubuntu-latest, 3.10)
  • GitHub Check: test_build_docs
🔇 Additional comments (5)
environment.yml (1)

6-7: SciPy version bounds are consistent
Verified that environment.yml (scipy>=1.15.0) and pyproject.toml (scipy>=1.15) use the same lower bound; no CI overrides detected.

dascore/transform/fourier.py (4)

441-447: Good: robust dim/axis resolution and even-sampling check.

Using get_dim_axis_value + require_evenly_sampled=True prevents invalid sampling-rate calculations.


460-468: Double-check fft_mode for complex data paths.

"onesided" for real and "centered" otherwise is fine for SciPy≥1.15. Ensure tests cover complex-valued inputs.

You likely already have tests; if not, add one complex-valued case asserting round-trip with istft.


472-486: Retain required ISTFT metadata; units scaling looks correct.

Storing hop, fs, mfft, and fft_mode enables faithful reconstruction; data_units via mul mirrors DFT behavior.


579-583: Units restoration uses dynamic time dim—good.

Using dim = patch.dims[time_axis] avoids hard-coding "time" and keeps units correct across custom dims.

Copy link
Contributor

@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: 0

♻️ Duplicate comments (3)
dascore/transform/fourier.py (3)

376-377: Decorator must be called: use @patch_function().

This matches other transform APIs and avoids decorator-factory surprises.

-@patch_function
+@patch_function()
 def stft(

449-453: Validate custom window length equals window_samples.

Mismatched ndarray windows can error or yield incorrect transforms.

 if isinstance(taper_window, ndarray):
-    window = taper_window
+    window = taper_window
+    if len(window) != window_samples:
+        raise PatchError(
+            f"Window length ({len(window)}) must equal window_samples ({window_samples})."
+        )
 else:
     window = get_window(taper_window, window_samples, fftbins=False)

505-521: Fix: _get_istft_coord returns a (None, coord) tuple; must unwrap Coord.

This breaks coord.step/len in istft; also keep names explicit.

-    coord_map = dict(coords.coord_map)
-    coord_map.pop("_stft_window")
-    time = coord_map.pop("_stft_old_coord")
-    coord_map.pop(coords.dims[frequency_axis])
-    coord_map[dims[time_axis]] = time
+    coord_map = dict(coords.coord_map)
+    # Remove stored window; unwrap and restore original time coord.
+    coord_map.pop("_stft_window")
+    orig_time_coord = coord_map.pop("_stft_old_coord")[1]
+    freq_name = dims[frequency_axis]
+    time_name = dims[time_axis]
+    coord_map.pop(freq_name)
+    coord_map[time_name] = orig_time_coord
@@
-    new_dims = list(dims)
-    new_dims[frequency_axis] = dims[time_axis]
-    new_dims.pop(time_axis)
-    return get_coord_manager(coords=coord_map, dims=new_dims), time
+    new_dims = list(dims)
+    new_dims[frequency_axis] = time_name
+    new_dims.pop(time_axis)
+    return get_coord_manager(coords=coord_map, dims=new_dims), orig_time_coord
🧹 Nitpick comments (6)
dascore/transform/fourier.py (6)

341-374: Prefer invert_quantity for unit inversion; keep style consistent.

Use the existing helper for clarity.

-    new_units = None
-    if coord.units is not None:
-        new_units = 1 / dc.get_quantity(coord.units)
+    new_units = invert_quantity(coord.units) if coord.units is not None else None

406-408: Doc mismatch: remove undocumented psd parameter.

psd isn’t in the signature or code; drop from docs (or implement).

-    psd
-        If True, return the power spectral density (PSD).

430-436: Doc typos.

“zero pading” → “zero padding”; minor clarity tweaks.

-    - If an array is passed for taper_window that has a different length
-      than specified in kwargs, artificial enriching of frequency resolution
-      (equivalent to zero pading in time domain) can occur.
+    - If an array is passed for `taper_window` with a length different from
+      `window_samples`, it artificially enriches frequency resolution
+      (equivalent to zero padding in the time domain).

446-447: Tiny cleanup: avoid double conversion.

You already converted step to float.

-step = dc.to_float(coord.step)
-sampling_rate = 1 / dc.to_float(step)
+step = dc.to_float(coord.step)
+sampling_rate = 1.0 / step

523-535: Harden reconstruction: validate required attrs/coords exist.

Fail fast with a helpful message if any STFT metadata is missing.

 def _get_short_time_fft(patch) -> ShortTimeFFT:
     """Reconstruct the short time fft from the attrs/coords in patch."""
-    sr = patch.attrs.get("_stft_sampling_rate")
+    sr = patch.attrs.get("_stft_sampling_rate")
+    win_coord = patch.get_coord("_stft_window", require_sorted=False)
+    hop = patch.attrs.get("_stft_hop")
+    fft_mode = patch.attrs.get("_stft_fft_mode")
+    mfft = patch.attrs.get("_stft_mfft")
+    if any(x is None for x in (sr, hop, fft_mode, mfft)) or win_coord is None:
+        raise PatchError("Patch is missing STFT metadata; cannot reconstruct transformer.")
     # Recreate STFFT class based on saved coords/attrs.
     stft = ShortTimeFFT(
-        win=patch.get_coord("_stft_window").values,
-        hop=patch.attrs.get("_stft_hop"),
-        fs=sr,
-        fft_mode=patch.attrs.get("_stft_fft_mode"),
-        mfft=patch.attrs.get("_stft_mfft"),
+        win=win_coord.values,
+        hop=hop,
+        fs=sr,
+        fft_mode=fft_mode,
+        mfft=mfft,
     )
     return stft

537-541: Add typing to istft; minor grammar nit in doc.

Keep parity with other APIs.

-@patch_function()
-def istft(
-    patch,
-):
+@patch_function()
+def istft(patch: PatchType) -> PatchType:
@@
-    Inverse a short-time fourier transform.
+    Invert a short-time fourier transform.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 622acf4 and 37c8e0f.

📒 Files selected for processing (1)
  • dascore/transform/fourier.py (4 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: d-chambers
PR: DASDAE/dascore#523
File: dascore/transform/fourier.py:433-434
Timestamp: 2025-09-02T16:22:22.829Z
Learning: In dascore STFT implementation, use `require_evenly_sampled=True` on `get_coord` calls instead of manual validation for coord.step to ensure sampling rate calculations are safe.
📚 Learning: 2025-09-02T16:22:22.829Z
Learnt from: d-chambers
PR: DASDAE/dascore#523
File: dascore/transform/fourier.py:433-434
Timestamp: 2025-09-02T16:22:22.829Z
Learning: In dascore STFT implementation, use `require_evenly_sampled=True` on `get_coord` calls instead of manual validation for coord.step to ensure sampling rate calculations are safe.

Applied to files:

  • dascore/transform/fourier.py
🧬 Code graph analysis (1)
dascore/transform/fourier.py (7)
dascore/core/patch.py (6)
  • attrs (203-205)
  • coords (208-210)
  • dims (188-190)
  • data (213-215)
  • ndim (193-195)
  • shape (218-220)
dascore/core/attrs.py (4)
  • PatchAttrs (65-294)
  • update (234-248)
  • get (174-179)
  • items (181-183)
dascore/core/coordmanager.py (7)
  • get_coord_manager (1036-1124)
  • get_coord (990-1009)
  • update (230-286)
  • min (1011-1013)
  • new (431-449)
  • ndim (758-760)
  • shape (744-750)
dascore/units.py (2)
  • invert_quantity (175-183)
  • get_quantity (70-94)
dascore/utils/misc.py (2)
  • broadcast_for_index (136-157)
  • iterate (256-268)
dascore/utils/patch.py (1)
  • _get_data_units_from_dims (671-683)
dascore/utils/transformatter.py (1)
  • FourierTransformatter (38-42)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (18)
  • GitHub Check: test_code (windows-latest, 3.13)
  • GitHub Check: test_code (windows-latest, 3.10)
  • GitHub Check: test_code (windows-latest, 3.11)
  • GitHub Check: test_code (windows-latest, 3.12)
  • GitHub Check: test_code (ubuntu-latest, 3.13)
  • GitHub Check: test_code (ubuntu-latest, 3.10)
  • GitHub Check: test_code (macos-latest, 3.11)
  • GitHub Check: test_code (macos-latest, 3.12)
  • GitHub Check: test_code (macos-latest, 3.10)
  • GitHub Check: test_code (ubuntu-latest, 3.11)
  • GitHub Check: test_code (ubuntu-latest, 3.12)
  • GitHub Check: test_code_min_deps (macos-latest, 3.12)
  • GitHub Check: test_code_min_deps (ubuntu-latest, 3.13)
  • GitHub Check: test_code_min_deps (windows-latest, 3.12)
  • GitHub Check: test_code_min_deps (windows-latest, 3.13)
  • GitHub Check: test_code_min_deps (ubuntu-latest, 3.12)
  • GitHub Check: test_code_min_deps (macos-latest, 3.13)
  • GitHub Check: test_build_docs
🔇 Additional comments (9)
dascore/transform/fourier.py (9)

11-13: LGTM: imports and dependency wiring look correct.

Necessary symbols are imported and used; no obvious redundancies other than a minor double to-float call noted separately.

Also applies to: 17-21, 26-29, 33-33, 35-35


160-162: LGTM: See Also updates are coherent.


310-312: LGTM: Cross-references between dft/idft and stft/istft are consistent.


443-447: Nice: require_evenly_sampled=True avoids bad sampling-rate math.

This aligns with prior guidance and prevents None/zero step pitfalls.


461-468: Request: confirm fft_mode values vs pinned SciPy.

With SciPy≥1.15 pinned, "onesided"/"centered" should be fine. If broader support is needed, consider a fallback to "twosided" for complex inputs.


470-487: LGTM: scaling and attrs metadata for inversion are sound.


490-503: LGTM: inversion axes contract and error message are clear.


560-579: LGTM: inverse execution, trimming, and shape assertion are appropriate.


580-584: LGTM: STFT attrs cleanup and unit restoration are correct and dimension-agnostic.

Copy link
Contributor

@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: 0

Caution

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

⚠️ Outside diff range comments (1)
dascore/units.py (1)

175-184: Fix invert_quantity return type and None-handling.

The implementation returns a Quantity but the annotation says Unit | None. It also indirectly assumes None by using isnull on non-scalars. Return a Unit and accept None explicitly.

Apply:

-def invert_quantity(unit: pint.Unit | str) -> pint.Unit | None:
-    """Invert a unit."""
-    # just get magnitude for isnull test to avoid warning of casting
-    # quantity to array.
-    unit_test = unit.magnitude if hasattr(unit, "magnitude") else unit
-    if pd.isnull(unit_test):
-        return None
-    quant = get_quantity(unit)
-    return 1 / quant
+def invert_quantity(unit: Unit | Quantity | str | None) -> Unit | None:
+    """Invert a unit."""
+    quant = get_quantity(unit)
+    if quant is None:
+        return None
+    # 1/x yields a Quantity; return its units to match the signature.
+    return (1 / quant).units
♻️ Duplicate comments (3)
dascore/transform/fourier.py (3)

373-381: Use @patch_function() (callable form) for stft.

Other transforms use the callable form; this keeps decorator semantics consistent.

-@patch_function
+@patch_function()
 def stft(

444-451: Validate custom window length equals window_samples.

Mismatched ndarray windows will cause incorrect STFT geometry or errors later; fail fast.

     if isinstance(taper_window, ndarray):
-        window = taper_window
+        window = taper_window
+        if len(window) != window_samples:
+            raise PatchError(
+                f"Window length ({len(window)}) must equal window_samples ({window_samples})."
+            )

500-516: _get_istft_coord returns tuple instead of Coord; breaks istft.

You’re returning (None, coord) and assigning it back as-is, which propagates a tuple and causes coord.step access to fail. Unwrap and reassign with the correct key; also return the Coord.

-    coord_map.pop("_stft_window")
-    time = coord_map.pop("_stft_old_coord")
-    coord_map.pop(coords.dims[frequency_axis])
-    coord_map[dims[time_axis]] = time
+    coord_map.pop("_stft_window")
+    # Unwrap the stored original coordinate.
+    orig_time_coord = coord_map.pop("_stft_old_coord")[1]
+    freq_name = dims[frequency_axis]
+    time_name = dims[time_axis]
+    coord_map.pop(freq_name)
+    coord_map[time_name] = orig_time_coord
@@
-    new_dims = list(dims)
-    new_dims[frequency_axis] = dims[time_axis]
-    new_dims.pop(time_axis)
-    return get_coord_manager(coords=coord_map, dims=tuple(new_dims)), time
+    new_dims = list(dims)
+    new_dims[frequency_axis] = time_name
+    new_dims.pop(time_axis)
+    return get_coord_manager(coords=coord_map, dims=tuple(new_dims)), orig_time_coord
🧹 Nitpick comments (2)
dascore/transform/fourier.py (2)

441-443: Use local to_float import and guard against negative step.

Minor consistency and robustness: prefer to_float over dc.to_float; also ensure fs is positive in case of reverse-sorted coords.

-    step = dc.to_float(coord.step)
-    sampling_rate = 1 / step
+    step = to_float(coord.step)
+    sampling_rate = 1 / abs(step)

532-536: Type annotate istft signature and return type.

Align with stft/dft/idft for clarity and tooling.

-@patch_function()
-def istft(
-    patch,
-):
+@patch_function()
+def istft(patch: PatchType) -> PatchType:
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 61f4788 and 3565ec8.

📒 Files selected for processing (2)
  • dascore/transform/fourier.py (4 hunks)
  • dascore/units.py (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: d-chambers
PR: DASDAE/dascore#523
File: dascore/transform/fourier.py:433-434
Timestamp: 2025-09-02T16:22:22.829Z
Learning: In dascore STFT implementation, use `require_evenly_sampled=True` on `get_coord` calls instead of manual validation for coord.step to ensure sampling rate calculations are safe.
📚 Learning: 2025-09-02T16:22:22.829Z
Learnt from: d-chambers
PR: DASDAE/dascore#523
File: dascore/transform/fourier.py:433-434
Timestamp: 2025-09-02T16:22:22.829Z
Learning: In dascore STFT implementation, use `require_evenly_sampled=True` on `get_coord` calls instead of manual validation for coord.step to ensure sampling rate calculations are safe.

Applied to files:

  • dascore/transform/fourier.py
🧬 Code graph analysis (1)
dascore/transform/fourier.py (6)
dascore/core/attrs.py (5)
  • PatchAttrs (65-294)
  • update (234-248)
  • drop (250-255)
  • get (174-179)
  • items (181-183)
dascore/core/coordmanager.py (8)
  • get_coord_manager (1036-1124)
  • get_coord (990-1009)
  • update (230-286)
  • min (1011-1013)
  • step (1019-1021)
  • new (431-449)
  • ndim (758-760)
  • shape (744-750)
dascore/proc/coords.py (1)
  • get_coord (87-139)
dascore/core/coords.py (11)
  • get_coord (1475-1663)
  • index (698-717)
  • update (738-751)
  • update (925-927)
  • values (942-946)
  • values (1201-1216)
  • min (438-440)
  • get_sample_count (753-789)
  • data (606-608)
  • new (592-603)
  • ndim (468-470)
dascore/utils/misc.py (2)
  • broadcast_for_index (136-157)
  • iterate (256-268)
dascore/utils/patch.py (2)
  • _get_data_units_from_dims (671-683)
  • get_dim_axis_value (553-637)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (18)
  • GitHub Check: test_code_min_deps (windows-latest, 3.12)
  • GitHub Check: test_code_min_deps (windows-latest, 3.13)
  • GitHub Check: test_build_docs
  • GitHub Check: test_code_min_deps (ubuntu-latest, 3.13)
  • GitHub Check: test_code_min_deps (ubuntu-latest, 3.12)
  • GitHub Check: test_code_min_deps (macos-latest, 3.13)
  • GitHub Check: test_code (ubuntu-latest, 3.13)
  • GitHub Check: test_code (windows-latest, 3.11)
  • GitHub Check: test_code (windows-latest, 3.12)
  • GitHub Check: test_code (macos-latest, 3.11)
  • GitHub Check: test_code (macos-latest, 3.12)
  • GitHub Check: test_code (ubuntu-latest, 3.12)
  • GitHub Check: test_code (windows-latest, 3.10)
  • GitHub Check: test_code (windows-latest, 3.13)
  • GitHub Check: test_code (macos-latest, 3.10)
  • GitHub Check: test_code (macos-latest, 3.13)
  • GitHub Check: test_code (ubuntu-latest, 3.11)
  • GitHub Check: test_code (ubuntu-latest, 3.10)
🔇 Additional comments (2)
dascore/transform/fourier.py (2)

465-467: Scaling by step matches DFT conventions.

Good job keeping STFT amplitude consistent with DFT scaling.


470-481: STFT metadata is comprehensive and inversion-friendly.

Attrs capture all needed inversion params and dims; looks solid.

@d-chambers d-chambers merged commit 47f83cc into master Sep 3, 2025
22 checks passed
@d-chambers d-chambers deleted the stft branch September 3, 2025 12:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation patch related to Patch class

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants