GPN-Star demo#

Use the published vertebrate GPN-Star checkpoint to inspect alignment-aware nucleotide probabilities, raw log-likelihood ratios, and the existing mutation-rate calibration example. This notebook uses only the tiny checked-in 128 bp alignment fixture; it never downloads a whole-genome MSA.

Setup#

from hashlib import sha256
from importlib.metadata import version
from pathlib import Path
from urllib.request import urlretrieve

from gpn import register_auto_classes
from gpn.data import Tokenizer
from huggingface_hub import hf_hub_download
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
import torch
from transformers import AutoModelForMaskedLM

register_auto_classes("star")
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(
    f"gpn={version('gpn')}, transformers={version('transformers')}, "
    f"torch={version('torch')}, device={DEVICE}"
)
gpn=0.9.0a1, transformers=5.15.0, torch=2.13.0+cpu, device=cpu
MODEL_ID = "songlab/gpn-star-hg38-v100-200m"
MODEL_REVISION = "0c949f132d35619a3eb188b402848c998a3313ae"
print(f"{MODEL_ID} @ {MODEL_REVISION}")
songlab/gpn-star-hg38-v100-200m @ 0c949f132d35619a3eb188b402848c998a3313ae

Tiny alignment fixture#

The original example locus is the zero-based, half-open hg38 interval chr6:31575665-31575793. The fixture contains the same target sequence and the 99 source-species columns expected by the vertebrate checkpoint.

Open the locus in the UCSC Genome Browser

FIXTURE_PATH = Path("hg38_chr6_31575665_31575793_multiz100way.npz")
FIXTURE_COMMIT = "7553aa4469c32404f1c595d33f4d165de8dae978"
FIXTURE_URL = (
    f"https://raw.githubusercontent.com/songlab-cal/gpn/{FIXTURE_COMMIT}/"
    "tests/fixtures/hg38_chr6_31575665_31575793_multiz100way.npz"
)
FIXTURE_SHA256 = "3e0a627626ce319932add04bd683d2151af7badfe3dc0e122f39c5e8492600c0"
if not FIXTURE_PATH.exists():
    urlretrieve(FIXTURE_URL, FIXTURE_PATH)
fixture_sha256 = sha256(FIXTURE_PATH.read_bytes()).hexdigest()
if fixture_sha256 != FIXTURE_SHA256:
    raise ValueError(f"Unexpected alignment fixture SHA-256: {fixture_sha256}")
with np.load(FIXTURE_PATH, allow_pickle=False) as fixture:
    alignment_tokens = fixture["gpn_star_v100_tokens"].astype(np.int64)

tokenizer = Tokenizer()
token_to_base = np.array(list(tokenizer.vocab), dtype="U1")
raw_alignment = token_to_base[alignment_tokens]
msa = torch.from_numpy(alignment_tokens).unsqueeze(0).to(DEVICE)
print(msa.shape)
raw_alignment[:, 0]
torch.Size([1, 128, 100])
array(['A', 'C', 'A', 'A', 'G', 'C', 'T', 'G', 'C', 'C', 'A', 'G', 'G',
       'C', 'A', 'G', 'G', 'T', 'T', 'C', 'T', 'C', 'T', 'T', 'C', 'C',
       'T', 'C', 'T', 'C', 'A', 'C', 'A', 'T', 'A', 'C', 'T', 'G', 'A',
       'C', 'C', 'C', 'A', 'C', 'G', 'G', 'C', 'T', 'C', 'C', 'A', 'C',
       'C', 'C', 'T', 'C', 'T', 'C', 'T', 'C', 'C', 'C', 'C', 'T', 'G',
       'G', 'A', 'A', 'A', 'G', 'G', 'A', 'C', 'A', 'C', 'C', 'A', 'T',
       'G', 'A', 'G', 'C', 'A', 'C', 'T', 'G', 'A', 'A', 'A', 'G', 'C',
       'A', 'T', 'G', 'A', 'T', 'C', 'C', 'G', 'G', 'G', 'A', 'C', 'G',
       'T', 'G', 'G', 'A', 'G', 'C', 'T', 'G', 'G', 'C', 'C', 'G', 'A',
       'G', 'G', 'A', 'G', 'G', 'C', 'G', 'C', 'T', 'C', 'C'], dtype='<U1')

