Skip to content

Support smallest and largest subunit mode for RLEs - #665

Open
theferrit32 wants to merge 10 commits into
mainfrom
kf/637-rle-subunit-mode
Open

theferrit32 wants to merge 10 commits into
mainfrom
kf/637-rle-subunit-mode

Conversation

@theferrit32

Copy link
Copy Markdown
Contributor

Closes #637.

Adds RleSubunitMode to choose which valid factor normalization picks as the repeatSubunitLength of a reference-derived ambiguous insertion:

  • LARGEST: VRS 2.0.x behavior, and the default (DEFAULT_RLE_SUBUNIT_MODE), so existing digests are unchanged.
  • SMALLEST: VRS 2.1 behavior (ga4gh/vrs#700).

The mode can be set through normalize(..., rle_subunit_mode=...), on the AlleleTranslator constructor, or per call to translate_from. Only reference-derived insertions are affected; other normalization paths ignore the mode.

The vrs/2.1 branch will make SMALLEST the default, in line with vrs spec v2.1.

vrs-annotate doesn't expose an option to switch between largest/smalleset, and only uses the default. This is intentional.

For a reference-derived ambiguous insertion, more than one factor of the seed
length may be circularly expandable to recreate the alternate sequence. VRS
<= 2.0 selects the greatest such factor as the repeatSubunitLength; ga4gh/vrs#700
changes this to the smallest.

Add RleSubunitMode.{LARGEST,SMALLEST} and thread it through _normalize_allele(),
normalize() and the translator (instance default plus per-call kwarg, mirroring
rle_seq_limit). _factor_gen() now yields factors ascending or descending per the
mode, so the first valid factor found is the selected one.

repeatSubunitLength is inherent to the ReferenceLengthExpression digest, so the
modes can yield different computed identifiers for one input Allele. LARGEST
remains the default; behavior is unchanged unless the mode is passed explicitly.

Only step 6.c (reference-derived ambiguous insertion) is affected.
Reference-agree alleles, substitutions and deletions take repeatSubunitLength
from the seed length directly and are mode-invariant.

Refs #637
Declarative table of 11 alleles asserted under both RleSubunitMode values, using
the local seqrepo dataproxy so no cassettes are involved.

Differing cases show LARGEST's repeatSubunitLength tracking the insertion size
rather than the repeat unit: A x 9 -> A x 11 gives 2 vs 1, A x 9 -> A x 18 gives
9 vs 1, and a GT microsatellite insertion of 2 and 3 units gives 4 and 6 vs 2.
A x 9 -> A x 20 covers the prime-seed fall-through where LARGEST already yields 1.

Mode-invariant cases pin the branches the spec change does not touch:
reference-agree, substitution, deletion, a short tandem duplication, and a CAG
insertion whose seed length exceeds the modified reference length.

Also assert the default mode still equals LARGEST, and that each RLE state
denormalizes back to its literal sequence under either mode, so translate_to
output is unaffected.

Refs #637
Explicitly declare the 14 pre-existing corpus alleles that normalize to a
ReferenceLengthExpression, along with the RLE parameters and the literal
alternate sequence each reconstructs to, and assert the full round trip
input -> RLE -> denormalize_reference_length_expression() -> alt.

The table supplies the alt sequences the *_normalized dicts omit: most of
those tests pass rle_seq_limit=0, so state.sequence is absent and only the
reconstructed length could be checked. allele_dict2 is excluded because its
indefinite Range positions give no single reference span to reconstruct from.
…agree

- Collapse the five accession constants onto the four sequences they actually
  name. _HOMOPOLYMER_AC and _MICROSAT_AC were the same chr1 accession, and the
  comments on two others named the wrong chromosome (chrX was labelled
  NC_000002.12, chr6 was labelled NC_000011.10).
- Rename homopolymer_* ids to poly_a_* and describe each locus at its case, so
  ids describe the behavior under test rather than the substrate.
- Correct the tandem_dup_gt and trinucleotide_ins_cag comments. Both claimed the
  modes agreed because larger factors were too long; in fact the deciding factor
  is that every competing candidate is rejected as an invalid cycle.
- Fix step numbers to match the 0-indexed spec list at
  vrs.ga4gh.org/en/stable/conventions/normalization.html: the factor search is
  5.c.1, returning via 5.d; reference-agree is 2.a, substitution 2.b, deletion
  5.b. Restores the numbering the corpus comments already used.
- Add an `agreement` field and test_normalize_rle_subunit_mode_agreement, which
  observes the step 5.c.1 candidates to assert *why* the modes agree, so a case
  drifting between categories fails instead of silently still passing.
…ings

Use one constant for all rle_subunit_mode defaults and describe the modes by VRS version.
@theferrit32 theferrit32 self-assigned this Oct 2, 2026
@theferrit32
theferrit32 requested review from a team as code owners October 2, 2026 21:52
_logger = logging.getLogger(__name__)


class RleSubunitMode(str, Enum):

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.

In the 2.1 branch, we'll only allow smallest, right?

This branch has not been deployed

No deployments
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.

Add support for switching RLE subunit between largest and smallest

2 participants