Skip to content

feat(rewards): add reciprocal-space reward (Fprotein from SFC)#272

Open
DorisMai wants to merge 14 commits into
mainfrom
dm/add-sfc-reward
Open

feat(rewards): add reciprocal-space reward (Fprotein from SFC)#272
DorisMai wants to merge 14 commits into
mainfrom
dm/add-sfc-reward

Conversation

@DorisMai

@DorisMai DorisMai commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator

What changed

  • Added StructureFactorRewardFunction (core/rewards/structure_factor.py) via SFC. Two-phase construction (__init__ config + prepare(atom_array)) as SFC requires knowing topology.
    • ensembles combine as a complex sum.: fits default scaled |Ftotal| = |Fprotein| + |Fsolvent|. The solvent part can have different modes: off (Fprotein only), combined (one mask for averaged protein), or one mask per conformation. This requires SFC to be the latest version that fixes the solvent scaling issue.
    • restricted by SFC to occupancy and B-factor shared across batch. For the solvent mask (per-conformation mode), occupancy is effectively required to be exactly 1/E, which is the production result now.
    • exposes normalize_amplitude for testing and batch_partition in case of OOM
  • Synthetic data generation fixes:
    • Patched eval/generate_synthetic_sf.py to generate test data with both Fprotein and Ftotal in the same mtz for debugging/development .
    • Fixed issues for the round trip of cif --> atomarray --> gemmi --> cif --> atomarray in eval/synthetic_utils.py. Tests added in tests/eval/test_generate_synthetic_sf.py.
  • Refactored and added reward tests.
    • Extracted common functions from test_real_space_density_reward.py to test_reward_function_contract.py. Real space specific tests that are unused (e.g. vmap related) remain untouched.
    • Added test_structure_factor_reward.py and synthetic test data (1vme cif and mtz) generated by:
      pixi run -e <env> python -m sampleworks.eval.generate_synthetic_sf \
          --structure tests/resources/1vme/1vme_final.cif \
          --selection "chain A" \
          --occupancy-mode custom --occupancy-values 0.5:0.5 \
          --remove-hydrogens --remove-waters \
          --resolution 1.8 \
          --simulate-solvent-and-scale \
          --save-structure \
          --output tests/resources/1vme/1vme_final_crystalframe_0.5occA_0.5occB_1.80A.mtz
      

Next steps

  • Wire the reward into a guidance step scaler for initial 40 experiment
  • Add scaling optimization

Summary by CodeRabbit

Summary of changes

  • New Features

    • Added reciprocal-space structure-factor reward evaluation (F/E) with configurable bulk-solvent handling (off, combined, per_conformer).
    • Enhanced synthetic MTZ generation to write multiple labeled outputs (including protein-only vs protein+total) in a single file.
  • Bug Fixes

    • Improved MTZ amplitude/sigma column detection with safer overrides.
    • Enforced consistency of occupancy and B-factors across the batch for structure-factor scoring.
    • Improved AtomArray → gemmi conversion for more reliable altloc/annotation preservation.
  • Tests

    • Expanded synthetic MTZ, mmCIF/altloc round-trip, and structure-factor reward coverage (including vmap/contract checks).

@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds reciprocal-space structure-factor reward scoring, shared AtomArray-to-gemmi conversion, multi-label synthetic MTZ output, and expanded reward test coverage for contract, SF-specific, and real-space behavior.

Changes

Structure Factor Reward and Synthetic SF Pipeline

Layer / File(s) Summary
Reward metadata, preparation, and forward pass
pyproject.toml, src/sampleworks/core/rewards/structure_factor.py
Updates the sfcalculator-torch dependency and adds MTZ metadata resolution, configuration, preparation, bulk-solvent handling, and masked reciprocal-space loss computation.
Synthetic MTZ assembly and gemmi conversion
src/sampleworks/eval/generate_synthetic_sf.py, src/sampleworks/eval/synthetic_utils.py
Moves AtomArray-to-gemmi conversion into a shared helper and refactors synthetic SF output to build per-label amplitude, sigma, and phase columns before MTZ writing.

Reward Validation and Tests

