Skip to content
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

Make skeleton nodes mutable #135

Merged
merged 1 commit into from
Nov 1, 2024
Merged

Make skeleton nodes mutable #135

merged 1 commit into from
Nov 1, 2024

Conversation

talmo
Copy link
Contributor

@talmo talmo commented Nov 1, 2024

This PR introduces an overhaul to how we define equality for skeleton objects to enable skeleton mutability.

Previously, we tested for equality between instances of classes like Skeleton and Node by value. This was convenient so that we could do Node("a") == Node("a") and get True.

We didn't use that pattern often, but we do use Node objects as keys (e.g., in Instance objects), meaning that they have to be hashable. If two objects are equal, then they must have the same hash. This also means that they have to be immutable (see this explanation).

This PR makes Node objects mutable at the cost of making them hashable by ID, e.g.:

# These are two different objects, even though they have the same contents in their attributes
assert Node("a") != Node("a")
assert Node("a").name == Node("a").name

# Mutating the node attribute doesn't change its identity
node = Node("a")
node_id = hash(node)
node.name = "b"
assert hash(node) == node_id

Summary by CodeRabbit

  • New Features

    • Enhanced handling of skeleton symmetries with new properties for symmetry indices and names.
    • Introduced a method for retrieving flipped node indices to aid in data augmentation.
    • Improved error handling to prevent duplicate node additions.
  • Bug Fixes

    • Updated assertions in tests to reflect changes in symmetry handling and node management.
  • Tests

    • Refined test cases for the Skeleton class to improve clarity and robustness, focusing on symmetry and edge management.

Copy link
Contributor

coderabbitai bot commented Nov 1, 2024

Walkthrough

The changes involve significant modifications to the Skeleton model in sleap_io/model/skeleton.py, including updates to class decorators, the introduction of new properties and methods, and refined error handling. The test files have also been adjusted to reflect these changes, focusing on assertions related to symmetry and flipped node indices, enhancing clarity and functionality. Overall, the updates improve the management of nodes and symmetries within the Skeleton class and ensure that the associated tests accurately validate these functionalities.

Changes

File Change Summary
sleap_io/model/skeleton.py - Changed Node class decorator from @define(frozen=True, cache_hash=True) to @define(eq=False).
- Removed flipped_node_inds property; added symmetry_inds and symmetry_names properties.
- Introduced get_flipped_node_inds method.
- Updated add_node to raise ValueError for duplicate names.
- Enhanced add_symmetry for clarity.
tests/io/test_slp.py - Updated assertions to check for string names in skeleton.symmetry_names instead of Node objects.
- Changed assertions to use get_flipped_node_inds() method.
tests/model/test_labels.py - Created skel variable as an instance of Skeleton for better readability.
- Updated new_skel creation to use skel.
- Enhanced assertions in test_labels_numpy and test_labels_clean_* functions for clarity.
tests/model/test_skeleton.py - Renamed test_flipped_node_inds to test_get_flipped_node_inds and updated its logic.
- Modified test_add_node to handle duplicate nodes with ValueError.
- Streamlined test_add_edge to focus on adding edges by node names.
- Updated test_add_symmetry to check symmetry indices and names.

Poem

🐇 In the meadow, changes bloom,
Skeletons dance, dispelling gloom.
Nodes align, symmetries sing,
With each update, new joys they bring.
A hop, a skip, in code we play,
Celebrating progress, hip-hip-hooray! 🌼


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

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

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

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

CodeRabbit Configuration File (.coderabbit.yaml)

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

Documentation and Community

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

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: 2

🧹 Outside diff range and nitpick comments (5)
tests/model/test_skeleton.py (2)

78-89: LGTM! Consider adding docstring for clarity.

The test comprehensively verifies the node flipping behavior with and without symmetries. The assertions properly check both the flipped indices and symmetry object properties.

Add a docstring to explain the test's purpose:

 def test_get_flipped_node_inds():
+    """Test node index flipping behavior with and without symmetries.
+    
+    Verifies that:
+    1. Without symmetries, indices remain unchanged
+    2. With symmetries, paired nodes swap indices
+    3. Symmetry objects maintain correct node relationships
+    """
     skel = Skeleton(["A", "BL", "BR", "C", "DL", "DR"])

Line range hint 1-1: Add dedicated test for node hash stability.

Given that a key objective of this PR is to maintain node hashability while allowing mutation, consider adding a dedicated test function to verify this behavior.

Add this test function:

