Skip to content

feat(cli): select and configure rewards via --reward-type and --reward-config - #375

Draft
manzuoni-astera wants to merge 13 commits into
diff-use:mainfrom
manzuoni-astera:michaelanzuoni/reward-cli
Draft

feat(cli): select and configure rewards via --reward-type and --reward-config#375
manzuoni-astera wants to merge 13 commits into
diff-use:mainfrom
manzuoni-astera:michaelanzuoni/reward-cli

Conversation

@manzuoni-astera

@manzuoni-astera manzuoni-astera commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Stacked on #373 and #374, and opened against main for the same reason they are: a fork branch
can't be a base. The diff includes both. Review from feat(cli): select and configure rewards via --reward-type and --reward-config onwards, and merge the other two first.

Summary

Puts the registry from #374 on the command line. --reward-type picks a reward and brings that
reward's own flags with it, generated from its schema; --reward-config takes the same
configuration from a file and is the only way to combine rewards. Both produce one
RewardConfig, so going from one reward to two is a change to the run, not to the plumbing. The
default is unchanged, so existing command lines, presets and tests keep working. This is also
what finally makes the structure-factor reward from #324 runnable.

Changes

sampleworks-guidance ... --reward-type structure_factor --mtzfile 1vme.mtz --bulk-solvent combined
sampleworks-guidance ... --reward-config rewards.yaml

The reward is resolved in the parser's existing first pass, beside --model, because it decides
which flags the second pass registers. Registering only the selected reward's options gets
cross-reward rejection for free: --density under --reward-type structure_factor is an
argparse error, the same way a Boltz flag already is on a Protenix run. Passing a config file and
a --reward-type together is rejected outright rather than one silently winning.

--reward-type defaults to real_space_density, and its flags keep their spellings, so nothing
anyone has typed before changes meaning. tests/cli/test_guidance_cli.py passes untouched, which
is the compatibility check that matters.

Two constraints shaped the serialization, and both are easy to get wrong:

reward_config serializes as a JSON string rather than a nested mapping. as_dict() is written
into the output CIF as the sampleworks category, and add_category_to_cif treats any
non-string iterable as a column of rows, so a nested dict there gets iterated into its keys and
produces a ragged category at the very end of an expensive run.

The flat density, resolution, loss_order and em fields stay on GuidanceConfig.
run_grid_search.py builds configs from them, grid_search_eval_utils.py reads density and
resolution back out of job_metadata.json, and job queues are pickled by one build and unpickled
by another. So the two representations are reconciled in one method, in whichever direction has
the information, and __setstate__ fills in reward_config for pickles written before it
existed.

A reward missing a required input is reported as a usage error before a model is loaded. The
builders check the same thing for callers that arrive another way.

CompositeReward lands here too. build_reward returns a single reward at full weight as
itself, so current runs keep the gradients they have, and anything else becomes a weighted sum
with 1/N defaults. README and AGENTS.md gain a section on picking rewards and on what adding a
reward type involves.

Testing

Full fast suite green, 972 tests. New CLI coverage for reward selection, config files in three
formats, cross-reward flag rejection, the config-plus---reward-type conflict, the legacy pickle
path, and a metadata round trip that writes the output CIF and reads it back.

tests/rewards/test_reward_build_integration.py runs a command line through to a scored
structure for both rewards and for a two-reward config. That seam is what let #324 merge
unreachable, so it now has a test of its own.

CI is green: lint, four typecheck environments, four test environments.

Not covered: the GPU workflow has not run on any of the stack, so the structure-factor numerics
have only been exercised on CPU. The gpu-marked reward tests are 122 passed and 21 failed on my
laptop, with every failure being "Torch not compiled with CUDA enabled", and the same tests fail
identically on main. Someone with a GPU box should confirm before merge.

Rollout

No migration. Existing command lines, preset TOMLs, pickled job queues and the evaluation scripts
all keep working, and the new flags are optional. Docs ship in this PR.

Two things to know after it lands. Grid search is still density-only: the proteins CSV is
structure,density,resolution, and structure factors would need it to carry an MTZ, with
RewardConfig.with_experimental_data() as the hook. And #319 conflicts with this by design; once
this is in, it rebases down to a reward class, an options dataclass and one registry entry.

Merge after #373 and #374.

Guidance backprops the value and FK steering picks with argmin, so every
reward here is really a loss. Nothing said so. Now the protocol docstring
does, and points at the contract test that catches a term with the wrong
sign.
The structure-factor reward from diff-use#324 is built in two phases, but nothing in
src/ ever called the second one, so it could not run from the pipeline at
all. Adds PreparableRewardFunctionProtocol and a prepare_reward_if_needed
helper, called from both trajectory scalers once the model atom array
exists. prepare() mutates the reward and returns None.