Layer / File(s) Summary
Shared reward fixtures and contract tests
tests/conftest.py, tests/rewards/reward_input_helpers.py, tests/rewards/test_reward_function_contract.py, tests/rewards/test_structure_factor_reward.py
Adds shared reward fixtures, batching helpers, reward protocol and loss-shape contracts, and SF-specific tests for column resolution, batch invariants, bulk solvent, and configuration.
Real-space test split and altloc round-trip
tests/rewards/test_real_space_density_reward.py, tests/eval/test_generate_synthetic_sf.py
Removes duplicated reward-agnostic checks from real-space tests, keeps real-space vmap and edge-case coverage, and adds the mmCIF altloc round-trip assertion.

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

Possibly related PRs

  • diff-use/sampleworks#234: Directly related to the synthetic MTZ multi-label column scheme and structure-factor generation path extended here.

Suggested reviewers: marcuscollins, k-chrispens

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.79% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: adding a reciprocal-space reward based on Fprotein from SFC.
✨ 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 dm/add-sfc-reward

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.

@DorisMai DorisMai marked this pull request as ready for review June 30, 2026 01:11

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

🧹 Nitpick comments (2)
src/sampleworks/eval/generate_synthetic_sf.py (1)

137-145: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use a NumPy-style docstring for this new helper.

The summary is useful, but this new function is missing the required Parameters / Returns sections. As per coding guidelines, “Always include NumPy-style docstrings for every function and class.”

Proposed docstring update
-    """Build a one-amplitude rs.DataSet with labelled F / SIGF / PHIF columns.
-
-    ``sfc.prepare_dataset`` returns an amplitude column and a phase column (degrees)
-    for the given ``structure_factor_column`` attribute. We auto-detect those by MTZ
-    dtype (rather than assuming the unexposed ``FMODEL`` / ``PHIFMODEL`` names),
-    rename them to ``F{label}`` / ``PHIF{label}``, and synthesize a ``SIGF{label}``
-    column so several structure-factor sets (e.g. protein and total) can coexist in
-    one MTZ.
-    """
+    """Build a one-amplitude dataset with labelled F / SIGF / PHIF columns.
+
+    Parameters
+    ----------
+    sfc
+        Structure-factor calculator containing the requested ASU amplitudes.
+    label
+        Output column label suffix, e.g. ``protein`` or ``total``.
+    structure_factor_column
+        SFcalculator attribute passed to ``prepare_dataset``.
+    miller_index_column
+        SFcalculator attribute containing Miller indices.
+    sigma_f_scale
+        Scale factor used to synthesize dummy SIGF values from amplitudes.
+
+    Returns
+    -------
+    rs.DataSet
+        Dataset containing ``F{label}``, ``SIGF{label}``, and ``PHIF{label}``.
+    """
🤖 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 `@src/sampleworks/eval/generate_synthetic_sf.py` around lines 137 - 145, The
new helper in generate_synthetic_sf.py has a summary docstring but is missing
the required NumPy-style structure. Update the docstring for the helper that
builds the rs.DataSet to use NumPy format with explicit Parameters and Returns
sections, documenting each input and the returned dataset/columns clearly; keep
the existing behavior unchanged and ensure the docstring matches the function
name and its role in auto-detecting and renaming the structure-factor columns.

Source: Coding guidelines

tests/rewards/test_reward_function_contract.py (1)

47-66: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Freeze RewardCase before sharing it across tests.

This bundle is passed around as shared state, so leaving the dataclass mutable makes accidental test-side mutation hard to spot. @dataclass(frozen=True) matches the repo's immutable-state convention and still works with batch(). As per coding guidelines, "Use frozen dataclasses with functional updates for immutable state management."

Suggested change
-@dataclass
+@dataclass(frozen=True)
 class RewardCase:
