Skip to content

Commit e480b31

Browse files
imjiaoyuanclaude
andcommitted
Feat: add genome/cai.py — per-gene Codon Adaptation Index
- Extract shared codon utilities to genome/core.py: AA_TABLE, iter_codons(), make_aa_to_codons(), calculate_cai() - Refactor genome/codon.py to use shared functions from genome/core.py - New genome/cai.py: computes per-gene CAI against a reference of highly expressed genes (unlike codon --cai which gives one global value) - 6 new tests, all pass Zero new dependencies — pure biopython + stdlib. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent e32790e commit e480b31

7 files changed

Lines changed: 283 additions & 109 deletions

File tree

docs/en/module-genome.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# jsrc genome
22

3-
Genome-level analysis tools. `jsrc genome` covers genome statistics, feature detection, comparative analysis, evolutionary analysis, and annotation utilities.
3+
Genome-level analysis tools. `jsrc genome` covers genome statistics, feature detection, codon analysis, comparative analysis, evolutionary analysis, and annotation utilities.
44

55
## cpg
66

@@ -112,6 +112,19 @@ jsrc genome window -fa genome.fa
112112
jsrc genome window -fa genome.fa --window 50000 --step 10000 --json
113113
```
114114

115+
## cai
116+
117+
Codon Adaptation Index measures how well a gene's codon usage matches a reference set of highly expressed genes. CAI ranges from 0 to 1; higher values indicate stronger similarity to the reference (and potentially higher expression).
118+
119+
Unlike `codon --cai` which computes a single global CAI for all input sequences combined, this command computes per-gene CAI values. Each gene in the query FASTA gets an individual score, making it suitable for genome-wide CAI profiling.
120+
121+
The reference should be a FASTA of highly expressed genes (e.g., ribosomal proteins, elongation factors). Both query and reference should be CDS sequences — introns and UTRs are not handled.
122+
123+
```bash
124+
jsrc genome cai -fa all_genes.fa --reference highly_expressed.fa
125+
jsrc genome cai -fa all_genes.fa --reference highly_expressed.fa --json
126+
```
127+
115128
## codon
116129

117130
Codon usage frequency and RSCU (Relative Synonymous Codon Usage) analysis. Input CDS sequences in FASTA format to count codon occurrences and calculate RSCU.

docs/zh/module-genome.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# jsrc genome
22

3-
基因组级别的分析功能。`jsrc genome` 涵盖了基因组统计、特征检测、比较分析、进化分析和注释辅助等常用功能。
3+
基因组级别的分析功能。`jsrc genome` 涵盖了基因组统计、特征检测、密码子分析、比较分析、进化分析和注释辅助等常用功能。
44

55
## cpg
66

@@ -112,6 +112,19 @@ jsrc genome window -fa genome.fa
112112
jsrc genome window -fa genome.fa --window 50000 --step 10000 --json
113113
```
114114

115+
## cai
116+
117+
密码子适应指数(CAI),衡量一个基因的密码子使用模式与参考高表达基因集的匹配程度。CAI 范围 0 到 1,值越高表示与参考集越相似(通常意味着更高的表达潜力)。
118+
119+
`codon --cai`(对所有输入序列计算一个全局 CAI 值)不同,这个命令对每个基因单独计算 CAI,适合全基因组范围的 CAI 分析。
120+
121+
参考序列应使用高表达基因的 CDS(如核糖体蛋白、延伸因子等)。输入的查询和参考都应该是 CDS 序列,不处理内含子和 UTR。
122+
123+
```bash
124+
jsrc genome cai -fa all_genes.fa --reference highly_expressed.fa
125+
jsrc genome cai -fa all_genes.fa --reference highly_expressed.fa --json
126+
```
127+
115128
## codon
116129

117130
密码子使用频率和 RSCU(相对同义密码子使用度)分析。输入 CDS 序列的 FASTA,统计每个密码子的出现次数并计算 RSCU。

