Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ mhctools>=3.44.64,<4.0.0
# and nullable integer/boolean cells. Public occurrence/reconciliation and
# additive rescoring APIs are consumed together; 5.93.2 fixes that replay.
# 5.93.3 accepts Osteosarc 0.15 alongside 0.14, enabling Isovar 1.45.
topiary>=5.93.3,<6.0.0
# 5.93.6 preserves saved DSL grouping, negative transform bounds and scalar
# literal compatibility (openvax/topiary#473, #475, #481; vaxrank#582).
topiary>=5.93.6,<6.0.0
roman
jinja2>=3.1
weasyprint>=62.0
Expand Down
47 changes: 47 additions & 0 deletions tests/test_selection_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import pytest
from topiary import replay_selection_policy
from topiary import read_tsv
from topiary.ranking import Column

from vaxrank.config.loader import (
load_vaxrank_config, extract_epitope_config_kwargs, resolve_config_path,
Expand Down Expand Up @@ -48,6 +49,52 @@ def test_frozen_policy_matches_legacy_scores_and_replays(tmp_path):
pd.testing.assert_frame_equal(evaluation.occurrences, restored.occurrences)


@pytest.mark.parametrize('renderer', ['repr', 'to_expr_string'])
@pytest.mark.parametrize('expression, measurements, threshold, expected_scores, eligible', [
pytest.param(
Column('review_score') - (Column('penalty') - Column('credit')),
dict(review_score=[10., 8.], penalty=[3., 3.], credit=[1., 1.]),
7., [8., 6.], [True, False], id='grouped-subtraction'),
pytest.param(
Column('signed_score').clip(-1, 1), dict(signed_score=[-2., 0., 2.]),
-1., [-1., 0., 1.], [True, True, True], id='negative-clip-bound'),
])
def test_rendered_policy_yaml_keeps_scores_selection_and_evidence_replay(
tmp_path, renderer, expression, measurements, threshold, expected_scores, eligible):
"""Saved policies must preserve arithmetic and signed transform parameters (#582)."""
def render(node):
return repr(node) if renderer == 'repr' else node.to_expr_string()

policy = dict(
name='saved-expression', score_by=render(expression), min_score=None,
criteria=[dict(name='accepted', expression=render(expression >= threshold),
role='eligibility')], filter_by='criterion("accepted")')
path = tmp_path / 'policy.yaml'
path.write_bytes(msgspec.yaml.encode({'epitopes': {'selection_policy': policy}}))
cfg = msgspec.convert(extract_epitope_config_kwargs(
load_vaxrank_config(config_path=str(path))), EpitopeConfig)
frame = _predictions_df([
dict(peptide='SIINFEKL', allele='HLA-A*02:01', peptide_offset=i, value=50.,
**{name: values[i] for name, values in measurements.items()})
for i in range(len(expected_scores))])

scores = score_predictions([], cfg, topiary_df=frame)
assert scores.tolist() == [score for score, keep in zip(expected_scores, eligible) if keep]
evaluation = scores.attrs['policy_evaluation']
assert evaluation.occurrences.eligible.tolist() == eligible
# Topiary evaluates scores after eligibility; rejected occurrences stay
# in the audit with a missing score rather than an evaluated value.
assert evaluation.occurrences.loc[eligible, 'score'].tolist() == scores.tolist()
assert evaluation.occurrences.loc[[not keep for keep in eligible], 'score'].isna().all()
assert len(evaluation.evidence.df) == len(frame)
decoded, = decode_evaluations(encode_evaluations([evaluation]))
pd.testing.assert_frame_equal(evaluation.occurrences, decoded.occurrences)
record, = save_policy_evaluations([evaluation], tmp_path / 'evidence')
restored = replay_selection_policy(read_tsv(tmp_path / 'evidence' / record['evidence']))
assert restored.policy.sha256 == evaluation.policy.sha256
pd.testing.assert_frame_equal(evaluation.occurrences, restored.occurrences)


def test_policy_overlay_preserves_explicit_null_and_cli_minimum(tmp_path):
overlay = tmp_path / 'custom.yaml'
overlay.write_text('name: custom\nepitopes:\n selection_policy:\n name: custom\n min_score: null\n')
Expand Down
2 changes: 1 addition & 1 deletion vaxrank/version.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = "3.38.2"
__version__ = "3.38.3"


def print_version():
Expand Down
Loading