Score expressions are the single source of truth (3.1.0) - #291
Merged
Conversation
Three layers of "default lives in two places" collapsed to one DSL expression per layer. No code path silently runs a hardcoded Python formula while a user expression governs elsewhere. Layer 3 — Combined score: - Delete ``combined_score_mode`` enum and the three legacy modes. - ``VaccineConfig.combined_score_expr: str`` defaults to ``DEFAULT_COMBINED_SCORE_EXPR = "sqrt(n_alt_reads) * target_epitope_score"``. - ``__post_init__`` always pre-parses the expression; ``combined_score`` always evaluates the AST. No mode branch, no fallback. Layer 2 — Per-epitope total: - New ``CandidateEpitope.per_allele_scores`` (dict[allele -> score]) populated at predict time. ``epitope_score`` property sums it. - ``target_epitope_score`` / ``self_epitope_score`` now read ``e.epitope_score`` directly. No re-scoring inside VaccinePeptide. - ``_legacy_score_one`` deleted from production. Test-only oracle moved to ``tests/_legacy_score_reference.py`` for parity assertions. Layer 1 — Per-(peptide, allele) score: - Delete ``_default_score_node`` Python builder. - Two string templates (``DEFAULT_AFFINITY_SCORE_EXPR_TEMPLATE`` / ``DEFAULT_PERCENTILE_SCORE_EXPR_TEMPLATE``) formatted against the scalar thresholds in ``EpitopeConfig`` and parsed via the topiary DSL. ``build_score_node`` always parses a string. - ``predict_epitopes`` writes the DSL-computed score onto each CandidateEpitope's ``per_allele_scores`` so VaccinePeptide can read it back without recomputation. Verified byte-identical rank table on the HCC1395 smoke run vs 3.0.2; 830 tests pass. Breaking config change: YAML files setting ``vaccine_peptides.combined_score_mode`` no longer parse — switch to ``combined_score_expr`` with one of: - ``"target_epitope_score"`` (was ``epitope_only``) - ``"n_alt_reads * target_epitope_score"`` (was ``reads_times_epitope``) - ``"sqrt(n_alt_reads) * target_epitope_score"`` (default, was ``sqrt_reads_times_epitope``)
This was referenced May 13, 2026
iskandr
added a commit
that referenced
this pull request
May 13, 2026
The 3.1.0 single-mechanism refactor (#291) moved per-(peptide, allele) scoring to the DSL and stored the result on ``CandidateEpitope.per_allele_scores``. ``predict_epitopes`` was updated to populate it; the external-input loaders (``load_lens`` / ``load_pvacseq``) were not. Result: every external-loaded epitope had ``epitope_score == 0``, so ``target_epitope_score`` was 0, ``combined_score`` collapsed to 0, and ranking dropped every variant as "No epitopes for peptide." End users running ``vaxrank --input-pvacseq`` or ``--input-lens`` got empty reports. Caught with a real-world smoke against cAIrn's HCC1395 pVACseq TSV (317 rows in, 0 variants ranked before the fix; 23 ranked after). Fix: add ``attach_per_allele_scores(epitopes, cfg=None)`` to ``epitope_dsl`` — runs the same DSL pipeline the upstream path uses, on the loaded epitopes, and stamps each one's per_allele_scores. Call it at the end of both loaders. Default ``EpitopeConfig()`` so the loader path's default scoring matches pre-3.1 semantics exactly. Two regression tests pin both loaders. 832 tests pass, lint clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Collapses three nested layers of "default formula lives in two places" into a single mechanism — DSL string expressions — per layer. After this PR, there is no production code path that runs a hardcoded Python score formula while a user-supplied expression governs elsewhere.
Layer 3 — Combined score (vaccine peptide)
combined_score_modeenum (sqrt_reads_times_epitope/reads_times_epitope/epitope_only).VaccineConfig.combined_score_expr: strnow defaults toDEFAULT_COMBINED_SCORE_EXPR = \"sqrt(n_alt_reads) * target_epitope_score\"— the canonical legacy formula, expressed once as a DSL string.VaccinePeptide.__post_init__resolvesNoneto that default and pre-parses;combined_scorealways evaluates the AST. No mode branch, no Python fallback.Layer 2 — Per-epitope total
CandidateEpitope.per_allele_scores: dict[str, float]populated at predict time.epitope_scoreis a property summing it.VaccinePeptide.target_epitope_score/self_epitope_scorenow reade.epitope_scoredirectly — no recomputation from raw IC50._legacy_score_oneis removed from production. A test-only oracle lives attests/_legacy_score_reference.pyand is only consumed by parity assertions.Layer 1 — Per-(peptide, allele) score
_default_score_node(the imperative DSLNode builder).DEFAULT_AFFINITY_SCORE_EXPR_TEMPLATE/DEFAULT_PERCENTILE_SCORE_EXPR_TEMPLATE— string templates thatdefault_score_expr(cfg)formats againstEpitopeConfig's scalar threshold fields.build_score_node(cfg)always parses a string (user expression or formatted default). Default and override travel the same code path.Breaking config change
YAML files setting
vaccine_peptides.combined_score_modeno longer parse. Migration:combined_score_exprepitope_only\"target_epitope_score\"reads_times_epitope\"n_alt_reads * target_epitope_score\"sqrt_reads_times_epitope\"sqrt(n_alt_reads) * target_epitope_score\"(this is the new default — omit to use it)Verification
test_default_score_matches_legacy_*pass against the new templated default (formula is mathematically identical).Test plan
combined_score_modein their YAML🤖 Generated with Claude Code