The tmol reward in diff-use#319 and the torchref one in diff-use#372 both need this hook.

Also replaces an `or` fallback on an AtomArray with a reward_atom_array
property. Whether an empty AtomArray is falsy is biotite's call, not ours.
@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@manzuoni-astera, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 38 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 25ba55ce-2852-411d-b17f-758491886aa7

📥 Commits

Reviewing files that changed from the base of the PR and between 62a226c and 6683c29.

📒 Files selected for processing (25)
  • AGENTS.md
  • README.md
  • src/sampleworks/core/rewards/composite.py
  • src/sampleworks/core/rewards/config.py
  • src/sampleworks/core/rewards/options.py
  • src/sampleworks/core/rewards/protocol.py
  • src/sampleworks/core/rewards/real_space_density.py
  • src/sampleworks/core/rewards/registry.py
  • src/sampleworks/core/rewards/structure_factor.py
  • src/sampleworks/core/scalers/fk_steering.py
  • src/sampleworks/core/scalers/pure_guidance.py
  • src/sampleworks/eval/structure_utils.py
  • src/sampleworks/utils/guidance_constants.py
  • src/sampleworks/utils/guidance_script_arguments.py
  • src/sampleworks/utils/guidance_script_utils.py
  • tests/cli/test_guidance_cli.py
  • tests/integration/test_pipeline_integration.py
  • tests/rewards/test_composite.py
  • tests/rewards/test_prepare_hook.py
  • tests/rewards/test_reward_build_integration.py
  • tests/rewards/test_reward_config.py
  • tests/rewards/test_reward_function_contract.py
  • tests/rewards/test_reward_registry.py
  • tests/utils/test_guidance_script_arguments.py
  • tests/utils/test_guidance_script_utils.py

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.

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

This PR makes rewards a first-class, selectable part of sampleworks-guidance: runs can choose a single reward via --reward-type (auto-registering only that reward’s flags) or define one-or-more rewards in a --reward-config file that parses into a unified RewardConfig. It also adds a “prepare” hook so topology-dependent rewards (e.g., structure factors) can bind to the model’s atom ordering before the first evaluation, and introduces CompositeReward for weighted combinations.

Changes:

  • Added a reward registry + per-reward option schemas, and a RewardConfig model that parses JSON/YAML/TOML and can be serialized safely into run metadata.
  • Updated the CLI parsing flow to resolve reward selection early, generating only the selected reward’s option flags and rejecting cross-reward flags automatically.
  • Implemented composable multi-reward guidance (CompositeReward) and a two-phase prepare() hook invoked by trajectory scalers.

Reviewed changes

Copilot reviewed 25 out of 25 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/utils/test_guidance_script_utils.py Updates tests to use load_guidance_structure() after reward building is decoupled.
tests/utils/test_guidance_script_arguments.py Adds tests for GuidanceConfig reward-config reconciliation, legacy pickle migration, and safe metadata serialization.
tests/rewards/test_reward_registry.py Contract tests for the reward registry and option schema coercion.
tests/rewards/test_reward_function_contract.py Documents/enforces the “rewards are minimized” sign convention in contract tests.
tests/rewards/test_reward_config.py Adds parsing/validation/weighting tests for reward config files across formats.
tests/rewards/test_reward_build_integration.py GPU-marked end-to-end tests from CLI argv → built reward → prepare() → scoring.
tests/rewards/test_prepare_hook.py Unit tests for the PreparableRewardFunctionProtocol and helper.
tests/rewards/test_composite.py Tests weighted reward composition, gradients, validation, and builder integration.
tests/integration/test_pipeline_integration.py Ensures preparable rewards are prepared before first call in both trajectory scalers.
tests/cli/test_guidance_cli.py Adds CLI coverage for reward selection, config-file composition, and flag rejection behavior.
src/sampleworks/utils/guidance_script_utils.py Splits structure loading from reward building; builds rewards via RewardConfig + registry.
src/sampleworks/utils/guidance_script_arguments.py Adds --reward-type / --reward-config, generates per-reward flags from schemas, and reconciles legacy density fields.
src/sampleworks/utils/guidance_constants.py Extends Rewards enum with STRUCTURE_FACTOR.
src/sampleworks/eval/structure_utils.py Introduces reward_atom_array to consistently choose model-vs-structure atom topology for rewards.
src/sampleworks/core/scalers/pure_guidance.py Calls prepare_reward_if_needed() once topology is known.
src/sampleworks/core/scalers/fk_steering.py Calls prepare_reward_if_needed() once topology is known.
src/sampleworks/core/rewards/structure_factor.py Adds a registry builder entrypoint for structure-factor reward construction.
src/sampleworks/core/rewards/registry.py Implements reward registry, lazy builder import, and option coercion.
src/sampleworks/core/rewards/real_space_density.py Adds a registry builder entrypoint for density reward construction.
src/sampleworks/core/rewards/protocol.py Adds sign-convention docs and the preparable reward protocol + helper.
src/sampleworks/core/rewards/options.py Defines frozen dataclass option schemas used by CLI/config/metadata.
src/sampleworks/core/rewards/config.py Implements RewardConfig parsing/validation, defaults materialization, path remapping, and reward building.
src/sampleworks/core/rewards/composite.py Implements CompositeReward for weighted combinations and preparation forwarding.
README.md Documents reward selection and config-file composition in user-facing docs.
AGENTS.md Updates agent guidance for reward selection, config files, and how to add new reward types.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +185 to +192
weight = entry.get(WEIGHT_KEY)
entries.append(
RewardEntry(
reward=reward,
weight=None if weight is None else float(weight),
options=dict(entry.get(REWARD_OPTIONS_KEY) or {}),
)
)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. reward_options is checked for mapping-ness and raises a ValueError naming the reward and the expected shape, so a typo in a config file reads as a usage error instead of a TypeError traceback out of dict().

