Selection pressure between two genomes (dN/dS)
TL;DR: dN/dS is a per-ortholog-pair measurement, so it is a colour on a
synteny track. This builds a human against rhesus macaque ortholog table with
jcvi and measures dN and dS on every pair with kaks_from_pairs.py.
Prerequisites
- a JBrowse to open them in: Desktop takes a local file by path, Web through Add track
- jcvi
- DIAMOND
- python3 with biopython
- htslib (
bgzip,tabix) wgetnode, for the JBrowse CLI
jcvi builds C extensions and will not install against every python. If
pip install jcvi fails compiling them, uv venv --python 3.12 followed by
uv pip install jcvi biopython gets an interpreter it does build on.
Where the data comes from
The two assemblies are GCA_000001405.29 (human GRCh38) and GCA_003339765.3 (rhesus macaque Mmul_10), with gene models and coding sequence from Ensembl release 116.
- human gene models: https://ftp.ensembl.org/pub/release-116/gff3/homo_sapiens/Homo_sapiens.GRCh38.116.gff3.gz
- human coding sequence: https://ftp.ensembl.org/pub/release-116/fasta/homo_sapiens/cds/Homo_sapiens.GRCh38.cds.all.fa.gz
- rhesus macaque gene models: https://ftp.ensembl.org/pub/release-116/gff3/macaca_mulatta/Macaca_mulatta.Mmul_10.116.gff3.gz
- rhesus macaque coding sequence: https://ftp.ensembl.org/pub/release-116/fasta/macaca_mulatta/cds/Macaca_mulatta.Mmul_10.cds.all.fa.gz
What dN/dS says
A coding substitution is synonymous (the codon changes, the amino acid does not) or non-synonymous. Synonymous changes are nearly invisible to selection, so their rate dS approximates the mutation rate; non-synonymous changes are seen, so their rate dN carries what selection did. The ratio is read against 1: below it is purifying selection, where most genes sit, and above it takes positive selection to explain.
Producing the data
dS has to be large enough to estimate and small enough not to saturate. Rhesus macaque sits in that window against human; chimpanzee leaves a denominator near zero on most genes.
Orthologs
The end-to-end script turns each GFF3 into the BED the adapter reads, translates each CDS to a proteome keyed the same way, and runs jcvi:
diamond makedb --in rhesus.pep -d rhesus.pep
diamond blastp --threads 14 --query human.pep --db rhesus.pep \
--out human.rhesus.last --max-target-seqs 20 --evalue 1e-10 --outfmt 6
python -m jcvi.compara.catalog ortholog --no_strip_names --dbtype prot \
--align_soft diamond_blastp --no_dotplot human rhesus
Two silent traps:
- the alignment file has to be query = the first species, subject = the
second. Reversed, every id is looked up in the wrong BED and the run ends
with
A total of 0 anchor was found - Ensembl versions transcript ids in its FASTA and not in its GFF3
(
ENST00000641515.7againstENST00000641515), so nothing matches. The script strips the version, andkaks_from_pairs.pytakes--strip-version
dN and dS
python3 kaks_from_pairs.py pairs.tsv both.cds.fa.gz \
--key record --strip-version -o kaks.tsv
kaks_from_pairs.py
aligns each pair as protein, back-translates to codons so that nothing shifts
frame, and runs Nei-Gojobori.
Filtering paralogs and low-count pairs
True orthologs share one divergence time, so their dS values cluster. A pair an
order of magnitude above the cluster is a paralog the aligner preferred, and
--max-ds removes it.
The top of a table sorted by dN/dS is the pairs with almost nothing to divide
by: HBA1 comes out over 2 off a single synonymous difference. --min-syn-subs
is a floor on that count.
Every row also carries that count and a two-sided Fisher exact p, the test
MEGA
prescribes for small substitution counts. Both are attributeColumns, so
clicking a link shows the evidence under its colour.
Loading the blocks table in JBrowse
The output is a pair table with the two rates after the two gene columns, which
is the .blocks shape
MCScanBlocksAdapter reads:
{
"type": "SyntenyTrack",
"trackId": "primate_orthologs",
"name": "Human / rhesus orthologs (dN/dS)",
"assemblyNames": ["human", "rhesus"],
"adapter": {
"type": "MCScanBlocksAdapter",
"uri": "primate.blocks.gz",
"blockAssemblies": ["human", "rhesus"],
"bedLocations": [{ "uri": "human.bed.gz" }, { "uri": "rhesus.bed.gz" }],
"assemblyNames": ["human", "rhesus"],
"attributeColumns": ["dn", "ds", "syn_subs", "fisher_p"]
}
}
jbrowse add-track-json '{
"type": "SyntenyTrack",
"trackId": "primate_orthologs",
"name": "Human / rhesus orthologs (dN/dS)",
"assemblyNames": ["human", "rhesus"],
"adapter": {
"type": "MCScanBlocksAdapter",
"uri": "primate.blocks.gz",
"blockAssemblies": ["human", "rhesus"],
"bedLocations": [{ "uri": "human.bed.gz" }, { "uri": "rhesus.bed.gz" }],
"assemblyNames": ["human", "rhesus"],
"attributeColumns": ["dn", "ds", "syn_subs", "fisher_p"]
}
}'
In JBrowse Desktop, or in any running JBrowse Web session, open a view on this track’s assembly, then File → Open track..., choose Add track from pasted JSON, and paste:
{
"type": "SyntenyTrack",
"trackId": "primate_orthologs",
"name": "Human / rhesus orthologs (dN/dS)",
"assemblyNames": ["human", "rhesus"],
"adapter": {
"type": "MCScanBlocksAdapter",
"uri": "primate.blocks.gz",
"blockAssemblies": ["human", "rhesus"],
"bedLocations": [{ "uri": "human.bed.gz" }, { "uri": "rhesus.bed.gz" }],
"assemblyNames": ["human", "rhesus"],
"attributeColumns": ["dn", "ds", "syn_subs", "fisher_p"]
}
}
attributeColumns names the columns after the two gene columns, and each
becomes a feature attribute in the detail panel. dn and ds drive Color
by... → dN/dS, whose ramp has 1 at its middle and 2 at its top.
Two LinearSyntenyView properties matter for a view this sparse: alpha
defaults to 0.2 for whole-genome views where ribbons overlap, and 0.95 shows the
colour as it is; drawCurves separates stacked neighbours.
Reading the plot
The neighbourhood is collinear, so colour is the only thing that varies. Adaptive evolution of primate lysozyme is one of the older results in molecular evolution, the enzyme having been recruited as a digestive protein in foregut fermenters.
Clicking the orange link shows a handful of synonymous differences and a Fisher p nowhere near significant. One pairwise comparison has little power; the published result rests on codon models across many primate lineages. Blue is the colour that tests strongly: a conserved gene accumulates measurable synonymous change while holding non-synonymous change near zero.
Checking the rates against the raw data
YEATS4 begins just past where LYZ ends, so the two share a locus and a divergence time and land at opposite ends of the ramp. It is conserved and compact, so its dS is low while its synonymous count clears the floor.
The script prints two genome-wide counts: pairs exceeding 1, and those surviving the Fisher test. The second is about what chance gives at that many tests.
Reproduce it end to end
build_primate_selection.sh
runs everything above and writes a config.json with both assemblies, both gene
tracks, the ortholog track and a session opening the locus.
curl -fO https://raw.githubusercontent.com/GMOD/jbrowse-components/main/scripts/build_primate_selection.sh
bash build_primate_selection.sh
npx --yes serve primate_selection_build/jbrowse2 # then open the printed URL
It needs the tools under Prerequisites on PATH.
See also
- Synteny visualization (a polyploid against itself)
- Synteny from an ortholog table (grape, peach, cacao)
- Synteny from MCScan anchors (grape, peach)
References
- Nei, M. & Gojobori, T. Simple methods for estimating the numbers of synonymous and nonsynonymous nucleotide substitutions. Molecular Biology and Evolution 3, 418-426 (1986). https://doi.org/10.1093/oxfordjournals.molbev.a040410
- Messier, W. & Stewart, C.-B. Episodic adaptive evolution of primate lysozymes. Nature 385, 151-154 (1997). https://doi.org/10.1038/385151a0
- Tang, H. et al. jcvi: A versatile toolkit for comparative genomics analysis. iMeta 3, e211 (2024). https://doi.org/10.1002/imt2.211
Feedback on this tutorial is welcome: contact us.