src/jsrc/genome/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"gc-skew": ("jsrc.genome.gc_skew", "Cumulative GC skew for replication origin"),
1616
"window": ("jsrc.genome.window", "Sliding-window GC and AT skew"),
1717
"codon": ("jsrc.genome.codon", "Codon usage and RSCU analysis"),
18+
"cai": ("jsrc.genome.cai", "Codon Adaptation Index"),
1819
"distance": ("jsrc.genome.distance", "Calculate pairwise genetic distances"),
1920
"kaks": ("jsrc.genome.kaks", "Calculate Ka/Ks ratio for two aligned CDS sequences"),
2021
"density": ("jsrc.genome.density", "Calculate gene/feature density along genome"),

src/jsrc/genome/cai.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import json
2+
import logging
3+
from argparse import Namespace
4+
from collections import Counter
5+
from typing import Any
6+
7+
from Bio import SeqIO
8+
9+
from jsrc.core import DataFormatError
10+
from jsrc.genome.core import (
11+
AA_TABLE,
12+
calculate_cai,
13+
iter_codons,
14+
make_aa_to_codons,
15+
)
16+
17+
logger = logging.getLogger(__name__)
18+
19+
20+
def cmd(args: Namespace) -> None:
21+
aa_to_codons = make_aa_to_codons(AA_TABLE)
22+
23+
ref_counts: Counter[str] = Counter()
24+
ref_records = list(SeqIO.parse(args.reference, "fasta"))
25+
if not ref_records:
26+
raise DataFormatError("No sequences found in reference FASTA")
27+
for rec in ref_records:
28+
for codon in iter_codons(str(rec.seq)):
29+
if AA_TABLE.get(codon) != "*":
30+
ref_counts[codon] += 1
31+
if not ref_counts:
32+
raise DataFormatError("No valid codons found in reference FASTA")
33+
logger.info(
34+
"Reference: %d sequences, %d codons",
35+
len(ref_records),
36+
sum(ref_counts.values()),
37+
)
38+
39+
query_records = list(SeqIO.parse(args.fa, "fasta"))
40+
if not query_records:
41+
raise DataFormatError("No sequences found in query FASTA")
42+
43+
results = []
44+
for rec in query_records:
45+
gene_counts: Counter[str] = Counter()
46+
for codon in iter_codons(str(rec.seq)):
47+
if AA_TABLE.get(codon) != "*":
48+
gene_counts[codon] += 1
49+
cai = calculate_cai(gene_counts, ref_counts, aa_to_codons)
50+
results.append(
51+
{
52+
"id": rec.id,
53+
"codon_count": sum(gene_counts.values()),
54+
"cai": round(cai, 6),
55+
}
56+
)
57+
58+
if args.json:
59+
print(json.dumps(results, ensure_ascii=False, indent=2))
60+
return
61+
print("id\tcodon_count\tcai")
62+
for r in results:
63+
print(f"{r['id']}\t{r['codon_count']}\t{r['cai']:.6f}")
64+
logger.info("Computed CAI for %d genes", len(results))
65+
66+
67+
def register(subparsers: Any) -> None:
68+
p = subparsers.add_parser(
69+
"cai", help="Codon Adaptation Index for each gene"
70+
)
71+
p.add_argument("-fa", required=True, help="Query CDS FASTA file")
72+
p.add_argument(
73+
"--reference",
74+
required=True,
75+
help="Reference CDS FASTA file (highly expressed genes)",
76+
)
77+
p.add_argument("--json", action="store_true", help="Print JSON output")
78+
p.set_defaults(func=cmd)

src/jsrc/genome/codon.py

Lines changed: 11 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,112 +1,19 @@
11
import json
22
import logging
3-
import math
43
from argparse import Namespace
54
from collections import Counter, defaultdict
6-
from collections.abc import Iterator
75
from typing import Any
86

97
from Bio import SeqIO
108

11-
logger = logging.getLogger(__name__)
9+
from jsrc.genome.core import (
10+
AA_TABLE,
11+
calculate_cai,
12+
iter_codons,
13+
make_aa_to_codons,
14+
)
1215

