Skip to content

feat(evals): add alternative loop classification scores when classifying altlocs into different types - #338

Open
k-chrispens wants to merge 1 commit into
mainfrom
kmc/altloc-loop-scorers
Open

feat(evals): add alternative loop classification scores when classifying altlocs into different types#338
k-chrispens wants to merge 1 commit into
mainfrom
kmc/altloc-loop-scorers

Conversation

@k-chrispens

@k-chrispens k-chrispens commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Added configurable loop scoring with LDDT and RMSD options.
    • Added command-line controls for selecting the scoring metric and threshold.
    • Classification results now report the selected metric, overall score, and per-pair scores.
  • Documentation

    • Updated guidance to describe RMSD scoring and the generalized altloc pairing approach.
  • Bug Fixes

    • Improved handling of redundant selections and prevented indeterminate classifications when no valid scores are available.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The altloc classifier now supports lDDT and RMSD loop scoring through a shared SpanScorer abstraction. CLI options, thresholds, output columns, pair-score serialization, and redundant-selection detection were updated accordingly.

Changes

Altloc loop scoring

Layer / File(s) Summary
Scorer strategies and output contract
scripts/eval/classify_altloc_regions.py
Adds metric-independent scoring definitions for lDDT and RMSD, with generic score and pair-score output fields.
Pair scoring and loop classification
scripts/eval/classify_altloc_regions.py
Computes per-pair scores, selects the worst finite score, and assigns small_loop or large_loop using the selected scorer.
Structure processing and CLI wiring
scripts/eval/classify_altloc_regions.py
Adds metric and threshold CLI options, derives scorer-specific defaults, threads them through processing, and detects redundant full-altloc selections by residue coverage.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant main
  participant _process_structure
  participant _classify_selection
  participant SpanScorer
  CLI->>main: provide score metric and threshold
  main->>_process_structure: pass scorer configuration
  _process_structure->>_classify_selection: classify each selection
  _classify_selection->>SpanScorer: compute altloc-pair scores
  SpanScorer-->>_classify_selection: return reduced scores
  _classify_selection-->>_process_structure: return classification row
Loading

Possibly related PRs

Suggested reviewers: mag-astera

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change: adding configurable loop classification scoring for altlocs.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch kmc/altloc-loop-scorers

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.

@k-chrispens k-chrispens changed the title feat: add alternative loop classification scores when classifying altlocs into different types feat(evals): add alternative loop classification scores when classifying altlocs into different types Jul 22, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds support for more robust altloc handling in eval workflows by (1) canonicalizing mixed modified/canonical residues so reference altloc ensembles can be stacked, and (2) generalizing altloc-loop classification to support alternative scoring metrics (e.g., RMSD in addition to lDDT).

Changes:

  • Add canonicalize_mixed_altloc_residues() (plus _closest_canonical_amino_acid()) and apply it in get_reference_atomarraystack() to make mixed-altloc PTM positions stackable.
  • Extend classify_altloc_regions.py with a scorer abstraction and new CLI flags to select loop scoring metric/threshold.
  • Add unit + end-to-end regression tests covering mixed-altloc canonicalization and stacking for a real structure case (6NI6).

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
tests/eval/test_structure_utils.py Adds regression and unit tests for mixed-altloc canonicalization and successful stacking into an AtomArrayStack.
src/sampleworks/eval/structure_utils.py Introduces canonicalization logic for mixed modified/canonical altloc residues and integrates it into reference structure loading.
scripts/eval/classify_altloc_regions.py Refactors loop scoring into a pluggable strategy (lDDT/RMSD) and updates output schema + CLI options accordingly.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +45 to 47
`Altloc pairing`: when > 2 altlocs are present, the scores above are
computed for every combination of altloc pairs and the span is
classified by the *minimum* score over pair combinations.
Comment on lines +230 to 233
3. Else compute the per residue metric for every altloc pair over
the backbone altloc residues in the span and take the minimum
pair mean. Compare against ``loop_lddt_threshold``, if it is above is is classified as
pair mean. Compare against ``loop_score_threshold``, if it is above is is classified as
``small_loop``. If it is below, it is classified as ``large_loop``.
Comment on lines +500 to +508
parser.add_argument(
"--loop-score-threshold",
type=float,
default=None,
help=(
"Threshold for the selected loop scoring strategy. Defaults to the scorer specific"
"threshold when omitted."
),
)
``res_id`` rather than an altloc. That path still needs the CIF-level
:func:`~sampleworks.utils.cif_utils.resolve_mixed_hetatm_atom_altlocs`.

