Skip to content

Record leiden/louvain modularity in adata.uns#648

Open
jpintar wants to merge 4 commits intoscverse:mainfrom
jpintar:record_modularity_in_uns
Open

Record leiden/louvain modularity in adata.uns#648
jpintar wants to merge 4 commits intoscverse:mainfrom
jpintar:record_modularity_in_uns

Conversation

@jpintar
Copy link
Copy Markdown
Contributor

@jpintar jpintar commented May 3, 2026

As of scverse/scanpy@9aaf126, Scanpy records the final modularity attained by sc.tl.leiden in adata.uns. This pull-request would add the same feature to rsc.tl.leiden and rsc.tl.louvain.

Additionally, for better consistency with Scanpy, which only allows a float rather than a list of floats for the resolution parameter, resolution and modularity are recorded as lists in .uns only when a list of resolutions was passed, and as single floats otherwise.

Closes #473.

@Intron7
Copy link
Copy Markdown
Member

Intron7 commented May 4, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 4, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 4, 2026

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Leiden and Louvain clustering now capture and expose modularity values, enabling quality assessment of clustering results across different resolutions.
  • Improvements

    • Resolution parameters in clustering metadata are now consistently formatted whether using single or multiple resolutions.

Walkthrough

Leiden and Louvain clustering functions now capture and record the modularity values returned by their underlying graph algorithms, storing them in adata.uns alongside adjusted resolution parameter formatting (scalar for single resolution, list for multiple).

Changes

Modularity Capture and Persistence

Layer / File(s) Summary
Leiden Modularity Capture
src/rapids_singlecell/tools/_clustering.py (lines 229–246)
Initialize modularities = [] before the resolution loop; append each modularity value returned by culeiden() after clustering per resolution.
Louvain Modularity Capture
src/rapids_singlecell/tools/_clustering.py (lines 391–403)
Initialize modularities = [] before the resolution loop; append each modularity value returned by culouvain() after clustering per resolution.
Leiden Metadata Persistence
src/rapids_singlecell/tools/_clustering.py (lines 275–281)
Store captured modularity in adata.uns[key_added]["modularity"] (scalar if single resolution, list if multiple); update adata.uns[key_added]["params"]["resolution"] format to match.
Louvain Metadata Persistence
src/rapids_singlecell/tools/_clustering.py (lines 432–438)
Store captured modularity in adata.uns[key_added]["modularity"] (scalar if single resolution, list if multiple); update adata.uns[key_added]["params"]["resolution"] format to match.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main change: recording modularity values from leiden/louvain clustering into adata.uns.
Description check ✅ Passed The description clearly explains the changes, references the Scanpy inspiration, mentions consistency improvements, and correctly cites the related issue #473.
Linked Issues check ✅ Passed The PR fully addresses issue #473 requirements: captures modularity from leiden/louvain, stores it in adata.uns, handles both single and multiple resolutions appropriately, and maintains Scanpy consistency.
Out of Scope Changes check ✅ Passed All changes in the clustering module are directly scoped to the stated objectives of recording and persisting modularity values for leiden and louvain algorithms.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with 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.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

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

Copy link
Copy Markdown

@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.

🧹 Nitpick comments (1)
src/rapids_singlecell/tools/_clustering.py (1)

279-281: ⚡ Quick win

Add adata.uns assertions to the existing parametrised clustering test.

The current test at tests/test_clustering.py:73-83 only checks that the correct adata.obs columns exist. It does not verify the new adata.uns[key_added]["modularity"] and the updated adata.uns[key_added]["params"]["resolution"] shape (scalar for a single resolution, list for multiple). Since these are the core deliverables of this PR, a missing assertion means a regression here would go undetected.

✅ Suggested additions to the existing test
 def test_clustering_resolution(adata_neighbors, clustering_function, resolution):
     adata = adata_neighbors.copy()
     clustering_function(adata, key_added="test_clustering", resolution=resolution)
     if isinstance(resolution, list):
         for r in resolution:
             assert f"test_clustering_{r}" in adata.obs.columns
+        # modularity and resolution should both be lists
+        assert isinstance(adata.uns["test_clustering"]["modularity"], list)
+        assert len(adata.uns["test_clustering"]["modularity"]) == len(resolution)
+        assert adata.uns["test_clustering"]["params"]["resolution"] == resolution
     else:
         assert "test_clustering" in adata.obs.columns