🤖 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 `@tests/rewards/test_reward_function_contract.py` around lines 47 - 66, Make
RewardCase immutable by marking the RewardCase dataclass as frozen so shared
test state cannot be mutated accidentally. Update the RewardCase definition to
use a frozen dataclass while keeping batch() unchanged, since it only reads
fields and still works with immutable instances. Use the existing RewardCase
symbol in tests/rewards/test_reward_function_contract.py to locate the class and
apply the repo’s immutable-state convention.

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 `@src/sampleworks/core/rewards/structure_factor.py`:
- Around line 152-163: Validate the batch_partition argument eagerly in the
structure_factor constructor before assigning it on self, and raise a clear
ValueError when it is zero or negative. Add the check in the initializer where
device, mtzfile, and batch_partition are set so invalid OOM-tuning input is
rejected immediately instead of failing later in calc_fprotein_batch().
- Around line 69-78: The auto-detection in structure_factor.py is ambiguous
because it independently chooses the first amplitude and sigma columns, which
can silently pair the wrong MTZ labels in multi-label datasets. Update the
selection logic in the structure-factor loading path to either require explicit
expcolumns when more than one StructureFactorAmplitudeDtype or
StandardDeviationDtype candidate exists, or ensure the chosen sigma column is
matched to the selected amplitude label in the same ds.select_mtzdtype/return
path.

In `@src/sampleworks/eval/generate_synthetic_sf.py`:
- Around line 543-547: The help text in generate_synthetic_sf should not escape
the braces around label because it is a plain string, not an f-string. Update
the help text in the argument definition near the existing bulk solvent option
so users see F{label}/SIGF{label}/PHIF{label} in --help, and verify the same
wording is used consistently wherever that option text is defined.

In `@tests/eval/test_generate_synthetic_sf.py`:
- Around line 135-139: The round-trip test in test_generate_synthetic_sf is only
checking that each non-blank altloc label exists, so it can miss cases where
some atoms lose their altloc assignment. Update the assertion near
find_all_altloc_ids/loaded to verify multiplicity as well, either by comparing
per-label counts for altloc_id or by comparing the full altloc_id annotation
when order is stable. Keep the check black-box by asserting observable
annotation behavior rather than implementation details.

In `@tests/rewards/reward_input_helpers.py`:
- Around line 7-13: The shared helper build_scattering_indices() still has a
prose-only docstring, so update it to the repository’s NumPy-style format. Add
the standard sections for parameters, returns, and any relevant notes/details so
the contract is explicit for the reward tests that reuse it, while keeping the
behavior unchanged.

In `@tests/rewards/test_reward_function_contract.py`:
- Around line 248-270: The debug-only `test_gradient_descent_loss_trace` in
`test_reward_function_contract.py` should be removed from collected tests
because it only prints loss values and never asserts anything. Delete this
`test_...` method from `TestRewardFunctionContract`, or if you want to keep the
trace, rename it to a non-test helper so pytest won’t run it; make sure no
`print`-only optimization loop remains in the test suite.
- Around line 39-44: The module-level test marker in the reward contract suite
should include both GPU and slow tagging. Update the existing pytestmark
assignment in the test module so the suite remains GPU-marked via
pytest.mark.gpu while also adding pytest.mark.slow, keeping the change localized
to the module-level marker used by these reward contract tests.

---

Nitpick comments:
In `@src/sampleworks/eval/generate_synthetic_sf.py`:
- Around line 137-145: The new helper in generate_synthetic_sf.py has a summary
docstring but is missing the required NumPy-style structure. Update the
docstring for the helper that builds the rs.DataSet to use NumPy format with
explicit Parameters and Returns sections, documenting each input and the
returned dataset/columns clearly; keep the existing behavior unchanged and
ensure the docstring matches the function name and its role in auto-detecting
and renaming the structure-factor columns.

In `@tests/rewards/test_reward_function_contract.py`:
- Around line 47-66: Make RewardCase immutable by marking the RewardCase
dataclass as frozen so shared test state cannot be mutated accidentally. Update
the RewardCase definition to use a frozen dataclass while keeping batch()
unchanged, since it only reads fields and still works with immutable instances.
Use the existing RewardCase symbol in
tests/rewards/test_reward_function_contract.py to locate the class and apply the
repo’s immutable-state convention.
🪄 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

Run ID: b9ae1d00-b4d9-46d0-87da-383baa1d031f

📥 Commits

Reviewing files that changed from the base of the PR and between 2777630 and f7c0f62.

📒 Files selected for processing (11)
  • src/sampleworks/core/rewards/structure_factor.py
  • src/sampleworks/eval/generate_synthetic_sf.py
  • src/sampleworks/eval/synthetic_utils.py
  • tests/conftest.py
  • tests/eval/test_generate_synthetic_sf.py
  • tests/resources/1vme/1vme_final_crystalframe_0.5occA_0.5occB_1.80A.cif
  • tests/resources/1vme/1vme_final_crystalframe_0.5occA_0.5occB_1.80A.mtz
  • tests/rewards/reward_input_helpers.py
  • tests/rewards/test_real_space_density_reward.py
  • tests/rewards/test_reward_function_contract.py
  • tests/rewards/test_structure_factor_reward.py