That CIF-level function is not simply reused here because has a different tolerance for losing
Introduce a SpanScorer strategy with --loop-score-metric and
--loop-score-threshold in classify_altloc_regions.py.
@k-chrispens
k-chrispens force-pushed the kmc/altloc-loop-scorers branch from 23eeb52 to c1735f7 Compare July 23, 2026 02:31

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
scripts/eval/classify_altloc_regions.py (2)

36-51: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Stale LDDT-only "minimum"/"mean" language in two docstrings after generalizing to a pluggable scorer. Both descriptions predate the SpanScorer abstraction and don't hold for the rmsd strategy, whose reducers are max, not "minimum"/"mean".

  • scripts/eval/classify_altloc_regions.py#L36-L51: reword the "Altloc pairing" paragraph to say the span is classified by the scorer's "worse" pair score rather than unconditionally "the minimum score".
  • scripts/eval/classify_altloc_regions.py#L222-L242: reword "take the minimum pair mean" to reference the scorer's generic reducer/worse-score functions, and separately fix the "if it is above is is classified as" typo.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/eval/classify_altloc_regions.py` around lines 36 - 51, Update
scripts/eval/classify_altloc_regions.py lines 36-51 to describe altloc pairing
using the configured scorer’s worse pair score instead of an unconditional
minimum. At lines 222-242, revise the docstring to reference the scorer’s
generic reducer/worse-score behavior rather than “minimum pair mean,” and
correct the “if it is above is is classified as” typo.

130-150: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add NumPy-style docstrings to SpanScorer and _score_pair_with_scorer.

Both docstrings are one-liners with no Attributes/Parameters/Returns sections. SpanScorer has 7 fields (metric_name, metric_compute, residue_scores_key, pair_score_reducer, worse_pair_score, is_small_loop_score, default_threshold) that aren't documented, and _score_pair_with_scorer documents neither its parameters nor its return semantics (e.g. what nan signifies).

As per coding guidelines, "Add NumPy-style docstrings to every function and class."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/eval/classify_altloc_regions.py` around lines 130 - 150, Expand the
docstrings for SpanScorer and _score_pair_with_scorer using NumPy style
sections. Document all seven SpanScorer fields under Attributes, and document
each function parameter plus the returned float, including the meaning of nan,
under Parameters and Returns.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/eval/classify_altloc_regions.py`:
- Around line 500-508: Fix the help text in the --loop-score-threshold parser
argument so the adjacent string literals include a space between “specific” and
“threshold”, producing the intended scorer-specific threshold message.
- Around line 335-349: Update the callers of _classify_selection, especially the
selection loop in _process_structure and the row loop in main(), to catch its
RuntimeError for spans with no finite pair scores, log the failure using the
existing error-handling pattern, and skip that selection or row while continuing
the batch. Preserve already accumulated rows and the final CSV write, and avoid
letting this expected classification failure abort the entire run.

---

Nitpick comments:
In `@scripts/eval/classify_altloc_regions.py`:
- Around line 36-51: Update scripts/eval/classify_altloc_regions.py lines 36-51
to describe altloc pairing using the configured scorer’s worse pair score
instead of an unconditional minimum. At lines 222-242, revise the docstring to
reference the scorer’s generic reducer/worse-score behavior rather than “minimum
pair mean,” and correct the “if it is above is is classified as” typo.
- Around line 130-150: Expand the docstrings for SpanScorer and
_score_pair_with_scorer using NumPy style sections. Document all seven
SpanScorer fields under Attributes, and document each function parameter plus
the returned float, including the meaning of nan, under Parameters and Returns.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5e2e30bf-1930-4d37-a8d2-7ab93da2048c

📥 Commits

Reviewing files that changed from the base of the PR and between 58c2311 and c1735f7.