Masked nucleotide probabilities#

model_for_mlm = AutoModelForMaskedLM.from_pretrained(
    MODEL_ID, revision=MODEL_REVISION
).to(DEVICE).eval()

position = 76
print("model dtype:", next(model_for_mlm.parameters()).dtype)
print("Start codon:", "".join(raw_alignment[position : position + 3, 0]))
input_ids = msa[:, :, :1].clone()
input_ids[0, position, 0] = tokenizer.mask_token_id()

with torch.inference_mode():
    all_logits = model_for_mlm(
        input_ids=input_ids,
        source_ids=msa,
        target_species=torch.tensor([[0]], device=DEVICE),
    ).logits

nucleotides = list("ACGT")
nucleotide_ids = [tokenizer.vocab.index(base) for base in nucleotides]
logits = all_logits[0, position, 0, nucleotide_ids].cpu()
probabilities = torch.softmax(logits, dim=0).numpy()
pd.DataFrame(
    {"nucleotide": nucleotides, "logit": logits.numpy(), "probability": probabilities}
)
model dtype: torch.float32
Start codon: ATG
nucleotide logit probability
0 A 7.706210 0.999677
1 C -1.922049 0.000066
2 G -0.691238 0.000225
3 T -2.650750 0.000032
probability_frame = pd.DataFrame(
    {"nucleotide": nucleotides, "probability": probabilities}
)
sns.barplot(data=probability_frame, x="nucleotide", y="probability")
plt.show()
../../_images/675ee7e6c010ccdc8fb751aa6cc6de99a79e39f79fc00b1f167116f0a6234d11.png

Raw log-likelihood ratios#

reference = raw_alignment[position, 0]
alternate_alleles = [base for base in nucleotides if base != reference]
reference_logit = logits[nucleotides.index(reference)]
raw_llrs = [
    logits[nucleotides.index(alternate)].item() - reference_logit.item()
    for alternate in alternate_alleles
]
llr_frame = pd.DataFrame(
    {
        "variant": [f"{reference}>{alternate}" for alternate in alternate_alleles],
        "alternate": alternate_alleles,
        "raw LLR": raw_llrs,
    }
)
llr_frame
variant alternate raw LLR
0 A>C C -9.628258
1 A>G G -8.397448
2 A>T T -10.356960
sns.barplot(data=llr_frame, x="variant", y="raw LLR")
plt.show()
../../_images/1b4826ada7599d428d52a5d465e196e8ec4949043edc05d982875ccce861d90f.png

Mutation-rate calibration#

The published calibration subtracts the neutral mean for the central pentanucleotide and alternate allele. This is separate from the raw LLR emitted by the maintained CLI.

context = "".join(raw_alignment[position - 2 : position + 3, 0])
calibration_path = hf_hub_download(
    MODEL_ID,
    "calibration_table/llr.parquet",
    revision=MODEL_REVISION,
)
calibration = pd.read_parquet(calibration_path)
llr_frame["pentanucleotide mutation"] = [
    f"{context}_{alternate}" for alternate in alternate_alleles
]
llr_frame = llr_frame.merge(
    calibration,
    left_on="pentanucleotide mutation",
    right_on="pentanuc_mut",
    how="left",
)
llr_frame["calibrated LLR"] = (
    llr_frame["raw LLR"] - llr_frame["llr_neutral_mean"]
)
llr_frame[["variant", "raw LLR", "llr_neutral_mean", "calibrated LLR"]]
variant raw LLR llr_neutral_mean calibrated LLR
0 A>C -9.628258 -1.431537 -8.196722
1 A>G -8.397448 -1.295980 -7.101467
2 A>T -10.356960 -1.452028 -8.904932
sns.barplot(data=llr_frame, x="variant", y="calibrated LLR")
plt.show()
../../_images/1a1c181cc91c94050fc64139966bf7018db2e0efc5395562572bb02da4395534.png