pax_global_header00006660000000000000000000000064152660267450014527gustar00rootroot0000000000000052 comment=d96eb065149b6a32d704786864776b415ab8e559 openvax-varcode-d96eb06/000077500000000000000000000000001526602674500152515ustar00rootroot00000000000000openvax-varcode-d96eb06/.gitattributes000066400000000000000000000000411526602674500201370ustar00rootroot00000000000000varcode/_version.py export-subst openvax-varcode-d96eb06/.github/000077500000000000000000000000001526602674500166115ustar00rootroot00000000000000openvax-varcode-d96eb06/.github/workflows/000077500000000000000000000000001526602674500206465ustar00rootroot00000000000000openvax-varcode-d96eb06/.github/workflows/docs.yml000066400000000000000000000035001526602674500223170ustar00rootroot00000000000000# Build the mkdocs site on every push/PR; deploy to GitHub Pages when # main is updated. Matches the pattern used by openvax/vaxrank. # # For the deploy to work, the repository's Pages source must be set to # "GitHub Actions" under Settings → Pages. The build job runs on PRs # so broken docs fail the check before merge. name: Docs on: push: branches: [main] pull_request: branches: [main] # Cancel in-flight runs on the same ref when a newer push arrives. concurrency: group: docs-${{ github.ref }} cancel-in-progress: true jobs: build: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v6 with: fetch-depth: 0 # mkdocs plugins can look at git history - name: Set up Python uses: actions/setup-python@v6 with: python-version: "3.11" cache: pip - name: Install varcode and docs dependencies run: | python -m pip install --upgrade pip # mkdocstrings needs varcode importable, but doesn't need # the Ensembl FASTA caches that the test workflow downloads. pip install -r requirements.txt pip install . pip install mkdocs mkdocs-material 'mkdocstrings[python]' - name: Build docs run: mkdocs build --strict - name: Upload site artifact if: github.ref == 'refs/heads/main' uses: actions/upload-pages-artifact@v5.0.0 with: path: site/ deploy: needs: build if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest permissions: pages: write id-token: write environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} steps: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v5 openvax-varcode-d96eb06/.github/workflows/tests.yml000066400000000000000000000141561526602674500225420ustar00rootroot00000000000000# This workflow will install Python dependencies, run tests and lint with a variety of Python versions # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python # TODO: # - update coveralls # - get a badge for tests passing # - download binary dependencies from conda name: Tests on: push: branches: [main] pull_request: concurrency: # Supersede only this PR's matrix. Unique run IDs keep every main push. group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.run_id }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: build: runs-on: ubuntu-latest strategy: fail-fast: true matrix: include: - python-version: "3.11" pandas: "pandas>=2.2.2,<3" gtfparse: "gtfparse>=3.0.2,<4" - python-version: "3.11" pandas: "pandas>=3,<4" gtfparse: "gtfparse>=3.0.2,<4" - python-version: "3.12" pandas: "pandas>=3,<4" gtfparse: "gtfparse>=3.0.2,<4" - python-version: "3.13" pandas: "pandas>=3,<4" gtfparse: "gtfparse>=3.0.2,<4" - python-version: "3.14" pandas: "pandas>=3,<4" gtfparse: "gtfparse>=3.0.2,<4" steps: - name: Checkout repository uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} cache: pip cache-dependency-path: requirements.txt - name: Install dependencies env: PANDAS_REQUIREMENT: ${{ matrix.pandas }} GTFPARSE_REQUIREMENT: ${{ matrix.gtfparse }} run: | python -m pip install --upgrade pip python -m pip install flake8 pytest pytest-cov coveralls ruff build 'setuptools>=64' python -m pip install -r requirements.txt "$PANDAS_REQUIREMENT" "$GTFPARSE_REQUIREMENT" - name: Verify, build and install the source distribution run: | python -m pytest -q tests/test_source_distribution.py python -m build --sdist --outdir "$RUNNER_TEMP/varcode-dist" mkdir "$RUNNER_TEMP/varcode-sdist" tar -xzf "$RUNNER_TEMP"/varcode-dist/varcode-*.tar.gz --strip-components=1 -C "$RUNNER_TEMP/varcode-sdist" # The supported Osteosarc range covers this Python matrix; # run native adapter checks against the bundled historical snapshot. python -m pip install "$RUNNER_TEMP/varcode-sdist[test-data,uorf]" python -m pip check python -m pip show pandas pyensembl gtfparse - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Run default linting script run: | ./lint.sh # Cache the pyensembl downloads. The key encodes: # - the exact release tags below (auto-invalidates if a # release is added/changed) # - the requirements.txt hash (auto-invalidates if pyensembl # is bumped to a version with a different on-disk layout) # Bump the trailing -vN suffix to force a refresh for other # reasons. - name: Cache pyensembl releases id: pyensembl-cache uses: actions/cache@v4 with: path: ~/.cache/pyensembl # PyEnsembl 2.27.5 keys downloads and indexes by their source URL. key: pyensembl-${{ runner.os }}-grch37.75-grch38.81-grch38.93-grch38.95-grcm38.95-canfam3.99-roscfam.2020_10-${{ hashFiles('requirements.txt') }}-v3 # Reuse unchanged releases when adding the current dog annotation. # Keep dependency and source-layout identities identical. restore-keys: | pyensembl-${{ runner.os }}-grch37.75-grch38.81-grch38.93-grch38.95-grcm38.95-canfam3.99-${{ hashFiles('requirements.txt') }}-v3 pyensembl-${{ runner.os }}-grch37.75-grch38.81-grch38.95-grcm38.95-${{ hashFiles('requirements.txt') }}-v3 - name: Install Ensembl data run: | echo "Before installing Ensembl releases" && df -h # pyensembl install is idempotent — when the cache hit # restores the data, these commands just verify and exit. # Use the same canonical sources as EnsemblRelease in the tests. # Mirror URLs have distinct cache identities (openvax/pyensembl#470). pyensembl install --release 75 --species human pyensembl install --release 81 --species human pyensembl install --release 93 --species human pyensembl install --release 95 --species human pyensembl install --release 95 --species mouse pyensembl install --release 99 --species dog pyensembl install --release 2020_10 --species dog echo "After installing Ensembl releases" && df -h - name: Run unit tests from the source distribution working-directory: ${{ runner.temp }}/varcode-sdist run: | ./test.sh --cov-report=xml:coverage.xml # Experimental annotators may decline inputs. Compare protein_diff on # the shared point-edit domain; the full suite above separately checks # SV refusal, partial plugins, and explicit dual-annotator regressions. - name: Cross-annotator parity gate (shared point-edit domain) if: matrix.python-version == '3.11' && matrix.pandas == 'pandas>=3,<4' working-directory: ${{ runner.temp }}/varcode-sdist env: COVERAGE_FILE: .coverage.parity run: | ./test.sh --annotator=protein_diff -m parity - name: Publish coverage to Coveralls uses: coverallsapp/github-action@v2.3.6 with: parallel: true file: ${{ runner.temp }}/varcode-sdist/coverage.xml base-path: ${{ runner.temp }}/varcode-sdist openvax-varcode-d96eb06/.gitignore000066400000000000000000000013231526602674500172400ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .cache nosetests.xml coverage.xml # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation docs/_build/ # mkdocs build output /site/ # PyBuilder target/ # PyCharm .idea openvax-varcode-d96eb06/AGENTS.md000066400000000000000000000070331526602674500165570ustar00rootroot00000000000000# AGENTS.md — varcode Guide for coding agents working in this repo. Read this before touching code. --- ## Golden Rules 1. **Never commit to `main`.** Always `git checkout -b ` before editing. Land via PR. 2. **Every PR bumps the version.** Even doc-only PRs — at minimum a patch bump in `varcode/version.py` with release notes. Include the bump in the PR; `deploy.sh [version]` publishes the merged version and checks any supplied version matches. 3. **"Done" means merged AND deployed to PyPI** — never stop at merge. After a PR merges, run `./deploy.sh` from a clean main. Skipping deploy = task not done. 4. **File problems as issues, don't silently work around them.** If you hit a bug here or in a sibling openvax/pirl-unc repo, open a GitHub issue on the correct repo and link it from the PR. 5. **After a PR ships, look for the next block of work.** Read open issues across the relevant openvax repos, group by dependency + urgency. Prefer *foundational* changes that unblock multiple downstream improvements; otherwise chain the smallest independent improvements. --- ## Before Completing Any Task Before telling the user a change is "complete": 1. **`./lint.sh`** — must pass 2. **`./test.sh`** — must pass 3. For a PR: **CI must be green on GitHub**, then merge, then **`./deploy.sh`**. `deploy.sh` gates on lint + test, refuses to run off `main`/`master`, and refuses a dirty tree — don't work around these. If deploy fails, fix the root cause. ## Scripts - `./develop.sh` — editable install (dev mode) - `./lint.sh` — ruff check - `./test.sh` — pytest (with coverage where configured) - `./deploy.sh [--dry-run] [version]` — clean synchronized main/master + unused-tag checks → lint → test → build → twine check → upload → tag → push. Dry runs stop before upload; version changes always go through a PR. ## Code Style - Python 3.11+ - Lint: ruff (config in `pyproject.toml`) - Docstrings: numpy style - Bugfixes include a regression test where feasible - varcode sits under vaxrank/isovar/topiary — API changes are breaking. Keep backwards-compat where reasonable; coordinate a minor/major bump when not. --- ## Workflow Orchestration ### 1. Upfront Planning - For any non-trivial task (3+ steps or architectural): write a short spec first. If something goes sideways, STOP and re-plan — don't keep pushing. ### 2. Verification Before Done - Never claim complete without proof: tests green, CI green, PyPI version live. ### 3. Autonomous Bug Fixing - Given a bug report: just fix it. Point at logs/errors/failing tests and resolve them without hand-holding. ### 4. Demand Elegance (Balanced) - For non-trivial changes pause and ask "is there a more elegant way?" — skip for trivial fixes. - Treat workarounds as bugs, not new abstractions. Rip out legacy paths decisively rather than accumulating special cases. ### 5. Issue Triage After Each Ship - Close superseded/outdated issues as you notice them. - New problems mid-task → file as issues (on the right repo, even if it's not this one), don't bury. --- ## Core Principles - **Simplicity first.** Minimal diffs, minimal abstractions. - **No laziness.** Find root causes; no temporary fixes, no empty-category fudges. - **Minimal blast radius.** Touch only what the task requires. ## Scientific Domain Knowledge - If a change touches immunology/genomics semantics, check primary sources (papers, UniProt, GenBank) before edits. - If the code expresses a scientific model at odds with your understanding, flag it — don't silently "fix" it into something wrong. openvax-varcode-d96eb06/CHANGELOG.md000066400000000000000000003573761526602674500171070ustar00rootroot00000000000000# Change Log ## v10.18.3 - Accept concrete annotation dates in reference strings, including `GRCh38:2026_04`, `GRCh37:2013_09` and `ROS_Cfam_1.0:2020_10`, in the API and CLI. Resolve and validate assemblies through PyEnsembl's shared selector, retaining historical cache identities and numbered releases (#616). - Reject malformed explicit dates instead of silently selecting another release. Require PyEnsembl 2.30 or later for assembly-aware selection. - Retain explicit numbered and dated UCSC releases when loading VCFs, and use assembly metadata for historical UCSC aliases and scaffolds (#622). ## v10.18.2 - Add native ROS_Cfam_1.0 canine lymphoma variants and a matched-normal, ddPCR-validated melanoma SPTBN1 frameshift. Pin the current Ensembl dog annotation (2020_10), source publications and genomic reference windows; compare published coding positions and independent peptide translation (#617). - Preserve the older CanFam3.1 benchmark and distinguish below-threshold candidates from stronger somatic evidence. Document current dog reference selection without converting or relabeling legacy variant coordinates. - Declare assembly identities in synthetic ORF test fixtures and test assembly and annotation mismatches separately for current PyEnsembl compatibility (#619). ## v10.18.1 - Bundle complete pinned HCC1395/COLO829 SEQC2, NYGC and Lancet2 small-variant benchmarks and published structural-variant sets, with source checksums, reproducible extraction and full loading/annotation checks in CI (#614). - Validate the complete published canine osteosarcoma table against CanFam3.1 / Ensembl 99. Independently check 1,954 protein changes, a stop-loss and a splice consequence; retain an uncertain initiation outcome and 184 records affected by the existing partial-transcript/biotype limitation (#195). - Install the additional pinned human and dog reference releases in CI and document local provisioning. Public fixtures travel with source releases. ## v10.18.0 - Use configured genome DNA automatically for noncanonical ORF intronic, splice and local SV consequences, preserving explicit sequence providers and validated transcript exons (#610). - Select genetic codes from explicit source models or offline NCBI species/compartment assignments. Support unambiguous NCBI codes including yeast and invertebrate mitochondria; reject unknown assignments and context-dependent stop codes explicitly (#611). - Add `--genome-fasta` and `--extra-orfs` CLI options with input validation and a single documented reference setup path. - Round-trip extra-ORF effects, reference RNA snapshots, source evidence, failed bindings and all alternative products in versioned JSON graphs. ORF-containing CSVs carry full effect archives beside summary columns, preventing reconstruction against the canonical CDS (#593). - Require ORFdata 0.34 or later for optional ORF functionality. ## v10.17.3 - Read reference ranges from contiguous exon slices and reject gaps in combined transcript coverage before scanning bases. This avoids repeated coordinate mapping during structural variant annotation without a chromosome FASTA (#407), preserving ranges covered jointly by several transcripts, transcript precedence, strand handling, and the existing adapter fallback. - Avoid loading transcript FASTA for ranges whose endpoint offsets already establish an intervening intron. ## v10.17.2 - Require Python 3.11 or later and test Python 3.11–3.14 in CI. Continue testing pandas 2 and 3 on Python 3.11; Python 3.12–3.14 use pandas 3. ## v10.17.1 - Cancel superseded Tests workflow runs for the same pull request (#604). Independent PRs and every main push retain separate test runs. ## v10.17.0 - Preserve complete `SpliceOutcomeSet` objects during top-priority selection, including `--one-per-variant`, and list every current candidate in summaries and CSV descriptions (#436). Exonic sets retain the ranking of their coding effect conditional on normal splicing. Call `effect_if_splicing_unchanged` explicitly when that single conditional consequence is wanted. - Add `splice_signal` to effect tables containing splice sets so the affected signal is reported separately from the container's `effect_type`. - Skip reference-identical VCF ALT alleles with a counted warning, preserving original allele indices and excluding these entries from `max_variants`. - Describe single-base deletions without a repeated coordinate, and explicit no-change variants with `=` over the original interval. - Install CI annotation data from the canonical sources used by the tests, matching PyEnsembl 2.27.5's source-specific cache identities (openvax/pyensembl#470). - Check optional ATM test data availability without downloading missing annotation data during test collection (#605). ## v10.16.4 - Show genomic forward-strand alleles and the checked interval in reference mismatch errors, including minus-strand transcripts and ORF annotations (#434, #601). Preserve the existing transcript-oriented error fields and expose explicit `genomic_expected_ref` and `genomic_observed_ref` fields. - Report CLI reference mismatches on one stderr line with a `--skip-errors` hint; retained failure rows carry the same corrected diagnostic. ## v10.16.3 - Report CLI input and output path mistakes on one stderr line with exit status 1. Validate `--variant` positions before annotation, including zero and negative coordinates (#431, #599). Keep annotation errors and retained `Failure` effects on their existing paths. ## v10.16.2 - Remove unused lint, coverage, and changelog-generator configuration files and their stale source-distribution references (#410, #600). The contributing guide already states the current Python minimum. ## v10.16.1 - Refresh annotator, fast-path, mutant-transcript, and germline documentation to describe the current implementation. Restore the fast-path regression test's comparison with the full effect pipeline (#409). ## v10.16.0 - `extra_orfs="all"`, an evidence name (`"ribosome"`, `"ms"`, `"proteomics"` or `"immunopeptidomics"`), a list of names for any of them, or an ORFdata rule such as `RiboSeq() & MS()` in `Variant.effects()` and `VariantCollection.effects()` uses ORFdata's ORFs for each variant's own genome. `True` raises, naming these choices, rather than guessing which ORFs are meant. ORFdata picks its species and assembly's catalogues, lifting them from another assembly when needed, so GRCh37, NCBI36, T2T-CHM13 and mouse (GRCm39, GRCm38) work as GRCh38 does. `False` means none. The `orf` extra now needs `orfdata>=0.28.0`. - Placements are made once per genome and reused: the last four genomes, and `"ms"` and `MS()` share them, as do one annotation's genomes however wrapped. `varcode.extra_orfs.clear_extra_orfs_cache()` drops them. Placements are indexed, so a variant adds about 2 ms on a warm cache, not a scan of every placement. - ORFs ORFdata could not place on the genome (an older annotation may lack their transcript) are left out, never an error. `varcode.extra_orfs.prepare_extra_orfs("all").excluded(genome)` lists them. An explicit ORFdata collection still reports them, now naming the ORF. - A bad `extra_orfs` value raises before any variant is annotated. - Pickled and copied effects keep their `orf_annotation`. - Each annotation carries the catalogue's identity and provenance, not its per-record lists, so a mouse annotation's JSON is about 90 KB, not 900 KB. ## v10.15.2 (2026-10-15) - Preserve source-declared non-AUG initiation in extra ORF variant, haplotype, splice and mapped RNA-product translation. Downstream variants in GTG/ACG ORFs now retain the reference protein and receive coding consequences instead of false Silent effects with empty proteins (#597). - Keep initiation specific to the source coding model, retain ordinary internal codon translation, and preserve conditional-translation evidence and explicit start-change/loss behavior. ## v10.15.1 (2026-10-14) - Preserve transcript-model candidates, hypotheses, realized baseline/mutant products, evidence and ranking in versioned effect JSON graphs, including shared references back to their owning effects (#574). - Convert UCSC accession-bearing unplaced/random/alternate contig names while preserving accession versions. Batch annotation and both CLIs retain missing contigs as explicit failures with one summary warning; collection `raise_on_error=True` remains strict, and unrelated errors still raise by default (#435). ## v10.15.0 (2026-10-13) - Accept `extra_orfs=` in `Variant.effects()` and `VariantCollection.effects()` for additive catalogue or bound-reference ORF consequences (#592). Preserve ordinary effects and expose each added effect's `orf`, `reference_transcript` and `orf_annotation`, with native outcomes in `collection.orf_annotations`. - Reuse the ORF transcript-model engine for germline hypotheses and known-cis collection products. Preserve input scope, generator inputs and catalogue binding failures; never attach a canonical observed protein to an extra frame. - Preserve original RNA IDs for grouping and expression lookup, and namespace internal ORF identities across sources. Label ORF dataframe/CSV rows and prevent generic serialization from silently losing their coding models (#593). ## v10.14.1 (2026-10-13) - Reorganize the README and user guides, move maintainer audits to `notes/`, and repair their links to the CSV/VCF and germline documentation. ## v10.14.0 (2026-10-13) - Expose native `ORFAnnotation.outcomes`, with `ORFOutcome` effects/hypotheses and baseline/mutant `ORFProduct` objects. Read sequences and initiation contexts through attributes without serializing the annotation (#589). - Add `annotate_orf_rna_product` returning an `ORFProductAnnotation`. Preserve `annotate_orf_product` dictionary returns for existing RNA JSON exporters. - Preserve annotation JSON exports and the `.data` export alias. Direct `ORFAnnotation` construction now accepts native fields instead of a serialized `data` dictionary. Examples explicitly select an ORF ID or iterate over ORFs. ## v10.13.0 (2026-10-12) - Use ORFdata's reference-context validation for RNA product overlays, accepting valid dog aliases while rejecting incompatible taxa, strains and annotations (#586). Retain reference identities and sequence fingerprints in exports. - Accept explicit contexts for custom Genome bindings; validate versioned source transcript IDs after lookup and reject ambiguous supplied transcript IDs. - Require ORFdata 0.2 in the optional ORF/uORF extras. - Preserve a structured unresolved annotation when binding fails, instead of crashing while constructing a transcript effect without a transcript (#587). ## v10.12.0 (2026-10-11) - Add optional species-scoped orfdata reference evidence and additive `Variant.orf_effects()` annotations using validated upstream transcript frames, reference peptide retention and explicit phase/splice products (#583). Keep canonical effects. - Accept bound PyEnsembl ReferenceORFs with separate model and evidence objects, including ORFs on noncoding RNAs. Preserve all duplicated native peptide copies. - Annotate mapped RNA ORF products, including Sid TPST1–CRCP, without promoting native MS observations into mutant translation or presentation evidence. - Use nucleotide-frame provenance to distinguish coding frameshifts from stop-loss when protein prefixes coincide (#584). ## [v10.11.10](https://github.com/openvax/varcode/tree/v10.11.10) (2026-10-08) - Allow osteosarc 0.15.x in the optional `test-data` extra and the snapshot fixture's version check (`>=0.14.4,<0.16`), fixing five dataset tests that errored during setup with osteosarc 0.15.5 (#581). ## [v10.11.9](https://github.com/openvax/varcode/tree/v10.11.9) (2026-10-06) - Require gtfparse 3.0.2+ across the supported Python/pandas matrix, coordinated with Topiary 5.94.2's gtfparse 3 migration (openvax/topiary#484). ## [v10.11.8](https://github.com/openvax/varcode/tree/v10.11.8) (2026-10-06) - Require PyEnsembl 2.24.1+ and gtfparse 2.9.1+, with explicit coverage of gtfparse 3.0.2+. Support pandas 2.2.2+ and 3.x, retaining Python 3.9/3.10 and existing Topiary dependency compatibility. - Explicitly test pandas 2 and 3 on Python 3.11, plus pandas 3 on Python 3.12, including the installed source distribution and cross-annotator parity. ## [v10.11.7](https://github.com/openvax/varcode/tree/v10.11.7) (2026-10-06) - Cache RNA variant support counts under the variant key, keeping it separate from sequencing-template keys (#575). Verify count accuracy, reuse of zero and missing-contig results, haplotype cache invalidation, and caching on real Sid RNA templates without changing the allele or fragment filtering rules. ## [v10.11.6](https://github.com/openvax/varcode/tree/v10.11.6) (2026-10-05) - Exercise real Sid RNA evidence through `VariantCollection.effects()` for NTF3 compound codons and MAP2 complex haplotypes (#464). Pin independent NCBI coding/protein references, retain complete available sequencing templates, and verify individual predictions, phase uncertainty and joint effect provenance. Keep corrected, deletion-only and historical MAP2 alleles distinct; RNA support does not replace their DNA predictions. ## [v10.11.5](https://github.com/openvax/varcode/tree/v10.11.5) (2026-10-05) - Test RNA read phasing and molecular phase resolution on real Sid RNA templates from the verified `openvax-v2` bundle (#528). Cover short-read and ONT cis evidence, trans evidence, insufficient shared coverage and fragment thresholds; verify that mates and supplementary records are kept. ## [v10.11.4](https://github.com/openvax/varcode/tree/v10.11.4) (2026-10-02) - Ship test helpers, bounded fixtures, provenance/checksums and the docs, examples and scripts required to run tests from the source distribution (#509). Existing fixture payloads are unchanged; large shared RNA assets remain external. - Verify archive contents and fixture integrity, and run the CI test matrix and parity checks from an installed, unpacked source archive. ## [v10.11.3](https://github.com/openvax/varcode/tree/v10.11.3) (2026-10-01) - Guard deployment with a clean main/master checkout synchronized with live origin, matching version, unused tag, lint/tests and distribution checks (#414). Publish the merged version and push its tag only after upload; `--dry-run` validates and builds without publishing or changing Git history. - Preserve each attempt's release artifacts for publication verification and recovery. Keep version bumps in PRs and align the release instructions. ## [v10.11.2](https://github.com/openvax/varcode/tree/v10.11.2) (2026-10-01) - Pin bare GRCh38 references during pytest to the existing Ensembl 81 fixture (#493). Remove the installed-release scan that could download missing data while probing `EnsemblRelease.db`. Test reference selection leaves the cache untouched and preserves explicit release choices; library behavior is unchanged. ## [v10.11.1](https://github.com/openvax/varcode/tree/v10.11.1) (2026-09-30) - Compose mixed inherited/somatic cis groups in the experimental transcript model (#500). Inherited alleles anchor the patient baseline and are edited once; only novel alleles contribute new somatic edits. Repeated normalized alleles are deduplicated without losing the supplied group membership. - Resolve germline phase through any member of a known-cis group, preserving uncertainty for unlinked alleles and phase provenance on each hypothesis. ## [v10.11.0](https://github.com/openvax/varcode/tree/v10.11.0) (2026-09-29) - `Genome(native_genome)` inherits optional native PyEnsembl reference DNA instead of hiding it (#488), enabling intronic/intergenic reference lookups. Construction and rewrapping remain lazy; explicit `fasta=` takes precedence. - Native `sequence()` preserves PyEnsembl's errors for uninstalled DNA and invalid intervals. Tiered reference lookups retain their cDNA fallback. Reads never download DNA. `close()` closes only explicit path readers opened by that wrapper, leaving native genomes and borrowed readers usable. - Preserve the identity of supplied genome objects during inference (#565). Equal annotation genomes can carry different DNA; caching them by equality could silently select the wrong reference file. ## [v10.10.0](https://github.com/openvax/varcode/tree/v10.10.0) (2026-09-29) - Add `reference_completion_hypotheses` for mapped Exacto and other RNA observations (#499). Explicitly requested hypotheses prepend/append compatible reference sequence, retaining the original observation and producer peptide. Alternative reference isoforms remain separate; evidence distinguishes observed bases, assumed spans and the original read support. - Completion respects known RNA ends and predicts from the retained annotated start using existing genetic-code/selenocysteine rules. Missing starts, ambiguous coding sequence and missing stops remain explicit. Completion does not establish RNA coverage, initiation, translation or expression. ## [v10.9.0](https://github.com/openvax/varcode/tree/v10.9.0) (2026-09-28) - Map Exacto's verified sense exonic runs to reference-transcript segments, retaining other bases as observed sequence and preserving producer ORFs and source rows (#499). Native mismatch and insertion flanks no longer cause valid linear imports to be rejected as overlapping paths (#558). - Partial structural observations can establish local coding/protein changes across junctions and indels from an in-frame reference anchor (#466). Missing coverage alone remains unresolved. This adds sequence comparisons, without completing transcript ends or inferring initiation at new start codons. ## [v10.8.0](https://github.com/openvax/varcode/tree/v10.8.0) (2026-09-28) - A breakend junction with both ends in one gene is annotated, on that gene's transcripts, as the local DEL, DUP or INV its kept sides describe (#551). Before, those transcripts got `TranslocationToIntergenic` with a retained fragment, although the junction's other end is in the same gene. A transcript read across the junction carries exactly that local event, and it is what `pair_breakends` types a caller-labeled pair as. So a single record now annotates as its labeled pair does, while its effects still report the record, with `sv_type="BND"` and `junction_event` in their evidence. - Example: openvax-v2's ATP5MG--KMT2A junction is exactly intron 1 of Ensembl's ATP5MG-KMT2A readthrough transcript AP001267.5-202, which is now `Intronic`. AP001267.5-201 loses its 3' end, as the equivalent DEL gives, and the ATP5MG isoforms still fuse with KMT2A. - The local models read their span from a small event object, so the effects of a span record are unchanged. `structural_variant.junction_span_details` gives the DEL/DUP/INV span of a single junction. ## [v10.7.0](https://github.com/openvax/varcode/tree/v10.7.0) (2026-09-28) - A structural effect gains `GeneFusion` candidates for 3' partners that start just past a junction end, rather than spanning it (#550). When the annotated transcript keeps its 5' end and the junction continues into the 10 kb (`MAX_UPSTREAM_PARTNER_DISTANCE`) before a protein-coding transcript on the strand it reads, the fused cDNA joins that transcript's second exon, because its first has no splice acceptor. This is LINX's rule, with LINX's limit for fusions other than known pairs. Each model records `three_prime_breakend="upstream"` and `three_prime_upstream_distance`. - These fusions follow the primary classification and never replace it, since RNA need not splice that way. openvax-v2's GABBR1--SLC29A1 junction lands 597 bases before SLC29A1. There, Isovar's long reads run from GABBR1 intronic sequence into the intergenic stretch and end in poly(A) before SLC29A1. So that breakend stays `TranslocationToIntergenic`, whose priority is its own, with one candidate per SLC29A1 isoform. A DEL, DUP or INV gains the same candidates behind its local consequence. Its `most_likely_effect` is unchanged, but its priority follows its most disruptive candidate, as for any structural effect set, so it can rise to `GeneFusion`. ## [v10.6.3](https://github.com/openvax/varcode/tree/v10.6.3) (2026-09-28) - A directly constructed BND `StructuralVariant` takes `mate_contig`, `mate_start` and `mate_orientation` from its breakend ALT when they aren't passed, as `parse_symbolic_alt` and `load_vcf` already did (#553). Before, `StructuralVariant("5", 116474281, "BND", alt="[chr4:75037126[C")` had no junction, so SEMA6A's side of this fusion was reported as `TranslocationToIntergenic` with no warning. A passed mate field the ALT contradicts now raises `ValueError`; contigs compare by their Ensembl-style names, so `pair_breakends` can still pass a converted `mate_contig` with the caller's ALT text. ## [v10.6.2](https://github.com/openvax/varcode/tree/v10.6.2) (2026-09-28) - Test all seven fusion targets in `openvax-v2`, the OpenVax libraries' shared Sid test data (part of #528), in `tests/test_osteosarc_shared_fusions.py`. Each junction is built from the recipe's breakends; where only `orientation` is given, the kept side comes from the strand the fusion follows (iskandr/osteosarc#96). The five junctions esvee called match its records. ATP5MG--KMT2A and TPST1--CRCP fuse every coding isoform of the 5' gene, and ATP5MG--KMT2A's fusion protein is MAQFVRNLVEKTPALVNG, as Isovar translates from its junction reads. The other five have no sense-oriented coding partner and keep the 5' fragment. Filed #550 (a 3' breakend just upstream of the partner gene, as in GABBR1--SLC29A1) and #551 (a junction with both ends in one gene is labeled `TranslocationToIntergenic`). No library code changes. ## [v10.6.1](https://github.com/openvax/varcode/tree/v10.6.1) (2026-09-28) - Test against `openvax-v2`, the OpenVax libraries' current shared Sid test data (osteosarc 0.14.4), instead of `openvax-v1`. Varcode's three junction members and all 187 small-variant targets are identical in both, so no expectation changes. The `test-data` extra requires osteosarc `>=0.14.4,<0.15`, the first release that resolves `openvax-v2`, and the osteosarc test corpus was re-collected with 0.14.4; only its recorded version changed. ## [v10.6.0](https://github.com/openvax/varcode/tree/v10.6.0) (2026-09-27) - `RNAReadPhasingSource.in_cis` decides from which allele combinations are really present, not from the smaller mixed arm (#547). 10.5.16's rule could call a pair cis from read errors: with a variant that isn't expressed, two of 200 other-variant fragments showing its allele by error were cis. Each combination (both alts, either alone, neither) now counts only with at least `min_alt_reads` fragments and more than errors would leak into it, by a one-sided binomial test at the new `phasing_error_rate` (0.01) and `max_p_value_for_phasing` (0.05). The pair is cis when both alts are together and not each alone, trans when each is alone and not together, and unknown otherwise, including four-gamete tables. This is the four-gamete test, as in Isovar 1.39.7 (openvax/isovar#410); nested-subclone and germline pairs stay cis (#527). The rule is also available as `varcode.rna_read_phasing.four_gamete_phase`. ## [v10.5.17](https://github.com/openvax/varcode/tree/v10.5.17) (2026-09-27) - A deletion that contains a whole coding transcript is `ExonLoss` of every exon, whether or not the transcript is annotated complete (#541). A complete transcript used to come out as `StartLoss` (`p.M1?`), which implies the rest of the transcript remains, while an incomplete one was `ExonLoss`. On the public osteosarc SV0055 deletion (chr9:22510296-35428808, Ensembl 95), 185 of the 242 protein-coding transcripts wholly inside it were `StartLoss` and 57 `ExonLoss`; all are now `ExonLoss`. `ExonLoss` ranks above `StartLoss`, so such deletions now lead with it. Partial deletions are unchanged: losing the start codon while keeping later exons is still `StartLoss`. ## [v10.5.16](https://github.com/openvax/varcode/tree/v10.5.16) (2026-09-27) - `RNAReadPhasingSource.in_cis` no longer calls a cis pair trans when one variant is germline or arose first in a larger clone (#527). Trans evidence was the sum of reads showing either variant's alt allele with the other's reference, but a cis pair fills one of those combinations from cells carrying only the earlier (or germline) variant. Trans evidence is now the smaller of the two, following the infinite-sites argument (Nik-Zainal et al. 2012, *Cell* 149:994) that Isovar also uses (openvax/isovar#409). A pair with only one of those combinations seen is now unknown (`None`) instead of trans. ## [v10.5.15](https://github.com/openvax/varcode/tree/v10.5.15) (2026-09-27) - Splice outcomes no longer propose skipping a transcript's first or last exon (#543). Skipping joins the donor before an exon to the acceptor after it, so a terminal exon can't be skipped. For a donor variant in exon 1, or an acceptor variant in the last exon, intron retention now leads the candidates. Example: BRCA1's exon 1 donor (17:43125270 C>A) was headlined `splice-set:exon-skip`, also as the variant's top effect. CFTR's exon 1 donor and last-exon acceptor offered `p.M1fs` and `p.V1415_L1480del` skips. Internal exons are unchanged. ## [v10.5.14](https://github.com/openvax/varcode/tree/v10.5.14) (2026-09-27) - Test the six openvax-v1 SV targets Varcode's esvee tests lacked (#528). The somatic esvee records for SV0055, SV0175, SV0402, SV0461, SV0499 and Isovar's DLG5 deletion (IPISRC044_tumor_T2_ucla, each with its mate) are in `tests/data/osteosarc_esvee_shared_svs.vcf`. They load as mated breakends and pair into the caller's DEL/DUP/INV events. Every record and event annotates without failure. The genes Varcode finds at each breakend agree with LINX's. The DLG5 deletion removes DLG5-201's start codon and 5' end, leaving its product unresolved, as Isovar concludes. Whole transcripts inside these deletions are classified inconsistently (#541). ## [v10.5.13](https://github.com/openvax/varcode/tree/v10.5.13) (2026-09-27) - Test every small variant the OpenVax libraries share (#528). A new test reads the targets from openvax-v1's recipe and annotates all 187 with both annotators, which must agree on every transcript. That is the 179 current catalogue alleles, confirmed identical to the ready alleles of `tests/data/osteosarc_variants.json`, plus the 8 other libraries depend on: the historical MAP2 deletion, NTF3's compound, the ACSL6/EPPK1/KTN1 count-export indels, NR2F2 on GRCh37 and MT_ND5 in rCRS and hg19 coordinates. The hg19 `chrM` coordinate is refused with `ReferenceMismatchError`, since hg19's chrM is not rCRS. Before 10.5.12 the CABLES1 insertion failed this test (#538). ## [v10.5.12](https://github.com/openvax/varcode/tree/v10.5.12) (2026-09-27) - `protein_diff` no longer calls every coding variant `StartLoss` on a transcript whose annotated start codon isn't ATG (#537). It reads a recognised start codon (e.g. CTG, TTG) at the CDS start as Met in both the reference and the mutant protein. Ensembl writes such initiators as Met in some proteins and literally in others (91 complete transcripts in Ensembl 115, among them FGF2, VEGFA and WT1). For example, BAG1-210 (CTG start) chr9:33262750 C>G is `p.E178Q`, as `fast` reports, and CTG→CAG at such a start is `StartLoss` under either convention. The default annotator (`fast`) was not affected. - `fast` places an in-frame insertion within a run of identical residues at its HGVS 3'-most position, as it already did for deletions (#321), and so agrees with `protein_diff` (#538). For example, two alanines inserted into CABLES1's poly-alanine stretch are `p.8insAA`, not `p.1insAA`; the mutant protein is unchanged. ## [v10.5.11](https://github.com/openvax/varcode/tree/v10.5.11) (2026-09-27) - The `test-data` extra requires osteosarc `>=0.14.0,<0.15`, in step with Isovar, Topiary and Vaxrank. The osteosarc test corpus was re-collected with 0.14.0 from the same snapshot, and every entry is unchanged (179 ready, 3 unresolved). Its `source.corrections` now lists only the corrections that change the variant sources (21 of the snapshot's 35), as osteosarc does since 0.13.1; the 14 it no longer lists edit BAM, specimen, event and bucket metadata, not variants. ## [v10.5.10](https://github.com/openvax/varcode/tree/v10.5.10) (2026-09-26) - The `test-data` extra requires osteosarc `>=0.13.0,<0.14`, in step with Isovar, Topiary and Vaxrank. The osteosarc test corpus was re-collected with 0.13.0 from the same snapshot; only its recorded osteosarc version changed. ## [v10.5.9](https://github.com/openvax/varcode/tree/v10.5.9) (2026-09-26) - The `test-data` extra requires osteosarc `>=0.12.0,<0.13`, in step with Isovar, Topiary and Vaxrank. The osteosarc test corpus was re-collected with 0.12.0 from the same snapshot; only its recorded osteosarc version changed. - The observed-junction check reads its openvax-v1 members with `osteosarc.bundle_file`, which exports them into the osteosarc cache once, instead of exporting them into a temporary directory on every run. ## [v10.5.8](https://github.com/openvax/varcode/tree/v10.5.8) (2026-09-26) - Check the three observed-junction fixtures against their ONT reads in `openvax-v1`, the OpenVax libraries' shared Sid test data (#528). Varcode keeps no read copies; the new test confirms each record's read name, source sequence hash and junction window. It runs with the `test-data` extra and downloads the bundle (28 MB) once. ## [v10.5.7](https://github.com/openvax/varcode/tree/v10.5.7) (2026-09-26) - The `test-data` extra requires osteosarc `>=0.11.1,<0.12`, in step with Isovar, Topiary and Vaxrank. The osteosarc test corpus was re-collected with 0.11.1 from the same snapshot; only its recorded osteosarc version changed. ## [v10.5.6](https://github.com/openvax/varcode/tree/v10.5.6) (2026-09-25) - List Vaxrank 3.24 in the compatible versions: it accepts Isovar 1.38.1 and later 1.38 releases, so it can use Isovar's germline trans calls. ## [v10.5.5](https://github.com/openvax/varcode/tree/v10.5.5) (2026-09-25) - The `test-data` extra requires osteosarc `>=0.9.0,<0.10`, in step with Isovar, Topiary and Vaxrank. The osteosarc test corpus was re-collected with 0.9.0 from the same snapshot; only its recorded osteosarc version changed. ## [v10.5.4](https://github.com/openvax/varcode/tree/v10.5.4) (2026-09-25) - Document Isovar 1.38's germline phase calls: fragments carrying the somatic alt allele now show a matched germline variant in cis or trans, instead of only cis from the assembled RNA. Update the compatible versions for Vaxrank 3.23 and Isovar 1.37.2 and later, and drop the stale shared-osteosarc note. ## [v10.5.3](https://github.com/openvax/varcode/tree/v10.5.3) (2026-09-25) - Generate the osteosarc test corpus with osteosarc 0.7.0 (#524). Re-collected offline from the same pinned snapshot, it has 179 ready and 3 unresolved entries instead of 177 and 5: 0.7.0's curation resolves `COL3A1-Splice` and `FAM157A-p_W70_Q71ins_14` and adds transcript, source and count corrections. All ready alleles annotate without errors under both implementations. The `test-data` extra now requires osteosarc `>=0.7.0,<0.8`, together with Isovar, Vaxrank and Topiary. ## [v10.5.2](https://github.com/openvax/varcode/tree/v10.5.2) (2026-09-25) - A release after a dot, as in Ensembl's file names (`"GRCh38.93"`), or after `_`, `-` or a space, now chooses that release like `"GRCh38:93"`. In 10.5.0 only the colon worked; the other spellings still silently gave the most recent release (#512). A release after one of these separators is read only when what precedes it is exactly a known assembly name or alias, so `"GRCh38.p13"` and `"Felis_catus_9.0"` keep their meaning. ## [v10.5.1](https://github.com/openvax/varcode/tree/v10.5.1) (2026-09-25) - Document Isovar as an RNA phase source (`IsovarReadPhasing` with `MolecularPhaseResolver`, Isovar ≥1.36), what it can and cannot resolve, and compatible Varcode/Isovar/Vaxrank versions. Remove the stale note that Vaxrank's DNA fallback collapses fusion candidates. ## [v10.5.0](https://github.com/openvax/varcode/tree/v10.5.0) (2026-09-25) - A reference name can choose an Ensembl release: `"GRCh38:93"`, or `--genome GRCh38:93` on the command line, gives Ensembl 93. Previously the release was silently dropped and the most recent release used instead (#512). The release must provide the assembly, so `"GRCh38:75"` (a GRCh37 release) is an error. UCSC names work too (`"hg19:75"`). Names without a release, and paths in VCF headers, resolve as before. This also serves Isovar, Vaxrank and other tools that take Varcode's `--genome` (openvax/isovar#122). ## [v10.4.2](https://github.com/openvax/varcode/tree/v10.4.2) (2026-09-25) - Allow osteosarc `>=0.2.3,<0.3` in the `test-data` extra, the range Isovar and Vaxrank also require. The 0.7.0 pin in 10.3.1 made a development environment with Varcode's test data and Isovar impossible to install. The snapshot check accepts any version in the range, so osteosarc patch releases no longer break it. ## [v10.4.1](https://github.com/openvax/varcode/tree/v10.4.1) (2026-09-25) - Ranking a structural candidate set that lists itself as a candidate, for example through attached RNA evidence, no longer recurses without end. The self-candidate ranks as the set's own class. Found while running Vaxrank on Varcode 10. ## [v10.4.0](https://github.com/openvax/varcode/tree/v10.4.0) (2026-09-25) - `MolecularPhaseResolver` no longer reports trans from a source's partner lists (#517). Variants that are not co-observed are unknown (`None`) unless the source implements its own `in_cis` from fragments that show one alt allele with the other's reference allele. Previously a germline variant outside an RNA source's input, which the source never examined, was reported as trans and collapsed germline-aware annotation to the reference-relative result. Co-observation still establishes cis, so haplotype grouping is unchanged. ## [v10.3.1](https://github.com/openvax/varcode/tree/v10.3.1) (2026-09-25) - Pin the optional `test-data` extra to osteosarc 0.7.0, the current release. The bundled snapshot checks pass unchanged on it; the 0.2.3 pin made them fail in any environment with a newer osteosarc. ## [v10.3.0](https://github.com/openvax/varcode/tree/v10.3.0) (2026-09-24) - Unenumerated phase no longer becomes a precise all-cis consequence (#503). When germline phase needs more hypotheses than the cap, the default path and the experimental `transcript_model` return a new `HypothesisLimit` effect (a kind of `Unresolved`). It records the phase partition and the effect without germline context, ranks as that effect would in top-priority selection, and round-trips through JSON. - New public helpers: `partition_germline_by_phase` returns a `PhasePartition` whose `hypotheses()` enumerates only what is unknown. Homozygous germline variants are always cis, phase follows through germline-to-germline answers, and variants phased to each other flip as one block, so partial evidence is kept and impossible combinations are not generated. `query_in_cis` gives every caller one policy for asking a resolver (NumPy booleans accepted; resolver errors logged and treated as unknown). Exceeding the cap raises a picklable `HypothesisLimitError`; `enumerate_phase_hypotheses` now raises it instead of returning an all-cis placeholder. - The phase cap is `GermlineContext.max_phase_hypotheses` (default 8), so `effects(germline=...)` can raise it. Caps must be positive integers (NumPy integers accepted). `transcript_model` uses the context's cap through `effects()`, and its combined phase/splice overflow now returns `HypothesisLimit` instead of raising. - `PhaseCandidateSet` now ranks as its most severe candidate instead of below every other effect. Noncoding and incompletely annotated transcripts skip phase enumeration, and haplotype labels depend only on the cis set. ## [v10.2.1](https://github.com/openvax/varcode/tree/v10.2.1) (2026-09-24) - Documentation readability pass: the README and docs home now describe what Varcode does and how to read its results (per-transcript predictions, severity vs. likelihood, `None` as unknown). Getting started gains a glossary of the main objects. - Reorganize troubleshooting, sample identity checks, VCF export, SV results, and RNA imports around plain-language summaries, with status/threshold tables and collapsible notes for edge cases. - Remove stale limitations already fixed in #437 and #449, note that genotype set operations treat uncalled normals as absent, and move the dated quality audit out of the user reference section. ## [v10.2.0](https://github.com/openvax/varcode/tree/v10.2.0) (2026-09-24) - Add `varcode check-samples` and a Python API for pairwise donor genotype concordance and somatic tumor overlap (#510), with explicit sample roles, matched-normal evidence, expected identity flags, JSON and TSV reports. - Report sparse or uninformative evidence as inconclusive; never infer reference genotypes from missing records. Tolerate tumor LOH and partial somatic overlap with documented, configurable screening heuristics and provenance/QC counts. - Include reproducible synthetic VCF builders and checks for swaps, mixed tumor/normal inputs, missing quality, allele ordering and precise SV matching. ## [v10.1.3](https://github.com/openvax/varcode/tree/v10.1.3) (2026-09-24) - Preserve sample identities, per-sample FORMAT values, missing samples, and original multi-allelic ALT order during VCF export (#502). - Write INFO/FORMAT declarations, accept original field definitions, and reject incomplete/ambiguous ALT reconstructions before writing output. - Support generator/empty exports, numeric position ordering, missing list elements, and per-sample filters; add independent identity round-trip tests. ## [v10.1.2](https://github.com/openvax/varcode/tree/v10.1.2) (2026-09-24) - Enforce VCF allele limits across simple and structural calls and retain structural ALT indexes; propagate parser failures instead of silently truncating input (#504). - Isolate default collection metadata (#505), preserve transcript failure diagnostics (#506), and run pytest with the interpreter used to probe optional plugins (#490). - Enable undefined-name lint checks, remove mutable parser defaults and deprecated logging calls, and preserve column order during MAF normalization. - Update developer/release instructions, fixture provenance guidance, Osteosarc compatibility, API navigation, and documented export/phase limits. ## [v10.1.1](https://github.com/openvax/varcode/tree/v10.1.1) (2026-09-24) - Do not infer protein truncation from an internal-Met reference suffix when an observed transcript's 5′ completeness is unknown (#467). Preserve the start-to-stop ORF, its evidence and mapped local changes, while retaining full comparisons for predictions with a mapped annotated initiator. ## [v10.1.0](https://github.com/openvax/varcode/tree/v10.1.0) (2026-09-23) - Report identical inherited alleles as `GermlineAlleleOverlap`, without manufacturing a new somatic sequence change or claiming LOH (#454). - Add `detect_germline_overlap`; deprecate the allele-only `detect_loh` query, which now returns `None` (not assessed). LOH needs independent allelic-state evidence. Mixed inherited/somatic transcript-model groups remain unresolved. ## [v10.0.4](https://github.com/openvax/varcode/tree/v10.0.4) (2026-09-23) - Leave overlapping DUP/INV events unresolved in the experimental transcript model when their junctions exceed its finite layout, instead of classifying a clipped allele as a UTR-only or unchanged product (#449). Preserve supplied assemblies and fully represented local rearrangements. ## [v10.0.3](https://github.com/openvax/varcode/tree/v10.0.3) (2026-09-23) - Archive structural effects as versioned graphs so self candidates, fusion partners, primary/cryptic/splice/external candidates, mutant transcript models, and annotation provenance survive JSON round trips (#438). ## [v10.0.2](https://github.com/openvax/varcode/tree/v10.0.2) (2026-09-23) - Load structural variants from both CLIs, normalize explicit chr contigs, report filtered VCF record counts, and add `--include-filtered` (#433). - Add `--skip-errors` with retained Failure rows and error details, including failed initial annotation lookups and failures alongside coding filters. ## [v10.0.1](https://github.com/openvax/varcode/tree/v10.0.1) (2026-09-23) - Reconstruct inserted junction bases in local DEL/DUP transcripts on both strands, including insertions at exon anchors flanking a deleted intron (#491). Protein consequences now use the net spliced edit with the retained insert. - Share insertion-retention rules with fusion models: retain exonic inserts, exclude intronic inserts under reference splicing, and preserve uncertainty for mixed retention or conflicting/unreadable reciprocal alleles. ## [v10.0.0](https://github.com/openvax/varcode/tree/v10.0.0) (2026-09-23) **Changed** - Classify local SV transcript models by their protein consequence (#420): start loss, in-frame deletion/insertion, frameshift, and other changes now replace event-type labels. Preserve conditional consequences in candidate sets with splice assumptions, event provenance, and mutant sequences. - Return unresolved candidates for unspecified insertion/CNV structure, unmaterialized inversions, and unmapped assemblies. Keep fusion consequences and legacy class imports; callers should read DNA type from `variant.sv_type`. - Map retained CDS starts on both strands and preserve selenocysteine/SECIS uncertainty and independent coding/protein change flags. ## [v9.5.0](https://github.com/openvax/varcode/tree/v9.5.0) (2026-09-23) **Added** - Compare SV call tables across samples and callers with a reusable API and command. Preserve every input record, distinguish exact reported alleles from nearby candidates, and export breakpoint/insertion disagreements, source IDs, unresolved calls, and reproducible run provenance. ## [v9.4.2](https://github.com/openvax/varcode/tree/v9.4.2) (2026-09-23) **Fixed** - Retain inserted junction bases when translating exonic breakend fusions (#485), with strand-correct sequence from either reciprocal record and paired structural events. Mixed exon/intron insertion retention and conflicting or unreadable inserted alleles remain explicitly unresolved. - Record the reference-splicing assumption when an insertion between two intronic breakpoints is excluded from the predicted fusion transcript. ## [v9.4.1](https://github.com/openvax/varcode/tree/v9.4.1) (2026-09-22) **Testing** - Bundle the verified historical Osteosarc metadata snapshot and run the native 0.1.4 adapter checks in CI on Python 3.10+. A fresh checkout with `.[test-data]` now runs these tests offline without a manually prepared cache or local sibling repository (#483). ## [v9.4.0](https://github.com/openvax/varcode/tree/v9.4.0) (2026-09-22) **Fixed** - Delegate known-cis groups to the selected annotator's optional `annotate_haplotype` method (#437). Retain unsupported groups as unresolved effects instead of silently dropping them or predicting with another backend. - Route experimental transcript-model joint effects through its existing multi-variant engine, including patient germline context from every member's window and separately retained observed RNA models. The default retains point-edit haplotypes and explicitly declines joint germline composition. **Testing** - Update the optional `test-data` extra to Osteosarc 0.1.4, retaining Python 3.10+ compatibility and the historical fixture's package/snapshot provenance (#478). ## [v9.3.7](https://github.com/openvax/varcode/tree/v9.3.7) (2026-09-21) **Testing** - Collect 177 real site variants and five unresolved entries from the pinned osteosarc snapshot into a portable fixture. Ordinary offline tests now annotate every ready allele with `fast` and `protein_diff`, including the mitochondrial variant and corrected MAP2 complex allele (#464). - Add an explicit offline regeneration command and native test-variant loader, preserving original alleles, reference identity, correction notes, and source hashes. Optional snapshot checks verify byte-for-byte regeneration. ## [v9.3.6](https://github.com/openvax/varcode/tree/v9.3.6) (2026-09-19) **Fixed** - The `fast` and `transcript_model` annotators now classify an insertion immediately before the retained CDS start on a reverse-strand transcript as 5′ UTR. The insertion no longer produces a false coding- or protein-sequence-change flag; all three annotators agree (#474). ## [v9.3.5](https://github.com/openvax/varcode/tree/v9.3.5) (2026-09-19) **Fixed** - Genomic-layout translation only recodes an annotated selenocysteine when all three codon origins remain contiguous. Insertions or deletions that create a new TGA from part of that codon now terminate translation (#473). - `protein_diff` inspects the mutant start codon at the same mapped CDS offset used for translation. An insertion immediately before the retained start codon is classified as 5′ UTR, without a false alternate-start or coding-sequence-change call (#473). **Testing** - Optional offline corpus checks use `osteosarc==0.1.0` and a pinned public snapshot, preserving native alleles and provenance and distinguishing the corrected MAP2 complex allele from the older deletion (#464). ## [v9.3.4](https://github.com/openvax/varcode/tree/v9.3.4) (2026-09-18) **Fixed** - Predicted proteins no longer stop at selenocysteine (#470). Mutant transcripts, splice outcomes, fusions and the experimental `transcript_model` layouts now read an annotated Sec UGA (`U` in the Ensembl reference protein) as Sec when no edit touches it and some selenoprotein 3′ UTR (SECIS) remains. Where none remains, as in a fusion downstream of Sec, UGA still terminates. Across the 60 complete selenoprotein transcripts in Ensembl 81, `protein_diff` had called most coding variants `PrematureStop` (176 of 240 probes), and `transcript_model` had called Sec→Trp a stop loss and missense changes after Sec silent (112 of 240). Both now agree with the default annotator on all 240. Opaque RNA imports without reference coordinates are still translated literally. When the 3′ UTR is only partly kept, the SV change flags ignore a supplied protein and stay unresolved. - Joint translation of several edits (germline context, phasing, splice outcomes) now finds the CDS start after a 5′ UTR indel instead of reading the wrong frame (#471). - `translate_sequence` accepts `selenocysteine=` offsets of TGA codons to read as `U`. ## [v9.3.3](https://github.com/openvax/varcode/tree/v9.3.3) (2026-09-18) **Fixed** - SV sequence-change flags no longer call unchanged selenoproteins protein-changing (#468). A TGA mapped onto an annotated selenocysteine (`U` in the Ensembl reference protein) is read as Sec where the model keeps that transcript through its 3′ end, which holds the SECIS element. It is read as a stop where no selenoprotein 3′ UTR remains (for example a fusion downstream of Sec, or a deletion of the whole 3′ UTR, which truncates the protein). When the SECIS may be only partly lost (a 3′ UTR deletion or duplication, a fusion in the 3′ UTR, or an unmapped import), the protein flag is reported only if both readings agree; otherwise it is `None`. The coding flag compares CDS bases and does not depend on Sec decoding. A supplied protein that ends exactly at a Sec residue no longer counts as a truncation, and partial observations continue past a decoded Sec codon. All 60 complete selenoprotein transcripts in Ensembl 81 now read unchanged for a reference-identical model. - Complete-ORF comparisons read any start codon as the initiator methionine on both sides. Ensembl writes CTG/TTG initiators as `L`, so the 68 complete non-ATG transcripts in Ensembl 81 were previously called protein-changing when unchanged. ## [v9.3.2](https://github.com/openvax/varcode/tree/v9.3.2) (2026-09-18) **Fixed** - SV sequence-change flags no longer call a partial observation (`protein_completeness` of `partial_start`, `partial_end`, `partial_both`, or an explicit unknown) protein-changing just because it is shorter than the full reference protein (#462). Missing sequence is neither unchanged nor a truncation: these flags are `True` only when ORF bounds and reference-transcript segments place an observed codon in frame on a differing reference CDS codon, including premature stops and stop loss, and otherwise stay `None`. Only `start_to_stop`, or no label, permits a whole-protein comparison or the complete-ORF fallback; any other label fails closed. Exacto partial peptides, which have no reference coordinates, remain unresolved. Complete predictions and 9.3.0 candidate aggregation/filtering are unchanged. ## [v9.3.1](https://github.com/openvax/varcode/tree/v9.3.1) (2026-09-18) **Documentation** - Explain the shared Varcode / Isovar / Vaxrank responsibility split and evidence handoffs, with short README links and a focused integration guide. Distinguish available functionality from planned RNA/SV reconciliation (Isovar #305) and remaining downstream candidate-selection limits. No annotation behavior changed. ## [v9.3.0](https://github.com/openvax/varcode/tree/v9.3.0) (2026-09-18) **Fixed** - Structural effects now report coding/protein changes from available sequence and retained reference ORFs, rather than inheriting `False` (#418). Compare every candidate; retain a set if any alternative predicts a protein change. Coding deletions and the seven audited CPEB2–FAM193A fusion proteins survive filtering, while unchanged and synonymous proteins do not. **Changed** - SV sequence-change flags can now be `None` for unknown, distinct from `False` for unchanged. `Unresolved` and unclassified cryptic-exon candidates also use `None`. `drop_silent_and_noncoding()` retains unknowns by default; pass `keep_unresolved=False` to require a positive protein-change prediction. Candidate ordering, evidence, effect classes, and stored proteins are unchanged. ## [v9.2.8](https://github.com/openvax/varcode/tree/v9.2.8) (2026-09-18) **Fixed** - Retain all compatible fusion partner isoforms and junctions as candidates instead of selecting only the first (#406). Each carries its predicted protein when available; missing transcript sequence stays unresolved. Preserve the primary ordering and existing span/splice/RNA candidates, with no arbitrary count cap and no merging of distinct isoforms by protein alone. - Add the audited CPEB2–FAM193A 1,541/1,500-aa alternatives, a 257-isoform case, and strand, provenance, assembly, and missing-sequence regressions. These are annotated-isoform predictions, not exhaustive splice/phase hypotheses or evidence of expression. ## [v9.2.7](https://github.com/openvax/varcode/tree/v9.2.7) (2026-09-17) **Documentation** - Split structural annotation, SV reference rules, and observed RNA imports; separate germline setup from phasing workflows. Replace the long API page with an index and five topic references, without duplicate object headings. - Group effect definitions by family, simplify headings, and move transform contributor notes out of the user guide. Preserve existing section links. - Correct the phased-VCF resolver example and cover it with executable tests. No annotation behavior or public APIs changed (#457, #458). ## [v9.2.6](https://github.com/openvax/varcode/tree/v9.2.6) (2026-09-17) **Documentation** - Shorten the effect annotation guide and split splice variants, transcript models, and experimental annotators into separate pages. Consolidate plugin instructions and provenance, use specific headings, and update navigation and example tests. Existing section links still lead to the relevant guides. No annotation behavior or public APIs changed. ## [v9.2.5](https://github.com/openvax/varcode/tree/v9.2.5) (2026-09-17) **Documentation** - Make the README a short introduction and add a reproducible getting-started guide. Group navigation into everyday tasks, advanced workflows, and reference; move implementation details behind usage examples (#415, #453). - Correct germline examples and resolver descriptions (#413), stale splice and serialization explanations, and the missing Genome API link (#451). - Lead the annotation guide with ordinary `effects()` usage. Explain optional implementations, supported-input limits, partial transcript models, sequence and candidate access, legacy aliases, and current haplotype ownership in an advanced section. No annotation behavior or public APIs changed. ## [v9.2.4](https://github.com/openvax/varcode/tree/v9.2.4) (2026-09-17) **Fixed** - BND fallbacks retain only the strand/orientation-correct reference cDNA prefix or suffix, not the entire transcript (#447). Single breakends retain their known local side; unknown local orientation leaves the model unresolved. Fragments are explicitly labeled partial, full cDNA/protein remain unknown, and supplied assemblies and existing coding-fusion predictions are preserved. ## [v9.2.3](https://github.com/openvax/varcode/tree/v9.2.3) (2026-09-17) **Fixed** - DUP/INV events with junction ends outside the selected transcript no longer fabricate a local duplicated/inverted transcript (#405). The DNA event class remains, but its unresolved mutant transcript is None. Existing fusion predictions and explicitly supplied allele assemblies are preserved. ## [v9.2.2](https://github.com/openvax/varcode/tree/v9.2.2) (2026-09-17) **Fixed** - Structural variants expose their actual REF/ALT, not the constructor's temporary nucleotide placeholders; small-edit flags are always false (#417). - Mixed/SV tables include type, endpoint, mate and affected-span columns; point-only and empty table schemas remain unchanged. Structural CSV import raises explicitly instead of reconstructing misleading point variants. ## [v9.2.1](https://github.com/openvax/varcode/tree/v9.2.1) (2026-09-17) **Fixed** - Parsed symbolic DEL/DUP/INV/CNV spans exclude the retained VCF padding base from exon and mutant-cDNA annotation (#404). POS and junctions are unchanged; direct constructors retain their explicit affected-span defaults. - Symbolic span records with missing/non-increasing END raise instead of being interpreted as a one-base event at the padding position. ## [v9.2.0](https://github.com/openvax/varcode/tree/v9.2.0) (2026-09-16) **Added** - `RNAReadPhasingSource.register_haplotype` tests an explicitly supplied local allele combination against anchored, quality-filtered RNA sequence. Equivalent deletion, splice-gap and split-gap alignments can support a known sequence without treating arbitrary RNA skips as DNA deletions (#441). - `load_exacto_fusions(..., primary_structures_path=...)` imports native Exacto peptide predictions, including separate ORFs and partial proteins. Codons and coordinates are checked against observed RNA; original per-base provenance and completeness remain explicit. Prediction is not evidence of translation. **Fixed** - RNA read support and pair grouping use `(RG, QNAME)`, preventing unrelated libraries with reused names from creating false cis evidence or being collapsed into one supporting fragment (#443). ## [v9.1.0](https://github.com/openvax/varcode/tree/v9.1.0) (2026-09-16) **Added** - `load_exacto_fusions` imports selected SV-linked transcript structures and DNA/RNA integration rows without guessing an ORF or choosing one isoform. Original oriented sequence, splice/path rows, model IDs, and variant links remain available on existing `MutantTranscript`/effect candidates (#259, #261). - `RNAEvidence` is a small concrete resolver for imported candidates; `make_fusion_outcome` imports a caller-specified observed sequence using existing structural effects. Partial and antisense/intergenic observations need not be promoted to coding fusions. Translation requires an explicit, complete start-to-stop ORF and is labeled prediction, not protein evidence. - `MutantTranscript.from_sequence` shares external-sequence construction with the existing assembled-SV path, removing the duplicate sequence wrapper. **Fixed** - RNA observations attached to single-outcome structural predictions such as `Intronic` are retained alongside the DNA prediction. Deterministic point-variant behavior and default annotation remain unchanged. ## [v9.0.0](https://github.com/openvax/varcode/tree/v9.0.0) (2026-09-15) **Breaking changes** - Removed `StructuralVariantAnnotator`, the `varcode.annotators.structural_variant` module, and `annotator="structural_variant"`. Use `effects()` or `annotator="fast"` for both point variants and SVs; direct callers can use `FastEffectAnnotator().annotate_on_transcript(variant, transcript)`. - Removed `UnsupportedVariantError` and its package/registry exports. Partial annotators return `NotImplemented`; public prediction APIs expose an `Unresolved` effect without silently selecting another annotator. - Pickles referring to classes in the removed structural-annotator module or to `UnsupportedVariantError` must be read with varcode 8 before migrating. **Changed** - The default annotator calls internal structural prediction helpers directly, without instantiating another annotator. The experimental transcript model shares those helpers for fusion prediction. Classification, sequence assembly, candidates, and existing provenance are unchanged. - `fast` remains the default; `protein_diff` and `transcript_model` remain experimental. Joint haplotype construction is unchanged (tracked in #437). ## [v8.0.3](https://github.com/openvax/varcode/tree/v8.0.3) (2026-09-15) **Fixed** - Contig validation now uses the actual Genome annotation dataset instead of sharing a cache by assembly name. Subsets, custom annotations and releases named GRCh38 no longer accept or reject chromosomes based on loading order (#402). - Suppressed gene/transcript lookup errors return an empty `EffectCollection` with annotator provenance, rather than a plain list, and log the error. ## [v8.0.2](https://github.com/openvax/varcode/tree/v8.0.2) (2026-09-15) **Fixed** - `varcode` and `varcode-genes` no longer fail at startup when `pkg_resources` is unavailable. Both commands load their packaged logging configuration with the standard library's `importlib.resources`, including from zipped packages. - Added subprocess regression tests for both commands with `pkg_resources` imports blocked, so older setuptools in a test environment cannot mask the missing runtime dependency. ## [v8.0.1](https://github.com/openvax/varcode/tree/v8.0.1) (2026-09-15) **Changed** - Renamed the experimental `realized` annotator to `transcript_model`, with `TranscriptModelEffectAnnotator` and `predict_transcript_model_effect` in `varcode.transcript_model`. The old registry name, class, function and module imports remain compatibility aliases. New annotation provenance uses `transcript_model`, including when selected through the old alias. - The transcript model remains experimental and opt-in. The `fast` default, biological model, candidate ordering and unsupported-input behavior are unchanged. ## [v8.0.0](https://github.com/openvax/varcode/tree/v8.0.0) (2026-09-15) **Changed** - One built-in default annotator (`fast`) now owns point-edit and structural routing. Explicit `annotator="fast"` handles SVs just like `effects()`. Single-transcript APIs accept annotator and germline context and honor the same scoped selection as collection annotation. - Partial experimental annotators return `NotImplemented` for unsupported inputs, without declaring `supports`. Public APIs expose those results as `Unresolved` with a reason and retain the selected annotator's provenance; they never silently substitute another annotator. `None` is an invalid plugin result. `UnsupportedVariantError` remains a compatibility import. - Nonempty germline context requires `annotate_with_context`. The default retains established germline point-edit prediction; SV/germline composition remains experimental in `realized`. `protein_diff` declines SVs before reading placeholder nucleotide alleles. Realized-layout limitations return explicit unknowns without swallowing other errors. - `protein_diff` and `realized` remain optional experiments; shared protein comparison helpers and point-edit parity tests remain in use. This release changes the annotator contract, not the default biological model or ranking. **Fixed** - The registry-default test now restores the prior annotator rather than leaking `protein_diff` into later tests. ## [v7.3.0](https://github.com/openvax/varcode/tree/v7.3.0) (2026-09-15) **Added** - Added the opt-in `realized` effect annotator and `predict_realized_effect`. It composes unknown germline phase, splice-site choices, point variants and local structural variants on one genomic layout, realizes the patient baseline and mutant product, classifies their protein difference, merges identical comparisons, and returns an ordinary top effect with ordered alternatives in `effect.candidates`. - Added graph-native exon projection and splicing on forward and reverse strands. Canonical and exon-skip products resolve without a FASTA; with genomic sequence, intron-retention and cryptic-site products are translated from the mutated haplotype. Sequence-dependent tier-0 branches are reported as `Unresolved`. - Added BND support to the realized annotator through the independently validated fusion assembler, including translated cross-gene products. **Fixed** - Splice choices whose required exon was removed by another choice are no longer enumerated, including the impossible combination of retaining an intron while skipping the exon that terminates it. - The 64-hypothesis safety limit now applies to the combined phase × splice product instead of separately to each phase branch. - Exonic alleles excluded from a realized mRNA are reported as `Silent` with `excluded_from_mrna=True`, rather than being mislabeled as genomic `Intronic` variants. - Structural effects now expose the protein on their realized `mutant_transcript` through `effect.mutant_protein_sequence`. ## [v7.2.0](https://github.com/openvax/varcode/tree/v7.2.0) (2026-09-15) **Added** - Added the internal coordinate-aware genomic-layout foundation for the realized effect engine. Layouts preserve base origins through point edits, deletions, duplications, inversions, reverse strands, and cross-contig breakend joins without expanding large introns into per-base objects. - Added composable phase/splice hypothesis and realized-product types. Splice hypotheses are validated as graph rewrites, rule preferences remain ordinal unless a calibrated probability is supplied, and candidate merging now keys on both the patient baseline and mutant product. ## [v7.1.2](https://github.com/openvax/varcode/tree/v7.1.2) (2026-09-15) **Fixed** - Cryptic splice-site scans now score the mutated allele and use one interbase boundary convention across donors, acceptors, and both strands. A destroyed canonical donor can no longer nominate itself as a cryptic replacement. - FASTA-backed cryptic-splice and intron-retention outcomes now realize the originating exonic or intronic allele on the same mutant transcript instead of constructing the splice mechanism from reference sequence alone. ## [v7.1.1](https://github.com/openvax/varcode/tree/v7.1.1) (2026-09-14) **Fixed** - Four regression tests no longer silently skip. Three splice-outcome tests now inspect the always-on `SpliceOutcomeSet` wrapper and assert its `disrupted_signal_class`; the explicit 50-base deletion test now derives its coordinates from the pinned CFTR transcript instead of using a position one base before the intended exon. ## [v7.1.0](https://github.com/openvax/varcode/tree/v7.1.0) (2026-09-11) **Changed** - Structural-variant fusion annotation follows breakend orientation. The side of each breakpoint that's kept, with transcript strand, decides which transcript is the 5' and which the 3' partner, and a partner has to be in another gene and join sense-to-sense. `GeneFusion` is reported on either partner (`GeneFusion.transcript` can be the 3' partner) and carries `five_prime_transcript` / `three_prime_transcript`. A breakend whose ALT keeps the wrong sides for a fusion now gives `TranslocationToIntergenic`. - A `DEL` / `DUP` / `INV` with one end in a transcript and a sense-to-sense partner at the other end gives a `GeneFusion` (e.g. the TMPRSS2-ERG deletion). The deletion, duplication or inversion of that transcript's exons follows the fusion in its `candidates`. - `pair_breakends` builds a typed `DEL` / `DUP` / `INV` from a breakend pair that carries that `SVTYPE` (esvee, GRIDSS) when both halves agree on the label and their kept sides fit it, so `effects()` covers the whole span. Each record still loads as the breakend it is, so annotating an unpaired collection doesn't report the event twice. - `StructuralVariant.junctions` exposes the novel adjacencies a variant creates as pairs of `Breakend` ends (position plus the side kept), and `breakpoints` lists their positions. Fusion assembly, transcript containment and cryptic-exon scanning all read from them. - The reverse-complement orientation warning is replaced by a warning when a breakend with a mate has no breakend ALT to read orientation from. **Fixed** - VCF single breakends (`.ACGT` / `ACGT.`) no longer crash `load_vcf`; they load as `BND`s with no mate. - SVs from `load_vcf` use the genome and contig-name settings passed to it. Previously they fell back to the default GRCh38 and kept `chr` names, so `effects()` raised on UCSC-named VCFs. - `StructuralVariant.mate_contig` is normalized like `contig`, and `Variant._convert_ucsc_contig_name_to_ensembl` converts its argument rather than the variant's own contig. - `StructuralVariant` equality compares every field the record carries (type, span, ALT, mate, assembled allele and confidence intervals), so `load_vcf(distinct=True)` no longer merges different SV records at the same position. - `StructuralVariant.to_dict` covers every SV field, so JSON and pickle round-trip SVs (pickling one previously raised `TypeError`). - A fusion keeps the base at each breakpoint, so a breakpoint inside an exon no longer drops one base from the 5' partner (and frameshifts the predicted protein). - A fusion partner must be in a gene that doesn't span both ends of the junction, so an event inside one gene isn't reported as a fusion with a nested or antisense gene at its far end. - A `GeneFusion` reported on its 3' partner carries a mutant transcript whose `reference_transcript` is the transcript being annotated. - `reference_range` reads a range with one locus query instead of one per position when no single transcript spans it, which is the path cryptic-exon scoring takes on a genome with no chromosome FASTA. **Added** - A structural variant annotation guide (`docs/structural_variants.md`) covering which transcripts are annotated, how strand and kept sides decide fusion direction, every breakend combination, deletions, duplications and inversions by strand, and where a breakpoint lands. - Fusion regression tests from the public osteosarc.com osteosarcoma dataset, validated against LINX (`tests/test_osteosarc_fusions.py`, `tests/data/osteosarc_esvee_somatic.vcf`). ## [v7.0.0](https://github.com/openvax/varcode/tree/v7.0.0) (2026-07-08) **Changed** - The default effect annotator is now **`fast`** (the offset-based classifier varcode has shipped since 2.0.0) instead of `protein_diff`. `Variant.effects()` / `VariantCollection.effects()` with no explicit `annotator=` now route through `fast`. This is a behavior change for callers that relied on the default: for SNVs / indels / MNVs the two annotators are fully reconciled (see `tests/test_protein_diff_parity.py`, `tests/test_annotator_parity_adversarial.py`, and `tests/test_annotator_divergence_scenarios.py`), so nearly all output is unchanged, but any residual divergence now resolves to `fast`'s classification. `protein_diff` stays available via `annotator="protein_diff"` or `varcode.use_annotator("protein_diff")`, and remains the substrate the `MutantTranscript` / splice-outcome / germline machinery builds on. Rationale: `fast` is the more battle-tested path — during the `protein_diff` bring-up it was effectively the correctness oracle `protein_diff` was reconciled against ([#318](https://github.com/openvax/varcode/issues/318)–[#321](https://github.com/openvax/varcode/issues/321)), and it has the cleaner bug history ([#397](https://github.com/openvax/varcode/issues/397)). Major version bump because default behavior changes. **Added** - Regression pins in `tests/test_annotator_divergence_scenarios.py` for the frameshift coincidental-shared-suffix class ([#396](https://github.com/openvax/varcode/pull/396) / [#397](https://github.com/openvax/varcode/issues/397)): a frameshift whose novel C-terminus coincidentally ends with the reference protein's terminal residue(s) must retain the full tail under **both** annotators (CFTR `p.L127fs` on the + strand, BRCA1 `p.R71fs` on the − strand). ## [v6.0.2](https://github.com/openvax/varcode/tree/v6.0.2) (2026-07-08) **Fixed** - The default protein-diff annotator (`classify_from_protein_diff`) no longer drops the last 1–2 residues of a frameshift/stop-loss novel C-terminus when that tail coincidentally ends with the same residue(s) as the reference protein's own C-terminus. Reading-frame-changing branches now trim only the shared *prefix* (`trim_shared_prefix`) instead of `trim_shared_flanking_strings`, which also stripped a shared *suffix* — correct for an in-frame indel, but wrong for a novel tail running to a new stop codon. Example: ATM p.F61fs (GRCh38 `chr11:g.108227882delT`, `ENST00000675843`) now reports `74 aa … FRKKQNV` from `Variant.effects()`, matching `annotator="fast"` and the true ORF, instead of the truncated `73 aa … FRKKQN`. `annotator="fast"` and `predict_variant_effect_on_transcript()` were never affected ([#396](https://github.com/openvax/varcode/pull/396), [#397](https://github.com/openvax/varcode/issues/397)). ## [v6.0.1](https://github.com/openvax/varcode/tree/v6.0.1) (2026-06-18) **Fixed** - In-frame deletions that remove the stop codon of a transcript with no 3' UTR sequence (e.g. MAPK3-006 / `ENST00000395199`, whose `three_prime_utr_sequence` is `""`) no longer raise `ValueError: If no amino acids added by StopLoss then it should be Silent`. With no readthrough sequence to translate into, the effect is now classified as a C-terminal `Deletion` instead of an invalid `StopLoss` with an empty `aa_alt`. Both annotators agree on this: the in-frame predictor (`FastEffectAnnotator`) no longer constructs the invalid `StopLoss`, and the default protein-diff classifier (`classify_from_protein_diff`) no longer mislabels the truncated protein as a `PrematureStop` — there is no stop codon in the mutant CDS, so a premature stop is incorrect. The earlier [#246](https://github.com/openvax/varcode/issues/246) fix only covered transcripts with a non-empty 3' UTR ([#394](https://github.com/openvax/varcode/issues/394)). ## [v6.0.0](https://github.com/openvax/varcode/tree/v6.0.0) (2026-05-26) **Fixed** - `apply_variants_to_transcript` now refuses an insertion abutting another edit at the same cDNA offset. The previous overlap check only caught range overlap and the two-insertions-at-the-same-offset case, so an insertion paired with a substitution or deletion starting at the same offset slipped through and produced an order-dependent joint cDNA. User-visible impact: phased haplotype / germline pipelines (`varcode/phasing.py`, `varcode/germline.py`) that previously returned a silently order-dependent joint effect for these inputs now return independent per-variant effects computed against the reference instead. - IUPAC ambiguity codes (R, Y, S, W, K, M, D, V, H, B) in a reverse-strand variant's alleles are now reverse-complemented via the full IUPAC translation table. The private helper in `varcode/mutant_transcript.py` only covered A/C/G/T/N and silently passed ambiguity codes through unchanged; `_resolve_variant_edit` now delegates to `varcode.nucleotides.reverse_complement`. **Added** - Added `RNAReadPhasingSource`, a BAM-backed `ReadPhasingSource` implementation for RNA read/fragment co-occurrence. It is consumed through `MolecularPhaseResolver(source)` and lives behind the optional `varcode[rna]` / `pysam` dependency. `ReadPhaseResolver` remains as the varcode 5.0 compatibility name. - `SpliceOutcomeSet.effect_if_splicing_unchanged` — the canonical "alternative outcome" accessor: the coding consequence that applies if splicing proceeds normally (the `NormalSplicing` candidate's `coding_effect`). Unlike the legacy `ExonicSpliceSite.alternate_effect`, it works for intronic splice disruptions — returning `None` when the nucleotide change leaves the protein untouched (i.e. there is no coding consequence to attach). Sits alongside `most_likely_effect` and `candidates` as the three-accessor surface on `SpliceOutcomeSet` ([#391](https://github.com/openvax/varcode/issues/391)). **Breaking** - `SpliceOutcomeSet` is now always-on for splice-disrupting variants ([#391](https://github.com/openvax/varcode/issues/391)). Every variant that lands in the canonical splice window — `SpliceDonor`, `SpliceAcceptor`, `ExonicSpliceSite`, `IntronicSpliceSite` — is wrapped in a `SpliceOutcomeSet` carrying the candidate mechanisms. Specifically: - The `splice_outcomes=True` flag on `Variant.effects()` / `VariantCollection.effects()` / `predict_variant_effects()` is **removed**. Callers passing it explicitly get a `TypeError`. Migration: drop the keyword — wrapping is unconditional. - `Variant.effect_on_transcript(transcript)` and the `FastEffectAnnotator` / `ProteinDiffEffectAnnotator` per-transcript paths return a `SpliceOutcomeSet` for splice-disrupting variants instead of the raw `ExonicSpliceSite` / `SpliceDonor` / etc. class. Migration: replace `isinstance(effect, ExonicSpliceSite)` with `isinstance(effect, SpliceOutcomeSet) and effect.disrupted_signal_class is ExonicSpliceSite`. `effect.alternate_effect` still works as a back-compat alias for `effect.effect_if_splicing_unchanged`, so attribute access keeps working through the wrapper. - `SpliceOutcomeSet.modifies_protein_sequence` is hardcoded to `True` (a splice disruption is always *potentially* protein- modifying via a non-`NormalSplicing` candidate). Closes a long- standing filter bug where `drop_silent_and_noncoding()` silently dropped exonic-splice-site variants whose `NormalSplicing.coding_effect` happened to be `Silent`. - Candidate construction is lazy: only the cheap `NormalSplicing` candidate is built eagerly; `ExonSkipping`, `IntronRetention`, `CrypticDonor` / `CrypticAcceptor` materialise on first `.candidates` access. Pipelines that filter on `modifies_protein_sequence` / `effect_priority` and never read `.candidates` pay only the eager cost. - `SpliceOutcomeSet` is now a `TranscriptMutationEffect` subclass (alongside `MultiOutcomeEffect`), so it carries `gene` / `transcript` and matches the standard `isinstance(effect, TranscriptMutationEffect)` filter used by downstream consumers. - Unified the multi-outcome machinery: `SpliceCandidate` deleted; `MultiOutcomeEffect.outcomes` accessor + `_with_extra_outcomes` helper + `_extra_outcomes` slot removed ([#382](https://github.com/openvax/varcode/issues/382)). - `SpliceOutcomeSet.candidates` now returns `tuple[EffectCandidate, ...]` — the same shape every other `MultiOutcomeEffect` subclass exposes. Each entry wraps an inner `SpliceMechanismEffect` (`NormalSplicing`, `ExonSkipping`, `IntronRetention`, `CrypticDonor`, or `CrypticAcceptor`). Mechanism identity now lives on `type(candidate.effect)`, and the mechanism object carries fields like `affected_exon`, `side`, `cryptic_genomic_position`, `aa_ref`, `aa_alt`, and `mutant_transcript`. The previous `candidate.outcome` / `candidate.plausibility` / `candidate.coding_effect` / `candidate.predicted_class_name` / `candidate.mutant_transcript` / `candidate.has_protein` fields are gone — read provenance off the `EffectCandidate` (`.source`, `.evidence`) and mechanism/protein state off the inner effect (`candidate.effect`, `candidate.effect.mutant_transcript`). `candidate.plausibility` has no one-for-one semantic replacement: it was the old splice-specific name for a DNA-only ordering heuristic, not evidence. There is no shared `EffectCandidate.probability`; producer-specific support belongs in `candidate.evidence` under explicit names. - `MultiOutcomeEffect.candidates` is the single accessor on every subclass (`SpliceOutcomeSet`, `StructuralVariantEffect`, `PhaseCandidateSet`, `ExonicSpliceSite`, `HaplotypeEffect`). A new `MultiOutcomeEffect.effects` convenience property unwraps to `tuple(c.effect for c in self.candidates)` for callers that don't need provenance. - The post-hoc attachment slot renamed `_extra_outcomes` → `_extra_candidates`; the merge helper renamed `_with_extra_outcomes` → `_combine_with_extra_candidates`. `apply_rna_evidence_to_effects` still uses `_extra_candidates` for non-splice multi-outcome effects; splice mechanism sets now reconcile RNA evidence into a replacement set with `rna_evidence`, `added_candidates`, `excluded_candidates`, and `candidate_rna_evidence` audit fields. External integrations that touched these private names must rename. - `StructuralVariantEffect.__init__` parameter renamed `candidates=` → `primary_effects=` (carries the inner `MutationEffect` tuple; the `candidates` accessor now lifts to `EffectCandidate` automatically). Same on `LargeDeletion`, `LargeDuplication`, `GeneFusion`. - `MultiOutcomeEffect.most_likely` is **removed**. Replaced by four explicit accessors so callers never confuse "wrapped vs unwrapped" or "likeliest vs most-disruptive": - `.most_likely_candidate` → `EffectCandidate` (same as `candidates[0]`) - `.most_likely_effect` → inner `MutationEffect` of the above - `.highest_priority_candidate` → `EffectCandidate` with the highest `effect_priority` among candidates (worst-case classification, independent of producer order) - `.highest_priority_effect` → inner `MutationEffect` of the above Callers doing `effect.most_likely.mutant_protein_sequence` or `effect.most_likely.aa_ref` should switch to `effect.most_likely_effect.mutant_protein_sequence` etc.; callers that want the wrapper (with `.source`, `.evidence`) use `effect.most_likely_candidate`. - Splice mechanisms promoted to first-class `MutationEffect` classes, each carrying its own protein vocab (`aa_ref` / `aa_alt` / `mutant_protein_sequence` / `mutant_transcript`) on the instance — no more wrapping a separate coding effect. New hierarchy: - `SpliceMechanismEffect(TranscriptMutationEffect)` — base, carries `splice_signal` referencing the underlying `SpliceDonor` / `SpliceAcceptor` / `IntronicSpliceSite` / `ExonicSpliceSite` so each mechanism knows *where* the disruption was. - `NormalSplicing` — splice signal hit but splicing proceeds; carries `coding_effect` for the underlying nucleotide-level change (or `None` for purely intronic disruption). - `ExonSkipping` — affected exon excluded; carries `affected_exon` and `in_frame`. - `IntronRetention` — intron retained; carries `retained_intron_start`, `retained_intron_end`, `side`. - `CrypticDonor` / `CrypticAcceptor` — cryptic site replaces canonical; carry `affected_exon`, `cryptic_genomic_position`, `motif_score`, `exon_length_delta`. Unresolved state is "protein fields are `None`" — no parallel placeholder class hierarchy. Class identity = mechanism; consumers dispatch on `isinstance(candidate.effect, ExonSkipping)` instead of evidence-key checks. Resolved mechanisms retain their classified protein consequence as `protein_effect` so severity queries (`effect_priority`, `modifies_protein_sequence`) behave like ordinary coding effects while preserving mechanism identity. - **Deleted**: `SpliceOutcome` enum (replaced by class identity), `PredictedIntronRetention` (subsumed by `IntronRetention`), `PredictedCrypticSpliceSite` (split into `CrypticDonor` + `CrypticAcceptor`), `SpliceOutcomeSet.to_dict` / `.from_dict` overrides (no enum left to stringify), `evidence["splice_outcome"]` / `evidence["placeholder"]` / `evidence["description"]` keys (info now lives on the mechanism Effect — `type(candidate.effect)`, `candidate.effect.resolved`, `candidate.effect.short_description`), `_placeholder_effect_for_outcome` and `_make_splice_candidate` helpers. - **`SpliceOutcomeSet.candidate_proteins`** now keys by mechanism class (e.g. `proteins[ExonSkipping]`) instead of `SpliceOutcome` enum value. - `varcode.Outcome` renamed to `varcode.EffectCandidate`. The helper `outcomes_from_candidates` renamed to `candidates_from_effects`. The module `varcode.outcomes` renamed to `varcode.effect_candidates`. The test file `tests/test_outcomes.py` renamed to `tests/test_effect_candidates.py`. The `description` field on the wrapper class is removed — use `candidate.effect.short_description` (it was always a passthrough). Also removed from `make_rna_outcome(description=...)`. No deprecation alias; update imports. The class ships with a module docstring explaining *why* the wrapper exists: the same `MutationEffect` instance can appear in multiple multi-outcome contexts with different per-context provenance (e.g. a splice candidate re-surfaced by the SV annotator with a different `source` tag and `sv_type` in evidence); putting metadata on the wrapper instead of the Effect lets the Effect stay shared while the labels diverge. (Per #382, `.outcomes` has since been removed and `MultiOutcomeEffect.candidates` is the single accessor across every subclass.) Aspirational `"isovar"` / `"exacto"` / `"longread_assembly"` example tags scrubbed from varcode docstrings. - Phasing API generalized; Isovar-named identifiers removed from the varcode public surface ([#378](https://github.com/openvax/varcode/issues/378)). Varcode no longer imports or names any upstream tool — implementations of the new generic Protocols live in their respective packages (e.g. `isovar.IsovarReadPhasing`, openvax/isovar#183). - `IsovarAssemblyProvider` (Protocol) **removed**, split into: - `ReadPhasingSource` with `has_evidence(variant) -> bool` and `partners_in_cis(variant) -> Sequence[Variant]`. - `MutantTranscriptSource` with `mutant_transcript(variant, transcript) -> Optional[MutantTranscript]`. - `IsovarPhaseResolver` renamed to `ReadPhaseResolver`. Constructor accepts any `ReadPhasingSource`; routes `mutant_transcript(...)` to the wrapped source when it also satisfies `MutantTranscriptSource`. Returns `None` otherwise instead of raising. - Resolver `source` tag changed from `"isovar"` to `"read_phasing"` on `ReadPhaseResolver`. Consumers filtering effects by phase source need to update their filter values. - `varcode.effects.effect_classes.PhaseAmbiguousEffect` renamed to `PhaseCandidateSet`. No deprecation alias — update imports ([#376](https://github.com/openvax/varcode/pull/376)). Per #382, the public surface is `.candidates` (a `tuple[EffectCandidate, ...]` with per-hypothesis evidence keys), `.most_likely_candidate` / `.most_likely_effect` / `.highest_priority_candidate` / `.highest_priority_effect`, and `.short_description`. **Changed** - `SpliceOutcomeSet` serialization migrated onto `serializable>=1.1.0`'s standard introspection (the parallel `SpliceCandidate` dataclass that also lived on this path has since been deleted per #382). The `__effect_class__` tagging and hand-rolled class registries (`_CODING_EFFECT_CLASS_REGISTRY`, `_SPLICE_SIGNAL_CLASS_REGISTRY`, `_rehydrate_coding_effect`) are gone; JSON round-trip now flows through `serializable.helpers`' standard `__class__` / `__module__` stamping ([#343](https://github.com/openvax/varcode/issues/343)). `SpliceOutcomeSet.to_dict` / `from_dict` are overridden to stringify the `SpliceOutcome` enum stored under `candidate.evidence["splice_outcome"]` (and rehydrate it on the way back) without mutating `self` mid-call, and to emit a single `candidates` key on the wire (no parallel `_candidates`). The JSON wire format is unchanged, but **the pre-#305 migration shim for the internal `_ExonSkipFrameshiftEffect` class has been removed**. JSON produced by varcode 2.4.x or earlier (which could contain `"__effect_class__": "_ExonSkipFrameshiftEffect"` tags) will no longer rehydrate; re-emit from the current annotator or hand-patch those tags to `FrameShift` before loading. Anyone still reading such payloads can pin `varcode<4.7` or resurrect the shim in user code. **Changed** - README "Effect Types" section rewritten: 14 missing concrete effect classes added (`Failure`, the splice mechanisms, the structural-variant effects, `CrypticExonCandidate`, `HaplotypeEffect`, `PhaseCandidateSet`), grouped into 7 sub-tables by biological context, with a new intro explaining `MultiOutcomeEffect` and how multi-possibility effects are represented. Each class name links to its source definition via GitHub text-fragment URLs that survive line-number drift. - Documented and streamlined the splice-effect model: clarified that `SpliceSite` effects (`SpliceDonor` / `SpliceAcceptor` / `IntronicSpliceSite` / `ExonicSpliceSite`) describe *where* a splice signal was hit and carry no protein consequence on their own, while `SpliceMechanismEffect` subclasses describe *what* the spliceosome does and carry the protein change. `SpliceSite` (previously an undocumented marker base) is now the documented, load-bearing type: `enumerate_splice_outcomes` gates on `isinstance(effect, SpliceSite)` rather than enumerating the four subclasses. Documented the `SpliceOutcomeSet.disrupted_signal_class` (a `SpliceSite` *subclass*, i.e. a type, for priority lookup) vs each candidate's `effect.splice_signal` (a `SpliceSite` *instance*) distinction. No behavior change. **Added** - Docs site now has an "Effect types" page that auto-renders every class in `varcode.effects.effect_classes` via mkdocstrings, so the documented catalog stays in sync with the code (previously only the abstract bases were in the API reference, while the full list lived only in the hand-maintained README table). - Exported the splice-signal disruption effects at the package root for parity with the already-public splice mechanism effects: `SpliceSite` (the shared base), `SpliceDonor`, `SpliceAcceptor`, `IntronicSpliceSite`, and `ExonicSpliceSite`. `from varcode import SpliceSite` now works, so `isinstance(effect, SpliceSite)` can be used to catch any splice-site disruption without reaching into `varcode.effects.effect_classes`. ## [v2.3.0](https://github.com/openvax/varcode/tree/v2.3.0) (2026-04-13) **Added** - Per-sample genotype / zygosity access ([#267](https://github.com/openvax/varcode/issues/267)). `Genotype` frozen dataclass, `Zygosity` enum (`ABSENT`/`HETEROZYGOUS`/`HOMOZYGOUS`/`MISSING`), new `VariantCollection` methods `.samples`, `.genotype(variant, sample)`, `.zygosity(variant, sample)`, `.for_sample(name)`, `.heterozygous_in(name)`, `.homozygous_alt_in(name)`. Multi-allelic aware: each split Variant reports zygosity relative to its own alt. - `varcode.SampleNotFoundError(KeyError)` raised on typoed sample names. ## [v2.2.1](https://github.com/openvax/varcode/tree/v2.2.1) (2026-04-13) **Fixed** - Ref-vs-genome mismatches now raise a dedicated `varcode.ReferenceMismatchError` (subclass of `ValueError`) with an actionable message naming the likely causes and pointing at `raise_on_error=False` ([#215](https://github.com/openvax/varcode/issues/215), [#246](https://github.com/openvax/varcode/issues/246)). ## [v2.2.0](https://github.com/openvax/varcode/tree/v2.2.0) (2026-04-12) **Added** - `from_csv` now accepts either `chr` or `contig` as the contig column name on both `VariantCollection` and `EffectCollection`, so CSVs are interchangeable between the two types ([#274](https://github.com/openvax/varcode/issues/274)). - `from_csv` warns on major `varcode_version` drift recorded in the CSV header ([#275](https://github.com/openvax/varcode/issues/275)). **Changed** - `from_csv` docstrings now point users at `from_json` for byte-for-byte round-trip or larger collections ([#276](https://github.com/openvax/varcode/issues/276)). ## [v2.1.0](https://github.com/openvax/varcode/tree/v2.1.0) (2026-04-12) **Added** - `VariantCollection.from_csv` and `EffectCollection.from_csv` for round-trip deserialization ([#273](https://github.com/openvax/varcode/pull/273)). - `to_csv` prepends a `# key=value` metadata header by default (`varcode_version`, `reference_name`); `from_csv` reads it so the `genome` argument becomes optional. Pass `include_header=False` for legacy consumers. **Fixed** - `VariantCollection.variants` and `EffectCollection.effects` now match the collection's iteration order instead of holding the raw pre-sort / pre-dedup input list ([#220](https://github.com/openvax/varcode/issues/220)). ## [v2.0.0](https://github.com/openvax/varcode/tree/v2.0.0) (2026-04-11) Major release — several backward-incompatible fixes. See [#263](https://github.com/openvax/varcode/pull/263) and [#265](https://github.com/openvax/varcode/pull/265) for full details. **Breaking** - `Silent.short_description` returns HGVS `p.{ref}{pos}=` (e.g. `p.R6=`) instead of the literal `"silent"` ([#217](https://github.com/openvax/varcode/issues/217)). - `Silent.aa_pos` no longer includes the shared-prefix offset; it now points at the actual synonymous codon ([#208](https://github.com/openvax/varcode/issues/208)). - `PrematureStop.short_description` returns `p.{pos}ins{alt}*` when `aa_ref` is empty instead of the ambiguous `p.{pos}{alt}*` ([#216](https://github.com/openvax/varcode/issues/216)). - `EffectCollection` is sorted by effect priority (most severe first) by default. Pass `sort_key=False` to disable or a custom callable to override ([#227](https://github.com/openvax/varcode/issues/227)). - Intronic splice classification is sequence-aware: variants at `+1`/`+2` or `-1`/`-2` with a non-canonical reference base are classified as `IntronicSpliceSite` rather than `SpliceDonor`/`SpliceAcceptor` ([#262](https://github.com/openvax/varcode/issues/262)). **Fixed** - SNV in the stop codon with a stop-prefixed 3' UTR is correctly classified as `StopLoss` instead of `Insertion` ([#250](https://github.com/openvax/varcode/issues/250), [#205](https://github.com/openvax/varcode/issues/205)). - Insertion before the stop codon that produces an identical protein is correctly classified as `Silent` ([#201](https://github.com/openvax/varcode/issues/201)). - `changes_exonic_splice_site` now applies the mutation before checking the splice pattern ([#262](https://github.com/openvax/varcode/issues/262)). - VCF loader skips symbolic alleles (``, ``, ``, ...) and breakend notation with a visible warning instead of crashing ([#88](https://github.com/openvax/varcode/issues/88)). Full SV support is tracked in [#264](https://github.com/openvax/varcode/issues/264). ## [v0.5.15](https://github.com/hammerlab/varcode/tree/v0.5.15) (2017-04-28) [Full Changelog](https://github.com/hammerlab/varcode/compare/v0.5.14...v0.5.15) **Closed issues:** - Allow contig to be empty to allow parsing of MAF with faulty mutation\(s\) [\#210](https://github.com/hammerlab/varcode/issues/210) **Merged pull requests:** - Fixes to load\_maf [\#223](https://github.com/hammerlab/varcode/pull/223) ([tavinathanson](https://github.com/tavinathanson)) - added raise\_on\_error option to load\_maf and load\_maf\_dataframe [\#221](https://github.com/hammerlab/varcode/pull/221) ([iskandr](https://github.com/iskandr)) - Optionally allow duplicated mutations when using load\_vcf or load\_maf. Fixes \#211 [\#212](https://github.com/hammerlab/varcode/pull/212) ([tuomastik](https://github.com/tuomastik)) ## [v0.5.14](https://github.com/hammerlab/varcode/tree/v0.5.14) (2017-04-05) [Full Changelog](https://github.com/hammerlab/varcode/compare/v0.5.13...v0.5.14) **Merged pull requests:** - Adding 'distinct' as a parameter to load\_vcf. [\#222](https://github.com/hammerlab/varcode/pull/222) ([julia326](https://github.com/julia326)) ## [v0.5.13](https://github.com/hammerlab/varcode/tree/v0.5.13) (2017-04-01) [Full Changelog](https://github.com/hammerlab/varcode/compare/v0.5.12...v0.5.13) **Closed issues:** - Effect prediction throws error \(even if raise\_on\_error=False\) [\#213](https://github.com/hammerlab/varcode/issues/213) - Optionally allow duplicated mutations when using load\_vcf or load\_maf [\#211](https://github.com/hammerlab/varcode/issues/211) **Merged pull requests:** - install ensembl 87 on travis [\#219](https://github.com/hammerlab/varcode/pull/219) ([iskandr](https://github.com/iskandr)) - Allow user to affect the sorting of variants when loading a VCF or MAF. [\#218](https://github.com/hammerlab/varcode/pull/218) ([tuomastik](https://github.com/tuomastik)) ## [v0.5.12](https://github.com/hammerlab/varcode/tree/v0.5.12) (2017-01-18) [Full Changelog](https://github.com/hammerlab/varcode/compare/v0.5.11...v0.5.12) **Closed issues:** - Make Varcode correctly infer genome for b37-decoy string [\#207](https://github.com/hammerlab/varcode/issues/207) - Longer indels in random variants [\#47](https://github.com/hammerlab/varcode/issues/47) - Predict coding sequence of StartLoss mutations [\#4](https://github.com/hammerlab/varcode/issues/4) **Merged pull requests:** - Add optional\_cols list to load\_maf [\#209](https://github.com/hammerlab/varcode/pull/209) ([tavinathanson](https://github.com/tavinathanson)) ## [v0.5.11](https://github.com/hammerlab/varcode/tree/v0.5.11) (2016-12-05) [Full Changelog](https://github.com/hammerlab/varcode/compare/v0.5.10...v0.5.11) **Fixed bugs:** - Varcode noncoding variant in a drop\_silent\_and\_noncoding\(\) list [\#200](https://github.com/hammerlab/varcode/issues/200) **Merged pull requests:** - Adding aa\_ref argument to StopLoss for variants which delete codons before stop [\#203](https://github.com/hammerlab/varcode/pull/203) ([iskandr](https://github.com/iskandr)) ## [v0.5.10](https://github.com/hammerlab/varcode/tree/v0.5.10) (2016-10-19) [Full Changelog](https://github.com/hammerlab/varcode/compare/v0.5.9...v0.5.10) **Fixed bugs:** - Variant pickling won't work for not-human and non-EnsemblRelease Genomes [\#147](https://github.com/hammerlab/varcode/issues/147) **Closed issues:** - Link on PyPI badge broken [\#191](https://github.com/hammerlab/varcode/issues/191) - Reference incorrectly inferred when "b36" in reference file path [\#181](https://github.com/hammerlab/varcode/issues/181) - Premature stop codon error [\#166](https://github.com/hammerlab/varcode/issues/166) **Merged pull requests:** - explicit args to \_\_init\_\_ of Intronic splice effects fixes serialization [\#199](https://github.com/hammerlab/varcode/pull/199) ([iskandr](https://github.com/iskandr)) - Update RELEASING.md, fixing tagging instructions [\#198](https://github.com/hammerlab/varcode/pull/198) ([julia326](https://github.com/julia326)) ## [v0.5.9](https://github.com/hammerlab/varcode/tree/v0.5.9) (2016-10-11) [Full Changelog](https://github.com/hammerlab/varcode/compare/v0.5.8...v0.5.9) **Fixed bugs:** - StopLoss pickling is broken [\#188](https://github.com/hammerlab/varcode/issues/188) **Closed issues:** - One logger per module [\#196](https://github.com/hammerlab/varcode/issues/196) - SNV results in deletion [\#193](https://github.com/hammerlab/varcode/issues/193) **Merged pull requests:** - One logger per module. [\#197](https://github.com/hammerlab/varcode/pull/197) ([julia326](https://github.com/julia326)) - Fix edge case where PrematureStop in last amino acid got interpreted as a Deletion [\#194](https://github.com/hammerlab/varcode/pull/194) ([iskandr](https://github.com/iskandr)) - Fix inferred-reference-bug [\#182](https://github.com/hammerlab/varcode/pull/182) ([jburos](https://github.com/jburos)) ## [v0.5.8](https://github.com/hammerlab/varcode/tree/v0.5.8) (2016-09-28) [Full Changelog](https://github.com/hammerlab/varcode/compare/v0.5.7...v0.5.8) **Merged pull requests:** - changed Markdown image links to HTML [\#192](https://github.com/hammerlab/varcode/pull/192) ([iskandr](https://github.com/iskandr)) ## [v0.5.7](https://github.com/hammerlab/varcode/tree/v0.5.7) (2016-09-28) [Full Changelog](https://github.com/hammerlab/varcode/compare/v0.5.3...v0.5.7) ## [v0.5.3](https://github.com/hammerlab/varcode/tree/v0.5.3) (2016-09-28) [Full Changelog](https://github.com/hammerlab/varcode/compare/v0.5.2...v0.5.3) **Merged pull requests:** - Use conda to install pypandoc [\#189](https://github.com/hammerlab/varcode/pull/189) ([arahuja](https://github.com/arahuja)) - Ensure README.md is packaged [\#186](https://github.com/hammerlab/varcode/pull/186) ([arahuja](https://github.com/arahuja)) - Upgrade serializable dependency with tests [\#185](https://github.com/hammerlab/varcode/pull/185) ([arahuja](https://github.com/arahuja)) - Add pypi badge [\#184](https://github.com/hammerlab/varcode/pull/184) ([arahuja](https://github.com/arahuja)) ## [v0.5.2](https://github.com/hammerlab/varcode/tree/v0.5.2) (2016-09-28) [Full Changelog](https://github.com/hammerlab/varcode/compare/v0.5.1...v0.5.2) **Closed issues:** - Make sure `Variant` works with any Genome \(not just a human EnsemblRelease\) [\#127](https://github.com/hammerlab/varcode/issues/127) **Merged pull requests:** - Move extraneous variables to properties for normalization [\#190](https://github.com/hammerlab/varcode/pull/190) ([tavinathanson](https://github.com/tavinathanson)) - Use is\_protein\_coding property of pyensembl.Transcript and pyensembl.Gene [\#180](https://github.com/hammerlab/varcode/pull/180) ([iskandr](https://github.com/iskandr)) ## [v0.5.1](https://github.com/hammerlab/varcode/tree/v0.5.1) (2016-09-16) [Full Changelog](https://github.com/hammerlab/varcode/compare/v0.5.0...v0.5.1) **Merged pull requests:** - Add MutationEffect to \_\_init\_\_.py [\#178](https://github.com/hammerlab/varcode/pull/178) ([timodonnell](https://github.com/timodonnell)) ## [v0.5.0](https://github.com/hammerlab/varcode/tree/v0.5.0) (2016-09-13) [Full Changelog](https://github.com/hammerlab/varcode/compare/v0.4.19...v0.5.0) **Implemented enhancements:** - Support collection.as\_dataframe\(\) [\#128](https://github.com/hammerlab/varcode/issues/128) **Closed issues:** - Substitution mis-annotated as stop-loss [\#176](https://github.com/hammerlab/varcode/issues/176) - Wrong aa\_mutation\_end\_offset for insertion of stop codon [\#175](https://github.com/hammerlab/varcode/issues/175) - Wrong aa\_ref for insertion of stop codon [\#174](https://github.com/hammerlab/varcode/issues/174) - Insertions after the stop codon annotated as plain Insertions [\#172](https://github.com/hammerlab/varcode/issues/172) - Mutations before the stop codon confused as StopLosses [\#171](https://github.com/hammerlab/varcode/issues/171) - StopLosses do not translate into 3' UTR [\#170](https://github.com/hammerlab/varcode/issues/170) - Insertion of stop codon is annotated as simple Insertion and not PrematureStop [\#169](https://github.com/hammerlab/varcode/issues/169) - Synonimous FrameShift over stop codon not annotated as silent [\#168](https://github.com/hammerlab/varcode/issues/168) - Wrong offset for insertion of StopCodon [\#167](https://github.com/hammerlab/varcode/issues/167) - Document release process [\#154](https://github.com/hammerlab/varcode/issues/154) - compare variants that use different references [\#83](https://github.com/hammerlab/varcode/issues/83) - Annotate with predicted pathogenicity [\#46](https://github.com/hammerlab/varcode/issues/46) **Merged pull requests:** - Reorganize effect prediction code, fixed annotation bugs/issues [\#173](https://github.com/hammerlab/varcode/pull/173) ([iskandr](https://github.com/iskandr)) ## [v0.4.19](https://github.com/hammerlab/varcode/tree/v0.4.19) (2016-09-12) [Full Changelog](https://github.com/hammerlab/varcode/compare/v0.4.18...v0.4.19) **Fixed bugs:** - original\_start doesn't get pickled [\#141](https://github.com/hammerlab/varcode/issues/141) **Closed issues:** - replace `load\_vcf` with `load\_vcf\_fast` ? [\#144](https://github.com/hammerlab/varcode/issues/144) - Add `annotate\_random\_variants` commandline script [\#49](https://github.com/hammerlab/varcode/issues/49) - support filtering a variant collection to variants overlapping specified gene names [\#32](https://github.com/hammerlab/varcode/issues/32) - Use SPANR to identify splicing misregulation [\#2](https://github.com/hammerlab/varcode/issues/2) ## [v0.4.18](https://github.com/hammerlab/varcode/tree/v0.4.18) (2016-08-08) [Full Changelog](https://github.com/hammerlab/varcode/compare/v0.4.17...v0.4.18) **Closed issues:** - vcf unit tests broken in python 3 \(?\) [\#164](https://github.com/hammerlab/varcode/issues/164) - maximum recursion depth exceeded when loading a vcf from a URL [\#163](https://github.com/hammerlab/varcode/issues/163) **Merged pull requests:** - In load\_vcf, when passed a URL download it first to a local file then… [\#165](https://github.com/hammerlab/varcode/pull/165) ([timodonnell](https://github.com/timodonnell)) - Removed Collection from varcode, moved to separate 'sercol' repo instead [\#162](https://github.com/hammerlab/varcode/pull/162) ([iskandr](https://github.com/iskandr)) ## [v0.4.17](https://github.com/hammerlab/varcode/tree/v0.4.17) (2016-08-05) [Full Changelog](https://github.com/hammerlab/varcode/compare/v0.4.16...v0.4.17) **Merged pull requests:** - Commandline interface, simplified serialization, merging VariantCollections [\#161](https://github.com/hammerlab/varcode/pull/161) ([iskandr](https://github.com/iskandr)) ## [v0.4.16](https://github.com/hammerlab/varcode/tree/v0.4.16) (2016-07-30) [Full Changelog](https://github.com/hammerlab/varcode/compare/v0.4.15...v0.4.16) ## [v0.4.15](https://github.com/hammerlab/varcode/tree/v0.4.15) (2016-07-15) [Full Changelog](https://github.com/hammerlab/varcode/compare/v0.4.14...v0.4.15) **Fixed bugs:** - Fix clone\_with\_new\_elements for VariantCollection [\#159](https://github.com/hammerlab/varcode/pull/159) ([tavinathanson](https://github.com/tavinathanson)) **Closed issues:** - load\_vcf\_fast fails when sample names contain spaces [\#158](https://github.com/hammerlab/varcode/issues/158) **Merged pull requests:** - Fix load\_vcf\_fast for sample names containing a space character [\#160](https://github.com/hammerlab/varcode/pull/160) ([timodonnell](https://github.com/timodonnell)) ## [v0.4.14](https://github.com/hammerlab/varcode/tree/v0.4.14) (2016-06-07) [Full Changelog](https://github.com/hammerlab/varcode/compare/v0.4.12...v0.4.14) **Merged pull requests:** - Don't memoize so much [\#157](https://github.com/hammerlab/varcode/pull/157) ([iskandr](https://github.com/iskandr)) ## [v0.4.12](https://github.com/hammerlab/varcode/tree/v0.4.12) (2016-05-28) [Full Changelog](https://github.com/hammerlab/varcode/compare/v0.4.13...v0.4.12) ## [v0.4.13](https://github.com/hammerlab/varcode/tree/v0.4.13) (2016-05-28) [Full Changelog](https://github.com/hammerlab/varcode/compare/v0.4.11...v0.4.13) **Merged pull requests:** - Fix versioneer prefix format [\#155](https://github.com/hammerlab/varcode/pull/155) ([armish](https://github.com/armish)) ## [v0.4.11](https://github.com/hammerlab/varcode/tree/v0.4.11) (2016-05-27) [Full Changelog](https://github.com/hammerlab/varcode/compare/v0.4.10...v0.4.11) ## [v0.4.10](https://github.com/hammerlab/varcode/tree/v0.4.10) (2016-05-27) [Full Changelog](https://github.com/hammerlab/varcode/compare/v0.4.9...v0.4.10) ## [v0.4.9](https://github.com/hammerlab/varcode/tree/v0.4.9) (2016-05-27) [Full Changelog](https://github.com/hammerlab/varcode/compare/v0.4.8...v0.4.9) **Closed issues:** - Add serialization for EffectCollection and VariantCollection [\#71](https://github.com/hammerlab/varcode/issues/71) **Merged pull requests:** - Reorganized coding effects to use KnownAminoAcidChange base class [\#153](https://github.com/hammerlab/varcode/pull/153) ([iskandr](https://github.com/iskandr)) ## [v0.4.8](https://github.com/hammerlab/varcode/tree/v0.4.8) (2016-05-27) [Full Changelog](https://github.com/hammerlab/varcode/compare/v0.4.2...v0.4.8) **Fixed bugs:** - Potentially wrong translated sequence from frameshift on mm10 [\#151](https://github.com/hammerlab/varcode/issues/151) **Closed issues:** - ExonicSpliceSite mutations are classified as Noncoding [\#136](https://github.com/hammerlab/varcode/issues/136) - Filter field is not saved after loading a VCF [\#89](https://github.com/hammerlab/varcode/issues/89) - investigate porting read evidence module to use impala [\#69](https://github.com/hammerlab/varcode/issues/69) - Attach genotypes and other sample information to Variants [\#30](https://github.com/hammerlab/varcode/issues/30) - support determining the evidence for a variant in a bam [\#26](https://github.com/hammerlab/varcode/issues/26) **Merged pull requests:** - Added unit tests for Klf6 frameshift, fix bug in frameshift translation [\#152](https://github.com/hammerlab/varcode/pull/152) ([iskandr](https://github.com/iskandr)) - Add as\_dataframe to EffectCollection [\#150](https://github.com/hammerlab/varcode/pull/150) ([arahuja](https://github.com/arahuja)) - Use versioneer to manage version number [\#149](https://github.com/hammerlab/varcode/pull/149) ([arahuja](https://github.com/arahuja)) - Fix pyvcf error from passing \_parse\_samples a tuple instead of a list [\#148](https://github.com/hammerlab/varcode/pull/148) ([timodonnell](https://github.com/timodonnell)) - Fix variant pickling [\#146](https://github.com/hammerlab/varcode/pull/146) ([tavinathanson](https://github.com/tavinathanson)) - Parse and expose sample info, including for multisample VCFs [\#145](https://github.com/hammerlab/varcode/pull/145) ([timodonnell](https://github.com/timodonnell)) - Preserve contig name [\#140](https://github.com/hammerlab/varcode/pull/140) ([iskandr](https://github.com/iskandr)) - Quotes around nucleotides in Variant representation [\#139](https://github.com/hammerlab/varcode/pull/139) ([iskandr](https://github.com/iskandr)) - added is\_deletion, is\_insertion, and is\_indel properties to variants [\#138](https://github.com/hammerlab/varcode/pull/138) ([iskandr](https://github.com/iskandr)) ## [v0.4.2](https://github.com/hammerlab/varcode/tree/v0.4.2) (2016-02-25) **Implemented enhancements:** - VariantCollection.high\_priority\_effect != Variant.top\_effect [\#58](https://github.com/hammerlab/varcode/issues/58) - Improves the documentation for varcode [\#110](https://github.com/hammerlab/varcode/pull/110) ([armish](https://github.com/armish)) - Convert effect-type section into a sorted table [\#104](https://github.com/hammerlab/varcode/pull/104) ([armish](https://github.com/armish)) - Start highlighting Python syntax in README [\#103](https://github.com/hammerlab/varcode/pull/103) ([armish](https://github.com/armish)) **Fixed bugs:** - Varcode requires pandas \>= 0.13.1, however it uses 0.15 functionality \#12 [\#92](https://github.com/hammerlab/varcode/issues/92) - Varcode version 0.3.10 cannot be imported when installed through pip [\#90](https://github.com/hammerlab/varcode/issues/90) - pip installing Varcode doesn't seem to work lately [\#84](https://github.com/hammerlab/varcode/issues/84) - AttributeError: 'FrameShiftTruncation' object has no attribute 'aa\_alt' [\#70](https://github.com/hammerlab/varcode/issues/70) - Use find\_packages correctly [\#85](https://github.com/hammerlab/varcode/pull/85) ([tavinathanson](https://github.com/tavinathanson)) **Closed issues:** - memoize a bit less [\#131](https://github.com/hammerlab/varcode/issues/131) - Intragenic variants do not have a short\_description field [\#129](https://github.com/hammerlab/varcode/issues/129) - move read\_evidence module and Locus class to varlens [\#124](https://github.com/hammerlab/varcode/issues/124) - Support Structural Variants [\#122](https://github.com/hammerlab/varcode/issues/122) - PrematureStop called as Silent [\#116](https://github.com/hammerlab/varcode/issues/116) - PrematureStop called as a Deletion [\#111](https://github.com/hammerlab/varcode/issues/111) - UnboundLocalError in in\_frame\_coding\_effect.py [\#107](https://github.com/hammerlab/varcode/issues/107) - Double mutations in a MAF file cause error [\#105](https://github.com/hammerlab/varcode/issues/105) - varcode.load\_vcf\_fast used 0.16.1 Pandas options [\#101](https://github.com/hammerlab/varcode/issues/101) - Configuring datacache default cache directory [\#98](https://github.com/hammerlab/varcode/issues/98) - Improve the README to include some examples of working with Varcode in IPython [\#95](https://github.com/hammerlab/varcode/issues/95) - support loading VCFs over HTTP [\#91](https://github.com/hammerlab/varcode/issues/91) - Travis should include setup.py testing [\#86](https://github.com/hammerlab/varcode/issues/86) - Make Variants pickle-able [\#77](https://github.com/hammerlab/varcode/issues/77) - modifies\_coding\_sequence is always false [\#64](https://github.com/hammerlab/varcode/issues/64) - AssertionError: aa\_ref and aa\_alt can't both be empty string [\#63](https://github.com/hammerlab/varcode/issues/63) - Too many open files on error on getting top effect [\#62](https://github.com/hammerlab/varcode/issues/62) - KeyError: 'reference' in load\_vcf [\#60](https://github.com/hammerlab/varcode/issues/60) - Issue with n\_skip? [\#56](https://github.com/hammerlab/varcode/issues/56) - Optional random seed argument for generating random variants [\#48](https://github.com/hammerlab/varcode/issues/48) - An argument for using == and not \>= for requirements? [\#43](https://github.com/hammerlab/varcode/issues/43) - deploy a test coverage tool [\#38](https://github.com/hammerlab/varcode/issues/38) - Replace raise\_on\_error parameter to property of VariantCollection [\#36](https://github.com/hammerlab/varcode/issues/36) - assertion error in infer\_coding\_effect [\#33](https://github.com/hammerlab/varcode/issues/33) - add a memoized "highest\_priority\_effect" property to Variant [\#31](https://github.com/hammerlab/varcode/issues/31) - support deep reloading varcode module [\#25](https://github.com/hammerlab/varcode/issues/25) - handle multiallelic variants [\#22](https://github.com/hammerlab/varcode/issues/22) - vcf.load\_vcf should provide an option to load all variants, regardless of whether filter is PASS [\#21](https://github.com/hammerlab/varcode/issues/21) - empty variant collection when loading strelka vcf [\#16](https://github.com/hammerlab/varcode/issues/16) - Incorrect handling of variants which run past the beginning/end of an exon's boundary [\#14](https://github.com/hammerlab/varcode/issues/14) - Reference amino acid sequence sometimes empty for coding variants [\#12](https://github.com/hammerlab/varcode/issues/12) - handle single-sample VCFs with INFO fields containing list values of size \> 1 [\#9](https://github.com/hammerlab/varcode/issues/9) - Do FrameShift \(or StopGain\) mutations affect splicing? [\#6](https://github.com/hammerlab/varcode/issues/6) - What to do with mutations that span the 5' UTR / CDS boundary? [\#5](https://github.com/hammerlab/varcode/issues/5) - Annotate essential splice site mutations [\#1](https://github.com/hammerlab/varcode/issues/1) **Merged pull requests:** - Modest change to filtering of coding mutations include ExonicSpliceSite [\#137](https://github.com/hammerlab/varcode/pull/137) ([iskandr](https://github.com/iskandr)) - Version bump [\#135](https://github.com/hammerlab/varcode/pull/135) ([tavinathanson](https://github.com/tavinathanson)) - Fix conda install on Travis [\#134](https://github.com/hammerlab/varcode/pull/134) ([iskandr](https://github.com/iskandr)) - Don't memoize EffectCollection.top\_priority\_effect\(\) [\#132](https://github.com/hammerlab/varcode/pull/132) ([timodonnell](https://github.com/timodonnell)) - All effects should have a default `short\_description` field [\#130](https://github.com/hammerlab/varcode/pull/130) ([armish](https://github.com/armish)) - Remove read\_evidence and locus modules [\#125](https://github.com/hammerlab/varcode/pull/125) ([timodonnell](https://github.com/timodonnell)) - Include a link to the iPython notebook in README.md [\#121](https://github.com/hammerlab/varcode/pull/121) ([armish](https://github.com/armish)) - Add varcode to Travis [\#120](https://github.com/hammerlab/varcode/pull/120) ([tavinathanson](https://github.com/tavinathanson)) - Minor problem in Variant.\_\_init\_\_ [\#119](https://github.com/hammerlab/varcode/pull/119) ([iskandr](https://github.com/iskandr)) - Update Varcode to work with new multi-species PyEnsembl [\#118](https://github.com/hammerlab/varcode/pull/118) ([iskandr](https://github.com/iskandr)) - Fix \#116 and call PrematureStop when stop codon is added in the middle of an insertion [\#117](https://github.com/hammerlab/varcode/pull/117) ([leekaiinthesky](https://github.com/leekaiinthesky)) - Warn when variants in MAF file have wrong end position [\#115](https://github.com/hammerlab/varcode/pull/115) ([iskandr](https://github.com/iskandr)) - Bump pyensembl/varcode version [\#114](https://github.com/hammerlab/varcode/pull/114) ([tavinathanson](https://github.com/tavinathanson)) - fix logic for determining whether the protein length decreases [\#112](https://github.com/hammerlab/varcode/pull/112) ([leekaiinthesky](https://github.com/leekaiinthesky)) - decreasing 3' splice site to distance 3 from boundary [\#109](https://github.com/hammerlab/varcode/pull/109) ([iskandr](https://github.com/iskandr)) - fixed typo in effect inference, added breaking variant to unit tests [\#108](https://github.com/hammerlab/varcode/pull/108) ([iskandr](https://github.com/iskandr)) - Allow Varcode to work with mouse data via Genome [\#106](https://github.com/hammerlab/varcode/pull/106) ([tavinathanson](https://github.com/tavinathanson)) - Manually set compression in read\_vcf\_into\_dataframe [\#102](https://github.com/hammerlab/varcode/pull/102) ([timodonnell](https://github.com/timodonnell)) - Added examples to README [\#100](https://github.com/hammerlab/varcode/pull/100) ([iskandr](https://github.com/iskandr)) - depend on pandas \>= 0.15 [\#99](https://github.com/hammerlab/varcode/pull/99) ([iskandr](https://github.com/iskandr)) - Faster VCFs loading, support HTTP, and refactored variant metadata [\#94](https://github.com/hammerlab/varcode/pull/94) ([timodonnell](https://github.com/timodonnell)) - Support for regular varcode variant instances in read evidence module [\#87](https://github.com/hammerlab/varcode/pull/87) ([timodonnell](https://github.com/timodonnell)) - Read and write json files [\#82](https://github.com/hammerlab/varcode/pull/82) ([iskandr](https://github.com/iskandr)) - JSON serialization for VariantCollection. [\#81](https://github.com/hammerlab/varcode/pull/81) ([timodonnell](https://github.com/timodonnell)) - Add short\_description field to intergenic variants [\#80](https://github.com/hammerlab/varcode/pull/80) ([timodonnell](https://github.com/timodonnell)) - Speed up PileupCollection.group\_by\_allele [\#79](https://github.com/hammerlab/varcode/pull/79) ([timodonnell](https://github.com/timodonnell)) - Variant serialization [\#78](https://github.com/hammerlab/varcode/pull/78) ([timodonnell](https://github.com/timodonnell)) - added option for genome name in load\_vcf [\#76](https://github.com/hammerlab/varcode/pull/76) ([iskandr](https://github.com/iskandr)) - Fix variant.effects\(\) to always return an EffectCollection [\#75](https://github.com/hammerlab/varcode/pull/75) ([timodonnell](https://github.com/timodonnell)) - Bump pysam dependency [\#74](https://github.com/hammerlab/varcode/pull/74) ([timodonnell](https://github.com/timodonnell)) - Cufflinks RNA filtering [\#73](https://github.com/hammerlab/varcode/pull/73) ([iskandr](https://github.com/iskandr)) - Read evidence tweaks [\#72](https://github.com/hammerlab/varcode/pull/72) ([timodonnell](https://github.com/timodonnell)) - Filter effect collection by expression [\#67](https://github.com/hammerlab/varcode/pull/67) ([iskandr](https://github.com/iskandr)) - Created EpitopeCollection, refactored effects, fix assertion failure while annotating silent stop codon [\#66](https://github.com/hammerlab/varcode/pull/66) ([iskandr](https://github.com/iskandr)) - Created EpitopeCollection, refactored effects [\#65](https://github.com/hammerlab/varcode/pull/65) ([iskandr](https://github.com/iskandr)) - include substitution in high priority effects [\#61](https://github.com/hammerlab/varcode/pull/61) ([arahuja](https://github.com/arahuja)) - don't annotate StopLoss variants that are immediately followed by another stop codon [\#57](https://github.com/hammerlab/varcode/pull/57) ([iskandr](https://github.com/iskandr)) - Refactor coding effect [\#55](https://github.com/hammerlab/varcode/pull/55) ([iskandr](https://github.com/iskandr)) - Add read\_evidence module [\#53](https://github.com/hammerlab/varcode/pull/53) ([timodonnell](https://github.com/timodonnell)) - Use transcript protein sequence [\#45](https://github.com/hammerlab/varcode/pull/45) ([iskandr](https://github.com/iskandr)) - Add contributing md [\#41](https://github.com/hammerlab/varcode/pull/41) ([iskandr](https://github.com/iskandr)) - Small coding effect refactoring and fixes [\#39](https://github.com/hammerlab/varcode/pull/39) ([iskandr](https://github.com/iskandr)) - Test problematic variants [\#37](https://github.com/hammerlab/varcode/pull/37) ([iskandr](https://github.com/iskandr)) - Typechecks and test fixes [\#35](https://github.com/hammerlab/varcode/pull/35) ([timodonnell](https://github.com/timodonnell)) - Fix maf parsing [\#34](https://github.com/hammerlab/varcode/pull/34) ([iskandr](https://github.com/iskandr)) - parse multiple alleles into distinct Variant records [\#29](https://github.com/hammerlab/varcode/pull/29) ([iskandr](https://github.com/iskandr)) - PEP8 & pyflakes fixes [\#28](https://github.com/hammerlab/varcode/pull/28) ([iskandr](https://github.com/iskandr)) - Remove pyfaidx [\#27](https://github.com/hammerlab/varcode/pull/27) ([iskandr](https://github.com/iskandr)) - Variant collection tweaks [\#24](https://github.com/hammerlab/varcode/pull/24) ([timodonnell](https://github.com/timodonnell)) - Improved vcf parsing [\#23](https://github.com/hammerlab/varcode/pull/23) ([timodonnell](https://github.com/timodonnell)) - Associate EnsemblRelease with each Variant object [\#20](https://github.com/hammerlab/varcode/pull/20) ([iskandr](https://github.com/iskandr)) - Variant collection filtering [\#19](https://github.com/hammerlab/varcode/pull/19) ([iskandr](https://github.com/iskandr)) - added IntronicSpliceSite, SpliceDonor, SpliceAcceptor effects [\#17](https://github.com/hammerlab/varcode/pull/17) ([iskandr](https://github.com/iskandr)) - collect effect annotation errors in dictionary, only look up overlapping... [\#13](https://github.com/hammerlab/varcode/pull/13) ([iskandr](https://github.com/iskandr)) - don't flatten INFO dictionary of VCF, lists are part of the field format [\#11](https://github.com/hammerlab/varcode/pull/11) ([iskandr](https://github.com/iskandr)) - Small fixes [\#10](https://github.com/hammerlab/varcode/pull/10) ([timodonnell](https://github.com/timodonnell)) - Add support for Python 3 [\#8](https://github.com/hammerlab/varcode/pull/8) ([timodonnell](https://github.com/timodonnell)) - Refactor core logic [\#7](https://github.com/hammerlab/varcode/pull/7) ([iskandr](https://github.com/iskandr)) - Classes for protein/transcript variant effects [\#3](https://github.com/hammerlab/varcode/pull/3) ([iskandr](https://github.com/iskandr)) \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* openvax-varcode-d96eb06/CONTRIBUTING.md000066400000000000000000000117171526602674500175110ustar00rootroot00000000000000# Contributing to Varcode Varcode supports Python 3.11 and later. Read [AGENTS.md](AGENTS.md) for the repository workflow and [RELEASING.md](RELEASING.md) before preparing a release. The [2026-09-24 audit](notes/development_audit.md) records known quality gaps and their issue links. ## Report a problem Search the [open issues](https://github.com/openvax/varcode/issues) first. Include a minimal reproduction, the Varcode, PyEnsembl and Python versions, the annotation release, and the variant's coordinates, REF/ALT and assembly. For file parsing problems, include a small synthetic input with the relevant header and sample fields. Report substantial new work as an issue before implementation, and link related issues from the PR. ## Set up a development environment From the repository root: ```bash python3 -m venv .venv source .venv/bin/activate python -m pip install -e '.[rna,test-data]' pytest pytest-cov pytest-xdist ruff python -m pip install mkdocs mkdocs-material 'mkdocstrings[python]' python -m pip install build twine 'setuptools>=64' ``` `rna` installs the optional BAM reader; `test-data` installs the pinned Osteosarc adapter. `pytest-xdist` is optional: `test.sh` also runs serially. Activate this environment before invoking the scripts so Python and its tools come from the same installation. `./develop.sh` installs just the editable library into the active environment. Install the reference datasets used by the full suite (these are downloads): ```bash pyensembl install --release 75 --species human pyensembl install --release 81 --species human pyensembl install --release 93 --species human pyensembl install --release 95 --species human pyensembl install --release 95 --species mouse pyensembl install --release 99 --species dog pyensembl install --release 2020_10 --species dog ``` These canonical sources match the `EnsemblRelease` objects used by the tests. PyEnsembl 2.27.5 keys downloads and indexes by their source URL; rerun the installer after upgrading from the older cache format. The optional ATM regressions also run when `pyensembl install --release 115 --species human` has completed. During pytest, bare GRCh38 references use Ensembl 81, matching the shared fixtures. Reference selection does not scan or populate the user cache; install the data above before running annotation tests. Explicit release choices are preserved. The bundled Osteosarc snapshot checks open their data offline. The complete public somatic benchmarks use human 75/93 and dog 99. Selected native ROS_Cfam_1.0 canine variants additionally use dog annotation 2020_10. Their [provenance and comparison limits](tests/data/public_somatic/README.md) explain the assemblies, source versions and explicitly unresolved canine records. See [tests/README.md](tests/README.md) for fixture provenance and regeneration. ## Make and verify a change Create a feature branch before editing; land changes through a reviewed PR. Use [PEP 8](https://peps.python.org/pep-0008/) and NumPy-style docstrings. Include a focused regression for a bug fix and keep the fixture construction or reproducible source acquisition alongside the tests. Scientific expected values should have an independent basis, rather than comparing a function with itself. Preserve public APIs where practical: Isovar, Topiary and Vaxrank consume Varcode results. ```bash ./lint.sh ./test.sh mkdocs build --strict ``` For a focused test run, use `python -m pytest tests/test_vcf_limits.py`. `test.sh` accepts pytest arguments and chooses a memory-aware worker count; set `TEST_SH_MAX=2` to cap it when running other suites concurrently. The CI parity check is `./test.sh --annotator=protein_diff -m parity`. Every PR includes a version bump in `varcode/version.py`, relevant changelog notes, and issue links. Wait for green GitHub checks before merging; follow [the release procedure](RELEASING.md) to publish and verify the package. ## Licensing Varcode and contributions are licensed under the Apache License 2.0. ## Writing transforms Every transform owes three things, documented in its docstring: | Field | Meaning | |---|---| | **Cardinality** | `preserves`, `reduces`, or `increases`. | | **Provenance** | Every output variant carries `source_variants: tuple[Variant, ...]`. Empty tuple for pass-through; one element for derived-from-one; two or more for combined. Not part of hash/equality. | | **Metadata behavior** | Explicit rule for how `source_to_metadata_dict` entries flow through (which fields are inherited from which source, which require agreement, what happens on disagreement). | Transforms are **idempotent on inputs they don't recognize**. Running `pair_breakends` twice produces the same VC; the second pass finds no unpaired BNDs to combine because every combined row's `source_variants` is already populated. The proposed `combine_cis_snvs` transform is tracked in [#368](https://github.com/openvax/varcode/issues/368); it is not a shipped API. User-facing examples belong in the [transforms guide](https://openvax.github.io/varcode/transforms/). openvax-varcode-d96eb06/LICENSE000066400000000000000000000260751526602674500162700ustar00rootroot00000000000000Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. openvax-varcode-d96eb06/MANIFEST.in000066400000000000000000000004361526602674500170120ustar00rootroot00000000000000include README.md include LICENSE include AGENTS.md CHANGELOG.md CONTRIBUTING.md RELEASING.md include develop.sh lint.sh test.sh deploy.sh mkdocs.yml recursive-include tests *.py *.md graft tests/data recursive-include docs *.md recursive-include examples *.py global-exclude *.py[cod] openvax-varcode-d96eb06/README.md000066400000000000000000000136611526602674500165370ustar00rootroot00000000000000[![Tests](https://github.com/openvax/varcode/actions/workflows/tests.yml/badge.svg)](https://github.com/openvax/varcode/actions/workflows/tests.yml) [![Coverage Status](https://coveralls.io/repos/openvax/varcode/badge.svg?branch=main&service=github)](https://coveralls.io/github/openvax/varcode?branch=main) [![PyPI](https://img.shields.io/pypi/v/varcode.svg?maxAge=1000)](https://pypi.org/project/varcode/) [![PyPI downloads](https://img.shields.io/pypi/dm/varcode.svg)](https://pypistats.org/packages/varcode) # Varcode Varcode is a Python library that predicts what genomic variants do to genes, transcripts, and proteins. Load a VCF or MAF file, and for each variant on each overlapping transcript, Varcode reports the predicted consequence (for example a substitution such as `p.L159M`, a frameshift, or a gene fusion). Where the sequence can be determined, it also gives the mutant protein. ## Installation Requires Python 3.11 or later: ```bash pip install varcode pyensembl install --release 81 --species human ``` The second command downloads the gene annotation and transcript sequences used in the example below (Ensembl 81, GRCh38). For your own data, choose an annotation that matches your input's genome build; see [reference setup](https://openvax.github.io/varcode/getting_started/#reference-data). ## Quick start Predict the effect of a single GRCh38 variant on a CFTR transcript: ```python from varcode import Variant variant = Variant("7", 117_531_100, "T", "A", genome=81) transcript = variant.genome.transcript_by_id("ENST00000003084") effect = variant.effect_on_transcript(transcript) print(effect.short_description) # p.L159M protein = effect.mutant_protein_sequence ``` Annotate every variant in a VCF called against the same genome build, and print the most severe effect for each: ```python from varcode import load_vcf variants = load_vcf("variants.vcf", genome=81) effects = variants.effects() for variant, effect in effects.top_priority_effect_per_variant().items(): print(variant.short_description, effect.short_description) ``` Or from the command line: ```bash varcode --genome GRCh38 --vcf variants.vcf --output-csv effects.csv ``` ## Reading the results - **One variant, many transcripts.** Each prediction belongs to one transcript, and a variant's effect can differ between transcripts. `top_priority_effect_per_variant()` picks one per variant for a summary. - **"Top priority" means most severe, not most likely.** The ranking is Varcode's ordering of consequence severity. It is not a probability, a pathogenicity score, or a clinical classification. - **Some effects have several possible outcomes.** Splice variants, structural variants, and variants with unknown phase keep all their candidate outcomes rather than guessing one. - **`None` means unknown, not unchanged.** A missing protein sequence means Varcode could not determine it. - **Predictions are not observations.** A predicted protein doesn't show that the transcript or protein is expressed. ## Learn more The [documentation](https://openvax.github.io/varcode/) starts with a [getting-started guide](https://openvax.github.io/varcode/getting_started/). Guides by topic: - [Effect annotation](https://openvax.github.io/varcode/effect_annotation/): SNVs, indels, and multi-base substitutions, classified as coding changes (substitution, frameshift, premature stop, ...) or by location (UTR, intron, splice site, intergenic). - [Splice variants](https://openvax.github.io/varcode/splice_variants/): possible outcomes such as exon skipping, intron retention, or a cryptic splice site, each with its own predicted protein when it can be determined. - [Structural variants](https://openvax.github.io/varcode/structural_variants/): transcript consequences and gene-fusion candidates for deletions, duplications, inversions, and breakends. - [Germline](https://openvax.github.io/varcode/germline/), [phasing](https://openvax.github.io/varcode/phasing/), and [observed RNA](https://openvax.github.io/varcode/rna_structures/): optional patient context that refines predictions. - [Sample queries](https://openvax.github.io/varcode/genotype/) and [identity checks](https://openvax.github.io/varcode/sample_identity/): select a sample's variants, or screen a cohort for possible sample mix-ups. - [Saving results](https://openvax.github.io/varcode/csv/): CSV, JSON, and VCF export, and what each keeps. - [Reference ORF consequences](https://openvax.github.io/varcode/uorf_annotations/): install `varcode[orf]` and pass `extra_orfs="all"` (or an evidence name such as `"ms"` or `"immunopeptidomics"`, or an ORFdata rule) to `variant.effects` for ordinary and additional coding consequences together, using ORFdata's ORFs for the variant's genome; a catalogue or bound ORFs also work. Access the reference through `effect.orf` and native outcomes through `effect.orf_annotation`. Reference pages cover [effect types](https://openvax.github.io/varcode/effect_types/), [coordinate conventions](https://openvax.github.io/varcode/getting_started/#coordinate-conventions), and the [API](https://openvax.github.io/varcode/api/). Varcode is part of the [OpenVax](https://github.com/openvax) tools. [Isovar](https://github.com/openvax/isovar) reconstructs variant sequences from RNA reads, and [Vaxrank](https://github.com/openvax/vaxrank) evaluates the resulting protein/peptide candidates; see [how the libraries fit together](https://openvax.github.io/varcode/library_roles/). For bugs or questions, [open an issue](https://github.com/openvax/varcode/issues). Contributions are welcome; see [CONTRIBUTING.md](https://github.com/openvax/varcode/blob/main/CONTRIBUTING.md) and the [changelog](https://github.com/openvax/varcode/blob/main/CHANGELOG.md). Varcode is released under the [Apache 2.0 license](https://github.com/openvax/varcode/blob/main/LICENSE). openvax-varcode-d96eb06/RELEASING.md000066400000000000000000000056101526602674500171060ustar00rootroot00000000000000# Releasing Varcode A release is complete after the reviewed PR is merged, CI passes, and the new version is available on PyPI. Use a feature branch for all code and version changes; never commit directly to `main`. ## Prepare the PR 1. Choose a [semantic version](https://semver.org/), update `__version__` in `varcode/version.py`, and add release notes to `CHANGELOG.md`. Every PR needs at least a patch bump. Recheck the version against `origin/main` before merging if another release has shipped. 2. Run `./lint.sh`, `./test.sh`, and `mkdocs build --strict` for documentation changes. Review the diff and wait for green GitHub CI on the final commit. 3. Merge the PR through GitHub. ## Publish from clean main Activate the development environment described in [CONTRIBUTING.md](CONTRIBUTING.md), with build/upload credentials configured for PyPI. Use a clean checkout: ```bash git switch main git pull --ff-only origin main git status --porcelain python -c 'import runpy; print(runpy.run_path("varcode/version.py")["__version__"])' ``` The script requires a clean `main` or `master` whose HEAD matches the live branch on `origin`. The release tag must be absent both locally and remotely. Then run: ```bash ./deploy.sh ``` Use `./deploy.sh X.Y.Z` to also assert the already-merged version. `deploy.sh` runs lint and tests, builds a wheel and source distribution in a new directory under `dist/`, and runs `twine check --strict` on that exact pair. It rechecks the checkout and remote refs before uploading, then tags the original merged commit with `v` and pushes only that tag. Version changes belong in the PR; a different version argument fails without editing or committing files. Activate the development environment first; the script uses `python` and the installed `build` and `twine` tools. `./deploy.sh --dry-run [version]` performs the same guards, lint, tests, build, and distribution checks, then stops before uploading or changing Git history. It needs read access to `origin` and leaves its build artifacts under `dist/`. After a successful upload, verify the exact version on [PyPI](https://pypi.org/project/varcode/), compare its wheel/sdist SHA256 hashes with the files in the printed artifact directory, and smoke-test the published package in an isolated environment. Verify that the pushed tag targets the merged commit. Check the main-branch CI and documentation deployment, then review open issues for the next dependency or correctness blocker. If publication fails, keep the printed artifact directory and inspect PyPI before retrying. Upload only missing files from that original pair after verifying the published hashes; rebuilding may produce different bytes. Finish tagging the original commit and pushing the tag once both files are verified. If only the tag push failed, verify the local tag's target and retry `git push origin refs/tags/vX.Y.Z`. Never move an existing release tag. openvax-varcode-d96eb06/code-of-conduct.md000066400000000000000000000064501526602674500205510ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies within all project spaces, and it also applies when an individual is representing the project or its community in public spaces. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hello@openvax.org. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq openvax-varcode-d96eb06/deploy.sh000077500000000000000000000060711526602674500171100ustar00rootroot00000000000000#!/usr/bin/env bash set -euo pipefail usage() { echo "Usage: ./deploy.sh [--dry-run] [version]" echo "Publish the merged version; an optional version must match varcode/version.py." } fail() { echo "deploy.sh: $*" >&2; exit 1; } dry_run=0 expected_version="" while [[ $# -gt 0 ]]; do case "$1" in --dry-run) dry_run=1 ;; -h|--help) usage; exit 0 ;; -*) fail "Unknown option: $1" ;; *) [[ -z "$expected_version" ]] || fail "Expected at most one version." expected_version="${1#v}" [[ -n "$expected_version" ]] || fail "Expected a release version." ;; esac shift done cd "$(dirname "$0")" branch="$(git symbolic-ref --quiet --short HEAD)" || fail "Deploy from main or master, not a detached HEAD." [[ "$branch" == main || "$branch" == master ]] || fail "Deploys require main or master (current: $branch)." commit="$(git rev-parse HEAD)" version="$(python -c 'import runpy; print(runpy.run_path("varcode/version.py")["__version__"])')" [[ "$version" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]] || fail "Invalid release version: $version" [[ -z "$expected_version" || "$expected_version" == "$version" ]] || fail "Requested $expected_version, but the merged version is $version. Bump versions in a PR." tag="v$version" # Check live origin refs, not a possibly stale remote-tracking branch. check_release() { local status remote_branch remote_tag status="$(git status --porcelain --untracked-files=all)" [[ -z "$status" ]] || fail "Working tree is not clean." [[ "$(git symbolic-ref --quiet --short HEAD)" == "$branch" && "$(git rev-parse HEAD)" == "$commit" ]] || fail "Checkout changed during release." remote_branch="$(git ls-remote --exit-code origin "refs/heads/$branch")" || fail "Cannot read origin/$branch." [[ "${remote_branch%%$'\t'*}" == "$commit" ]] || fail "HEAD must match live origin/$branch; pull the merged release first." if git show-ref --verify --quiet "refs/tags/$tag"; then fail "Tag $tag already exists locally. Verify the publication state before retrying." fi remote_tag="$(git ls-remote origin "refs/tags/$tag")" || fail "Cannot check release tags on origin." [[ -z "$remote_tag" ]] || fail "Tag $tag already exists on origin." } check_release ./lint.sh ./test.sh # Keep each attempt's original bytes, including after a failed/partial upload. mkdir -p dist artifact_dir="$(mktemp -d "dist/$tag.XXXXXX")" echo "Building $tag at $commit; artifacts: $artifact_dir" python -m build --outdir "$artifact_dir" distributions=("$artifact_dir/varcode-$version-py3-none-any.whl" "$artifact_dir/varcode-$version.tar.gz") python -m twine check --strict "${distributions[@]}" check_release if [[ "$dry_run" -eq 1 ]]; then echo "Dry run passed for $tag; no upload, commit, tag, or push. Artifacts: $artifact_dir" exit 0 fi python -m twine upload --non-interactive --disable-progress-bar "${distributions[@]}" git tag "$tag" "$commit" git push origin "refs/tags/$tag" echo "Deployed varcode $version: https://pypi.org/project/varcode/$version/" openvax-varcode-d96eb06/develop.sh000077500000000000000000000000671526602674500172510ustar00rootroot00000000000000#!/usr/bin/env bash set -e python -m pip install -e . openvax-varcode-d96eb06/docs/000077500000000000000000000000001526602674500162015ustar00rootroot00000000000000openvax-varcode-d96eb06/docs/annotator_contract.md000066400000000000000000000121701526602674500224260ustar00rootroot00000000000000# Writing an annotator An annotator predicts an effect for a variant on a transcript. Register a custom implementation when you want to compare your model through Varcode's public annotation interface. For the built-in experiments, see [Experimental annotators](experimental_annotators.md). ## Required method An annotator needs a `name` and an `annotate_on_transcript` method. It may also provide a `version` for provenance. Return a `MutationEffect` for a supported input, or Python's `NotImplemented` singleton for an input your implementation cannot handle. No capability list is required. For example, this adapter takes a callable that returns an effect or `None`: ```python class MyAnnotator: name = "my_model" def __init__(self, model): self.model = model def annotate_on_transcript(self, variant, transcript): prediction = self.model(variant, transcript) if prediction is None: return NotImplemented return prediction ``` Register an instance, then select it explicitly: ```python import varcode annotator = MyAnnotator(my_model) varcode.register_annotator(annotator) effects = variant.effects(annotator=annotator.name) ``` Here `my_model` is your prediction callable and `variant` is a loaded variant. ## Unsupported inputs and errors Public APIs convert `NotImplemented` into `Unresolved(mechanism="unsupported_annotation", reason=...)`, preserving the selected annotator's provenance. They do not silently fall back to the default. For example, selecting `protein_diff` for an SV returns `Unresolved`, including inside `use_annotator("protein_diff")`. Returning `None` directly is a plugin error. Exceptions retain the ordinary `raise_on_error` behavior; do not turn unexpected failures into unsupported inputs. ## Germline context An optional `annotate_with_context(variant, transcript, germline_ctx, phase_resolver=None)` method has the same return contract. Without it, nonempty germline context produces `Unresolved` rather than being ignored or sent to another implementation. Empty context uses `annotate_on_transcript`. `effects()`, `effect_on_transcript()`, and `predict_variant_effect_on_transcript()` use the same selection rules, including the scoped default. ## Joint haplotypes To predict combined effects, implement the optional method: ```python def annotate_haplotype(self, variants, transcript, germline_ctx=None, phase_resolver=None): # variants is a tuple of two or more known-cis variants on this transcript. return NotImplemented ``` The collection groups variants using the phase resolver, then calls the selected annotator for each group. Return a `MutationEffect` or `NotImplemented`, just as for individual annotation. Missing hooks and refusals produce `Unresolved(mechanism="unsupported_haplotype")`; `None` is a plugin error. Exceptions follow `raise_on_error`, with handled errors retained as `Failure`. There is no automatic fallback to another annotator. Joint results coexist with individual effects and carry `variants`, `phase_source`, `annotator`, and `annotator_version`. Handle all supplied context or decline the group; ignoring germline changes would produce the wrong baseline. JSON round trips retain this group context and any attached observed RNA models. The phase resolver may also supply observed mutant transcripts, whose source and coverage must remain distinguishable from inferred sequences. The default and `protein_diff` explicitly share the established point-edit haplotype builder, returning `HaplotypeEffect`. They prefer a supplied RNA model and decline joint patient-baseline composition. Without an observed model, unsupported structural/splice groups and conflicting edits stay unresolved. `transcript_model` uses its multi-variant engine and returns an ordinary classified effect with alternatives in `candidates`. It uses germline windows around every group member and retains `(variant, model)` observations in `observed_mutant_transcripts`, without replacing modeled candidate sequences. ## Default implementation The default owns point-variant prediction, structural handling, and the existing germline-aware point-edit path. Structural code lives in `varcode.effects.structural` as internal helpers; both the default and the transcript model's fusion path call those helpers directly. There is no separate structural annotator or automatic router between implementations. Structural transcript models can retain the builder label `structural_variant` in their provenance, while the effect collection records the selected annotator. A stored builder label is not a selectable annotator name. ## Testing an integration Test supported inputs, explicit refusal, context-dependent refusal, and genuine errors separately. Check direct transcript calls as well as collections, and preserve provenance and unresolved results when serializing. Compare implementations only on inputs they both support. Passing a parity test for point edits does not establish SV support. When sending results to other tools, retain candidate evidence, transcript identity, and sequence completeness; a protein string alone does not establish a complete expressed protein. openvax-varcode-d96eb06/docs/api.md000066400000000000000000000370711526602674500173040ustar00rootroot00000000000000# API reference Start with [Getting started](getting_started.md) for examples, or choose a topic below for signatures, parameters, and return types. Most annotation uses the same `effects()` interface; [annotator selection](experimental_annotators.md) is optional. ## Variants and files [Variants and files reference](api_variants.md) - [Genome](api_variants.md#varcode.Genome) - [Variant](api_variants.md#varcode.Variant) - [VariantCollection](api_variants.md#varcode.VariantCollection) - [StructuralVariant](api_variants.md#varcode.StructuralVariant) - [parse_symbolic_alt](api_variants.md#varcode.parse_symbolic_alt) - [SV_TYPES](api_variants.md#varcode.SV_TYPES) - [Genotype](api_variants.md#varcode.Genotype) - [Zygosity](api_variants.md#varcode.Zygosity) - [transforms.pair_breakends](api_variants.md#varcode.transforms.pair_breakends) - [transforms.left_align_indels](api_variants.md#varcode.transforms.left_align_indels) - [load_vcf](api_variants.md#varcode.vcf.load_vcf) - [load_maf](api_variants.md#varcode.load_maf) - [load_maf_dataframe](api_variants.md#varcode.load_maf_dataframe) - [ReferenceMismatchError](api_variants.md#varcode.ReferenceMismatchError) - [SampleNotFoundError](api_variants.md#varcode.SampleNotFoundError) - [GenomeBuildMismatchError](api_variants.md#varcode.GenomeBuildMismatchError) - [HypothesisLimitError](api_variants.md#varcode.HypothesisLimitError) ## Effects [Effects reference](api_effects.md) - [MutationEffect](api_effects.md#varcode.MutationEffect) - [NonsilentCodingMutation](api_effects.md#varcode.NonsilentCodingMutation) - [MultiOutcomeEffect](api_effects.md#varcode.MultiOutcomeEffect) - [EffectCollection](api_effects.md#varcode.EffectCollection) - [EffectCandidate](api_effects.md#varcode.EffectCandidate) - [effect_priority](api_effects.md#varcode.effect_priority) - [top_priority_effect](api_effects.md#varcode.top_priority_effect) ## Phasing and germline [Phasing and germline reference](api_phasing.md) - [ReadPhasingSource](api_phasing.md#varcode.ReadPhasingSource) - [MutantTranscriptSource](api_phasing.md#varcode.MutantTranscriptSource) - [MolecularPhaseResolver](api_phasing.md#varcode.MolecularPhaseResolver) - [ReadPhaseResolver](api_phasing.md#varcode.ReadPhaseResolver) - [RNAReadPhasingSource](api_phasing.md#varcode.RNAReadPhasingSource) - [VCFPhaseResolver](api_phasing.md#varcode.VCFPhaseResolver) - [query_in_cis](api_phasing.md#varcode.query_in_cis) - [apply_phase_resolver_to_effects](api_phasing.md#varcode.apply_phase_resolver_to_effects) - [GermlineContext](api_phasing.md#varcode.GermlineContext) - [Completeness](api_phasing.md#varcode.Completeness) - [predict_germline_aware_effect](api_phasing.md#varcode.predict_germline_aware_effect) - [apply_germline_to_transcript](api_phasing.md#varcode.apply_germline_to_transcript) - [partition_germline_by_phase](api_phasing.md#varcode.partition_germline_by_phase) - [PhasePartition](api_phasing.md#varcode.PhasePartition) - [enumerate_phase_hypotheses](api_phasing.md#varcode.enumerate_phase_hypotheses) - [detect_germline_overlap](api_phasing.md#varcode.detect_germline_overlap) - [GermlineAlleleOverlap](api_phasing.md#varcode.GermlineAlleleOverlap) - [detect_loh](api_phasing.md#varcode.detect_loh) - [default_germline_window](api_phasing.md#varcode.default_germline_window) ## RNA and transcripts [RNA and transcripts reference](api_rna.md) - [load_exacto_fusions](api_rna.md#varcode.load_exacto_fusions) - [make_fusion_outcome](api_rna.md#varcode.make_fusion_outcome) - [RNAEvidence](api_rna.md#varcode.RNAEvidence) - [MutantTranscript](api_rna.md#varcode.MutantTranscript) - [apply_variant_to_transcript](api_rna.md#varcode.apply_variant_to_transcript) - [apply_variants_to_transcript](api_rna.md#varcode.apply_variants_to_transcript) - [RNAEvidenceResolver](api_rna.md#varcode.RNAEvidenceResolver) - [NullRNAEvidenceResolver](api_rna.md#varcode.NullRNAEvidenceResolver) - [apply_rna_evidence_to_effects](api_rna.md#varcode.apply_rna_evidence_to_effects) - [make_rna_outcome](api_rna.md#varcode.make_rna_outcome) ## Annotators [Annotators reference](api_annotators.md) - [EffectAnnotator](api_annotators.md#varcode.EffectAnnotator) - [FastEffectAnnotator](api_annotators.md#varcode.FastEffectAnnotator) - [ProteinDiffEffectAnnotator](api_annotators.md#varcode.ProteinDiffEffectAnnotator) - [register_annotator](api_annotators.md#varcode.register_annotator) - [get_annotator](api_annotators.md#varcode.get_annotator) - [get_default_annotator](api_annotators.md#varcode.get_default_annotator) - [set_default_annotator](api_annotators.md#varcode.set_default_annotator) - [use_annotator](api_annotators.md#varcode.use_annotator) - [TranscriptModelEffectAnnotator](api_annotators.md#varcode.TranscriptModelEffectAnnotator) - [predict_transcript_model_effect](api_annotators.md#varcode.predict_transcript_model_effect) openvax-varcode-d96eb06/docs/api_annotators.md000066400000000000000000000015531526602674500215500ustar00rootroot00000000000000# Annotators API Ordinary annotation uses the default without selecting an implementation. See [experimental annotators](experimental_annotators.md) for alternatives and [Writing an annotator](annotator_contract.md) for extensions. ## Annotators ::: varcode.EffectAnnotator ::: varcode.FastEffectAnnotator ::: varcode.ProteinDiffEffectAnnotator ## Registry ::: varcode.register_annotator ::: varcode.get_annotator ::: varcode.get_default_annotator ::: varcode.set_default_annotator ::: varcode.use_annotator ## Experimental transcript model This annotator is opt-in with `annotator="transcript_model"`; `fast` remains the default. The former `realized` name and public imports remain aliases. See [supported inputs and results](experimental_annotators.md) before using it. ::: varcode.TranscriptModelEffectAnnotator ::: varcode.predict_transcript_model_effect openvax-varcode-d96eb06/docs/api_effects.md000066400000000000000000000010431526602674500207710ustar00rootroot00000000000000# Effects API See [effect annotation](effect_annotation.md) for result access and the [effect catalog](effect_types.md) for individual consequence classes. ## Effects ::: varcode.MutationEffect options: skip_local_inventory: true ::: varcode.NonsilentCodingMutation options: skip_local_inventory: true ::: varcode.MultiOutcomeEffect options: skip_local_inventory: true ::: varcode.EffectCollection ::: varcode.EffectCandidate ## Priority ordering ::: varcode.effect_priority ::: varcode.top_priority_effect openvax-varcode-d96eb06/docs/api_phasing.md000066400000000000000000000014751526602674500210140ustar00rootroot00000000000000# Phasing and germline API See [Phasing](phasing.md) and [Germline-aware annotation](germline.md) for input requirements and examples. ## Phasing ::: varcode.ReadPhasingSource ::: varcode.MutantTranscriptSource ::: varcode.MolecularPhaseResolver ::: varcode.ReadPhaseResolver ::: varcode.RNAReadPhasingSource ::: varcode.VCFPhaseResolver ::: varcode.apply_phase_resolver_to_effects ::: varcode.query_in_cis ## Germline-aware annotation ::: varcode.GermlineContext ::: varcode.Completeness ::: varcode.predict_germline_aware_effect ::: varcode.apply_germline_to_transcript ::: varcode.partition_germline_by_phase ::: varcode.PhasePartition ::: varcode.enumerate_phase_hypotheses ::: varcode.detect_loh ::: varcode.detect_germline_overlap ::: varcode.GermlineAlleleOverlap ::: varcode.default_germline_window openvax-varcode-d96eb06/docs/api_rna.md000066400000000000000000000012361526602674500201360ustar00rootroot00000000000000# RNA and transcripts API See [Transcript models](transcript_models.md) for sequence access and [RNA imports](rna_structures.md) for observed structures and Exacto predictions. ## Mutant transcripts ::: varcode.MutantTranscript ::: varcode.apply_variant_to_transcript ::: varcode.apply_variants_to_transcript ## Observed RNA import ::: varcode.load_exacto_fusions ::: varcode.make_fusion_outcome ::: varcode.RNAEvidence ## Reference-completion hypotheses ::: varcode.reference_completion_hypotheses ## RNA evidence ::: varcode.RNAEvidenceResolver ::: varcode.NullRNAEvidenceResolver ::: varcode.apply_rna_evidence_to_effects ::: varcode.make_rna_outcome openvax-varcode-d96eb06/docs/api_variants.md000066400000000000000000000133241526602674500212060ustar00rootroot00000000000000# Variants and files API For examples, see [file loading](getting_started.md#load-a-file), [sample queries](genotype.md), and [transforms](transforms.md). ## Reference genomes ### Pin an annotation release Use the same reference string in Python's `genome=` argument and the CLI's `--genome` option: | Reference string | Assembly and annotation | |---|---| | `GRCh38:93` | Human GRCh38, numbered Ensembl release 93 | | `GRCh38:2026_04` | Human GRCh38, annotation dated April 2026 | | `GRCh37:2013_09` | Human GRCh37, historical annotation dated September 2013 | | `ROS_Cfam_1.0:2020_10` | Dog ROS_Cfam_1.0, annotation dated October 2020 | Selection is offline. Install the matching annotation before using it, for example: ```bash pyensembl install --release 2013_09 --assembly GRCh37 varcode --genome GRCh37:2013_09 --vcf variants.vcf --output-csv effects.csv ``` Dates use `YYYY_MM`. An incompatible assembly/date combination or malformed explicit date raises an error. An assembly name without a release selects the most recent installed **numbered** release, or the newest numbered release when none is installed. Selecting a release does not convert coordinates. Historical dated datasets keep PyEnsembl's accession-based reference names and cache directories. For example, `GRCh37:2013_09` has `genome.assembly == "GRCh37"` and `genome.reference_name == "GCA_000001405.14"`. ### Chromosome DNA **Provide chromosome DNA once on your reference genome.** Varcode then uses it for SV breakpoint windows, cryptic-exon searches, intron-retention products, ncORF consequences and indel left alignment. Without chromosome DNA, exonic consequences still work; outcomes needing unavailable intronic/intergenic bases remain unresolved or unavailable. Use the same assembly for the annotation, DNA and variant coordinates: ```python from pyensembl import EnsemblRelease from varcode import load_vcf genome = EnsemblRelease(81, genome_fasta="/data/GRCh38.fa") variants = load_vcf("variants.vcf", genome=genome, parse_structural_variants=True) effects = variants.effects(extra_orfs="all") # optional: requires varcode[orf] ``` Construction and rewrapping do not open or download native DNA. The first read can index a local FASTA or decompress it into PyEnsembl's cache. For a remote source, install DNA explicitly before querying: ```python genome = EnsemblRelease(81, genome_fasta=True) genome.download_genome_fasta() # explicit, potentially large download genome.index_genome_fasta() ``` Varcode requires PyEnsembl 2.30 or later. For a pre-opened FASTA reader, use `varcode.Genome(native, fasta="/data/GRCh38.fa")`. An explicit `Genome(native, fasta="/data/other.fa")` overrides native DNA; the supplied reference must match the annotation assembly. Lookups behave as follows: - Without DNA, `reference_base` and `reference_range` fall back to transcript cDNA and return `""` for uncovered positions. - `sequence` reads only chromosome DNA. Configured native DNA preserves PyEnsembl's missing-DNA and invalid-interval errors, while unconfigured genomes return `""`. - Explicit FASTA overrides retain their legacy permissive lookup behavior. - Coordinates are 1-based inclusive, and returned bases are uppercase on the genomic plus strand. The native PyEnsembl genome owns its reader. Calling `genome.close()` leaves native DNA and caller-provided reader objects open; it closes only readers that this wrapper opened from an explicit `fasta=` path. Rewrapping borrows an explicit reader, so its owning wrapper must stay open. Close the native genome or a caller-provided reader only after all its borrowers have finished. The command-line equivalent is: ```bash varcode --genome GRCh38:81 --genome-fasta /data/GRCh38.fa \ --vcf variants.vcf --extra-orfs all --output-csv effects.csv ``` `--genome-fasta` works for VCF, MAF, individual variants and variant JSON. All combined inputs must use one assembly. `--extra-orfs` is optional; repeat an evidence selection (for example `--extra-orfs proteomics --extra-orfs immunopeptidomics`) to include ORFs supported by either. ### Current dog reference For variants called on **ROS_Cfam_1.0** (GCA_014441545.1), select the matching dog annotation explicitly. Ensembl's current dog annotation is dated **2020_10**, as verified on 2026-10-20; this is its build date, not the date of the website release. ```bash pyensembl install --release 2020_10 --species dog varcode --genome ROS_Cfam_1.0:2020_10 --vcf ros_cfam_variants.vcf --output-csv effects.csv ``` ```python variants = load_vcf("ros_cfam_variants.vcf", genome="ROS_Cfam_1.0:2020_10") ``` For SV or intronic sequence analysis, install with `--with-genome-fasta` and pass `EnsemblRelease("2020_10", species="dog", genome_fasta=True)` as `genome=`, or supply a matching local FASTA. The complete genomic DNA download requires additional disk space. Keep CanFam3.1 calls on their original reference unless their coordinates and alleles have been explicitly converted and validated. Selecting a new assembly does not convert variants. Our [native dog benchmark](https://github.com/openvax/varcode/tree/main/tests/data/public_somatic) uses published ROS_Cfam_1.0 calls; the historical CanFam3.1 comparison stays pinned. ::: varcode.Genome ## Variants ::: varcode.Variant ::: varcode.VariantCollection ::: varcode.StructuralVariant ::: varcode.parse_symbolic_alt ::: varcode.SV_TYPES ## Genotypes ::: varcode.Genotype ::: varcode.Zygosity ## VariantCollection transforms ::: varcode.transforms.pair_breakends ::: varcode.transforms.left_align_indels ## File loading ::: varcode.vcf.load_vcf ::: varcode.load_maf ::: varcode.load_maf_dataframe ## Exceptions ::: varcode.ReferenceMismatchError ::: varcode.SampleNotFoundError ::: varcode.GenomeBuildMismatchError ::: varcode.HypothesisLimitError openvax-varcode-d96eb06/docs/changelog.md000066400000000000000000000000601526602674500204460ustar00rootroot00000000000000--- title: Changelog --- --8<-- "CHANGELOG.md" openvax-varcode-d96eb06/docs/csv.md000066400000000000000000000214511526602674500173210ustar00rootroot00000000000000# Saving and reloading results Save results as CSV for a table you can inspect or share, as [JSON](#csv-vs-json) to archive supported objects, or write variants back to [VCF](#vcf-export). Whichever you choose, keep the original inputs and evidence too: a summary table does not preserve every alternative outcome, sequence, or annotation context. ## Writing ```python from varcode import load_vcf vc = load_vcf("variants.vcf", genome="GRCh38") vc.to_csv("variants.csv") effects = vc.effects() effects.to_csv("effects.csv") ``` Splice rows list every current candidate description in `effect`, retain `effect_type=SpliceOutcomeSet`, and name the affected signal in `splice_signal`. The descriptions include unresolved candidates; they do not archive each candidate's sequences or evidence. When a collection includes extra ORFs, `to_csv()` adds an `effect_json` column to every row. This archives the complete effects alongside the readable summaries. `from_csv()` restores those saved graphs, including ORF source identity, reference RNA, evidence, failures and alternative products, without reannotation or catalogue downloads. Keep the archive column intact: missing payloads or summary identities that disagree with them raise an error. `to_dataframe()` remains a compact summary; use `to_dataframe(include_archive=True)` to include the archives. By default, both writers prepend `#`-prefixed provenance lines so the file is self-describing: ``` # varcode_version=10.2.1 # reference_name=GRCh38 chr,start,ref,alt,gene_name,gene_id 17,43082575,C,T,BRCA1,ENSG00000012048 ... ``` Pass `include_header=False` for fast consumers that can't tolerate comment lines: ```python vc.to_csv("plain.csv", include_header=False) ``` ## Reading The examples here apply to ordinary point-variant tables. Structural-variant CSV summaries without `effect_json` cannot be restored: `from_csv` rejects them rather than reconstructing incomplete SVs. See [structural exports](sv_reference.md#alleles-coordinates-and-exports). When the header is present, `from_csv` recovers the reference genome automatically: ```python from varcode import VariantCollection, EffectCollection vc = VariantCollection.from_csv("variants.csv") effects = EffectCollection.from_csv("effects.csv") ``` For summary files without `effect_json`, `EffectCollection.from_csv` re-runs annotation. It does not recover the original germline/phase/RNA evidence, candidate sets, or historical predictions. A reference-name header also does not pin the original annotation dataset; pass the same explicit `genome` when that matters. When the CSV has no header (written by an older varcode or with `include_header=False`), pass `genome` explicitly: ```python vc = VariantCollection.from_csv("plain.csv", genome="GRCh38") ``` Missing both the header *and* an explicit `genome` produces a clear error: ``` ValueError: from_csv needs a reference genome: pass the `genome` argument explicitly, or write the CSV with `to_csv(include_header=True)` so `# reference_name=...` is recorded in the header. Neither was found at plain.csv. ``` ## Column-name flexibility `VariantCollection` historically writes a `chr` column while `EffectCollection` writes `contig`. Both readers accept either spelling for that column. This does not make their full schemas interchangeable: effect tables also need effect/transcript fields. ## Version drift For summary files without `effect_json`, `EffectCollection.from_csv` re-runs annotation, so loading under another version can produce different effects. Archived rows restore their saved predictions. `from_csv` emits a `UserWarning` when the header's `varcode_version` differs in major version from the currently-installed version. Minor and patch drift is silent, but bug fixes or a different annotation dataset can still change predictions; API compatibility is not a guarantee of identical scientific output. ## CSV vs JSON | | CSV | JSON | |---|---|---| | Intended use | Inspect or share a table | Serialize supported objects | | Reloading effects | Summary rows re-annotate; `effect_json` rows restore saved graphs | Restores serialized state where supported | | Structural variants | Summary import rejected; archived rows restore graphs | Restore effect/candidate graphs and mutant transcript models | | Carries annotator version header | Yes | (via the serialized object) | | Preserves all effect-specific fields | With `effect_json`, subject to graph support | Depends on the effect type and serialization support | JSON (`to_json` / `from_json`) avoids CSV's re-annotation for supported objects. Deserialization restores recorded predictions without recomputing them, and collection annotator provenance is preserved. Structural results use a versioned effect graph, which older Varcode versions cannot read. These survive a round trip: - Self candidates and shared effect links. - Primary, cryptic, splice, and external candidates, with their source/evidence fields. - Fusion partners and affected exons. - Attached mutant transcript sequences and segments. This remains an object archive with Python class and reference-dataset dependencies. Arbitrary extra attributes and third-party sequence providers need their own serialization support. Test the round-trip for the actual result types you use, and retain source variants, reference release, and original RNA/phase/germline evidence independently. Extra-ORF graphs snapshot their full reference RNA and use ORFdata's public record constructors to restore the bound coding model. Reading these ORF graphs needs ORFdata but no installed catalogue or reference database. The snapshot supplies archived transcript properties; it is not a reference genome for annotating new variants. See [ORF annotations](uorf_annotations.md). ## VCF export `varcode.vcf_output.variants_to_vcf` writes small variants back to VCF: ```python from varcode import load_vcf from varcode.vcf_parsing import VCFHeader from varcode.vcf_output import variants_to_vcf variants = load_vcf("input.vcf", genome=81) header = VCFHeader.from_path("input.vcf") with open("export.vcf", "w") as out: variants_to_vcf(variants, variants.metadata, out=out, header=header) ``` What is preserved: - **Sample identities.** Each value is looked up by sample name and FORMAT key, so values can't shift between sample columns. Sample order is deterministic, GT comes first, missing values are written as `.`, and samples present in only some records are kept. - **Multi-allelic records.** `load_vcf` remembers each record's original ALT list and indexes, so export rebuilds the complete record in its original order, with GT and allele-indexed values intact. - **INFO/FORMAT declarations,** when you pass the original `VCFHeader` as above. Without it, standard FORMAT definitions and the observed value types determine new declarations; custom descriptions and cardinalities can't be recovered from values alone. - **Ordering.** Positions sort numerically within each contig. What is refused or not kept: - A subset that drops one of a record's original ALT alleles is rejected before any output is written. Keep all alleles, or explicitly remap the genotype and allele-indexed fields first. This includes invalid multiallelic input whose reference-identical ALT was skipped by `load_vcf`; correct that source record before exporting it. - Records are not merged just because their IDs match. - Hand-constructed records without an ALT index are treated as biallelic; their GT and standard allele-depth/likelihood fields must agree with that. - Metadata saved by older Varcode versions lacks the full ALT list; reload it from the source VCF. - Header lines other than INFO/FORMAT declarations are not archived, conflicting source records are not resolved, and `StructuralVariant` objects are not exported. Keep the original VCF as the source of record. ## Annotation provenance Annotated effect collections record `annotator`, `annotator_version`, and `annotated_at` (an ISO-8601 UTC timestamp). Filtering and grouping preserve these fields. CSV export writes them into the metadata header, and loading recovers the original values. A mismatch between the recorded annotator and the current default raises a warning on load. Selecting the recorded implementation does not restore the original predictions: CSV loading still re-annotates. Keep the original reference release and evidence separately; see [CSV vs JSON](#csv-vs-json). ## Custom header fields `read_metadata_header` is available in `varcode.csv_helpers` for tools that want to add their own metadata lines: ```python from varcode.csv_helpers import read_metadata_header, write_metadata_header meta = read_metadata_header("variants.csv") # OrderedDict([('varcode_version', '10.2.1'), ('reference_name', 'GRCh38')]) ``` Annotator provenance fields (`annotator`, `annotator_version`) use the same `# key=value` convention. openvax-varcode-d96eb06/docs/effect_annotation.md000066400000000000000000000106341526602674500222150ustar00rootroot00000000000000# Effect annotation Call `effects()` to predict the consequences of your variants. If you have not installed reference data yet, start with [Getting started](getting_started.md). ## Annotate variants ```python import varcode variants = varcode.load_vcf("variants.vcf", genome=81) # GRCh38 effects = variants.effects() for variant, effect in effects.top_priority_effect_per_variant().items(): print(variant.short_description, effect.short_description) ``` The collection holds one prediction per variant per overlapping transcript, and a variant's effect can differ between transcripts. The loop above prints one summary per variant, picking the most severe effect by Varcode's consequence ordering, not by likelihood or clinical significance. `effects.top_priority_effect()` instead selects one effect from the entire collection. See [how to read results](getting_started.md#how-to-read-results). The same interface handles structural variants. To include them when loading a VCF, pass `parse_structural_variants=True`; see [SV loading](structural_variants.md#basic-usage). ## Protein sequences Given an effect from the collection: ```python protein = effect.mutant_protein_sequence # may be None ``` `None` means the sequence could not be determined, not that the protein is unchanged. The prediction belongs to `effect.transcript`; intergenic effects have no transcript. For cDNA, partial structures, and sequence evidence, see [Transcript models](transcript_models.md). To keep only effects that may change the protein, use `effects.drop_silent_and_noncoding()`. It removes known silent and noncoding predictions but keeps unresolved ones by default; see [filtering by protein change](structural_variants.md#filtering-by-protein-change). ## Alternative outcomes When Varcode can't decide between several possible consequences, it returns a `MultiOutcomeEffect` holding every candidate instead of guessing. This happens for splice variants, structural variants, and variants whose phase relative to a nearby germline variant is unknown: ```python from varcode import MultiOutcomeEffect if isinstance(effect, MultiOutcomeEffect): for candidate in effect.candidates: print(candidate.effect.short_description) print(candidate.effect.mutant_protein_sequence) print(candidate.source, candidate.evidence) ``` Two shortcuts pick a single candidate, and they answer different questions: - `most_likely_effect` returns the first candidate in the order the predictor listed them. That order reflects the predictor's preference; it is not a calibrated probability. - `highest_priority_effect` returns the most severe candidate. When you report a candidate's sequence, keep its `candidate.source` (which predictor or evidence produced it) and `candidate.evidence` alongside it. ## Related guides - [Splice variants](splice_variants.md): affected signals, candidate mechanisms, and RNA evidence. - [Structural variants](structural_variants.md): deletions, duplications, inversions, and fusions. - [Germline](germline.md) and [phasing](phasing.md): patient-specific baselines and linked variants. - [Transcript models](transcript_models.md): cDNA, partial structures, and missing sequences. - [Saving results](csv.md#annotation-provenance): tables, provenance, and round-trip limits. - [Experimental annotators](experimental_annotators.md): optional implementations and their limits. - [Writing an annotator](annotator_contract.md): custom implementations and unsupported inputs. - [Effect types](effect_types.md) and [API reference](api.md): class definitions and parameters. openvax-varcode-d96eb06/docs/effect_types.md000066400000000000000000000123301526602674500212020ustar00rootroot00000000000000# Effect types Use this page to look up a class after reading the [effect annotation guide](effect_annotation.md). Definitions below are generated from the source docstrings; this is reference material, not a required first read. ## Effect families | Family | Examples | |---|---| | [Coding changes](#coding-effects) | `Substitution`, `Insertion`, `Deletion`, `FrameShift`, `PrematureStop`, `Silent` | | [Noncoding and untranslated regions](#noncoding-effects) | `Intronic`, `FivePrimeUTR`, `ThreePrimeUTR`, `NoncodingTranscript`, `Intergenic` | | [Splice effects](#splice-effects) | `SpliceOutcomeSet`, with `NormalSplicing`, `ExonSkipping`, `IntronRetention`, or cryptic-site candidates | | [Structural effects](#structural-effects) | `StructuralVariantEffect`, `GeneFusion`, `TranslocationToIntergenic` | | [Linked variants and phase](#haplotype-effects) | `HaplotypeEffect`, `PhaseCandidateSet` | | [Inherited allele overlap](germline.md#loss-of-heterozygosity-loh) | `GermlineAlleleOverlap`: no new allele sequence; LOH unassessed | | [Unknown results and failures](#unknown-results) | `Unresolved`, `HypothesisLimit`, `IncompleteTranscript`, `Failure` | These labels describe predictions and their limits, not clinical significance. Some effects describe a change to the protein; others only say where the variant landed. Coding effects (`Substitution`, `FrameShift`, `PrematureStop`, ...) and splice mechanism effects (`NormalSplicing`, `ExonSkipping`, ...) describe a protein change. Splice-signal disruption effects (`SpliceDonor`, `SpliceAcceptor`, `IntronicSpliceSite`, `ExonicSpliceSite`, all subclasses of `SpliceSite`) and region effects (`Intronic`, `FivePrimeUTR`, ...) carry no protein consequence on their own. Most effects are a single answer. Subclasses of `MultiOutcomeEffect` (`SpliceOutcomeSet`, `ExonicSpliceSite`, the structural-variant effects, `HaplotypeEffect`, `PhaseCandidateSet`) hold several candidate effects when the protein-level outcome isn't determined. Each provides `.candidates`, `.most_likely_effect`, and `.highest_priority_effect`. Severity ordering across types is set by [`effect_priority`](api_effects.md#varcode.effect_priority). ## Coding effects ::: varcode.effects.effect_classes.Silent ::: varcode.effects.effect_classes.AlternateStartCodon ::: varcode.effects.effect_classes.StartLoss ::: varcode.effects.effect_classes.Substitution ::: varcode.effects.effect_classes.ComplexSubstitution ::: varcode.effects.effect_classes.Insertion ::: varcode.effects.effect_classes.Deletion ::: varcode.effects.effect_classes.PrematureStop ::: varcode.effects.effect_classes.StopLoss ::: varcode.effects.effect_classes.FrameShift ::: varcode.effects.effect_classes.FrameShiftTruncation ::: varcode.effects.effect_classes.ExonLoss ## Noncoding effects ::: varcode.effects.effect_classes.Intergenic ::: varcode.effects.effect_classes.NoncodingTranscript ::: varcode.effects.effect_classes.FivePrimeUTR ::: varcode.effects.effect_classes.ThreePrimeUTR ::: varcode.effects.effect_classes.Intronic ## Splice effects See the [splice guide](splice_variants.md) for candidate access. ::: varcode.SpliceOutcomeSet ::: varcode.effects.effect_classes.SpliceSite ::: varcode.effects.effect_classes.IntronicSpliceSite ::: varcode.effects.effect_classes.SpliceDonor ::: varcode.effects.effect_classes.SpliceAcceptor ::: varcode.effects.effect_classes.ExonicSpliceSite ::: varcode.effects.effect_classes.SpliceMechanismEffect ::: varcode.effects.effect_classes.NormalSplicing ::: varcode.effects.effect_classes.ExonSkipping ::: varcode.effects.effect_classes.IntronRetention ::: varcode.effects.effect_classes.CrypticSpliceSiteEffect ::: varcode.effects.effect_classes.CrypticDonor ::: varcode.effects.effect_classes.CrypticAcceptor ::: varcode.effects.effect_classes.CrypticExonCandidate ## Structural effects ::: varcode.effects.effect_classes.StructuralVariantEffect Legacy event-type classes remain importable; the default annotator emits [transcript consequences](structural_variants.md#reading-sv-results). ::: varcode.effects.effect_classes.LargeDeletion ::: varcode.effects.effect_classes.LargeDuplication ::: varcode.effects.effect_classes.Inversion ::: varcode.effects.effect_classes.GeneFusion ::: varcode.effects.effect_classes.TranslocationToIntergenic ## Haplotype effects ::: varcode.effects.effect_classes.HaplotypeEffect ::: varcode.effects.effect_classes.PhaseCandidateSet ::: varcode.effects.effect_classes.GermlineAlleleOverlap ## Unknown results ::: varcode.effects.effect_classes.Unresolved ::: varcode.effects.effect_classes.HypothesisLimit ::: varcode.effects.effect_classes.IncompleteTranscript ::: varcode.effects.effect_classes.Failure ## Base classes ::: varcode.effects.effect_classes.MutationEffect ::: varcode.effects.effect_classes.MultiOutcomeEffect ::: varcode.effects.effect_classes.Intragenic ::: varcode.effects.effect_classes.TranscriptMutationEffect ::: varcode.effects.effect_classes.Exonic ::: varcode.effects.effect_classes.CodingMutation ::: varcode.effects.effect_classes.NonsilentCodingMutation ::: varcode.effects.effect_classes.KnownAminoAcidChange openvax-varcode-d96eb06/docs/errors.md000066400000000000000000000162171526602674500200460ustar00rootroot00000000000000# Troubleshooting Most errors come from a mismatch between your input and the reference data: the wrong genome build, a REF allele that doesn't match the reference, or a misspelled sample name. Understand an error before suppressing it, because suppressing it loses those predictions. | Problem | First check | |---|---| | Reference data missing | Install the release used by `genome=`; see [setup](getting_started.md#reference-data) | | `ReferenceMismatchError` | Input genome build, REF allele, and forward-strand convention; see [below](#referencemismatcherror) | | `GenomeBuildMismatchError` | Somatic and germline inputs must use the same genome build | | `SampleNotFoundError` | Inspect `variants.samples` for the available names | | Missing SV results | Python: pass `parse_structural_variants=True`. The CLIs already load SVs; inspect skip warnings. See [SV loading](structural_variants.md#basic-usage) | | No protein sequence | Not an error: the effect may be unresolved or noncoding; see [protein sequences](effect_annotation.md#protein-sequences) | | A few bad records stop a batch run | See [continuing past errors](#continuing-past-errors) | The domain-specific exceptions below subclass the standard `ValueError` or `KeyError`, so existing `except` clauses keep working. ## `ReferenceMismatchError` Raised when a variant's `ref` allele doesn't match the reference transcript sequence at the variant's position: ```python import varcode v = varcode.Variant("7", 117531114, "T", "A", genome=81) # The real + strand ref at chr7:117531114 is G, not T. v.effects() ``` Produces: ``` varcode.errors.ReferenceMismatchError: Reference allele mismatch for Variant(contig='7', start=117531114, ref='T', alt='A', reference_name='GRCh38') on Transcript(...) at transcript offset 620 (chromosome positions 117531114:117531114): variant reports ref='T' but the reference genome has 'G' at this position. This usually means the variant was called against a different genome build, the ref field was filled in with the patient's germline allele rather than the reference, or the variant is on the wrong strand. Pass raise_on_error=False to .effects() to receive a Failure effect instead of raising. ``` ### Common causes 1. **Wrong genome build.** A VCF called against GRCh37 but annotated with GRCh38 (or vice versa) produces these errors wherever the builds differ. Check that `v.reference_name` (e.g. `"GRCh38"`) matches the build your variants were called against. 2. **Germline allele in the `ref` field.** VCF requires `ref` to match the reference genome. Encode a patient's germline variant at that position as a separate variant rather than changing `ref`. 3. **Strand confusion.** Varcode expects alleles on the reference genome's forward (+) strand. For a gene on the reverse strand, the cDNA is the reverse complement of what you should enter. ### Handling it in code The exception subclasses `ValueError` and has structured fields: ```python try: v.effects() except varcode.ReferenceMismatchError as e: e.variant # the Variant e.transcript # the Transcript being compared against e.expected_ref # reference bases in transcript orientation e.observed_ref # variant REF bases in transcript orientation e.transcript_offset # position in the transcript ``` On minus-strand transcripts, `expected_ref` and `observed_ref` are reverse-complemented, so they won't match the forward-strand REF text in your VCF. The current error message calls them genome bases; that misleading wording is tracked in [#434](https://github.com/openvax/varcode/issues/434). ## `GenomeBuildMismatchError` Raised by `VariantCollection.effects(germline=...)` when the somatic variants and the germline context use different genome builds (e.g. GRCh37 vs GRCh38). Unlike `ReferenceMismatchError`, which is per variant, this check runs once for the whole collection, so a build mismatch fails immediately instead of producing one error per variant. ```python try: effects = somatic.effects(germline=germline) except varcode.GenomeBuildMismatchError as e: e.somatic_reference # the somatic collection's reference (e.g. 'GRCh38') e.germline_reference # the germline context's reference (e.g. 'GRCh37') ``` It subclasses `ValueError`. `validate_reference=False` on `VariantCollection.effects()` skips this check, but it does not convert coordinates or verify that the data match. Prefer fixing the input builds. ## `SampleNotFoundError` Raised by `VariantCollection` genotype and filter methods when a sample name isn't in the collection's source VCFs. The message lists the available names: ```python vc = varcode.load_vcf("tumor_normal.vcf", genome="GRCh38") vc.samples # ['normal', 'tumor'] vc.for_sample("patient_01") # SampleNotFoundError: Sample 'patient_01' not found. # Available samples: ['normal', 'tumor'] ``` It subclasses `KeyError` for backward compatibility. Failing on a typo is intentional: silently returning an empty collection for a misspelled sample name would hide bugs in analysis scripts. ## Continuing past errors `VariantCollection.effects()`, `VariantCollection.to_dataframe()` and both CLIs retain records on contigs absent from the selected annotation, with one summary warning. Such records have a `Failure` effect or gene-table diagnostic columns; missing annotation does not establish an intergenic location. Collection methods accept `raise_on_error=True` for strict handling. Their default (`None`) still raises other annotation errors. Single-variant `Variant.effects()` remains strict by default. With UCSC name conversion enabled (the VCF and CLI default), accession-bearing names such as `chrUn_KI270438v1` become `KI270438.1`. GRCh37 names such as `chrUn_gl000216` become `GL000216.1`, following the [UCSC hg19 alias table](https://hgdownload.soe.ucsc.edu/goldenPath/hg19/bigZips/hg19.chromAlias.txt). Explicit accession versions are preserved; names without a supported mapping remain unchanged. This does not lift coordinates between assemblies. In Python, pass `raise_on_error=False` to `.effects()`. Each failed annotation becomes a `Failure` effect instead of an exception, and other transcripts can still annotate successfully: ```python from varcode.effects import Failure effects = v.effects(raise_on_error=False) failures = [e for e in effects if isinstance(e, Failure)] assert failures print(failures[0].error) # retained diagnostic ``` On the command line, `--skip-errors` does the same: - Effects CSVs contain `Failure` rows and an `annotation_error` column. - Gene CSVs add `annotation_status` and `annotation_error` columns, with the gene fields left empty for failures. - Failures stay visible even with `--only-coding` and `--one-per-variant`. The latter selects one successful effect per variant and also keeps each failed transcript. - Input parsing errors still stop the command. Either way, the failed records are not corrected or annotated; record and review them. Filtered records are a separate matter: the CLI skips non-`PASS` records unless you pass `--include-filtered`, which you should use only when you intend to annotate them. openvax-varcode-d96eb06/docs/experimental_annotators.md000066400000000000000000000164041526602674500234750ustar00rootroot00000000000000# Experimental annotators **Most users don't need this page.** The default annotator already predicts protein sequences and handles structural variants; use [effects()](effect_annotation.md) without selecting an implementation. Varcode also includes two opt-in implementations, `protein_diff` and `transcript_model`, for comparing predictions. They do not support every input the default handles. ## Supported inputs | Selection | Method | Inputs | |---|---|---| | Default (omit `annotator=`) | Predicts small-edit and structural consequences; compares small edits against a patient baseline when supplied | Small variants and SVs. Germline/phase context for small edits, but not general SV-plus-germline combinations. | | `annotator="protein_diff"` | Edits and translates transcripts, then compares proteins; shares the default's splice, location, and germline helpers | Small variants. SVs are unsupported. | | `annotator="transcript_model"` | Builds transcripts for phase/splice hypotheses, compares each with the patient baseline, and merges equivalent results | Small variants and local DEL/DUP/INV. Insertions need `alt_assembly`; CNVs are unsupported. | The transcript-model experiment delegates BNDs to the structural helper; it does not resolve BND-plus-haplotype combinations. DUP/INV events that extend beyond its layout are left unresolved; see [limitations](#known-limitations). The default's registry name is `fast`; it appears in provenance but need not be passed explicitly. `protein_diff` is an alternative implementation, not an option you need for protein output. ## Comparing annotators Given a `variant` and one of its `transcript` objects, selection is explicit: ```python effect = variant.effect_on_transcript(transcript) # ordinary use comparison = variant.effect_on_transcript(transcript, annotator="protein_diff") experimental = variant.effect_on_transcript(transcript, annotator="transcript_model") ``` The transcript model can use `germline=` and `phase_resolver=` context. Canonical and exon-skip paths need only transcript annotation; a genome with reference FASTA additionally supplies sequence for intron retention and cryptic splice sites. Without a calibrated scorer, mechanism preference is an ordering rule, not a probability. Selecting this experiment does not guarantee that every structural or combined input is supported. Through `effects(germline=...)`, the transcript model uses the same phase cap as the default path (`GermlineContext.max_phase_hypotheses`, default 8). It also caps the combined phase × splice outcomes at `max_hypotheses=64`, which is the phase cap too when `predict_transcript_model_effect` is called directly. Exceeding either returns a [`HypothesisLimit`](germline.md#phase-enumeration-limit) rather than a partial set of candidates. ### Mixed inherited and somatic haplotypes `predict_transcript_model_effect(variants, transcript, germline_variants=...)` and `annotate_haplotype` take a known-cis group. Members matching the supplied germline are included once in the patient baseline. Only novel alleles are applied to that baseline to obtain the mutant; `effect.variant` is the first novel member and `effect.variants` retains the supplied group. Matching uses reference assembly, contig, normalized position, REF and ALT. A group with no novel allele returns `GermlineAlleleOverlap`, without inferring LOH. The group itself supplies cis constraints. Phase can propagate from any member to other germline alleles; unlinked alleles still produce separate hypotheses. Homozygous alleles are on both haplotypes and do not bridge their phase. As with other phase constraints, conflicting resolver answers are logged and ignored. Each hypothesis retains its germline cis/trans assignments, phase state, resolver source and novel `somatic_variants` in its evidence. `VariantCollection.effects` constructs these groups only when the resolver supports cis. Unknown or trans relationships retain individual predictions; listing alleles in a collection alone does not assert that they are in cis. ## Transcript-model results Use the [sequence accessors](transcript_models.md#sequence-access) for a single effect, including the same checks for missing sequences and partial structures. The ordinary and experimental candidate wrappers are not interchangeable: | Candidate type | Shared access | Additional information | |---|---|---| | `EffectCandidate` (ordinary splice/SV/RNA outcome sets) | `candidate.effect` | `source`, `evidence`; sequences are on the effect or its optional `mutant_transcript` | | `RealizedEffectCandidate` (transcript-model hypothesis pipeline) | `candidate.effect` | `outcomes`, `hypotheses`, `probability`, `ordinal_key`; each outcome has `baseline` and `mutant` products with `cdna_sequence`, `protein_sequence` and `evidence` | The experiment returns an ordinary top effect with candidates attached, not necessarily a `MultiOutcomeEffect`. Its BND delegation uses ordinary candidates; early noncoding, incomplete or unresolved results may have no candidates at all. Inspect the returned shape rather than assuming it from the annotator name: ```python from varcode import EffectCandidate from varcode.effect_hypotheses import RealizedEffectCandidate print(experimental.short_description) for candidate in getattr(experimental, "candidates", ()): print(candidate.effect.short_description) if isinstance(candidate, RealizedEffectCandidate): # A merged candidate can retain several hypotheses and their products. for outcome in candidate.outcomes: print(outcome.baseline.protein_sequence, outcome.mutant.protein_sequence) print(outcome.mutant.cdna_sequence, outcome.hypothesis.evidence) elif isinstance(candidate, EffectCandidate): print(candidate.source, candidate.evidence) print(candidate.effect.mutant_protein_sequence) ``` ## Known limitations - Combined variants: the selected annotator makes known-cis joint predictions through `VariantCollection.effects(phase_resolver=...)`. The default and `protein_diff` build point-edit haplotypes; joint germline and structural composition requires the opt-in `transcript_model`. Unsupported groups remain explicit unresolved results. See the [joint contract](annotator_contract.md#joint-haplotypes). - Rearrangements beyond the layout: the transcript model leaves an overlapping DUP/INV unresolved when either boundary extends beyond its finite genomic layout, because a clipped copy of the interval cannot establish the complete rearranged transcript or an unchanged protein ([#449](https://github.com/openvax/varcode/issues/449)). Fully represented local events still use the layout model, and supplied transcript assemblies keep their sequence, with unmapped CDS/translation uncertainty preserved. Unresolved results report `None`, not `False`, for sequence-change flags. `drop_silent_and_noncoding()` retains them by default; see [SV filtering](structural_variants.md#filtering-by-protein-change). ## Previous names `realized`, `RealizedEffectAnnotator`, and `predict_realized_effect` are aliases for the transcript-model implementation. Use the `transcript_model` names in new code. The `RealizedEffectCandidate` wrapper still has a different interface from ordinary `EffectCandidate`, as shown above. To implement your own, see [Writing an annotator](annotator_contract.md). openvax-varcode-d96eb06/docs/genotype.md000066400000000000000000000077511526602674500203670ustar00rootroot00000000000000# Genotypes and sample-aware queries When you load a multi-sample VCF, Varcode keeps each sample's genotype fields (GT, AD, DP, GQ, PS). You can read them as `Genotype` objects, or use them to select the variants a particular sample carries. ## The basics ```python from varcode import load_vcf vc = load_vcf("tumor_normal.vcf", genome="GRCh38") vc.samples # ['normal', 'tumor'] vc.has_sample_data() # True ``` ### One variant, one sample ```python variant = vc[0] gt = vc.genotype(variant, "tumor") # Genotype(raw_gt='0/1', alleles=(0, 1), phased=False, phase_set=None, # allele_depths=(10, 5), total_depth=15, genotype_quality=99) gt.is_called # True gt.alleles # (0, 1) gt.phased # False gt.allele_depths # (10, 5) — (ref_depth, alt_depth) gt.total_depth # 15 gt.depth_for_alt(1) # 5 ``` ### Zygosity is relative to *this variant's* alt ```python from varcode import Zygosity vc.zygosity(variant, "tumor") # vc.zygosity(variant, "normal") # — normal is ref/ref at this site ``` Four states, relative to the alt of the `Variant` you query: | State | Meaning | |---|---| | `HETEROZYGOUS` | Sample has at least one copy of this alt, and not all copies are this alt | | `HOMOZYGOUS` | All called copies are this alt | | `ABSENT` | Sample was called, but doesn't carry this alt (ref/ref, or a different alt at a multi-allelic site) | | `MISSING` | Sample's call was `./.` or the sample wasn't called | ## Filtering These return filtered `VariantCollection`s: ```python # Variants where this sample carries the alt (het or hom). vc.for_sample("tumor") # Finer distinctions: vc.heterozygous_in("tumor") vc.homozygous_alt_in("tumor") ``` Typoed sample names fail fast: ```python vc.for_sample("typo") # SampleNotFoundError: Sample 'typo' not found. Available samples: ['normal', 'tumor'] ``` ## Cross-sample queries Tumor/normal, trio, and cohort queries fall out of set operations on the primitives: ```python # Somatic candidates: in tumor, not in normal. tumor = set(vc.for_sample("tumor")) normal = set(vc.for_sample("normal")) somatic = tumor - normal # De novo candidates in a trio. de_novo = (set(vc.for_sample("child")) - set(vc.for_sample("mom")) - set(vc.for_sample("dad"))) ``` These are genotype comparisons, not somatic or de novo calling. A variant counts as absent from the normal whenever the normal doesn't carry it, including when the normal has no call at that site (`MISSING`). Check `vc.zygosity(variant, "normal")` if that distinction matters. To check whether samples are labelled with the right donor, see [sample identity checks](sample_identity.md). ## Multi-allelic sites VCF rows like `REF=A ALT=T,G` are split into two `Variant` objects (one per alt). `zygosity` is computed relative to the specific alt of the variant you query, so a sample with `GT=1/2` is reported as heterozygous for both A→T and A→G (one copy of each, with the other copy being a *different* alt). A sample with `GT=0/1` (one ref, one T) is heterozygous for A→T and `ABSENT` for A→G. This means `vc.heterozygous_in("tumor")` on a row where tumor is `1/2` yields two entries, one per alt — the correct biological interpretation. ## Beyond zygosity: phased and germline-aware effects The Genotype API gives you the data; downstream effect prediction uses it when you explicitly provide a phase resolver or germline context: - **Phased effects of cis variants** — when two variants share a phase set, varcode builds a joint `HaplotypeEffect` via `effects(phase_resolver=...)`. See [phasing](phasing.md#phased-vcf). - **Germline-aware somatic annotation** — pass a `GermlineContext` to `effects(germline=...)` and somatic variants are classified against the patient's germline-applied transcript. See [Germline-aware annotation](germline.md). There are no dedicated helpers such as `vc.de_novo_in(...)` or `vc.somatic(...)`; use the set operations above. openvax-varcode-d96eb06/docs/germline.md000066400000000000000000000210141526602674500203230ustar00rootroot00000000000000# Germline-aware annotation By default, Varcode compares each variant with the reference genome. Pass the patient's germline calls to classify somatic variants against the patient's own sequence instead. This matters when an inherited variant sits nearby: if it already changed a codon, a somatic mutation in that codon can produce a different amino acid than the reference codon would suggest. ## Basic usage ```python from varcode import GermlineContext, load_vcf somatic_variants = load_vcf("tumor.vcf", genome=81) germline_ctx = GermlineContext.from_germline_vcf("normal.vcf", genome=81) effects = somatic_variants.effects(germline=germline_ctx) ``` Use matching genome builds. `normal.vcf` should be a germline call set, not a sparse normal column from a somatic caller; see [constructing a context](#constructing-a-germlinecontext) for that distinction. Unknown relative phase can produce several candidate effects. See [two variants in one codon](phasing.md#two-variants-in-one-codon) for a worked example, or [phased VCF input](phasing.md#phased-vcf) to supply evidence. With many unphased germline variants nearby, the result is unresolved instead; see the [phase enumeration limit](#phase-enumeration-limit). No annotator selection is needed. ## Constructing a `GermlineContext` Choose the source that matches your input: ```python from varcode import Completeness, GermlineContext # Route 1: full germline call set from a real germline caller. ctx = GermlineContext.from_germline_vcf("normal.vcf", genome=81) # Route 2: multi-sample VCF, extract one column. ctx = GermlineContext.from_multi_sample_vcf( "tumor_normal.vcf", sample="NORMAL", genome=81, completeness=Completeness.SPARSE, # required, no default ) # Route 3: explicit no-germline fallback (== germline=None). ctx = GermlineContext.empty() # Route 4: direct construction from an existing collection of germline variants. ctx = GermlineContext.from_variants(germline_variants, reference_name="GRCh38") ``` `completeness=` distinguishes "no call here = ref/ref" (a real germline caller's output) from "no call here = unknown" (the `NORMAL` column of a somatic VCF, which only reports rows the somatic caller looked at). Forcing the caller to declare prevents silently mis-treating sparse data as complete. | Completeness | Pipeline | Absence at a position | |---|---|---| | `COMPLETE` | DeepVariant, HaplotypeCaller, Strelka2 germline | ⇒ ref/ref | | `SPARSE` | Mutect2 `NORMAL`, Strelka2 somatic | ⇒ unknown | | `HOTSPOTS_ONLY` | Panel-of-normals, ClinVar | ⇒ unknown | | `EMPTY` | Explicit no-data | n/a | When the context is sparse and a somatic variant lands in a window with no germline calls, varcode flags `effect.germline_unknown = True` rather than silently assuming ref/ref. ## Cross-VCF build mismatch Use a `VariantCollection` for collection-level build validation. It raises `GenomeBuildMismatchError` when its somatic variants and germline context have different reference builds: ```python from varcode import GenomeBuildMismatchError try: effects = somatic_variants.effects(germline=germline_ctx) except GenomeBuildMismatchError as error: print(error.somatic_reference, error.germline_reference) raise # correct the input builds before retrying ``` The one-variant API does not provide the same collection-level preflight. `validate_reference=False` is an option on `VariantCollection.effects()`, not `Variant.effects()`; it suppresses that check and does not convert coordinates. Prefer correcting mismatched inputs. ## How germline changes predictions Nearby germline variants can change the baseline codon or splice signal used to classify a somatic variant. The current default uses a local window, not a complete patient-genome reconstruction; see [limitations](#limitations). Phase-set tags describe phase within their source VCF. Do not equate tags from independently produced tumor and normal files. Use a resolver backed by a jointly phased VCF or molecular evidence. Short- and long-read data can both leave phase unknown; coverage and linked alleles determine whether a particular pair is resolved. ## Phase enumeration limit Each germline variant of unknown phase doubles the number of cis/trans combinations. Homozygous germline variants are always cis, and variants phased to each other (for example in one VCF phase set) count once, so both reduce the count. When the combinations exceed the cap (default eight, so more than three unphased germline variants in a window), Varcode does not guess a phase. It returns a `HypothesisLimit` effect, a kind of `Unresolved`: - Its `phase` records which germline variants are known cis, known trans, or unphased. - Its `reference_effect` is the effect without germline context. The `HypothesisLimit` is ranked like that effect. To raise the cap, set it on the context: ```python import dataclasses ctx = dataclasses.replace(ctx, max_phase_hypotheses=16) ``` ## Loss of heterozygosity (LOH) When a reported somatic allele matches the supplied germline, Varcode returns `GermlineAlleleOverlap` with `is_germline_overlap=True`, `is_loh=None`, and `loh_status="not_assessed"`. It does not apply the inherited allele a second time or claim a new somatic protein sequence. The allele may still have a functional effect relative to the genome reference. LOH requires evidence about loss of an allele previously present in the normal sample ([NCI definition](https://www.cancer.gov/publications/dictionaries/genetics-dictionary/def/loss-of-heterozygosity)). Varcode's sequence annotation does not perform that tumor/normal allelic-state analysis. An allele match, including unchanged heterozygous or inherited homozygous calls, cannot establish LOH. Integrate a dedicated LOH caller's evidence separately. ```python for effect in effects: if getattr(effect, "is_germline_overlap", False): print("Germline-overlap flag:", effect.short_description) ``` Here `effects` is the collection annotated with germline context above. The check requires `germline=`; annotation without that context cannot attach the flag. Use `detect_germline_overlap` for an allele-match query. The deprecated `detect_loh` now returns `None` (not assessed) and warns, rather than making a zygosity claim. The experimental transcript model composes known-cis groups that mix inherited and somatic alleles, applying each inherited allele once; see [mixed inherited and somatic haplotypes](experimental_annotators.md#mixed-inherited-and-somatic-haplotypes). ## Limitations - **Germline-disrupted splice sites get no explicit downgrade.** Classification runs against the patient's signal, but there's no "germline already broke this, downgrade severity" path. - **Subclonal somatic and CNV dosage are not modeled.** Every somatic variant is treated as 100% present. - **Hypothesis cap of 8** by default when phase is unknown across multiple germline variants in a window; above it the effect is [unresolved](#phase-enumeration-limit). Change it with `GermlineContext.max_phase_hypotheses`. - **Normalization mismatch** between germline and somatic VCFs (left-alignment, MNV split) causes apparent position mismatches. Normalize both with the same tool first. - **Population-frequency germline** (gnomAD/ExAC as a substitute for patient germline) is not supported. ## Lower-level helpers The high-level `effects(germline=...)` path delegates to `predict_germline_aware_effect`. Both that function and `apply_germline_to_transcript` (returns a `MutantTranscript` with germline edits applied) are public — call them directly if you have a custom annotator or want the patient protein without full effect prediction. ## Related guides - [Two variants in one codon](phasing.md#two-variants-in-one-codon). - [Phasing from VCF or RNA](phasing.md). - [Combining germline, phase, and RNA evidence](phasing.md#combining-evidence). - [Known deletion haplotypes in RNA](phasing.md#known-deletion-haplotypes-in-rna-alignments). - [Genotypes and sample queries](genotype.md), [effect annotation](effect_annotation.md), and the [germline API](api_phasing.md#germline-aware-annotation). - [Germline development roadmap (#268)](https://github.com/openvax/varcode/issues/268). openvax-varcode-d96eb06/docs/getting_started.md000066400000000000000000000223301526602674500217120ustar00rootroot00000000000000# Getting started This guide takes you from installation to a predicted effect and protein sequence, then shows how to annotate a file and read the results. The examples use Python 3.11 or later. ## Install ```bash pip install varcode ``` If you are upgrading an existing environment, see [dependency versions](#dependency-versions). ## Reference data Varcode uses [PyEnsembl](https://github.com/openvax/pyensembl) for gene annotations and transcript/protein sequences. Download the release used in these examples: ```bash pyensembl install --release 81 --species human ``` This is a one-time download. The examples pin Ensembl 81 (GRCh38) so their coordinates and transcript IDs are reproducible; you don't need to use that release for your own data. For your own data, use an annotation that matches the genome build your variants were called against. GRCh37 input, for example, needs a GRCh37 annotation such as Ensembl 75, installed separately. To pin the annotation exactly, pass `genome=` one of: - A release number, such as `genome=81`. - An assembly with a release, such as `genome="GRCh38:93"` or `"GRCh38.93"`. A release that does not provide the assembly, such as `"GRCh38:75"`, is an error. - An assembly with an annotation date, such as `genome="GRCh38:2026_04"`, `"GRCh37:2013_09"` or `"ROS_Cfam_1.0:2020_10"`. Use `YYYY_MM`; the requested assembly must match. See [dated reference setup](api_variants.md#pin-an-annotation-release). - A PyEnsembl genome object. An assembly name alone, such as `genome="GRCh38"`, uses the most recent installed numbered release of that assembly. Selecting a reference does not download data or convert variant coordinates. ### Chromosome DNA for SV and splice analysis Transcript FASTA contains exons. Supply a chromosome FASTA to also resolve intronic sequence, SV breakpoint windows and extra-ORF splice products: ```python from pyensembl import EnsemblRelease genome = EnsemblRelease(81, genome_fasta="/data/GRCh38.fa") ``` Pass this same `genome` to your variants or file loader. For explicit download setup, older PyEnsembl versions and resource ownership, see [reference genomes](api_variants.md#reference-genomes). ## Annotate one variant ```python from varcode import Variant variant = Variant("7", 117_531_100, "T", "A", genome=81) transcript = variant.genome.transcript_by_id("ENST00000003084") effect = variant.effect_on_transcript(transcript) print(effect.gene.name) # CFTR print(effect.short_description) # p.L159M ``` This predicts a leucine-to-methionine substitution at amino acid 159 of the selected transcript. To annotate all overlapping transcripts instead, use `variant.effects()`. A variant may have different consequences on different transcripts, so keep the transcript identity with each result. ## Read the predicted protein ```python protein = effect.mutant_protein_sequence if protein is not None: print(protein[158]) # M: Python offsets start at zero ``` `None` means the sequence could not be determined for that effect, not that the protein is unchanged. Some effects contain several possible outcomes, each with its own protein; see [alternative outcomes](effect_annotation.md#alternative-outcomes). ## Load a file Replace the path with your own VCF called against GRCh38: ```python from varcode import load_vcf variants = load_vcf("variants.vcf", genome=81) effects = variants.effects() ``` `load_vcf` loads passing records by default. Use `only_passing=False` if you also want records marked by the caller's FILTER field. If your input contains structural variants, add `parse_structural_variants=True`; otherwise symbolic and breakend records are skipped with a warning. See the [structural variant guide](structural_variants.md#basic-usage). For a MAF, use `variants = varcode.load_maf("variants.maf")` after importing `varcode`; the MAF's build information supplies the reference. See [file-loading parameters](api_variants.md#file-loading) for format-specific options. ## Summarize and save results Choose one effect per variant when you need a compact report: ```python for variant, effect in effects.top_priority_effect_per_variant().items(): print(variant.short_description, effect.short_description) effects.to_csv("effects.csv") ``` Keep the full `effects` collection when you need every transcript's prediction. Selected splice effects retain their complete candidate sets. Note that `effects.top_priority_effect()` (without `_per_variant`) returns a single effect for the entire collection. CSV summaries list every current splice candidate's description, with the affected signal in `splice_signal`. Collections containing extra ORFs also carry full graphs in `effect_json`; ordinary summary CSVs do not archive candidate objects or their evidence. Keep your input files too. See [saving and reloading results](csv.md) for what survives a round trip. ## How to read results Varcode's main objects: | Object | What it is | |---|---| | `Variant` | One alternate allele at a genomic position. A VCF row with two ALT alleles becomes two variants. | | `VariantCollection` | The variants loaded from your files, with their sample genotypes and source metadata. | | Effect | The predicted consequence of one variant on one transcript, such as `Substitution` (`p.L159M`), `FrameShift`, or `Intronic`. All effects subclass `MutationEffect`. | | `EffectCollection` | The effects for a collection of variants. Filter, summarize, or save it. | | Multi-outcome effect | An effect with several possible `candidates`, used for splice variants, structural variants, and unknown phase. See [alternative outcomes](effect_annotation.md#alternative-outcomes). | | `Unresolved` | Varcode could not determine the consequence from the available sequence and annotation. | When interpreting them: - **Predictions are per transcript.** Keep the transcript with each result; the same variant can be missense on one isoform and noncoding on another. - **Priority means severity.** `top_priority_effect_per_variant()` uses Varcode's consequence ordering. It is not the likeliest outcome, a pathogenicity score, or a clinical classification. - **`None` means unknown.** A missing protein sequence, or a `None` answer to "does this change the protein?", is not the same as "unchanged". - **Predictions are not observations.** A predicted protein is not evidence that the transcript or protein is expressed. ## Command line The `varcode` command annotates files without writing Python, and `varcode-genes` lists the genes each variant overlaps: ```bash varcode --genome GRCh38 --vcf variants.vcf --output-csv effects.csv varcode-genes --genome GRCh38 --variant chr12 25245350 C T --output-csv genes.csv ``` For SV/splice analysis with optional noncanonical ORFs: ```bash pip install 'varcode[orf]' varcode --genome GRCh38:81 --genome-fasta /data/GRCh38.fa \ --vcf variants.vcf --extra-orfs all --output-csv effects.csv ``` Use `--extra-orfs ms` to select ORFs with MS evidence. ORFdata chooses the catalogue for the reference species and assembly; missing data produces setup instructions. CSV files containing extra ORFs include an `effect_json` column so `EffectCollection.from_csv()` can restore the complete consequences. Inputs can be combined and repeated: `--vcf`, `--maf`, `--variant`, and `--json-variants`. `--genome GRCh38:93` pins Ensembl release 93; `--genome GRCh38:2026_04` pins the April 2026 annotation, as in Python. The commands differ from `load_vcf` in two ways: - They load structural variants automatically. - They skip records whose FILTER is not `PASS` or `.`, and report how many were skipped. Use `--include-filtered` to keep them. Records on contigs missing from the annotation remain visible as failures with a summary warning. Other annotation errors stop the command. `--skip-errors` continues and records each failure in the output; see [continuing past errors](errors.md#continuing-past-errors). For sample mix-up screening, see [`varcode check-samples`](sample_identity.md). ## Coordinate conventions Genomic positions are 1-based and ranges are inclusive. Specify variant alleles on the reference genome's forward strand, even for genes on the reverse strand. Protein/cDNA offsets exposed for Python slicing are 0-based. Structural records also distinguish [junction coordinates from affected spans](sv_reference.md#alleles-coordinates-and-exports). ## Dependency versions Varcode requires PyEnsembl 2.24.1+ and gtfparse 3.0.2+, and supports pandas 2.2.2+ and 3.x on Python 3.11 or later. To upgrade an existing environment to pandas 3, use Python 3.11 or later and run: ```bash pip install --upgrade varcode pyensembl 'gtfparse>=3.0.2,<4' 'pandas>=3,<4' ``` Environments shared with Topiary require Topiary 5.94.2 or later for its gtfparse 3 support. ## Next steps - [Effect annotation](effect_annotation.md): inspect results, sequences, and alternatives. - [Sample-aware queries](genotype.md): filter a multi-sample VCF. - [Sample identity checks](sample_identity.md): compare donor genotypes and somatic tumor calls. - [Structural variants](structural_variants.md): SVs, fusions, and observed RNA. - [Germline and phasing](germline.md): use a patient's baseline and linked variants. - [Troubleshooting](errors.md): check genome build, alleles, and sample names. Detailed signatures and options are in the [API reference](api.md). openvax-varcode-d96eb06/docs/index.md000066400000000000000000000047421526602674500176410ustar00rootroot00000000000000# Varcode Varcode is a Python library that predicts what genomic variants do to genes, transcripts, and proteins. For each variant on each overlapping transcript, it reports the predicted consequence and, where it can be determined, the mutant protein sequence. **New here?** [Getting started](getting_started.md) walks through installing reference data, annotating a variant and a VCF, reading the predicted protein, and saving results. ## Annotate a VCF ```python import varcode # Use an annotation release matching your VCF's genome build. variants = varcode.load_vcf("variants.vcf", genome=81) # GRCh38 effects = variants.effects() for variant, effect in effects.top_priority_effect_per_variant().items(): print(variant.short_description, effect.short_description) ``` The [setup instructions](getting_started.md#reference-data) install the reference data for this example. Before relying on the output, read [how to read results](getting_started.md#how-to-read-results): predictions are per transcript, and "top priority" means most severe, not most likely. ## Guides **Everyday tasks** - [Read effects and protein sequences](effect_annotation.md) - [Select variants for a sample or compare tumor and normal](genotype.md) - [Check a cohort for sample mix-ups](sample_identity.md) - [Save and reload tables](csv.md) - [Fix reference, allele, and sample-name errors](errors.md) **Specific variant types and evidence** - [Splice variants and their possible outcomes](splice_variants.md) - [Structural variants and gene fusions](structural_variants.md) - [Compare SV calls across callers and samples](sv_comparison.md) - [Include patient germline variants](germline.md) or [phase linked variants](phasing.md) - [Attach observed RNA structures or Exacto protein predictions](rna_structures.md) - [Pair breakends or left-align indels](transforms.md) - [Effects on upstream and other reference ORFs](uorf_annotations.md) (optional `varcode[orf]` extra) ## Reference and extensions - [Varcode, Isovar, and Vaxrank](library_roles.md): which library does what. - [Transcript models](transcript_models.md): cDNA, partial structures, and missing sequence. - [Effect types](effect_types.md) and [API reference](api.md): classes and parameters. - [Experimental annotators](experimental_annotators.md): optional alternative implementations. - [Writing an annotator](annotator_contract.md): plugging in your own model. - [Changelog](changelog.md): release history. openvax-varcode-d96eb06/docs/library_roles.md000066400000000000000000000111661526602674500214000ustar00rootroot00000000000000# Varcode, Isovar, and Vaxrank The libraries answer different questions about the same variant. Use Varcode for predicted consequences, Isovar for RNA reconstruction and evidence, and Vaxrank for evaluating protein/peptide candidates. | Library | Responsibility | |---|---| | **Varcode** | Generate structural/transcript hypotheses and predict their coding consequences. | | **Isovar** | Reconstruct RNA-supported sequences, compare them with those hypotheses, and preserve unresolved alternatives. | | **Vaxrank** | Evaluate the resulting protein/peptide candidates, retaining their evidence. | This is the shared responsibility split, not a claim that every path is already connected. The implementation limits below matter when building a pipeline. ## Varcode's part Varcode models possible effects on transcripts and proteins. Its inputs can include DNA variants, germline/phase context, and externally reconstructed RNA structures. It should not independently collect BAM reads or decide which RNA assembly the patient expresses. Use the ordinary `effects()` interface; no special annotator selection is needed. Keep candidate sets when a variant has several plausible consequences. Candidate order and effect severity are not RNA support or calibrated probabilities. Phase evidence follows the same split. Isovar's `IsovarReadPhasing` reports cis or trans only from RNA fragments that cover both variants, and Varcode treats every other pair as unknown; see [RNA phase from Isovar](phasing.md#rna-phase-from-isovar). Isovar can return a structure that changes the original hypothesis, including a previously unmodeled junction. Varcode's job is to represent that structure and predict its coding consequences, not force it into the first reference isoform. [RNA imports](rna_structures.md) describe the existing entry points. ## What must survive the handoff - Nucleotide sequence and structure, coordinates, reference/annotation identity, and compatible transcript IDs—not only a protein string. - Partial versus complete sequence, reading-frame evidence, and unresolved regions. - Sample/library and read/fragment provenance, support, conflicts, and assumptions. - Separate alternatives when evidence cannot distinguish them. Grouping identical proteins must preserve the contributing structures and their evidence. RNA support for a local junction does not establish a full-length transcript or prove translation. Missing coverage is not evidence against a hypothesis. A predicted protein change and evidence that the RNA exists are separate facts. ## Available today and remaining work - **Varcode:** structural partner candidates and supplied RNA imports exist. Splice/phase/SV combinations are not exhaustively composed ([#423](https://github.com/openvax/varcode/issues/423)). Change flags for imported partial proteins stay unresolved unless mapped, in-frame observed codons differ from the reference; an unobserved suffix is not a demonstrated protein deletion. - **Isovar:** the small-variant path reconstructs RNA context; the separate supplied-fusion path validates RNA and retains alternative frame hypotheses. Automated collection, alternative-path assembly, and competitive reconciliation for nominated SVs are tracked in [Isovar #305](https://github.com/openvax/isovar/issues/305). - **Vaxrank:** the ordinary RNA path selects Isovar's top protein; a separate supplied-fusion adapter retains coding hypotheses. The opt-in DNA fallback checks each structural candidate's protein before ranking ([Vaxrank #482](https://github.com/openvax/vaxrank/issues/482)). `--germline-vcf` reaches Isovar, so matched germline edits in assembled RNA are labelled rather than left unexplained. A DNA-only fallback remains a prediction without RNA confirmation. It must not silently discard alternatives or relabel missing RNA as evidence of absence. ## Compatible versions | Package | Requires | |---|---| | Vaxrank 3.24 | Varcode ≥10.4.1, <11 and Isovar ≥1.38.1, <1.39 | | Vaxrank 3.22, 3.23 | Varcode ≥10.4.1, <11 and Isovar 1.37 | | Isovar 1.37.2 or later | Varcode ≥10.5.2, <11 | | Isovar ≥1.38 | Needed for germline trans from RNA reads; 1.36 and 1.37 report germline cis only | | Isovar ≥1.36 | Needed for `IsovarReadPhasing.in_cis`; older versions report cis only | Varcode 10.4.1 fixes a structural-effect ranking crash that Vaxrank's DNA fallback hits on earlier Varcode 10 releases. ## Other library guides - [Isovar: RNA reconstruction and reconciliation](https://github.com/openvax/isovar/blob/master/docs/library-responsibilities.md). - [Vaxrank: candidate evaluation and evidence](https://openvax.github.io/vaxrank/library-responsibilities/). openvax-varcode-d96eb06/docs/phasing.md000066400000000000000000000257241526602674500201660ustar00rootroot00000000000000# Phasing Two nearby variants, for example in the same codon, can have a different combined effect than either one alone, but only if they sit on the same copy of the chromosome (*in cis*). Variants on different copies (*in trans*) act separately. Phasing evidence tells Varcode which is the case. Pass `phase_resolver=` when you have that evidence, from a phased VCF or RNA reads. Varcode then predicts combined effects for variants in cis. To classify somatic variants against the patient's own germline sequence, also pass [germline context](germline.md). ## Phased VCF Load the jointly phased calls and select the sample whose phase should be used: ```python from varcode import load_vcf, VCFPhaseResolver phased_variants = load_vcf("merged_phased.vcf", genome=81) phaser = VCFPhaseResolver(phased_variants, sample="TUMOR") effects = phased_variants.effects(phase_resolver=phaser) ``` The resolver uses phased `GT` and `PS` fields from the loaded collection. Use one resolver per sample. Phase-set tags are local to their source VCF: matching tags in independently produced tumor and normal files do not establish relative phase. A joint call set or molecular evidence is needed. Individual effects remain alongside joint results. For supported point edits, the default annotator returns a joint `HaplotypeEffect`. Unsupported groups are retained as `Unresolved` with their `variants` and `phase_source`; they are never silently dropped. The default declines joint germline composition rather than ignoring the patient baseline. The selected annotator owns the joint prediction: the experimental `annotator="transcript_model"` returns classified phase/splice candidates instead. See the [annotator contract](annotator_contract.md#joint-haplotypes). For germline-aware somatic annotation, the resolver's collection should contain both sets of alleles, but call `somatic_variants.effects(germline=..., phase_resolver=phaser)` on the somatic variants you want to classify. ## RNA evidence For direct read/fragment co-occurrence in an RNA-seq BAM: ```python from varcode import MolecularPhaseResolver, RNAReadPhasingSource source = RNAReadPhasingSource("tumor.rna.bam") phaser = MolecularPhaseResolver(source) effects = variants.effects(phase_resolver=phaser) ``` Here `variants` is the loaded collection to annotate. Short- and long-read data can both leave phase unknown; coverage and linked alleles determine whether a pair is resolved. `RNAReadPhasingSource` sorts the fragments covering both loci into four combinations: both alt alleles, either variant's alt allele alone, or neither. A combination counts when it has at least `min_alt_reads` fragments and more than reads showing the wrong allele would leak into it. The second condition is a one-sided binomial test at `max_p_value_for_phasing` (default 0.05), with error rate `phasing_error_rate` (default 1%). Then: - Cis: both alts are seen together, and not each alone. - Trans: each alt is seen alone, and not together. - Unknown: anything else, including alts seen both together and each alone. A variant that arose later on the other's copy, or next to a germline variant, is always seen with it, while the earlier one also appears alone; that is cis, not trans. The rule, from `varcode.rna_read_phasing.four_gamete_phase`, matches Isovar 1.39.7. A source that only reports co-observed partners establishes cis and leaves every other pair unknown. Sources that see reference alleles too, such as `RNAReadPhasingSource`, report trans through their own `in_cis`. Raw BAM phasing does not provide an assembled `MutantTranscript`. Assembly-backed sources can provide one; see the [source protocols](api_phasing.md#phasing). `ReadPhaseResolver` remains a compatibility name for `MolecularPhaseResolver`. ### RNA phase from Isovar Isovar's results can serve as the phasing source. Give `run_isovar` the matched germline variants so they are recognized in the assembled RNA: ```python from isovar import IsovarReadPhasing, run_isovar from varcode import MolecularPhaseResolver results = run_isovar( variants=somatic_variants, alignment_file="tumor.rna.bam", germline_variants=germline_ctx.variants, ) phaser = MolecularPhaseResolver(IsovarReadPhasing(results)) effects = somatic_variants.effects(germline=germline_ctx, phase_resolver=phaser) ``` With Isovar 1.36 or later, `IsovarReadPhasing.in_cis` answers from fragments that cover both variants. Since Isovar 1.39.7 it uses the same four-combination rule as `RNAReadPhasingSource` above, with `min_shared_fragments_for_phasing` fragments, `phasing_error_rate` and `max_p_value_for_phasing`. Earlier versions called cis or trans by majority, which could call a nested subclone's pair trans or a read-error pattern cis. For a matched germline variant, Isovar 1.38 or later reads the germline site in fragments that carry the somatic alt allele: the germline alt allele there means cis, its reference allele trans. Fragments with the somatic reference allele are not counted, since the germline alt allele also comes from normal cells and homozygous sites. When those fragments do not decide, a germline edit in the somatic variant's assembled RNA is cis; when they say trans but the assembly has the edit, the answer is unknown. Isovar 1.36 and 1.37 only give the assembly-based cis. Any pair Isovar cannot call stays unknown, and Varcode keeps every phase hypothesis for it. ## Unknown phase When a somatic and germline variant share a codon and relative phase is unknown, the result can be a `PhaseCandidateSet`, with one classified effect per haplotype hypothesis. Read `.candidates` to retain the alternatives. Resolved phase can reduce the set to one effect, as in the example below. ## Two variants in one codon CFTR has a somatic at GRCh38 7:117531100 `T→A`. A neighbouring germline at 7:117531101 `T→C` lands in the same codon. ```python from pyensembl import cached_release from varcode import Variant, VariantCollection, GermlineContext g = cached_release(81) cftr = g.transcript_by_id("ENST00000003084") somatic = Variant("7", 117_531_100, "T", "A", genome=g) germline = Variant("7", 117_531_101, "T", "C", genome=g) ctx = GermlineContext.from_variants([germline], reference_name="GRCh38") ``` ### Without germline context ```python eff = somatic.effect_on_transcript(cftr) print(type(eff).__name__, eff.short_description) # Substitution p.L159M ``` ### With germline context, phase unknown ```python eff = somatic.effect_on_transcript(cftr, germline=ctx) print(type(eff).__name__, eff.short_description) # PhaseCandidateSet ?p.L159M for c in eff.candidates: ev = c.evidence print(f" haplotype={ev['haplotype']:<2} " f"germline_in_cis={[v.short_description for v in ev['germline_variants']]} " f"=> {c.effect.short_description}") # haplotype=B germline_in_cis=[] => p.L159M # haplotype=A germline_in_cis=['chr7 g.117531101T>C'] => p.S159T ``` The `?` prefix on `?p.L159M` flags the description as the most-likely candidate of a `PhaseCandidateSet`. In application code, check `isinstance(eff, PhaseCandidateSet)` and iterate `eff.candidates` (a tuple of `EffectCandidate` objects carrying per-hypothesis evidence keys); use `eff.effects` if only the inner classified `MutationEffect`s are needed. ### With known phase A real pipeline gets the cis/trans answer from a phased VCF or an RNA assembly. For this demo, two stub resolvers force each answer so you can see the candidate set collapse to a single effect: ```python class ForceCis: source = "demo" def in_cis(self, v1, v2, transcript=None): return True class ForceTrans: source = "demo" def in_cis(self, v1, v2, transcript=None): return False eff_cis = somatic.effect_on_transcript( cftr, germline=ctx, phase_resolver=ForceCis()) print("cis ->", type(eff_cis).__name__, eff_cis.short_description) # cis -> Substitution p.S159T eff_trans = somatic.effect_on_transcript( cftr, germline=ctx, phase_resolver=ForceTrans()) print("trans ->", type(eff_trans).__name__, eff_trans.short_description) # trans -> Substitution p.L159M ``` For real evidence, use the [phased VCF](#phased-vcf) or [RNA](#rna-evidence) resolvers above. The stubs implement only `in_cis(...)`, which is all the codon-collapse path consults. Richer resolvers implement more of the duck-typed interface, including `mutant_transcript` and `phased_partners`; the [phasing API](api_phasing.md#phasing) documents the built-in resolvers and source protocols. No public `PhaseResolver` base class is required. ## Combining evidence Given a somatic `VariantCollection`, patient context, phase resolver, and RNA resolver: ```python effects = somatic_variants.effects( germline=germline_ctx, phase_resolver=phaser, rna_resolver=rna, ) ``` The inputs apply in order: 1. Germline variants modify the transcript. 2. Phase evidence collapses the candidate set. 3. RNA evidence refines multi-candidate effects. Splice mechanism sets are reconciled to the observed mechanisms; other multi-outcome effects gain observed-only candidates. `EffectCandidate.evidence["haplotype"]` links the three: an RNA observation tagged with the same haplotype aligns with the matching germline-aware outcome. ## Known deletion haplotypes in RNA alignments Splice-aware aligners can encode a known deletion's RNA sequence with `N` rather than `D`, or split the gap among mismatches and smaller deletions. Opt in to a local **sequence hypothesis** instead of interpreting the gap as proof of a DNA deletion: ```python from varcode import MolecularPhaseResolver, RNAReadPhasingSource source = RNAReadPhasingSource("tumor.rna.bam", min_alt_reads=1) source.register_haplotype([known_deletion, adjacent_snv_1, adjacent_snv_2]) resolver = MolecularPhaseResolver(source) effects = variants.effects(phase_resolver=resolver) ``` The variants must share one genome dataset and contig. The genome must provide reference sequence across the interval, through a FASTA or annotated transcript. Every retained base between five-base reference flanks must match one alignment and pass the configured quality/read-edge filters. No mates or separate partial haplotypes are stitched together. Registration invalidates cached counts. Registration does not assert cis phase: it supplies a candidate to test. For registered variants, only full-context matches count as alternate support; nonmatches remain unknown, not trans evidence. The current local mode supports nonoverlapping substitutions and deletions. Register competing combinations separately. Unregistered variants keep the ordinary CIGAR-based behavior. For example, in GRCh38 MAP2 the combination of `209694769 C>A`, `209694770 T>G`, and the 28-base deletion at `209694773` gives the same local sequence under `28D`, `28N`, and `22D2M6D` alignments. An unrelated 19,449-base exon skip lacks the required local anchors and is not supporting evidence. ## Implementation limits See [germline limitations](germline.md#limitations) for the phase-hypothesis cap and normalization requirements, and the [phasing API](api_phasing.md) for custom sources. openvax-varcode-d96eb06/docs/rna_structures.md000066400000000000000000000352311526602674500216120ustar00rootroot00000000000000# Importing observed RNA structures If an RNA tool such as Exacto or a transcript assembler has reconstructed the transcript a [structural variant](structural_variants.md) produces, you can attach that observed structure to the DNA variant. Varcode then predicts the coding consequences of the observed RNA alongside its own DNA-based predictions, without treating an RNA splice junction as a DNA breakpoint. The examples below assume that the DNA variants and relevant reference transcripts are already loaded. Varcode represents supplied structures and predicts their coding consequences; RNA reconstruction and read-based reconciliation belong to Isovar or another RNA producer. See [the library responsibilities](library_roles.md) for the split and the remaining end-to-end SV workflow. ## Import Exacto transcripts Use an existing DNA `StructuralVariant` as the anchor, so an RNA splice junction is not mistaken for a genomic breakpoint. Multiple observed RNA models can be attached to that same variant: ```python from varcode import load_exacto_fusions rna = load_exacto_fusions( "transcript_structures.tsv", "integrated_variants.tsv", variants_by_id={"42": structural_variant}, ) effects = variants.effects(rna_resolver=rna) for candidate in rna.candidates: sequence = candidate.effect.mutant_transcript.cdna_sequence model_id = candidate.evidence["transcript_model_id"] ``` `variants_by_id` maps Exacto DNA call IDs, not assumed VCF record IDs, to already loaded variants. Only these IDs are selected from the integration file. Imported models are keyed by both `transcript_model_id` and the set of reference transcript IDs. Different models or reference interpretations, and their DNA links, are kept; duplicate integration rows do not duplicate a candidate. The complete original structure and integration rows are in `candidate.evidence` and the mutant transcript's evidence. Row counts are **not** supporting-read counts. The adapter handles linear, two-locus, SV-linked models with an annotated sense 5' anchor: - A missing or antisense 3' annotation gives `TranslocationToIntergenic`, whose existing meaning includes non-sense joins. - Unknown transcript IDs and incomplete or unsupported structures raise errors. - Other Exacto DNA/RNA variant formats, circular RNA, and multi-gene paths are not implemented. They remain in the broader [#260](https://github.com/openvax/varcode/issues/260) roadmap. For how Exacto rows and coordinates are converted, see [Exacto format details](#exacto-format-details). ## Other RNA producers For other producers, use the same existing effect classes directly: ```python from varcode import RNAEvidence, make_fusion_outcome candidate = make_fusion_outcome( structural_variant, five_prime_transcript, partner_transcript=three_prime_transcript, # only when sense-oriented sequence=observed_sequence, # already 5' to 3' transcript_model_id="assembled-model-7", source="rna_assembler", read_count=6, extra_evidence={"sequence_status": "junction_fragment"}, ) effects = variants.effects(rna_resolver=RNAEvidence([candidate])) ``` Do not supply a sense partner merely because a gene name appears near a breakpoint. Leave `partner_transcript=None` for unresolved/intergenic/antisense partners and record the observed loci/orientation in `extra_evidence`. The importer preserves the supplied sequence, including insertions, without appending reference exons. Read counts do not become probabilities or change default candidate ranking. DNA predictions and RNA observations coexist. Protein sequence is `None` by default. A caller can pass `cds_start=<0-based offset>` to `make_fusion_outcome`, or a `cds_starts` mapping keyed by `("model-id", ("sorted-reference-id-1", "sorted-reference-id-2"))` to the Exacto loader. This requires a valid unambiguous start-to-stop ORF in the supplied sequence. It produces a **sequence-predicted protein**, not evidence of translation, full-length RNA, tumor-cell identity, or a mature two-gene fusion. ## Import Exacto's protein predictions When Exacto has already selected reading frames, supply its native primary-structures table instead of choosing `cds_starts` yourself: ```python rna = load_exacto_fusions( "transcript_structures.tsv", "integrated_variants.tsv", variants_by_id=variants_by_id, primary_structures_path="primary_structures.tsv", ) for candidate in rna.candidates: print(candidate.evidence.get("exacto_peptide_id")) print(candidate.effect.mutant_protein_sequence) print(candidate.evidence.get("protein_completeness")) ``` Each `(model, reference-transcript group, peptide_id)` remains separate. The loader validates native `primary_structure_index`, `amino_acid_index`, and `codon_index`, checks each nucleotide against its `transcript_structure_index` and inclusive read coordinates, and checks the amino acid against its codon. The amino acid repeated on all three base rows is emitted once. Terminal `*` is retained in provenance but omitted from `mutant_protein_sequence`. Partial peptides are available, with `protein_completeness` set to `partial_start`, `partial_end`, or `partial_both`; start-to-stop predictions are labeled `start_to_stop`. Trailing incomplete codons cannot claim an amino acid. Missing peptide rows leave that model's protein `None`. Per-base variant IDs, frameshift state and all original fields remain in `exacto_primary_structure`. Exacto's standard genetic-code choice is recorded as `protein_translation_table=1`. !!! warning "Check completeness before using a protein" These sequences are **predictions from RNA**, not evidence of translation. A complete start-to-stop ORF need not cross the rearrangement or establish a full-length fusion transcript. Check `protein_completeness` and the source coordinates rather than treating every protein string as a complete expressed fusion protein. SV sequence-change flags respect `protein_completeness`: a partial peptide is never compared with the full reference protein, because missing sequence is neither unchanged nor a truncation. With native primary structures, the imported ORF bounds and verified transcript segments can establish a local change even when the protein is partial. A junction into different sequence or a frameshift can therefore have `modifies_protein_sequence=True`. Unchanged observed fragments and observations without a usable reference frame stay unresolved (`None`). ??? note "Exact rules for partial and completeness-unknown observations" The ORF bounds (`cds_start` / `cds_end`) define the producer's reading frame. The first codon mapped contiguously and in frame onto the reference CDS anchors comparison of the observed sequence on either side, including sequence across a junction or indel. Only positions within the reference CDS and the observed ORF are compared, through the observed stop. A known nucleotide difference establishes a coding-sequence change; a known amino-acid difference establishes a protein change. Equal codons and ambiguous bases do not establish a protein change. Missing sequence before or after the observation is never compared. The same caution applies to `start_to_stop` when `sequence_status` is `observed_model_completeness_unknown`: an internal methionine followed by the unchanged reference-protein suffix can reflect missing 5′ coverage. Without the annotated initiator mapped to the observed ORF start, that suffix leaves change flags `None`, except for changes established by mapped observed codons. The prediction keeps its `start_to_stop` label, sequence and provenance, and is retained by default but excluded by `drop_silent_and_noncoding(keep_unresolved=False)`. This does not demote a different fusion N terminus or an observed premature stop merely because transcript completeness is unknown. A mapped annotated start can establish that a shorter prediction reflects an actual sequence change; initiation and translation still remain predictions. Mapping does not choose a new ORF or establish initiation at an internal or upstream ATG. Initiation-dependent extensions remain under [#466](https://github.com/openvax/varcode/issues/466). ## Make explicit reference-completion hypotheses An observed fragment may omit the annotated start or end of a transcript. Use `reference_completion_hypotheses` to ask what protein a compatible reference continuation would predict: ```python from varcode import reference_completion_hypotheses # `rna` is the result of load_exacto_fusions above. Keep these collections # separate: inferred continuations are not RNA-observed outcomes. observations = rna.candidates hypotheses = tuple( hypothesis for observation in observations for hypothesis in reference_completion_hypotheses(observation) ) for hypothesis in hypotheses: model = hypothesis.effect.mutant_transcript spans = hypothesis.evidence["reference_completion"] start, end = spans["observed_span"] observed_sequence = model.cdna_sequence[start:end] print(model.mutant_protein_sequence, spans["assumed_spans"]) ``` The helper adds a reference prefix and/or suffix only when that terminal observed base is mapped to its sense transcript. It preserves every observed base and internal junction, insertion or mismatch. An unmapped end remains uncompleted. No candidate is returned when no reference bases can be added. To request one end, use `ends=("five_prime",)` or `ends=("three_prime",)`. By default, each end uses the transcript already named by the observation. To consider alternative 5′ isoforms, pass `five_prime_transcripts=observation.effect.five_prime_transcript.gene.transcripts`; `three_prime_transcripts` works the same way for a sense partner. Only isoforms that agree with all mapped donor runs in genomic coordinates, sequence and splice path survive. The reference build, gene, contig and strand must agree. Different transcript pairs remain distinct even when their proteins match. An explicitly empty list of choices produces no hypotheses for an eligible end. Each result has source `varcode_reference_completion` and records its provenance: - `reference_completion` evidence holds the selected transcript IDs, the half-open `observed_span` in completed cDNA, and `assumed_spans` with their cDNA and reference coordinates. - Assumed segments are labeled `assumed_reference_five_prime` or `assumed_reference_three_prime`. - The original candidate and model evidence and peptide are retained in `observed_evidence`, `observed_model_evidence` and `observed_protein_sequence`; `observed_source` identifies the producer. - Candidate JSON serialization preserves this provenance. Read counts and abundance stay nested with the original observation. They provide no extra support for a completed isoform, and must not be summed across its alternative completions. RNA producers can set `rna_five_prime_complete=True` and/or `rna_three_prime_complete=True` in candidate or model evidence when they have actual transcript-end evidence. Completion never extends those ends. False or None leaves the end eligible. Exacto's ORF-completeness labels and read endpoints alone do not establish transcript ends; native rows retain their original scope. Translation starts only at the retained annotated 5′ initiator, recorded as `initiation_status="assumed_reference_start"`. There is no search for a new internal/upstream ATG. A mapped start can span observed and assumed segments. The original Exacto ORF is unchanged, even when its reading frame differs; `observed_orf_frame` reports its frame relative to the completed CDS when both starts are known. Missing/partial start annotation or ambiguous coding bases leave the protein None. A missing stop gives `protein_completeness="partial_end"`; adding a reference 3′ end does not guarantee an in-frame stop after a frameshift. Existing transcript-specific genetic codes and mapped selenocysteine/3′-UTR rules apply, including explicit uncertain Sec offsets in the hypothesis evidence. For example, the observed fusion fragment `CCC GGG AAA TTT` predicts `PGKF` in its supplied frame. A compatible reference prefix `ATG AAA` and suffix `CCC TAA` produce a separate `MKPGKFP` hypothesis. Those added bases remain assumptions. Its sequence-change flags describe that hypothesis; neither a start-to-stop prediction nor a changed-protein flag establishes observed initiation, translation, full-length RNA, tumor specificity or abundance. This distinction follows the producer's [observed-sequence ORF selection](https://github.com/pirl-unc/exacto/blob/307c08670d5e706734bddf393bcebc84db497f9f/exacto/exacto-translator/src/algorithms/translation.rs), the [experimental dependence of initiation on sequence context](https://pubmed.ncbi.nlm.nih.gov/2601709/), and the [3′-UTR requirement for selenocysteine decoding](https://www.nature.com/articles/353273a0). ## Exacto format details The adapter follows Exacto's `index` ordering and inclusive `read_start` / `read_end` base-row coordinates. Sequences already run 5' to 3' even for reverse-strand genes, so they are never reverse-complemented a second time. Event rows preserve splice structure but do not invent missing bases. Exacto uses inclusive genomic bounds for matching runs and flanking genomic positions for mismatches and insertions, on both strands. The importer excludes those flanks when validating the observed path. The imported model's `reference_segments` maps matching, sense-oriented exonic bases to the row's annotated transcript. Genomic position and reference cDNA sequence must agree; a `match` label alone is insufficient. Mismatches, insertions, intronic/intergenic bases and antisense sequence remain observed sequence segments. No reference sequence is substituted or appended, and the original versioned transcript IDs remain in the evidence. Not every RNA junction produces a coding fusion. In the osteosarcoma regression fixtures, GABBR1 joins sequence upstream of SLC29A1, OTUD7A joins an antisense FMN1 intron, and the KLF15-side reads are intronic. Varcode keeps these RNA junctions without inventing coding fusions or protein sequences. Format reference: [Exacto's structure translation source, pinned revision 307c086](https://github.com/pirl-unc/exacto/blob/307c08670d5e706734bddf393bcebc84db497f9f/exacto/exacto-translator/src/algorithms/translation.rs). Genomic bounds: [Exacto's native record writer at the same revision](https://github.com/pirl-unc/exacto/blob/307c08670d5e706734bddf393bcebc84db497f9f/exacto/exacto-caller/src/structs/alignment_structure.rs#L765). ## Related reference - [Transcript models](transcript_models.md): partial structures and sequence access. - [RNA and transcript API](api_rna.md): import parameters and evidence objects. - [Phasing](phasing.md): evidence linking variants on the same allele. openvax-varcode-d96eb06/docs/sample_identity.md000066400000000000000000000335401526602674500217220ustar00rootroot00000000000000# Check sample identities `varcode check-samples` screens a cohort of VCFs for possible sample mix-ups, such as a tumor labelled with the wrong patient or two swapped samples. For every pair of samples it reports two separate kinds of evidence: - **Donor:** do the samples' germline SNP genotypes look like they come from the same person? - **Tumor:** do two tumor samples share somatic mutations, including precise SV junctions? This is a screening heuristic with uncalibrated thresholds. It does not give a probability of identity, test kinship, estimate contamination, or decide which samples were swapped. "Compatible" means only that the available evidence did not distinguish the samples; relatives and identical twins can be compatible. Confirm a suspected mix-up with an informative SNP panel and a validated identity workflow. ## Run a check ```bash varcode check-samples \ --germline normal1.vcf.gz --germline normal2.vcf.gz \ --somatic tumor1.vcf.gz --somatic tumor2.vcf.gz \ --assembly GRCh38 --json checks.json --tsv pairs.tsv ``` Each file contributes all of its sample columns. Declare what each file contains, repeating a flag as often as needed: | Flag | Use for | |---|---| | `--germline` | Germline calls, such as a normal sample's VCF. | | `--somatic` | Caller-reported somatic calls. A single-sample somatic VCF is treated as a tumor. | | `--mixed` | A combined germline/somatic callset. | A sample's role is never guessed from its column name. Multi-sample somatic or mixed VCFs need `##tumor_sample` / `##normal_sample` header lines, or roles in a [manifest](#declare-expected-identities). Outputs: - A short summary on stderr. - A JSON report, written to stdout unless you pass `--json`. It includes every pair's statuses and counts, reasons for inconclusive results, QC counts, absolute input paths and SHA-256 hashes, sample roles, the Varcode version, and all thresholds. It does not export individual genotypes or mutation lists. - With `--tsv`, a table with one row per pair: statuses, counts, concordance, somatic overlap, and flags. ## Declare expected identities To check samples against what you expect, list them in a CSV or `.tsv` manifest. It selects exact sample columns and records which donor and tumor each should belong to. Paths are relative to the manifest, not the shell's working directory: ```csv path,sample,kind,role,label,donor_id,tumor_id,normal_sample,assembly normal.vcf.gz,N,germline,normal,normal-A,patient-A,,,GRCh38 paired.vcf.gz,N,mixed,normal,paired-normal-A,patient-A,,,GRCh38 paired.vcf.gz,T,mixed,tumor,tumor-A,patient-A,tumor-A,N,GRCh38 recurrence.vcf.gz,T,somatic,tumor,recurrence-A,patient-A,tumor-A,,GRCh38 ``` ```bash varcode check-samples --manifest samples.csv \ --json checks.json --tsv pairs.tsv --fail-on-mismatch ``` - Only `path` and `sample` are required. `kind` defaults to `germline`. - `label` names the sample in reports and must be unique. It defaults to `absolute-path::sample`. - The same file and sample can't be listed twice. - A tumor's `normal_sample` names a column in the same VCF, even if that normal isn't selected for output. A single declared normal is paired automatically; with several normals, name one explicitly. - Roles are never guessed from words such as `TUMOR` or `NORMAL`. Expected identities never change the scores. They only add [flags](#flags) where the results contradict your expectations. ## Reading the results Each pair gets a donor status and a tumor status. An inconclusive status comes with `reasons` in the JSON report. | Donor status | Meaning | |---|---| | `compatible` | Genotypes agree closely enough that the samples may come from the same person. Relatives and identical twins can also be compatible. | | `discordant` | Genotypes disagree enough to suggest different people. | | `inconclusive` | Too little usable shared evidence, a result between the compatible and discordant thresholds, or different genome builds. | | Tumor status | Meaning | |---|---| | `shared_somatic_support` | Both samples are tumors, and they share enough somatic alleles to suggest a common tumor. | | `low_overlap` | Both tumors have enough somatic alleles, but few are shared. On its own, this does not show a different tumor or donor. | | `inconclusive` | One sample isn't a tumor, a tumor has too few somatic alleles, or the genome builds differ. | Somatic overlap cannot prove that two samples came from the same tumor. Recurrent drivers, germline leakage, assay overlap, purity, copy number, and caller sensitivity can all raise or lower it. When you've declared expected identities, these flags mark results to review: | Flag | Meaning | | --- | --- | | `expected_donor_mismatch` | Discordant donor evidence despite equal donor or tumor IDs, or a declared tumor/normal pairing. | | `unexpected_donor_compatibility` | Compatible evidence despite different donor IDs; review potential swaps, relatedness and evidence limitations. | | `expected_tumor_low_overlap` | Equal tumor IDs but low somatic overlap; review coverage, purity, calling and tumor evolution. | | `paired_normal_has_different_donor_label` | The declared pairing and donor labels disagree. | | `unexpected_tumor_overlap` | Shared somatic support despite different tumor IDs; review shared origins, recurrent mutations or possible duplicate/mislabelled samples. | | `same_tumor_has_different_donor_labels` | Equal tumor IDs conflict with different donor IDs. | Exit codes: with `--fail-on-mismatch`, the command returns 1 only for `expected_donor_mismatch`. A completed analysis otherwise returns 0, even when results are inconclusive; invalid inputs return 2. Without declared expectations, the report gives pairwise evidence and never declares a swap. ## Python API ```python from varcode import ( SampleCheckConfig, SampleSpec, load_vcf_samples, compare_samples, check_sample_identity, ) config = SampleCheckConfig() normals = load_vcf_samples("normal.vcf.gz", assembly="GRCh38", config=config) paired = load_vcf_samples( "paired.vcf.gz", kind="mixed", assembly="GRCh38", config=config, samples=[ SampleSpec("N", role="normal", donor_id="patient-A"), SampleSpec("T", role="tumor", donor_id="patient-A", normal_sample="N"), ], ) pair = compare_samples(paired[0], paired[1]) report = check_sample_identity(normals + paired) ``` `load_vcf_samples` returns `SampleFingerprint` objects. `compare_samples` and `check_sample_identity` return JSON-compatible dictionaries. Use the same config when loading every sample. Requirements and limits: - No transcript annotation or reference sequence download is needed. - Local VCF and gzip-compressed VCF files are supported, not BCF or remote URLs. - Assemblies must be GRCh37/hg19/b37 or GRCh38/hg38/b38. A recognized `##reference` header is used unless you supply the assembly explicitly; contradictions are errors. - Samples on different builds give inconclusive comparisons. There is no liftover. ## How the evidence is judged The numbers below are defaults; each can be [changed](#configure-and-reproduce). ### Donor checks **Which genotypes count.** Only complete diploid genotypes at individually represented SNPs on autosomes (chromosomes 1–22; `chr1` and `1` are equivalent). Genotypes are compared as actual bases, so phase and ALT order don't matter. - The record FILTER and sample FT must be `PASS` or missing. - Depth must be at least 10 and GQ at least 20 where available. Complete AD can supply depth when DP is absent. Missing values are counted and accepted by default; set `require_quality=True` to require both. - Missing records, partial or haploid GTs, gVCF blocks, indels, and SVs don't count. A missing call is **never** assumed to be homozygous reference. - An individually called SNP stays usable when an uncalled `` or indel ALT is also listed; a GT that actually calls such an allele is excluded. - Loci with conflicting passing SNP records are excluded. Reference-base disagreements between samples are counted separately and excluded. - Malformed GT indexes, non-finite or negative quality values, and AD/AF with the wrong number of values stop the analysis with a file and line number. **Minimum evidence.** Without all of these, the status is `inconclusive`: - 100 shared callable SNPs; - 20 non-reference SNPs in each sample; - at least two of the three genotype categories (homozygous reference, heterozygous, homozygous ALT) in each sample; - SNPs spread over at least 20 distinct 100 kb genomic bins. These are screening safeguards, not a claim that the sites are independent or population-informative. **Decision.** IBS0 is the fraction of sites where the two genotypes share no allele. | Comparison | Score | Compatible | Discordant | |---|---|---|---| | Two non-tumor samples | Exact genotype concordance | score ≥ 0.98 and IBS0 ≤ 0.01 | score < 0.90 or IBS0 ≥ 0.05 | | Either sample is a tumor | Allele sharing (`1 - IBS0`) | score ≥ 0.98 and IBS0 ≤ 0.01 | score < 0.90 or IBS0 ≥ 0.05 | Anything in between is `inconclusive`. **Comparisons involving a tumor.** Loss of heterozygosity can turn a heterozygous genotype into a homozygous one, so exact concordance would penalize a true match. Allele sharing tolerates that change; both measures are reported. These comparisons also require 20 sites that are homozygous in both samples, and 20 explicit homozygous-reference calls in **each** sample at shared sites. Without reference calls, two variant-only VCFs automatically share an ALT allele at every overlapping site, which would make the test meaningless. Such data can still contribute somatic evidence. A normal column containing only `0/0` calls at somatic sites also stays inconclusive for identity. **Sampling.** Up to 50,000 SNP loci per sample are kept, chosen by a deterministic SHA-256 bottom-k sketch keyed only by assembly and position. This bounds memory and doesn't depend on genotypes or record order. Comparisons report counts for the loci actually retained, not whole-genome estimates; omitted and conflicting loci are listed in QC. ### Somatic checks **Which alleles count.** Only tumor samples contribute somatic alleles. - The allele must be present in a complete GT. Without a called GT, it needs at least 3 ALT reads in AD, VAF at least 0.03, and depth at least 10. GT takes precedence over contradictory AD. AF is kept for VAF comparison, but AF alone does not establish a call. - The allele must also be shown to be somatic: - With a declared matched normal, the allele must be absent from the normal's GT with adequate normal depth and, if AD is available, at most 1 ALT read and VAF at most 0.02. Missing or filtered normal evidence cannot establish absence. - Otherwise, the input must be declared `somatic` or the record must carry a VCF `SOMATIC` flag. Unlabelled non-reference alleles in a mixed tumor VCF are not assumed to be somatic. - QC distinguishes paired-normal contrasts from caller-reported evidence. **Matching alleles.** Small alleles are minimally trimmed, without repeat left-alignment. Precise, sequence-resolved SVs use the [SV comparison normalizer](sv_comparison.md), including reciprocal-BND deduplication. Imprecise, unknown-insertion, and unsupported SVs are excluded with counts, and nearby breakpoints are not treated as identical. As a result, equivalent repeats or caller representations may undercount overlap. Explicit sequence alleles and symbolic SVs use separate keys; comparing across those representations requires the dedicated SV comparison workflow. **Decision.** The report includes shared and total counts, the shared fraction in each direction, Jaccard similarity, and the overlap coefficient (`shared / min(count_a, count_b)`). The overlap coefficient tolerates branching evolution and callsets of different sizes. | Tumor status | Requirement | |---|---| | `shared_somatic_support` | At least 10 alleles in each tumor, at least 5 shared, and an overlap coefficient of at least 0.20 | | `low_overlap` | At least 10 alleles in each tumor, but fewer than 5 shared or an overlap coefficient below 0.20 | | `inconclusive` | Fewer than 10 alleles in either tumor | VAF Pearson correlation is descriptive only and never determines status. It requires at least 5 shared VAFs with nonzero variance. Repeated alleles count once, and conflicting duplicate VAFs are left out of the correlation. A sample with more than 100,000 somatic alleles raises an explicit error rather than being silently truncated. Coverage and callable regions are not modeled, and population allele frequencies are not used. ### Configure and reproduce Every threshold is a `SampleCheckConfig` field and appears in the report's `config`. The CLI accepts the same names in a JSON object, for example: ```json {"require_quality": true, "min_shared_snps": 200, "max_snps": 100000} ``` Pass it with `--config thresholds.json`. Changing thresholds changes the evidence requirements; the defaults have **not** been calibrated on an independent clinical cohort. Synthetic ground-truth VCF generators and regression cases are in `tests/test_sample_identity.py`. The distinction between genotype concordance and tumor-specific allele-fraction changes is informed by the primary [Somalier paper](https://pmc.ncbi.nlm.nih.gov/articles/PMC7362544/) and its [cancer concordance guidance](https://github.com/brentp/somalier/blob/master/cancer-concordance-contamination.md). [NGSCheckMate](https://pmc.ncbi.nlm.nih.gov/articles/PMC5499645/) illustrates a validated SNP-panel allele-fraction approach. This implementation does not reproduce their calibrated models, panels, or accuracy claims. Input semantics follow the [VCF specification](https://samtools.github.io/hts-specs/VCFv4.5.pdf). ## API reference ::: varcode.sample_identity options: members: - SampleCheckConfig - SampleSpec - SampleFingerprint - load_vcf_samples - compare_samples - check_sample_identity openvax-varcode-d96eb06/docs/splice_variants.md000066400000000000000000000152331526602674500217150ustar00rootroot00000000000000# Splice variants Variants near an exon–intron boundary can change how a transcript is spliced, for example by skipping an exon or keeping an intron. DNA alone doesn't show which will happen, so Varcode returns a `SpliceOutcomeSet` listing the possible outcomes, each with its own predicted protein where it can be determined. ## Example This CFTR variant affects an exonic splice position in Ensembl 81 (GRCh38). See [reference setup](getting_started.md#reference-data) if needed. ```python from varcode import Variant variant = Variant("7", 117_531_114, "G", "T", genome=81) transcript = variant.genome.transcript_by_id("ENST00000003084") splice_set = variant.effect_on_transcript(transcript) print(type(splice_set).__name__) # SpliceOutcomeSet print(splice_set.disrupted_signal_class.__name__) # ExonicSpliceSite ``` No opt-in flag is needed. `disrupted_signal_class` identifies the affected signal; `candidates` describes what might happen to the transcript. Since 10.17, top-priority selection and `--one-per-variant` retain the selected `SpliceOutcomeSet` with all of its candidates. Its `short_description` lists every current candidate, including unresolved predictions. Effect CSVs keep `effect_type=SpliceOutcomeSet` and report the affected signal separately in `splice_signal`. Candidate order remains the producer's order, not a measured probability. These summaries do not preserve the full evidence or sequences. Exonic sets rank at the higher priority of their signal and their coding consequence conditional on normal splicing. Code that previously received an unwrapped coding effect from `top_priority_effect()` should now explicitly read `effect_if_splicing_unchanged` when it needs that conditional consequence. ## Candidate outcomes ```python for candidate in splice_set.candidates: print(type(candidate.effect).__name__) print(candidate.effect.mutant_protein_sequence) print(candidate.source, candidate.evidence) ``` Candidates can include `NormalSplicing`, `ExonSkipping`, `IntronRetention`, `CrypticDonor`, and `CrypticAcceptor`. Their sequences and amino-acid changes are available when the required sequence can be resolved; otherwise the fields may be `None`. Intron retention and cryptic-site predictions can need genomic sequence beyond the annotated transcript. `ExonSkipping` is offered only for internal exons: skipping joins the donor before an exon to the acceptor after it, so a transcript's first and last exons can't be skipped. To inspect mechanism-specific fields: ```python from varcode import ExonSkipping, IntronRetention for candidate in splice_set.candidates: mechanism = candidate.effect if isinstance(mechanism, ExonSkipping): print(mechanism.affected_exon.exon_id, mechanism.in_frame) elif isinstance(mechanism, IntronRetention): print(mechanism.side, mechanism.retained_intron_start) ``` Each mechanism's `splice_signal` points to the underlying site-disruption effect. Only `NormalSplicing` is built eagerly; the other candidates are created and cached when `candidates` is first accessed. ## Coding effects when splicing is unchanged An exonic variant can affect a codon and a splice signal at the same time. Exons also contain untranslated sequence, so exonic does not always mean coding ([Ensembl glossary](https://www.ensembl.org/Help/Glossary?id=521)). ```python coding = splice_set.effect_if_splicing_unchanged if coding is not None: print(coding.short_description) ``` This returns the `NormalSplicing` candidate's coding effect when one applies. It is `None` for a purely intronic disruption with no directly changed coding base. `alternate_effect` is a compatibility alias. ## Ranking and protein sequences | Accessor | Returns | |---|---| | `most_likely_effect` | First effect in the producer's candidate order | | `highest_priority_effect` | Effect with the highest consequence priority | | `most_likely_candidate`, `highest_priority_candidate` | The same choices with their provenance wrappers | These answer different questions: the producer's preferred mechanism may not be the most disruptive. Neither ordering establishes that a mechanism occurred, and candidate order is not a calibrated probability. ```python preferred = splice_set.most_likely_effect most_disruptive = splice_set.highest_priority_effect proteins = splice_set.mutant_protein_sequences ``` `mutant_protein_sequences` contains distinct nonempty sequences. The separate `candidate_proteins` mapping is keyed by mechanism class and uses an empty string for unresolved protein sequence. To retain the evidence for each sequence, iterate the candidates instead of using either summary. ## Splice signals The default classifier uses a positional window around each exon–intron boundary, not a learned splice score: | Signal class | Position | |---|---| | `ExonicSpliceSite` | Last 3 exonic bases at a donor, or the first exonic base at an acceptor | | `SpliceDonor` | Canonical `GT` at intronic +1/+2 | | `SpliceAcceptor` | Canonical `AG` at intronic −2/−1 | | `IntronicSpliceSite` | Intronic +3…+6 at a donor or −3 at an acceptor; also the canonical positions when the supplied reference signal is not canonical | These classes identify the site inside the outcome set; they are not separate top-level predictions. Coding variants outside this window receive their coding effect without a splice annotation. ## RNA evidence `SpliceOutcomeSet.with_rna_evidence(...)` returns a new set whose current candidates are the RNA-observed mechanisms. The original predictions remain in `dna_candidates`; `rna_evidence`, `excluded_candidates`, `added_candidates`, and `candidate_rna_evidence` retain the comparison. Use `splice_set.rna_evidence_for(candidate)` to inspect support for one candidate. Candidate evidence can include named measurements such as `read_count`, `junction_id`, `psi`, or `motif_score`. Varcode preserves these as provenance; it does not normalize them into a probability. The ordinary `EffectCandidate` wrapper has no `probability` or `plausibility` field. See [germline, phase, and RNA inputs](phasing.md#combining-evidence) for collection-level use and the [RNA evidence API](api_rna.md#rna-evidence) for integration details. ## Limitations A variant outside the positional window can still affect splicing biologically. The default does not flag mid-exon enhancer/silencer disruption, branch-point changes, or deep-intronic cryptic-site activation. Broader sequence-based prediction is tracked in [#297](https://github.com/openvax/varcode/issues/297). RNA evidence can help distinguish predicted alternatives, but missing evidence does not establish normal splicing. For fields and methods, see the [SpliceOutcomeSet reference](effect_types.md#splice-outcome-container). openvax-varcode-d96eb06/docs/structural_variants.md000066400000000000000000000247521526602674500226540ustar00rootroot00000000000000# Structural variant annotation Structural variants (SVs) are deletions, duplications, inversions, insertions, copy-number changes, and breakends (translocations and other novel junctions). Load them with an explicit VCF option, then use the same `effects()` interface as for small variants. ## Basic usage ```python from varcode import load_vcf variants = load_vcf( "structural_variants.vcf", genome=81, # GRCh38; use the annotation chosen for your input parse_structural_variants=True, ) effects = variants.effects() for effect in effects: if effect.variant.is_structural: print(effect.variant.sv_type, effect.short_description) print(effect.mutant_protein_sequence) # may be None ``` Without `parse_structural_variants=True`, symbolic alleles and breakends are skipped with a warning. No separate annotator selection is needed. [Reference setup](getting_started.md#reference-data) explains the genome argument. ## Reading SV results Varcode reports what an SV does to each transcript, not just the type of DNA event. The event type is always available as `effect.variant.sv_type` (`DEL`, `DUP`, `INV`, `BND`, ...); the effect class describes the consequence: - **One clear consequence.** Local deletions and tandem duplications are modeled, translated, and classified like small variants: `StartLoss`, `Deletion`, `Insertion`, `FrameShift`, or another protein consequence. UTR-only edits keep `FivePrimeUTR` / `ThreePrimeUTR` labels when the protein is unchanged. - **Several possible consequences.** When a breakpoint cuts an exon, or splice/motif alternatives exist, a `StructuralVariantEffect` holds each consequence in `.candidates`, and its description and priority reflect them. See [alternative outcomes](effect_annotation.md#alternative-outcomes). - **Gene fusions.** `GeneFusion` and `TranslocationToIntergenic`; see [fusion protein candidates](#fusion-protein-candidates). - **Unknown.** Unmaterialized inversions, unspecified insertions/CNVs, and assemblies without a mapped CDS give `Unresolved` candidates. A deletion that contains a whole transcript, or cuts an incompletely annotated coding transcript, is `ExonLoss`. For example, on CFTR (`ENST00000003084`, Ensembl 81), deleting exons 1–3 loses the annotated start, deleting exon 5 yields an in-frame `Deletion` (1480 → 1450 aa), and deleting exons 5–6 yields a `FrameShift` (171 aa). Reading-frame classification uses the spliced edit, consistent with [Ensembl's consequence definitions](https://www.ensembl.org/info/genome/variation/prediction/predicted_data.html). Also note: - The annotated start codon is mapped through the retained transcript segments; a downstream ATG that survives a deletion does not rescue a deleted start. - Candidate evidence records `sv_type`, `splice_ambiguous`, and the `reference_splicing` assumption; DUP models also record `tandem_duplication`. `mutant_transcript` holds the modeled cDNA, translated protein, and evidence. - A DNA rearrangement does not by itself establish a complete expressed fusion protein. Sequence may be unknown or partial; `None` is not an unchanged protein. - Not every SV effect has `.candidates`: check `isinstance(effect, MultiOutcomeEffect)`. ## Filtering by protein change To drop SV effects known not to change the protein: ```python retained = effects.drop_silent_and_noncoding() # Require a positive prediction of protein change, excluding unknowns: resolved_changes = effects.drop_silent_and_noncoding(keep_unresolved=False) ``` SV effects report `modifies_protein_sequence` and `modifies_coding_sequence` as `True` (changed), `False` (unchanged), or `None` (unresolved). A changed CDS can still encode the same protein. Comparisons use the transcript being annotated; for a fusion, the 5′ partner supplies the initiation site. The flags cover the whole candidate set: any changed alternative makes the flag `True`; otherwise an unresolved alternative makes it `None`. The filter retains the original set, including provenance, rather than selecting one candidate. Unresolved effects are kept by default. To test for a *known* unchanged SV, use `effect.modifies_protein_sequence is False`, not `not effect.modifies_protein_sequence`. These flags describe existing predictions; they do not establish expression. Partial BND fragments and assemblies without a mapped ORF stay unresolved. So do partial observations (`protein_completeness` other than `start_to_stop`) unless a mapped, in-frame observed codon differs from the reference. The filter does not construct missing proteins or change the effect class. ??? note "Selenocysteine and non-ATG start codons" Selenocysteine (`U` in the Ensembl reference protein) is encoded by UGA, which is decoded as Sec only with a SECIS element in the mRNA's 3′ UTR. Ensembl doesn't annotate SECIS positions. A fusion's stored protein reads Sec unless no selenoprotein 3′ UTR remains ([transcript models](transcript_models.md#selenocysteine)). For the flags, UGA is read as Sec where a model keeps the transcript intact from that codon through its 3′ end, and as a stop where no selenoprotein 3′ UTR remains. Otherwise the protein flag is reported only if both readings agree, else `None`; for example, a partial 3′ UTR deletion leaves the protein unresolved. The coding flag compares CDS bases, which don't depend on how Sec is decoded. A start codon other than ATG counts as the initiator methionine, even though Ensembl writes CTG and TTG starts as `L`. ## Fusion protein candidates For an annotated effect, inspect every compatible partner isoform rather than only the protein on the first result: ```python from varcode.effects import GeneFusion for candidate in effect.candidates: fusion = candidate.effect if isinstance(fusion, GeneFusion): print(fusion.five_prime_transcript.id, fusion.three_prime_transcript.id) print(fusion.mutant_protein_sequence) # None when unresolved ``` There is no candidate-count cap. Distinct transcript pairs remain separate even when their predicted proteins match. The first candidate follows annotation order, not measured likelihood. These are reference-isoform predictions, not every possible splice/phase combination or evidence that a fusion is expressed. See [fusion rules](sv_reference.md#fusion-partners) and [RNA imports](rna_structures.md). ## Which transcripts get annotated `effects()` produces one effect per overlapping transcript, as for any variant. What "overlapping" means depends on the SV: | Variant | Transcripts annotated | |---|---| | Breakend record (`BND`) | Those at its own breakpoint. The mate's transcripts are annotated from the mate's record. | | `DEL`, `DUP`, `INV`, `INS`, `CNV` | Every transcript overlapping `start`..`end`. | Two outcomes apply before any SV logic: - No gene at the variant's position(s) → a single `Intergenic` effect. - A non-coding transcript → `NoncodingTranscript`. ## Pairing breakend records `pair_breakends` joins the two records of a breakend pair: - If both carry the same `SVTYPE` of `DEL`, `DUP` or `INV` and their kept sides fit it, the result is that typed event, annotated over its whole span. See [pairing rules and examples](transforms.md#pair_breakends). - Otherwise the result is one `BND` anchored at the record whose VCF ID sorts first, and only that end's transcripts are annotated. If that end is intergenic the combined variant reports just `Intergenic`; the other end is still reachable through `combined.source_variants`. ## Effect classes | Class | Meaning | |---|---| | `GeneFusion` | The SV joins this transcript sense-to-sense with a coding transcript in another gene. | | `TranslocationToIntergenic` | A breakend in this transcript that doesn't form a gene fusion. | | `StartLoss` | The annotated initiation codon is not retained. | | `Deletion` / `Insertion` | An in-frame protein deletion / insertion. | | `FrameShift` | A spliced coding edit changes the reading frame. | | `StructuralVariantEffect` | A set of conditional consequences or unresolved models. | | `Unresolved` | Available sequence/coordinates cannot establish the consequence. | | `ExonLoss` | Deleted exons: every exon of a transcript wholly inside a deletion, or those of a transcript whose CDS annotation is incomplete. | | `Intronic` | A span inside the transcript that overlaps no exon. | | `Intergenic` | No gene at the variant's position. | | `NoncodingTranscript` | The transcript isn't protein-coding. | Local DEL/DUP and fusion models retain known inserted junction bases when both retained breakend anchors are exonic, in transcript orientation. Local consequences use the net spliced edit, including those bases, to distinguish in-frame changes from frameshifts. Insertions at two intronic anchors are excluded under the reference-splicing assumption. The model records `junction_inserted_sequence` and `junction_insertion_status` in its evidence. ## Limitations - Partner isoforms are enumerated but not ranked by RNA support or likelihood. - Inserted junction bases with mixed exonic/intronic retention, conflicting or unreadable reciprocal alleles, or unknown local retention remain unresolved. Ambiguous coding bases are preserved in cDNA without asserting a protein. - Chains of several SVs aren't assembled into one allele, and regulatory effects (promoter or enhancer hijacking) aren't modeled. - Annotating multi-megabase spans can be slow ([#407](https://github.com/openvax/varcode/issues/407)). ## Migrating to 10.0 `LargeDeletion`, `LargeDuplication`, and `Inversion` remain importable for legacy objects but are no longer emitted. Read the DNA event from `effect.variant.sv_type`; use the consequence or its candidates for protein impact. ## Related guides - [SV coordinates and exports](sv_reference.md#alleles-coordinates-and-exports). - [Junction orientation and fusion partners](sv_reference.md#junction-orientation). - [Breakend, span, and protein outcomes](sv_reference.md#breakend-outcomes). - [Import observed RNA structures](rna_structures.md). - [Import Exacto protein predictions](rna_structures.md#import-exactos-protein-predictions). For exhaustive callset reconciliation, see [Compare SV calls](sv_comparison.md). openvax-varcode-d96eb06/docs/sv_comparison.md000066400000000000000000000075641526602674500214210ustar00rootroot00000000000000# Compare SV calls across samples and callers Different SV callers, and different samples, often report the same event in different forms: a pair of reciprocal breakends or a symbolic ``, explicit or symbolic alleles, slightly different coordinates. `varcode.sv_comparison` groups calls that report the same junctions, or nearby ones, so you can see where callers agree and disagree. It compares what was reported; it does not validate the calls. ```sh python -m varcode.sv_comparison --calls calls.csv --output comparison --max-distance 100 ``` `calls.csv` has one row per source ALT allele, with unique `call_id` and columns `caller,sample,build,chrom,pos,ref,alt`. Coordinates are one-based VCF coordinates. Use `GRCh37` or `GRCh38` for `build`; a trailing parenthesized note, such as `GRCh38 (from caller header)`, is accepted and preserved. Optional `info` is the original VCF INFO text; `end` supplies END when absent from INFO. Include `record_id`, `mate_id`, `source_url`, `source_id`, filter status and `duplicate_export_of` to retain source provenance. All additional columns survive unchanged in each member's `raw` mapping. No PASS filter or record deduplication is applied. Outputs, written to the `--output` directory: - `groups.csv`: comparison group, exact representation IDs, samples, callers, original call IDs, per-breakpoint range/spread, insertion disagreement and sequence/imprecision status. - `members.csv`: every input row, normalized oriented breakpoints and insertion sequence, exact ID, comparison group, or explicit unresolved reason. - `comparison.json`: the same exhaustive data plus input SHA256, package version and distance threshold. The output directory must be new. The Python API is `varcode.sv_comparison.compare_sv_calls(rows, max_distance=100)`. It needs no reference sequence downloads. Synthetic unit fixtures are constructed inline in `tests/test_sv_comparison.py`, including a complete CSV/CLI example; there is no machine-specific fixture-generation prerequisite. ## What a group means `exact_reported_allele` means identical reported oriented adjacencies and inserted bases, with no reported positional uncertainty. Reciprocal BNDs and equivalent explicit/symbolic deletions share that representation. Multi-junction events must match all junctions: one BND cannot establish a complete inversion. This is representation matching, not biological validation of a caller. `same_reported_adjacency` retains uncertainty about inserted bases or coordinates. `nearby_candidate` contains distinct exact IDs. Every corresponding breakpoint of every pair must differ by at most the stated distance. Greedy complete-link clustering, ordered by coordinates and stable identifiers, prevents an A–B–C chain from grouping A with a distant C. There may be multiple valid partitions; the deterministic partition is not a biological event-resolution claim. At equal coordinates, different inserted sequences still have different exact IDs, and `insertion_disagreement` is true. Original positions, confidence intervals and alleles remain available for review. Different assemblies, contigs, orientations or numbers of adjacencies never share a group. Reference liftover, repeat-aware left-alignment and inferred complex-event decomposition are not performed. Single breakends and unsupported or malformed alleles remain `unresolved` singleton rows rather than disappearing. They do not establish a complete adjacency even if their local positions agree. Counts are source rows. Reciprocal records, repeat exports, caller dependencies, and multiple samples do not establish independent molecular support. Use the preserved source metadata when interpreting support. Assemblies come from the input declarations; reference alleles are not checked against a genome FASTA. The input contract and coordinate normalization follow the [VCF specification](https://samtools.github.io/hts-specs/VCFv4.5.pdf). openvax-varcode-d96eb06/docs/sv_reference.md000066400000000000000000000370211526602674500211740ustar00rootroot00000000000000# SV coordinates and fusion rules For loading and result access, start with [Structural variants](structural_variants.md). This reference describes how the current implementation handles junctions and transcript boundaries. The biological examples use Ensembl 95. ## Alleles, coordinates, and exports Structural `ref`/`alt` and `original_ref`/`original_alt` expose the actual record alleles, including symbolic or breakend ALT. Small-edit flags (`is_snv`, `is_indel`, `is_insertion`, `is_deletion`, `is_transition`, `is_transversion`) are false; use `is_structural` and `sv_type` for event classification. SV-containing tables add `sv_type`, `end`, `mate_contig`, `mate_start`, `affected_start`, and `affected_end`; point-only and empty table columns are unchanged. These CSVs are summaries, not lossless structural archives: `from_csv` rejects them. Retain original VCF/JSON variants and their RNA evidence. Symbolic VCF span records retain `POS` as the padding/junction coordinate; `affected_start..affected_end` is the inclusive changed interval `POS+1..END`. This applies to DEL, DUP, INV and CNV (including CN0/CN3). This parser requires a nonempty span with `END > POS`. Direct `StructuralVariant(...)` construction keeps its existing explicit-coordinate defaults: pass `affected_start` separately when `start` includes padding. Paired breakends already supply it. A directly constructed BND takes `mate_contig`, `mate_start` and `mate_orientation` from a breakend ALT when they aren't passed, and raises `ValueError` when a passed one contradicts it. ## Junction orientation An SV creates one or more *junctions*, each joining two breakpoints. At each breakpoint the junction keeps either the bases to the left (up to and including the position) or those to the right. `StructuralVariant.junctions` lists them. For a breakend record, the ALT says which sides are kept: | ALT | This record keeps | Mate keeps | |---|---|---| | `t[p[` | left | right | | `t]p]` | left | left | | `]p]t` | right | left | | `[p[t` | right | right | For the other SV types the junctions follow from the type (`start` is the base before the event, as in VCF): | Type | Junctions | |---|---| | `DEL` | `start` (keeps left) joined to `end + 1` (keeps right) | | `DUP` | `end` (keeps left) joined to `start + 1` (keeps right) | | `INV` (symbolic) | two: `start`–`end` keeping left, and `start + 1`–`end + 1` keeping right | | `INV` built by `pair_breakends` | only the junction its breakend pair observed | | `INS`, `CNV`, single breakend | none | A breakend with no readable ALT has unknown sides, except that `mate_orientation` `"[["` / `"]]"` still gives the mate's side (right / left). ## Fusion partners Keeping a side of a transcript keeps either its 5' end or its 3' end, depending on its strand: | Strand | Keeps left | Keeps right | |---|---|---| | forward (`+`) | its 5' end → **5' partner** | its 3' end → **3' partner** | | reverse (`−`) | its 3' end → **3' partner** | its 5' end → **5' partner** | A junction makes a `GeneFusion` for a transcript when all of these hold: 1. The transcript contains one end of the junction. 2. Its gene doesn't also contain the other end (that would be intragenic). 3. At the other end there's a protein-coding transcript in a different gene, whose gene doesn't contain this end either. 4. The two roles are opposite: one 5' partner, one 3' partner. Two 5' ends (head to head) or two 3' ends (tail to tail) can't be read through, so they don't fuse. Every partner transcript satisfying these rules is retained as a `GeneFusion` candidate, across all compatible junctions. Junctions retaining the annotated transcript's 5' end come first, followed by pyensembl's partner order. The first candidate stays the primary result for compatibility; this is not a likelihood ranking. There is no count cap, and distinct isoforms remain separate even if they predict the same protein. Repeated copies of the same junction/partner record are deduplicated. Each candidate has its own transcript pair and reference-derived cDNA/protein, when sequence is available. A missing sequence leaves that candidate unresolved. This enumerates the current annotated-isoform model, not novel splicing, phased multi-SV paths, or all possible mature RNAs. A supplied `alt_assembly` still takes precedence over reference-derived sequence. See [candidate access](structural_variants.md#fusion-protein-candidates). `GeneFusion.transcript` is the transcript being annotated, which can be either partner. `five_prime_transcript` and `three_prime_transcript` say which is which, and `partner_transcript` is the other one. ### Partners starting just past the other end When the annotated transcript is the 5' partner and no transcript at the other end needs to span it, a protein-coding transcript can still be joined whole: one that starts on the other end's kept side, at most `MAX_UPSTREAM_PARTNER_DISTANCE` (10 kb) past it, on the strand the junction continues along, and that has a second exon. Its first exon has no splice acceptor, so the fused cDNA continues from its second exon. This follows LINX's rule for 3' breakends upstream of a gene, with LINX's limit for fusions other than known pairs. These fusions are further candidates after the effect's primary classification, never the primary one, because RNA need not splice that way. The model's evidence records `three_prime_breakend="upstream"` and `three_prime_upstream_distance`. A DEL, DUP or INV gains the same candidates behind its local consequence. Its `most_likely_effect` is unchanged, but, like any structural effect set, its priority follows its most disruptive candidate. For example, in an osteosarcoma sample used for testing, the GABBR1--SLC29A1 junction lands 597 bases before SLC29A1's first transcript starts. Isovar's long reads there run from GABBR1 intronic sequence into the intergenic stretch and end in poly(A) before SLC29A1. So a breakend there stays `TranslocationToIntergenic`, with one fusion candidate per SLC29A1 isoform. ## Breakend outcomes Effects on the transcripts at the record's own breakpoint: | This breakpoint | Mate | Effect | |---|---|---| | Intergenic | anything | `Intergenic` | | In a coding gene | intergenic | `TranslocationToIntergenic`, plus fusion candidates for [partners starting just past the mate](#partners-starting-just-past-the-other-end) | | In a coding gene | in a non-coding gene only (e.g. MALAT1) | `TranslocationToIntergenic` | | In a coding gene | in a coding gene, opposite roles | `GeneFusion` | | In a coding gene | in a coding gene, head to head or tail to tail | `TranslocationToIntergenic` | | In a coding gene | in the same gene | the local DEL, DUP or INV its kept sides describe (see **Same gene** below) | | In a coding gene | single breakend (no mate) | `TranslocationToIntergenic` | | In a coding gene | ALT isn't a breakend | `GeneFusion` treating this transcript as the 5' partner, with a warning | `TranslocationToIntergenic` therefore means "a breakend that doesn't form a gene fusion", not only "the mate is intergenic". Its reference-derived `mutant_transcript` contains only the retained local 5' prefix or 3' suffix, including the base at an exonic breakpoint. Intronic breakpoints retain the corresponding annotated exons. `A.` keeps genomic left; `.A` keeps genomic right, even without a mate. Transcript cDNA is already strand-oriented, so the segment itself has `strand="+"` on either gene strand. The fragment is labeled `evidence["sequence_status"] = "retained_reference_fragment"`; full `cdna_sequence` and `mutant_protein_sequence` remain None. Concatenating this one fragment does not establish the full allele or prove a mature RNA/protein. Unknown local orientation (including mate-orientation metadata alone) leaves `mutant_transcript` None. A supplied `alt_assembly` still takes precedence. **Intergenic ↔ gene.** The record at the intergenic end reports `Intergenic`; the gene is annotated only from its own record, which reports `TranslocationToIntergenic`. Fusions with a transcript starting just past the intergenic end are its only further candidates; varcode doesn't model an intergenic promoter or enhancer driving a gene. **Same gene.** A junction with both ends in one gene is intragenic, so it isn't a fusion for that gene's transcripts. A transcript read across it carries the DEL, DUP or INV its kept sides describe, so varcode annotates each of those transcripts as that local event: - keeping the left side of the lower position and the right side of the higher one is a DEL between them; - keeping the right side of the lower and the left side of the higher is a tandem DUP; - keeping the same side at both is an INV. This is also what a labeled breakend pair gives once `pair_breakends` types it by the caller's `SVTYPE`. A single record annotates exactly as its labeled pair would, but its effects still report the record. Their evidence gives `sv_type="BND"` and `junction_event`, the local event it was read as. For example, a test sample's ATP5MG--KMT2A junction, chr11:118,401,717 to 118,468,775, is exactly intron 1 of Ensembl's ATP5MG-KMT2A readthrough transcript AP001267.5-202. A breakend there reports that transcript as `Intronic`, and the ATP5MG isoforms, whose gene doesn't reach KMT2A, as fusions. **Direction.** Both records of a junction report the same fusion direction, each on its own gene. For CFTR (chr7:117,485,000, `+`, keeping left) joined to BRCA1 (chr17:43,120,000, `−`, keeping left): | Record | Annotated on | Result | |---|---|---| | `N]17:43120000]` at chr7:117,485,000 | CFTR | `GeneFusion`, 5' = CFTR, 3' = BRCA1 | | `N]7:117485000]` at chr17:43,120,000 | BRCA1 | `GeneFusion`, 5' = CFTR, 3' = BRCA1 | | `N[17:43120000[` at chr7:117,485,000 | CFTR | `TranslocationToIntergenic` (head to head) | | `]17:43120000]N` at chr7:117,485,000 | CFTR | `TranslocationToIntergenic` (tail to tail) | The two records' primary proteins can differ (712 aa vs 1,854 aa here), because their first transcript pairs differ. Both records now retain other compatible isoforms in `.candidates`; matching the same 5'/3' transcript pair gives the same predicted protein from either end. ## Deletions, duplications and inversions Whether a span fuses the genes at its two ends depends on their strands: | Type | Genes on the same strand | Genes on opposite strands | |---|---|---| | `DEL` | fusion: the gene upstream in transcription is 5' | no fusion | | `DUP` | fusion: the gene downstream in transcription is 5' | no fusion | | `INV` (symbolic) | no fusion | reciprocal fusion candidates; each gene's own 5' contribution comes first | Examples from chr7, with CFTR (`+`), LSM8 (`+`, downstream) and CTTNBP2 (`−`, downstream): | Event | On CFTR | On the other gene | |---|---|---| | `DEL` CFTR..LSM8 | `GeneFusion` CFTR → LSM8 | `GeneFusion` CFTR → LSM8 | | `DUP` CFTR..LSM8 | `GeneFusion` LSM8 → CFTR | `GeneFusion` LSM8 → CFTR | | `INV` CFTR..LSM8 | `Unresolved` candidate | `Unresolved` candidate | | `DEL` CFTR..CTTNBP2 | deletion consequence | deletion consequence | | `DUP` CFTR..CTTNBP2 | `Unresolved` candidate | `Unresolved` candidate | | `INV` CFTR..CTTNBP2 | `GeneFusion` CFTR → CTTNBP2 | `GeneFusion` CTTNBP2 → CFTR | On the reverse strand the same rules give TMPRSS2 → ERG for the chr21 deletion behind that prostate-cancer fusion. Per transcript: | Transcript | Effect | |---|---| | Wholly inside the span | `ExonLoss` of every exon for a deletion; unresolved DUP/INV model | | Holds both ends | translated local consequence, unresolved model, or `Intronic` if no exon overlaps | | Holds one end, fusion conditions met | `GeneFusion`, with the span's effect as a further candidate | | Holds one end, no partner | the span's consequence (e.g. `FrameShift` or `StartLoss`) | When a boundary transcript gets a `GeneFusion`, what the span does to its exons isn't lost: ```python from varcode.effects import StructuralVariantEffect fusion = deletion.effect_on_transcript(tmprss2) (span_effect,) = [c.effect for c in fusion.candidates if type(c.effect) is StructuralVariantEffect] span_effect.affected_exons ``` `INS` and `CNV` never fuse. When they overlap exons, unspecified sequence or copy-number structure yields an `Unresolved` candidate; it does not establish a tandem duplication. Local DUP/INV transcript models require every junction end to lie in the selected transcript. An event extending outside it (including a span enclosing the entire transcript) does not establish duplicated or inverted cDNA within that transcript. A longer isoform of the same gene does not establish the shorter isoform's structure. Without a fusion partner or supplied `alt_assembly`, such an event's result contains an `Unresolved` candidate and `mutant_transcript` is None. This means unresolved sequence, not an unchanged protein or a proven truncation. The same restriction applies to a span candidate attached to a fusion; it does not remove the fusion's own transcript model. ## Breakpoint position and protein sequence The fused cDNA is the 5' partner's cDNA up to its breakpoint followed by the 3' partner's from its breakpoint on. For an exonic join, bases inserted in the VCF breakend ALT are retained between the two transcript segments, in transcriptional orientation. Reciprocal records contribute the insertion once, including after `pair_breakends`. `variant.junction_inserted_sequence(junction)` returns the sequence while traversing the ordered junction, `""` for no insertion, or `None` when the records are unreadable or disagree. This follows [VCF 4.5 sections 5.4–5.4.1](https://samtools.github.io/hts-specs/VCFv4.5.pdf). At two intronic breakpoints, the model excludes the insert under reference splicing and records `junction_insertion_status="excluded_by_reference_splicing"` in its evidence. With one exonic and one intronic breakpoint, insertion retention requires RNA evidence: both cDNA and protein are `None`, with `sequence_status="unresolved_insertion_retention"`. Unknown/conflicting insert sequence similarly gives `sequence_status="unresolved_junction_insertion"`. A supplied `alt_assembly` still takes precedence. | Breakpoint | What's kept | |---|---| | In an intron | The 5' side ends with the last complete exon before it; the 3' side starts with the first exon after it. For CFTR intron 1 that's exon 1's 185 bases. | | In an exon | The base at the breakpoint stays on the side that keeps it, so the cut is mid-exon: 51 bases for a breakpoint 50 bases into CFTR exon 1. | The protein is translated from the 5' partner's start codon, through the junction, to the first stop. That gives these cases, all still reported as `GeneFusion`: | 5' partner breakpoint | `mutant_transcript.mutant_protein_sequence` | |---|---| | Before its start codon (5' UTR or an early intron) | `None`. BRCA1's start codon is in exon 2, so a break in intron 1 loses it. | | Within its coding sequence, in frame with the 3' partner | 5' partner's N-terminus plus the 3' partner's C-terminus (CPEB2::FAM193A, 1,500 aa) | | Within its coding sequence, out of frame | 5' partner's N-terminus, then the 3' partner read in a shifted frame to the first stop (OTX1::KIF3C, 35 aa) | | After its stop codon (3' UTR) | the 5' partner's unchanged protein (CFTR, 1,480 aa) | There is no separate in-frame flag; compare the protein with the partners' reference proteins. A 5' partner transcript without a complete coding sequence also gives `None`. Breakpoints near exon boundaries add candidates to any SV effect: - **Splice outcomes**, when the SV's `start` or `end` is within 6 bp of an exon edge (`source="varcode_splice"`). - **Cryptic exons**, scanned in ±500 bp windows around every breakpoint (`source="varcode_motif"`), from a chromosome FASTA when one is attached. openvax-varcode-d96eb06/docs/transcript_models.md000066400000000000000000000066561526602674500222740ustar00rootroot00000000000000# Transcript models An effect may carry a `MutantTranscript` describing edits, retained reference segments, or an observed RNA structure. It can contain cDNA and protein sequence, but it may also be partial or have no resolved sequence. You usually want the effect's `mutant_protein_sequence`; a transcript model is useful when you also need the RNA structure or its provenance. Not every effect has a model, even when a predicted protein is available. ## Sequence access Given an annotated `effect`: ```python protein = effect.mutant_protein_sequence model = effect.mutant_transcript cdna = model.cdna_sequence if model is not None else None evidence = model.evidence if model is not None else None ``` `None` means unavailable. It does not establish an unchanged protein, an absent transcript, or a harmless variant. ## Selenocysteine Ensembl marks selenocysteine (Sec) as `U` in the reference protein, encoded by an in-frame UGA. UGA is decoded as Sec only with a SECIS element in the same mRNA's 3′ UTR, whose position isn't annotated. Predicted proteins read an annotated Sec codon as `U` when the model still maps it (no edit touches it) and keeps some of a selenoprotein's 3′ UTR. Where no selenoprotein 3′ UTR remains, for example a fusion downstream of Sec, UGA is read as a stop. A sequence without reference coordinates, such as an imported RNA assembly, is translated literally. A partly kept 3′ UTR still gets the Sec reading; the [SV change flags](structural_variants.md#filtering-by-protein-change) report such cases as unresolved. ## Partial structures A model with reference segments is not necessarily a complete mutant RNA. For example, a breakend can retain only a transcript's local 5′ prefix or 3′ suffix. That model has `evidence["sequence_status"] = "retained_reference_fragment"`, while full cDNA and protein remain unknown. Concatenating its segments does not reconstruct the missing partner or establish the full allele. Likewise, a sequence supplied by an RNA assembler may be only a junction fragment. Keep its completeness and source information with the sequence. The [RNA import guide](rna_structures.md) explains how observed structures and sequence-predicted proteins are represented; neither alone proves translation. ## Related objects Effects produced by `annotator="transcript_model"` retain their typed candidates, hypotheses, baseline and mutant products, evidence and ranking through `EffectCollection.to_json()` / `from_json()` and individual effect serialization. The versioned graph preserves candidate links to the owning effect and retains unknown probabilities as `None`. Older effect JSON remains readable. Transcript references still use the existing PyEnsembl serialization contract; the archive is not a replacement for reference annotation data. Extra-ORF effect JSON remains a separate unsupported operation; use its native `orf_annotation.to_dict()` export. | Object | Contents | |---|---| | `MutationEffect` | A predicted consequence or an unresolved result | | `MutantTranscript` | Edits or transcript structure, provenance, and optional sequences | | `MultiOutcomeEffect` | Several candidate effects, each with its own consequence and evidence | The [API reference](api_rna.md#mutant-transcripts) lists model fields and construction helpers. The experimental transcript-model annotator uses additional baseline and mutant product wrappers; see [its result format](experimental_annotators.md#transcript-model-results). openvax-varcode-d96eb06/docs/transforms.md000066400000000000000000000177021526602674500207300ustar00rootroot00000000000000# VariantCollection transforms Transforms return a new variant collection without modifying the input. Use them when you need to pair SV records or normalize indel coordinates; ordinary `effects()` calls do not require a preprocessing pipeline. ## Common tasks ```python from varcode import load_vcf from varcode.transforms import left_align_indels, pair_breakends variants = load_vcf("tumor.vcf", genome=81, parse_structural_variants=True) paired = pair_breakends(variants) normalized = left_align_indels(variants) ``` Choose the operation you need; these are independent examples. Pairing reduces the number of records and can change [which partner's transcripts are annotated](#which-partner-gets-annotated). Left-alignment depends on available reference sequence. ## `pair_breakends` Merges MATEID-paired BND rows into a single combined `StructuralVariant`, reducing the number of records. A VCF can represent the same translocation event two ways: | Caller pattern | Input rows | After `pair_breakends` | |---|---|---| | Manta / DELLY / SVABA / newer GRIDSS | Two BND rows linked by `MATEID` | One combined row, `source_variants=(a, b)` | | Older GRIDSS | Two BND rows linked by `PARID` | One combined row (PARID treated as MATEID alias) | | BreakDancer / CREST / older DELLY | One row, `SVTYPE=TRA`, `CHR2`/`END` in INFO | Pass-through, `source_variants=()` | | GRIDSS unresolved single-end | One BND row, no `MATEID` | Pass-through | Recognized reciprocal pairs become one variant per junction. Unpaired or unsupported records pass through; a complex rearrangement may still require several variants. ### Usage ```python from varcode import load_vcf from varcode.transforms import pair_breakends vc = load_vcf("manta.vcf", genome="GRCh38", parse_structural_variants=True) vc = pair_breakends(vc) # Each combined variant carries both endpoints + provenance. for v in vc: if v.is_structural and v.sv_type == "BND" and v.source_variants: bnd_a, bnd_b = v.source_variants print(f"{v.contig}:{v.start} <-> {v.mate_contig}:{v.mate_start} " f"from {bnd_a.short_description} + {bnd_b.short_description}") effects = vc.effects() ``` ### Pairing rules 1. **Primary key**: `MATEID` on the variant's `info` dict matched against VCF row IDs captured at load time. 2. **Alias**: `PARID` (older GRIDSS) is treated as `MATEID`. 3. **Symmetric**: A.mateid must equal B.id **and** B.mateid must equal A.id. Asymmetric references log a warning and pass through. 4. **In-degree 1**: each ID must be referenced by exactly one other row's MATEID. If any ID has incoming degree > 1, the whole connected component is left unpaired with a warning. 5. **Mate present**: if a MATEID points to an ID not in this collection (filtered out, chunked load), the half passes through with a warning. ### Metadata merge The combined variant's per-source metadata entry is built fresh: | Field | Rule | |---|---| | `id` | A's ID (lex-earlier of the two). | | `qual` | `min(A.qual, B.qual)` when both present. | | `filter` | Union of FILTER tokens; `PASS` drops out if any non-PASS label appears. | | `info` | A's values; `MATEID`/`PARID` removed (no longer meaningful); `paired_with` added pointing at B's ID. | | `sample_info` | Per-sample: GT must match across A and B (raises on disagreement); other FORMAT fields taken from A. | | `alt_assembly` | One populated -> use it. Both equal -> use shared. Both differ -> A's wins with warning. | The strict-GT rule is intentional: both halves of a legitimate paired BND describe the same biological event, so disagreement indicates either a caller bug (asymmetric filtering, separate re-genotyping per half) or a real analytical concern. The transform raises with both row IDs and both GT values so the problem surfaces. ### Which partner gets annotated A breakend pair describes a single novel junction. Pre-pair, each half is annotated against the transcripts at its own position, so a fusion shows up on both partner genes, and `GeneFusion.five_prime_transcript` / `three_prime_transcript` say which partner is which. Post-pair, a combined BND is anchored at the lex-earlier endpoint, so its effects cover only that endpoint's transcripts. (Pairs typed as DEL / DUP / INV span both ends, so their effects already cover both partners.) The other partner is reachable: `combined.source_variants` returns both originals, and you can annotate the other half directly: ```python combined = next(v for v in vc if v.source_variants) other_partner = combined.source_variants[1].effects() ``` If you want both partners in the same effect collection, don't run `pair_breakends`; the parser already emits both halves. A reciprocal translocation's second derivative chromosome (`der(19)t(15;19)` as well as `der(15)t(15;19)` for BRD4-NUTM1) is a separate breakend pair in the VCF. ## `left_align_indels` Shifts indels to their canonical leftmost equivalent position. The number of records stays the same; their coordinates may change. Two pipelines that exchange variants need a canonical representation per indel. `CTT→T` at position 10 inside a `CT`-repeat means the same biological event as `CT→_` at position 8, but tools that compare variants by `(contig, start, ref, alt)` see those representations as distinct. Left-alignment normalizes to the leftmost equivalent position. ### Usage ```python from varcode import load_vcf, Genome from varcode.transforms import left_align_indels # Without installed reference DNA — exonic indels normalize, # intronic/intergenic pass through unchanged. vc = load_vcf("tumor.vcf", genome="GRCh38") vc = left_align_indels(vc) # Full coverage — every indel normalizes. g = Genome(81, fasta="/path/to/GRCh38.fa") vc = load_vcf("tumor.vcf", genome=g) vc = left_align_indels(vc) ``` There is no `reference` parameter: `left_align_indels` reads bases via the genome the variants already carry (see [varcode.Genome](api_variants.md#varcode.Genome)). Coverage depends on which genome shape was passed. Native PyEnsembl reference DNA is also inherited when wrapping a configured genome; see [reference genomes](api_variants.md#reference-genomes) for setup. ### Behavior | Variant kind | Result | |---|---| | Pure SNV / MNV / complex (`ATG→GCC`) | Pass-through, `source_variants=()` | | Already-canonical indel (no equivalent leftward position) | Pass-through, `source_variants=()` | | Indel inside an exon, no FASTA | Shifts via transcript cDNA | | Indel in homopolymer / STR repeat | Shifts to leftmost equivalent position | | Indel in intron / intergenic, no FASTA | Pass-through (no reference coverage) | | Indel in intron / intergenic, FASTA attached | Shifts via FASTA | | Indel that starts exonic but would shift past the exon boundary, no FASTA | **Partial shift** — moves left until the boundary, sets `info["left_align_partial"] = True` | | Indel at chromosome start | Shift bounded by `start > 1` | ### Metadata Shifted variants carry: - `source_variants = (original,)` — the pre-shift variant. - Source-path metadata re-keyed under the shifted variant; the original key is removed. - `info["original_start"]` — the pre-shift start position, for round-trip diagnostics. - `info["left_align_partial"] = True` — set only when the shift was bounded by reference coverage (not by the reference disagreeing). Tells downstream tools that a chromosome FASTA might have shifted the variant further. ### Idempotence Running `left_align_indels` twice produces the same result on the second call — every variant is already at its canonical leftmost position after the first call. Composes cleanly with `pair_breakends` in either order; SVs and BNDs are not indels and pass through. ## Extending transforms For implementation rules and planned transforms, see [Contributing](https://github.com/openvax/varcode/blob/main/CONTRIBUTING.md#writing-transforms). See the [transform API](api_variants.md#variantcollection-transforms) for parameters. openvax-varcode-d96eb06/docs/uorf_annotations.md000066400000000000000000000306311526602674500221160ustar00rootroot00000000000000# Reference ORF consequences Install `varcode[orf]` for the optional [orfdata](https://github.com/openvax/orfdata) reference layer. PyEnsembl supplies reference transcripts, orfdata supplies ORF models and evidence, and Varcode compares those objects with altered products. This includes upstream and overlapping ORFs, internal/downstream frames, and ORFs on transcripts annotated as noncoding. The bundled catalogue is human GRCh38; explicit species-specific catalogues use the same API. ## Annotate with ORFdata's ORFs for the variant's genome Pass `extra_orfs="all"`, an evidence name or an ORFdata rule, and ORFdata chooses the catalogues for the variant's genome and places each ORF on every transcript that carries its exact coding path and protein: ```python import orfdata from pyensembl import cached_release from varcode import Variant variant = Variant("7", 66205486, "C", "G", genome=cached_release(115)) effects = variant.effects(extra_orfs="all") # every catalogue ORF effects = variant.effects(extra_orfs="ms") # only MS-supported ones effects = variant.effects(extra_orfs=orfdata.RiboSeq() & orfdata.MS()) ``` | `extra_orfs=` | ORFs used | | --- | --- | | `None` (default) | none | | `"all"` | every ORF ORFdata has for the genome | | `"ribosome"` or `"riboseq"` | translated in ribosome profiling | | `"ms"` | any supporting MS peptide | | `"proteomics"` | peptides from digested cellular proteins | | `"immunopeptidomics"` | peptides eluted from MHC molecules (HLA in human) | | a list of names, e.g. `["proteomics", "immunopeptidomics"]` | ORFs with any of them | | an ORFdata rule, e.g. `orfdata.RiboSeq() & orfdata.MS()` | ORFs meeting every part (`&`) or any part (`\|`) | | an ORFdata catalogue or bound ORFs | exactly those | `True` raises rather than guessing which of these you mean. The ORFs are placed once per genome and reused by later calls and by `VariantCollection.effects`. The same call works on other assemblies and species once ORFdata has what they need, and fails with the command that installs anything missing: - human GRCh38: the bundled catalogue, nothing to install; - human GRCh37, NCBI36 or T2T-CHM13: `orfdata setup --species human` for the chain files (and, for CHM13, `orfdata genomes install t2t-chm13` for the annotation); - mouse: `pip install "orfdata[data,build-data]"`, then `orfdata setup --species mouse`, which builds the three mouse catalogues from their sources (add `--accept-terms` for UCSC's GRCm38 and NCBIM37 chains). ORFs ORFdata could not place on the genome, for example because an older annotation lacks their transcript, are left out rather than failing the call; `varcode.extra_orfs.prepare_extra_orfs("all").excluded(genome)` lists them with the reason. ## Annotate ordinary and extra ORFs together Supply a catalogue to the usual effects API. Varcode finds the overlapping records and resolves their transcripts using the variant's genome: ```python from orfdata import load_catalog from pyensembl import cached_release from varcode import Variant genome = cached_release(95) # provision the reference before annotation variant = Variant("7", 66205486, "C", "G", genome=genome) effects = variant.effects(extra_orfs=load_catalog()) for annotation in effects.orf_annotations: print(annotation.orf_id, annotation.effect.short_description) for outcome in annotation.outcomes: if outcome.mutant is not None: print(outcome.mutant.protein_sequence) ``` This returns the ordinary UTR consequences alongside the `p.P2A` consequence in `c7riboseqorf56`. Omitting `extra_orfs` retains ordinary annotation without loading ORFdata. `extra_orfs` also accepts a bound reference ORF or an iterable of bound ORFs, including ORFdata collections; `VariantCollection.effects(extra_orfs=...)` reuses an iterable across all variants. Several ORFs on one RNA remain separate. No MS threshold is imposed by Varcode. Added entries are ordinary Varcode effect classes and participate in collection sorting and filtering. Their `effect.orf` is the bound reference; `effect.reference_transcript` is its original PyEnsembl RNA. The existing engine uses a private coding view in `effect.transcript`; its ID distinguishes the ORF for candidate grouping. Public `effect.transcript_id` and collection grouping / expression lookups use the original RNA identity. `effect.orf_annotation` holds native products, source evidence, and initiation-context changes. `effects.orf_annotations` selects these detailed annotations in collection order, including failures. `annotation.to_dict()` exports the ORF-specific report. `effects.to_json()` / `EffectCollection.from_json()` round-trip native effects, reference models, evidence and every alternative product through a versioned graph. Bound ORFs retain a snapshot of their full parent RNA and reference context, so their reconstruction needs no catalogue download or annotation lookup. These snapshots describe the archived RNA; they do not provide a genome database for new annotation. Dataframe/CSV summaries add `orf_id`, `orf_source` and `orf_unresolved_reason`. `effects.to_csv()` also includes an `effect_json` archive column when extra ORFs are present; `EffectCollection.from_csv()` restores those graphs and rejects edited identity columns that disagree with their payloads. `to_dataframe(include_archive=True)` provides the same full rows. Legacy CSV summaries lacking the archive still cannot reconstruct ORF models and are rejected. Ordinary CSV files retain their existing format. Bound inputs must match the variant genome's reference and annotation context. Catalogue records resolve against that genome and retain their source identities. Reference-scope mismatches raise even with `raise_on_error=False`; per-record binding/calculation errors raise by default and become `Failure` entries with `orf_annotation.unresolved_reason` when that flag is false. ORFdata collection exclusions overlapping the variant are reported in the same way. The `annotator` argument selects ordinary transcript annotation. Extra ORFs reuse Varcode's existing transcript-model engine, including patient germline hypotheses and known-cis collection effects. DNA phase queries use the original RNA identity. Observed RNA outcomes for the ordinary CDS cannot be directly reused for an extra ORF: such observations raise or produce a `Failure` until explicitly mapped with `annotate_orf_rna_product`, described below. This interface does not discover new ORFs or infer IRES activity, and unsupported translation models remain explicit. ## Work with a selected reference and its products ```python from pyensembl import cached_release from orfdata import load_catalog from varcode import Variant, annotate_orf_variants genome = cached_release(95) # explicitly provision this reference beforehand transcript = genome.transcript_by_id("ENST00000304842") catalog = load_catalog() reference = catalog.get("c7riboseqorf56").bind(transcript) variant = Variant("7", 66205486, "C", "G", genome=genome) annotation = annotate_orf_variants([variant], reference) assert annotation.reference is reference for outcome in annotation.outcomes: print(outcome.effect) if outcome.mutant is not None: print(outcome.mutant.protein_sequence) print(outcome.mutant.initiation_context.start_codon) ``` Select a known ORF explicitly by ID as above. To annotate every ORF on a transcript, iterate over `catalog.for_transcript(transcript)`; a transcript can have zero, one or several ORFs. ```python for reference in catalog.for_transcript(transcript): annotation = annotate_orf_variants([variant], reference) print(annotation.orf_id, annotation.effect) ``` The TPST1 +4 substitution is P2A in `c7riboseqorf56` and changes an initiation context base. Canonical `variant.effects()` still reports its ordinary UTR consequence. `variant.orf_effects()` discovers overlapping reference models and reports unavailable/mismatched models as unresolved. `ReferenceORF.model` holds the ORF identity, species, assembly, coding blocks and protein; `.evidence` holds source observations, and `.transcript` is the original PyEnsembl object. Varcode's `ORFAnnotation.reference` retains that base object; `.effect` and `.outcomes` describe modifications, including alternative phase/splice products. Each `ORFOutcome` carries native `.effect`, `.hypothesis`, `.baseline` and `.mutant` objects. Products expose sequences, termination and an orfdata `InitiationContext` directly. Unavailable structural products remain `None` with an `.unresolved_reason`; failed reference binding is reported by `annotation.unresolved_reason`. An empty outcome tuple does not prove no effect. Use `annotation.to_dict()` only when exporting JSON. A private coding view adapts this ORF to the existing transcript-model engine. It does not relabel a noncoding PyEnsembl transcript. Binding checks the entire spliced path, protein, stop, assembly and available species identity. Literal genomic REF mismatches raise, including VCF padding. Configured genome DNA is used automatically for intronic edits and splice/SV products; absent sequence remains unresolved. An explicit `sequence_provider` on `annotate_orf_variants` overrides this fallback. Bound transcript exons retain the validated ORF reference sequence. ORFdata 0.34 supports binding complete, stopped ORFs with NCBI tables 1, 2 and 3. Varcode preserves the declared code through variant and RNA-product translation. Other ORF binding codes still depend on ORFdata support; see [ORFdata #57](https://github.com/openvax/orfdata/issues/57). For ordinary transcripts, Varcode uses source-declared translation tables first, then its offline NCBI species/compartment assignments. Human/mouse mitochondria use table 2, budding yeast mitochondria table 3, fly/worm mitochondria table 5, and *Candida albicans* nuclear genes table 12. References without species metadata can use an exact assembly registered with PyEnsembl, including mirrored references. The bundled snapshot covers 22 taxa; unknown assignments require explicit `transcript.translation_table` or the relevant `genome.nuclear_translation_table`, `genome.mitochondrial_translation_table` or `genome.plastid_translation_table`. Unscoped nuclear transcripts retain the standard-code default. Codes 27, 28 and 31 require context-dependent stop recoding and remain explicitly unsupported. Definitions and assignments come from [NCBI](https://www.ncbi.nlm.nih.gov/Taxonomy/Utils/wprintgc.cgi); snapshot hashes and the refresh script are included with Varcode. For RNA products, `annotate_orf_rna_product(reference, product)` accepts a `RealizedTranscriptProduct` with one `(contig, position, strand)` or `None` in `evidence['base_origins']` per cDNA base. It validates the retained reference start and compares the conditional translation with the base ORF. Its `ORFProductAnnotation` exposes `.reference`, `.effect`, `.product`, `.status` and `.reason`. An unresolved result retains the supplied `.cdna_sequence` and `.evidence` but has no translated `.product`. Isovar owns read support and RNA reconstruction; Varcode owns the sequence consequence. Reference peptides are projected through codon ancestry, retaining every copied occurrence and identifying lost/altered peptides. Retention remains reference MS evidence, never mutant translation or MHC presentation evidence. All Sid TPST1–CRCP alternatives lack the native KIILSKISV peptide; each keeps its own RNA support and unresolved polarity/initiation status. The compatibility module `varcode.uorf` preserves the original upstream-specific call shapes and `varcode[uorf]` remains an installation alias. New code should use `varcode.orf` and the bound reference API above. `annotate_orf_product` retains its original dictionary return for existing RNA exporters; new Python code should use `annotate_orf_rna_product`. `ORFAnnotation.data` remains an export alias; construct annotations with native fields rather than a `data` dictionary. The model catalogue is not exhaustive ORF discovery: new starts and downstream CDS regulation are unassessed. ## Reference identity The ORF extras require ORFdata 0.34. A bound `ReferenceORF` retains its species, strain, assembly, annotation version and sequence fingerprints in both native and modified-product exports. RNA product metadata must agree with that context. For custom Genomes without taxonomy, use `record.bind(transcript, context=...)` before calling the generic annotation API. The compatibility uORF wrappers also accept `reference_context=...`. See [ORFdata's reference guide](https://github.com/openvax/orfdata/blob/main/docs/species.md). openvax-varcode-d96eb06/examples/000077500000000000000000000000001526602674500170675ustar00rootroot00000000000000openvax-varcode-d96eb06/examples/varcode-quick_start.ipynb000066400000000000000000000551311526602674500241110ustar00rootroot00000000000000{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Varcode - Quick Start\n", "\n", "`Varcode` is a library for working with genomic variant data in Python and predicting the impact of those variants on protein sequences.\n", "What this means is that this library can help you annotate gene variants, *i.e.* changes in DNA with their potential effect on the protein/transcript that they encode for.\n", "This notebook highlights the basic functionality provided by `varcode` in a nut-shell.\n", "If you haven't installed `varcode` yet, \n", "please refer to the original [README](https://github.com/hammerlab/varcode) file for specific instructions on how to install the library.\n", "\n", "## Working with variants and their effects\n", "Let's start with a really simple example to demonstrate what `varcode` can do for you.\n", "Imagine that the sequencing core facility has just sent you the sequencing results of that sample you submitted earlier to them.\n", "And now, they sent you a list of variants that they inferred using their analysis pipeline.\n", "You open the file just to see that they haven't annotated the variants,\n", "so you don't know whether these variants are within a gene\n", "and if so whether they affect the protein sequence or not.\n", "Enter `varcode`, which is specifically designed to solve this annotation problem.\n", "\n", "For the sake of the example,\n", "let's simplify things and assume that we are interested in annotating this variant, \n", "a nucleotide change from an `A` into a `T` at the `1,404,553,136`th base of `chromosome 7`.\n", "To annotate this variant, we first have to model it within `varcode` using the `Variant` class.\n", "We are going to assume that this coordinate is consistent with the human assembly [GRCh37](http://grch37.ensembl.org/index.html).\n", "`Varcode` stands on the shoulders of [`PyEnsembl`](https://github.com/hammerlab/pyensembl), a Python interface to Ensembl reference genome metadata.\n", "\n", "So to start things off, let's import the following classes that are relevant to our example:" ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "collapsed": false }, "outputs": [], "source": [ "from varcode import Variant\n", "from pyensembl import ensembl_grch37" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now let's create a new `Variant` that will represent our variant of interest:" ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "collapsed": false }, "outputs": [], "source": [ "myVariant = Variant(contig=7, start=140453136, ref=\"A\", alt=\"T\", ensembl=ensembl_grch37)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now that we defined this variant,\n", "we can start annotating it;\n", "but let's start with this trivial example,\n", "where we ask for a short descriptive description of the variant:" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "'chr7 g.140453136A>T'" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "myVariant.short_description" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "this is our variation, but expressed using the offical variation nomenclature.\n", "\n", "How about asking about the gene this variant is in:" ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "collapsed": false }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "INFO:root:Cached file Homo_sapiens.GRCh37.75.gtf from URL ftp://ftp.ensembl.org/pub/release-75/gtf/homo_sapiens/Homo_sapiens.GRCh37.75.gtf.gz\n" ] }, { "data": { "text/plain": [ "[Gene(id=ENSG00000157764, name=BRAF, biotype=protein_coding, location=7:140419127-140624564)]" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "myVariant.coding_genes" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Looks like this variant lies within the [BRAF gene](http://grch37.ensembl.org/Homo_sapiens/Gene/Summary?db=core;g=ENSG00000157764;r=7:140419127-140624564);\n", "but what about the potential effects of this variant to the product of this gene?" ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "collapsed": false }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "INFO:root:Cached file Homo_sapiens.GRCh37.75.cdna.all.fa from URL ftp://ftp.ensembl.org/pub/release-75/fasta/homo_sapiens/cdna/Homo_sapiens.GRCh37.75.cdna.all.fa.gz\n", "INFO:root:Cached file Homo_sapiens.GRCh37.75.pep.all.fa from URL ftp://ftp.ensembl.org/pub/release-75/fasta/homo_sapiens/pep/Homo_sapiens.GRCh37.75.pep.all.fa.gz\n", "/usr/local/lib/python3.4/site-packages/Bio/Seq.py:151: BiopythonWarning: Biopython Seq objects now use string comparison. Older versions of Biopython used object comparison. During this transition, please use hash(id(my_seq)) or my_dict[id(my_seq)] if you want the old behaviour, or use hash(str(my_seq)) or my_dict[str(my_seq)] for the new string hashing behaviour.\n", " \"the new string hashing behaviour.\", BiopythonWarning)\n" ] }, { "data": { "text/plain": [ "\n", " -- Substitution(variant=chr7 g.140453136A>T, transcript_name=BRAF-001, transcript_id=ENST00000288602, effect_description=p.V600E)\n", " -- IncompleteTranscript(variant=chr7 g.140453136A>T, transcript_name=BRAF-005, transcript_id=ENST00000479537)\n", " -- IncompleteTranscript(variant=chr7 g.140453136A>T, transcript_name=BRAF-003, transcript_id=ENST00000496384)\n", " -- IncompleteTranscript(variant=chr7 g.140453136A>T, transcript_name=BRAF-002, transcript_id=ENST00000497784)" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "myEffects = myVariant.effects()\n", "myEffects" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "What the above list tells us is that this variation can potentially affect four different **BRAF** transcripts\n", "and out of four, one will result in a `Substitution`, \n", "*i.e.* a coding mutation which causes simple substitution of one amino acid for another.\n", "For the other transcripts, `IncompleteTranscript` type implies that \n", "`varcode` can't determine effect since transcript annotation is incomplete (often missing either the start or stop codon).\n", "\n", "That is all great, but dealing with multiple effects is hard and we usually want to pick the one that causes the most dramatic change in the gene product.\n", "`Varcode` provides an easy way to get to this **worst effect**:" ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "Substitution(variant=chr7 g.140453136A>T, transcript_name=BRAF-001, transcript_id=ENST00000288602, effect_description=p.V600E)" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "topPriorityEffect = myEffects.top_priority_effect()\n", "topPriorityEffect" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "So overall, this what we have learned about the variant using `varcode`:" ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "The mutation chr7 g.140453136A>T leads to a Substitution in gene BRAF (p.V600E)\n" ] } ], "source": [ "print ('The mutation %s leads to a %s in gene %s (%s)' % (myVariant.short_description, type(topPriorityEffect).__name__, topPriorityEffect.gene_name, topPriorityEffect.short_description))\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Moreover, `varcode` can provide you with the altered protein sequence,\n", "which becomes important especially for analyses that use the variation information within the context of a few aminoacids surrounding the mutated location:" ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "Seq('LATVKSR', SingleLetterAlphabet())" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "variantLocation = topPriorityEffect.aa_mutation_start_offset\n", "topPriorityEffect.original_protein_sequence[variantLocation-3:variantLocation+4]" ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "Seq('LATEKSR', SingleLetterAlphabet())" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "topPriorityEffect.mutant_protein_sequence[variantLocation-3:variantLocation+4]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "See that valenine (`V`) changing into a glutamine (`E`)?\n", "That is the effect of our variant within the aminoacid context.\n", "That was easy, right?" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Importing variants from a MAF or VCF file\n", "Now that we know how to work with a single variant\n", "and extract annotations for it,\n", "it is now time for us to learn how to work with collections of variants all together.\n", "\n", "In the previous section, \n", "we assumed that there was a single variation that we wanted to annotate;\n", "but in the real world,\n", "we usually receive the variant calls from a sequencing analysis as a file\n", "that is formatted with either [Mutation Annotation Format](https://wiki.nci.nih.gov/display/TCGA/Mutation+Annotation+Format+(MAF)+Specification) (MAF) or [Variant Call Format](http://www.1000genomes.org/wiki/Analysis/Variant%20Call%20Format/vcf-variant-call-format-version-40) (VCF).\n", "`Varcode` has built-in methods to load variants described by either of the formats:" ] }, { "cell_type": "code", "execution_count": 10, "metadata": { "collapsed": false }, "outputs": [], "source": [ "from varcode import load_maf, load_vcf" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The following loads mutations from the MAF file distributed within [our code repository](https://github.com/hammerlab/pyensembl/tree/master/test/data):" ] }, { "cell_type": "code", "execution_count": 11, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "\n", " -- Variant(contig=1, start=1650797, ref=A, alt=G, genome=GRCh37)\n", " -- Variant(contig=1, start=23836447, ref=C, alt=A, genome=GRCh37)\n", " -- Variant(contig=1, start=231401797, ref=A, alt=C, genome=GRCh37)\n", " -- Variant(contig=11, start=124617502, ref=C, alt=G, genome=GRCh37)" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "mafVariants = load_maf(\"../test/data/tcga_ov.head.maf\")\n", "mafVariants # should load 4 variants" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "and this should load variants from one of the VCF files:" ] }, { "cell_type": "code", "execution_count": 12, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "\n", " -- Variant(contig=1, start=53513530, ref=A, alt=C, genome=GRCh37)\n", " -- Variant(contig=1, start=228295398, ref=G, alt=T, genome=GRCh37)\n", " -- Variant(contig=10, start=49658590, ref=T, alt=C, genome=GRCh37)\n", " -- Variant(contig=10, start=51585166, ref=G, alt=T, genome=GRCh37)\n", " -- Variant(contig=10, start=96709040, ref=A, alt=C, genome=GRCh37)\n", " -- Variant(contig=10, start=119134281, ref=G, alt=T, genome=GRCh37)\n", " -- Variant(contig=11, start=118244286, ref=., alt=., genome=GRCh37)\n", " -- Variant(contig=12, start=14794076, ref=C, alt=A, genome=GRCh37)\n", " -- Variant(contig=12, start=25398284, ref=C, alt=G, genome=GRCh37)\n", " -- Variant(contig=12, start=42778752, ref=T, alt=A, genome=GRCh37)\n", " -- Variant(contig=14, start=31144202, ref=A, alt=C, genome=GRCh37)\n", " -- Variant(contig=16, start=25704209, ref=G, alt=A, genome=GRCh37)\n", " -- Variant(contig=17, start=7577548, ref=., alt=A, genome=GRCh37)\n", " -- Variant(contig=17, start=36731197, ref=C, alt=AAT, genome=GRCh37)" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "vcfVariants = load_vcf(\"../test/data/somatic_hg19_14muts.vcf\")\n", "vcfVariants # should load 14 variants" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You can even extract summary statistics from these variant collections\n", "if you would like to have a quick look at the overall variants and the altered genes:" ] }, { "cell_type": "code", "execution_count": 13, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "Counter({'SCP2': 1, 'HS3ST4': 1, 'SCFD1': 1, 'TP53': 1, 'GUCY2C': 1, 'CYP2C9': 1, 'PDZD8': 1, 'MRPL55': 1, 'NCOA4': 1, 'UBE4A': 1, 'SRCIN1': 1, 'ARHGAP22': 1, 'KRAS': 1, 'PPHLN1': 1})" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "vcfVariants.gene_counts()" ] }, { "cell_type": "code", "execution_count": 14, "metadata": { "collapsed": false }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "INFO:root:Cached file Homo_sapiens.GRCh37.75.gtf from URL ftp://ftp.ensembl.org/pub/release-75/gtf/homo_sapiens/Homo_sapiens.GRCh37.75.gtf.gz\n" ] }, { "data": { "text/plain": [ "Counter({'VSIG2': 1, 'GNPAT': 1, 'E2F2': 1, 'RP1-283E3.8': 1, 'CDK11A': 1})" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "mafVariants.gene_counts()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Filtering Variants\n", "Having a collection of variants is great,\n", "but you often need to filter them down to see, for example, whether any of the variants affect your gene of interest (**TP53**):" ] }, { "cell_type": "code", "execution_count": 15, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "\n", " -- FrameShift(variant=chr17 g.7577548_7577549insA, transcript_name=TP53-001, transcript_id=ENST00000269305, effect_description=p.G245fs)\n", " -- FrameShift(variant=chr17 g.7577548_7577549insA, transcript_name=TP53-019, transcript_id=ENST00000359597, effect_description=p.G245fs)\n", " -- FrameShift(variant=chr17 g.7577548_7577549insA, transcript_name=TP53-018, transcript_id=ENST00000413465, effect_description=p.G245fs)\n", " -- FrameShift(variant=chr17 g.7577548_7577549insA, transcript_name=TP53-005, transcript_id=ENST00000420246, effect_description=p.G245fs)\n", " -- FrameShift(variant=chr17 g.7577548_7577549insA, transcript_name=TP53-002, transcript_id=ENST00000445888, effect_description=p.G245fs)\n", " -- FrameShift(variant=chr17 g.7577548_7577549insA, transcript_name=TP53-004, transcript_id=ENST00000455263, effect_description=p.G245fs)\n", " -- NoncodingTranscript(variant=chr17 g.7577548_7577549insA, transcript_name=TP53-006, transcript_id=ENST00000504290)\n", " -- NoncodingTranscript(variant=chr17 g.7577548_7577549insA, transcript_name=TP53-008, transcript_id=ENST00000504937)\n", " -- IncompleteTranscript(variant=chr17 g.7577548_7577549insA, transcript_name=TP53-015, transcript_id=ENST00000509690)\n", " -- NoncodingTranscript(variant=chr17 g.7577548_7577549insA, transcript_name=TP53-007, transcript_id=ENST00000510385)\n", " -- IncompleteTranscript(variant=chr17 g.7577548_7577549insA, transcript_name=TP53-013, transcript_id=ENST00000514944)" ] }, "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ "tp53Mutations = vcfVariants.groupby_gene_name()[\"TP53\"].effects()\n", "tp53Mutations" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "or, for example, you might want to drop all mutations that do not affect a protein sequence or variants that fall in a non-coding genomic region:" ] }, { "cell_type": "code", "execution_count": 16, "metadata": { "collapsed": false }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/usr/local/lib/python3.4/site-packages/Bio/Seq.py:151: BiopythonWarning: Biopython Seq objects now use string comparison. Older versions of Biopython used object comparison. During this transition, please use hash(id(my_seq)) or my_dict[id(my_seq)] if you want the old behaviour, or use hash(str(my_seq)) or my_dict[str(my_seq)] for the new string hashing behaviour.\n", " \"the new string hashing behaviour.\", BiopythonWarning)\n" ] }, { "data": { "text/plain": [ "OrderedDict([('ENSG00000110344', Insertion(variant=chr11 g.118244286, transcript_name=UBE4A-002, transcript_id=ENST00000431736, effect_description=p.340ins)), ('ENSG00000165650', Substitution(variant=chr10 g.119134281G>T, transcript_name=PDZD8-001, transcript_id=ENST00000334464, effect_description=p.T153K)), ('ENSG00000162910', Substitution(variant=chr1 g.228295398G>T, transcript_name=MRPL55-201, transcript_id=ENST00000366731, effect_description=p.R103S)), ('ENSG00000017373', FrameShift(variant=chr17 g.36731197C>AAT, transcript_name=SRCIN1-002, transcript_id=ENST00000578925, effect_description=p.V117fs)), ('ENSG00000128805', Substitution(variant=chr10 g.49658590T>C, transcript_name=ARHGAP22-002, transcript_id=ENST00000417912, effect_description=p.S544G)), ('ENSG00000092108', Substitution(variant=chr14 g.31144202A>C, transcript_name=SCFD1-006, transcript_id=ENST00000458591, effect_description=p.E391D)), ('ENSG00000138293', Substitution(variant=chr10 g.51585166G>T, transcript_name=NCOA4-205, transcript_id=ENST00000452682, effect_description=p.C438F)), ('ENSG00000141510', FrameShift(variant=chr17 g.7577548_7577549insA, transcript_name=TP53-001, transcript_id=ENST00000269305, effect_description=p.G245fs)), ('ENSG00000070019', Substitution(variant=chr12 g.14794076C>A, transcript_name=GUCY2C-001, transcript_id=ENST00000261170, effect_description=p.A670S)), ('ENSG00000133703', Substitution(variant=chr12 g.25398284C>G, transcript_name=KRAS-004, transcript_id=ENST00000256078, effect_description=p.G12A))])" ] }, "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ "vcfEffects = vcfVariants.effects()\n", "nonSilentMutations = vcfEffects.drop_silent_and_noncoding()\n", "nonSilentMutations.top_priority_effect_per_gene_id()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "or, you might want to get all effects whose priority falls below an `Insertion`:" ] }, { "cell_type": "code", "execution_count": 17, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "OrderedDict([('ENSG00000110344', Insertion(variant=chr11 g.118244286, transcript_name=UBE4A-002, transcript_id=ENST00000431736, effect_description=p.340ins)), ('ENSG00000138109', ExonicSpliceSite(exon=Exon(exon_id=ENSE00002485068, gene_name=CYP2C9, contig=10, start=96708865, end=96709041), alternate_effect=Substitution(variant=chr10 g.96709040A>C, transcript_name=CYP2C9-001, transcript_id=ENST00000260682, effect_description=p.K273T))), ('ENSG00000141510', FrameShift(variant=chr17 g.7577548_7577549insA, transcript_name=TP53-001, transcript_id=ENST00000269305, effect_description=p.G245fs)), ('ENSG00000017373', FrameShift(variant=chr17 g.36731197C>AAT, transcript_name=SRCIN1-002, transcript_id=ENST00000578925, effect_description=p.V117fs)), ('ENSG00000116171', ExonicSpliceSite(exon=Exon(exon_id=ENSE00003489518, gene_name=SCP2, contig=1, start=53513530, end=53513609), alternate_effect=Substitution(variant=chr1 g.53513530A>C, transcript_name=SCP2-001, transcript_id=ENST00000371514, effect_description=p.D490A)))])" ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from varcode import Insertion\n", "vcfEffects.filter_by_effect_priority(Insertion).top_priority_effect_per_gene_id()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Summary\n", "Here, we went over how you can `varcode` to annotate variants (either one-by-one or as a batch from a collection)\n", "and filter their effects based on biological criteria.\n", "For more examples on methods provided by `varcode`,\n", "we suggest taking a look at our code tests.\n", "A significant portion of the `varcode` terminology is inherited from [PyEnsembl](https://github.com/hammerlab/pyensembl);\n", "therefore, you can also take a look at its documentation for further clarification on internal workings of this library.\n" ] } ], "metadata": { "kernelspec": { "display_name": "Python 2", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.4.3" } }, "nbformat": 4, "nbformat_minor": 0 } openvax-varcode-d96eb06/examples/vcf_header_usage.py000066400000000000000000000047741526602674500227270ustar00rootroot00000000000000#!/usr/bin/env python3 # Licensed under the Apache License, Version 2.0 (the "License"). """ Standalone usage example for ``varcode.vcf_parsing.VCFHeader``. This demonstrates how to parse a VCF header and decode INFO / FORMAT cells without going through ``load_vcf`` — useful when you want the typed dicts but are streaming records yourself, building a custom data frame, etc. Run from the repo root: python examples/vcf_header_usage.py The fixture used here is the canonical VCF 4.2 spec example (samtools/hts-specs). """ import sys from pathlib import Path # Make the example runnable from anywhere — fall back to the local checkout # even when a different varcode happens to be on sys.path first. REPO_ROOT = Path(__file__).resolve().parent.parent if str(REPO_ROOT) not in sys.path: sys.path.insert(0, str(REPO_ROOT)) from varcode.vcf_parsing import VCFHeader # noqa: E402 TRIO_VCF = REPO_ROOT / "tests" / "data" / "spec_examples" / "vcf42_spec_trio.vcf" def main() -> None: header = VCFHeader.from_path(str(TRIO_VCF)) print("== Header ==") print(f"fileformat : {header.metadata['fileformat']}") print(f"reference : {header.metadata['reference']}") print(f"samples : {header.samples}") print(f"INFO fields : {list(header.info_fields)}") print(f"FORMAT fields : {list(header.format_fields)}") # The typed declaration for an INFO field is a FieldDef dataclass. af = header.info_fields["AF"] print() print("== INFO[AF] declaration ==") print(f"id : {af.id}") print(f"number : {af.number} # -1 means 'A' (one value per ALT)") print(f"type : {af.type}") print(f"description : {af.description}") # Walk records and pretty-print decoded INFO + per-sample dicts. We # split rows ourselves to keep this example dependency-free. print() print("== Records ==") with open(TRIO_VCF) as fh: for line in fh: if line.startswith("#"): continue cols = line.rstrip("\n").split("\t") chrom, pos, vid, ref, alt = cols[0], cols[1], cols[2], cols[3], cols[4] info = header.parse_info(cols[7]) samples = header.parse_samples(cols[9:], cols[8]) print(f" {chrom}:{pos} {vid or '.'} {ref} -> {alt}") print(f" INFO = {info}") for sample_name, fields in samples.items(): print(f" {sample_name:<8} = {dict(fields)}") print() if __name__ == "__main__": main() openvax-varcode-d96eb06/lint.sh000077500000000000000000000001211526602674500165500ustar00rootroot00000000000000#!/bin/bash set -o errexit ruff check varcode/ tests/ echo 'Passes ruff check' openvax-varcode-d96eb06/mkdocs.yml000066400000000000000000000054651526602674500172660ustar00rootroot00000000000000site_name: varcode site_description: Library for manipulating genomic variants and predicting their effects site_url: https://openvax.github.io/varcode/ repo_url: https://github.com/openvax/varcode repo_name: openvax/varcode edit_uri: edit/main/docs/ theme: name: material features: - navigation.instant - navigation.tracking - navigation.sections - navigation.top - content.code.copy - content.action.edit palette: - media: "(prefers-color-scheme: light)" scheme: default primary: indigo accent: indigo toggle: icon: material/weather-sunny name: Switch to dark mode - media: "(prefers-color-scheme: dark)" scheme: slate primary: indigo accent: indigo toggle: icon: material/weather-night name: Switch to light mode nav: - Home: index.md - Getting started: getting_started.md - Everyday tasks: - Effect annotation: effect_annotation.md - Genotypes & sample queries: genotype.md - Sample identity checks: sample_identity.md - Saving & reloading results: csv.md - Troubleshooting: errors.md - Advanced workflows: - Splice variants: splice_variants.md - Structural variants: structural_variants.md - Comparing SV callers: sv_comparison.md - Observed RNA imports: rna_structures.md - Germline-aware annotation: germline.md - Phasing: phasing.md - VariantCollection transforms: transforms.md - Reference ORF consequences: uorf_annotations.md - Experimental annotators: experimental_annotators.md - Reference: - Varcode, Isovar & Vaxrank: library_roles.md - Transcript models: transcript_models.md - SV coordinates & fusion rules: sv_reference.md - Effect types: effect_types.md - API: - Index: api.md - Variants & files: api_variants.md - Effects: api_effects.md - Phasing & germline: api_phasing.md - RNA & transcripts: api_rna.md - Annotators: api_annotators.md - Writing an annotator: annotator_contract.md - Project: - Changelog: changelog.md markdown_extensions: - admonition - def_list - footnotes - toc: permalink: true - pymdownx.details - pymdownx.highlight: anchor_linenums: true - pymdownx.inlinehilite - pymdownx.snippets: base_path: - . - docs - pymdownx.superfences - tables plugins: - search - mkdocstrings: default_handler: python handlers: python: options: show_source: true show_root_heading: true heading_level: 3 show_signature_annotations: true members_order: source docstring_style: numpy docstring_section_style: spacy merge_init_into_class: true openvax-varcode-d96eb06/notes/000077500000000000000000000000001526602674500164015ustar00rootroot00000000000000openvax-varcode-d96eb06/notes/README.md000066400000000000000000000004311526602674500176560ustar00rootroot00000000000000These are maintainer working notes, not published user documentation. They are kept for reference and are not built into the docs site. Files: development_audit.md (2026-09-24 quality audit), orf-layer-design.md and uorf_implementation_plan.md (identical ORF-layer design notes). openvax-varcode-d96eb06/notes/benchmark_structural_407.py000066400000000000000000000072451526602674500235770ustar00rootroot00000000000000"""Reproduce #407 with locally installed Ensembl 115 and public ESVEE VCFs. Run this same script with PYTHONPATH pointing to each checkout being compared. The output includes complete effect graphs for equality checks; only annotation time and Varcode version are removed from those graphs. """ import argparse import copy import cProfile import hashlib import json import pstats import statistics import time from importlib.metadata import version from pathlib import Path from pyensembl import EnsemblRelease import varcode from varcode.transforms import pair_breakends def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("organoid_vcf", help="T1 organoid ESVEE somatic VCF") parser.add_argument("tumor_vcf", help="T2 tumor ESVEE somatic VCF") parser.add_argument("output_directory", type=Path) parser.add_argument("--repeats", type=int, default=3) parser.add_argument("--profile", action="store_true") args = parser.parse_args() if args.repeats < 1: parser.error("--repeats must be positive") genome = EnsemblRelease(115) if not genome.installed(): parser.error("Install human Ensembl 115 before running this benchmark") organoid = varcode.load_vcf( args.organoid_vcf, genome=genome, parse_structural_variants=True) positions = {58909683, 54155308, 54153839, 36148058} cases = [v for v in organoid if v.start in positions] if len(cases) != 4: parser.error("Organoid VCF must contain the four reported breakends") tumor = pair_breakends(varcode.load_vcf( args.tumor_vcf, genome=genome, parse_structural_variants=True)) cases.append(next(v for v in tumor if ( v.contig == "2" and v.sv_type == "INV" and v.start == 25955847 and v.end == 63053516))) args.output_directory.mkdir(parents=True, exist_ok=True) report = { "versions": {"varcode": varcode.__version__, **{ p: version(p) for p in ("pyensembl", "datacache", "numpy", "pandas")}}, "source_sha256": {str(path): hashlib.sha256(Path(path).read_bytes()).hexdigest() for path in (args.organoid_vcf, args.tumor_vcf)}, "cases": [], } for variant in cases: times = [] def annotate(): # Retain the reference cache, but recompute variant-level candidates. fresh = copy.copy(variant) fresh._annotation_cache = {} return fresh.effects() for _ in range(args.repeats): started = time.perf_counter() effects = annotate() times.append(time.perf_counter() - started) data = json.loads(effects.to_json()) data.pop("annotated_at", None) data.pop("annotator_version", None) encoded = json.dumps(data, sort_keys=True, separators=(",", ":")).encode() stem = args.output_directory / str(variant.start) stem.with_suffix(".json").write_bytes(encoded) row = { "case": variant.short_description, "seconds": times, "median_seconds": statistics.median(times), "effects": len(effects), "sha256": hashlib.sha256(encoded).hexdigest(), } report["cases"].append(row) print(json.dumps(row), flush=True) if args.profile: profiler = cProfile.Profile() profiler.runcall(annotate) profiler.dump_stats(str(stem.with_suffix(".prof"))) with stem.with_suffix(".txt").open("w") as stream: pstats.Stats(profiler, stream=stream).sort_stats("cumulative").print_stats(50) (args.output_directory / "report.json").write_text(json.dumps(report, indent=2) + "\n") if __name__ == "__main__": main() openvax-varcode-d96eb06/notes/build_genetic_code_data.py000066400000000000000000000044161526602674500235400ustar00rootroot00000000000000"""Build the offline genetic-code snapshot from NCBI gc.prt and Taxonomy XML. Usage: python notes/build_genetic_code_data.py gc.prt taxonomy.xml output.json Sources: https://ftp.ncbi.nlm.nih.gov/entrez/misc/data/gc.prt and EUtils efetch.fcgi?db=taxonomy&id=&retmode=xml. No network access at build or annotation time; review refreshed data in a PR. """ import hashlib import json from pathlib import Path import re import sys import xml.etree.ElementTree as ET def build(codes_path, taxonomy_path): codes_data = Path(codes_path).read_bytes() taxonomy_data = Path(taxonomy_path).read_bytes() codes = {} for block in re.findall(r"\{([^{}]+)\}", codes_data.decode()): identifier = re.search(r"\bid\s+(\d+)", block) if identifier is None: continue codes[identifier[1]] = dict( name=re.search(r'name\s+"([^"]+)"', block)[1], amino_acids=re.search(r'ncbieaa\s+"([^"]+)"', block)[1], starts_stops=re.search(r'sncbieaa\s+"([^"]+)"', block)[1]) taxa = {} for taxon in ET.fromstring(taxonomy_data).findall("Taxon"): aliases = {taxon.findtext("ScientificName")} for tag in ("Synonym", "GenbankCommonName", "CommonName", "EquivalentName"): aliases.update(n.text for n in taxon.findall("OtherNames/" + tag)) codes_by_compartment = dict( nuclear=int(taxon.findtext("GeneticCode/GCId")), mitochondrial=int(taxon.findtext("MitoGeneticCode/MGCId"))) for prop in taxon.findall("Properties/Property"): if prop.findtext("PropName") == "pgcode": codes_by_compartment["plastid"] = int(prop.findtext("PropValueInt")) taxa[taxon.findtext("TaxId")] = dict( name=taxon.findtext("ScientificName"), aliases=sorted(name.lower().replace(" ", "_") for name in aliases), **codes_by_compartment) return dict( source="NCBI gc.prt version 4.6 and Taxonomy EUtils; retrieved 2026-10-20", code_source_sha256=hashlib.sha256(codes_data).hexdigest(), taxonomy_source_sha256=hashlib.sha256(taxonomy_data).hexdigest(), codes=codes, taxa=taxa) if __name__ == "__main__": Path(sys.argv[3]).write_text(json.dumps(build(*sys.argv[1:3]), indent=2, sort_keys=True) + "\n") openvax-varcode-d96eb06/notes/development_audit.md000066400000000000000000000221421526602674500224340ustar00rootroot00000000000000# Documentation and code quality audit — 2026-09-24 !!! note "Historical record" This page records the state of the project when the audit was done. Some findings have since been fixed; check the linked issues and the [changelog](../CHANGELOG.md) for current status. This audit started at `525499d` (Varcode 10.1.1). It combines a broad static review with targeted execution of parsing, metadata, failure handling, and phase-enumeration paths. The accompanying 10.1.2 maintenance change contains bounded corrections; larger export and scientific-model problems are tracked separately below. ## Coverage and method The repository contained 67 library Python modules, 106 test/helper modules, and 26 documentation pages before this work. The audit checked: - README, contributor/release instructions, test-data provenance, navigation, troubleshooting, API links, and workflow examples against code and CI. - VCF/MAF loading and VCF export: mixed allele types, limits, multi-allelic indexes, sample/FORMAT ordering, normalization, and round-trip assertions. - Collection construction, mutable defaults, annotation error reporting, lint configuration, test interpreter selection, and fixture construction. - Germline phase enumeration and its consumer, including an end-to-end CFTR reproduction where changing the cap changes the reported consequence. - Existing issue and PR coverage, to avoid duplicating known problems. The static pass checked undefined names and nearby disabled lint rules. Reproductions used inline synthetic files or explicitly pinned Ensembl 81. The audit does not establish biological validity for every annotator or exhaustively review every source line. Large consequence modules still need domain-specific reviews and independent scientific oracles. ## Major findings requiring dedicated work | Priority | Finding and evidence | Disposition | |---|---|---| | High | VCF export derives header sample order from a set but writes row values in dictionary order. A two-row reproduction assigns tumor GT/DP to normal and emits `44:0/1` under `GT:DP`. Differing sample sets also discard sample data; headers and allele-indexed metadata lack a complete preservation contract. | Filed [#502](https://github.com/openvax/varcode/issues/502); resolved in 10.1.3 with explicit [export guarantees and limits](../docs/csv.md#vcf-export). | | High | The phase cap returns an all-cis placeholder that the consumer classifies as a precise effect. The existing CFTR pair yields cis/trans alternatives at cap 8 but only `p.S159T` at cap 1, with no molecular evidence resolving phase. Four unphased germline alleles exceed the default cap. | Filed [#503](https://github.com/openvax/varcode/issues/503); documented in [germline annotation](../docs/germline.md). | | High | `deploy.sh` lacked the branch/clean-tree guards, version handling, tagging, and pushing described in AGENTS.md. | Resolved in 10.11.3 ([#414](https://github.com/openvax/varcode/issues/414)): guarded deployment of the merged version, dry runs, distribution checks and automatic tagging; version bumps remain in PRs. | | Medium | The test cache probe accesses `EnsemblRelease.db`, which can download missing reference data while supposedly checking local installation. | Resolved [#493](https://github.com/openvax/varcode/issues/493) in 10.11.2 by pinning bare GRCh38 test references to the existing Ensembl 81 fixture and removing the cache probe. | | Medium | Some tests cannot detect failures, including the fast-path self-comparison and protocol/attribute surveys. VCF round-trip checks omitted ALT and complete record counts. | Existing [#479](https://github.com/openvax/varcode/issues/479); this change strengthens the VCF checks. The broader test review remains open. | | Medium | Reference mismatch fields contain transcript-oriented bases but the message labels them as genome bases; minus-strand errors can be read backwards. | Existing [#434](https://github.com/openvax/varcode/issues/434); troubleshooting now explains the current orientation. | | Medium | Historical changelog coverage is incomplete. | Existing [#411](https://github.com/openvax/varcode/issues/411); no invented reconstruction of missing release history. | The export review used the primary [VCF specification](https://samtools.github.io/hts-specs/VCFv4.5.pdf), particularly header/sample ordering and FORMAT field interpretation. The phase finding is an observed inconsistency between unresolved evidence and the implementation's precise output, not a proposed replacement biological model. ## Small corrections in the maintenance change | Area | Correction | Verification | |---|---|---| | VCF limits | Count successfully loaded alleles consistently across small/SV calls, ALT lists and chunks; zero means none; reject invalid caps; stop without consuming a later chunk. | Synthetic mixed VCF, multiple chunk sizes and limits, explicit deduplication boundary ([#504](https://github.com/openvax/varcode/issues/504)). | | VCF errors | Remove StopIteration as an internal early-exit mechanism so unexpected parser failures propagate; validate dataframe columns with ValueError even under optimized Python. | Deliberately failing parser and malformed dataframe regressions. | | Structural metadata | Retain the original ALT index for structural alleles using the same metadata path as small variants. | A `C,` / `GT=0/2` fixture selects only the deletion for the tumor sample. | | Collection state | Allocate independent default metadata dictionaries and source sets; remove nearby mutable parser defaults. | Mutating one collection cannot change another ([#505](https://github.com/openvax/varcode/issues/505)). | | Error diagnostics | Keep the original exception text in the transcript failure helper, matching the collection annotation path. | Real pinned-reference mismatch checks variant, transcript and error ([#506](https://github.com/openvax/varcode/issues/506)). | | MAF normalization | Rename case-only columns in place rather than moving them to the end; remove the Python 2 workaround. | Synthetic partly lowercased header retains canonical column order ([#507](https://github.com/openvax/varcode/issues/507)). | | Test execution | Probe and invoke pytest with the same Python interpreter; preserve arguments and process exit status. | A deliberately conflicting pytest executable with and without xdist ([#490](https://github.com/openvax/varcode/issues/490)). | | Logging and lint | Replace deprecated `warn` calls and enable undefined-name checks that the code already passes. | Ruff on library and tests. | | Existing test quality | Check every round-tripped ALT and record count, clean temporary exports, and materialize parametrization iterables for pytest compatibility. | Existing VCF/MAF round-trip and parsing tests. | | Developer workflow | Add environment, reference-data, test, parity and docs-build commands; make editable installation use the active interpreter. | Compare with CI and execute documented checks. | | Release documentation | Use `main`, the actual version-file path and real script capabilities; document clean-main publishing and artifact verification. | Inspect script and release workflow. | | Test data documentation | Correct current Osteosarc pin/support to 0.2.3 and Python 3.9+, while preserving historical 0.1.0 collection provenance and snapshot hashes. Require reproducible fixture recipes alongside tests. | Compare optional extra, CI matrix and integration fixture. | | User documentation | Add missing inherited-overlap API links and SV comparison navigation; clarify CLI SV defaults, Failure handling, transcript-oriented mismatch bases, and large unresolved limitations. | Strict MkDocs build and local link checks. | All new synthetic fixture construction is checked in beside its tests. No machine-local data bundle or bespoke external generator is required. ## Existing maintenance PRs [#409](https://github.com/openvax/varcode/pull/409) covers broader staging-note cleanup and a fast-path test correction. [#410](https://github.com/openvax/varcode/pull/410) covers unused developer configuration. [#431](https://github.com/openvax/varcode/pull/431) covers CLI error presentation. This audit does not supersede those PRs; shared files will need rebasing. In particular, this PR does not remove legacy config files or rewrite all experimental-annotator documentation. ## Follow-up sequence Sample identity/FORMAT export integrity (#502) was fixed in 10.1.3. Phase-cap uncertainty (#503) is the next scientific correctness priority. Then make release guards executable (#414). Test reference selection no longer probes the cache (#493, fixed in 10.11.2). Broader uncertainty composition (#421/#423), shared RNA fixture adoption (#464), and test-oracle cleanup (#479) remain foundational work; they should be reviewed as their own changes. ## Validation for the maintenance change Local verification on Python 3.12 passed `./lint.sh`, all 2,735 tests through `./test.sh`, and the 123-test focused parser/collection/error/script set. The strict MkDocs build passed; checking the generated HTML found no broken files or anchors among 8,055 local links. The PR records CI and publication verification separately, including the supported Python 3.9–3.11 matrix. openvax-varcode-d96eb06/notes/orf-layer-design.md000066400000000000000000000114641526602674500221000ustar00rootroot00000000000000# General ORF reference and evidence layer Status: architectural correction to the unmerged oncoref/Varcode/Isovar PRs. Working distribution name: orfdata. No release is authorized by this document. ## Ownership PyEnsembl remains responsible for reference genomes, annotations, transcripts, exons and canonical proteins. A small separate library adds source-defined ORFs on those transcripts and their experimental evidence. oncoref remains a consumer for cancer-specific analysis. Neither Varcode nor Isovar owns reference datasets. PepData's immune-epitope/property scope is not a fit. The new library has a dependency-light data layer and a PyEnsembl-compatible binding API. It must not import Varcode, Isovar or oncoref. No new PyEnsembl core API is needed to bind a Transcript; avoid reverse dependencies or monkey-patching. ## Public objects - ORFModel: immutable source ORF identity, assembly, species (NCBI taxon ID and scientific name), translation table, parent transcript, spliced coding blocks, protein sequence, ORF class. ORF class is distinct from transcript biotype. - ORFEvidence: ribosome observations and peptide-level MS observations with their source, accession, mapping ambiguity, assay and quality. Native evidence does not become evidence for a modified product; orthology is not direct evidence. - ORFRecord: model plus evidence, retaining legacy flat accessors for Isovar. - ReferenceORF: validated binding of a record to a PyEnsembl Transcript, retaining the original transcript (including noncoding biotype), cDNA coding interval, coding sequence and actual resolved annotation/version. Multiple ORFs per RNA are normal. Annotation lookup never relabels the transcript protein-coding. - ORFCatalog: explicitly one species/assembly per catalogue. Query by transcript, gene, locus, class or evidence. for_transcript(transcript) validates and returns ReferenceORFs. Load custom versioned catalogues offline. The initial bundled human data is identified as human-only; no implied mouse evidence. - Varcode ORFAnnotation: reference ORF plus effect and realized products. Exports separately identify reference model/evidence and modifications, preserving phase/splice alternatives. A private transcript-engine adapter permits reuse of existing variant logic without exposing a fake transcript as the base ORF. - Isovar: attach those same annotations to RNA products with candidate-specific RNA support, maintaining conditional translation and unresolved RNA polarity. ## Generality and compatibility Support uORF, overlapping uORF, internal/out-of-frame, downstream and noncoding- transcript ORFs with the same model and evidence schema. Preserve source classes verbatim rather than equating them with Ensembl transcript biotypes. Complete stopped ORFs are the initial binding contract; partial models are rejected explicitly rather than given invented boundaries. A generic v1 schema requires species, assembly, translation table and source provenance. Legacy Isovar human data may be read only through its explicit known human/GRCh38 provenance. Keep legacy Isovar uorf_* imports/call shapes working. New generic APIs use orf terminology; defaults remain explicit about dataset scope. Multi-species path: one versioned catalogue per organism/assembly, importer input using the same model/evidence schema, optional organism-specific source adapters (e.g. OpenProt). Test a mouse noncoding transcript with real PyEnsembl objects, plus human coding transcripts, different frames, minus strand, splice junctions, assembly/species mismatches and unknown translation tables. Synthetic examples are identified as test fixtures, never scientific observations. OpenProt already publishes multiple species with Ensembl/RefSeq mappings and evidence filters. ## Review corrections Remove the oncoref runtime dependency from both consumers and withdraw its ORF PR when the replacement is reviewable. Keep unrelated lint fixes independently. Review evidence retention for duplicate reference codons (retain all copies), explicit translation tables and noncoding parents; preserve all Sid alternatives. Document unsupported de novo initiation discovery and partial ORFs honestly. ## Verification New library: existing evidence/context/builder tests, generic data round-trip, PyEnsembl binding and multi-species isolation, independently installed wheel. Varcode/Isovar: focused tests then required full lint/test scripts. Regenerate Sid outputs from the original packaged reads. Existing human evidence counts and native peptide attribution must remain unchanged. Do not claim release until CI, merge and PyPI publication actually complete; publication remains unapproved. Primary sources: - https://www.nature.com/articles/s41587-022-01369-0 - https://www.nature.com/articles/s41586-026-10459-x - https://openprot.org/p/help - https://github.com/openvax/pyensembl openvax-varcode-d96eb06/notes/pr-test-concurrency-plan.md000066400000000000000000000013521526602674500236020ustar00rootroot00000000000000# Varcode #604: cancel superseded PR test matrices Scope the Tests workflow concurrency group to its workflow name, event type and PR number. Non-PR runs use their unique run ID, so separate PRs and every main push remain independent. Cancel in-progress runs only on PR events. Run lint and the full suite, then push two successive PR commits while the first matrix is active. Verify GitHub cancels the first run automatically and the latest completes every matrix job. Record the run links in the PR, merge only after green CI, and deploy the patch release from clean main. Reference: GitHub's workflow syntax, concurrency and cancel-in-progress: https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#concurrency openvax-varcode-d96eb06/notes/public-somatic-validation-plan.md000066400000000000000000000036471526602674500247300ustar00rootroot00000000000000# Public somatic variant validation ## Scope Add complete, version-pinned public HCC1395 and COLO829 benchmark call sets (small variants and published structural variants), and published canine somatic variants with independent consequence annotations. Keep original source identity, confidence, assembly, transcript release and licensing. 1. Identify primary releases and record checksums, row counts and variant types. Use complete truth call sets, including variants outside high-confidence intervals; preserve confidence categories without treating candidate calls as validated truth. Explain any unavailable source explicitly. 2. Add reproducible data provisioning and fixtures appropriate for routine CI. Exercise every loaded allele in the complete datasets, accounting for source rows, duplicates and paired breakends; no silent dropped records. 3. Annotate every benchmark variant against the correct reference. Report errors and unresolved annotations by reason; fix Varcode defects found. 4. Compare canine consequences to published transcript-specific annotations on matching reference versions. Retain and explain all disagreements; never generate the expected labels with Varcode itself. 5. Run focused and full tests, lint, CI, merge and release a patch after 10.18.0. ## Boundaries Varcode implementation only. File upstream reference or dataset problems in the appropriate repository. Public truth sets are a reproducible definition of the requested benchmark coverage, not every historical caller output for these cell lines. Primary sources and pinned versions define completeness. ## Provisioning adjustment The initial CI cache key caused all six releases to download. Reuse main's verified four-release cache (same requirements hash and source-layout version) as a fallback, then install the two new releases and save the expanded cache. Keep the source URLs and explicit reference installation unchanged. openvax-varcode-d96eb06/notes/python-311-plan.md000066400000000000000000000031621526602674500215000ustar00rootroot00000000000000# Python 3.11 support floor (#607) ## Specification Require Python 3.11 or newer without a future upper bound. Keep the existing pandas 2.2/3 compatibility policy and test both on Python 3.11; add Python 3.13 and 3.14 pandas 3 jobs while retaining the Python 3.12 job. Update current support claims and release notes, then bump the patch version to 10.17.2. Preserve variant annotation behavior and dependency bounds. ## Plan - [x] Read issue, package metadata, CI, release guidance, and current docs. - [x] Check scope before implementation: change interpreter support policy without altering pandas or genome behavior. - [x] Update metadata, CI, support docs, changelog, and version. - [x] Install the source with optional test dependencies on Python 3.11 and 3.14; verify dependency resolution. - [x] Run `./lint.sh` and `./test.sh` with writable Ensembl cache copies and review source-distribution contents. - [ ] Open PR, pass CI, merge, deploy from clean main, verify PyPI and tag. ## Review `./lint.sh` passed. The full suite passed 3,446 tests with one skip on Python 3.11.12 and Python 3.14.6. The first 3.11 run found that this isolated venv lacked the repo's documented `setuptools>=64` test prerequisite; after adding it, the source-distribution test and full suite passed. Both environments resolve the optional Osteosarc and ORF dependencies, and dependency checks pass. After upgrading to released PyEnsembl 2.28.1, the documented CFTR example returns `p.L159M` on both interpreters. Installed metadata reports varcode 10.17.2 and `Requires-Python: >=3.11`. The source-distribution integrity test passed in the full suite. openvax-varcode-d96eb06/notes/reference-orfs-integration-plan.md000066400000000000000000000066661526602674500251170ustar00rootroot00000000000000# Reference DNA, species and ORF integration ## Scope One Varcode PR addresses #610, #611, CLI reference/ORF access, and #593. Keep ordinary annotations and existing input formats compatible. 1. Use the variant genome's reference DNA as the default fallback for ORF calculations, preserving bound transcript sequence and explicit providers. 2. Select translation codes from explicit source metadata and authoritative species/compartment assignments. Support the needed NCBI codes; reject unknown assignments instead of silently assuming vertebrate mitochondria. 3. Expose chromosome FASTA and ORF evidence selection in the CLI with early validation. Explain one reference setup path and sequence limitations. 4. Add versioned, lossless ORF effect/collection serialization, including failed bindings, alternative products, reference identity and evidence. Deserialization must not require a catalogue download or reannotation. Preserve ordinary JSON/CSV compatibility. ## Verification - Regression tests for native and overridden DNA, both strands, missing DNA, splice/SV products, and collection/haplotype calls. - Species/code tests against NCBI definitions, including human/mouse, invertebrate and yeast mitochondria, explicit overrides and unknown codes. - CLI tests exercise actual parsing, annotation and exported ORF identity. - ORF round trips cover mixed collections, failures, source ID collisions, phased outcomes and portable reference reconstruction. - Run lint and the full suite, obtain green CI, merge and deploy from clean main. ## Dependency scope Inspect PyEnsembl/ORFdata APIs and report missing capabilities upstream. Implementation and release work remain in Varcode. ## Implementation and checks - The ORF adapter now defaults to the existing genome sequence provider. Tiny GTF/RNA/DNA fixtures exercise native and overridden DNA on both strands, intronic edits, donor disruption, local SVs and known-cis groups. - NCBI gc.prt 4.6 plus Taxonomy EUtils supply the packaged, hashed offline snapshot. Explicit models override species/compartment defaults. Unknown assignments and context-dependent stop/sense tables remain explicit errors. PyEnsembl's missing metadata contract is filed as openvax/pyensembl#481. Broader ORF binding-code coverage remains upstream in openvax/orfdata#57. - Reuse the existing effect graph for ORF dataclasses and validated public ORFdata record constructors. Snapshot full RNAs so reference reconstruction does not consult a catalogue or annotation DB. Incomplete fusion partners retain only defined start/stop features. - ORF-containing CSVs carry each effect graph beside the summary. Import rejects missing payloads and edited identifying columns. - First full run: 3,512 passed; one documentation test stopped before the export block because the new setup snippet changed the fixed block count. Corrected the count; the documentation regression passes independently. - Focused reference/ORF/CLI/species tests: 73 passed before the final scoped evidence/multiple-placement addition; that addition also passes independently. - Source and wheel build successfully; wheel import verifies that the NCBI data is packaged. Lint and shared-environment `pip check` pass. - Final full suite: **3,520 passed**, 64 existing warnings, 93% coverage. This includes exact-assembly species resolution for mirrored human/yeast references and explicit rejection of an unregistered mitochondrial reference. openvax-varcode-d96eb06/notes/structural-performance-plan.md000066400000000000000000000111511526602674500243610ustar00rootroot00000000000000# Structural annotation performance (#407) ## Scope Reproduce the reported ESVEE breakend and large-span slowdowns on current Varcode using the existing public Osteosarc callsets and Ensembl 115 cache. Keep this investigation and any implementation changes within Varcode. ## Method 1. Record exact source records, versions, cold/warm timings, and Python/SQL profiles for the named GNAS/CNOT3 breakends and a large typed event. 2. Identify repeated or unnecessarily broad work. Preserve transcript and candidate coverage, biological interpretations, and uncertainty semantics. 3. If the bottleneck is in Varcode, implement the smallest measured correction with output-parity and behavioral regression tests. Report dependency bottlenecks separately for the owning agent. 4. Record before/after evidence. For a code fix, bump the version, update release notes, pass lint/full tests and CI, merge, and deploy from clean main. ## Acceptance Explain the observed cost with a reproducible profile. Any performance fix must preserve complete effect output on the benchmark inputs and avoid a new global unbounded cache or silently reduced annotation scope. ## Findings and implementation The original minute-long cases do not reproduce on current main (`cb49a63`, 10.17.2), PyEnsembl 2.28.1, Datacache 1.29.0, Python 3.12.6. Earlier commit `07873df` already reduced the reference-range fallback to one locus query and cached variant-level mate lookups. The remaining hotspot is repeated `spliced_offset` calls inside that range: every base is mapped through every overlapping transcript, repeatedly walking the same exons. A coverage-only early rejection was insufficient: some breakpoint windows are fully covered by complementary transcript exons. The final implementation reads each intersecting exon as a contiguous slice, combining answers in the same transcript order and retaining disagreement diagnostics. Uncovered unions return empty immediately; missing/short sequences and adapters without usable exon intervals retain the existing all-or-nothing behavior and fallback. Endpoint offsets now reject intron-spanning slices before loading cDNA. Coordinate and sequence contracts were checked against the installed PyEnsembl `Transcript.exon_intervals`, `spliced_offset`, and `sequence` implementations and the [primary API documentation](https://pyensembl.readthedocs.io/en/latest/pyensembl.html#module-pyensembl.transcript). No biological prediction or candidate-selection rule changes. ### Measurements Same local ESVEE files, Ensembl 115, no chromosome FASTA; one unprofiled run per checkout. Cache state and machine load affect timing, particularly the first case, which loads transcript/protein dictionaries. These are illustrative measurements, not portable timing assertions. | Input | Before (s) | After (s) | Transcript effects | | --- | ---: | ---: | ---: | | CNOT3 19:54153839 → 54155308 (first, dictionaries unloaded) | 3.030 | 1.609 | 24 | | CNOT3 19:54155308 → 54153839 | 0.355 | 0.042 | 24 | | 20:36148058 → 36148629 | 0.028 | 0.013 | 51 | | GNAS 20:58909683 → 58909577 | 0.692 | 0.039 | 63 | | INV 2:25955847–63053516 (37 Mb) | 1.148 | 1.131 | 6,373 | All five complete serialized effect graphs compare equal after removing only the annotation timestamp and Varcode version. This includes structural primary, cryptic and splice candidates, model sequences, and evidence. The large inversion continues to annotate every transcript in its span; its cost is proportional to that output, not a reason to silently switch to breakpoint-only annotation. The GNAS profile falls from 100,386 to 904 `spliced_offset` calls. Its reference lookup falls from 2.135 s to 0.055 s under cProfile. Regression tests check work counts instead of wall time: the uncovered-flank case drops from 40,060 offset calls to 40 on each strand. The old implementation fails that test and the test that forbids loading cDNA for an already-proven intron-spanning range. ### Reproduction `benchmark_structural_407.py` accepts the public T1 organoid and T2 tumor ESVEE somatic VCFs from [Osteosarc](https://osteosarc.com/data/), installed human Ensembl 115, and an output directory. It records exact source SHA256s, dependency versions, repeated timings, and complete effect graphs. `--profile` also writes cProfile dumps and summaries. Compare the generated JSON graphs across checkouts; run benchmarks separately from the test suite to limit interference. ```sh PYTHONPATH=/path/to/checkout python notes/benchmark_structural_407.py \ IPISRC044_tumor_T1_organoid_ucla.esvee.somatic.vcf.gz \ IPISRC044_tumor_T2_ucla.esvee.somatic.vcf.gz /tmp/varcode-407-results --profile ``` openvax-varcode-d96eb06/notes/uorf_implementation_plan.md000066400000000000000000000114641526602674500240230ustar00rootroot00000000000000# General ORF reference and evidence layer Status: architectural correction to the unmerged oncoref/Varcode/Isovar PRs. Working distribution name: orfdata. No release is authorized by this document. ## Ownership PyEnsembl remains responsible for reference genomes, annotations, transcripts, exons and canonical proteins. A small separate library adds source-defined ORFs on those transcripts and their experimental evidence. oncoref remains a consumer for cancer-specific analysis. Neither Varcode nor Isovar owns reference datasets. PepData's immune-epitope/property scope is not a fit. The new library has a dependency-light data layer and a PyEnsembl-compatible binding API. It must not import Varcode, Isovar or oncoref. No new PyEnsembl core API is needed to bind a Transcript; avoid reverse dependencies or monkey-patching. ## Public objects - ORFModel: immutable source ORF identity, assembly, species (NCBI taxon ID and scientific name), translation table, parent transcript, spliced coding blocks, protein sequence, ORF class. ORF class is distinct from transcript biotype. - ORFEvidence: ribosome observations and peptide-level MS observations with their source, accession, mapping ambiguity, assay and quality. Native evidence does not become evidence for a modified product; orthology is not direct evidence. - ORFRecord: model plus evidence, retaining legacy flat accessors for Isovar. - ReferenceORF: validated binding of a record to a PyEnsembl Transcript, retaining the original transcript (including noncoding biotype), cDNA coding interval, coding sequence and actual resolved annotation/version. Multiple ORFs per RNA are normal. Annotation lookup never relabels the transcript protein-coding. - ORFCatalog: explicitly one species/assembly per catalogue. Query by transcript, gene, locus, class or evidence. for_transcript(transcript) validates and returns ReferenceORFs. Load custom versioned catalogues offline. The initial bundled human data is identified as human-only; no implied mouse evidence. - Varcode ORFAnnotation: reference ORF plus effect and realized products. Exports separately identify reference model/evidence and modifications, preserving phase/splice alternatives. A private transcript-engine adapter permits reuse of existing variant logic without exposing a fake transcript as the base ORF. - Isovar: attach those same annotations to RNA products with candidate-specific RNA support, maintaining conditional translation and unresolved RNA polarity. ## Generality and compatibility Support uORF, overlapping uORF, internal/out-of-frame, downstream and noncoding- transcript ORFs with the same model and evidence schema. Preserve source classes verbatim rather than equating them with Ensembl transcript biotypes. Complete stopped ORFs are the initial binding contract; partial models are rejected explicitly rather than given invented boundaries. A generic v1 schema requires species, assembly, translation table and source provenance. Legacy Isovar human data may be read only through its explicit known human/GRCh38 provenance. Keep legacy Isovar uorf_* imports/call shapes working. New generic APIs use orf terminology; defaults remain explicit about dataset scope. Multi-species path: one versioned catalogue per organism/assembly, importer input using the same model/evidence schema, optional organism-specific source adapters (e.g. OpenProt). Test a mouse noncoding transcript with real PyEnsembl objects, plus human coding transcripts, different frames, minus strand, splice junctions, assembly/species mismatches and unknown translation tables. Synthetic examples are identified as test fixtures, never scientific observations. OpenProt already publishes multiple species with Ensembl/RefSeq mappings and evidence filters. ## Review corrections Remove the oncoref runtime dependency from both consumers and withdraw its ORF PR when the replacement is reviewable. Keep unrelated lint fixes independently. Review evidence retention for duplicate reference codons (retain all copies), explicit translation tables and noncoding parents; preserve all Sid alternatives. Document unsupported de novo initiation discovery and partial ORFs honestly. ## Verification New library: existing evidence/context/builder tests, generic data round-trip, PyEnsembl binding and multi-species isolation, independently installed wheel. Varcode/Isovar: focused tests then required full lint/test scripts. Regenerate Sid outputs from the original packaged reads. Existing human evidence counts and native peptide attribution must remain unchanged. Do not claim release until CI, merge and PyPI publication actually complete; publication remains unapproved. Primary sources: - https://www.nature.com/articles/s41587-022-01369-0 - https://www.nature.com/articles/s41586-026-10459-x - https://openprot.org/p/help - https://github.com/openvax/pyensembl openvax-varcode-d96eb06/pyproject.toml000066400000000000000000000032771526602674500201760ustar00rootroot00000000000000[build-system] requires = ["setuptools>=64"] build-backend = "setuptools.build_meta" [project] name = "varcode" requires-python = ">=3.11" authors = [{name = "Alex Rubinsteyn", email = "alex.rubinsteyn@unc.edu"}] description = "Variant annotation in Python" classifiers = [ "Development Status :: 4 - Beta", "Environment :: Console", "Operating System :: OS Independent", "Intended Audience :: Science/Research", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Topic :: Scientific/Engineering :: Bio-Informatics", ] readme = "README.md" dynamic = ["version", "dependencies"] [tool.setuptools.dynamic] version = {attr = "varcode.version.__version__"} dependencies = {file = ["requirements.txt"]} [tool.setuptools.packages.find] include = ["varcode", "varcode.*"] [tool.setuptools.package-data] "varcode.cli" = ["logging.conf"] "varcode" = ["data/*.json"] [project.urls] "Homepage" = "https://github.com/openvax/varcode" "Bug Tracker" = "https://github.com/openvax/varcode/issues" [project.optional-dependencies] orf = ["orfdata[pyensembl]>=0.34.0,<1"] uorf = ["orfdata[pyensembl]>=0.34.0,<1"] rna = ["pysam>=0.21"] test-data = ["osteosarc>=0.14.4,<0.16", "biopython>=1.85"] [project.scripts] varcode-genes = "varcode.cli.genes_script:main" varcode = "varcode.cli.main:main" [tool.pytest.ini_options] markers = [ "parity: fast vs protein_diff byte-for-byte parity harness (tests/test_protein_diff_parity.py). Runs by default; also selectable with `pytest -m parity`.", ] [tool.ruff.lint] select = ["E", "F"] ignore = ["E501", "F841", "E731", "E741", "E722", "E721"] [tool.ruff.lint.per-file-ignores] "__init__.py" = ["F401"] "test*/*" = ["F401"] openvax-varcode-d96eb06/requirements.txt000066400000000000000000000002151526602674500205330ustar00rootroot00000000000000numpy>=2.0.0,<3.0.0 pandas>=2.2.2,<4.0.0 pyensembl>=2.30.0 gtfparse>=3.0.2,<4.0.0 memoized_property>=1.0.2 serializable>=1.1.0 sercol>=1.0.3 openvax-varcode-d96eb06/test.sh000077500000000000000000000066061526602674500165770ustar00rootroot00000000000000#!/usr/bin/env bash # Run the varcode test suite with a memory- and CPU-aware pytest-xdist # worker count. Running several suites concurrently can exhaust memory, # so we cap workers at min(cpu_reserve, available_RAM / PER_WORKER_GB). # xdist is optional — fall back to serial pytest when it isn't installed. # # Tunables (env vars): # PER_WORKER_GB per-worker memory budget in GB (default: 1.5) # TEST_SH_MIN floor on workers (default: 1) # TEST_SH_MAX hard ceiling on workers (default: unset) set -eo pipefail PER_WORKER_GB="${PER_WORKER_GB:-1.5}" TEST_SH_MIN="${TEST_SH_MIN:-1}" TEST_SH_MAX="${TEST_SH_MAX:-0}" log() { printf '[test.sh] %s\n' "$*" >&2; } case "$(uname -s)" in Darwin) OS=macos ;; Linux) OS=linux ;; *) OS=unknown ;; esac cpu_count() { local n="" if command -v getconf >/dev/null 2>&1; then n=$(getconf _NPROCESSORS_ONLN 2>/dev/null || true) fi if [[ -z "$n" && "$OS" == "macos" ]]; then n=$(sysctl -n hw.logicalcpu 2>/dev/null || true) fi if [[ -z "$n" && -r /proc/cpuinfo ]]; then n=$(grep -c '^processor' /proc/cpuinfo 2>/dev/null || true) fi if [[ -z "$n" || "$n" -lt 1 ]]; then n=1; fi echo "$n" } cpu_cap() { local c="$1" if (( c <= 1 )); then echo 1 elif (( c <= 3 )); then echo $(( c - 1 )) else echo $(( c - 2 )) fi } mac_available_bytes() { local page_size page_size=$(sysctl -n hw.pagesize 2>/dev/null) || return 1 vm_stat 2>/dev/null | awk -v ps="$page_size" ' /Pages free/ { gsub(/\./, "", $3); free = $3 } /Pages inactive/ { gsub(/\./, "", $3); inactive = $3 } /Pages speculative/ { gsub(/\./, "", $3); spec = $3 } END { print (free + inactive + spec) * ps } ' } linux_available_bytes() { [[ -r /proc/meminfo ]] || return 1 awk ' /^MemAvailable:/ { print $2 * 1024; found=1; exit } END { if (!found) exit 1 } ' /proc/meminfo } available_bytes() { case "$OS" in macos) mac_available_bytes ;; linux) linux_available_bytes ;; *) return 1 ;; esac } CPUS=$(cpu_count) CPU_CAP=$(cpu_cap "$CPUS") avail="" if avail=$(available_bytes 2>/dev/null) && [[ -n "$avail" ]]; then MEM_CAP=$(awk -v b="$avail" -v g="$PER_WORKER_GB" 'BEGIN { n = int(b / 1024^3 / g) if (n < 1) n = 1 print n }') AVAIL_GB=$(awk -v b="$avail" 'BEGIN { printf "%.1f", b / 1024^3 }') mem_note="ram_free=${AVAIL_GB}GB mem_cap=${MEM_CAP}" else MEM_CAP=$CPU_CAP mem_note="ram_free=? (probe unavailable) mem_cap=cpu_cap" fi if (( CPU_CAP < MEM_CAP )); then WORKERS=$CPU_CAP; else WORKERS=$MEM_CAP; fi if (( TEST_SH_MAX > 0 && WORKERS > TEST_SH_MAX )); then WORKERS=$TEST_SH_MAX; fi if (( WORKERS < TEST_SH_MIN )); then WORKERS=$TEST_SH_MIN; fi XDIST_FLAGS=() if python -c "import xdist" 2>/dev/null; then XDIST_FLAGS=(-n "$WORKERS") log "platform=${OS} cpus=${CPUS} cpu_cap=${CPU_CAP} ${mem_note} per_worker=${PER_WORKER_GB}GB" log "workers=${WORKERS} → exec python -m pytest -n ${WORKERS} --cov=varcode/ --cov-report=term-missing tests $*" else log "platform=${OS} cpus=${CPUS} (pytest-xdist not installed; running serial)" log "→ exec python -m pytest --cov=varcode/ --cov-report=term-missing tests $*" fi exec python -m pytest "${XDIST_FLAGS[@]}" --cov=varcode/ --cov-report=term-missing tests "$@" openvax-varcode-d96eb06/tests/000077500000000000000000000000001526602674500164135ustar00rootroot00000000000000openvax-varcode-d96eb06/tests/README.md000066400000000000000000000545141526602674500177030ustar00rootroot00000000000000# Test data and reproducibility Keep small synthetic fixture constructors in the test module or a shared helper under `tests/`. Document the expected behavior and why the inputs exercise it. For captured public data, check in the acquisition/subsetting recipe, source identifiers, reference release, and integrity hashes with the fixture. Do not make an untracked local cache or sibling checkout a test requirement. RNA fixture harmonization is tracked in [#464](https://github.com/openvax/varcode/issues/464). See [CONTRIBUTING.md](../CONTRIBUTING.md) for environment and reference setup. During pytest, bare GRCh38 references (including aliases) use the suite's Ensembl 81 fixture. Selection does not download data, probe newer releases, or modify the reference cache. Install the documented datasets before annotation tests; explicit release choices remain available. The offline guarantees below apply to the bundled Osteosarc snapshot checks. ## Source-distribution contract The source archive ships the complete test Python package, the bounded `tests/data/` tree with its [checksum inventory and provenance](data/README.md), and the documentation, Python examples, shell scripts and configuration used by those tests. `MANIFEST.in` declares these inputs; generated bytecode is excluded. Wheels contain the runtime library, without the development tests. Large reference datasets and the shared RNA bundle remain external, installed through the documented PyEnsembl/Osteosarc mechanisms. `tests/test_source_distribution.py` checks every bundled fixture hash and builds an archive to verify that all test support files retain their bytes. It needs `setuptools>=64`, as installed by the contributor setup. The build uses the installed backend without downloading build dependencies. CI builds and unpacks the source distribution outside the checkout, installs that source with the `test-data` extra, and runs the full suite and parity checks from the unpacked directory on Python 3.11–3.14. Python 3.11 tests both pandas 2 and 3; Python 3.12–3.14 and the parity gate use pandas 3. All jobs require gtfparse 3.0.2+. To reproduce this locally, first install the development tools and reference releases from [CONTRIBUTING.md](../CONTRIBUTING.md), then: ```sh python -m build --sdist --outdir /tmp/varcode-dist mkdir /tmp/varcode-sdist tar -xzf /tmp/varcode-dist/varcode-X.Y.Z.tar.gz --strip-components=1 -C /tmp/varcode-sdist cd /tmp/varcode-sdist python -m pip install '.[test-data]' ./test.sh ``` Use a new output/unpack directory and replace `X.Y.Z` with the built version. The checkout is not needed after the archive is built. The same reference and optional-dependency requirements apply to checkout and archive tests; the shared RNA checks can download the pinned bundle on first use. ## Osteosarc test variants `tests/data/osteosarc_variants.json` contains 179 ready site variants and three unresolved entries collected through `osteosarc==0.14.4` from the pinned public snapshot below. The ready variants comprise 158 SNVs, 14 deletions, four insertions, and three complex alleles. Original alleles, assemblies, source IDs, correction IDs, source receipts, and hashes of complete osteosarc entries are retained. The fixture is approximately 122 KiB; large count and peptide tables remain in the source snapshot. Use the collection in tests without installing osteosarc or opening its cache: ```python from tests.osteosarc_variants import load_variants, read_fixture variants = load_variants() # native VariantCollection, GRCh38 / Ensembl 81 for variant in variants: source_entries = variants.metadata[variant]["entries"] unresolved = [entry for entry in read_fixture()["entries"] if entry["status"] != "ready"] ``` The ordinary suite annotates every ready allele with both `fast` and `protein_diff` and checks for errors or unresolved annotator results. These are real-input coverage checks; they do not certify source somatic status or use source protein labels as expected consequences. Dedicated regressions still provide independent expected effects. The mitochondrial allele keeps its original `chrM` spelling; Varcode's explicit UCSC conversion maps it to `MT`. Osteosarc 0.1.4's native `to_varcode` adapter uses the same conversion and preserves the source name. Corrected MAP2 and its separately published split representation retain distinct source IDs and correction notes; they are not assumed to be independent events. ## Regenerate from the verified snapshot The fixture records Osteosarc 0.14.4. To reproduce it byte for byte, install that version, unpack the bundled snapshot into a new cache directory, then collect offline: ```sh python -m pip install -e . 'osteosarc==0.14.4' python -m zipfile -e tests/data/osteosarc_snapshot_2026-09-18t.zip /path/to/new/cache python -m tests.collect_osteosarc_variants \ --cache /path/to/new/cache --snapshot 2026-09-18t ``` The exporter checks the package version and snapshot identity before writing the fixture. `--output` selects a different destination. To collect a new snapshot deliberately, supply its identity with `--expected-snapshot-id`, review the changed data, and update the test pins. Source acquisition remains separate from export and tests. ## Snapshot integration checks The targeted GPX4 and BRCA1 regressions use Ensembl 81. Offline integration checks use the public osteosarc dataset through the published osteosarc 0.14.x/0.15.x adapter (`>=0.14.4,<0.16`) from the optional `test-data` extra (Python 3.11+): ```sh python -m pip install -e '.[test-data]' python -m pytest -q tests/test_osteosarc_dataset.py ``` These five tests run in the ordinary suite and in CI on Python 3.11–3.14. They unpack `tests/data/osteosarc_snapshot_2026-09-18t.zip` into a pytest temporary directory, check the archive SHA256, and open it offline through Osteosarc, which verifies all source objects against their receipts. No sibling checkout, pre-existing Osteosarc cache, or environment variables are required. As elsewhere in the suite, Ensembl 81 reference data must already be installed; CI provisions this in its existing reference-data step. The archive contains the unchanged snapshot manifest and its 20 public metadata objects in the standard OpenVax cache layout. It is approximately 3.6 MiB compressed (53 MiB unpacked), with SHA256 `cbb688ca5cbe775fc4e6a826124f861d6d605c65e34473582c5d86853a6018fa`. The manifest retains original URLs, acquisition timestamps, sizes and hashes. It contains no BAMs, generated reads, local cache bookkeeping, or absolute paths. Its snapshot identity remains `9b34ea0e13f9c1c35c3c88b7e646c0e608b86a143dee0e668bf3f74b909f815c`. To test an existing shared cache explicitly, set both `OSTEOSARC_TEST_CACHE=/path/to/shared/cache` and `OSTEOSARC_TEST_SNAPSHOT=2026-09-18t`. The name may differ, but the content identity must match. Missing packages, objects, or a mismatched snapshot then fail; the tests never fall back to the bundled snapshot. Without an explicit snapshot, these checks skip only when the optional Osteosarc dependency is absent. The supported Osteosarc range covers every Python version in the CI matrix. These snapshot integration tests never download data or refresh sources. `Dataset.sync` acquires new snapshots separately; current remote sources do not reproduce the historical identity. Change this fixture only by deliberately reviewing a new snapshot and updating its source provenance, identity, archive hash, and expectations. These additional checks compare every ready fixture allele with the dataset and verify native conversion of all 179 ready entries in the snapshot (182 total). Osteosarc 0.7.0's allele corrections resolve two entries the original 0.1.0 export left unresolved, `COL3A1-Splice` and `FAM157A-p_W70_Q71ins_14`; their correction IDs are recorded in the fixture. Checks preserve source provenance and reference identity, exercise mitochondrial annotation and the corrected MAP2 allele, reopen the shared cache offline, and reject missing or tampered objects using a private copy. Package updates do not change the pinned snapshot identity. They do not treat upstream peptide annotations or vaccine membership as a protein oracle, or change the existing RNA-read fixtures. Broader RNA fixture adoption is tracked in [#464](https://github.com/openvax/varcode/issues/464). Dataset: [osteosarc.com](https://osteosarc.com/data/), snapshot acquired 2026-09-18, collected 2026-09-21; public data listed as CC0-1.0 by the [AWS Open Data Registry](https://registry.opendata.aws/sid-osteosarc/). ## Shared test data (openvax-v2) The OpenVax libraries share one set of Sid test data, `openvax-v2`, published by osteosarc 0.14.4 ([iskandr/osteosarc#56](https://github.com/iskandr/osteosarc/issues/56)). It replaced `openvax-v1` (osteosarc 0.11) with the same members; Varcode's reads and small-variant targets are identical in both. Its recipe lists every target the libraries test. `tests/test_osteosarc_shared_targets.py` checks that its 179 current catalogue alleles are exactly the ready alleles of `tests/data/osteosarc_variants.json`. It then annotates all 187 small-variant targets with both annotators, which must agree on every transcript. The other 8 targets are alleles the other libraries depend on: - the historical MAP2 deletion and NTF3's compound substitution; - three count-export indels (ACSL6, EPPK1, KTN1); - NR2F2 at its GRCh37 position; - MT_ND5 in rCRS (`MT:12994`) and hg19 (`chrM:12995`) coordinates. hg19's `chrM` is not the rCRS sequence Ensembl uses, so Varcode raises `ReferenceMismatchError` for the hg19 coordinate rather than annotate a different base; the test pins that. The recipe's 12 SV targets are tested with the esvee calls behind them. Six come from `tests/data/osteosarc_esvee_somatic.vcf` and esvee alleles written into the fusion tests. `tests/test_osteosarc_shared_svs.py` covers the other six with the records in `tests/data/osteosarc_esvee_shared_svs.vcf`, checking the genes at each breakend against LINX's. That module needs no download. `tests/test_osteosarc_shared_fusions.py` builds each of the recipe's seven fusion targets as a breakend record from its breakends. Where a target gives `orientation` rather than `retained_side`, the side comes from the strand the fusion follows ([iskandr/osteosarc#96](https://github.com/iskandr/osteosarc/issues/96)). The five junctions esvee called must match its records. ATP5MG--KMT2A and TPST1--CRCP must fuse every coding isoform of the 5' gene, and ATP5MG--KMT2A's fusion protein must be the one Isovar translates from its junction reads. The other five have no sense-oriented coding partner, so the 5' gene keeps its 5' fragment. GABBR1--SLC29A1's junction lies 597 bases before SLC29A1 starts, so SLC29A1's isoforms follow that fragment as fusion candidates (#550). Five targets have newer IDs than the fixture's pinned 2026-09-18 snapshot gives them, but the same alleles. For example, the fixture's `CABLES1-chr18-23135500` is the recipe's `CABLES1-chr18-23135764`. Varcode stores no reads of its own. Each record in `tests/data/osteosarc_observed_junctions.json` keeps a 40-base junction window and its annotations, and the `openvax-v2` member `varcode/osteosarc_observed_junctions.json#