-
Notifications
You must be signed in to change notification settings - Fork 28
Implement Short time Fourier transform and its inverse #523
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
WalkthroughAdds STFT/ISTFT functions, coords, metadata, and inversion support; exports Changes
Possibly related PRs
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 💡 Knowledge Base configuration:
You can enable these sources in your CodeRabbit configuration. 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
⏰ 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)
✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
Codecov Report✅ All modified and coverable lines are covered by tests. 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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this 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 avoidTypeErroron 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 newdascore/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_dtypeto also acceptdatetime.datetimeanddatetime.timedeltawhen inspecting object-dtype arrays. Note thatis_datetime64/is_timedelta64are invoked across several modules—transform/fourier.py,units.py,utils/chunk.py,core/coords.py—and exercised by tests intest_utils/test_time.pyandtest_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_unitsis 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 styleUse the common alias
import dascore as dcas 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 patchtests/test_transform/test_fourier.py (5)
9-16: Prefer unit constant import over ad-hoc “seconds” aliasImport
secondonce at module scope and use it consistently. This removes the customseconds = 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 = 2And 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 variableUse
patch2for 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 testsReuse 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 istftValidate 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.
📒 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_datetime64can 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_timedelta64can 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 — LGTMBinding
transform.stft/istftonto Patch is consistent with other transform shortcuts and enables the method-style API. No issues spotted.
dascore/transform/fourier.py
Outdated
| window_samples = coord.get_sample_count(val, samples=samples, enforce_lt_coord=True) | ||
| sampling_rate = 1 / dc.to_float(coord.step) |
There was a problem hiding this comment.
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.
| 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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| 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 | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
There was a problem hiding this 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.
📒 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.ndimwarning—Patch.ndimis a defined property and used throughout the codebase.Likely an incorrect or invalid review comment.
There was a problem hiding this 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, notidftagain.- - [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, withshell-initas the default. Keeping this is fine for GitHub-hosted runners; if you move to self-hosted and want stronger isolation, considerenvironmentorall. (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.
📒 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:
Anyfor type hints,ShortTimeFFTfor the core functionality, andget_windowfor window functions.Also applies to: 17-18
20-20: LGTM: Import dascore for internal utilities.The
import dascore as dcprovides access to internal utility functions liketo_timedelta64andget_quantity.
21-21: LGTM: Additional imports for STFT support.All new imports are appropriately used:
ndarrayfor type hintsPatchErrorfor error handlingQuantity,invert_quantity,percentfor unit handlingbroadcast_for_indexfor array manipulationget_dim_axis_valuefor 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_coordsfunction 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_axesfunction properly validates that the required STFT metadata exists before attempting inverse transformation.
485-500: LGTM: Proper coordinate restoration for inverse STFT.The
_get_istft_coordfunction correctly:
- Removes STFT-specific metadata
- Restores the original time coordinate
- Reconstructs the proper dimension ordering
503-552: LGTM: Comprehensive inverse STFT implementation.The
istftfunction 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.
There was a problem hiding this 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.
📒 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.updateis defined and returns a new instance.Likely an incorrect or invalid review comment.
| 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 | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_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.
| 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.
There was a problem hiding this 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,
stftmay not be registered as a Patch method.-@patch_function +@patch_function() def stft(
531-534: Exposeistftas a Patch method.Add the patch decorator so
patch.istft()works.- def istft( +@patch_function() +def istft( patch, ):
499-515: Fix_get_istft_coordto 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 inistftto 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 canonicalsecond.Drop the custom
secondsalias and use the importedsecondfor 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.
📒 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=Trueandget_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.dataproxies to it).
17-19: ShortTimeFFT compatibility requirement satisfied
pyproject.toml already pins SciPy≥1.15, which exceeds the ≥1.12 requirement for ShortTimeFFT.
There was a problem hiding this 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.
📒 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).
|
✅ Documentation built: |
There was a problem hiding this 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 matcheswindow_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/idftby 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.1rather than>2.1to avoid excluding the exact2.1.*series.- - pydantic>2.1 + - pydantic>=2.1dascore/transform/fourier.py (4)
406-409: Remove undocumentedpsdmention or implement it.
psdis documented but not in the signature or implementation.- psd - If True, return the power spectral density (PSD).Or add
psd: bool = Falseand implement PSD scaling.
504-520: Return tuple ok; prefer tuple for dims and clarify variable.
get_coord_manageraccepts 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_axisis 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.
📒 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=Trueprevents invalid sampling-rate calculations.
460-468: Double-checkfft_modefor 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_unitsviamulmirrors 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.
There was a problem hiding this 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.
📒 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.
There was a problem hiding this 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.
📒 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.
Description
This PR implements the stft by simply wrapping the scipy function.
Todos
Patch.spectrogramChecklist
I have (if applicable):
Summary by CodeRabbit
New Features
Documentation
Refactor
Tests
Chores
Deprecated