def test_node_hash_stability():
    """Verify that Node objects maintain stable hash values when mutated."""
    node = Node("test")
    original_hash = hash(node)
    
    # Verify hash remains stable through various mutations
    node.x = 10
    assert hash(node) == original_hash
    
    node.name = "new_name"
    assert hash(node) == original_hash
    
    # Verify nodes with same attributes are distinct
    other_node = Node("test")
    assert hash(other_node) != original_hash
    assert node != other_node
sleap_io/model/skeleton.py (2)

188-208: Fix docstring example to use method name instead of property.

The docstring example incorrectly references skel.flipped_node_inds but should use skel.get_flipped_node_inds() to match the actual method name.

Apply this change to the docstring:

-            >>> skel.flipped_node_inds
+            >>> skel.get_flipped_node_inds()

253-255: Use isinstance() for type checking.

Replace direct type comparisons with isinstance() for more robust type checking.

Apply this change:

-        node_name = node.name if type(node) == Node else node
+        node_name = node.name if isinstance(node, Node) else node
🧰 Tools
🪛 Ruff

253-253: Use is and is not for type comparisons, or isinstance() for isinstance checks

(E721)

tests/io/test_slp.py (1)

Line range hint 1-1: Add test coverage for node mutability behavior

The test file lacks coverage for key behaviors described in the PR objectives:

  1. Verifying that Node("a") != Node("a")
  2. Confirming that mutating a Node's attributes doesn't change its hash value
  3. Testing node attribute access and modification

Consider adding a new test function to verify these behaviors:

def test_node_mutability():
    """Test Node mutability and identity behavior."""
    # Test that nodes with same attributes are different objects
    assert Node("a") != Node("a")
    
    # Test that mutating attributes doesn't change identity
    node = Node("a")
    node_hash = hash(node)
    node.name = "b"
    assert hash(node) == node_hash
    
    # Test attribute access and modification
    node = Node("test")
    assert node.name == "test"
    node.name = "modified"
    assert node.name == "modified"

Would you like me to help implement these test cases or create a GitHub issue to track this task?

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between b15e36e and fade4fa.

📒 Files selected for processing (4)
  • sleap_io/model/skeleton.py (4 hunks)
  • tests/io/test_slp.py (3 hunks)
  • tests/model/test_labels.py (1 hunks)
  • tests/model/test_skeleton.py (3 hunks)
🧰 Additional context used
🪛 Ruff
sleap_io/model/skeleton.py

253-253: Use is and is not for type comparisons, or isinstance() for isinstance checks

(E721)


256-256: Use is and is not for type comparisons, or isinstance() for isinstance checks

(E721)

🔇 Additional comments (9)
sleap_io/model/skeleton.py (4)

14-14: LGTM: Node mutability change aligns with requirements.

The decorator change from @define(frozen=True, cache_hash=True) to @define(eq=False) correctly implements the desired behavior where nodes are mutable and compared by identity rather than value, ensuring Node("a") != Node("a") while allowing attribute modifications.


174-186: LGTM: Well-designed symmetry properties.

The new properties provide a clean interface with:

  • Consistent ordering through sorted indices
  • Proper type hints
  • Clear mapping between indices and names

307-310: LGTM: Clean symmetry handling refactor.

The use of a local symmetry variable improves code readability while maintaining proper node addition behavior.


14-14: Verify node mutability and identity behavior.

Let's confirm that the changes achieve the desired node behavior.

✅ Verification successful

Node mutability and identity behavior confirmed

The verification shows that the changes achieve the desired behavior:

  • Node class is defined with @define(eq=False), ensuring identity-based comparison
  • Nodes are used as dictionary keys in _node_ind_map which relies on identity-based comparison
  • Node instances can be mutated as evidenced by:
    • No frozen=True decorator on the Node class
    • Direct modification of nodes in _convert_nodes method
    • Mutable list of nodes with on_setattr callback
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify that Node instances are used as dictionary keys based on identity
# rather than value, and that they can be mutated.

# Search for dictionary usage with Node keys
rg -l "Dict\[.*Node.*\]|dict.*Node" 

# Search for Node mutation after being used as a key
rg -A 5 "node\.[a-zA-Z_]+ = "

Length of output: 123


Script:

#!/bin/bash
# Let's examine the Node class implementation and its usage more thoroughly

# First, check the Node class definition and its attributes
rg -A 10 "class Node" sleap_io/model/skeleton.py

# Check how Node instances are used in dictionaries
rg -A 5 "Dict\[.*Node" sleap_io/model/skeleton.py sleap_io/model/instance.py

