Nextflow¶
vepyr/annotate is an nf-core-style Nextflow module around
vepyr annotate: one VCF in, one bgzip VCF with CSQ plus its tabix
index out. It ships Docker and Singularity containers for linux/amd64 and
linux/arm64, so it runs natively on x86_64 servers, ARM servers and Apple
Silicon.
Not in nf-core/modules yet
The module is staged in this repository under
nf-core-module/modules/nf-core/vepyr/annotate
until it is submitted to nf-core/modules. Until then, copy it into your
pipeline as shown below; nf-core modules install vepyr/annotate will work
once it is merged there.
The diagram shows the vcf_annotate_vepyr subworkflow described in
Normalizing first; with val_normalize false, or when you
call VEPYR_ANNOTATE directly, only the dashed path runs. Names starting with
ch_ are Nextflow channels, not chromosomes. Each VCF is one whole task per
process, with no per-chromosome scatter; vepyr parallelizes inside its task, up
to cpus pipelines.
Adding the module to a pipeline¶
Copy the module directory to the path nf-core tooling would install it at, so a
later nf-core modules install replaces it in place:
git clone --depth 1 https://github.com/biodatageeks/vepyr.git
mkdir -p my-pipeline/modules/nf-core/vepyr
cp -R vepyr/nf-core-module/modules/nf-core/vepyr/annotate my-pipeline/modules/nf-core/vepyr/
Example¶
A minimal pipeline that annotates one VCF with --everything:
include { VEPYR_ANNOTATE } from './modules/nf-core/vepyr/annotate/main'
workflow {
vcf = channel.of([
[ id: params.sample ],
file(params.vcf, checkIfExists: true),
file("${params.vcf}.tbi", checkIfExists: true)
])
cache = channel.value([
[ id: file(params.cache).name ],
file(params.cache, checkIfExists: true, type: 'dir')
])
// A bgzip FASTA needs its .gzi next to the .fai; a plain FASTA has only the .fai.
def fai = file("${params.fasta}.fai", checkIfExists: true)
fasta = channel.value([
[ id: 'GRCh38' ],
file(params.fasta, checkIfExists: true),
params.fasta.endsWith('.gz') ? [ fai, file("${params.fasta}.gzi", checkIfExists: true) ] : fai
])
VEPYR_ANNOTATE(vcf, cache, fasta, params.cache_version, [[], []])
VEPYR_ANNOTATE.out.vcf.view { meta, annotated -> "${meta.id}: ${annotated}" }
}
params {
sample = 'sample'
vcf = null
cache = null
fasta = null
cache_version = 116
outdir = 'results'
}
docker.enabled = true
process {
withName: 'VEPYR_ANNOTATE' {
ext.args = '--everything'
cpus = 4
publishDir = [ path: { "${params.outdir}/vepyr" }, mode: 'copy' ]
}
}
profiles {
// main.nf names the linux/amd64 image; on arm64 hosts (Apple Silicon,
// Graviton) use the arm64 build from the module's meta.yml.
arm64 {
docker.runOptions = '--platform=linux/arm64'
process {
withName: 'VEPYR_ANNOTATE' {
container = 'community.wave.seqera.io/library/htslib_vepyr:806fe605983a885b'
}
}
}
}
# x86_64 Linux
nextflow run main.nf \
--sample HG002 \
--vcf HG002.vcf.gz \
--cache ~/vepyr_cache/116_GRCh38_ensembl \
--fasta Homo_sapiens.GRCh38.dna.primary_assembly.fa.gz
# Apple Silicon or another arm64 host: add -profile arm64
Results land in results/vepyr/HG002.vcf.gz and results/vepyr/HG002.vcf.gz.tbi.
This exact pipeline, run with -profile arm64 on all 50,861 normalized HG002
chr22 records against a release-116 ensembl cache, reproduces the record-body
md5 of Ensembl VEP 116 --everything (see Testing vs Ensembl VEP).
Normalizing first¶
vepyr annotates records as given. The parity inputs were normalized with
bcftools norm -m -both (multiallelic records split, indels not left-aligned).
To run that step in the same pipeline, use the vcf_annotate_vepyr subworkflow,
which runs nf-core's bcftools/norm module before VEPYR_ANNOTATE. It takes the
same inputs as the module plus a boolean, val_normalize; set it to true to
normalize. Normalizing needs the reference: bcftools/norm always reads the
FASTA from channel 3, so pass one even when ext.args has no --everything.
The subworkflow is staged in this repository next to the module, and it needs
both vepyr/annotate (installed as above) and nf-core's bcftools/norm at the
paths nf-core tooling uses:
mkdir -p my-pipeline/subworkflows/nf-core
cp -R vepyr/nf-core-module/subworkflows/nf-core/vcf_annotate_vepyr my-pipeline/subworkflows/nf-core/
# bcftools/norm: in an nf-core pipeline, `nf-core modules install bcftools/norm`.
# Otherwise copy it at the commit the subworkflow is tested against:
ref=56155f73713bc32c5343b59f05d968794c1b596d
mkdir -p my-pipeline/modules/nf-core/bcftools/norm
for f in main.nf meta.yml environment.yml; do
curl -fsSL -o my-pipeline/modules/nf-core/bcftools/norm/$f \
https://raw.githubusercontent.com/nf-core/modules/$ref/modules/nf-core/bcftools/norm/$f
done
In the example above, include the subworkflow instead of the module and call it
with the same channels plus true:
include { VCF_ANNOTATE_VEPYR } from './subworkflows/nf-core/vcf_annotate_vepyr/main'
// ... vcf, cache and fasta channels as in the example above ...
VCF_ANNOTATE_VEPYR(vcf, cache, fasta, params.cache_version, [[], []], true)
VCF_ANNOTATE_VEPYR.out.vcf_tbi.view { meta, annotated, tbi -> "${meta.id}: ${annotated}" }
Configure BCFTOOLS_NORM as validated. This is required, not optional:
process {
withName: 'BCFTOOLS_NORM' {
ext.args = '--multiallelics -both --do-not-normalize --output-type z --write-index=tbi'
ext.prefix = { "${meta.id}.norm" }
}
}
Without this block, bcftools/norm falls back to its default ext.args of
--output-type z: it left-aligns indels against the reference, leaves
multiallelic records unsplit and writes no index, so vepyr annotates the
unsplit records on a single pipeline.
Without --do-not-normalize, bcftools also left-aligns indels against the
reference, and it fails when the FASTA and VCF name contigs differently. The
index from --write-index=tbi lets vepyr use more than one pipeline. On the
raw HG002 chr22 benchmark records this subworkflow reproduces the Ensembl VEP
116 --everything record-body md5.
Keep the ext.prefix when sample ids match file names
bcftools/norm writes ${meta.id}.vcf.gz. If meta.id equals the input
VCF's basename (sample.vcf.gz with id: 'sample'), it writes over its own
staged input, which is a symlink to your file, and truncates the original.
The .norm prefix avoids that. VEPYR_ANNOTATE stages its input under
input/, so it needs no distinct prefix of its own.
Inputs¶
| Channel | Shape | Notes |
|---|---|---|
| 1 | [ meta, vcf, tbi ] |
Input VCF (plain, gzip or bgzip). The index, .tbi or .csi, is optional — pass [] — but without it the task runs a single pipeline. |
| 2 | [ meta2, cache ] |
vepyr Parquet cache directory, e.g. 116_GRCh38_ensembl. Not an Ensembl VEP cache. |
| 3 | [ meta3, fasta, fai ] |
Reference FASTA and its .fai. For a bgzip FASTA pass [ fai, gzi ] as the third element. Required by --everything, and by the subworkflow when val_normalize is true; pass [ meta3, [], [] ] otherwise. |
| 4 | cache_version |
Release the cache must carry in its metadata, e.g. 116. Pass [] to skip the check. |
| 5 | [ meta4, plugin_cache ] |
Root of a plugin cache tree, or [ [], [] ] for none. |
The module never builds indexes: vepyr opens the reference through its .fai
(and a bgzip reference through its .gzi as well), so a missing index fails the
task.
Outputs¶
| Emit | Shape | Content |
|---|---|---|
vcf |
[ meta, "${prefix}.vcf.gz" ] |
Annotated VCF, bgzip. |
tbi |
[ meta, "${prefix}.vcf.gz.tbi" ] |
tabix index of it. |
versions_vepyr, versions_tabix |
[ process, tool, version ] |
Also published on the versions topic. |
Configuration¶
| Setting | Effect |
|---|---|
ext.args |
Extra vepyr annotate flags, e.g. '--everything' or '--hgvsc'. See Command line. |
ext.args2 |
Extra tabix flags for indexing the output. |
ext.prefix |
Output file name stem. Default: meta.id. |
cpus |
Annotation pipelines (--fork). |
The module sets -i, -o, --dir_cache, --fasta, --cache_version,
--plugin_cache_root, --fork and --no_progress itself.
--fork comes from cpus, not ext.args. The module appends
--fork ${task.cpus} after ext.args, so a --fork or --workers in
ext.args is overridden. When the input has no index it passes --fork 1,
since more than one pipeline needs an indexed VCF.
Unknown flags fail the task. vepyr annotate rejects any VEP flag it does not
implement, including one that is a prefix of a vepyr flag: --hgvs is not read
as --hgvsc, nor --dir as --dir_cache. An ext.args copied from an
ensemblvep/vep configuration therefore fails rather than silently annotating
differently.
Caches¶
Channel 2 stages a directory, so it must be something Nextflow can stage as one:
a local or shared-filesystem path, or an object-store prefix Nextflow can list.
A plain https:// URL to a cache directory does not work, because web servers
serve files, not directory trees.
Get a prebuilt cache from Downloads. For a quick test, a single chromosome is enough — see Downloading only part of a cache; such a cache annotates only the contigs it holds.
Pair cache_version with the cache you pass: a 116_GRCh38_* cache with
cache_version = 116.
Plugins¶
Pass the plugin cache root as channel 5 and choose plugins in ext.args:
VEPYR_ANNOTATE(vcf, cache, fasta, 116, [ [ id: 'plugins' ], file(params.plugin_cache, type: 'dir') ])
Without --plugin, every plugin under the root is applied, in alphabetical
order; each --plugin narrows the set and fixes its CSQ block order. The root
is the directory that contains plugin/. See Plugins.
Containers¶
| Engine | linux/amd64 | linux/arm64 |
|---|---|---|
| Docker | community.wave.seqera.io/library/htslib_vepyr:84d01ceaf76003ed |
community.wave.seqera.io/library/htslib_vepyr:806fe605983a885b |
| Singularity / Apptainer | oras://community.wave.seqera.io/library/htslib_vepyr:5872e79a887b6765 |
oras://community.wave.seqera.io/library/htslib_vepyr:5282c570a1e32022 |
They are Seqera Wave builds of the module's
environment.yml: bioconda::vepyr and bioconda::htslib (for tabix). The
module's meta.yml lists them with conda lock files for both platforms, and
-profile conda uses environment.yml directly. For Singularity and Apptainer,
main.nf refers to the image by its https download URL, which meta.yml lists
next to the oras:// URI above; the two name the same build, and either works as
a container override.
Use the image for your host's architecture
main.nf names the linux/amd64 images, which is what Nextflow runs unless
you override container. On an arm64 host, select the arm64 image as the
arm64 profile above does. Running the amd64 image under emulation on Apple
Silicon fails with SIGILL (exit 132): Docker's emulated x86_64 guest has no
AVX, which the native extension uses.
The same applies to Singularity and Apptainer: main.nf names the amd64
image for them too. On an arm64 host, override it with the arm64 image from
the table above:
process {
withName: 'VEPYR_ANNOTATE' {
container = 'oras://community.wave.seqera.io/library/htslib_vepyr:5282c570a1e32022'
}
}
nf-core modules name a single image per engine; pipelines choose the
architecture in their own config, as the arm64 profile above does for Docker.