The reward was never actually called, so 'no calls before prepare' held
trivially. Uses a real DPS step scaler, counts the calls, and drives a
mismatch case where the model has four atoms and the structure five, so
preparing against the wrong array fails the test.

Both from CodeRabbit on diff-use#373.
Reward arguments lived in add_generic_args as if density were the only
reward we would ever have, and construction was hardwired to
RealSpaceRewardFunction. Each reward now declares its options as a frozen
dataclass and registers a lazily-imported builder, so a new reward is a
schema plus one registry entry.

Builders sit next to their rewards and raise their own missing-input errors,
naming both the flag and the config key. CLI flags derive from option names,
so flag, config key and schema cannot drift apart.

get_reward_function_and_structure splits at its seam: loading the structure
is reward-agnostic and becomes load_guidance_structure, the rest is the
density builder.

Adds Rewards.STRUCTURE_FACTOR, which diff-use#324 never got.
RewardConfig reads the {reward: {weight, reward_options}} mapping from diff-use#358,
as JSON, YAML or TOML. YAML goes through OmegaConf, already a dependency, so
${oc.env:VAR} works the way it does in the run presets.

Weights are 1/N when none are given and verbatim when all are. Giving only
some is an error: a default quietly disagreeing with a number someone typed
is worse than a complaint.

with_experimental_data lets grid search drop in a per-protein map or MTZ
without knowing which reward it is filling.
build_reward turns a configuration into the reward a run scores against. A
single reward at full weight comes back as itself, so current runs keep the
gradients they have today. Anything else becomes a weighted sum.

Weights default to 1/N rather than 1, so adding a term does not quietly
scale the gradient up and change what the step size means. Negative weights
are rejected: against a minimized objective they flip a term instead of
damping it.

prepare() forwards to whichever terms need it.
…ard_options

option_type stripped None from any hint with type args, so a bare list[str]
came back as str and its CLI flag would have lost nargs. Only unions are
unwrapped now.

reward_options holding a list reached dict() and raised TypeError, which the
CLI does not catch, so a typo in a config file printed a traceback. It is a
ValueError naming the reward now.

Both from Copilot on diff-use#374 and diff-use#375.
…d-config

--reward-type picks a reward and brings that reward's flags with it,
generated from its schema. --reward-config takes the same configuration from
a file and is the only way to combine rewards. Both produce one RewardConfig.

The reward is resolved in the existing first parse pass, beside --model,
because it decides which flags exist. Registering only the selected reward's
options gets us cross-reward rejection for free, the way a Boltz flag is
already rejected on a Protenix run.

The default is real_space_density and its flags keep their spellings, so
existing command lines, presets and CLI tests are untouched. GuidanceConfig
keeps the flat density fields in step with the configuration both ways: grid
search and old pickles build from the flat fields, the eval scripts read
density and resolution back out of job_metadata.json.

reward_config serializes as a JSON string. as_dict() also becomes a CIF
category, and add_category_to_cif reads any non-string iterable as a column
of rows.

A missing required input is a usage error, raised before a model loads.
--density is no longer a fact about every run, so the README and AGENTS.md
now say which options belong to which reward. Structure factors move to
implemented in the data-types list.
Covers argv, configuration, build, prepare, score. Every piece of that had
tests; the seams between them did not, which is how the structure-factor
reward merged without being runnable.
Its two halves are load_guidance_structure and the density builder now, and
nothing calls it.
--help read "--mtzfile REWARD_OPTION_MTZFILE". Options with choices keep
showing their choices.
Only the space-separated spelling was caught.
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.

3 participants