| """ |
| 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 |
|
|
|
|
| |
| |
| |
| |
| _PFM: List[Tuple[float, float, float, float]] = [ |
| |
| (0.22, 0.28, 0.28, 0.46, 0.22, 0.22, 1.00, 0.00, 0.00, 0.25), |
| (0.28, 0.28, 0.18, 0.12, 0.22, 0.22, 0.00, 0.00, 0.00, 0.25), |
| (0.22, 0.22, 0.28, 0.30, 0.22, 0.22, 0.00, 0.00, 1.00, 0.25), |
| (0.28, 0.22, 0.26, 0.12, 0.34, 0.34, 0.00, 1.00, 0.00, 0.25), |
| ] |
| _BASES = "ACGT" |
| _CONTEXT_LEN = 10 |
|
|
|
|
| @dataclass |
| class KozakResult: |
| """Result of Kozak consensus analysis for one ATG.""" |
| atg_position: int |
| context: str |
| score: float |
| has_optimal_r3: bool |
| matches_consensus: bool |
| strength: str |
|
|
| 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] |
| |
| return total / 10.0 |
|
|
|
|
| def _strength(score: float) -> str: |
| |
| |
| 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}.") |
|
|
| |
| ctx_start = pos - 6 |
| ctx_end = pos + 4 |
| |
| 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) |
|
|
| |
| 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 |
|
|