DupCaller is a tool for calling somatic mutations and calculating somatic mutational burden from barcoded error-corrected next generation sequencing (ecNGS/duplex sequencing) data with matched normal (e.g. NanoSeq, UDSeq).
- Quick Start
- Prerequisites
- Installation
- Verify Installation
- Pipeline
- Results
- End-to-End Examples
- Resources
- Citation
- Copyright
- Contact
Use this if you have no preferred reference genome file and want to follow the developers' defaults: GRCh38 (human) or GRCm39 (mouse), with the reference FASTA, DupCaller index, germline VCF and noise/SNP mask(s) that --premade downloads (listed in Resources).
What you need:
- This repository, cloned:
git clone https://github.com/AlexandrovLab/DupCaller.git - Anaconda or equivalent: if you use another package manager such as mamba or pixi, replace the installation commands with their equivalents.
- Nextflow 23.10.1 to 24.10.4 on your
PATH(the versions CI tests; the commands below install 24.10.4). Other versions print a warning at launch and are untested. - Docker (for local/cloud) or Singularity (for HPC) on your
PATH. If you are on HPC and do not have Singularity, contact your admin to install it, or follow Installation and Pipeline instead. - Paired-end FASTQs (R1 and R2) for the duplex sample and, if you have one, its matched normal. Without a matched normal, see Without a matched normal below. We do not recommend calling samples <=100x duplex coverage without a matched normal.
- Internet access on the first run, from wherever the tasks run, to pull the containers and download the genome (see Pre-made genome downloads if your compute nodes are offline).
- Disk: ~50 GB for the downloaded genome (hg38), plus space for the Nextflow work directory and results. Expect ~300GB of space per 30x data.
- CPUs and memory: see the box below.
Compute requirements — set these for real data.
nextflow/pipeline.configships with conservative defaults,threads = 1,max_cpus = 1andmax_memory = 96.GB, which align and call a whole genome on one core. Always set--threads,--max_cpusand--max_memoryto what your machine or cluster job allows, as in the commands below (e.g.--threads 32 --max_cpus 32 --max_memory 192.GB).DupCaller.py callasks for 6 GB per CPU andbwa-mem2 mem | samtools sortfor 32 GB + 1 GB per CPU, each capped by--max_memory. The pipeline warns at launch when it would run single-threaded, or when--max_memoryis below what calling or the bwa-mem2 index needs.- The first run builds the bwa-mem2 index locally: ~90 GB of RAM on one node for hg38 (~80 GB for mm39), about 20 minutes, once per
--premade_dir. No pre-built bwa-mem2 index is hosted, so set--max_memoryto at least 90.GB for that first run. Every later run reuses the index and needs only the per-CPU memory above.--premade(below) is the recommended route: it downloads the reference and the pre-built DupCaller index, so you never runDupCaller.py indexyourself. With your own reference, build both indexes once on a large-memory node (bwa-mem2 index needs ~28 bytes of RAM per reference base) and reuse them; see Before running a real dataset.
Installation
With Singularity (HPC):
# 1. Nextflow and Singularity
module load singularity # Module name varies by cluster. Contact HPC admin if you have problem.
conda create -y -n nextflow -c conda-forge -c bioconda nextflow=24.10.4
conda activate nextflow
# 2. This repository
git clone https://github.com/AlexandrovLab/DupCaller.git
cd DupCallerWith Docker (Linux workstation with sudo):
# 1. Docker, and permission to run it without sudo
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker "$USER"
newgrp docker # or log out and back in
docker run --rm hello-world # check: prints "Hello from Docker!"
### If using macOS, install and launch Docker Desktop and ignore step 1
# 2. Nextflow
conda create -y -n nextflow -c conda-forge -c bioconda nextflow=24.10.4
conda activate nextflow
# 3. This repository
git clone https://github.com/AlexandrovLab/DupCaller.git
cd DupCallerConfigure your scheduler profile:
-
-profile singularity,localruns every step on the current machine (e.g. inside one batch job). To submit each step as its own cluster job instead, use-profile singularity,slurm(orlsf,sge,pbs,pbspro), and first replace the"PLEASE CHANGE"values for that scheduler in the "Scheduler settings" block ofnextflow/pipeline.configwith your cluster's information (nothing site-specific is preset): the run stops with an error until they are set.nullomits a flag (e.g. no QOS), andcluster_optionsadds extra submit flags such as your account. For SLURM, for example:slurm_queue = "your_partition" // sbatch --partition slurm_qos = "your_qos" // sbatch --qos, or null cluster_options = "--account=your_account"
Fill in the sample map: copy the example into the DupCaller directory and edit it:
cd /path/to/DupCaller # change to the actual path
cp nextflow/examples/sample.map sample.map # with a matched normal
cp nextflow/examples/sample_tumor_only.map sample.map # or: without a matched normalReplace SAMPLE1 with your sample name and each /path/to/... with the absolute path of that FASTQ. Alternatively, create your own tab-delimited sample.map with the same header as the example.
With a matched normal (Singularity): replace each /path/to/... and adjust --protocol, the resource flags and -profile as described in the notes below the commands.
conda activate nextflow
export NXF_SINGULARITY_CACHEDIR=/path/to/sif_cache # Singularity only
cd /path/to/DupCaller # change to the actual path
nextflow run nextflow/DupCaller.nf \
-c nextflow/nextflow.config -c nextflow/pipeline.config \
--premade hg38 --premade_dir /path/to/dupcaller_premade \
--protocol renanoseq \
--sample_map sample.map --outdir results \
--threads 32 --max_cpus 32 --max_memory 192.GB \
-profile singularity,slurm -resume
Without a matched normal (Singularity): use the tumor-only sample map and add --tumor_only true:
conda activate nextflow
export NXF_SINGULARITY_CACHEDIR=/path/to/sif_cache # Singularity only
cd /path/to/DupCaller # change to the actual path
nextflow run nextflow/DupCaller.nf \
-c nextflow/nextflow.config -c nextflow/pipeline.config \
--premade hg38 --premade_dir /path/to/dupcaller_premade \
--protocol renanoseq \
--tumor_only true \
--sample_map sample.map --outdir results \
--threads 32 --max_cpus 32 --max_memory 192.GB \
-profile singularity,slurm -resume-profile:singularity,slurmsubmits each step to SLURM (configure it as above); replaceslurmwith your scheduler (lsf,sge,pbs,pbspro), or usesingularity,localto run everything on the current machine. With Docker, use-profile docker,localand skipNXF_SINGULARITY_CACHEDIR.NXF_SINGULARITY_CACHEDIR(on shared storage) keeps the container images, so they are downloaded once instead of into every run's work directory.--premade hg38or--premade mm39. For mm39, add--premade_strain STRAIN(e.g.C57BL_6NJ) to use that mouse strain's germline VCF.--premade_diris where the genome is downloaded and kept (~50 GB for hg38). The first run downloads it and builds the bwa-mem2 index (~90 GB of RAM, about 20 minutes for hg38); every later run with the same--premade_dir, for any sample, reuses it. Put it on shared storage.- Each downloaded file is checked against the md5 listed in
nextflow/assets/premade/<genome>.tsv(all hg38 files; for mm39, the FASTA and the DupCaller index). On every run, before any alignment,PREMADE_CHECK_INDEXopens each of the five DupCaller index files and compares its index schema version with the one the pipeline's DupCaller image reads. An outdated or incompatible index stops the run at that step with an error naming the file; it is never used silently. (With your own reference,DupCaller.py callandestimaterun the same check when they start.) --protocolis your library protocol:renanoseq,nanoseqv2,udseqormetacs. It sets the barcode pattern and how far from fragment and read ends mutations are ignored (see Protocol Settings); a wrong barcode pattern silently breaks duplex family formation.metacsalso needs--barcode_list barcode_list.txt, the allowed Tn5 barcodes, one per line. For another protocol, give the barcode pattern with--barcode_patterninstead (N = barcode base, X = skipped base).--threads/--max_cpus/--max_memory: required for real data (the shipped defaults are 1 CPU; see the box above). Set them to your machine or the largest job your cluster allows.--max_memoryshould be at least 6 GB ×--threads(whatDupCaller.py callasks for; e.g.--threads 16 --max_memory 96.GB), and at least ~90 GB for the first run.- Results go to
results/SAMPLE1/. Rerunning the same command in the same directory resumes after a failure (-resume).
Pre-made genome downloads. The first --premade run downloads the files listed in nextflow/assets/premade/<genome>.tsv (GDC, GATK and Google Drive) from wherever its PREMADE_DOWNLOAD tasks run: the current machine with -profile ...,local, or the compute nodes with -profile ...,slurm. The download tool (gdown) is bundled in DupCaller images after 1.2.12, so nothing is installed at run time (the 1.2.12 image still installs gdown from PyPI in the download task), but the download itself needs internet access. If your compute nodes have none, either run that first --premade run inside an interactive job on a node that does, or download each file in the TSV yourself on such a node and put it in <premade_dir>/<genome>/ under the name in the TSV's file column; files already there are used as is (see Pre-made genomes). Later runs need no network access for the genome.
If you have your own reference genome, see Before running a real dataset and One sample with your own reference in the Nextflow README, or run the command-line steps in Pipeline below.
DupCaller requires python>=3.11 to run. Earlier versions are not supported.
The complete DupCaller pipeline also requires the following tools for data preprocessing. The Nextflow pipeline (nextflow/) runs exactly these versions and commands. Other versions may work but have not been validated.
- bwa-mem2 2.3 (https://github.com/bwa-mem2/bwa-mem2), run as
bwa-mem2 mem -C -T 0(Step 3). (The v2.3 release still prints2.2.1frombwa-mem2 versionand in the BAM@PGheader.) - GATK 4.3.0.0 (https://github.com/broadinstitute/gatk/releases), for MarkDuplicates (Step 4)
- samtools, for sorting and indexing BAMs
- htslib, which provides
tabixandbgzipfor compressed, indexed genomic files (recommended installation:conda install bioconda::htslib) - PERF (Pattern-based Exhaustive Repeat Finder) v0.4.6 (MIT license) is bundled with DupCaller, runs on the same Python, and is installed as the
PERFcommand, so it needs no separate installation.
The tool uses pip for installing scripts and prerequisites. We recommend creating a new environment that also holds the Prerequisites tools (bwa-mem2, GATK, samtools, htslib). If you do not have conda, install Miniforge first:
curl -fsSL -o Miniforge3.sh "https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$(uname)-$(uname -m).sh"
bash Miniforge3.sh -b -p "$HOME/miniforge3"
source "$HOME/miniforge3/bin/activate"Then create the environment:
conda create -y -n DupCaller -c conda-forge -c bioconda python=3.12 bwa-mem2=2.3 gatk4=4.3.0.0 samtools htslibTo install DupCaller, simply clone this repository and install via pip:
conda activate DupCaller
git clone https://github.com/AlexandrovLab/DupCaller.git
cd DupCaller
pip install .A pre-built image is published by the Alexandrov Lab on the GitHub Container Registry at ghcr.io/alexandrovlab/dupcaller:1.2.12 (public, no login needed). From the release after 1.2.12 on, each release's image is built from that release's tagged source by GitHub Actions (.github/workflows/release.yml), which runs the Nextflow mock test on it before publishing it and again on a fresh runner that pulls it anonymously.
Pull and run with Docker:
docker pull ghcr.io/alexandrovlab/dupcaller:1.2.12
docker run --rm ghcr.io/alexandrovlab/dupcaller:1.2.12 DupCaller.py --helpPull and run with Singularity:
Pull the image (only needed once):
singularity pull dupcaller-1.2.12.sif docker://ghcr.io/alexandrovlab/dupcaller:1.2.12For quick verification:
singularity exec dupcaller-1.2.12.sif DupCaller.py --helpFor installation-free execution of DupCaller commands, run all DupCaller.py commands with singularity exec and binding of current directories:
singularity exec --bind $(pwd):$(pwd) dupcaller-1.2.12.sif DupCaller.py {your commands}The repository ships three automated checks, the same ones GitHub Actions runs on every commit (.github/workflows/test.yml). Run them from the repository root. The two end-to-end tests run every step on a 3.2 kb synthetic genome with planted mutations and compare every deterministic output (VCFs, burdens, spectra, error profiles, stats) with the expected files in tests/mock_pipeline/, so a passing run reproduces the reference results exactly.
-rs lists skipped tests with the reason: an end-to-end test skips instead of failing when a tool it needs is not on PATH, so a skip means "not checked", not "passed".
1. Unit tests (DupCaller installed with pip, plus tabix from htslib):
conda activate DupCaller # the environment from pip Installation
pip install ".[test]" # adds pytest
pytest tests/unit/ -v -rs2. Mock end-to-end test, command line (index → trim → bwa-mem2 mem → gatk MarkDuplicates → call → estimate, run directly; needs bwa-mem2, GATK, samtools and htslib, all in the environment from pip Installation):
conda activate DupCaller
pytest tests/mock_pipeline/test_mock_pipeline.py -v -rs3. Mock end-to-end test, Nextflow (the same pipeline through nextflow/DupCaller.nf in containers, in 11 configurations: defaults, each optional input, tumor-only, and the udseq and metacs protocols). Needs Nextflow and Docker or Singularity on PATH, plus pytest and pysam (both in the DupCaller environment):
conda activate DupCaller
conda install -y -c conda-forge -c bioconda nextflow=24.10.4 # if nextflow is not already on PATH
module load singularity # HPC only; skip with Docker
pytest tests/mock_pipeline/test_mock_pipeline_nextflow.py -v -rsIt uses the DupCaller image pinned in nextflow/nextflow.config; set DUPCALLER_IMAGE to test another one (CI tests an image built from the checked-out commit this way). Without pytest, tests/mock_pipeline/run_nextflow_pipeline.sh OUTDIR [docker|singularity] all runs the same cases.
The barcode pattern (trim -p, Step 2) and how far from fragment and read ends mutations are ignored (call -tt/-tr, Step 5) depend on the library protocol:
| Protocol | Nextflow protocol |
trim -p |
call -tt |
call -tr |
|---|---|---|---|---|
| RE-NanoSeq | renanoseq |
NNNXXXX |
7 (default) | 7 (default) |
| NanoSeq v2 | nanoseqv2 |
NNNXX |
7 (default) | 7 (default) |
| UDSeq | udseq |
NNNNNNNN |
15 | 8 |
| META-CS | metacs |
BXXXXXXXXXXXXXXXXXXX with -bl barcode_list.txt (see Step 2) |
7 (default) | 7 (default) |
The Nextflow pipeline sets all three from the protocol name (--protocol, or protocol in pipeline.config; metacs also needs --barcode_list); see Quick Start.
-tt/--trimF ignores mutations within N bp of either end of the sequenced fragment, and -tr/--trimR within N bp of either end of each read. For example, for UDSeq:
DupCaller.py trim -i ${SAMPLE}_R1.fq.gz -i2 ${SAMPLE}_R2.fq.gz -p NNNNNNNN -o ${SAMPLE}
...
DupCaller.py call -b ${SAMPLE}.mkdped.bam -n ${NORMAL}.mkdped.bam -f ${REF} -o ${SAMPLE} \
-g ${GERMLINE} -m ${NOISE} -p ${THREADS} -tt 15 -tr 8Without a matched normal, whatever the protocol, omit -n and add -maf 0.1 to call so that variants above 10% allele fraction (mostly germline) are not called; see Without Matched Normal (the Nextflow pipeline does this with tumor_only; see Quick Start). A -maf below 1 also masks positions with raw tumor depth below 1/maf (10x for 0.1).
DupCaller uses a numpyrized reference genome to perform memory-efficient reference fetching, trinucleotide context lookup, and repeat (homopolymer/short tandem repeat, STR) annotation used to improve indel calling near repetitive regions.
STR annotation comes from a single tsv produced by PERF. First, run PERF against the reference FASTA:
PERF -m 2 -M 10 -u 2 -i reference.fa -o repeats.tsv-m/-M— min/max repeat unit (motif) size to search for.-m 2 -M 10covers 2–10bp-unit STRs; going much higher increases PERF's runtime and memory non-trivially. Homopolymers (unit length 1) are not taken from this tsv:indexderives them directly from the reference sequence and ignores any unit-length-1 rows, so-m 1only adds runtime.-u 2(--min-units) — minimum number of repeat copies to report.
Then index the reference, passing PERF's tsv directly:
DupCaller.py index -f reference.fa -rt repeats.tsvThe command will generate five h5 files in the same folder as the reference: {reference}.ref.h5, {reference}.tn.h5, {reference}.hp.h5, {reference}.str.h5, and {reference}.dbs.h5 — numpyrized reference sequences, trinucleotide contexts, homopolymer annotations, STR (unit length >=2) annotations, and per-position dinucleotide (DBS) classes, respectively.
For human reference genome hg38 and mouse reference genome mm39, we provided pre-built indexes and resource files in the Resources.
The reference also needs a samtools FASTA index and a bwa-mem2 index (for Step 3), built once:
samtools faidx reference.fa
bwa-mem2 index reference.fa # writes reference.fa.{0123,amb,ann,bwt.2bit.64,pac}; ~90 GB RAM for GRCh38 (28 bytes per base)| Short | Long | Description |
|---|---|---|
| -f | --reference | Reference genome fasta file (required) |
| -rt | --repeatTsv | PERF-format repeat tsv (chrom, start, end, motif, length, strand, num_units, motif_repeat) for the reference (required). Repeat unit length and repeat count are read directly from the motif and num_units columns; rows with unit length 1 are ignored. |
DupCaller.py trim extracts 5-prime barcodes from paired-end FASTQs:
DupCaller.py trim -i read1.fq -i2 read2.fq -p barcode_pattern -o sample_nameread1.fq/read2.fq— FASTQ files from read 1 and read 2. Both unzipped and gzip-compressed files are supported.barcode_pattern— pattern of barcodes starting from the 5-prime end, withNrepresenting a barcode base andXrepresenting a skipped base (similar notation to UMI-tools). For example, NanoSeq uses a 3-base barcode followed by 4 constant bases, so the pattern should beNNNXXXX.- Barcodes from a fixed list, of varying length (e.g. Tn5-based protocols such as META-CS, where each read starts with one of a set of 11–13 nt transposon barcodes followed by the 19 nt ME): put
Bin the pattern for "one barcode from the list" and pass the list with-bl/--barcode-list(one barcode per line). Positions afterBcount from the end of the matched barcode, e.g.-p BXXXXXXXXXXXXXXXXXXXtakes the barcode and skips the 19 nt ME. Each read's barcode may have up to-mm/--max-mismatchmismatches (default 1); the barcode with the fewest mismatches wins, then the shortest. Up to 1 mismatch is matched with lookup tables (fast, any list size); larger limits walk all listed barcodes at once, dropping each as soon as it exceeds the limit. The listed (corrected) sequence is written toDB. Read pairs whose read 1 or read 2 matches no barcode, or matches two equally well, are dropped and counted in the summarytrimprints. sample_name— prefix of output paired FASTQs. After the run completes,{sample_name}_1.fastqand{sample_name}_2.fastqwill be generated. Barcodes are appended to each read name ({original_read_name}_{read1_barcode}+{read2_barcode}, for optical-duplicate read-name matching only) and also written as aDB:Z:{read1_barcode}-{read2_barcode}FASTQ comment; the comment is promoted to a BAM tag bybwa-mem2 mem -C(Step 3) and is whatDupCaller.py callactually reads to assign duplex family membership — it never parses the read name for barcodes.
If the matched normal is prepared in the same way as the sample, apply trimming with the same scheme to the matched normal FASTQs. For traditional bulk normal, trimming is not needed.
Align the trimmed FASTQs of both sample and matched normal with bwa-mem2 (bwa-mem2 index from Step 1 must already exist). GATK requires read group fields ID, SM, and PL, so adding those tags during alignment is recommended. FASTQ tags must be kept — for bwa-mem2 mem (as for bwa mem) this requires the -C option. The Nextflow pipeline and the mock-pipeline regression test also use -T 0, which outputs every alignment regardless of its score, so low-scoring alignments are filtered by DupCaller (MAPQ/AS-XS) rather than dropped by the aligner.
bwa-mem2 mem -C -T 0 -t {threads} -R "@RG\tID:{sample_name}\tSM:{sample_name}\tPL:ILLUMINA" \
reference.fa {sample_name}_1.fastq {sample_name}_2.fastq \
| samtools sort -@ {threads} > {sample_name}.bam
samtools index -@ {threads} {sample_name}.bamthreads— number of cores used for aligningreference.fa— reference genome FASTA file, with its bwa-mem2 index next to it{sample_name}_1.fastq/{sample_name}_2.fastq— trimmed FASTQ files from Step 2
Run GATK MarkDuplicates on sample and matched-normal BAMs. Optical and PCR duplicates must be treated differently in ecNGS variant calling:
- Set
--TAGGING_POLICY OpticalOnlyto differentiate optical from PCR duplicates. - Set
--DUPLEX_UMI truefor duplex UMI handling. - Set
--READ_NAME_REGEXas shown below, because the read names were modified in Step 2.
Note: The published analyses used GATK 4.3.0.0. Older GATK releases do not have the
--DUPLEX_UMIflag.
gatk MarkDuplicates \
-I sample.bam -O sample.mkdped.bam -M sample.mkdp_metrics.txt \
--READ_NAME_REGEX "(?:.*:)?([0-9]+)[^:]*:([0-9]+)[^:]*:([0-9]+)[^:]*$" \
--DUPLEX_UMI --TAGGING_POLICY OpticalOnly --BARCODE_TAG DB
samtools index sample.mkdped.bamDupCaller.py call needs the BAM index (.bai) of every input BAM.
After preprocessing, run DupCaller.py call to call somatic mutations. Usage depends on your experimental design:
Whole genome / whole exome / reduced genome (e.g. NanoSeq) with a matched normal:
DupCaller.py call -b ${sample}.mkdped.bam -f reference.fa -o {output_prefix} \
-p {threads} -n {normal.bam} -g germline.vcf.gz -m noise_mask.bed.gzMutagenesis panel without a matched normal:
DupCaller.py call -b ${sample}.mkdped.bam -f reference.fa -o {output_prefix} \
-p {threads} -g germline.vcf.gz -m noise_mask.bed.gz -maf 0.1Note: DupCaller partitions jobs by genomic region; multithreading is less effective for small targeted panels. In this case, use at most one thread per distinct targeted region.
Non-default barcode tags: BAMs produced by DupCaller's own trim (Step 2) carry a shared DB:Z:{bc1}-{bc2} tag per read pair, and this is what call reads by default. If instead you are calling directly from a BAM that stores the duplex barcode differently, pass -bc/--barcode TAG,NORMALIZE,SEP: TAG is the bam tag holding the barcode, and NORMALIZE says whether it still needs splitting/reordering. For example, the Sanger NanoSeq pipeline tags each read with a single RB tag that already identifies the duplex family (e.g. chrom,fragment_start,fragment_end,bc1,bc2) independent of read orientation — pass -bc RB,0,- to read it directly (SEP is ignored in this case). DupCaller's own DB:Z:{bc1}-{bc2} tag is the NORMALIZE=1 case (the default, -bc DB,1,-): it's an unnormalized bc1<SEP>bc2 pair that still needs read-orientation-based reordering to group both strand-copies of a molecule together.
Input validation: DupCaller checks for the existence of all required files (BAM, reference, h5 index files, and optional files) before starting analysis, providing clear error messages for missing files.
Multi-threading: Coverage files from different threads are automatically merged post-processing with region-aware boundary detection and tabix indexing.
See the Results section for descriptions of all output files.
| Short | Long | Description |
|---|---|---|
| -b | --bam | BAM file of ecNGS data |
| -f | --reference | Reference genome FASTA file |
| -o | --output | Output directory; created if needed. Files inside are named{sample}_*, where {sample} is the directory's basename |
These options should be understood and customized accordingly.
| Short | Long | Description | Default |
|---|---|---|---|
| -r | --regions | Contigs to consider for variant calling. For non-human species, set accordingly (e.g. for mouse:-r chr{1..19} chrX) |
chr{1..22} chrX |
| -g | --germline | Indexed germline VCF with AF field | None |
| -p | --threads | Number of threads | 1 |
| --minChunkLength | Minimum chunk length in bases; short contigs stay unsplit. May use fewer workers than requested. Also accepts--min-chunk-length. |
10000 | |
| -n | --normalBams | BAM file(s) of matched normals. When unavailable, set-maf to an appropriate value (e.g. 0.1) |
None |
| -m | --noise | BED interval file(s) masking noisy positions | None |
| -R | --regionfile | Inclusive BED file specifying target regions | None |
| -maf | --maxAF | Maximum allele fraction to call a somatic mutation. Must be set when matched normal (-n) is unavailable |
1 |
| -tt | --trimF | Ignore mutations less than n bp from template ends | 7 |
| -tr | --trimR | Ignore mutations less than n bp from read ends | 7 |
| -bc | --barcode | Molecular/duplex barcode read tag, asTAG,NORMALIZE,SEP (see note above) |
DB,1,- |
The effect of changing these parameters should be evaluated before implementation.
| Short | Long | Description | Default |
|---|---|---|---|
| --naf | Maximum VAF in matched normal for a mutation to be called | 0.01 | |
| -res | --rescue | Output discarded variants whose LR clears their channel's final threshold, with the reason in the FILTER field (called in the coverage pass; not with --skipCoveragePass) |
False |
| -nm | --nmflt | Drop a read family when at least half of its reads have an edit distance (NM) above this value | 5 |
| -ax | --minMeanASXS | Minimum mean AS-XS alignment score difference for a read group to be considered | 50 |
| -gaf | --germlineAfCutoff | Skip positions with germline AF above this threshold | 0.001 |
| -d | --minNdepth | Minimum coverage in normal for called variants | 10 |
| -sc | --skipCoveragePass | Skip round 2 (the coverage-only pass), roughly halving total runtime: every output except the per-locus coverage.bed.gz is still written, with coverage from round 1's power tallies contracted at the final thresholds. estimate then skips the duplex allele counts and per-gene coverage, and -rb/--reestimatebed is unavailable. Not with --rescue |
False |
| -nms | --doNotOutputMaskedSite | Skip snp_mask/noise_mask (-m) candidates entirely: no depth extraction for them and no masked records in the fail VCFs. Calls, mutation rates and corrected burdens are unchanged; Unmasked Coverage (_stats.txt) and Unmasked burden (_sbs_burden.txt) are written as N/A |
False |
These are variant calling model parameters; adjustment is unnecessary for general use.
| Short | Long | Description | Default |
|---|---|---|---|
| -E | --errprefix | Prefix for all six error files ({prefix}.amp.tn.srd.txt, {prefix}.amp.hp.txt, {prefix}.amp.str.txt, {prefix}.dmg.tn.txt, {prefix}.dmg.hp.txt, {prefix}.dmg.str.txt); overrides the default (output prefix) | None |
| -lo | --learnOnly | Stop after estimating/writing the error-rate files (ERROR/ dir); skip variant calling entirely | False |
| -lfdr | --lfdrThreshold | Maximum local false discovery rate (lFDR) of a PASS call. A call's lFDR is (1-mu)/(LR*mu+1-mu), where mu is the estimated mutation rate of its mutation channel. A call passes when its lFDR is at most this value and its log10 LR is at least --minLR, i.e. each channel's log10 LR threshold is max(minLR, log10((1-lfdr)(1-mu)/(lfdr*mu))) |
0.05 |
| -a | --pseudocount | Pseudocount (> 0) of the SBS amplification-error (SRD) fit and of the error-matrix smoothing. The per-channel mutation-rate solve uses none | 0.5 |
| -mlr | --minLR | Minimum log10 LR for a PASS call (applied together with -lfdr): floor on every channel's log10 LR threshold, used for calling and for the detection-power simulation behind the burden's sensitivity correction |
5 |
| -mq | --mapq | Minimum MAPQ for an alignment to be considered | 40 |
| -w | --windowSize | Genomic window size for coverage calculation and BAM partitioning | 100000 |
| -bq | --minBq | Bases with quality below this value are zeroed out and excluded from variant calling/learning (usable iff BQ >= minBq) | 18 |
| -aq | --minAltQual | Minimum summed per-strand consensus base quality at a position for SBS damage-rate learning to use it | 90 |
| --minRef | Minimum per-position read depth (ref+alt combined) for damage-rate learning to use it; the stricter of--minRef/--minAlt is applied, since the underlying check doesn't separate ref vs alt counts |
3 | |
| --minAlt | Minimum per-position read depth (ref+alt combined) for damage-rate learning to use it; the stricter of--minRef/--minAlt is applied, since the underlying check doesn't separate ref vs alt counts |
3 | |
| --srdMinRead | Minimum reads on a single strand (F1R2 or F2R1) for that strand to be included in SBS single-read discrepancy (SRD) rate learning; evaluated independently per strand, and also gates the minimum BQ-qualifying bases required for a site to be counted | 3 | |
| --ssmMinRead | Minimum reads required on EACH strand (F1R2 and F2R1 both) for a duplex family to be considered for single-strand mismatch (SSM) damage-rate learning | 3 | |
| -pt | --p_threshold | Strand independence filter threshold for SBS and indel calls: a PASS call fails with FILTER strand_independence if either strand's p-value (INFO MSP) is <= this. Per strand, not corrected for the number of calls. Must be in [0, 1); 0 disables the filter (MSP is still reported) |
0.05 |
| -id | --indelbed | Indel enhanced Panel of Normals (ePoN) for indel calling | None |
| -rt | --regionst | Contigs to consider for error-profile training, if different from-r/--regions |
same as--regions |
| -pd | --maxPileupDepth | Maximum depth for samtools mpileup | 1000000 |
| -mr | --muterateprefix | Reuse per-channel mutation-rate tables from an earlier run instead of estimating them from this run's own candidates (useful when calling a small region). Pass{out}/tmp/{sample} from that run, which holds {sample}_sbs96_rate_n1.txt and {sample}_indel_rate_by_hp_str.txt (tables written before the per-run indel Eeff change are refused). LR thresholds are still solved against this run's -lfdr |
None |
| --seed | RNG seed for the Monte Carlo detection-power simulation, for reproducible results across runs and-p values. If unset, a random seed is generated and recorded in {sample}_call_params.log |
random |
The -m option accepts BED files and will ignore any mutations overlapping an excluded locus. Any custom BED file can be used as input.
After mutation calling, run burden estimation on the call output directory (-i is the same path that was passed to call -o):
DupCaller.py estimate -i sample -f reference.fa -r chr{1..22} chrXAdjust the -r regions according to the reference genome used.
For dNdScv coverage correction, DupCaller can output mean duplex depth per gene using the -gb option:
DupCaller.py estimate -i sample -f reference.fa -r chr{1..22} chrX -gb {target}.bed.gzThe gene BED is a tab-separated BED with one row per exon (or target interval). DupCaller reads only the first four columns; any further columns (score, strand, ...) are ignored:
| Column | Content | Example |
|---|---|---|
| 1 | Chromosome, named as in the reference | chr17 |
| 2 | Start (0-based) | 7669608 |
| 3 | End (exclusive) | 7669690 |
| 4 | {gene_name}_{exon_number} |
TP53_12 |
chr17 7669608 7669690 TP53_12 0 -
chr17 7670608 7670715 TP53_11 0 -
chr17 7673218 7673266 TP53_10 0 -
The gene name is the part of column 4 before the first _, and all rows with the same gene name are pooled, so a gene name must not itself contain _. Each gene's depth is averaged over the summed length of its rows. The file must be sorted by position within each chromosome, bgzip-compressed and tabix-indexed (bgzip {target}.bed && tabix -p bed {target}.bed.gz).
For hg38 exomes, a ready-to-use gene BED of the IDT xGen Exome Hyb Panel v2 targets (197,769 targets, 4th column {gene}_{exon}) is included in this repository: resources/xgen-exome-hyb-panel-v2-targets-hg38.bed.gz (with its .tbi):
DupCaller.py estimate -i sample -f reference.fa -r chr{1..22} chrX \
-gb resources/xgen-exome-hyb-panel-v2-targets-hg38.bed.gzIn the Nextflow pipeline, pass the same file as --gene_bed.
To re-estimate trinucleotide-corrected mutational burden in specific regions without re-running variant calling, use -rb. This is a separate mode: it writes only the _re_estimate files (see Optional / Conditional Files) and the parameter log, not the main burden outputs, so run it after a normal estimate. It needs the per-locus coverage, so it is unavailable when call was run with -sc/--skipCoveragePass:
DupCaller.py estimate -i sample -f reference.fa -r chr{1..22} chrX -rb {re_estimate}.bed.gzAs with -gb, the BED file must be bgzip-compressed and tabix-indexed.
| Short | Long | Description |
|---|---|---|
| -i | --prefix | Output directory of thecall command (the -o value) |
| -f | --reference | FASTA file of reference genome (its h5 index files must sit next to it) |
| Short | Long | Description | Default |
|---|---|---|---|
| -r | --regions | Contigs to consider for trinucleotide calculation | chr{1..22} chrX |
| -ot | --outTrinuc | Write the reference trinucleotide composition to this file, for reuse with-ft |
None |
| -ft | --refTrinuc | Load a trinucleotide composition written by-ot instead of rescanning the reference (-f is still required) |
None |
| -d | --dilute | Set when sample and matched normal come from the same starting DNA material: SNVs with a tumor alt allele count (AC) above 1 are dropped when their tumor and normal allele counts differ significantly (Barnard's exact test, p <= 0.05), and kept calls are written to SBS/{sample}_sbs_flt.vcf |
False |
| -gb | --genebed | bgzipped, tabix-indexed gene BED file for per-gene coverage calculation, 4th column {gene}_{exon} (format in Per-Gene Coverage). For hg38 exomes, use the included resources/xgen-exome-hyb-panel-v2-targets-hg38.bed.gz |
None |
| -rb | --reestimatebed | bgzipped, tabix-indexed BED file for burden re-estimation in specific regions. Writes only the _re_estimate outputs (see Re-estimation for Specific Regions) |
None |
After running estimate on all samples, use DupCaller.py summarize to collate burden metrics and SBS96 profiles into multi-sample tables:
DupCaller.py summarize -i sample1 sample2 sample3 -o cohort_summary.txtThis reads {sample}/{sample}_stats.txt, {sample}/SBS/{sample}_sbs_burden.txt, {sample}/INDEL/{sample}_indel_burden.txt, {sample}/DBS/{sample}_dbs_burden.txt, and {sample}/SBS/{sample}_sbs_96_corrected.txt from each sample folder (all five are required) and writes four output files:
| File | Description |
|---|---|
cohort_summary.txt |
One row per sample with SBS, indel, and DBS burden metrics and library statistics |
cohort_summary_SBS96_uncorrected.txt |
96-context raw mutation counts across all samples (SigProfiler-compatible format) |
cohort_summary_SBS96_corrected.txt |
96-context trinucleotide-corrected counts across all samples |
cohort_summary_SBS96_genome.txt |
96-context estimated mutations per genome across all samples |
All SBS96 file can be directly input into SigProfilerPlotting
| Short | Long | Description |
|---|---|---|
| -i | --input | One or more sample folders (space-separated) |
| -o | --output | Output filename for the summary table (e.g.cohort_summary.txt) |
DupCaller.py aggregate pools the learned error profiles of several call runs into one set, which can then be passed to call -E (for example, for a low-coverage sample from the same library prep):
DupCaller.py aggregate -i sample1 sample2 sample3 -o pooled
DupCaller.py call ... -E pooledIt sums each sample's ERROR/{sample}.*.txt count tables and re-fits the SRD amplification matrix from the summed base-quality histograms in each sample's tmp/{sample}.amp.tn.bqhist.npz, so the tmp/ folder of each input run must still exist. It writes pooled.amp.tn.txt, pooled.amp.tn.srd.txt, pooled.dmg.tn.txt, and the four pooled.{amp,dmg}.{hp,str}.txt files.
| Short | Long | Description | Default |
|---|---|---|---|
| -i | --input | One or morecall output directories |
None |
| -f | --input-file | File with one error-file prefix per line (e.g.sample1/ERROR/sample1), as an alternative or addition to -i |
None |
| -o | --output | Output prefix for the aggregated error files (required) | None |
| -a | --pseudocount | Pseudocount for the SRD matrix re-fit (same ascall -a) |
0.5 |
For detailed column-by-column and field-by-field descriptions of every output file, see the docs/ folder:
docs/call_outputs.md— all files fromDupCaller.py calldocs/estimate_outputs.md— all files fromDupCaller.py estimatedocs/summarize_outputs.md— the cohort summary table fromDupCaller.py summarize
SBS/indel/DBS-specific outputs (VCFs, corrected-context tables, burden files, and signature plots) are written into per-type SBS/, INDEL/, and DBS/ subfolders of the sample's output directory.
| File | Description |
|---|---|
SBS/{sample}_sbs.vcf |
VCF of detected SNVs (adjacent SNVs from the same read family are reported as DBS instead) |
INDEL/{sample}_indel.vcf |
VCF of detected short indel mutations |
DBS/{sample}_dbs.vcf |
VCF of detected dinucleotide substitution (DBS) mutations |
{sample}_coverage.bed.gz |
Duplex coverage depths across genomic positions, merged across threads and tabix-indexed. Not written with -sc/--skipCoveragePass |
{sample}_coverage.bed.gz.tbi |
Tabix index for the coverage BED file |
SBS/{sample}_trinuc_by_duplex_group.txt |
Trinucleotide context counts grouped by duplex read number, used for SBS burden estimation |
INDEL/{sample}_indel_by_duplex_group.txt |
Indel context counts grouped by duplex read number, used for indel burden estimation |
DBS/{sample}_dbs_by_duplex_group.txt |
144-class dinucleotide context counts grouped by duplex read number, used for DBS burden estimation |
{sample}_duplex_family_strand_composition.txt |
Strand composition statistics for duplex read families |
{sample}_duplex_family_strand_composition_heatmap.pdf |
Heatmap visualization of duplex family strand composition |
{sample}_call_params.log |
Full record of all resolved parameters used for thecall run |
{sample}_stats.txt |
Overall sequencing and analysis metrics |
Written to the ERROR/ subfolder of the call output directory. If a complete set of the six files used for calling already exists there, learning is skipped and they are reused.
| File | Description |
|---|---|
ERROR/{sample}.amp.tn.txt |
Amplification SBS mismatch counts by trinucleotide context (diagnostic only) |
ERROR/{sample}.amp.tn.srd.txt |
SRD-EM-fitted amplification SBS error-rate matrix used for calling |
ERROR/{sample}.dmg.tn.txt |
Damage SBS error counts by trinucleotide context |
ERROR/{sample}.amp.hp.txt |
Amplification indel error counts for homopolymer contexts |
ERROR/{sample}.amp.str.txt |
Amplification indel error counts for short-tandem-repeat contexts |
ERROR/{sample}.dmg.hp.txt |
Damage indel error counts for homopolymer contexts |
ERROR/{sample}.dmg.str.txt |
Damage indel error counts for short-tandem-repeat contexts |
| File | Description |
|---|---|
SBS/{sample}_sbs_burden.txt |
SBS burden with uncorrected and corrected estimates and 95% confidence intervals |
INDEL/{sample}_indel_burden.txt |
Indel burden with uncorrected and corrected estimates and 95% confidence intervals |
DBS/{sample}_dbs_burden.txt |
DBS burden with uncorrected and corrected estimates and 95% confidence intervals |
SBS/{sample}_sbs_96_corrected.txt |
Corrected SBS counts across 96 trinucleotide contexts for signature analysis, including amutations_per_opportunity column |
INDEL/{sample}_indel_83_corrected.txt |
Corrected indel counts across the 83 ID contexts, including amutations_per_opportunity column |
DBS/{sample}_dbs_78_corrected.txt |
Corrected DBS counts across the 78 DBS contexts, including amutations_per_opportunity column |
SBS/{sample}_sbs_burden_by_group_size.txt |
SBS burden stratified by duplex group size, both cumulative ("min group size >= N") and exact ("group size == N"), corrected and uncorrected, with 95% CI |
INDEL/{sample}_indel_burden_by_group_size.txt |
Same stratification as above, for indels |
DBS/{sample}_dbs_burden_by_group_size.txt |
Same stratification as above, for DBS |
{sample}_duplex_allele_counts.txt |
Duplex depths and allele counts for each PASS SNV and indel. Not written when call used -sc/--skipCoveragePass |
{sample}_estimate_params.log |
Full record of all resolved parameters used for theestimate run. Written next to the sample folder ({-i}_estimate_params.log), not inside it |
Mutation number per genome (and its 95% bounds) in each _burden.txt is the corrected burden multiplied by Reference base number, the number of reference bases considered (one copy of each region in -r). It is therefore the expected number of mutations per haploid genome; double it for a diploid genome. The same applies to the per-channel mutation_number_genome column of _sbs_96_corrected.txt and _indel_83_corrected.txt and to summarize's *_mutations_per_genome columns.
| File | Description |
|---|---|
SBS/SBS_96_plots_{sample}.pdf |
96-context SBS mutational signature plots (uncorrected and corrected counts) |
INDEL/ID_83_plots_{sample}.pdf |
83-context indel (ID83) mutational signature plots |
DBS/DBS_78_plots_{sample}.pdf |
78-context DBS mutational signature plots |
SBS/{sample}_sbs_burden_by_group_size.pdf |
SBS burden across cumulative and exact duplex group sizes |
INDEL/{sample}_indel_burden_by_group_size.pdf |
Same, for indels |
DBS/{sample}_dbs_burden_by_group_size.pdf |
Same, for DBS |
| File | Condition | Description |
|---|---|---|
{sample}_gene_coverage.txt |
-gb option |
Mean duplex coverage per gene for dNdScv correction |
SBS/{sample}_sbs_burden_re_estimate.txt |
-rb option |
Re-estimated SBS burden for specific regions |
INDEL/{sample}_indel_burden_re_estimate.txt |
-rb option |
Re-estimated indel burden for specific regions |
SBS/{sample}_sbs_96_corrected_re_estimate.txt |
-rb option |
Re-estimated 96-context SBS counts for specific regions |
SBS/SBS_96_plots_{sample}_re_estimate.pdf |
-rb option |
Signature plots for re-estimated regions |
SBS/{sample}_sbs_flt.vcf |
-d option |
SNV calls kept after the--dilute normal-comparison filter |
The following examples assume the reference genome has already been indexed (DupCaller.py index, samtools faidx, bwa-mem2 index; Step 1) and that the germline VCF and noise mask are available. Replace all placeholder paths with your actual files.
SAMPLE=sample1
NORMAL=normal1
REF=/path/to/hg38.fa
GERMLINE=/path/to/gnomad.hg38.vcf.gz
NOISE=/path/to/noise_mask.bed.gz
THREADS=16
# 1. Trim barcodes
DupCaller.py trim -i ${SAMPLE}_R1.fq.gz -i2 ${SAMPLE}_R2.fq.gz -p NNNXXXX -o ${SAMPLE}
DupCaller.py trim -i ${NORMAL}_R1.fq.gz -i2 ${NORMAL}_R2.fq.gz -p NNNXXXX -o ${NORMAL}
# 2. Align
bwa-mem2 mem -C -T 0 -t ${THREADS} -R "@RG\tID:${SAMPLE}\tSM:${SAMPLE}\tPL:ILLUMINA" \
${REF} ${SAMPLE}_1.fastq ${SAMPLE}_2.fastq | samtools sort -@ ${THREADS} > ${SAMPLE}.bam
samtools index -@ ${THREADS} ${SAMPLE}.bam
bwa-mem2 mem -C -T 0 -t ${THREADS} -R "@RG\tID:${NORMAL}\tSM:${NORMAL}\tPL:ILLUMINA" \
${REF} ${NORMAL}_1.fastq ${NORMAL}_2.fastq | samtools sort -@ ${THREADS} > ${NORMAL}.bam
samtools index -@ ${THREADS} ${NORMAL}.bam
# 3. Mark duplicates
gatk MarkDuplicates -I ${SAMPLE}.bam -O ${SAMPLE}.mkdped.bam -M ${SAMPLE}.mkdp_metrics.txt \
--READ_NAME_REGEX "(?:.*:)?([0-9]+)[^:]*:([0-9]+)[^:]*:([0-9]+)[^:]*$" \
--DUPLEX_UMI --TAGGING_POLICY OpticalOnly --BARCODE_TAG DB
samtools index ${SAMPLE}.mkdped.bam
gatk MarkDuplicates -I ${NORMAL}.bam -O ${NORMAL}.mkdped.bam -M ${NORMAL}.mkdp_metrics.txt \
--READ_NAME_REGEX "(?:.*:)?([0-9]+)[^:]*:([0-9]+)[^:]*:([0-9]+)[^:]*$" \
--DUPLEX_UMI --TAGGING_POLICY OpticalOnly --BARCODE_TAG DB
samtools index ${NORMAL}.mkdped.bam
# 4. Call variants
DupCaller.py call -b ${SAMPLE}.mkdped.bam -f ${REF} -o ${SAMPLE} \
-n ${NORMAL}.mkdped.bam -g ${GERMLINE} -m ${NOISE} -p ${THREADS}
# 5. Estimate mutational burden
DupCaller.py estimate -i ${SAMPLE} -f ${REF} -r chr{1..22} chrXWhen no matched normal is available, omit -n and set -maf to cap the maximum allele frequency of called variants. A value of 0.1 is appropriate for most somatic applications to exclude common germline variants. The Nextflow pipeline runs the same steps with --tumor_only true; see Quick Start.
SAMPLE=sample1
REF=/path/to/hg38.fa
GERMLINE=/path/to/gnomad.hg38.vcf.gz
NOISE=/path/to/noise_mask.bed.gz
THREADS=16
# 1. Trim barcodes
DupCaller.py trim -i ${SAMPLE}_R1.fq.gz -i2 ${SAMPLE}_R2.fq.gz -p NNNXXXX -o ${SAMPLE}
# 2. Align
bwa-mem2 mem -C -T 0 -t ${THREADS} -R "@RG\tID:${SAMPLE}\tSM:${SAMPLE}\tPL:ILLUMINA" \
${REF} ${SAMPLE}_1.fastq ${SAMPLE}_2.fastq | samtools sort -@ ${THREADS} > ${SAMPLE}.bam
samtools index -@ ${THREADS} ${SAMPLE}.bam
# 3. Mark duplicates
gatk MarkDuplicates -I ${SAMPLE}.bam -O ${SAMPLE}.mkdped.bam -M ${SAMPLE}.mkdp_metrics.txt \
--READ_NAME_REGEX "(?:.*:)?([0-9]+)[^:]*:([0-9]+)[^:]*:([0-9]+)[^:]*$" \
--DUPLEX_UMI --TAGGING_POLICY OpticalOnly --BARCODE_TAG DB
samtools index ${SAMPLE}.mkdped.bam
# 4. Call variants (no matched normal; filter by maximum allele frequency)
DupCaller.py call -b ${SAMPLE}.mkdped.bam -f ${REF} -o ${SAMPLE} \
-g ${GERMLINE} -m ${NOISE} -maf 0.1 -p ${THREADS}
# 5. Estimate mutational burden
DupCaller.py estimate -i ${SAMPLE} -f ${REF} -r chr{1..22} chrXPre-built reference indexes, germline VCFs, and noise masks are available for download:
| Resource | Link/Source |
|---|---|
| Reference genome | TCGA hg38 reference file |
| Reference index | Pre-built hg38 DupCaller reference |
| Germline VCF | af-only-gnomad.hg38.vcf.gz from the GATK somatic resource bundle (download, index) |
| Noise mask | NanoSeq SNP_GRCh38.wgns.bed.gz and NOISE_GRCh38.wgns.bed.gz masks from NanoSeq; pass both to call -m (--premade hg38 does) |
| Gene BED (exome) | resources/xgen-exome-hyb-panel-v2-targets-hg38.bed.gz in this repository: IDT xGen Exome Hyb Panel v2 targets, for estimate -gb (see Per-Gene Coverage) |
| Resource | Link |
|---|---|
| Reference genome | UCSC mm39 reference file |
| Reference index | Pre-built mm39 DupCaller reference |
| Germline VCF | mgp_strains — includes VCF for SNPs in popular mouse strains. Use the vcf file for your strain. |
| Noise mask | NOISE.mm39.bed.gz, in-house noise mask from mouse duplex sequencing data |
DupCaller: Cheng, Y. et al. Improved Mutation Detection in Duplex Sequencing Data with Sample-Specific Error Profiles. bioRxiv (2025). https://doi.org/10.1101/2025.07.13.664565
PERF: Avvaru, A. K., Sowpati, D. T. & Mishra, R. K. PERF: an exhaustive algorithm for ultra-fast and efficient identification of microsatellites from large DNA sequences. Bioinformatics 34, 943-948 (2018). https://doi.org/10.1093/bioinformatics/btx721
BSD 2-Clause License
Copyright (c) 2024, Alexandrov Lab
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
- Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Yuhe Cheng (yuc211@ucsd.edu)