Comment thread src/sampleworks/core/rewards/structure_factor.py Outdated
Comment thread src/sampleworks/core/rewards/structure_factor.py
Comment thread src/sampleworks/eval/generate_synthetic_sf.py
Comment thread tests/eval/test_generate_synthetic_sf.py Outdated
Comment thread tests/rewards/reward_input_helpers.py
Comment thread tests/rewards/test_reward_function_contract.py
Comment thread tests/rewards/test_reward_function_contract.py Outdated
@DorisMai DorisMai marked this pull request as draft June 30, 2026 17:39
@DorisMai DorisMai force-pushed the dm/add-sfc-reward branch from b3f83d6 to de306a5 Compare June 30, 2026 21:54
@DorisMai DorisMai marked this pull request as ready for review July 2, 2026 21:03

@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: 1

🤖 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 `@src/sampleworks/eval/synthetic_utils.py`:
- Around line 265-276: Residue grouping in the AtomArray-to-Gemmi conversion
currently ignores insertion codes, so residues with the same res_id but
different ins_code get merged. Update the residue identity used in the
synthetic_utils flow (the key comparison around current_res / prev_key) to
include atom_array.ins_code, and propagate that value into gemmi.SeqId when
creating each residue so author insertion codes are preserved on write.
🪄 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

Run ID: 17750ecd-c9a6-4df7-8992-9af5dd711124

📥 Commits

Reviewing files that changed from the base of the PR and between a6d2117 and 4c0c81d.

📒 Files selected for processing (4)
  • src/sampleworks/eval/synthetic_utils.py
  • tests/eval/test_generate_synthetic_sf.py
  • tests/resources/1vme/1vme_final_crystalframe_0.5occA_0.5occB_1.80A.cif
  • tests/rewards/test_structure_factor_reward.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/eval/test_generate_synthetic_sf.py
  • tests/rewards/test_structure_factor_reward.py

Comment on lines +265 to +276
# New residue on any (chain_id, res_id) change. Keying on the pair (not res_id
# alone) keeps residues that share a res_id across a chain boundary separate.
if key != prev_key:
if prev_key is not None:
assert current_res is not None # prev_key set implies a residue exists
current_chain.add_residue(current_res)
current_res = gemmi.Residue()
current_res.name = str(atom_array.res_name[i])
current_res.seqid = gemmi.SeqId(str(res_id)) # author numbering
current_res.label_seq = res_id # label numbering -> label_seq_id on write
current_res.subchain = chain_id # label_asym_id == chain name
prev_key = key

@coderabbitai coderabbitai Bot Jul 6, 2026

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether ins_code is present/used in the atom arrays flowing into atomarray_to_gemmi
rg -nP '\bins_code\b' -C2 --type=py
# Confirm biotite AtomArray exposes ins_code in this environment's version
rg -nP 'atomworks|biotite' pyproject.toml

Repository: diff-use/sampleworks

Length of output: 279


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the target file and locate the converter/callers.
ast-grep outline src/sampleworks/eval/synthetic_utils.py --view expanded || true
rg -n "atomarray_to_gemmi|ins_code|label_seq|res_id|SeqId" src/sampleworks -C 3

# Inspect the relevant slice of the converter.
sed -n '220,320p' src/sampleworks/eval/synthetic_utils.py

# Find where the atom arrays come from and whether any pipeline preserves insertion codes.
rg -n "get_annotation_categories|ins_code|AtomArray|AtomArrayStack|load_structure|to_atom_array|atom_array" src -C 2

Repository: diff-use/sampleworks

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Narrow search for actual insertion-code handling in the repo.
rg -n "\bins_code\b|ins[-_ ]?code|insertion code" src tests .

# Inspect the exact converter slice with line numbers.
sed -n '250,290p' src/sampleworks/eval/synthetic_utils.py | cat -n

# Inspect the atom-array ID helpers, since they define the matching semantics used upstream.
sed -n '580,760p' src/sampleworks/utils/atom_array_utils.py | cat -n