📒 Files selected for processing (1)
  • scripts/eval/classify_altloc_regions.py

Comment on lines +335 to 349
finite_vals = [v for v in pair_scores.values() if np.isfinite(v)]
if not finite_vals:
raise RuntimeError(
f"[{protein}] could not compute lDDT for any altloc pair in span "
f"'{selection_str}' (backbone-altloc residues: {backbone_altloc_res_ids}). "
f"[{protein}] could not compute {scorer.metric_name} for any altloc pair "
f"in span '{selection_str}' "
f"(backbone-altloc residues: {backbone_altloc_res_ids}). "
"Refusing to emit an indeterminate classification."
)

worst = float(min(finite_vals))
row["worst_pair_mean_backbone_lddt"] = worst
row["classification"] = "small_loop" if worst > loop_lddt_threshold else "large_loop"
worst = float(scorer.worse_pair_score(finite_vals))
row["score"] = worst
row["classification"] = (
"small_loop" if scorer.is_small_loop_score(worst, loop_score_threshold) else "large_loop"
)
return row, covered_altloc_residues

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Uncaught RuntimeError can abort the entire batch run and lose all accumulated results.

When no finite pair scores exist for a span, _classify_selection raises RuntimeError. Neither _process_structure's selection loop (lines 404-419) nor main()'s row loop (lines 452-462) catches this. Since all_rows is only converted to a DataFrame and written to CSV after the entire input loop finishes (lines 464-466), a single problematic span in any structure will crash the whole script and discard every previously classified row — a severe regression for long batch runs over many structures/rows.

Other failure modes in this same function (bad selection syntax, metric compute exceptions, missing residue keys) are handled by logging and returning None/nan so processing continues; this new path should follow the same pattern at the call site.

🛡️ Proposed fix: catch and skip instead of aborting the batch
         out = _classify_selection(
             atom_array=atom_array,
             pair_arrays=pair_arrays,
             altloc_ids=altloc_info.altloc_ids,
             selection_str=selection_str,
             protein=protein,
             structure_altloc_mask=structure_altloc_mask,
             structure_backbone_mask=structure_backbone_mask,
             domain_shift_min_span=domain_shift_min_span,
             scorer=scoring_strategy,
             loop_score_threshold=loop_score_threshold,
-        )
+        )
+    except RuntimeError as e:
+        logger.error(f"[{protein}] skipping selection '{selection_str}': {e}")
+        continue
         if out is None:
             continue
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/eval/classify_altloc_regions.py` around lines 335 - 349, Update the
callers of _classify_selection, especially the selection loop in
_process_structure and the row loop in main(), to catch its RuntimeError for
spans with no finite pair scores, log the failure using the existing
error-handling pattern, and skip that selection or row while continuing the
batch. Preserve already accumulated rows and the final CSV write, and avoid
letting this expected classification failure abort the entire run.

Comment on lines +500 to +508
parser.add_argument(
"--loop-score-threshold",
type=float,
default=None,
help=(
"Threshold for the selected loop scoring strategy. Defaults to the scorer specific"
"threshold when omitted."
),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Missing space in help text produces "specificthreshold".

The two adjacent string literals concatenate without a space between "specific" and "threshold".

✏️ Proposed fix
         help=(
-            "Threshold for the selected loop scoring strategy. Defaults to the scorer specific"
-            "threshold when omitted."
+            "Threshold for the selected loop scoring strategy. Defaults to the scorer-specific "
+            "threshold when omitted."
         ),
📝 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
parser.add_argument(
"--loop-score-threshold",
type=float,
default=None,
help=(
"Threshold for the selected loop scoring strategy. Defaults to the scorer specific"
"threshold when omitted."
),
)
parser.add_argument(
"--loop-score-threshold",
type=float,
default=None,
help=(
"Threshold for the selected loop scoring strategy. Defaults to the scorer-specific "
"threshold when omitted."
),
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/eval/classify_altloc_regions.py` around lines 500 - 508, Fix the help
text in the --loop-score-threshold parser argument so the adjacent string
literals include a space between “specific” and “threshold”, producing the
intended scorer-specific threshold message.

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.

2 participants