13-
AA_TABLE = {
14-
"TTT": "F",
15-
"TTC": "F",
16-
"TTA": "L",
17-
"TTG": "L",
18-
"CTT": "L",
19-
"CTC": "L",
20-
"CTA": "L",
21-
"CTG": "L",
22-
"ATT": "I",
23-
"ATC": "I",
24-
"ATA": "I",
25-
"ATG": "M",
26-
"GTT": "V",
27-
"GTC": "V",
28-
"GTA": "V",
29-
"GTG": "V",
30-
"TCT": "S",
31-
"TCC": "S",
32-
"TCA": "S",
33-
"TCG": "S",
34-
"CCT": "P",
35-
"CCC": "P",
36-
"CCA": "P",
37-
"CCG": "P",
38-
"ACT": "T",
39-
"ACC": "T",
40-
"ACA": "T",
41-
"ACG": "T",
42-
"GCT": "A",
43-
"GCC": "A",
44-
"GCA": "A",
45-
"GCG": "A",
46-
"TAT": "Y",
47-
"TAC": "Y",
48-
"TAA": "*",
49-
"TAG": "*",
50-
"CAT": "H",
51-
"CAC": "H",
52-
"CAA": "Q",
53-
"CAG": "Q",
54-
"AAT": "N",
55-
"AAC": "N",
56-
"AAA": "K",
57-
"AAG": "K",
58-
"GAT": "D",
59-
"GAC": "D",
60-
"GAA": "E",
61-
"GAG": "E",
62-
"TGT": "C",
63-
"TGC": "C",
64-
"TGA": "*",
65-
"TGG": "W",
66-
"CGT": "R",
67-
"CGC": "R",
68-
"CGA": "R",
69-
"CGG": "R",
70-
"AGT": "S",
71-
"AGC": "S",
72-
"AGA": "R",
73-
"AGG": "R",
74-
"GGT": "G",
75-
"GGC": "G",
76-
"GGA": "G",
77-
"GGG": "G",
78-
}
79-
80-
81-
def _iter_codons(seq: str) -> Iterator[str]:
82-
seq = seq.upper().replace("U", "T")
83-
for i in range(0, len(seq) - 2, 3):
84-
c = seq[i : i + 3]
85-
if len(c) == 3 and set(c) <= {"A", "C", "G", "T"}:
86-
yield c
87-
88-
89-
def _calculate_cai(
90-
counts: Counter[str], ref_counts: Counter[str], aa_to_codons: dict[str, list[str]]
91-
) -> float:
92-
w_values = {}
93-
for _aa, codons in aa_to_codons.items():
94-
max_count = max((ref_counts[c] for c in codons), default=0)
95-
if max_count == 0:
96-
for c in codons:
97-
w_values[c] = 1.0
98-
else:
99-
for c in codons:
100-
w_values[c] = ref_counts[c] / max_count
101-
102-
log_sum = 0.0
103-
total = 0
104-
for codon, count in counts.items():
105-
if codon in w_values and count > 0:
106-
log_sum += count * math.log(w_values[codon]) if w_values[codon] > 0 else 0
107-
total += count
108-
109-
return math.exp(log_sum / total) if total > 0 else 0.0
16+
logger = logging.getLogger(__name__)
11017

11118

11219
def _calculate_enc(counts: Counter[str], aa_to_codons: dict[str, list[str]]) -> float:
@@ -144,14 +51,11 @@ def homozygosity(codons: list[str]) -> float:
14451

14552
def cmd(args: Namespace) -> None:
14653
counts: Counter[str] = Counter()
147-
aa_to_codons: dict[str, list[str]] = defaultdict(list)
148-
for codon, aa in AA_TABLE.items():
149-
if aa != "*":
150-
aa_to_codons[aa].append(codon)
54+
aa_to_codons = make_aa_to_codons(AA_TABLE)
15155

