offtargeteffect's picture
Deploy mRNA Design Studio (Docker SDK)
99f834c verified
Raw
History Blame Contribute Delete
4.64 kB
"""
Kozak sequence analysis.
The Kozak consensus for vertebrates is: (GCC)GCCRCCATGG
Where R = A or G at position -3 relative to ATG.
Scoring follows Cavener & Ray (1991) positional weight matrix approach.
Positions scored: -6 to +4 relative to the A of ATG (+1 = A, +2 = T, +3 = G).
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import List, Optional, Tuple
# Kozak context: positions -6 to +4 (11 nt total, ATG at [6,7,8])
# Positional frequency matrix derived from vertebrate Kozak sequences.
# Rows: A, C, G, T. Columns: positions -6 through +4.
# Normalised to [0, 1] (1 = dominant base at that position).
_PFM: List[Tuple[float, float, float, float]] = [
# pos: -6 -5 -4 -3 -2 -1 +1(A) +2(T) +3(G) +4
(0.22, 0.28, 0.28, 0.46, 0.22, 0.22, 1.00, 0.00, 0.00, 0.25), # A
(0.28, 0.28, 0.18, 0.12, 0.22, 0.22, 0.00, 0.00, 0.00, 0.25), # C
(0.22, 0.22, 0.28, 0.30, 0.22, 0.22, 0.00, 0.00, 1.00, 0.25), # G
(0.28, 0.22, 0.26, 0.12, 0.34, 0.34, 0.00, 1.00, 0.00, 0.25), # T
]
_BASES = "ACGT"
_CONTEXT_LEN = 10 # positions -6 through +4 (10 positions around ATG start)
@dataclass
class KozakResult:
"""Result of Kozak consensus analysis for one ATG."""
atg_position: int # 0-based position of A in ATG within the full sequence
context: str # extracted Kozak context window
score: float # normalised score 0–1 (1 = perfect consensus)
has_optimal_r3: bool # A or G at position -3
matches_consensus: bool # True if score > 0.7 threshold
strength: str # "strong", "adequate", "weak"
def __repr__(self) -> str:
return (
f"KozakResult(pos={self.atg_position}, "
f"context={self.context!r}, score={self.score:.2f}, "
f"strength={self.strength!r})"
)
def _score_context(context: str) -> float:
"""Score a 10-nt Kozak context window against the PFM."""
if len(context) != 10:
return 0.0
total = 0.0
for i, nt in enumerate(context.upper()):
if nt not in _BASES:
continue
row = _BASES.index(nt)
total += _PFM[row][i]
# Max possible score: 10 (1.0 per position)
return total / 10.0
def _strength(score: float) -> str:
# Thresholds calibrated against achievable scores with the PFM above.
# Max achievable for ideal Kozak (GCCACCATGG) ≈ 0.48.
if score >= 0.43:
return "strong"
if score >= 0.33:
return "adequate"
return "weak"
def check_kozak(sequence: str, atg_position: Optional[int] = None) -> KozakResult:
"""
Analyse the Kozak context around the first (or specified) ATG in the sequence.
Parameters
----------
sequence : str
Nucleotide sequence (DNA).
atg_position : int, optional
0-based position of the ATG to analyse. If None, uses the first ATG found.
Returns
-------
KozakResult
"""
seq = sequence.upper().replace("U", "T")
if atg_position is None:
pos = seq.find("ATG")
if pos == -1:
raise ValueError("No ATG start codon found in sequence.")
else:
pos = atg_position
if seq[pos:pos+3] != "ATG":
raise ValueError(f"No ATG at position {pos}.")
# Extract context: 6 nt before ATG + ATG + 1 nt after = 10 nt total
ctx_start = pos - 6
ctx_end = pos + 4 # positions -6 to +4 (ATG at indices 6,7,8 of the 10-nt window)
# Pad with N if near sequence edges
left_pad = max(0, -ctx_start) * "N"
right_pad = max(0, ctx_end - len(seq)) * "N"
actual_start = max(0, ctx_start)
actual_end = min(len(seq), ctx_end)
context = left_pad + seq[actual_start:actual_end] + right_pad
score = _score_context(context)
# -3 position relative to ATG = index 3 in the 10-nt context
r3_base = context[3] if len(context) > 3 else "N"
has_r3 = r3_base in "AG"
return KozakResult(
atg_position=pos,
context=context,
score=score,
has_optimal_r3=has_r3,
matches_consensus=score >= 0.55,
strength=_strength(score),
)
def find_all_kozak_contexts(sequence: str, min_score: float = 0.0) -> List[KozakResult]:
"""Find and score Kozak contexts for every ATG in the sequence."""
seq = sequence.upper().replace("U", "T")
results = []
start = 0
while True:
pos = seq.find("ATG", start)
if pos == -1:
break
result = check_kozak(seq, pos)
if result.score >= min_score:
results.append(result)
start = pos + 1
return results