# Check whether the code assumes res_id alone anywhere else for residue identity.
rg -n "\bres_id\b.*\bres_id\b|make_atom_id|make_normalized_atom_id|filter_to_common_atoms" src/sampleworks/utils/atom_array_utils.py src/sampleworks/eval/synthetic_utils.py -C 2

Repository: diff-use/sampleworks

Length of output: 21337


🌐 Web query:

biotite AtomArray ins_code annotation PDBx PDB insertion code AtomArray load_any

💡 Result:

In Biotite, the insertion code is a mandatory annotation within an AtomArray, stored in the 'ins_code' array [1]. When loading structures from PDBx/mmCIF files, Biotite maps the PDBx field _atom_site.pdbx_PDB_ins_code to this 'ins_code' annotation [2]. Regarding your specific queries: 1. 'ins_code' annotation: Every AtomArray object in Biotite stores fundamental structural attributes as NumPy arrays, including 'ins_code' (residue insertion code), 'chain_id', 'res_id', 'res_name', 'atom_name', and 'element' [1][3]. These are essential components of the molecular structure representation in Biotite [1]. 2. PDBx/PDB integration: When using PDBx (mmCIF) files, the Biotite parser automatically attempts to extract the insertion code [2]. Note that historical issues have been reported where files lacking the _atom_site.pdbx_PDB_ins_code field caused errors, though this has been addressed in recent updates to better handle missing values [4]. 3. load_any: Biotite does not have a single monolithic 'load_any' function; instead, structure loading is handled through format-specific modules in biotite.structure.io, such as pdb.get_structure or pdbx.get_structure [5][6][7]. To load a structure and preserve attributes like insertion codes, you should use the get_structure method provided by the appropriate IO module (e.g., pdbx for PDBx/mmCIF files) [5][7]. These methods automatically populate the standard annotations, including 'ins_code', if the information is available in the source file [5][2].

Citations:


Include ins_code in residue identity src/sampleworks/eval/synthetic_utils.py:265-276
Biotite exposes ins_code on AtomArray, and these CIF inputs already carry _atom_site.pdbx_PDB_ins_code, so residues that share a res_id but differ by insertion code will be merged here and written back without the author insertion code. Fold ins_code into the residue boundary key and gemmi.SeqId.

🤖 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 `@src/sampleworks/eval/synthetic_utils.py` around lines 265 - 276, Residue
grouping in the AtomArray-to-Gemmi conversion currently ignores insertion codes,
so residues with the same res_id but different ins_code get merged. Update the
residue identity used in the synthetic_utils flow (the key comparison around
current_res / prev_key) to include atom_array.ins_code, and propagate that value
into gemmi.SeqId when creating each residue so author insertion codes are
preserved on write.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Currently the entire repo does not use ins_code, and key on (chain_id, res_id) or (chain_id, res_id, atom_name). I am not sure when or how often we encounter ins_code. I did notice that Vratin uses (chain_id, res_id, ins_code) in WaterFlow. @marcuscollins Do you think this is worth opening an issue to fix repo-wide or low priority / assume no ins code handling? either way I don't think it should block this PR.

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.

@DorisMai Agreed this shouldn't block this PR — merging residues that share res_id but differ by ins_code is a pre-existing, repo-wide convention here rather than something introduced by this change. Since it's a broader design question (repo-wide key convention vs. one-off fix), it does seem worth tracking separately so it doesn't get lost, especially given the inconsistency you noticed with WaterFlow's (chain_id, res_id, ins_code) keying.

If you and @marcuscollins agree, I'm happy to open a follow-up GitHub issue summarizing this discussion (current repo convention, the WaterFlow inconsistency, and the specific spot in atomarray_to_gemmi as a starting point) — just let me know and I'll file it.


