Bio core computational genetics
Skill Pavel-Kravchenko/Bioinformatics/Skills/bio-core-computational-genetics
Translate DNA per-frame, score codon usage bias (RSCU/CAI), simulate restriction digests/ORFs, and test three-point-cross mapping and Hardy-Weinberg equilibrium. Use for CAI, virtual digests, crossover mapping, or HWE tests.From its SKILL.md
npx -y skills add Pavel-Kravchenko/Bioinformatics --skill bio-core-computational-geneticsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 4 stars4 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
11.2 KB, ~3.1k tokens by cl100k_base, as published. Nobody here has run it
Computational Genetics
When to Use
- Translating DNA in a given reading frame or finding ORFs across all 6 frames
- Computing codon usage bias (RSCU) or Codon Adaptation Index (CAI) for codon optimization / heterologous expression
- Simulating a restriction enzyme digest (fragment sizes, virtual gel) for cloning design
- Mapping gene order and distance from two-point or three-point cross data
- Testing observed genotype counts against Hardy-Weinberg equilibrium, or computing Ts/Tv ratio and CpG O/E from aligned sequences
Version Compatibility
Python >= 3.10, NumPy >= 1.24, SciPy >= 1.11 (for exact chi-squared p-values), Matplotlib >= 3.8. No external bioinformatics package is required for the core logic below; Biopython (Bio.Seq, Bio.Data.CodonTable) is a drop-in replacement for the hand-rolled genetic code if already in your environment.
Prerequisites
pip install numpy scipy matplotlib- Comfortable with basic Mendelian genetics (dominant/recessive, linkage, crossover) and DNA/codon notation
- Related skills:
bio-sequence-manipulation-codon-usage,bio-sequence-manipulation-transcription-translation,bio-restriction-analysis-restriction-mapping
Genetic Code, Translation, and Codon Usage
Goal: Build the standard genetic code table, translate DNA in a chosen frame, and score a coding sequence's codon usage bias (RSCU, CAI) against a reference.
Approach: Generate the 64-codon table programmatically (avoids typos), translate stopping at the first in-frame stop codon, then compute RSCU per synonymous family and CAI as the geometric mean of relative codon adaptiveness.
from collections import defaultdict, Counter
import math
bases = 'TCAG'
amino_acids = 'FFLLSSSSYY**CC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG'
codon_table = {}
for i, first in enumerate(bases):
for j, second in enumerate(bases):
for k, third in enumerate(bases):
codon_table[first + second + third] = amino_acids[i * 16 + j * 4 + k]
stop_codons = {c for c, a in codon_table.items() if a == '*'}
def translate(dna_sequence, codon_table, stop_at_stop=True):
"""Translate a DNA sequence codon-by-codon, stopping at the first stop codon."""
seq = dna_sequence.upper()
protein = []
for i in range(0, len(seq) - 2, 3):
codon = seq[i:i + 3]
aa = codon_table.get(codon, 'X')
if aa == '*' and stop_at_stop:
break
protein.append(aa)
return ''.join(protein)
def translate_frame(dna_sequence, codon_table, frame=0):
"""Translate starting from a given reading frame (0, 1, or 2)."""
return translate(dna_sequence[frame:], codon_table)
def count_codons(coding_sequence):
"""Count in-frame codons in a coding sequence (must start at position 0)."""
seq = coding_sequence.upper()
counts = Counter()
for i in range(0, len(seq) - 2, 3):
codon = seq[i:i + 3]
if len(codon) == 3 and 'N' not in codon:
counts[codon] += 1
return counts
def compute_rscu(codon_counts, codon_table):
"""RSCU_ij = observed count / expected count under uniform synonymous usage."""
aa_groups = defaultdict(list)
for codon, aa in codon_table.items():
if aa != '*':
aa_groups[aa].append(codon)
rscu = {}
for aa, synonyms in aa_groups.items():
total = sum(codon_counts.get(c, 0) for c in synonyms)
expected = total / len(synonyms)
for codon in synonyms:
rscu[codon] = (codon_counts.get(codon, 0) / expected) if expected > 0 else 0.0
return rscu
def compute_cai(gene_sequence, rscu_reference, codon_table):
"""CAI = geometric mean of w_i = RSCU_i / max(RSCU) within each synonymous family."""
aa_groups = defaultdict(list)
for codon, aa in codon_table.items():
if aa != '*':
aa_groups[aa].append(codon)
max_rscu = {aa: max(rscu_reference.get(c, 0) for c in syns) for aa, syns in aa_groups.items()}
w = {c: (rscu_reference.get(c, 0) / max_rscu[aa] if max_rscu[aa] > 0 else 0)
for c, aa in codon_table.items() if aa != '*'}
log_w = []
seq = gene_sequence.upper()
for i in range(0, len(seq) - 2, 3):
codon = seq[i:i + 3]
wi = w.get(codon, 0)
if wi > 0:
log_w.append(math.log(wi))
return math.exp(sum(log_w) / len(log_w)) if log_w else 0.0
test_seq = 'TAATGCCCGAATTTGCCTAAATGGGCAAATAG'
for frame in range(3):
print(f'Frame {frame}: {translate_frame(test_seq, codon_table, frame) or "(no ORF)"}')
Virtual Restriction Digest and ORF Finding
Goal: Predict fragment sizes from a multi-enzyme restriction digest and locate ORFs in all 6 reading frames.
Approach: Store each enzyme as {site, cut} (cut offset from the start of the recognition site on the top strand), scan for all occurrences, sort cut positions, and take consecutive differences as fragment sizes. ORF finding scans both strands in 3 frames for ATG...stop runs.
restriction_enzymes = {
'EcoRI': {'site': 'GAATTC', 'cut': 1},
'BamHI': {'site': 'GGATCC', 'cut': 1},
'HindIII': {'site': 'AAGCTT', 'cut': 1},
'SmaI': {'site': 'CCCGGG', 'cut': 3},
'NotI': {'site': 'GCGGCCGC', 'cut': 2},
}
def reverse_complement(seq):
"""Reverse complement of a DNA sequence."""
return seq.upper().translate(str.maketrans('ACGT', 'TGCA'))[::-1]
def find_cut_sites(sequence, enzyme_info):
"""Return absolute cut positions for one enzyme in a linear sequence."""
site, cut_offset = enzyme_info['site'], enzyme_info['cut']
seq_upper = sequence.upper()
cuts, start = [], 0
while True:
pos = seq_upper.find(site, start)
if pos == -1:
break
cuts.append(pos + cut_offset)
start = pos + 1
return cuts
def virtual_digest(sequence, selected_enzymes, restriction_enzymes):
"""Multi-enzyme virtual digest of a linear sequence -> fragment sizes (largest first)."""
all_cuts = sorted(
(cut, name)
for name in selected_enzymes
for cut in find_cut_sites(sequence, restriction_enzymes[name])
)
boundaries = [0] + [c for c, _ in all_cuts] + [len(sequence)]
fragments = [boundaries[i + 1] - boundaries[i] for i in range(len(boundaries) - 1) if boundaries[i + 1] > boundaries[i]]
return sorted(fragments, reverse=True), all_cuts
def find_orfs(sequence, codon_table, min_length=30):
"""Find ORFs (ATG...stop) across all 6 reading frames; returns list of dicts sorted by length."""
seq = sequence.upper()
stops = {c for c, a in codon_table.items() if a == '*'}
def scan(strand_seq, label):
orfs = []
for frame in range(3):
i, orf_start = frame, None
while i <= len(strand_seq) - 3:
codon = strand_seq[i:i + 3]
if codon == 'ATG' and orf_start is None:
orf_start = i
elif codon in stops and orf_start is not None:
if i - orf_start >= min_length:
protein = translate(strand_seq[orf_start:i], codon_table, stop_at_stop=False)
orfs.append({'start': orf_start, 'end': i + 3, 'frame': frame + 1,
'strand': label, 'length_nt': i - orf_start, 'protein': protein})
orf_start = None
i += 3
return orfs
return sorted(scan(seq, '+') + scan(reverse_complement(seq), '-'), key=lambda o: -o['length_nt'])
plasmid = 'GAATTC' + 'A' * 500 + 'GGATCC' + 'A' * 800 + 'AAGCTT' + 'A' * 300 + 'GAATTC' + 'A' * 400
fragments, cuts = virtual_digest(plasmid, ['EcoRI', 'BamHI', 'HindIII'], restriction_enzymes)
print(f'Fragments (bp): {fragments}, total={sum(fragments)} (== {len(plasmid)})')
Classical Genetics: Three-Point Cross and Hardy-Weinberg
Goal: Order genes and compute map distances from three-point testcross progeny; test genotype counts for Hardy-Weinberg equilibrium.
Approach: The rarest progeny classes are double crossovers — the gene that differs between the two double-CO classes and the parental classes is the middle gene. Map distance (cM) = (single COs in that region + double COs) / total x 100. For HWE, estimate allele frequencies from observed genotype counts, compute expected Hardy-Weinberg counts, and run a chi-squared goodness-of-fit test (1 df for a biallelic locus).
from scipy.stats import chi2 as chi2_dist
three_point_data = {'+++': 580, 'abc': 592, 'ab+': 85, '++c': 83,
'a++': 218, '+bc': 214, 'a+c': 22, '+b+': 18}
total = sum(three_point_data.values())
# 'a+c' and '+b+' are the smallest (double-CO) classes -> b is the middle gene
double_co = three_point_data['a+c'] + three_point_data['+b+']
dist_a_b = (three_point_data['a++'] + three_point_data['+bc'] + double_co) / total * 100
dist_b_c = (three_point_data['ab+'] + three_point_data['++c'] + double_co) / total * 100
print(f'Order a-b-c: a-b = {dist_a_b:.1f} cM, b-c = {dist_b_c:.1f} cM')
expected_double_co = (dist_a_b / 100) * (dist_b_c / 100) * total
coc = double_co / expected_double_co
print(f'Coefficient of coincidence = {coc:.3f}, interference = {1 - coc:.3f}')
def hwe_chi_squared(n_AA, n_Aa, n_aa):
"""Estimate allele freqs, expected HWE genotype counts, and chi-squared test (df=1)."""
n_total = n_AA + n_Aa + n_aa
p = (2 * n_AA + n_Aa) / (2 * n_total)
q = 1 - p
expected = {'AA': p ** 2 * n_total, 'Aa': 2 * p * q * n_total, 'aa': q ** 2 * n_total}
observed = {'AA': n_AA, 'Aa': n_Aa, 'aa': n_aa}
chi2_stat = sum((observed[g] - expected[g]) ** 2 / expected[g] for g in observed)
p_value = chi2_dist.sf(chi2_stat, df=1)
return p, q, chi2_stat, p_value
p, q, chi2_stat, p_value = hwe_chi_squared(320, 480, 200)
print(f'p={p:.3f}, q={q:.3f}, chi2={chi2_stat:.3f}, p-value={p_value:.4f}')
print('Deviates from HWE' if p_value < 0.05 else 'Consistent with HWE')
Pitfalls
- Undefined
translate(): naivetranslate_frameimplementations call atranslate()helper that was never defined — always define it explicitly (shown above) rather than assuming Biopython'sSeq.translate()is in scope. - Coordinate systems: BED is 0-based half-open; VCF/GFF/genetic-map cM positions are 1-based inclusive — mixing them causes off-by-one errors in cut sites and ORF coordinates.
- CAI reference set: CAI is only meaningful relative to a reference set of highly expressed genes (ribosomal proteins, glycolytic enzymes) from the same organism — using the wrong organism or a non-HEG reference gives misleading scores.
- Double crossovers: the smallest progeny classes in a three-point cross are the double-CO classes; misidentifying them flips the inferred gene order.
- HWE p-value approximation: without SciPy, chi-squared p-values need a proper CDF (Wilson-Hilferty or similar) — a crude approximation can flip significance calls near p = 0.05.
See Also
bio-sequence-manipulation-codon-usagebio-restriction-analysis-restriction-mappingbio-population-genetics-population-structurebio-population-genetics-selection-statistics
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.