Skip to content

feat(rewards): add differentiable structure-factor reward - #324

Merged
DorisMai merged 12 commits into
mainfrom
dm/sf-reward
Aug 1, 2026
Merged

feat(rewards): add differentiable structure-factor reward#324
DorisMai merged 12 commits into
mainfrom
dm/sf-reward

Conversation

@DorisMai

@DorisMai DorisMai commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator

The older PR #272 is broken into 3 smaller PRs for (1) improve synthetic data generation, (2) refactor reward tests, and (3) add structure factor reward. This PR addresses (3).

Changes relative to main branch

  1. Add StructureFactorRewardFunction
  2. Add reward-specific tests + committed related test fixture files
  3. Add SF reward to run the reward-agnostic contract tests too (Depends on PR #)

Changes relative to the previous PR #272

Most changes were addressing reviewer comments, except for MTZ parsing and inferring column types

  • MTZ is now parsed once with in _resolve_mtz_metadata and cached; mtzdata moved out of _sfc_kwargs into prepare() to reduce the number of file reading. Dropped _detect_mtz_metadata.
  • _resolve_expcolumns rewritten to take the parsed rs.Dataset. Auto-detect mtz column now requires exactly one amplitude + one sigma column — a multi-set MTZ (Fprotein+Ftotal) raises instead of silently picking the first with a warning.
  • add _build_reflection_mask to also validate the built mask retains a reasonable number of reflections

Summary by CodeRabbit

  • New Features

    • Added a structure-factor reward for comparing calculated and experimental crystallographic amplitudes.
    • Supports normalized amplitudes, bulk-solvent corrections, excluded reflections, configurable resolution, and custom loss functions.
    • Crystal unit-cell and space-group metadata are now read directly from MTZ data.
    • Added validation for crystallographic metadata, reflection selections, batch settings, and reward preparation.
  • Tests

    • Added comprehensive coverage for configuration, masking, bulk-solvent behavior, ensemble handling, metadata validation, and reward accuracy.

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Changes

Structure-factor reward

Layer / File(s) Summary
Reward configuration and MTZ metadata
src/sampleworks/core/rewards/structure_factor.py
Adds StructureFactorRewardFunction, validates configuration, resolves MTZ metadata and amplitude columns, and sources unit-cell and space-group values from the MTZ.
Calculator preparation and reflection selection
src/sampleworks/core/rewards/structure_factor.py
Builds prepared SFcalculator state, configures solvent data, validates normalized amplitudes, and creates the reflection mask.
Batched amplitude evaluation and solvent modes
src/sampleworks/core/rewards/structure_factor.py
Computes ensemble structure factors, supports bulk-solvent modes, selects normalized or raw amplitudes, and returns the masked loss.
Reward fixtures and behavioral coverage
tests/rewards/conftest.py, tests/rewards/test_reward_function_contract.py, tests/rewards/test_structure_factor_reward.py
Adds synthetic SF fixtures, contract-test integration, and tests for construction, batching, solvent handling, normalization, and reflection filtering.

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

Sequence Diagram(s)

sequenceDiagram
  participant RewardCaller
  participant StructureFactorRewardFunction
  participant SFcalculator
  RewardCaller->>StructureFactorRewardFunction: provide batched coordinates, B-factors, and occupancies
  StructureFactorRewardFunction->>SFcalculator: compute ensemble protein amplitudes
  StructureFactorRewardFunction->>SFcalculator: compute optional solvent-adjusted amplitudes
  StructureFactorRewardFunction-->>RewardCaller: return masked amplitude loss
Loading

Possibly related PRs

Suggested reviewers: k-chrispens, marcuscollins

🚥 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 clearly and concisely summarizes the primary change: adding a differentiable structure-factor reward.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 💡 1
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dm/sf-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.

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

🧹 Nitpick comments (3)
src/sampleworks/core/rewards/structure_factor.py (1)

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

Add a class-level docstring.

StructureFactorRewardFunction has only an __init__ docstring, so class-level API documentation is absent. 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 `@src/sampleworks/core/rewards/structure_factor.py` at line 121, Add a
NumPy-style class-level docstring directly inside StructureFactorRewardFunction,
documenting its purpose and relevant attributes or behavior; retain the existing
__init__ docstring and avoid changing implementation logic.

Source: Coding guidelines

tests/rewards/conftest.py (1)

63-106: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document the new fixtures with NumPy-style docstrings.

The four added fixtures rely on comments or have no docstring. Add concise Parameters/Returns documentation. 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 `@tests/rewards/conftest.py` around lines 63 - 106, Add concise NumPy-style
docstrings to the fixtures mtz_path_1vme, structure_1vme_sf,
test_coordinates_1vme_sf, and reward_function_1vme_sf, documenting their
parameters and return values. Replace or supplement the existing inline comments
as needed while preserving the fixture behavior.

Source: Coding guidelines

tests/rewards/test_structure_factor_reward.py (1)

342-370: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert free-set exclusion through the public reward call.

The test reads sfc.free_flag and _reflection_mask, coupling it to internals. Use a public custom loss that returns the received reflection count, then assert the excluded-free reward receives fewer reflections.

As per coding guidelines, “Write black-box tests that verify public behavior and contracts … rather than implementation details.”

🤖 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_structure_factor_reward.py` around lines 342 - 370, Update
test_exclude_free_set_drops_reflections to verify exclusion through the public
reward-call interface instead of inspecting sfc.free_flag or _reflection_mask.
Define a public custom loss that records or returns the number of received
reflections, pass it to both prepared rewards, invoke each reward, and assert
that the exclude_free_reflections=True result receives fewer reflections than
the unrestricted result.

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 221-223: Update the constructor validation immediately before
assigning self.batch_partition to require batch_partition to be an int but not a
bool, while preserving the existing positive-value check and error behavior for
invalid values. Ensure non-integer positive values are rejected before reaching
PARTITION.
- Around line 357-362: Update the reflection-mask construction near
self._reflection_mask to validate mask_np before converting it to a tensor.
Raise a clear ValueError when mask_np.any() is false, while preserving the
existing outlier and optional free-reflection filtering for non-empty masks.
- Around line 241-242: Update the sfcalculator_kwargs handling in the
surrounding reward initialization to reject or remove reward-owned keys mtzdata,
pdbmodel, expcolumns, device, and set_experiment before updating
self._sfc_kwargs. Preserve only safe extension options such as n_bins and
free-flag configuration, and ensure reserved arguments cannot override
reward-controlled values or reach the constructor twice.

---

Nitpick comments:
In `@src/sampleworks/core/rewards/structure_factor.py`:
- Line 121: Add a NumPy-style class-level docstring directly inside
StructureFactorRewardFunction, documenting its purpose and relevant attributes
or behavior; retain the existing __init__ docstring and avoid changing
implementation logic.

In `@tests/rewards/conftest.py`:
- Around line 63-106: Add concise NumPy-style docstrings to the fixtures
mtz_path_1vme, structure_1vme_sf, test_coordinates_1vme_sf, and
reward_function_1vme_sf, documenting their parameters and return values. Replace
or supplement the existing inline comments as needed while preserving the
fixture behavior.

In `@tests/rewards/test_structure_factor_reward.py`:
- Around line 342-370: Update test_exclude_free_set_drops_reflections to verify
exclusion through the public reward-call interface instead of inspecting
sfc.free_flag or _reflection_mask. Define a public custom loss that records or
returns the number of received reflections, pass it to both prepared rewards,
invoke each reward, and assert that the exclude_free_reflections=True result
receives fewer reflections than the unrestricted result.
🪄 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: 9e1ae148-0d34-43f4-92a5-09a9a8383486

📥 Commits

Reviewing files that changed from the base of the PR and between 3182f7e and 34bf4f0.

📒 Files selected for processing (6)
  • src/sampleworks/core/rewards/structure_factor.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/conftest.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/core/rewards/structure_factor.py Outdated

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

There are a couple testing things I think should be added/changed, and a bunch of smaller issues that should be easy to address. Very close to ready.

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.

I would rather not add binary files to a git repo. The problem is that a binary file can't have tracked changes, and so if you change it, you keep both the old and the new files in the history forever. This leads to the repo growing out of control if you change the binary files often enough.

In any case for unit testing, it is usually better to create files like this "on the fly" as needed for testing (that would include the corresponding CIF too). Integration testing (which we aren't really doing right now) could use a real file like this, but then we'd store it somewhere else, not in the repo.

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.

the fixtures and the resources files were both mirroring the density reward ones. I am changing the files for SF to be generated on the fly. Would you want an issue opened about moving the density ccp4 files out as well or keep it as legacy code/file?

Comment thread src/sampleworks/core/rewards/structure_factor.py Outdated
Comment thread src/sampleworks/core/rewards/structure_factor.py
Comment thread src/sampleworks/core/rewards/structure_factor.py Outdated
Comment thread src/sampleworks/core/rewards/structure_factor.py Outdated
self.sfc._set_scales(requires_grad=False)

# |Eo| are computed in SFC's experiment init (inside a try/except).
if self.normalize_amplitude and getattr(self.sfc, "Eo", None) is None:

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.

Should we set the SFC args/kwargs in accordance with self.normalize_amplitude instead, and make sure Eo are computed? Or is that already the case and the calculation might have failed?

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.

there is no args/kwargs in SFC to control this, Eo is always computed when an mtz file is provided. If Eo computation failed, outlier mask is just all False, so I will modify the check here such that it logs a warning even if we don't use Eo (self.normalize_amplitude = False)

Comment thread src/sampleworks/core/rewards/structure_factor.py
if self.bulk_solvent == "per_conformer":
# calc_fsolvent_batch masks each conformer (from Fprotein_asu_batch, set by
# calc_fprotein_batch); the mean applies the 1/E weight -> <mask(rho)>.
Fmask_HKL_batch = self.sfc.calc_fsolvent_batch(

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.

I haven't looked at the tests yet, so maybe you've got this: Make sure to check that this method gets called when self.bulk_solvent == "per_conformer". You can do that by making a mock for sfc.calc_fsolvent_batch (e.g. using MagicMock) and then the mock should keep track of how many times it is called.

@DorisMai DorisMai Jul 29, 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.

There is a test (test_ftotal_modes_diverge_for_distinct_conformer_ensemble) that check the resulted Ftotal differs depending on the bulk_solvent mode being "per_conformer" or "combined". I added a MagicMock component to that test to also count the correct dispatch function call, which I think helps diagnose if there is incidental agreement between <mask(protein_density)> and mask(<protein_density>). I will caveat that I've never heard of MagicMock/Mock before so I am not sure if I used it the best way.

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.

I'll check it!

Comment thread tests/rewards/conftest.py Outdated
Comment thread tests/rewards/conftest.py Outdated

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

🧹 Nitpick comments (3)
tests/rewards/test_structure_factor_reward.py (1)

460-470: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Docstring says "committed MTZ", but the source MTZ is generated per session.

mtz_path_1vme comes from the synthetic generator fixture, not a committed file — worth correcting to avoid misleading future readers.

🤖 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_structure_factor_reward.py` around lines 460 - 470, Update
the docstring of the mtz_all_free_1vme fixture to describe the generated source
MTZ accurately, replacing the reference to a committed MTZ while preserving the
documented flag column name and test-set convention.
tests/rewards/conftest.py (1)

95-141: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Fixture couples to a private generator and its filename convention.

_process_single_row is private and returns None, so the fixture must reconstruct {stem}_sf_input.cif / {stem}_{resolution:.2f}A.mtz by hand — a silent break if the generator's naming changes. Since the comment already notes this, consider making the generator return the written paths (or accept explicit output names) so the fixture can consume a public contract.

As per coding guidelines, tests should "verify public behavior and contracts" rather than implementation details.

🤖 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/conftest.py` around lines 95 - 141, The fixture currently
depends on private _process_single_row behavior and hard-coded output filenames.
Update the generator’s public contract to return the generated CIF and MTZ paths
(or accept explicit output names), then have the fixture consume that returned
contract instead of reconstructing names from source_cif and resolution.
Preserve the existing generation parameters and missing-output failure behavior.

Source: Coding guidelines

src/sampleworks/core/rewards/structure_factor.py (1)

387-392: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use sfc.init_scales(requires_grad=False) instead of _set_scales.

generate_synthetic_sf.py uses init_scales for the same default scale initialization, while _set_scales is private SFC_Torch/SFcalculattorch internal API. If this code intentionally skips init_scales’s extra setup, add a short justification; otherwise call the public API consistently.

🤖 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/core/rewards/structure_factor.py` around lines 387 - 392,
Replace the private self.sfc._set_scales call in the bulk-solvent initialization
block with the public self.sfc.init_scales(requires_grad=False) API, matching
generate_synthetic_sf.py’s default scale initialization. If the extra setup
performed by init_scales must be skipped, document that exception briefly
instead.
🤖 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.

Nitpick comments:
In `@src/sampleworks/core/rewards/structure_factor.py`:
- Around line 387-392: Replace the private self.sfc._set_scales call in the
bulk-solvent initialization block with the public
self.sfc.init_scales(requires_grad=False) API, matching
generate_synthetic_sf.py’s default scale initialization. If the extra setup
performed by init_scales must be skipped, document that exception briefly
instead.

In `@tests/rewards/conftest.py`:
- Around line 95-141: The fixture currently depends on private
_process_single_row behavior and hard-coded output filenames. Update the
generator’s public contract to return the generated CIF and MTZ paths (or accept
explicit output names), then have the fixture consume that returned contract
instead of reconstructing names from source_cif and resolution. Preserve the
existing generation parameters and missing-output failure behavior.

In `@tests/rewards/test_structure_factor_reward.py`:
- Around line 460-470: Update the docstring of the mtz_all_free_1vme fixture to
describe the generated source MTZ accurately, replacing the reference to a
committed MTZ while preserving the documented flag column name and test-set
convention.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 35b347ea-e816-4f7d-a83d-16344a8fa70c

📥 Commits

Reviewing files that changed from the base of the PR and between 84d8ef9 and 4a21600.

📒 Files selected for processing (4)
  • src/sampleworks/core/rewards/structure_factor.py
  • tests/rewards/conftest.py
  • tests/rewards/test_reward_function_contract.py
  • tests/rewards/test_structure_factor_reward.py

@DorisMai
DorisMai requested a review from marcuscollins July 30, 2026 00:54

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

Thanks for the changes. I left a few more comments (look for unresolved comments) that you can think about and decide what to do, but after you look at those, feel free to merge.

Comment thread src/sampleworks/core/rewards/structure_factor.py Outdated
Comment thread src/sampleworks/core/rewards/structure_factor.py Outdated
Comment thread src/sampleworks/core/rewards/structure_factor.py Outdated
Comment thread src/sampleworks/core/rewards/structure_factor.py Outdated
if self.bulk_solvent == "per_conformer":
# calc_fsolvent_batch masks each conformer (from Fprotein_asu_batch, set by
# calc_fprotein_batch); the mean applies the 1/E weight -> <mask(rho)>.
Fmask_HKL_batch = self.sfc.calc_fsolvent_batch(

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.

I'll check it!

Comment thread tests/rewards/test_structure_factor_reward.py Outdated
Comment thread tests/rewards/test_structure_factor_reward.py Outdated

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

🧹 Nitpick comments (1)
src/sampleworks/core/rewards/structure_factor.py (1)

142-146: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Typos in the new docstring paragraph.

NiavelyNaively, overrrideoverride, and there are not argumentsthere are no arguments.

✏️ Proposed fix
-        the structure by the MTZ's whenever they disagree. Niavely overriding the cell
-        and/or the space group of MTZ can be dangerous because the reflections' indexing
-        could become wrong, so there are not arguments here to overrride them.
+        the structure by the MTZ's whenever they disagree. Naively overriding the cell
+        and/or the space group of MTZ can be dangerous because the reflections' indexing
+        could become wrong, so there are no arguments here to override them.
🤖 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/core/rewards/structure_factor.py` around lines 142 - 146,
Correct the typos in the docstring paragraph near SFcalculator metadata
handling: change “Niavely” to “Naively,” “overrride” to “override,” and “there
are not arguments” to “there are no arguments,” without altering the meaning.
🤖 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.

Nitpick comments:
In `@src/sampleworks/core/rewards/structure_factor.py`:
- Around line 142-146: Correct the typos in the docstring paragraph near
SFcalculator metadata handling: change “Niavely” to “Naively,” “overrride” to
“override,” and “there are not arguments” to “there are no arguments,” without
altering the meaning.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: dedbf114-d1b8-480e-b1ea-38b96c3965ca

📥 Commits

Reviewing files that changed from the base of the PR and between 4a21600 and 001903d.

📒 Files selected for processing (2)
  • src/sampleworks/core/rewards/structure_factor.py
  • tests/rewards/test_structure_factor_reward.py

"E" collided with the normalized structure-factor amplitude |E| that
`normalize_amplitude` scores, so the ensemble/batch dimension is now
spelled `batch_size` throughout the docstrings and comments. Docs only;
no behavior change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@DorisMai
DorisMai merged commit d69b062 into main Aug 1, 2026
12 of 16 checks passed
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