# Look for any dictionary operations with Node keys
rg "dict\(.*node.*\)|{.*node.*}" sleap_io/model/skeleton.py sleap_io/model/instance.py

# Check for any mutation patterns
rg -A 3 "def.*node" sleap_io/model/skeleton.py sleap_io/model/instance.py

Length of output: 2836


Script:

#!/bin/bash
# Let's examine the Node class implementation and its usage in more detail

# Check the complete Node class implementation including any decorators
rg -B 5 -A 15 "@define" sleap_io/model/skeleton.py

# Check for dictionary usage with escaped braces
rg -A 3 "\{.*?node.*?\}" sleap_io/model/skeleton.py

# Look for Node instances in dictionary comprehensions
rg -A 3 "= \{.*?node.*?for" sleap_io/model/skeleton.py

# Check for any instance attribute modifications
rg -A 3 "setattr|__setattr__|\.name\s*=" sleap_io/model/skeleton.py

Length of output: 4261

tests/io/test_slp.py (3)

95-96: LGTM: Robust symmetry verification using node names

The change from comparing Node objects to checking symmetry_names is a good approach that aligns with making nodes mutable while maintaining reliable symmetry verification.


160-163: LGTM: Proper encapsulation of flipped node indices

The change to use get_flipped_node_inds() method instead of directly accessing the property improves encapsulation while maintaining the verification of serialization/deserialization correctness.


230-231: LGTM: Consistent use of new API across multiple skeletons

The test properly verifies flipped node indices for multiple skeletons using the new get_flipped_node_inds() method, maintaining consistency with the updated API.

tests/model/test_labels.py (2)

23-23: LGTM: Good practice to reuse the Skeleton instance.

Creating a single skel instance and reusing it across multiple test cases is a good practice. This approach:

  1. Reduces redundancy in the test code
  2. Ensures consistent testing conditions
  3. Aligns with the new mutable Node design where identity matters more than value equality

30-31: Verify instance creation with mutable nodes.

The test creates both regular and predicted instances using the same skeleton. However, we should verify that the instances maintain correct behavior with mutable nodes.

Let's check if there are any other tests specifically covering node mutability:

Consider adding explicit assertions to verify:

  1. Instances remain valid when their skeleton's nodes are mutated
  2. Different instances using the same skeleton maintain independence
✅ Verification successful

Based on the gathered information, I can now provide the final response:

Node mutability is handled safely by design

The Instance class maintains points in a dictionary with Node objects as keys, and the test suite comprehensively covers Instance initialization and comparison scenarios. The skeleton's nodes are used as reference keys rather than being directly modified, which inherently maintains instance independence when sharing skeletons.

  • The test at tests/model/test_instance.py verifies instance equality semantics and proper point-to-node mapping
  • Instance comparison tests explicitly verify that instances with the same skeleton maintain their independence
  • The Instance class implementation in sleap_io/model/instance.py shows points are stored in a separate dictionary, isolating them from skeleton mutations
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for tests related to node mutability
rg -l "Node.*mutable|mutable.*Node" "tests/"

# Search for node-related test assertions
ast-grep --pattern 'assert$_($_, Node($_))' 

Length of output: 90


Script:

#!/bin/bash
# Let's search for test files related to Instance and Node classes
rg -l "class.*Instance|Instance.*test" tests/

# Look for test cases involving Instance and skeleton/nodes
rg -A 5 "def test.*skeleton|test.*node" tests/

# Search for Instance initialization patterns
ast-grep --pattern 'Instance($$$)'

# Search for test assertions involving Instance objects
ast-grep --pattern 'assert$_($_, Instance($_))'

Length of output: 5790


Script:

#!/bin/bash
# Let's look at Instance test cases to see if node mutability is covered
rg -A 10 "test.*instance" tests/model/test_instance.py

# Check for any mutation-related tests in Instance class
rg -A 5 "def test.*mut|test.*change" tests/model/test_instance.py

# Look at the Instance class implementation to understand node handling
rg -A 10 "class Instance" sleap_io/model/

Length of output: 2596

Comment on lines +114 to +116
with pytest.raises(ValueError):
skel.add_node("B")
assert skel.node_names == ["A", "B", "C"]
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Remove unreachable assertion in error handling test.

The assertion after raise ValueError is unreachable. The test should be restructured to properly verify the error case.

Apply this change:

     with pytest.raises(ValueError):
         skel.add_node("B")
-        assert skel.node_names == ["A", "B", "C"]
+    # Verify state remains unchanged after failed operation
+    assert skel.node_names == ["A", "B", "C"]
📝 Committable suggestion

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