✏️ Learnings added
Learnt from: DorisMai
Repo: diff-use/sampleworks PR: 272
File: src/sampleworks/eval/synthetic_utils.py:265-276
Timestamp: 2026-07-06T17:02:04.734Z
Learning: In the diff-use/sampleworks repo, residue identity throughout the codebase (e.g., in `src/sampleworks/eval/synthetic_utils.py`'s `atomarray_to_gemmi`) is generally keyed on (chain_id, res_id) or (chain_id, res_id, atom_name), without incorporating `ins_code`. This is an intentional, repo-wide convention, not a bug specific to one function. Note: WaterFlow code (referenced by DorisMai/Vratin) uses (chain_id, res_id, ins_code) instead, which is an inconsistency that may warrant a future repo-wide follow-up issue, but should not block individual PRs.

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.


🧠 Learnings used
Learnt from: k-chrispens
Repo: diff-use/sampleworks PR: 73
File: src/sampleworks/core/samplers/protocol.py:1-18
Timestamp: 2026-02-02T19:13:51.594Z
Learning: In Python files across the repo, do not suggest or add inline or file-level suppressions for Ruff F722 (jaxtyping dimension strings). Instead rely on a global ignore configured in pyproject.toml (as done by the sampleworks project). Verify that F722 is included in the global ignore, and avoid recommending per-file # ruff: noqa: F722 directives unless explicitly documented as an exception.

Learnt from: marcuscollins
Repo: diff-use/sampleworks PR: 132
File: src/sampleworks/utils/guidance_script_utils.py:585-586
Timestamp: 2026-03-05T16:30:40.514Z
Learning: In Python code, if enums subclassing StrEnum are used (e.g., GuidanceType, StructurePredictor), they serialize to plain strings with json.dump and pickle without special handling. Do not flag these as non-serializable in code reviews. Treat StrEnum values as strings for JSON serialization and ensure tests cover that behavior; no extra pickle handling needed.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@CodeRabbit go ahead and create the issue. Please indicate in the issue that we need to research when ins_code is used and why. That should inform our decision on what to do.

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.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo="diff-use/sampleworks"

body=$(cat <<'EOF'
## Summary

`atomarray_to_gemmi` in `src/sampleworks/eval/synthetic_utils.py` (and residue-identity logic elsewhere in the repo) keys residues on `(chain_id, res_id)` or `(chain_id, res_id, atom_name)`, without incorporating `ins_code`. Biotite `AtomArray` objects do expose an `ins_code` annotation, and CIF inputs can carry `_atom_site.pdbx_PDB_ins_code`, so residues that share a `res_id` but differ by insertion code could currently be merged/misidentified.

Separately, WaterFlow code keys residue identity on `(chain_id, res_id, ins_code)`, which is inconsistent with the convention used in this repo.

## Why this needs research first

Before deciding whether to change the repo-wide convention (or leave it as-is), we need to understand:
- How often, and in what real-world structures/datasets, `ins_code` is actually populated (non-blank) in inputs processed by this codebase.
- Whether any current pipelines (synthetic SF/density generation, reward functions, structure loading utilities) could produce silently incorrect results due to ignoring `ins_code`.
- Why WaterFlow chose to include `ins_code` in its identity key, and whether that reflects a real correctness requirement or just defensive coding.

## Affected areas

- `src/sampleworks/eval/synthetic_utils.py` (`atomarray_to_gemmi`)
- `src/sampleworks/utils/atom_array_utils.py` (residue/atom identity helpers)
- Any other residue-identity keying in the repo that currently omits `ins_code`

## Acceptance criteria

- [ ] Document findings on when/why `ins_code` is populated and relevant in practice (e.g., which structures/datasets), and whether it's already handled by upstream loading/filtering steps.
- [ ] Decide whether to adopt `(chain_id, res_id, ins_code)` repo-wide, keep the current convention, or handle it case-by-case.
- [ ] If a change is warranted, list the specific functions/files to update.

## References

- PR: https://github.com/diff-use/sampleworks/pull/272
- Discussion: https://github.com/diff-use/sampleworks/pull/272#discussion_r3530470906
- Requested by: `@marcuscollins`
EOF
)

gh issue create \
  --repo "$repo" \
  --title "Research: when/why is ins_code needed for residue identity?" \
  --assignee "marcuscollins" \
  --body "$body"

Length of output: 2503


@marcuscollins Done — I've opened #306 to track this, framed as a research task on when/why ins_code is used and why, which should inform whether we adopt (chain_id, res_id, ins_code) repo-wide, keep the current convention, or handle it case-by-case. It includes backlinks to this PR and discussion, the affected files, and acceptance criteria.

(ᵔᴥᵔ)


🧠 Learnings used
Learnt from: DorisMai
Repo: diff-use/sampleworks PR: 272
File: src/sampleworks/eval/synthetic_utils.py:265-276
Timestamp: 2026-07-06T17:02:04.734Z
Learning: In diff-use/sampleworks, treat residue identity as following the repo convention: key residue-related data primarily by (chain_id, res_id) and, when needed, (chain_id, res_id, atom_name)—do not require ins_code to be included in these identity keys. For example, functions like atomarray_to_gemmi in src/sampleworks/eval/synthetic_utils.py should follow this approach. Note that some external/related code (e.g., WaterFlow) uses (chain_id, res_id, ins_code); this inconsistency may justify a future repo-wide follow-up, but it should not be used to block individual PRs that adhere to the existing convention.

@marcuscollins marcuscollins left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Lots of comments to look at. The biggest concern I have is about picking the first type-matching column for amplitudes, etc...

I'm also confused about how some of the changes in generate_synthetic_sf.py relate to the changes in structure_factor.py. They appear disconnected, but maybe I'm missing something. Can you explain?

Comment thread src/sampleworks/eval/generate_synthetic_sf.py
Comment on lines +265 to +276
# New residue on any (chain_id, res_id) change. Keying on the pair (not res_id
# alone) keeps residues that share a res_id across a chain boundary separate.
if key != prev_key:
if prev_key is not None:
assert current_res is not None # prev_key set implies a residue exists
current_chain.add_residue(current_res)
current_res = gemmi.Residue()
current_res.name = str(atom_array.res_name[i])
current_res.seqid = gemmi.SeqId(str(res_id)) # author numbering
current_res.label_seq = res_id # label numbering -> label_seq_id on write
current_res.subchain = chain_id # label_asym_id == chain name
prev_key = key

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@CodeRabbit go ahead and create the issue. Please indicate in the issue that we need to research when ins_code is used and why. That should inform our decision on what to do.

Comment thread src/sampleworks/eval/synthetic_utils.py
Comment thread src/sampleworks/eval/synthetic_utils.py
Comment thread src/sampleworks/eval/synthetic_utils.py
# Ftotal uses the default (unrefined) scales kiso=1, kmask=0.35, small uaniso,
# matching generate_synthetic_sf's Ftotal. They don't depend on coordinates, so
# set them once here. _set_scales only needs atom_pos_frac (set at construction)
# and n_bins (set by the experiment init) for dtype/device and bin count.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

what is the "experiment init"?

@DorisMai DorisMai Jul 10, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

SFC's internal method (SFcalculator.init_mtz()) called in its own __init__

if self.bulk_solvent != "off":
self.sfc._set_scales(requires_grad=False)

# |Eo| are computed in SFC's experiment init (inside a try/except).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

remind me, what is Eo?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

observed amplitudes normalized in each resolution bin

Comment thread src/sampleworks/core/rewards/structure_factor.py
unit_cell=self.unit_cell,
space_group=self.space_group,
)
self.sfc = SFcalculator(pdbmodel=PDBParser(gemmi_structure), **self._sfc_kwargs)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

so does self.sfc contain the actual amplitudes and sigmas? I don't see anywhere here that explicitly stores the data in the mtz file. I would call out somewhere where that data lives to make the code easier to understand.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

updated the docstring of prepare to describe the sfc object better

# Multi-conformer combination: complex sum over the ensemble [batch, n_hkl].
# occ = 1/E (set per call) makes the summed |F| the multi-conformer total.
Fprotein_batch = self.sfc.calc_fprotein_batch(
coordinates, Return=True, PARTITION=self.batch_partition

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

omg we need to talk to Minhuan about his Python style. capital letters kwarg names!?

@DorisMai

DorisMai commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator Author

Lots of comments to look at. The biggest concern I have is about picking the first type-matching column for amplitudes, etc...

I'm also confused about how some of the changes in generate_synthetic_sf.py relate to the changes in structure_factor.py. They appear disconnected, but maybe I'm missing something. Can you explain?

My previous PR implemented atomarray_to_gemmi() that correctly feed into SFC, and optionally saved a cif file for my visual inspection. However, that cif file doesn't correctly round trip back to atomarray. I noticed this issue/needed to correct it in order to generate the tests for the structure factor reward function, which happens in the middle of this PR, hence the diffs look a bit messy.

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