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.20. Desktop beta builds for Linux and
macOS are on the releases page;
the Windows build waits on code signing.
The ratio of non-synonymous to synonymous substitution rates, dN/dS, measures the selection pressure on a protein-coding gene between two species. We:
- build a human against rhesus macaque ortholog table with jcvi, and measure dN
and dS on every pair with
kaks_from_pairs.py - load both genomes and genes, and the table as a synteny track
- color each ortholog link by the ratio
- read the lysozyme (LYZ) neighbourhood on human chromosome 12, with the conserved neighbour YEATS4 as the control
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.
# Debian/Ubuntu
sudo apt install tabix diamond-aligner nodejs
# or conda, which has every tool above
conda install -c bioconda -c conda-forge htslib diamond nodejs
# any platform
pip install jcvi biopython
npm install -g @jbrowse/cli@nextThe data
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 tracks below read local files that the build script writes from these, so run it first, or run Producing the human-rhesus ortholog table and its rates on a GFF3, a protein FASTA and a CDS FASTA per genome of your own. The script fetches everything itself.
| What it is | File |
|---|---|
| Human gene models | Homo_sapiens.GRCh38.116.gff3.gz |
| Human coding sequence | Homo_sapiens.GRCh38.cds.all.fa.gz |
| Rhesus macaque gene models | Macaca_mulatta.Mmul_10.116.gff3.gz |
| Rhesus macaque coding sequence | 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
jcvi takes a BED and a proteome per genome under one set of ids. The BED comes from the GFF3, one primary transcript per gene on the assembled chromosomes, and the adapter reads the same file:
python -m jcvi.formats.gff bed --type=mRNA --key=transcript_id \
--primary_only human.gff3.gz -o human.all.bed
awk -F'\t' '$1 ~ /^([0-9]+|X|Y)$/' human.all.bed > human.bedThe end-to-end script translates each CDS into
human.pep and rhesus.pep, keeping the transcripts the BED names under the
same ids. DIAMOND aligns human against rhesus, and jcvi chains the hits:
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
The pair list 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.
# skip the ### lines that separate blocks
awk '!/^#/ && NF >= 2 {print $1 "\t" $2}' human.rhesus.anchors > pairs.tsv
# one CDS file for both genomes, whose ids (ENST, ENSMMUT) are distinct
cat human.cds.fa.gz rhesus.cds.fa.gz > both.cds.fa.gzcurl -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 view's
encoding.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
encoding.opacity, which starts low for whole-genome views where ribbons overlap. 0.95 shows the color as it is. - Curved lines on the same menu sets
mark: { interpolate: 'bump' }, 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"]],
"encoding": { "color": "dnds", "opacity": 0.95 },
"mark": { "interpolate": "bump" }
}
]
}
}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"]],
"encoding": { "color": "dnds", "opacity": 0.95 },
"mark": { "interpolate": "bump" }
}
]
}
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. Click the blue YEATS4 ribbon beside LYZ for the same evidence at the other end of the ramp. A conserved gene accumulates measurable synonymous change while its non-synonymous change stays near zero, so its synonymous count clears the floor and the Fisher test reaches significance at the low end too.
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 and Gojobori (1986). Simple methods for estimating the numbers of synonymous and nonsynonymous nucleotide substitutions
- Messier and Stewart (1997). Episodic adaptive evolution of primate lysozymes
- Tang et al. (2024). jcvi: A versatile toolkit for comparative genomics analysis
Notes
-
Chimpanzee leaves a denominator near zero on most genes. ↩
Feedback on this tutorial is welcome: contact us.