Suggested change
with pytest.raises(ValueError):
skel.add_node("B")
assert skel.node_names == ["A", "B", "C"]
with pytest.raises(ValueError):
skel.add_node("B")
# Verify state remains unchanged after failed operation
assert skel.node_names == ["A", "B", "C"]

Comment on lines +141 to +155
assert skel.symmetry_inds == [(0, 1)]
assert skel.symmetry_names == [("A", "B")]

# Don't duplicate reversed symmetries
skel.add_symmetry("B", "A")
assert skel.symmetries == [Symmetry([Node("A"), Node("B")])]
assert skel.symmetry_inds == [(0, 1)]
assert skel.symmetry_names == [("A", "B")]

# Add new symmetry with new node objects
skel.add_symmetry(Symmetry([Node("C"), Node("D")]))
assert skel.symmetries == [
Symmetry([Node("A"), Node("B")]),
Symmetry([Node("C"), Node("D")]),
]
assert skel.symmetry_inds == [(0, 1), (2, 3)]

# Add new symmetry with node names
skel.add_symmetry("E", "F")
assert skel.symmetries == [
Symmetry([Node("A"), Node("B")]),
Symmetry([Node("C"), Node("D")]),
Symmetry([Node("E"), Node("F")]),
]
assert skel.symmetry_inds == [(0, 1), (2, 3), (4, 5)]
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider adding mutability verification.

The test thoroughly covers symmetry addition scenarios, but given the PR's focus on node mutability, consider adding a test case that verifies nodes remain mutable after being added to symmetries.

Add this test case:

     # Add new symmetry with node names
     skel.add_symmetry("E", "F")
     assert skel.symmetry_inds == [(0, 1), (2, 3), (4, 5)]
+
+    # Verify nodes remain mutable after symmetry addition
+    node = skel["E"]
+    original_hash = hash(node)
+    node.x = 42
+    assert node.x == 42
+    assert hash(node) == original_hash  # Hash should be stable
📝 Committable suggestion

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

Suggested change
assert skel.symmetry_inds == [(0, 1)]
assert skel.symmetry_names == [("A", "B")]
# Don't duplicate reversed symmetries
skel.add_symmetry("B", "A")
assert skel.symmetries == [Symmetry([Node("A"), Node("B")])]
assert skel.symmetry_inds == [(0, 1)]
assert skel.symmetry_names == [("A", "B")]
# Add new symmetry with new node objects
skel.add_symmetry(Symmetry([Node("C"), Node("D")]))
assert skel.symmetries == [
Symmetry([Node("A"), Node("B")]),
Symmetry([Node("C"), Node("D")]),
]
assert skel.symmetry_inds == [(0, 1), (2, 3)]
# Add new symmetry with node names
skel.add_symmetry("E", "F")
assert skel.symmetries == [
Symmetry([Node("A"), Node("B")]),
Symmetry([Node("C"), Node("D")]),
Symmetry([Node("E"), Node("F")]),
]
assert skel.symmetry_inds == [(0, 1), (2, 3), (4, 5)]
assert skel.symmetry_inds == [(0, 1)]
assert skel.symmetry_names == [("A", "B")]
# Don't duplicate reversed symmetries
skel.add_symmetry("B", "A")
assert skel.symmetry_inds == [(0, 1)]
assert skel.symmetry_names == [("A", "B")]
# Add new symmetry with new node objects
skel.add_symmetry(Symmetry([Node("C"), Node("D")]))
assert skel.symmetry_inds == [(0, 1), (2, 3)]
# Add new symmetry with node names
skel.add_symmetry("E", "F")
assert skel.symmetry_inds == [(0, 1), (2, 3), (4, 5)]
# Verify nodes remain mutable after symmetry addition
node = skel["E"]
original_hash = hash(node)
node.x = 42
assert node.x == 42
assert hash(node) == original_hash # Hash should be stable

Copy link

codecov bot commented Nov 1, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 96.13%. Comparing base (b15e36e) to head (fade4fa).
Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #135      +/-   ##
==========================================
- Coverage   96.50%   96.13%   -0.37%     
==========================================
  Files          17       17              
  Lines        2115     2124       +9     
==========================================
+ Hits         2041     2042       +1     
- Misses         74       82       +8     

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

@talmo talmo merged commit 611947e into main Nov 1, 2024
9 checks passed
@talmo talmo deleted the talmo/skeleton-mutation branch November 1, 2024 05:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant