Skip to content

Conversation

@d-chambers
Copy link
Contributor

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

Description

This PR implements the Patch.flip method which just flips the underlying data, and optionally, the coordinates.

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
    • Flip data and associated coordinates along one or more dimensions; patch.flip added with option to flip data only or also update coordinates.
  • Refactor
    • Replaced manual mirroring in dispersion processing with the unified flip capability for cleaner, consistent behavior.
  • Tests
    • Added comprehensive tests for single/multi-dimension flips, associated-coordinate propagation, error cases, idempotency, empty coordinates, and optional coordinate preservation.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 29, 2025

Walkthrough

Adds CoordManager.flip to reverse 1D coordinates (propagating to dependents), a proc-level flip(patch, *dims, flip_coords=True), exposes patch.flip, updates dispersion to use patch.flip("distance"), and adds tests for coord and patch flipping behaviors.

Changes

Cohort / File(s) Summary of Changes
Core coordinate management
dascore/core/coordmanager.py
Added CoordManager.flip(*dims) to reverse 1D coordinates in place, propagate flips to dependent coordinates via dim-to-coord mappings, validate targets (raise CoordError for non-1D), and use broadcast_for_index (imported from dascore.utils.misc). Returns an updated CoordManager via update.
Patch interface
dascore/core/patch.py
Exposed flip on Patch as flip = dascore.proc.flip.
Processing function
dascore/proc/basic.py
Added @patch_function flip(patch, *dims, flip_coords=True) that flips patch data with numpy.flip on specified axes and optionally calls coords.flip(*dims); returns a new patch.
Dispersion transform
dascore/transform/dispersion.py
Replaced manual distance-axis data flip logic with patch.flip("distance") for left-sided dispersion handling.
Tests: CoordManager
tests/test_core/test_coordmanager.py
Added TestFlip test class covering flipping single/multiple dims, propagation to associated coordinates, errors for 2D/nonexistent coords, empty coords, property preservation, double-flip restoration, and flipping all dimensions.
Tests: Proc basic
tests/test_proc/test_basic.py
Added TestFlip test class validating patch.flip behavior (time-only, time+distance, flip_coords=False, data reversal, double-flip), with the suite appearing duplicated.

Suggested labels

proc, patch, ready_for_review

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The pull request description includes the required Description section with a clear summary of the new Patch.flip functionality, but the Checklist section remains entirely unchecked despite tests being added and documentation updates being applicable. This leaves out necessary references to the issue, documentation changes, test confirmation, contributor credit, and the ready_for_review tag. Please update the Checklist by marking the items for including tests and documentation, referencing the related GitHub issue if any, adding docstring or docs changes, crediting yourself in contributors.md, and adding the ready_for_review tag.
Title Check ❓ Inconclusive The title “Patch flip” is a brief two-word phrase that references the new flip functionality but does not clearly describe the main change of implementing a Patch.flip method or its purpose. It is too generic to convey the action being performed and does not follow the convention of a concise, descriptive pull request title. Please rename the title to a clear, concise description of the change, for example “Implement Patch.flip method to reverse data and coordinates,” to accurately summarize the main update.
✅ Passed checks (1 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 96.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch patch_flip

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 29e593b and 7ba16b5.

📒 Files selected for processing (4)
  • dascore/core/coordmanager.py (2 hunks)
  • dascore/proc/basic.py (1 hunks)
  • tests/test_core/test_coordmanager.py (1 hunks)
  • tests/test_proc/test_basic.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/test_core/test_coordmanager.py
🧰 Additional context used
🧬 Code graph analysis (2)
dascore/core/coordmanager.py (2)
dascore/utils/misc.py (1)
  • broadcast_for_index (137-158)
dascore/exceptions.py (1)
  • CoordError (38-39)
tests/test_proc/test_basic.py (4)
dascore/examples.py (1)
  • random_patch (29-111)
dascore/core/coordmanager.py (6)
  • flip (1084-1124)
  • shape (793-799)
  • get_array (1072-1074)
  • get_coord (1039-1058)
  • get_axis (756-771)
  • equals (778-788)
dascore/core/patch.py (4)
  • data (272-283)
  • shape (286-299)
  • dims (212-226)
  • coords (255-269)
dascore/proc/coords.py (3)
  • get_array (151-200)
  • get_coord (96-148)
  • get_axis (787-806)
⏰ 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 (ubuntu-latest, 3.13)
  • GitHub Check: test_code_min_deps (macos-latest, 3.13)
  • GitHub Check: test_code_min_deps (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.13)
  • GitHub Check: test_code (windows-latest, 3.12)
  • GitHub Check: test_code (ubuntu-latest, 3.11)
  • GitHub Check: test_code (macos-latest, 3.10)
  • GitHub Check: test_code (ubuntu-latest, 3.13)
  • GitHub Check: test_code (windows-latest, 3.11)
  • GitHub Check: test_code (macos-latest, 3.12)
  • GitHub Check: test_code (ubuntu-latest, 3.12)
  • GitHub Check: test_code (macos-latest, 3.11)
  • GitHub Check: test_code (windows-latest, 3.10)
  • GitHub Check: Run benchmarks
🔇 Additional comments (3)
dascore/proc/basic.py (1)

758-791: LGTM! Clean implementation of the flip functionality.

The implementation correctly handles:

  • Early return for no-op case when no dimensions are specified
  • Proper axis resolution from dimension names
  • Clean separation between data flipping (using numpy.flip) and coordinate flipping (delegating to coords.flip)
  • Optional coordinate flipping controlled by the flip_coords parameter
dascore/core/coordmanager.py (1)

1119-1122: Ensure associated coordinates maintain proper tuple format.

The code correctly handles the primary coordinate being flipped, but when flipping associated coordinates at line 1122, it directly uses self.get_coord(associated) which returns the raw coord object. This should maintain the tuple format (dims, coord) for consistency with the get_coord_manager input contract.

The current implementation appears correct since it wraps the flipped coord in a tuple at line 1122: out[associated] = (dims, _flip_coord(associated_coord, axis)). However, please verify this handles all edge cases properly.

tests/test_proc/test_basic.py (1)

777-836: Comprehensive test coverage for the flip functionality.

The test suite effectively covers:

  • Single and multiple dimension flipping
  • Coordinate preservation with flip_coords=False
  • Data reversal verification
  • Idempotence (double flip restoration)
  • No-op behavior with empty dimensions

Good job on the thorough test coverage!


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

❤️ Share

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

@codecov
Copy link

codecov bot commented Sep 29, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.92%. Comparing base (23c13a0) to head (7ba16b5).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master     #558   +/-   ##
=======================================
  Coverage   99.92%   99.92%           
=======================================
  Files         127      127           
  Lines       10586    10616   +30     
=======================================
+ Hits        10578    10608   +30     
  Misses          8        8           
Flag Coverage Δ
unittests 99.92% <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.

@coderabbitai coderabbitai bot added patch related to Patch class proc Related to processing module ready_for_review PR is ready for review labels Sep 29, 2025
coderabbitai bot added a commit that referenced this pull request Sep 29, 2025
Docstrings generation was requested by @d-chambers.

* #558 (comment)

The following files were modified:

* `dascore/core/coordmanager.py`
* `dascore/proc/basic.py`
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 29, 2025

Note

Generated docstrings for this pull request at #559

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

🧹 Nitpick comments (8)
dascore/proc/basic.py (1)

758-791: Normalize dims, no-op fast-path, and clearer handling when dims is empty

  • Deduplicate dims to avoid double-flipping a dimension.
  • Fast-return when no dims and flip_coords is False to avoid unnecessary history churn.
  • Guard np.flip when dims is empty (axis=()) to be explicit.
 @patch_function()
 def flip(patch, *dims, flip_coords=True):
@@
-    axes = tuple(patch.get_axis(name) for name in dims)
-    data = np.flip(patch.data, axis=axes)
-    coords = patch.coords
-    if flip_coords:
-        coords = coords.flip(*dims)
-    return patch.new(data=data, coords=coords)
+    # drop duplicate dims, preserve first-seen order
+    dims = tuple(dict.fromkeys(dims))
+    if not dims and not flip_coords:
+        return patch  # no-op
+    axes = tuple(patch.get_axis(name) for name in dims)
+    data = np.flip(patch.data, axis=axes) if dims else patch.data
+    coords = patch.coords.flip(*dims) if flip_coords and dims else patch.coords
+    return patch.new(data=data, coords=coords)
tests/test_proc/test_basic.py (1)

777-813: Augment flip tests to assert data reversal and double-flip identity

Add assertions that data actually reversed, and that double flip restores the original; also verify data reversal when flip_coords=False.

 class TestFlip:
@@
     def test_no_coords(self, random_patch):
         """Ensure coord doesn't change with flip_coords=False"""
         flipped_patch = random_patch.flip("time", flip_coords=False)
         assert flipped_patch.get_coord("time") == random_patch.get_coord("time")
+
+    def test_flip_reverses_data_time(self, random_patch):
+        axis = random_patch.get_axis("time")
+        out = random_patch.flip("time")
+        expected = np.flip(random_patch.data, axis=axis)
+        assert np.array_equal(out.data, expected)
+
+    def test_double_flip_restores(self, random_patch):
+        out = random_patch.flip("time").flip("time")
+        assert random_patch.equals(out)
+
+    def test_flip_coords_false_reverses_data_only(self, random_patch):
+        axis = random_patch.get_axis("time")
+        out = random_patch.flip("time", flip_coords=False)
+        assert np.array_equal(out.data, np.flip(random_patch.data, axis=axis))
+        assert out.get_coord("time") == random_patch.get_coord("time")
dascore/core/coordmanager.py (1)

1098-1120: Nit: avoid shadowing dims variable inside the loop

dims is used as both the method’s argument and a local name for associated coord dims. Rename the local to assoc_dims for clarity.

-                dims = dim_map[associated]
-                axis = dims.index(name)
-                out[associated] = (dims, _flip_coord(out[associated], axis))
+                assoc_dims = dim_map[associated]
+                axis = assoc_dims.index(name)
+                out[associated] = (assoc_dims, _flip_coord(out[associated], axis))
tests/test_core/test_coordmanager.py (5)

1355-1373: Assert immutability (flip should not mutate original CoordManager)

Add a quick identity check to ensure a new instance is returned.

         flipped_cm = cm.flip("time")
 
         # The flipped coordinate should be reversed
         flipped_time = flipped_cm.get_array("time")
         assert np.array_equal(flipped_time, original_time[::-1])
 
         # Other coordinates should remain unchanged
         assert np.array_equal(
             flipped_cm.get_array("distance"), cm.get_array("distance")
         )
 
         # Shape and dimensions should remain the same
         assert flipped_cm.shape == cm.shape
         assert flipped_cm.dims == cm.dims
+        # Should not mutate original object
+        assert flipped_cm is not cm

1393-1412: Also verify 2D associated coord flips along the targeted axis

When flipping distance, 2D coords that include distance (e.g., quality with dims ("time","distance")) should reverse along that axis. Adding this assertion hardens coverage and would catch regressions from implementation refactors.

Note: Impl in dascore/core/coordmanager.py flips associated coords via dim_to_coord_map; a test on quality guards against variable shadowing in that method.

         # Associated coordinates (like latitude) should also be flipped
         flipped_latitude = flipped_cm.get_array("latitude")
         assert np.array_equal(flipped_latitude, original_latitude[::-1])
 
         # Time should remain unchanged
         assert np.array_equal(flipped_cm.get_array("time"), cm.get_array("time"))
+
+        # 2D associated coordinate (quality) should be flipped along distance axis
+        q_before = cm.coord_map["quality"].values
+        q_after = flipped_cm.coord_map["quality"].values
+        assert np.array_equal(q_after, q_before[:, ::-1])

1374-1392: Strengthen multi-dim flip test by asserting 2D coord flips on both axes

This ensures quality reverses on time and distance when both dims are flipped.

         flipped_cm = cm.flip("time", "distance")
 
         # Both coordinates should be reversed
         flipped_time = flipped_cm.get_array("time")
         flipped_distance = flipped_cm.get_array("distance")
 
         assert np.array_equal(flipped_time, original_time[::-1])
         assert np.array_equal(flipped_distance, original_distance[::-1])
 
         # Shape and dimensions should remain the same
         assert flipped_cm.shape == cm.shape
         assert flipped_cm.dims == cm.dims
+
+        # And 2D coord should reverse on both axes
+        q0 = cm.coord_map["quality"].values
+        q1 = flipped_cm.coord_map["quality"].values
+        assert np.array_equal(q1, q0[::-1, ::-1])

1420-1426: Clarify semantics for flipping non‑dim 1D coords (e.g., 'latitude')

Today, flip allows flipping any 1D coord, not just dims. Flipping a non‑dim coord alone will reverse it without touching its associated dim, which changes value‑to‑index alignment. If this is intended, make it explicit with a test; if not, consider raising CoordError for non‑dim inputs.

+    def test_flip_non_dim_coord_semantics(self, cm_multidim):
+        """Flipping a 1D non-dim coord reverses it without altering dims."""
+        lat0 = cm_multidim.get_array("latitude")
+        dist0 = cm_multidim.get_array("distance")
+        out = cm_multidim.flip("latitude")
+        assert np.array_equal(out.get_array("latitude"), lat0[::-1])
+        assert np.array_equal(out.get_array("distance"), dist0)

Alternative: if undesired, assert it raises and adjust implementation accordingly.


1464-1477: Add a companion “flip all dims” test for associated coords (multidim fixture)

The cm_basic case can’t validate associated coords. Consider an extra test using cm_multidim to confirm propagation when flipping all dims.

+    def test_flip_all_dimensions_propagates_to_associated(self, cm_multidim):
+        out = cm_multidim.flip(*cm_multidim.dims)
+        # latitude shares 'distance' -> should reverse
+        lat0 = cm_multidim.get_array("latitude")
+        assert np.array_equal(out.get_array("latitude"), lat0[::-1])
+        # quality shares both -> reverse on both axes
+        q0 = cm_multidim.coord_map["quality"].values
+        q1 = out.coord_map["quality"].values
+        assert np.array_equal(q1, q0[::-1, ::-1])
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 23c13a0 and 29e593b.

📒 Files selected for processing (6)
  • dascore/core/coordmanager.py (2 hunks)
  • dascore/core/patch.py (1 hunks)
  • dascore/proc/basic.py (1 hunks)
  • dascore/transform/dispersion.py (1 hunks)
  • tests/test_core/test_coordmanager.py (1 hunks)
  • tests/test_proc/test_basic.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
tests/test_proc/test_basic.py (5)
tests/test_core/test_coordmanager.py (1)
  • TestFlip (1347-1477)
dascore/core/coordmanager.py (4)
  • flip (1084-1123)
  • shape (793-799)
  • get_array (1072-1074)
  • get_coord (1039-1058)
dascore/proc/basic.py (1)
  • flip (759-790)
dascore/core/patch.py (4)
  • data (272-283)
  • shape (286-299)
  • dims (212-226)
  • coords (255-269)
dascore/proc/coords.py (2)
  • get_array (151-200)
  • get_coord (96-148)
dascore/core/coordmanager.py (3)
dascore/utils/misc.py (1)
  • broadcast_for_index (137-158)
dascore/core/coords.py (3)
  • ndim (468-470)
  • get_coord (1515-1703)
  • index (698-717)
dascore/exceptions.py (1)
  • CoordError (38-39)
dascore/core/patch.py (2)
dascore/core/coordmanager.py (1)
  • flip (1084-1123)
dascore/proc/basic.py (1)
  • flip (759-790)
dascore/proc/basic.py (4)
dascore/utils/patch.py (1)
  • patch_function (180-286)
dascore/core/coordmanager.py (2)
  • flip (1084-1123)
  • get_axis (756-771)
dascore/core/patch.py (3)
  • dims (212-226)
  • data (272-283)
  • coords (255-269)
dascore/proc/coords.py (1)
  • get_axis (787-806)
tests/test_core/test_coordmanager.py (5)
tests/test_proc/test_basic.py (1)
  • TestFlip (777-812)
tests/conftest.py (2)
  • cm_basic (106-108)
  • cm_multidim (129-139)
dascore/core/coordmanager.py (4)
  • get_array (1072-1074)
  • flip (1084-1123)
  • shape (793-799)
  • get_axis (756-771)
dascore/exceptions.py (1)
  • CoordError (38-39)
dascore/core/coords.py (1)
  • empty (679-696)
⏰ 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 (ubuntu-latest, 3.13)
  • GitHub Check: test_code (windows-latest, 3.11)
  • GitHub Check: test_code (ubuntu-latest, 3.10)
  • GitHub Check: test_code_min_deps (windows-latest, 3.12)
  • GitHub Check: test_code (macos-latest, 3.12)
  • GitHub Check: test_code (windows-latest, 3.13)
  • GitHub Check: test_code (ubuntu-latest, 3.12)
  • GitHub Check: test_code (windows-latest, 3.12)
  • GitHub Check: test_code (ubuntu-latest, 3.11)
  • GitHub Check: test_code (windows-latest, 3.10)
  • GitHub Check: test_code (macos-latest, 3.10)
  • GitHub Check: test_code (macos-latest, 3.11)
  • GitHub Check: test_code_min_deps (windows-latest, 3.13)
  • GitHub Check: Run benchmarks
  • GitHub Check: test_code_min_deps (macos-latest, 3.12)
  • 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 (macos-latest, 3.13)
🔇 Additional comments (3)
dascore/core/patch.py (1)

425-425: LGTM: public Patch.flip binding is consistent with other proc bindings

Binding flip alongside pad/roll/where maintains a uniform API surface. No issues.

dascore/transform/dispersion.py (1)

81-81: Doc example improvement: prefer patch.flip over manual reversal

Switching the left-sided example to use patch.flip("distance") improves clarity and reusability. Looks good.

tests/test_core/test_coordmanager.py (1)

1348-1353: Good addition: solid coverage of flip happy paths and errors

The new TestFlip suite meaningfully exercises dim flips, propagation to associated coords, empty coords, and double‑flip invariants. LGTM overall.

Minor follow‑up: do we want to define behavior for duplicate dims passed to flip (e.g., flip("time", "time"))? Current impl will no‑op the duplicate; consider either deduping inputs or raising ParameterError for consistency with transpose tests.

@codspeed-hq
Copy link

codspeed-hq bot commented Sep 29, 2025

CodSpeed Performance Report

Merging #558 will not alter performance

Comparing patch_flip (7ba16b5) with master (23c13a0)

Summary

✅ 50 untouched

@d-chambers d-chambers merged commit 2cb70d6 into master Sep 29, 2025
23 checks passed
@d-chambers d-chambers deleted the patch_flip branch September 29, 2025 16:51
@coderabbitai coderabbitai bot mentioned this pull request Oct 3, 2025
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

patch related to Patch class proc Related to processing module ready_for_review PR is ready for review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants