Alphagenome Variant Impact Score
Google DeepMindOfficialPublished by Google DeepMind
- Skill
- Tool integration
- Science and research
- Apache-2.0
- Low risk
- Has scripts
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
The AlphaGenome Variant Impact (AVI) skill provides a programmatic interface to the AlphaGenome Atlas API for scoring, annotating, and analyzing the functional impact of genetic variants. It supports querying individual variants, annotating large callsets, performing saturation mutagenesis scans, and retrieving detailed gene structure information from GENCODE v46. The tool is strictly for research purposes and emphasizes molecular and functional interpretation over clinical diagnosis.
What it does
- Scores genetic variants using AVI models with Phred-scaled impact metrics.
- Annotates VCF and tabular variant files with functional impact data.
- Performs saturation mutagenesis window scans to identify high-impact hotspots.
- Retrieves GENCODE v46 gene, exon, and splice junction coordinates.
- Provides attribution weights for 18 biological feature modalities.
- Integrates with the AlphaGenome Atlas for deep-linking and track visualization.
When to use it
- When you need to prioritize genetic variants based on predicted functional impact.
- When analyzing the potential effect of variants on splicing or transcription factor binding.
- When performing large-scale variant annotation for research datasets.
- When investigating the functional consequences of mutations within a specific genomic window.
Advantages
- Provides standardized, calibrated Phred-scaled impact scores.
- Offers granular attribution to specific biological modalities like splicing or chromatin accessibility.
- Supports high-throughput annotation of large variant callsets.
- Ensures consistency by using built-in GENCODE v46 annotations.
Usage examples
- “Score the variant chr9:128225994:G>A and provide feature importance.”
- The agent executes the query command and returns the AVI Phred score, quantile, and the top contributing biological feature.
- “Annotate my variants.vcf file and output the top 20 candidates.”
- The agent runs the annotation script on the provided file, generating an annotated VCF and a JSON file containing the top-ranked variants.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/google-deepmind/alphagenome-variant-impact-score/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip alphagenome-variant-impact-score.zip -d .agents/skills
Documentation by Google DeepMind
AlphaGenome Variant Impact (AVI) Analysis
Score and prioritize genetic variants using AlphaGenome Variant Impact (AVI)
models via scripts/alphagenome_atlas_avi.py.
[!IMPORTANT] Research Use Only & Clinical Safety Rules: The AlphaGenome AVI Skill and the underlying AlphaGenome model/Atlas are strictly research tools. Access to outputs requires an AlphaGenome API key subject to terms of service prohibiting clinical use.
No Medical Advice or Clinical Diagnosis: You MUST NOT provide medical advice, clinical diagnoses, disease management strategies, or treatment recommendations based on outputs from this skill or the AlphaGenome Atlas.
Strict Molecular & Functional Framing: A high AVI score reflects predicted molecular/functional impact (e.g., disruption of splicing, alteration of transcription factor binding, chromatin accessibility changes, or coding consequences). Frame all findings in terms of molecular mechanisms and biological annotations—never as clinical diagnoses or medical conclusions.
No Diagnostic Leaps: Never extrapolate high functional impact to clinical disease causation, penetrance, or patient prognosis. If a user asks a clinical or diagnostic question, explicitly clarify that AlphaGenome is a research tool and restrict your answer to the predicted molecular and functional effects.
[!IMPORTANT] Always Use AlphaGenome GENCODE v46 GTF (
scripts/alphagenome_atlas_avi.py gtf) for Gene Annotations: When retrieving gene models, transcript IDs, exon coordinates, CDS/UTR regions, or splice junction donor/acceptor boundaries, always use the built-inscripts/alphagenome_atlas_avi.py gtfcommand. Do NOT query external sources (e.g., Ensembl REST API, UCSC, external GTF databases, or NCBI) for gene annotations or transcript coordinates. This ensures that the annotations match the scores and website.
Prerequisites
Run scripts/alphagenome_atlas_avi.py using uv run:
# Display CLI help:
uv run scripts/alphagenome_atlas_avi.py --help[!TIP] Agent & Programmatic Execution Format: When invoking the CLI in agent workflows, prefer
--format jsonor direct file export (-o <file>) for deterministic, structured parsing rather than extracting fields from stdout markdown tables.
1. Query Variants (query)
Query single or multiple variants in 1-based chr:pos:ref>alt format to inspect
scores and the 18 biological feature attribution weights:
# Query single variant with basic feature importances (stdout JSON preview):
uv run scripts/alphagenome_atlas_avi.py query "chr9:128225994:G>A" --format json
# Query variant with exact underlying Atlas track indices, biosamples, and target genes:
uv run scripts/alphagenome_atlas_avi.py query "chr9:128225994:G>A" --include_track_info --format json
# Query multiple variants and export full track info to a file (prevents stdout overflow):
uv run scripts/alphagenome_atlas_avi.py query "chr9:128225994:G>A" "chr22:36201698:A>C" \
--include_track_info --format json -o query_results.jsonOutput Size & Redirection: Single variant table/JSON queries are compact (~1.0–1.5 KB). When querying $>3$ variants or passing
--include_track_info --format json(which generates ~3.9 KB per variant), always export directly to a file using-o <file.json>or-o <file.tsv>to avoid exceeding context window limits.Typical Runtime: ~0.5–1.0 s per variant (~12–15 s total including environment startup). Single calls with $>100$ variants take $>1$ minute.
2. Annotate & Rank Variant Files (annotate)
Annotate a VCF (or CSV/TSV/Parquet) in standard Ensembl VEP CSQ format:
uv run scripts/alphagenome_atlas_avi.py annotate \
--input test_data/example_variants.vcf \
--output annotated_variants.vcf \
--top_k 20 \
--min_phred 15.0 \
--top_output top_variants.jsonOutput Size & Redirection:
annotatestreams the full callset directly to disk via--output(.vcf,.vcf.gz,.parquet,.tsv,.csv). Stdout displays a bounded summary (top candidate table + top 3 modality breakdowns, ~4.0–5.5 KB). Use--top_output <file.json>when downstream tools need machine-readable top candidate data.Typical Runtime & Callset Scaling: Throughput is ~10 variants/s (default) and ~5 variants/s (with track info). Small callsets ($\le 100$ variants) take ~15 s. Callsets $\ge 500$ variants execute silently for $>1$ minute (e.g., 1,000 variants take ~2–3 min; 10,000 variants take ~20 min).
3. Saturation Mutagenesis Window Scan (region)
Scan a 1-based closed genomic window (chr:start-end) to score all possible
single nucleotide substitutions ($3 \times N$ variants for an $N$-bp window):
uv run scripts/alphagenome_atlas_avi.py region \
--region chr9:128225990-128226000 \
--min_phred 15.0 \
--top_k 20 \
--output region_hotspots.tsvOutput Size & Redirection: Scanning a 100 bp window produces 300 SNVs (~42 KB TSV / ~105 KB JSON), while a 1,000 bp window produces 3,000 SNVs (~421 KB TSV / ~1.06 MB JSON). Always specify
--output <file.tsv|parquet>to save the complete dataset; stdout will only show a top-20 candidate preview.Typical Runtime: Queries precomputed dense scores over gRPC in 1.5–3.0 s for up to 1,000 bp (default
--max_window_size).
4. Inspect Atlas Database Metadata (metadata)
Dump or search registered scorers, the 18 biological feature definitions, or experimental track catalogs:
# List all registered scorers:
uv run scripts/alphagenome_atlas_avi.py metadata --scorers --format json
# List the 18 biological feature attribution modalities:
uv run scripts/alphagenome_atlas_avi.py metadata --features --format json
# Search experimental tracks by query keyword with a controlled preview (top 20):
uv run scripts/alphagenome_atlas_avi.py metadata --tracks --query "GATA1" --top_n 20
# Dump complete 9,440-track catalog to Parquet or TSV for offline search:
uv run scripts/alphagenome_atlas_avi.py metadata --tracks --output atlas_tracks.parquetOutput Size & Redirection:
--features(~1.3 KB) and--scorerstable (~1.7 KB) are safely under 4 KB. The experimental track catalog contains 9,440 tracks (~972 KB table / ~2.55 MB JSON). Never dump unfiltered tracks to stdout; always supply a narrow--query, specify--top_n 20, or export to--output atlas_tracks.parquet.Typical Runtime: ~1.5–2.5 s.
5. Inspect Gene Structure & Splice Junctions (gtf)
Query GENCODE v46 gene annotations, extract 1-based exon boundaries with donor/acceptor coordinates, and compute canonical and exon-skipping splice junction coordinates:
# Query MANE Select exon boundaries for a gene (stdout JSON preview):
uv run scripts/alphagenome_atlas_avi.py gtf --gene CAPN3 --exons --format json
# Query canonical and exon-skipping splice junctions for a variant locus:
uv run scripts/alphagenome_atlas_avi.py gtf --variant "chr15:42387805:C>G" --junctions --format json
# Extract CDS and UTR segments for an Ensembl transcript ID:
uv run scripts/alphagenome_atlas_avi.py gtf --transcript_id ENST00000397163.8 --cds --utr --format jsonOutput Size & Redirection: Single-gene MANE Select queries with
--exonsare compact (<2.5 KB). Always export to--output <file.tsv|parquet>or redirect (> gtf_out.json) when passing--all_transcripts(30–500 KB),--region($>50\text{ kb}$),--format json, or querying genes with $>30$ exons (e.g.TTN,DMD).Typical Runtime & Memory: Ingests the 318 MB GENCODE v46 feather dataset (~4.37 GB RAM). Cached queries execute in ~4–6 s. Cold-start runs downloading the file from GCS take ~5–10 s on corp/Cloudtop networks, but can take up to 60–90 s on external networks. Do not kill the process prematurely during initial download.
Programmatic Python SDK Usage
When querying AlphaGenome programmatically in Python:
Client Factory: Always instantiate the client using
atlas.create(api_key)— do NOT instantiateatlas.AtlasClient()directly (which requires an internal gRPC stub).Coordinate System:
genome.Intervaloperates with 0-based half-open indexing ([start, end)). When querying a 1-based closed intervalchr:start_1_based-end_1_based, passstart = start_1_based - 1andend = end_1_based.Single Variants:
genome.Variant.from_str("chr:pos:ref>alt")expects 1-based position coordinates.
import os
from alphagenome.atlas import atlas
from alphagenome.data import genome
import dotenv
dotenv.load_dotenv(os.path.expanduser('~/.env'))
client = atlas.create(os.environ['ALPHAGENOME_API_KEY'])
# Query 1-based closed interval chr11:5225727-5226575 using 0-based half-open [5225726, 5226575)
interval = genome.Interval(chromosome='chr11', start=5225726, end=5226575)
results = client.query_interval(
interval,
requested_scorers=['AVI_SCORE', 'AVI_SCORE_FEATURE_IMPORTANCE'],
)
# Query single variant with 1-based coordinate
variant = genome.Variant.from_str('chr11:5225488:A>T')
variant_scores = client.query_variant(
variant,
requested_scorers=['AVI_SCORE', 'AVI_SCORE_FEATURE_IMPORTANCE'],
)Output Schemas & Response Structures
1. VCF CSQ & INFO Tag Schema (annotate)
When writing annotated VCF files, the following annotations are injected:
Ensembl VEP
CSQFormat String:Allele|AVI_PHRED|AVI_RAW|AVI_QUANTILE|AVI_TOP_PERCENTILE|AVI_TOP_FEATURESubfield Definitions:
Allele(string): Alternate allele base(s) (e.g.A).AVI_PHRED(float): Calibrated Phred impact score ($\text{Phred} = -10 \cdot \log_{10}(1.0 - \text{quantile})$). Range:[0.0, ~70.0]. Higher = greater functional impact.AVI_RAW(float): Raw continuous model prediction.AVI_QUANTILE(float): Calibrated tail quantile ($1 - \text{CDF}$). Range:(0.0, 1.0].AVI_TOP_PERCENTILE(float): Top percentile of genome-wide SNVs ($10^{-\text{Phred}/10} \times 100%$, e.g.0.0380for Top 0.038%).AVI_TOP_FEATURE(string): Display name of the top contributing biological modality (e.g.Splicing,AlphaMissense,ChIP-TF,DNASE-seq,Cactus).
Standalone INFO Tags (unless
--vep_csq_only):AVI_PHRED=Float,AVI_RAW=Float,AVI_QUANTILE=Float,AVI_TOP_PERCENTILE=Float,AVI_TOP_FEATURE=String.
2. Tabular & JSON Record Schema (query, region, --top_output)
Output records exported to JSON, TSV, CSV, or Parquet contain the following fields:
rank(integer, present in--top_outputandregiontabular exports): 1-based candidate rank sorted by Phred descending.variant(string): Genomic variant string inchr:pos_1_based:ref>altformat (e.g.chr9:128225994:G>A, where coordinate is 1-based).chromosome(string): Contig name withchrprefix (e.g.chr9).position(integer): 1-based genomic coordinate.ref(string): Reference allele base(s).alt(string): Alternate allele base(s).avi_phred(float): Calibrated Phred-scaled score.avi_raw(float): Raw continuous model score.avi_quantile(float): Tail quantile ($1 - \text{CDF}$).top_percentile(float): Exact top percentile value (e.g.0.3421for Top 0.34%).top_modality(string): Display name of the top contributing biological modality (e.g.Splicing,AlphaMissense,ChIP-TF).top_feature_importance(float): Attribution weight (SHAP value) of the top modality.Optional Attribution Weights (
--include_features): 18 columnsfi_<MODALITY>(e.g.fi_MERGED_SPLICING,fi_ALPHAMISSENSE,fi_MAX_ABS_RNA_SEQ,fi_CACTUS_241_WAY).Optional Track Provenance (
--include_track_info):track_idx_<MODALITY>,track_name_<MODALITY>,track_biosample_<MODALITY>,track_gene_<MODALITY>.
3. GTF Query Schema (gtf)
When querying gene structure with gtf --format json, each transcript object
provides:
Gene & Transcript Metadata:
gene_name,gene_id,transcript_id,transcript_type,chromosome,start,end,strand,is_mane_select,num_exons(coordinates are 1-based closed).Exons Array (
--exons):exon_number(int): 1-based exon index in 5' to 3' transcript order.start,end,width(int): 1-based closed exon coordinates and length in bp.acceptor,donor(int): 1-based 5' splice acceptor and 3' splice donor coordinates.overlaps_variant(bool): True if overlapping the query mutation site.
Junctions Array (
--junctions):type(string):Canonical Intron {i}orExon {k} Skipping.upstream_exon,downstream_exon(int): 1-based flanking exon numbers in 5' to 3' transcript order.junction_start,junction_end(int): 1-based genomic donor and acceptor coordinates.intron_length(int): Intron length in bp.score_id(string): Constructed Atlas track comparison token.atlas_url(string): Clickable deep-link to Atlas track predictions.
CDS & UTR Arrays (
--cds,--utr): 1-based closedstart,end,width(int).
Score Metrics & Interpretation
AVI Phred: Calibrated score (
Phred = -10 * log10(1.0 - quantile)):Phred >= 40: Top 0.01% predicted impact of all genome-wide SNVs.Phred >= 30: Top 0.10% predicted impact of all genome-wide SNVs.Phred >= 20: Top 1.0% predicted impact of all genome-wide SNVs.Phred >= 15: Top 3.16% predicted impact of all genome-wide SNVs.Phred >= 10: Top 10.0% predicted impact of all genome-wide SNVs.Phred < 10: Bottom 90% of all genome-wide SNVs.
Top Percentile: Exact percentage of genome-wide SNVs with equal or greater impact:
Top Percentile = 10^(-Phred/10) * 100%(e.g. Phred 29.7 $\implies$ Top 0.11%).Top Modality: Leading biological feature attribution (
metadata --features).
Visualizations & Atlas Deep-Linking
[!IMPORTANT] Mandatory Atlas Deep-Linking with Variant Scores: Whenever reporting, discussing, or displaying an AVI score for a variant (whether for a single variant query, a ranked candidate table, or a genomic region scan), you MUST always provide a clickable deep-link to the AlphaGenome Atlas for each variant.
User Context Prioritization: Always prioritize context from the user first (e.g., disease, tissue, or relevant cell types). When absent, use
--include_track_infowithqueryto discover the driving biosample, cell line, ontology CURIE, and track index.Mandatory Splicing & RNA-seq Co-Plotting Rule: Whenever plotting or visualizing splicing variants, always plot continuous RNA-seq expression alongside splicing tracks (
SPLICE_JUNCTIONS,SPLICE_SITE_USAGE,SPLICE_SITES) to evaluate both structural splice disruption and resulting transcript abundance changes.Atlas Link Construction: Use the
alphagenome-atlas-website-linksskill (scripts/alphagenome_atlas_links.py) for all URL generation, layout configuration (lItems), biosample filtering (f), and/atlas/track-predictionscomparison chart links.
Files
- ATTRIBUTION.mdAdded by AutoGineer946 B
- LICENSELicence file added by AutoGineer11.1 KiB
- SKILL.md16.3 KiB
- pyproject.toml314 B
- scripts/alphagenome_atlas_avi.pyScript71.1 KiB
scripts/alphagenome_atlas_avi.py
# Copyright 2026 Google LLC
#
# 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.
"""AlphaGenome Variant Impact (AVI) CLI.
Provides unified subcommands:
1. `annotate`: Annotate input VCF/tabular variant files in Ensembl VEP CSQ
format and rank hits.
2. `query`: Query one or more variant strings to get scores, full 18-modality
feature importances, and underlying track attributions.
3. `region`: Perform in silico saturation mutagenesis across a genomic window
and rank variants.
5. `gtf`: Query GENCODE v46 gene annotations, extract exons, CDS, UTRs, and
compute canonical and exon-skipping splice junction coordinates to build exact
Atlas track prediction `ScoreId` tokens.
Powered by the AlphaGenome Atlas API (`alphagenome.atlas.atlas`) and
AlphaGenome GENCODE v46 annotations.
Usage:
uv run alphagenome_atlas_avi.py query chr9:128225994:G>A
uv run alphagenome_atlas_avi.py query chr9:128225994:G>A --include_track_info
uv run alphagenome_atlas_avi.py annotate -i variants.vcf -o annotated.vcf
uv run alphagenome_atlas_avi.py region -r chr9:128225900-128226050
uv run alphagenome_atlas_avi.py metadata --scorers
uv run alphagenome_atlas_avi.py metadata --tracks --query "GATA1"
uv run alphagenome_atlas_avi.py metadata --features
uv run alphagenome_atlas_avi.py gtf --gene CAPN3 --exons
uv run alphagenome_atlas_avi.py gtf --variant chr15:42387805:C>G --junctions
"""
import argparse
from collections.abc import Mapping, MutableSequence, Sequence
import dataclasses
import enum
import json
import math
import os
import re
import sys
from typing import Any
from alphagenome.atlas import atlas
from alphagenome.data import genome
import anndata
import dotenv
import numpy as np
import pandas as pd
import polars as pl
import tabulate
_ATLAS_TRACK_PREDICTIONS_URL = (
'https://deepmind.google.com/science/alphagenome/atlas/track-predictions'
)
@dataclasses.dataclass(frozen=True, kw_only=True)
class TrackAttribution:
"""Detailed provenance for an assay track driving a feature maximum.
Attributes:
scorer: Name or identifier of the scorer modality.
track_index: Index of the assay track.
track_name: Name or description of the track.
biosample_name: Name of the biosample, cell line, or tissue.
ontology_curie: Ontology CURIE for the biosample (e.g. UBERON or CL term).
raw_score: Raw attribution score or effect size for this track.
gene_name: Optional HGNC gene symbol associated with the track.
gene_id: Optional Ensembl gene ID associated with the track.
details: Summary string describing the maximum signal or attribution
details.
"""
scorer: str
track_index: int
track_name: str
biosample_name: str
ontology_curie: str
raw_score: float
gene_name: str | None = None
gene_id: str | None = None
details: str = ''
@dataclasses.dataclass
class VariantRecord(genome.Variant):
"""Genomic variant annotated with AVI scores and feature importances.
Subclasses `genome.Variant` to inherit chromosome, position,
reference_bases, alternate_bases, validation, and serialization.
"""
raw_score: float | None = dataclasses.field(
default=None, compare=False, hash=False
)
phred_score: float | None = dataclasses.field(
default=None, compare=False, hash=False
)
quantile: float | None = dataclasses.field(
default=None, compare=False, hash=False
)
top_feature_name: str | None = dataclasses.field(
default=None, compare=False, hash=False
)
top_feature_value: float | None = dataclasses.field(
default=None, compare=False, hash=False
)
feature_importances: dict[str, float] = dataclasses.field(
default_factory=dict, compare=False, hash=False
)
track_attributions: dict[str, TrackAttribution] = dataclasses.field(
default_factory=dict, compare=False, hash=False
)
raw_record: Any = dataclasses.field(
default=None, repr=False, compare=False, hash=False
)
@property
def variant_id(self) -> str:
"""Returns formatted variant string: chr:pos:ref>alt."""
return str(self)
@property
def top_percentile(self) -> float | None:
"""Returns the exact top percentile (e.g. 0.10715 for top 0.11% impact)."""
if self.quantile is not None:
return self.quantile * 100.0
if self.phred_score is not None:
return (10.0 ** (-self.phred_score / 10.0)) * 100.0
return None
def to_row(
self,
*,
include_features: bool = False,
include_track_info: bool = False,
rank: int | None = None,
) -> dict[str, object]:
"""Serializes to a flat dictionary for tabular/JSON export."""
row: dict[str, object] = {}
if rank is not None:
row['rank'] = rank
row.update({
'variant': self.variant_id,
'chromosome': self.chromosome,
'position': self.position,
'ref': self.reference_bases,
'alt': self.alternate_bases,
'avi_phred': self.phred_score,
'avi_raw': self.raw_score,
'avi_quantile': self.quantile,
'top_percentile': self.top_percentile,
'top_modality': self.top_feature_name,
'top_feature_importance': self.top_feature_value,
})
if include_features:
for feat_name, feat_val in self.feature_importances.items():
row[f'fi_{feat_name}'] = feat_val
if include_track_info and self.track_attributions:
for feat_name, attr in self.track_attributions.items():
row[f'track_idx_{feat_name}'] = int(attr.track_index)
row[f'track_name_{feat_name}'] = str(attr.track_name)
row[f'track_biosample_{feat_name}'] = str(attr.biosample_name)
if attr.gene_name:
row[f'track_gene_{feat_name}'] = str(attr.gene_name)
return row
class AviFeature(enum.Enum):
"""AlphaGenome Variant Impact (AVI) biological modality features."""
MERGED_SPLICING = (
'Splicing',
'Splicing',
('SPLICE_SITES', 'SPLICE_SITE_USAGE', 'SPLICE_JUNCTIONS'),
)
MAX_ABS_RNA_SEQ = ('RNA-seq', 'Transcription / RNA-seq', ('RNA_SEQ',))
MAX_ABS_ATAC = ('ATAC-seq', 'Chromatin Accessibility', ('ATAC',))
MAX_ABS_DNASE = ('DNASE-seq', 'Chromatin Accessibility', ('DNASE',))
MAX_ABS_CHIP_TF = (
'ChIP-TF',
'Transcription Factor Binding',
('CHIP_TF',),
)
MAX_ABS_CHIP_HISTONE = (
'ChIP-Histone',
'Histone Modification',
('CHIP_HISTONE',),
)
MAX_ABS_CAGE = ('CAGE', 'Transcription / RNA-seq', ('CAGE',))
MAX_ABS_PROCAP = ('PRO-cap', 'Transcription / RNA-seq', ('PROCAP',))
MAX_ABS_POLYADENYLATION = (
'Polyadenylation',
'Transcription / RNA-seq',
('POLYADENYLATION',),
)
MAX_ABS_CONTACT_MAPS = (
'3D Genome Contacts',
'3D Genome Organization',
('CONTACT_MAPS',),
)
ALPHAMISSENSE = ('AlphaMissense', 'Protein Impact', ())
CACTUS_241_WAY = ('Cactus', 'Evolutionary Conservation', ())
PROTEIN_TERMINATION = ('Protein Termination', 'Coding Consequence', ())
START_LOST = ('Start Lost', 'Coding Consequence', ())
STOP_LOST = ('Stop Lost', 'Coding Consequence', ())
PHASTCONS_470_WAY = ('PhastCons 470', 'Evolutionary Conservation', ())
IS_INSERTION = ('Insertion', 'Structural Variant', ())
IS_DELETION = ('Deletion', 'Structural Variant', ())
display_name: str
category: str
scorers: Sequence[str]
def __init__(
self,
display_name: str,
category: str,
scorers: Sequence[str] = (),
):
self.display_name = display_name
self.category = category
self.scorers = scorers
@property
def is_sequence_or_conservation(self) -> bool:
"""Returns True if the feature has no associated experimental scorers."""
return not self.scorers
@classmethod
def from_key(cls, key: str) -> 'AviFeature | None':
"""Returns the AviFeature corresponding to the given string key or None."""
try:
return cls[key]
except KeyError:
return None
_REQUESTED_SCORERS = ('AVI_SCORE', 'AVI_SCORE_FEATURE_IMPORTANCE')
_SPLICE_SUB_SCORERS: Sequence[tuple[str, float, str]] = (
('SPLICE_SITES', 1.0, 'Core Splice Site'),
('SPLICE_SITE_USAGE', 1.0, 'Splice Usage'),
('SPLICE_JUNCTIONS', 1.0 / 5.0, 'Novel Junction'),
)
def format_feature_name(name: str | None) -> str:
"""Maps backend feature name to website display name if available."""
if not name:
return ''
feat = AviFeature.from_key(name)
return feat.display_name if feat is not None else name
def _normalize_chrom(chromosome: str, *, with_chr_prefix: bool = True) -> str:
"""Normalizes chromosome names (e.g. 'chr1' <-> '1')."""
chrom_str = str(chromosome).strip()
has_chr = chrom_str.lower().startswith('chr')
if with_chr_prefix:
return chrom_str if has_chr else f'chr{chrom_str}'
return chrom_str[3:] if has_chr else chrom_str
def _cdf_to_phred(cdf_quantile: float) -> tuple[float, float]:
"""Converts a CDF quantile to (tail_quantile, phred_score).
Args:
cdf_quantile: The cumulative distribution function quantile value.
Returns:
Tuple of (tail_quantile, phred_score) where phred = -10 * log10(tail).
"""
tail = max(1e-7, 1.0 - cdf_quantile)
return tail, -10.0 * math.log10(tail)
def _resolve_feature_names(adata: anndata.AnnData) -> Sequence[str]:
"""Extracts ordered feature names from an AnnData var axis."""
if (
hasattr(adata, 'var')
and adata.var is not None
and 'name' in adata.var.columns
):
return list(adata.var['name'])
if hasattr(adata, 'var_names'):
return list(adata.var_names)
return ()
def _compute_feature_attributions(
feature_values: np.ndarray,
feature_names: Sequence[str],
) -> tuple[dict[str, float], str | None, float | None]:
"""Builds feature importance dict and identifies the top contributor.
Args:
feature_values: 1-D array of attribution weights per feature.
feature_names: Ordered feature names matching the values.
Returns:
Tuple of (importances_dict, top_feature_display_name, top_feature_value).
"""
if len(feature_names) == 0 or len(feature_names) != len(feature_values):
return {}, None, None
importances: dict[str, float] = {}
for name, value in zip(feature_names, feature_values, strict=True):
importances[str(name)] = float(value)
max_index = int(np.argmax(np.abs(feature_values)))
top_name = format_feature_name(str(feature_names[max_index]))
top_value = float(feature_values[max_index])
return importances, top_name, top_value
@dataclasses.dataclass(frozen=True, kw_only=True)
class _AnnDataMaxHit:
"""Result of finding the maximum absolute value in an AnnData matrix."""
gene_idx: int
track_idx: int
raw_value: float
track_name: str
biosample_name: str
ontology_curie: str
gene_name: str | None
gene_id: str | None
def _find_adata_max(
adata: anndata.AnnData,
*,
use_abs: bool = False,
scale: float = 1.0,
) -> _AnnDataMaxHit | None:
"""Finds the element with maximum (optionally absolute) value in an AnnData.
Args:
adata: AnnData object with a 2-D X matrix (genes × tracks).
use_abs: If True, find argmax of |X| but return the original value.
scale: Multiplicative factor applied to the raw value (e.g. 1/5 for splice
junctions).
Returns:
An _AnnDataMaxHit or None if the matrix is empty.
"""
if adata.X is None or adata.X.size == 0:
return None
arr = np.nan_to_num(adata.X)
search_arr = np.abs(arr) if use_abs else arr
flat_idx = int(np.argmax(search_arr))
g_idx, t_idx = (int(x) for x in np.unravel_index(flat_idx, arr.shape))
raw_val = float(arr[g_idx, t_idx]) * scale
var_df = adata.var
has_var = var_df is not None and len(var_df) > t_idx
t_name = str(var_df.iloc[t_idx].get('name', '')) if has_var else ''
b_name = str(var_df.iloc[t_idx].get('biosample_name', '')) if has_var else ''
onto = str(var_df.iloc[t_idx].get('ontology_curie', '')) if has_var else ''
g_name: str | None = None
g_id: str | None = None
if len(adata.obs) > g_idx:
obs_row = adata.obs.iloc[g_idx]
g_name = str(obs_row.get('gene_name', obs_row.get('name', ''))) or None
g_id = str(obs_row.get('gene_id', '')) or None
return _AnnDataMaxHit(
gene_idx=g_idx,
track_idx=t_idx,
raw_value=raw_val,
track_name=t_name,
biosample_name=b_name,
ontology_curie=onto,
gene_name=g_name,
gene_id=g_id,
)
def _resolve_track_attributions(
variant_obj: genome.Variant,
client: atlas.AtlasClient,
feature_importances: Mapping[str, float],
) -> dict[str, TrackAttribution]:
"""Queries underlying assay scorers to resolve winning track indices."""
needed_scorers: set[str] = set()
for feat_name in feature_importances:
feat = AviFeature.from_key(feat_name)
if feat is not None and feat.scorers:
needed_scorers.update(feat.scorers)
if not needed_scorers:
return {}
assay_scores = client.query_variant(
variant_obj, requested_scorers=list(needed_scorers)
)
attributions: dict[str, TrackAttribution] = {}
for feat_name, feat_weight in feature_importances.items():
feat = AviFeature.from_key(feat_name)
if feat is AviFeature.MERGED_SPLICING:
# Splicing: SPLICE_SITES + SPLICE_SITE_USAGE + (SPLICE_JUNCTIONS / 5)
sub_attrs: MutableSequence[tuple[float, TrackAttribution]] = []
for scorer_key, scale, label in _SPLICE_SUB_SCORERS:
if scorer_key not in assay_scores:
continue
hit = _find_adata_max(assay_scores[scorer_key], scale=scale)
if hit is None:
continue
# SPLICE_SITES has no per-track biosample; use 'Consensus'.
b_name = hit.biosample_name or (
'Consensus' if scorer_key == 'SPLICE_SITES' else ''
)
detail_ctx = hit.track_name if scorer_key == 'SPLICE_SITES' else b_name
sub_attrs.append((
hit.raw_value,
TrackAttribution(
scorer=scorer_key,
track_index=hit.track_idx,
track_name=hit.track_name or scorer_key.lower(),
biosample_name=b_name,
ontology_curie=hit.ontology_curie or 'N/A',
raw_score=hit.raw_value,
gene_name=hit.gene_name,
details=f'{label} ({detail_ctx}): {hit.raw_value:.3f}',
),
))
if sub_attrs:
sub_attrs.sort(key=lambda x: x[0], reverse=True)
dominant_attr = sub_attrs[0][1]
all_details = ' | '.join(a[1].details for a in sub_attrs)
attributions['MERGED_SPLICING'] = dataclasses.replace(
dominant_attr, details=all_details
)
elif feat is not None and feat.scorers:
scorer_name = feat.scorers[0]
if scorer_name in assay_scores:
hit = _find_adata_max(assay_scores[scorer_name], use_abs=True)
if hit is not None:
attributions[feat_name] = TrackAttribution(
scorer=scorer_name,
track_index=hit.track_idx,
track_name=hit.track_name,
biosample_name=hit.biosample_name,
ontology_curie=hit.ontology_curie,
raw_score=hit.raw_value,
gene_name=hit.gene_name,
gene_id=hit.gene_id,
details=f'Max Abs: {hit.raw_value:+.4f}',
)
else:
# Sequence / Conservation feature
attributions[feat_name] = TrackAttribution(
scorer='SEQUENCE_OR_CONSERVATION',
track_index=0,
track_name=format_feature_name(feat_name),
biosample_name='N/A',
ontology_curie='N/A',
raw_score=feat_weight,
details='Sequence annotation',
)
return attributions
def _variant_sort_key(record: VariantRecord) -> float:
"""Sort key for ranking variants by Phred score descending, then raw."""
if record.phred_score is not None and not np.isnan(record.phred_score):
return record.phred_score
if record.raw_score is not None and not np.isnan(record.raw_score):
return record.raw_score
return -999.0
def _rank_and_filter(
scored: Sequence[VariantRecord],
*,
min_phred: float = 0.0,
top_k: int | None = None,
) -> tuple[Sequence[VariantRecord], Sequence[VariantRecord]]:
"""Sorts by score descending and filters to top candidates.
Args:
scored: Scored variant records.
min_phred: Minimum Phred score threshold for inclusion.
top_k: Maximum number of top hits to return.
Returns:
Tuple of (all_ranked, top_hits).
"""
ranked = sorted(scored, key=_variant_sort_key, reverse=True)
top_hits = [
v
for v in ranked
if (v.phred_score is not None and v.phred_score >= min_phred)
or (v.phred_score is None and v.raw_score is not None)
]
if top_k is not None:
top_hits = top_hits[:top_k]
return ranked, top_hits
def _to_variant_record(variant_obj: genome.Variant | str) -> VariantRecord:
"""Coerces a genome.Variant or string to a VariantRecord."""
if isinstance(variant_obj, VariantRecord):
return variant_obj
if isinstance(variant_obj, genome.Variant):
return VariantRecord(
chromosome=variant_obj.chromosome,
position=variant_obj.position,
reference_bases=variant_obj.reference_bases,
alternate_bases=variant_obj.alternate_bases,
)
parsed = genome.Variant.from_str(str(variant_obj))
return VariantRecord(
chromosome=parsed.chromosome,
position=parsed.position,
reference_bases=parsed.reference_bases,
alternate_bases=parsed.alternate_bases,
)
def get_atlas_client() -> atlas.AtlasClient:
"""Initializes the AlphaGenome Atlas client using ALPHAGENOME_API_KEY."""
api_key = os.environ.get('ALPHAGENOME_API_KEY')
if not api_key:
raise ValueError(
'ALPHAGENOME_API_KEY environment variable not set. Please ensure'
' ALPHAGENOME_API_KEY is configured in ~/.env.'
)
return atlas.create(api_key)
def _score_single_variant(
record: VariantRecord,
client: atlas.AtlasClient,
*,
include_track_info: bool = False,
) -> VariantRecord:
"""Scores a single VariantRecord via the Atlas API."""
normalized_chrom = _normalize_chrom(record.chromosome, with_chr_prefix=True)
variant_obj = genome.Variant(
chromosome=normalized_chrom,
position=record.position,
reference_bases=record.reference_bases,
alternate_bases=record.alternate_bases,
)
scores = client.query_variant(
variant_obj, requested_scorers=_REQUESTED_SCORERS
)
raw_score: float | None = None
quantile_val: float | None = None
phred_val: float | None = None
feature_importances: dict[str, float] = {}
top_feature_name: str | None = None
top_feature_value: float | None = None
if 'AVI_SCORE' in scores:
avi_adata = scores['AVI_SCORE']
if avi_adata.X is not None and avi_adata.X.size > 0:
raw_score = float(np.ravel(avi_adata.X)[0])
if 'quantiles' in avi_adata.layers:
cdf_quantile = float(np.ravel(avi_adata.layers['quantiles'])[0])
quantile_val, phred_val = _cdf_to_phred(cdf_quantile)
if 'AVI_SCORE_FEATURE_IMPORTANCE' in scores:
fi_adata = scores['AVI_SCORE_FEATURE_IMPORTANCE']
if fi_adata.X is not None and fi_adata.X.size > 0:
feature_values = np.ravel(fi_adata.X)
feature_names = _resolve_feature_names(fi_adata)
feature_importances, top_feature_name, top_feature_value = (
_compute_feature_attributions(feature_values, feature_names)
)
track_attributions: dict[str, TrackAttribution] = {}
if include_track_info and feature_importances:
track_attributions = _resolve_track_attributions(
variant_obj, client, feature_importances
)
return dataclasses.replace(
record,
raw_score=raw_score,
phred_score=phred_val,
quantile=quantile_val,
top_feature_name=top_feature_name,
top_feature_value=top_feature_value,
feature_importances=feature_importances,
track_attributions=track_attributions,
)
def score_variant_batch(
variants: Sequence[VariantRecord],
client: atlas.AtlasClient,
*,
include_track_info: bool = False,
) -> Sequence[VariantRecord]:
"""Scores a sequence of VariantRecord objects using the Atlas client."""
return [
_score_single_variant(
record, client, include_track_info=include_track_info
)
for record in variants
]
def _read_variants_vcf(path: str) -> Sequence[VariantRecord]:
"""Parses variants from a VCF / VCF.GZ file using Polars."""
try:
df = pl.read_csv(
path,
separator='\t',
comment_prefix='#',
has_header=False,
columns=[0, 1, 3, 4],
new_columns=['chrom', 'pos', 'ref', 'alt'],
schema_overrides={
'chrom': pl.String,
'pos': pl.Int64,
'ref': pl.String,
'alt': pl.String,
},
)
except pl.exceptions.NoDataError:
return []
except Exception as e:
sys.stderr.write(f"Error reading VCF file '{path}': {e}\n")
sys.exit(1)
df = (
df.with_columns(pl.col('alt').str.split(','))
.explode('alt')
.filter(
(pl.col('alt') != '.')
& (pl.col('alt') != '')
& (pl.col('alt') != '*')
& (~pl.col('alt').str.starts_with('<'))
)
)
records = [
VariantRecord(
chromosome=row['chrom'],
position=int(row['pos']),
reference_bases=row['ref'],
alternate_bases=row['alt'],
)
for row in df.iter_rows(named=True)
]
return records
def _read_variants_tabular(path: str) -> Sequence[VariantRecord]:
"""Parses variants from CSV, TSV, or Parquet."""
if path.endswith('.parquet'):
table_df = pl.read_parquet(path)
elif path.endswith('.tsv') or path.endswith('.txt'):
table_df = pl.read_csv(path, separator='\t')
else:
table_df = pl.read_csv(path)
column_lookup = {col.lower(): col for col in table_df.columns}
chrom_col = (
column_lookup.get('chrom')
or column_lookup.get('chromosome')
or column_lookup.get('#chrom')
or column_lookup.get('#chromosome')
)
pos_col = (
column_lookup.get('pos')
or column_lookup.get('position')
or column_lookup.get('start')
)
ref_col = (
column_lookup.get('ref')
or column_lookup.get('reference')
or column_lookup.get('reference_bases')
)
alt_col = (
column_lookup.get('alt')
or column_lookup.get('alternate')
or column_lookup.get('alternate_bases')
)
if not (chrom_col and pos_col and ref_col and alt_col):
raise ValueError(
'Input tabular file must contain chromosome, position, ref, and alt'
f' columns. Found columns: {table_df.columns}'
)
records: MutableSequence[VariantRecord] = []
for row in table_df.iter_rows(named=True):
records.append(
VariantRecord(
chromosome=str(row[chrom_col]),
position=int(row[pos_col]),
reference_bases=str(row[ref_col]),
alternate_bases=str(row[alt_col]),
)
)
return records
def _write_tabular(
output_path: str,
rows: Sequence[dict[str, object]],
) -> None:
"""Writes rows to CSV, TSV, Parquet, or JSON based on file extension."""
if output_path.endswith('.json'):
with open(output_path, 'w', encoding='utf-8') as json_file:
json.dump(rows, json_file, indent=2)
else:
dataframe = pl.DataFrame(rows)
if output_path.endswith('.parquet'):
dataframe.write_parquet(output_path)
elif output_path.endswith('.tsv') or output_path.endswith('.txt'):
dataframe.write_csv(output_path, separator='\t')
else:
dataframe.write_csv(output_path)
def _export_scored_variants(
output_path: str,
variants: Sequence[VariantRecord],
*,
include_features: bool = False,
include_track_info: bool = False,
include_rank: bool = False,
) -> None:
"""Exports scored variants to a tabular or JSON file."""
rows = [
v.to_row(
include_features=include_features,
include_track_info=include_track_info,
rank=rank if include_rank else None,
)
for rank, v in enumerate(variants, start=1)
]
_write_tabular(output_path, rows)
def _update_vcf_header(
header: Any,
scored_variants: Sequence[VariantRecord],
*,
vep_csq_only: bool,
) -> bool:
"""Updates VCF header with AVI info tags and CSQ schema."""
for variant in scored_variants:
contig_name = _normalize_chrom(variant.chromosome, with_chr_prefix=True)
if contig_name not in header.contigs:
header.contigs.add(contig_name)
if not vep_csq_only:
if 'AVI_PHRED' not in header.info:
header.info.add(
'AVI_PHRED',
1,
'Float',
'AlphaGenome Variant Impact Phred-scaled Score',
)
if 'AVI_RAW' not in header.info:
header.info.add(
'AVI_RAW', 1, 'Float', 'AlphaGenome Raw Variant Impact Score'
)
if 'AVI_QUANTILE' not in header.info:
header.info.add(
'AVI_QUANTILE', 1, 'Float', 'AlphaGenome Tail Quantile Score'
)
if 'AVI_TOP_PERCENTILE' not in header.info:
header.info.add(
'AVI_TOP_PERCENTILE',
1,
'Float',
'AlphaGenome Top Percentile of Genome-Wide SNVs',
)
if 'AVI_TOP_FEATURE' not in header.info:
header.info.add(
'AVI_TOP_FEATURE',
1,
'String',
'AlphaGenome Top Contributing Modality',
)
has_existing_csq = 'CSQ' in header.info
avi_csq_fields = [
'AVI_PHRED',
'AVI_RAW',
'AVI_QUANTILE',
'AVI_TOP_PERCENTILE',
'AVI_TOP_FEATURE',
]
if has_existing_csq:
csq_desc = header.info['CSQ'].description or ''
match = re.search(r'Format:\s*([^\\\">]+)', csq_desc)
csq_format_fields = (
match.group(1).split('|') if match else ['Allele', 'Consequence']
)
fields_to_add = [f for f in avi_csq_fields if f not in csq_format_fields]
updated_format = '|'.join(csq_format_fields + fields_to_add)
updated_desc = (
f'Consequence annotations from Ensembl VEP. Format: {updated_format}'
)
header.info.remove_header('CSQ')
header.add_line(
f'##INFO=<ID=CSQ,Number=.,Type=String,Description="{updated_desc}">'
)
else:
updated_format = 'Allele|' + '|'.join(avi_csq_fields)
header.add_line(
'##INFO=<ID=CSQ,Number=.,Type=String,Description="Consequence'
f' annotations from Ensembl VEP. Format: {updated_format}">'
)
return has_existing_csq
def _import_pysam() -> Any:
"""Imports and returns pysam, or raises RuntimeError if not installed."""
try:
import pysam
return pysam
except ImportError as e:
raise RuntimeError(
'pysam is not installed so VCF output is not supported. '
'Use an alternative output format: TSV, Parquet, or CSV.'
) from e
def _write_annotated_vcf(
input_path: str,
output_path: str,
scored_variants: Sequence[VariantRecord],
*,
vep_csq_only: bool = False,
) -> None:
"""Writes annotated variants to an output VCF."""
pysam = _import_pysam()
with pysam.VariantFile(input_path) as vcf_in:
has_existing_csq = _update_vcf_header(
vcf_in.header, scored_variants, vep_csq_only=vep_csq_only
)
variant_map: Mapping[tuple[str, int, str, str], VariantRecord] = {
(
_normalize_chrom(v.chromosome),
v.position,
v.reference_bases,
v.alternate_bases,
): v
for v in scored_variants
}
with pysam.VariantFile(output_path, 'w', header=vcf_in.header) as vcf_out:
for record in vcf_in.fetch():
new_record = record.copy()
chrom = _normalize_chrom(record.chrom)
position = int(record.pos)
ref = record.ref
alt_avi_map: dict[str, str] = {}
for alt in record.alts or ():
variant = variant_map.get((chrom, position, ref, alt))
phred_str = (
f'{variant.phred_score:.3f}'
if (variant and variant.phred_score is not None)
else ''
)
raw_str = (
f'{variant.raw_score:.4f}'
if (variant and variant.raw_score is not None)
else ''
)
quant_str = (
f'{variant.quantile:.6f}'
if (variant and variant.quantile is not None)
else ''
)
pct_str = (
f'{variant.top_percentile:.4f}'
if (variant and variant.top_percentile is not None)
else ''
)
mod_str = (
variant.top_feature_name
if (variant and variant.top_feature_name)
else ''
)
if not vep_csq_only and variant:
if variant.phred_score is not None:
new_record.info['AVI_PHRED'] = round(variant.phred_score, 3)
if variant.raw_score is not None:
new_record.info['AVI_RAW'] = round(variant.raw_score, 4)
if variant.quantile is not None:
new_record.info['AVI_QUANTILE'] = round(variant.quantile, 6)
if variant.top_percentile is not None:
new_record.info['AVI_TOP_PERCENTILE'] = round(
variant.top_percentile, 4
)
if variant.top_feature_name:
new_record.info['AVI_TOP_FEATURE'] = variant.top_feature_name
alt_avi_map[alt] = (
f'{phred_str}|{raw_str}|{quant_str}|{pct_str}|{mod_str}'
)
if has_existing_csq and 'CSQ' in new_record.info:
current_csq_list = list(new_record.info['CSQ'])
updated_csq_list = []
for csq_item in current_csq_list:
csq_tokens = csq_item.split('|')
csq_allele = csq_tokens[0] if csq_tokens else ''
avi_vals_str = alt_avi_map.get(csq_allele, '||||')
updated_csq_list.append(f'{csq_item}|{avi_vals_str}')
new_record.info['CSQ'] = tuple(updated_csq_list)
else:
csq_entries = [
f'{alt}|{alt_avi_map[alt]}' for alt in record.alts or ()
]
new_record.info['CSQ'] = tuple(csq_entries)
vcf_out.write(new_record)
def print_top_summary_table(
top_variants: Sequence[VariantRecord], total_count: int
) -> None:
"""Prints a summary markdown table of top candidate variants."""
print(f'\n### Top Ranked AVI Variants (Total Analyzed: {total_count})\n')
table_rows = []
for rank, variant in enumerate(top_variants, start=1):
phred_str = (
f'{variant.phred_score:.2f}'
if variant.phred_score is not None
else 'N/A'
)
raw_str = (
f'{variant.raw_score:.4f}' if variant.raw_score is not None else 'N/A'
)
if variant.top_percentile is not None:
pct = variant.top_percentile
pct_str = f'Top {pct:.2f}%' if pct >= 0.01 else f'Top {pct:.4g}%'
else:
pct_str = 'N/A'
mod_str = variant.top_feature_name if variant.top_feature_name else 'Global'
table_rows.append({
'Rank': rank,
'Variant': f'`{variant.variant_id}`',
'AVI Phred': f'**{phred_str}**',
'Raw Score': raw_str,
'Top Percentile': pct_str,
'Top Modality': mod_str,
})
print(tabulate.tabulate(table_rows, headers='keys', tablefmt='github'))
print(
'\n> [!NOTE] Phred = -10 * log10(quantile), representing the top'
' (10^(-Phred/10) * 100)% predicted impact of all genome-wide SNVs.\n'
)
def print_feature_importance_breakdown(
variant: VariantRecord,
top_n: int = 10,
*,
include_track_info: bool = False,
) -> None:
"""Prints a detailed feature importance breakdown table for a variant."""
if not variant.feature_importances:
print(f'No feature importances available for {variant.variant_id}.')
return
sorted_features = sorted(
variant.feature_importances.items(),
key=lambda item: abs(item[1]),
reverse=True,
)
phred_display = (
f'{variant.phred_score:.2f}' if variant.phred_score is not None else 'N/A'
)
if variant.top_percentile is not None:
if variant.top_percentile >= 0.01:
pct_info = f' | Top {variant.top_percentile:.2f}%'
else:
pct_info = f' | Top {variant.top_percentile:.4g}%'
else:
pct_info = ''
print(
f'\n#### Feature Importances for `{variant.variant_id}` (AVI Phred:'
f' {phred_display}{pct_info})'
)
max_abs = abs(sorted_features[0][1]) if sorted_features else 1.0
max_abs = max(max_abs, 1e-9)
if include_track_info and variant.track_attributions:
table_rows = []
for rank, (feat_name, feat_val) in enumerate(
sorted_features[:top_n], start=1
):
display_name = format_feature_name(feat_name)
bar_length = int(round((abs(feat_val) / max_abs) * 15))
bar_str = '#' * bar_length
attr = variant.track_attributions.get(feat_name)
if attr:
track_idx_str = (
f'`{attr.track_index}`' if attr.track_index >= 0 else 'N/A'
)
top_info = (
f'{attr.biosample_name} ({attr.track_name})'
if (attr.biosample_name and attr.biosample_name != 'N/A')
else attr.track_name
)
gene_str = attr.gene_name if attr.gene_name else 'N/A'
table_rows.append({
'Rank': rank,
'Modality / Feature': f'`{display_name}`',
'Attribution': f'`{feat_val:+.4f}`',
'Track Index': track_idx_str,
'Top Track / Biosample': top_info,
'Target Gene': gene_str,
'Relative Impact': f'`{bar_str}`',
})
else:
table_rows.append({
'Rank': rank,
'Modality / Feature': f'`{display_name}`',
'Attribution': f'`{feat_val:+.4f}`',
'Track Index': 'N/A',
'Top Track / Biosample': 'N/A',
'Target Gene': 'N/A',
'Relative Impact': f'`{bar_str}`',
})
print(tabulate.tabulate(table_rows, headers='keys', tablefmt='github'))
else:
table_rows = []
for rank, (feat_name, feat_val) in enumerate(
sorted_features[:top_n], start=1
):
display_name = format_feature_name(feat_name)
bar_length = int(round((abs(feat_val) / max_abs) * 15))
bar_str = '#' * bar_length
table_rows.append({
'Rank': rank,
'Modality / Feature': f'`{display_name}`',
'Attribution Weight': f'`{feat_val:+.4f}`',
'Relative Impact': f'`{bar_str}`',
})
print(tabulate.tabulate(table_rows, headers='keys', tablefmt='github'))
print()
def _print_feature_breakdowns(
variants: Sequence[VariantRecord],
*,
header_text: str = '',
top_n: int = 10,
include_track_info: bool = False,
max_variants: int | None = None,
) -> None:
"""Prints feature importance breakdowns for a list of variants."""
if not variants:
return
if header_text:
print(header_text)
display = variants[:max_variants] if max_variants is not None else variants
for variant in display:
print_feature_importance_breakdown(
variant, top_n=top_n, include_track_info=include_track_info
)
def _extract_region_variants(
avi_adata: anndata.AnnData,
fi_adata: anndata.AnnData | None,
) -> Sequence[VariantRecord]:
"""Extracts scored VariantRecord instances from interval query AnnData."""
variant_objs = list(avi_adata.obs['variant'])
raw_scores = np.ravel(avi_adata.X) if avi_adata.X is not None else None
quantiles = (
np.ravel(avi_adata.layers['quantiles'])
if 'quantiles' in avi_adata.layers
else None
)
fi_matrix = (
fi_adata.X if (fi_adata is not None and fi_adata.X is not None) else None
)
fi_names: Sequence[str] = ()
if fi_adata is not None:
fi_names = _resolve_feature_names(fi_adata)
has_valid_fi = (
fi_matrix is not None
and bool(fi_names)
and fi_matrix.shape[1] == len(fi_names)
)
candidate_variants: MutableSequence[VariantRecord] = []
for index, variant_obj in enumerate(variant_objs):
base = _to_variant_record(variant_obj)
raw_val = float(raw_scores[index]) if raw_scores is not None else None
phred_val: float | None = None
quantile_val: float | None = None
if quantiles is not None:
quantile_val, phred_val = _cdf_to_phred(float(quantiles[index]))
fi_dict: dict[str, float] = {}
top_feature_name: str | None = None
top_feature_value: float | None = None
if has_valid_fi and fi_matrix is not None:
row_fi = np.ravel(fi_matrix[index])
fi_dict, top_feature_name, top_feature_value = (
_compute_feature_attributions(row_fi, fi_names)
)
candidate_variants.append(
dataclasses.replace(
base,
raw_score=raw_val,
phred_score=phred_val,
quantile=quantile_val,
top_feature_name=top_feature_name,
top_feature_value=top_feature_value,
feature_importances=fi_dict,
)
)
return candidate_variants
def handle_annotate(args: argparse.Namespace) -> None:
"""Handler for `annotate` subcommand."""
is_vcf_output = args.output.endswith('.vcf') or args.output.endswith(
'.vcf.gz'
)
if is_vcf_output:
# Fail early if pysam is not installed.
_import_pysam()
client = get_atlas_client()
is_vcf = (
args.input.endswith('.vcf')
or args.input.endswith('.vcf.gz')
or args.input.endswith('.bcf')
)
if is_vcf:
variants = _read_variants_vcf(args.input)
else:
variants = _read_variants_tabular(args.input)
if not variants:
print(f'No variants found in input file: {args.input}')
return
print(
f'Loaded {len(variants)} variants. Scoring via AlphaGenome Atlas API...'
)
scored = score_variant_batch(
variants, client, include_track_info=args.include_track_info
)
ranked, top_hits = _rank_and_filter(
scored, min_phred=args.min_phred, top_k=args.top_k
)
if is_vcf and is_vcf_output:
_write_annotated_vcf(
args.input,
args.output,
ranked,
vep_csq_only=args.vep_csq_only,
)
else:
_export_scored_variants(
args.output,
ranked,
include_features=args.include_features,
include_track_info=args.include_track_info,
)
print(f'Annotated variants successfully written to: {args.output}')
print_top_summary_table(top_hits, len(ranked))
if args.show_features and top_hits:
_print_feature_breakdowns(
top_hits,
header_text='\n### Top Candidate Modality Attribution Breakdowns:',
include_track_info=args.include_track_info,
max_variants=3,
)
if args.top_output:
_export_scored_variants(
args.top_output,
top_hits,
include_features=args.include_features,
include_track_info=args.include_track_info,
include_rank=True,
)
print(f'Top variants summary written to: {args.top_output}')
def handle_query(args: argparse.Namespace) -> None:
"""Handler for `query` subcommand."""
client = get_atlas_client()
raw_variant_strings: MutableSequence[str] = []
if args.variants:
raw_variant_strings.extend(args.variants)
if args.input_file:
with open(args.input_file, 'r', encoding='utf-8') as input_file:
for line in input_file:
line_str = line.strip()
if line_str and not line_str.startswith('#'):
raw_variant_strings.append(line_str)
if not raw_variant_strings:
print('No variants specified. Use positional arguments or --input_file.')
return
variants: MutableSequence[VariantRecord] = []
for variant_str in raw_variant_strings:
try:
parsed = genome.Variant.from_str(variant_str.strip())
variants.append(_to_variant_record(parsed))
except ValueError:
sys.stderr.write(
f'Invalid variant format: {variant_str}. Expected chr:pos:ref>alt.\n'
)
if not variants:
return
scored = score_variant_batch(
variants, client, include_track_info=args.include_track_info
)
if args.format == 'json':
out_rows = [
v.to_row(
include_features=True,
include_track_info=args.include_track_info,
)
for v in scored
]
json_str = json.dumps(out_rows, indent=2)
if args.output:
with open(args.output, 'w', encoding='utf-8') as out_f:
out_f.write(json_str)
print(f'Results written to {args.output}')
else:
print(json_str)
return
print_top_summary_table(scored, len(scored))
if args.show_features:
for variant in scored:
print_feature_importance_breakdown(
variant,
top_n=args.top_features,
include_track_info=args.include_track_info,
)
if args.output:
_export_scored_variants(
args.output,
scored,
include_features=args.include_features,
include_track_info=args.include_track_info,
)
print(f'Results exported to: {args.output}')
def handle_region(args: argparse.Namespace) -> None:
"""Handler for `region` subcommand (in silico saturation mutagenesis)."""
client = get_atlas_client()
region_match = re.match(
r'^(chr[0-9XYM]+|\d+|X|Y|M):(\d+)-(\d+)$', args.region
)
if not region_match:
raise ValueError(
f'Invalid region string: {args.region}. Expected format chr:start-end'
' (e.g. chr9:128225900-128226000).'
)
chrom = region_match.group(1)
start_pos = int(region_match.group(2))
end_pos = int(region_match.group(3))
if end_pos < start_pos:
raise ValueError(
f'End position ({end_pos}) must be >= start position ({start_pos}).'
)
window_size = end_pos - start_pos + 1
if window_size > args.max_window_size:
raise ValueError(
f'Requested region size ({window_size} bp) exceeds maximum window size'
f' ({args.max_window_size} bp). Please specify a smaller region or'
' increase --max_window_size.'
)
print(f'Scanning region {chrom}:{start_pos}-{end_pos} ({window_size} bp)...')
chrom_normalized = _normalize_chrom(chrom, with_chr_prefix=True)
# Convert 1-based closed CLI interval [start, end] to 0-based half-open [start-1, end)
interval = genome.Interval(
chromosome=chrom_normalized,
start=start_pos - 1,
end=end_pos,
)
print(
'Querying AlphaGenome Atlas interval API for'
f' {chrom_normalized}:{start_pos}-{end_pos}...'
)
scores = client.query_interval(interval, requested_scorers=_REQUESTED_SCORERS)
if 'AVI_SCORE' not in scores:
raise ValueError(
f'AlphaGenome Atlas did not return AVI_SCORE for region {args.region}.'
)
avi_adata = scores['AVI_SCORE']
fi_adata = scores.get('AVI_SCORE_FEATURE_IMPORTANCE')
candidate_variants = _extract_region_variants(avi_adata, fi_adata)
valid_scored = [
v
for v in candidate_variants
if v.phred_score is not None or v.raw_score is not None
]
print(
f'Successfully retrieved and scored {len(valid_scored)} SNVs in region.'
)
ranked, top_hits = _rank_and_filter(
valid_scored, min_phred=args.min_phred, top_k=args.top_k
)
print_top_summary_table(top_hits, len(valid_scored))
if args.show_features and top_hits:
_print_feature_breakdowns(
top_hits,
header_text='\n### Top Region Hotspot Feature Importances:',
include_track_info=args.include_track_info,
max_variants=3,
)
if args.output:
_export_scored_variants(
args.output,
ranked,
include_features=args.include_features,
include_track_info=args.include_track_info,
include_rank=True,
)
print(f'Region scan exported to: {args.output}')
def _print_feature_metadata(args: argparse.Namespace) -> None:
"""Prints AVI feature definitions (no API client needed)."""
rows: list[dict[str, object]] = []
for rank, feature in enumerate(AviFeature, start=1):
rows.append({
'rank': rank,
'feature_key': feature.name,
'display_name': feature.display_name,
'category': feature.category,
})
if getattr(args, 'format', 'table') == 'json':
print(json.dumps(rows, indent=2))
return
if getattr(args, 'output', None):
_write_tabular(args.output, rows)
print(f'AVI feature definitions written to: {args.output}')
return
print('\n### AlphaGenome Variant Impact (AVI) 18 Biological Modalities:\n')
display_rows = [
{
'Rank': r['rank'],
'Modality Key': f'`{r["feature_key"]}`',
'Website Display Name': f'**{r["display_name"]}**',
'Category': r['category'],
}
for r in rows
]
print(tabulate.tabulate(display_rows, headers='keys', tablefmt='github'))
print()
def handle_metadata(args: argparse.Namespace) -> None:
"""Handler for `metadata` subcommand."""
meta_type = args.type
if meta_type == 'features':
_print_feature_metadata(args)
return
# scorers and tracks modes require the Atlas client.
client = get_atlas_client()
if meta_type == 'scorers':
scorer_map = client.scorer_metadata()
rows: MutableSequence[dict[str, object]] = [
{
'scorer_name': 'AVI_SCORE',
'category': 'Variant Impact (Composite)',
'is_signed': False,
'tracks': 1,
'biosamples': 1,
'description': (
'AlphaGenome Variant Impact calibrated Phred/Quantile score'
),
},
{
'scorer_name': 'AVI_SCORE_FEATURE_IMPORTANCE',
'category': 'Feature Attribution',
'is_signed': True,
'tracks': 18,
'biosamples': 1,
'description': (
'18-modality biological feature attribution weights (SHAP)'
),
},
]
for name, meta in scorer_map.items():
df = meta.track_metadata
num_tracks = len(df)
num_biosamples = (
int(df['biosample_name'].nunique())
if (df is not None and 'biosample_name' in df.columns)
else 0
)
rows.append({
'scorer_name': name,
'category': 'Experimental Track Scorer',
'is_signed': meta.is_signed,
'tracks': num_tracks,
'biosamples': num_biosamples,
'description': f'Atlas experimental track predictions ({name})',
})
if args.format == 'json':
print(json.dumps(rows, indent=2))
return
if args.output:
_write_tabular(args.output, rows)
print(f'Scorer metadata written to: {args.output}')
return
print('\n### Registered AlphaGenome Atlas Scorers:\n')
display_rows = [
{
'Scorer Name': f'`{row["scorer_name"]}`',
'Category': row['category'],
'Signed': row['is_signed'],
'Tracks': row['tracks'],
'Biosamples': row['biosamples'],
}
for row in rows
]
print(tabulate.tabulate(display_rows, headers='keys', tablefmt='github'))
print()
elif meta_type == 'tracks':
scorer_map = client.scorer_metadata()
track_rows: MutableSequence[dict[str, object]] = []
target_scorers = [args.scorer] if args.scorer else list(scorer_map.keys())
for name in target_scorers:
if name not in scorer_map:
continue
df = scorer_map[name].track_metadata
if df is None or df.empty:
continue
for index, row in df.iterrows():
track_dict: dict[str, object] = {
'scorer': name,
'track_index': int(index),
'track_name': str(row.get('name', '')),
'biosample_name': str(row.get('biosample_name', '')),
'biosample_type': str(row.get('biosample_type', '')),
'ontology_curie': str(row.get('ontology_curie', '')),
'strand': str(row.get('strand', '')),
}
for extra_col in ('transcription_factor', 'histone_mark', 'assay'):
if extra_col in row and str(row[extra_col]):
track_dict[extra_col] = str(row[extra_col])
# Filter by ontology
if args.ontology and str(track_dict['ontology_curie']) != args.ontology:
continue
# Filter by text query
if args.query:
q_lower = args.query.lower()
searchable = ' '.join(str(v).lower() for v in track_dict.values())
if q_lower not in searchable:
continue
track_rows.append(track_dict)
if not track_rows:
print('No tracks matched the specified metadata filter criteria.')
return
if args.output:
_write_tabular(args.output, track_rows)
print(
f'Dumped {len(track_rows)} track metadata records to: {args.output}'
)
return
if args.format == 'json':
print(json.dumps(track_rows[: args.top_n], indent=2))
return
print(
f'\n### Atlas Track Metadata Catalog (Matched: {len(track_rows)},'
f' Showing Top {min(len(track_rows), args.top_n)}):\n'
)
display_tracks = [
{
'Scorer': f'`{row["scorer"]}`',
'Track Index': f'`{row["track_index"]}`',
'Track Name': f'`{row["track_name"]}`',
'Biosample': row['biosample_name'],
'Ontology': f'`{row["ontology_curie"]}`',
}
for row in track_rows[: args.top_n]
]
print(tabulate.tabulate(display_tracks, headers='keys', tablefmt='github'))
print()
def _clean_chrom(chrom_str: str) -> str:
"""Ensures chromosome string has chr prefix."""
c = str(chrom_str).strip()
return c if c.startswith('chr') else f'chr{c}'
GTF_URL = (
'https://storage.googleapis.com/alphagenome/reference/gencode/'
'hg38/gencode.v46.annotation.gtf.gz.feather'
)
def load_gtf_dataframe(custom_path: str | None = None) -> pd.DataFrame:
"""Loads GENCODE v46 GTF annotations from a local path, environment variable, cache, or GCS."""
path = custom_path or os.environ.get('ALPHAGENOME_GTF_PATH')
if path and os.path.exists(path):
print(f'Loading GTF from {path}...')
return pd.read_feather(path)
cache_dir = os.path.expanduser('~/.cache/alphagenome')
cache_path = os.path.join(cache_dir, 'gencode.v46.annotation.gtf.gz.feather')
if os.path.exists(cache_path):
print(f'Loading GTF from cache {cache_path}...')
return pd.read_feather(cache_path)
target_url = path or GTF_URL
print(f'Loading GTF from {target_url}...')
try:
df = pd.read_feather(target_url)
try:
os.makedirs(cache_dir, exist_ok=True)
df.to_feather(cache_path)
except Exception:
pass
return df
except Exception as e:
raise RuntimeError(
f'Failed to load GTF annotations from {target_url}: {e}'
) from e
def handle_gtf(args: argparse.Namespace) -> None:
"""Handles gene and transcript GTF queries, exon extraction, and splice junctions."""
gtf = load_gtf_dataframe()
if getattr(args, 'protein_coding', False):
if 'gene_type' in gtf.columns:
gtf = gtf[gtf['gene_type'] == 'protein_coding']
if 'transcript_type' in gtf.columns:
gtf = gtf[gtf['transcript_type'] == 'protein_coding']
if not getattr(args, 'all_transcripts', False):
if 'tag' in gtf.columns:
gtf = gtf[gtf['tag'].fillna('').str.contains('MANE_Select')]
if getattr(args, 'longest', False):
lengths = gtf[gtf['Feature'] == 'transcript'].reset_index(drop=True)
lengths['t_len'] = lengths['End'] - lengths['Start'] + 1
longest = lengths.loc[lengths.groupby('gene_id')['t_len'].idxmax()]
gtf = gtf[gtf['transcript_id'].isin(longest['transcript_id'])]
query_variant = None
query_pos = None
query_chrom = None
if getattr(args, 'variant', None):
v_str = args.variant.strip()
match = re.match(
r'^(chr[0-9XYM]+):(\d+)(?:[:/_>]([A-Za-z]+)(?:[:/_>]([A-Za-z]+))?)?$',
v_str,
)
if match:
query_chrom = match.group(1)
query_pos = int(match.group(2))
query_variant = v_str
else:
parts = v_str.split(':')
if len(parts) >= 2 and parts[0].startswith('chr') and parts[1].isdigit():
query_chrom = parts[0]
query_pos = int(parts[1])
query_variant = v_str
else:
raise ValueError(f'Invalid variant/position format: {args.variant}')
t_df = gtf[
(gtf['Chromosome'] == query_chrom) & (gtf['Feature'] == 'transcript')
]
overlapping_t = t_df[
(t_df['Start'] <= query_pos) & (t_df['End'] >= query_pos)
]
if overlapping_t.empty:
nearby_t = t_df[
(t_df['Start'] - 50000 <= query_pos)
& (t_df['End'] + 50000 >= query_pos)
]
if nearby_t.empty:
print(f'No transcripts found overlapping or near {args.variant}.')
return
t_ids = nearby_t['transcript_id'].unique()
else:
t_ids = overlapping_t['transcript_id'].unique()
target_rows = gtf[gtf['transcript_id'].isin(t_ids)]
elif getattr(args, 'gene', None):
gene_name = args.gene.strip().upper()
target_rows = gtf[gtf['gene_name'].fillna('').str.upper() == gene_name]
if target_rows.empty:
print(f'No gene found with symbol: {args.gene}')
return
elif getattr(args, 'gene_id', None):
gene_id = args.gene_id.strip()
target_rows = gtf[
(gtf['gene_id'] == gene_id)
| (gtf['gene_id'].str.split('.').str[0] == gene_id.split('.')[0])
]
if target_rows.empty:
print(f'No gene found with ID: {args.gene_id}')
return
elif getattr(args, 'transcript_id', None):
t_id = args.transcript_id.strip()
target_rows = gtf[
(gtf['transcript_id'] == t_id)
| (gtf['transcript_id'].str.split('.').str[0] == t_id.split('.')[0])
]
if target_rows.empty:
print(f'No transcript found with ID: {args.transcript_id}')
return
elif getattr(args, 'region', None):
r_str = args.region.strip()
match = re.match(r'^(chr[0-9XYM]+):(\d+)-(\d+)$', r_str)
if not match:
raise ValueError(f'Invalid region format: {args.region}')
r_chrom = match.group(1)
r_start = int(match.group(2))
r_end = int(match.group(3))
t_df = gtf[
(gtf['Chromosome'] == r_chrom) & (gtf['Feature'] == 'transcript')
]
overlapping_t = t_df[(t_df['Start'] <= r_end) & (t_df['End'] >= r_start)]
if overlapping_t.empty:
print(f'No transcripts found overlapping region {args.region}.')
return
target_rows = gtf[
gtf['transcript_id'].isin(overlapping_t['transcript_id'].unique())
]
else:
raise ValueError(
'Must specify one of --gene, --gene_id, --transcript_id, --variant, or'
' --region.'
)
transcripts_data = []
for t_id, group in target_rows.groupby('transcript_id'):
t_feat_rows = group[group['Feature'] == 'transcript']
t_feat = t_feat_rows.iloc[0] if not t_feat_rows.empty else group.iloc[0]
gene_name = str(t_feat.get('gene_name', ''))
gene_id = str(t_feat.get('gene_id', ''))
chrom = str(t_feat.get('Chromosome', ''))
strand = str(t_feat.get('Strand', ''))
t_start = int(t_feat.get('Start', 0))
t_end = int(t_feat.get('End', 0))
t_type = str(t_feat.get('transcript_type', ''))
tags = str(t_feat.get('tag', ''))
is_mane = 'MANE_Select' in tags
ascending = strand == '+'
exon_rows = (
group[group['Feature'] == 'exon']
.sort_values('Start', ascending=ascending)
.reset_index(drop=True)
)
exons_list = []
for idx, e in exon_rows.iterrows():
e_num = int(e.get('exon_number', idx + 1))
e_start = int(e['Start'])
e_end = int(e['End'])
e_width = e_end - e_start + 1
acceptor = e_start if strand == '+' else e_end
donor = e_end if strand == '+' else e_start
overlaps_v = False
if query_pos is not None and chrom == query_chrom:
overlaps_v = e_start <= query_pos <= e_end
exons_list.append({
'exon_number': e_num,
'chromosome': chrom,
'start': e_start,
'end': e_end,
'width': e_width,
'strand': strand,
'acceptor': acceptor,
'donor': donor,
'overlaps_variant': overlaps_v,
})
if exon_rows.empty:
print('No exons found for target.', file=sys.stderr)
sys.exit(1)
junctions_list = []
num_exons = len(exon_rows)
for i in range(num_exons - 1):
e1 = exon_rows.iloc[i]
e2 = exon_rows.iloc[i + 1]
if strand == '+':
j_start = int(e1['End'])
j_end = int(e2['Start'])
else:
j_start = int(e2['End'])
j_end = int(e1['Start'])
score_id = None
atlas_url = None
if query_variant:
v_enc = query_variant.replace('>', '%3E')
track_name = (
getattr(args, 'track', None)
or 'junction_UBERON:0011907 gtex Muscle_Skeletal polyA plus RNA-seq'
)
track_enc = track_name.replace(' ', '%20')
splice_score_id = (
f'{v_enc}:SPLICE_JUNCTIONS:{track_enc}:Unstranded:'
f'{gene_id}:{j_start}:{j_end}'
)
# Splicing visualization: pair SPLICE_JUNCTIONS with continuous RNA_SEQ
rna_track_name = re.sub(r'^junction_', '', track_name)
rna_track_enc = rna_track_name.replace(' ', '%20')
rna_score_id = (
f'{v_enc}:RNA_SEQ:{rna_track_enc}:Unstranded:{gene_id}:0:0'
)
scores_param = f'{splice_score_id},{rna_score_id}'
score_id = splice_score_id
i_start = max(1, t_start)
i_end = t_end
atlas_url = (
f'{_ATLAS_TRACK_PREDICTIONS_URL}?'
f'q={v_enc}&m=variant'
'&lItems=avi,section:RNA_SEQ,section:SPLICE_JUNCTIONS,section:SPLICE_SITE_USAGE,section:SPLICE_SITES'
f'&scores={scores_param}'
f'&i={chrom}:{i_start}-{i_end}'
)
junctions_list.append({
'type': f'Canonical Intron {i+1}',
'upstream_exon': int(e1.get('exon_number', i + 1)),
'downstream_exon': int(e2.get('exon_number', i + 2)),
'junction_start': j_start,
'junction_end': j_end,
'junction_str': f'{j_start}:{j_end}',
'intron_length': j_end - j_start - 1,
'score_id': score_id,
'atlas_url': atlas_url,
})
for i in range(num_exons - 2):
e1 = exon_rows.iloc[i]
e_skipped = exon_rows.iloc[i + 1]
e3 = exon_rows.iloc[i + 2]
if strand == '+':
j_start = int(e1['End'])
j_end = int(e3['Start'])
else:
j_start = int(e3['End'])
j_end = int(e1['Start'])
junctions_list.append({
'type': f'Exon {e_skipped.get("exon_number", i+2)} Skipping',
'upstream_exon': int(e1.get('exon_number', i + 1)),
'downstream_exon': int(e3.get('exon_number', i + 3)),
'junction_start': j_start,
'junction_end': j_end,
'junction_str': f'{j_start}:{j_end}',
'intron_length': j_end - j_start - 1,
})
cds_rows = group[group['Feature'] == 'CDS'].sort_values(
'Start', ascending=ascending
)
cds_list = [
{
'start': int(r['Start']),
'end': int(r['End']),
'width': int(r['End']) - int(r['Start']) + 1,
}
for _, r in cds_rows.iterrows()
]
utr_rows = group[group['Feature'] == 'UTR'].sort_values(
'Start', ascending=ascending
)
utr_list = [
{
'start': int(r['Start']),
'end': int(r['End']),
'width': int(r['End']) - int(r['Start']) + 1,
}
for _, r in utr_rows.iterrows()
]
transcripts_data.append({
'gene_name': gene_name,
'gene_id': gene_id,
'transcript_id': t_id,
'transcript_type': t_type,
'chromosome': chrom,
'start': t_start,
'end': t_end,
'strand': strand,
'is_mane_select': is_mane,
'num_exons': len(exons_list),
'exons': exons_list,
'junctions': junctions_list,
'cds': cds_list,
'utr': utr_list,
})
if not transcripts_data:
print('No matching transcript records found.')
return
if getattr(args, 'format', 'table') == 'json':
print(json.dumps(transcripts_data, indent=2))
return
if getattr(args, 'output', None):
# Flatten exons or junctions for output export
export_rows = []
for t in transcripts_data:
if getattr(args, 'junctions', False):
for j in t['junctions']:
row = {
'gene_name': t['gene_name'],
'gene_id': t['gene_id'],
'transcript_id': t['transcript_id'],
**j,
}
export_rows.append(row)
else:
for e in t['exons']:
row = {
'gene_name': t['gene_name'],
'gene_id': t['gene_id'],
'transcript_id': t['transcript_id'],
**e,
}
export_rows.append(row)
_write_tabular(args.output, export_rows)
print(f'Exported {len(export_rows)} records to: {args.output}')
return
for t in transcripts_data:
mane_badge = ' [MANE Select]' if t['is_mane_select'] else ''
print(
f'\n### Gene: {t["gene_name"]} ({t["gene_id"]}) | Transcript:'
f' {t["transcript_id"]}{mane_badge}'
)
print(
f'Coordinates: {t["chromosome"]}:{t["start"]}-{t["end"]} ({t["strand"]}'
f' strand) | Type: {t["transcript_type"]} | Exons: {t["num_exons"]}\n'
)
if getattr(args, 'exons', False) or (
not getattr(args, 'junctions', False)
and not getattr(args, 'cds', False)
and not getattr(args, 'utr', False)
):
print('#### Exon Boundaries:')
exon_rows = [
{
'Exon #': f'Exon {e["exon_number"]}',
'Coordinates': f'`{e["start"]}-{e["end"]}`',
'Width (bp)': f'{e["width"]} bp',
"5' Acceptor": f'`{e["acceptor"]}`',
"3' Donor": f'`{e["donor"]}`',
'Overlaps Query': (
'**Yes (Mutation Site)**' if e['overlaps_variant'] else '-'
),
}
for e in t['exons']
]
print(tabulate.tabulate(exon_rows, headers='keys', tablefmt='github'))
print()
if getattr(args, 'junctions', False):
print('#### Splice Junctions (Canonical & Exon Skipping):')
junction_rows = [
{
'Junction Type': j['type'],
'Flanking Exons': (
f'Exon {j["upstream_exon"]} → Exon {j["downstream_exon"]}'
),
'Junction Coordinates': f'`{j["junction_str"]}`',
'Intron Length': f'{j["intron_length"]} bp',
}
for j in t['junctions']
]
print(tabulate.tabulate(junction_rows, headers='keys', tablefmt='github'))
print()
if getattr(args, 'cds', False):
print('#### CDS Segments:')
for idx, c in enumerate(t['cds'], start=1):
print(
f'- CDS {idx}: `{c["start"]}-{c["end"]}` (Width: {c["width"]} bp)'
)
print()
if getattr(args, 'utr', False):
print('#### UTR Segments:')
for idx, u in enumerate(t['utr'], start=1):
print(
f'- UTR {idx}: `{u["start"]}-{u["end"]}` (Width: {u["width"]} bp)'
)
print()
def _add_common_args(parser: argparse.ArgumentParser) -> None:
"""Adds shared CLI arguments across all subcommands."""
parser.add_argument(
'--show_features',
'--features',
dest='show_features',
action=argparse.BooleanOptionalAction,
default=True,
help='Display feature importance breakdowns',
)
parser.add_argument(
'--include_features',
action=argparse.BooleanOptionalAction,
default=False,
help='Include all 18 feature importance columns in exported files',
)
parser.add_argument(
'--include_track_info',
action=argparse.BooleanOptionalAction,
default=False,
help=(
'Resolve and include the exact underlying Atlas track index,'
' biosample, and target gene for each feature'
),
)
parser.add_argument('--output', '-o', help='Path to write output file')
def main() -> None:
dotenv.load_dotenv(os.path.expanduser('~/.env'))
parser = argparse.ArgumentParser(
description='AlphaGenome Variant Impact (AVI) CLI',
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
subparsers = parser.add_subparsers(dest='subcommand', required=True)
# Subcommand: annotate
parser_annotate = subparsers.add_parser(
'annotate',
help=(
'Annotate variant callset (VCF, CSV, TSV, Parquet) in Ensembl VEP'
' CSQ format'
),
)
parser_annotate.add_argument(
'--input',
'-i',
required=True,
help=(
'Path to input variant file (VCF or tabular CSV/TSV/Parquet with'
' 1-based coordinates)'
),
)
_add_common_args(parser_annotate)
parser_annotate.add_argument(
'--top_k',
type=int,
default=20,
help='Number of top variants to summarize',
)
parser_annotate.add_argument(
'--min_phred',
type=float,
default=0.0,
help='Minimum Phred score for top filtering',
)
parser_annotate.add_argument(
'--top_output',
help='Optional path to export top-ranked summary (JSON/TSV/CSV)',
)
parser_annotate.add_argument(
'--vep_csq_only',
action='store_true',
help='Only annotate within VEP CSQ tag',
)
# Subcommand: query
parser_query = subparsers.add_parser(
'query',
help=(
'Query one or more variant strings to get scores and full modality'
' feature importances'
),
)
parser_query.add_argument(
'variants',
nargs='*',
help=(
'Variant strings to score in 1-based chr:pos:ref>alt format (e.g.'
' chr9:128225994:G>A chr22:36201698:A>C)'
),
)
parser_query.add_argument(
'--input_file',
'-i',
help=(
'Path to file containing variant strings in 1-based chr:pos:ref>alt'
' format (one per line)'
),
)
_add_common_args(parser_query)
parser_query.add_argument(
'--top_features',
type=int,
default=10,
help='Number of top features to show in breakdown table',
)
parser_query.add_argument(
'--format',
choices=['table', 'json', 'tsv'],
default='table',
help='Output format',
)
# Subcommand: region
parser_region = subparsers.add_parser(
'region',
help='In silico saturation mutagenesis scan across a genomic window',
)
parser_region.add_argument(
'--region',
'-r',
required=True,
help=(
'Genomic interval in 1-based closed chr:start-end format (e.g.'
' chr9:128225900-128226050)'
),
)
_add_common_args(parser_region)
parser_region.add_argument(
'--top_k',
type=int,
default=20,
help='Number of top variants to summarize',
)
parser_region.add_argument(
'--min_phred',
type=float,
default=0.0,
help='Minimum Phred score threshold for reporting',
)
parser_region.add_argument(
'--max_window_size',
type=int,
default=1000,
help='Maximum permitted window size in basepairs',
)
# Subcommand: metadata
parser_metadata = subparsers.add_parser(
'metadata',
help=(
'Dump and search Atlas database scorers, track catalogs, and AVI'
' feature attribution modalities'
),
)
parser_metadata.add_argument(
'--type',
choices=['scorers', 'tracks', 'features'],
default='scorers',
help='Type of metadata to dump or search',
)
parser_metadata.add_argument(
'--scorers',
action='store_const',
const='scorers',
dest='type',
help='Shortcut for --type scorers',
)
parser_metadata.add_argument(
'--tracks',
action='store_const',
const='tracks',
dest='type',
help='Shortcut for --type tracks',
)
parser_metadata.add_argument(
'--features',
action='store_const',
const='features',
dest='type',
help='Shortcut for --type features',
)
parser_metadata.add_argument(
'--scorer',
help='Filter tracks by Atlas scorer name (e.g. CHIP_TF, RNA_SEQ)',
)
parser_metadata.add_argument(
'--ontology',
help='Filter tracks by ontology CURIE (e.g. UBERON:0002107, CL:0002545)',
)
parser_metadata.add_argument(
'--query',
'-q',
help='Search text across track names, biosamples, and assay targets',
)
parser_metadata.add_argument(
'--top_n',
type=int,
default=50,
help='Maximum rows to print in table preview',
)
parser_metadata.add_argument(
'--format',
choices=['table', 'json', 'tsv'],
default='table',
help='Output format for stdout',
)
parser_metadata.add_argument(
'--output',
'-o',
help='Optional path to export metadata (.parquet, .tsv, .csv, .json)',
)
# Subcommand: gtf
parser_gtf = subparsers.add_parser(
'gtf',
help=(
'Query GENCODE v46 gene annotations, extract exons, CDS, UTRs, and'
' compute splice junction coordinates'
),
)
parser_gtf.add_argument(
'--gene',
help='Gene symbol (e.g. CAPN3, NAGS, TERT, DNM1)',
)
parser_gtf.add_argument(
'--gene_id',
help='Ensembl Gene ID (e.g. ENSG00000092529 or ENSG00000092529.26)',
)
parser_gtf.add_argument(
'--transcript_id',
help='Ensembl Transcript ID (e.g. ENST00000397163.8)',
)
parser_gtf.add_argument(
'--variant',
help=(
'Variant string in 1-based chr:pos:ref>alt format (e.g.'
' chr15:42387805:C>G)'
),
)
parser_gtf.add_argument(
'--region',
'-r',
help=(
'Genomic interval in 1-based closed chr:start-end format (e.g.'
' chr15:42380000-42420000)'
),
)
parser_gtf.add_argument(
'--all_transcripts',
action='store_true',
default=False,
help='Include all transcripts instead of filtering to MANE Select',
)
parser_gtf.add_argument(
'--protein_coding',
action='store_true',
default=False,
help='Filter to protein-coding genes/transcripts',
)
parser_gtf.add_argument(
'--longest',
action='store_true',
default=False,
help='Filter to longest transcript per gene',
)
parser_gtf.add_argument(
'--exons',
action='store_true',
default=False,
help=(
'Display detailed 1-based exon boundaries and donor/acceptor'
' coordinates'
),
)
parser_gtf.add_argument(
'--junctions',
action='store_true',
default=False,
help=(
'Display canonical and exon-skipping splice junctions with 1-based'
' coordinates and constructed Atlas ScoreIds'
),
)
parser_gtf.add_argument(
'--track',
type=str,
default=None,
help=(
'Track name for constructing splice junction ScoreIds (e.g.'
' junction_UBERON:0011907 gtex Muscle_Skeletal polyA plus RNA-seq)'
),
)
parser_gtf.add_argument(
'--cds',
action='store_true',
default=False,
help='Display 1-based coding sequence (CDS) intervals',
)
parser_gtf.add_argument(
'--utr',
action='store_true',
default=False,
help="Display 1-based 5' and 3' UTR intervals",
)
parser_gtf.add_argument(
'--format',
choices=['table', 'json', 'tsv'],
default='table',
help='Output format',
)
parser_gtf.add_argument(
'--output',
'-o',
help='Optional path to export tabular records (TSV, CSV, Parquet, JSON)',
)
args = parser.parse_args()
if args.subcommand == 'annotate':
handle_annotate(args)
elif args.subcommand == 'query':
handle_query(args)
elif args.subcommand == 'region':
handle_region(args)
elif args.subcommand == 'metadata':
handle_metadata(args)
elif args.subcommand == 'gtf':
handle_gtf(args)
if __name__ == '__main__':
main()
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Needs an API key, Needs Python
Security
What our scanner noticed· Low risk
- Scripts that your agent may run
This skill includes scripts that your agent may run. Read them before you use it.
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Published by
- Google DeepMind
- Publisher
- Google DeepMind (Official)
- Source
- google-deepmind/science-skills · skills/alphagenome_variant_impact_score · commit 4de6786
- Licence
- Apache License 2.0 (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Google DeepMind.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download alphagenome-variant-impact-score.zip