Selection pressure between two genomes (dN/dS)
The tutorials target the JBrowse v5 beta, and the v4.3.0 release
on the download page lacks some of what they show. To install the
beta of JBrowse Web, run npm install -g @jbrowse/cli@next, then
jbrowse create jbrowse2 --branch v5.0.0-beta.13. Desktop beta builds are coming soon.
The ratio of non-synonymous to synonymous substitution rates, dN/dS, measures
the selection pressure on a protein-coding gene between two species. jcvi builds
a human against rhesus macaque ortholog table, kaks_from_pairs.py measures dN
and dS on every pair, and JBrowse colors each ortholog link on a synteny track
by the ratio. We then read the lysozyme neighbourhood on human chromosome 12.
Prerequisites
- a JBrowse to open the files 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 compiles C extensions at install time, and the compile fails on some Python
versions. If pip install jcvi fails, uv venv --python 3.12 followed by
uv pip install jcvi biopython gives an interpreter it builds on.
Where the data comes from
The 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.
The build script fetches these files, so there is nothing to download by hand.
- human gene models: ftp.ensembl.org/…/Homo_sapiens.GRCh38.116.gff3.gzhttps://ftp.ensembl.org/pub/release-116/gff3/homo_sapiens/Homo_sapiens.GRCh38.116.gff3.gz
- human coding sequence: ftp.ensembl.org/…/Homo_sapiens.GRCh38.cds.all.fa.gzhttps://ftp.ensembl.org/pub/release-116/fasta/homo_sapiens/cds/Homo_sapiens.GRCh38.cds.all.fa.gz
- rhesus macaque gene models: ftp.ensembl.org/…/Macaca_mulatta.Mmul_10.116.gff3.gzhttps://ftp.ensembl.org/pub/release-116/gff3/macaca_mulatta/Macaca_mulatta.Mmul_10.116.gff3.gz
- rhesus macaque coding sequence: ftp.ensembl.org/…/Macaca_mulatta.Mmul_10.cds.all.fa.gzhttps://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.
- dS is the synonymous rate. Selection acts weakly on synonymous changes, so dS approximates the mutation rate.
- dN is the non-synonymous rate, which reflects the selection acting on those changes.
- dN/dS below 1 indicates purifying selection, where most genes sit. Above 1 needs positive selection to explain.
Producing the human-rhesus ortholog table and its rates
dS has to be large enough to estimate and small enough not to saturate. Rhesus macaque sits in that window against human.1
Calling human-rhesus orthologs with jcvi
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 rhesusTwo input mistakes leave jcvi with no orthologs:
- the alignment file has to be query = the first species, subject = the
second. Reversed, jcvi looks up every id 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
Measuring dN and dS on each pair
pairs.tsv is the two gene columns of human.rhesus.anchors. The script skips
human.rhesus.lifted.anchors, which recruits extra pairs near an established
syntenic block. Their median dS is several times that of the chained pairs,
which marks them as paralogs.
curl -fO https://raw.githubusercontent.com/GMOD/jbrowse-components/main/scripts/kaks_from_pairs.pypython3 kaks_from_pairs.py pairs.tsv both.cds.fa.gz \
--key record --strip-version --min-syn-subs 3 --max-ds 0.3 -o primate.blocks
gzip -kf primate.blocks human.bed rhesus.bedkaks_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
well above the cluster is a paralog the aligner preferred, and --max-ds drops
every pair whose dS exceeds its value.
The top of a table sorted by dN/dS is the pairs with almost nothing to divide
by: HBA1 lands there off a single synonymous difference. --min-syn-subs is a
floor on that count, and drops every pair with fewer synonymous differences than
its value.
Every row also has that count and a two-sided Fisher exact p, the test
MEGA
prescribes for small substitution counts. The track config below exposes both as
attributeColumns, so clicking a link shows the evidence under its color.
Loading the human and rhesus genomes and genes
A gene-level synteny view reads no sequence, so each assembly is a
.chrom.sizes file (a name and a length per line) that the script writes from
the GFF3's ##sequence-region header. We load human here and rhesus the same
way under the name rhesus:
Goes in the assemblies array of config.json. See Assemblies.
{ "name": "human", "uri": "human.chrom.sizes" }jbrowse add-assembly human.chrom.sizes \
--name human \
--load copyIn JBrowse Desktop, Open new genome on the start screen (or File → Open genome... in a session), then Open from a URL and paste:
human.chrom.sizesJBrowse reads the format off the file name. Then fill in:
- Genome name:
human
human.chrom.sizes is relative to a config.json. Replace it with its URL or its path on this computer.
Each panel draws genes from a track under its assembly. The script writes
human.genes.gff3.gz and rhesus.genes.gff3.gz, bgzipped and tabix-indexed
(prep). For your own annotation the GFF3 needs the same
refNames as the assembly:
Goes in the tracks array of config.json. See Tracks.
{
"type": "FeatureTrack",
"trackId": "human_genes",
"name": "human genes",
"assemblyNames": ["human"],
"adapter": {
"type": "Gff3TabixAdapter",
"uri": "human.genes.gff3.gz"
}
}jbrowse add-track human.genes.gff3.gz \
--trackId human_genes \
--name "human genes" \
--assemblyNames human \
--load copyIn 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": "FeatureTrack",
"trackId": "human_genes",
"name": "human genes",
"assemblyNames": ["human"],
"adapter": {
"type": "Gff3TabixAdapter",
"uri": "human.genes.gff3.gz"
}
}human.genes.gff3.gz is relative to a config.json. Replace it with its URL or its path on this computer.
Add the rhesus track the same way, with rhesus_genes under rhesus.
Loading the ortholog table as a synteny track
primate.blocks lists each pair's two gene ids, then dN, dS, the synonymous
substitution count and the Fisher p, the .blocks shape
MCScanBlocksAdapter reads.
attributeColumns names the columns after the two gene columns, and the detail
panel lists each as a feature attribute:
Goes in the tracks array of config.json. See Tracks.
{
"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": ["human.bed.gz", "rhesus.bed.gz"],
"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": ["human.bed.gz", "rhesus.bed.gz"],
"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": ["human.bed.gz", "rhesus.bed.gz"],
"attributeColumns": ["dn", "ds", "syn_subs", "fisher_p"]
}
}primate.blocks.gz, human.bed.gz, rhesus.bed.gz are relative to a config.json. Replace each with its URL or its path on this computer.
That shows it in the linear view. For the synteny view, open Add → Linear synteny view, pick the track under Quick start, and click Launch.
Color by value → dN/dS in the palette button menu sets the link's color, a
color scale that reads dn and ds, on a ramp with 1 at the middle and 2 at
the top.
Two view settings matter for a view this sparse:
- Opacity on the sliders button in the view header sets
opacity, which defaults to 0.25 for whole-genome views where ribbons overlap. 0.95 shows the color as it is. - Curved lines on the same menu sets
drawCurves, which separates stacked neighbours.
Reading lysozyme (LYZ) against its neighbours by dN/dS
The session below opens, from the hosted copy, the stretch around lysozyme (LYZ) on human chromosome 12 where gene order matches rhesus, colored by dN/dS. In your own build, pick Color by value → dN/dS.
Goes at the top level of config.json, replacing any defaultSession there. See Default session.
{
"defaultSession": {
"name": "Selection pressure across a primate gene neighbourhood",
"views": [
{
"type": "LinearSyntenyView",
"views": [
{
"assembly": "human",
"loc": "12:67,835,000-70,835,000",
"tracks": ["human_genes"]
},
{
"assembly": "rhesus",
"loc": "11:67,401,000-70,319,000",
"tracks": ["rhesus_genes"]
}
],
"tracks": [["primate_orthologs"]],
"color": { "field": "dnds" },
"opacity": 0.95,
"drawCurves": true
}
]
}
}jbrowse set-default-session --session - << 'EOF'
{
"name": "Selection pressure across a primate gene neighbourhood",
"views": [
{
"type": "LinearSyntenyView",
"views": [
{
"assembly": "human",
"loc": "12:67,835,000-70,835,000",
"tracks": ["human_genes"]
},
{
"assembly": "rhesus",
"loc": "11:67,401,000-70,319,000",
"tracks": ["rhesus_genes"]
}
],
"tracks": [["primate_orthologs"]],
"color": { "field": "dnds" },
"opacity": 0.95,
"drawCurves": true
}
]
}
EOFGene order is the same in both genomes here, so color is the only thing that varies. Click the orange LYZ ribbon for its synonymous count and Fisher p in the detail panel. Messier and Stewart reported adaptive evolution of primate lysozyme in 1997; the published result rests on codon models across many primate lineages.
YEATS4, a conserved neighbour, as the control for LYZ
YEATS4 begins just past where LYZ ends, so the two share a locus and a divergence time yet sit at opposite ends of the ramp. YEATS4 is conserved and compact, so its dS is low while its synonymous count clears the floor. Blue is the low end of the ramp, where the Fisher test does reach significance, because a conserved gene accumulates measurable synonymous change while its non-synonymous change stays near zero.
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:
- Turn each GFF3 into a BED and each CDS into a proteome keyed the same way, stripping Ensembl's transcript versions so the ids match.
- Align human against rhesus with DIAMOND and chain the hits with jcvi, keeping the chained anchors and not the lifted ones.
- Measure dN and dS on each pair, dropping pairs with too few synonymous differences or a dS far above the cluster.
- Print the count of pairs genome-wide with dN/dS above 1, and dN/dS and dS for every pair around LYZ, and write the config.
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 URLSee also
- Tutorial: Synteny visualization (a polyploid against itself)
- Tutorial: Synteny from an ortholog table (grape, peach, cacao)
- Tutorial: Synteny from MCScan anchors (grape, peach)
Citations
- 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
Notes
-
Chimpanzee leaves a denominator near zero on most genes. ↩
Feedback on this tutorial is welcome: contact us.