+        # modularity and resolution should both be scalars
+        assert isinstance(adata.uns["test_clustering"]["modularity"], float)
+        assert adata.uns["test_clustering"]["params"]["resolution"] == resolution
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/rapids_singlecell/tools/_clustering.py` around lines 279 - 281, Add
assertions in the parametrised clustering test to validate adata.uns entries
created by the clustering function: assert that
adata.uns[key_added]["modularity"] exists and equals modularities[0] when a
single resolution was passed and equals the full modularities list when multiple
resolutions were passed, and assert that
adata.uns[key_added]["params"]["resolution"] is a scalar (single value) for
single-resolution runs and a list (same length as resolutions) for
multi-resolution runs; locate symbols key_added, modularities and adata.uns in
the clustering function (e.g., where adata.uns[key_added]["modularity"] and
adata.uns[key_added]["params"]["resolution"] are set) and add matching checks to
the test to cover both single and multiple resolution parametrizations.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/rapids_singlecell/tools/_clustering.py`:
- Around line 279-281: Add assertions in the parametrised clustering test to
validate adata.uns entries created by the clustering function: assert that
adata.uns[key_added]["modularity"] exists and equals modularities[0] when a
single resolution was passed and equals the full modularities list when multiple
resolutions were passed, and assert that
adata.uns[key_added]["params"]["resolution"] is a scalar (single value) for
single-resolution runs and a list (same length as resolutions) for
multi-resolution runs; locate symbols key_added, modularities and adata.uns in
the clustering function (e.g., where adata.uns[key_added]["modularity"] and
adata.uns[key_added]["params"]["resolution"] are set) and add matching checks to
the test to cover both single and multiple resolution parametrizations.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4e8504fb-e185-4689-be19-4872e5682c6c

📥 Commits

Reviewing files that changed from the base of the PR and between 64e4bdc and a866e4e.

📒 Files selected for processing (1)
  • src/rapids_singlecell/tools/_clustering.py

@Intron7
Copy link
Copy Markdown
Member

Intron7 commented May 4, 2026

Adding to @coderabbitai's note:

Also worth checking: a release-notes entry in docs/release-notes/blank.md for both the new adata.uns[key]["modularity"] field and the resolution storage shape change (always-list → scalar-when-single, list-when-multiple — the latter is a small breaking change to user code that destructures params["resolution"]).

Skip this if the PR isn't flagged for the next dev release — no release note needed in that case.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 4, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find and read the release notes blank file
fd -i "blank.md" docs/release-notes/ --exec cat {}

echo "---SEPARATOR---"

# Check the clustering file for context
fd -i "_clustering.py" src/ --exec grep -n "modularity\|resolution\|uns\[" {}

Length of output: 1992


@Intron7 Good catch — docs/release-notes/blank.md has no entries for this PR yet. Here are two suggested additions:

Under {rubric} Features:

- {func}`~rapids_singlecell.tl.leiden` and {func}`~rapids_singlecell.tl.louvain` now record the final modularity value in `adata.uns[key_added]["modularity"]` (scalar for a single resolution, list for multiple resolutions) {pr}`648`.

Under {rubric} Misc (or {rubric} Removals if considered breaking):

- `adata.uns[key_added]["params"]["resolution"]` is now stored as a scalar `float` when a single resolution is passed, and as a `list` when multiple resolutions are passed. Previously it was always stored as a list. {pr}`648`.

The params["resolution"] shape change is worth highlighting because any user code that unconditionally does adata.uns["leiden"]["params"]["resolution"][0] will break on single-resolution runs — it now gets a float instead of a one-element list.

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented May 4, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.04%. Comparing base (4aeefb6) to head (d5fc3ea).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #648      +/-   ##
==========================================
+ Coverage   88.03%   88.04%   +0.01%     
==========================================
  Files          96       96              
  Lines        7035     7041       +6     
==========================================
+ Hits         6193     6199       +6     
  Misses        842      842              
Files with missing lines Coverage Δ
src/rapids_singlecell/tools/_clustering.py 95.83% <100.00%> (+0.18%) ⬆️

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEA] Record Leiden/Louvain modularity in uns

3 participants