15256
total_codons = 0
15357
for rec in SeqIO.parse(args.fa, "fasta"):
154-
for codon in _iter_codons(str(rec.seq)):
58+
for codon in iter_codons(str(rec.seq)):
15559
if AA_TABLE.get(codon) == "*":
15660
continue
15761
counts[codon] += 1
@@ -172,10 +76,10 @@ def cmd(args: Namespace) -> None:
17276
if args.cai:
17377
ref_counts: Counter[str] = Counter()
17478
for rec in SeqIO.parse(args.cai, "fasta"):
175-
for codon in _iter_codons(str(rec.seq)):
79+
for codon in iter_codons(str(rec.seq)):
17680
if AA_TABLE.get(codon) != "*":
17781
ref_counts[codon] += 1
178-
cai_value = _calculate_cai(counts, ref_counts, aa_to_codons)
82+
cai_value = calculate_cai(counts, ref_counts, aa_to_codons)
17983

18084
enc_value = None
18185
if args.enc:

src/jsrc/genome/core.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,75 @@
1+
import math
2+
from collections import Counter, defaultdict
3+
from collections.abc import Iterator
4+
5+
16
def normalize_sequence(seq: str) -> str:
27
return seq.upper().replace("U", "T")
38

49

10+
AA_TABLE: dict[str, str] = {
11+
"TTT": "F", "TTC": "F", "TTA": "L", "TTG": "L",
12+
"CTT": "L", "CTC": "L", "CTA": "L", "CTG": "L",
13+
"ATT": "I", "ATC": "I", "ATA": "I", "ATG": "M",
14+
"GTT": "V", "GTC": "V", "GTA": "V", "GTG": "V",
15+
"TCT": "S", "TCC": "S", "TCA": "S", "TCG": "S",
16+
"CCT": "P", "CCC": "P", "CCA": "P", "CCG": "P",
17+
"ACT": "T", "ACC": "T", "ACA": "T", "ACG": "T",
18+
"GCT": "A", "GCC": "A", "GCA": "A", "GCG": "A",
19+
"TAT": "Y", "TAC": "Y", "TAA": "*", "TAG": "*",
20+
"CAT": "H", "CAC": "H", "CAA": "Q", "CAG": "Q",
21+
"AAT": "N", "AAC": "N", "AAA": "K", "AAG": "K",
22+
"GAT": "D", "GAC": "D", "GAA": "E", "GAG": "E",
23+
"TGT": "C", "TGC": "C", "TGA": "*", "TGG": "W",
24+
"CGT": "R", "CGC": "R", "CGA": "R", "CGG": "R",
25+
"AGT": "S", "AGC": "S", "AGA": "R", "AGG": "R",
26+
"GGT": "G", "GGC": "G", "GGA": "G", "GGG": "G",
27+
}
28+
29+
30+
def iter_codons(seq: str) -> Iterator[str]:
31+
seq = seq.upper().replace("U", "T")
32+
for i in range(0, len(seq) - 2, 3):
33+
c = seq[i : i + 3]
34+
if len(c) == 3 and set(c) <= {"A", "C", "G", "T"}:
35+
yield c
36+
37+
38+
def make_aa_to_codons(codon_table: dict[str, str] | None = None) -> dict[str, list[str]]:
39+
if codon_table is None:
40+
codon_table = AA_TABLE
41+
result: dict[str, list[str]] = defaultdict(list)
42+
for codon, aa in codon_table.items():
43+
if aa != "*":
44+
result[aa].append(codon)
45+
return dict(result)
46+
47+
48+
def calculate_cai(
49+
counts: Counter[str],
50+
ref_counts: Counter[str],
51+
aa_to_codons: dict[str, list[str]],
52+
) -> float:
53+
w_values: dict[str, float] = {}
54+
for _aa, codons in aa_to_codons.items():
55+
max_count = max((ref_counts[c] for c in codons), default=0)
56+
if max_count == 0:
57+
for c in codons:
58+
w_values[c] = 1.0
59+
else:
60+
for c in codons:
61+
w_values[c] = ref_counts[c] / max_count
62+
63+
log_sum = 0.0
64+
total = 0
65+
for codon, count in counts.items():
66+
if codon in w_values and count > 0:
67+
log_sum += count * math.log(w_values[codon]) if w_values[codon] > 0 else 0
68+
total += count
69+
70+
return math.exp(log_sum / total) if total > 0 else 0.0
71+
72+
573
def gc_content(seq: str) -> float:
674
seq = normalize_sequence(seq)
775
g = seq.count("G")

0 commit comments

Comments
 (0)