diff --git a/AGENTS.md b/AGENTS.md index 108414ac..a011c665 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -62,10 +62,35 @@ src/ config.rs — YAML configuration loading (serde), nested tool configs io.rs — Shared I/O utilities (gzip-transparent file reading) gtf.rs — GTF annotation file parser (with configurable attribute extraction) + common/ + mod.rs — Re-exports the shared modules + bam_flags.rs — BAM flag constants and aux-tag helpers + bam_stat.rs — bam_stat.py reimplementation, result types + bam_stat_accum.rs — Read-level counter accumulator feeding bam_stat and samtools + cpp_rng.rs — C++ RNG FFI shim for preseq bootstrap reproducibility + preseq.rs — preseq lc_extrap library complexity extrapolation + samtools/ + mod.rs — Re-exports the samtools writers + stats.rs — samtools stats full output (SN + all histogram sections) + flagstat.rs — samtools flagstat-compatible output + idxstats.rs — samtools idxstats-compatible output + dna/ + mod.rs — Re-exports the DNA submodules + depth.rs — Per-contig depth accumulator (delta array, CIGAR walk, + mate-overlap correction, prefix sum) + gc_bias.rs — Picard CollectGcBiasMetrics reimplementation + hs_metrics.rs — Picard CollectHsMetrics reimplementation (targeted mode) + insert_size.rs — Picard CollectInsertSizeMetrics reimplementation + intervals.rs — BED interval parsing and merging for targeted mode + qualimap.rs — Qualimap bamqc accumulation (windows, coverage, composition) + qualimap_output.rs — genome_results.txt, the raw data tables and the HTML report + wgs_metrics.rs — Picard CollectWgsMetrics reimplementation + mosdepth/ + mod.rs — Per-contig summarisation feeding the mosdepth outputs + output.rs — The six mosdepth-compatible writers (bgzf for the BED outputs) rna/ - mod.rs — Re-exports all submodules (dupradar, featurecounts, rseqc, bam_flags, cpp_rng, preseq, qualimap) - bam_flags.rs — BAM flag constants - cpp_rng.rs — C++ RNG FFI shim for preseq bootstrap reproducibility + mod.rs — Re-exports the RNA submodules (dupradar, featurecounts, rseqc, qualimap) + and re-exports the shared ones from `common` for compatibility dupradar/ mod.rs — Re-exports counting, dupmatrix, fitting, plots counting.rs — BAM read counting engine (largest module) @@ -75,7 +100,6 @@ src/ featurecounts/ mod.rs — Re-exports output output.rs — featureCounts-format output & biotype counting - preseq.rs — preseq lc_extrap library complexity extrapolation qualimap/ mod.rs — Re-exports all Qualimap modules accumulator.rs — Gene body coverage accumulation logic @@ -88,17 +112,15 @@ src/ mod.rs — Re-exports all RSeQC modules + common helpers accumulators.rs — Shared RSeQC accumulator infrastructure (read dispatch) common.rs — Shared junction/intron extraction, from_genes builders - bam_stat.rs — bam_stat.py reimplementation - flagstat.rs — samtools flagstat-compatible output - idxstats.rs — samtools idxstats-compatible output infer_experiment.rs — infer_experiment.py reimplementation inner_distance.rs — inner_distance.py reimplementation junction_annotation.rs — junction_annotation.py reimplementation junction_saturation.rs — junction_saturation.py reimplementation plots.rs — RSeQC plot generation (duplication, junctions, etc.) + genebody_coverage.rs — geneBody_coverage.py reimplementation read_distribution.rs — read_distribution.py reimplementation + read_gc.rs — read_GC.py reimplementation read_duplication.rs — read_duplication.py reimplementation - stats.rs — samtools stats full output (SN + all histogram sections) tin.rs — TIN (Transcript Integrity Number) analysis tests/ integration_test.rs — 12 integration tests vs R dupRadar reference output @@ -107,13 +129,23 @@ tests/ create_test_data.R — R script to regenerate test data + references ``` -Nested module structure — top-level modules (`cli`, `config`, `io`, `gtf`, `rna`) declared -in `main.rs`, no `lib.rs`. The `rna` module contains sub-modules for each tool group. -Inter-module access uses `crate::` paths (e.g., `use crate::rna::dupradar::counting::GeneCounts;`). +Nested module structure. The library crate root is `src/lib.rs`, which declares +`common`, `config`, `cpu`, `gtf`, `io`, `rna` and `summary`; the binary +(`src/main.rs`) additionally declares `cli`, `citations` and `ui`. +Inter-module access uses `crate::` paths (e.g., `use crate::common::bam_stat_accum::BamStatAccum;`). +Assay-agnostic analyses belong in `common`; put new code under `rna` only if it +needs a gene annotation or a library strand protocol. -The CLI uses a single subcommand: +The CLI has two subcommands: - `rustqc rna ... --gtf [OPTIONS]` +- `rustqc dna ... [OPTIONS]` + +The `dna` subcommand needs no annotation. It runs depth of coverage +(mosdepth-compatible), the samtools-compatible outputs and preseq in one pass, +with one worker per contig. Shared flags keep their `rna` names, short forms +and `RUSTQC_*` environment variables, with one deliberate exception: +`-Q/--mapq` defaults to 0 for `dna`, matching mosdepth, rather than 30. A GTF gene annotation file (`--gtf`) is required. This runs all analyses: dupRadar duplicate rate analysis, featureCounts-compatible gene counting, diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b67cce4..6fbf5154 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ # RustQC Changelog +## Unreleased + +### Features + +- New `rustqc dna` subcommand for DNA (WGS) quality control: depth of coverage + with mosdepth-compatible outputs, samtools-compatible stats, flagstat and + idxstats, and preseq library complexity, all in a single pass over the + alignment with one worker per contig, plus Picard-compatible + CollectWgsMetrics, CollectInsertSizeMetrics and CollectGcBiasMetrics. + Passing `--targets` switches on targeted mode and Picard-compatible + CollectHsMetrics. Qualimap-compatible `bamqc` output rounds it out, with + `genome_results.txt`, the raw data tables and an HTML summary. Validated + against mosdepth 0.3.14, samtools 1.24, Picard 3.4.0 and Qualimap 2.3. + +- RSeQC `geneBody_coverage` and `read_GC` equivalents, bringing the RSeQC + tools RustQC reimplements from eight to ten (#127). + +### Changed + +- Internal: assay-agnostic analyses (BAM flag helpers, read-level statistics, + the samtools stats/flagstat/idxstats writers, preseq) moved from `rna` to a + new `common` module. The old `rustqc::rna::...` paths still resolve through + re-exports, so this is not a breaking change for library users. + ## [Version 0.2.1](https://github.com/seqeralabs/RustQC/releases/tag/v0.2.1) - 2026-04-09 ### Bug fixes diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 4e1c2f42..ca7e6f08 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -72,6 +72,15 @@ export default defineConfig({ { label: "Samtools", slug: "rna/samtools" }, ], }, + { + label: "DNA", + items: [ + { label: "Overview", slug: "dna/overview" }, + { label: "mosdepth", slug: "dna/mosdepth" }, + { label: "Picard metrics", slug: "dna/picard" }, + { label: "Qualimap bamqc", slug: "dna/qualimap" }, + ], + }, { label: "About", items: [ diff --git a/docs/src/content/docs/dna/mosdepth.mdx b/docs/src/content/docs/dna/mosdepth.mdx new file mode 100644 index 00000000..0c4e12fd --- /dev/null +++ b/docs/src/content/docs/dna/mosdepth.mdx @@ -0,0 +1,69 @@ +--- +title: mosdepth +description: Depth of coverage outputs compatible with mosdepth, and the semantics RustQC reproduces. +--- + +import { Aside } from "@astrojs/starlight/components"; + + + +RustQC reproduces mosdepth's outputs exactly. On the project's test alignment +every one of the six files is identical to mosdepth 0.3.14's, including the +1094-line global distribution and the 721-interval per-base BED. + +## Files + +| File | Written when | +| --- | --- | +| `{sample}.mosdepth.summary.txt` | always | +| `{sample}.mosdepth.global.dist.txt` | always | +| `{sample}.per-base.bed.gz` (+ `.csi`) | unless `--skip-per-base` | +| `{sample}.regions.bed.gz` (+ `.csi`) | with `--window-size` | +| `{sample}.mosdepth.region.dist.txt` | with `--window-size` | +| `{sample}.thresholds.bed.gz` (+ `.csi`) | with `--window-size` and thresholds | + +Compressed outputs are bgzf with a CSI companion index, so `tabix` can seek +into them just as it can into mosdepth's own. + +## What counts towards depth + +RustQC applies mosdepth's default filters: + +- records carrying any of `UNMAP`, `SECONDARY`, `QCFAIL` or `DUP` are skipped, + which is mosdepth's `-F 1796`; +- records below `--mapq` are skipped, defaulting to 0; +- `M`, `=` and `X` cover the reference; `D` and `N` advance without covering; + `I`, `S`, `H` and `P` do not advance at all; +- **a base covered by both mates of one pair counts once.** + + + +## Reading the distribution files + +`{sample}.mosdepth.global.dist.txt` holds `chrom`, `depth` and `proportion` +rows in descending depth order, where the proportion is the fraction of that +contig's bases at depth **at or above** the given value, ending at depth 0 with +`1.00`. + +Which depths get a row is worth knowing, because it is not simply "every depth +seen": + +- depths 0 through 300 always get a row, whether or not any base sits at that + exact depth; +- above 300, only depths that actually occur; +- the maximum observed depth gets a row when it falls inside that dense range, + and none when it does not. + +The region distribution follows the same rules but is computed over windows and +their **rounded mean** depth, not over individual bases. diff --git a/docs/src/content/docs/dna/overview.mdx b/docs/src/content/docs/dna/overview.mdx new file mode 100644 index 00000000..31799902 --- /dev/null +++ b/docs/src/content/docs/dna/overview.mdx @@ -0,0 +1,99 @@ +--- +title: DNA QC Overview +description: What the rustqc dna subcommand runs, what it writes, and how it differs from the RNA pipeline. +--- + +import { Aside, FileTree } from "@astrojs/starlight/components"; + +`rustqc dna` runs a DNA (whole-genome or targeted) quality control pipeline in +a single pass over each alignment file. Unlike [`rustqc rna`](/rna/dupradar/), +it needs no gene annotation. + +```bash +rustqc dna sample.bam --reference genome.fasta --outdir results/ +``` + +## What it runs + +| Upstream tool | What RustQC produces | +| --- | --- | +| [mosdepth](/dna/mosdepth/) | depth of coverage, per base, per window and per region | +| [Picard `CollectWgsMetrics`](/dna/picard/) | genome-wide coverage metrics with the exclusion breakdown | +| [Picard `CollectInsertSizeMetrics`](/dna/picard/) | insert size distribution per pair orientation | +| [Picard `CollectGcBiasMetrics`](/dna/picard/) | coverage bias against reference GC content | +| [Picard `CollectHsMetrics`](/dna/picard/) | targeted enrichment metrics, when `--targets` is given | +| [Samtools](/rna/samtools/) | `stats`, `flagstat` and `idxstats` | +| [Preseq](/rna/preseq/) | library complexity extrapolation | + +Every one of them is fed from the same record stream, so the alignment is read +once no matter how many are enabled. + +## Requirements + +The input must be **duplicate-marked, not duplicate-removed**. Duplicate rate +is a headline metric here, and several exclusion fractions are defined against +it. RustQC refuses input with no duplicate flags at all unless you pass +`--skip-dup-check`. + +A reference FASTA is needed for three things: reading CRAM, sizing +`GENOME_TERRITORY` for `CollectWgsMetrics`, and binning reference windows for +`CollectGcBiasMetrics`. Without one, those two analyses are skipped with a +warning and everything else still runs. + + + +## Output tree + + +- results/ + - mosdepth/ + - sample.mosdepth.summary.txt + - sample.mosdepth.global.dist.txt + - sample.mosdepth.region.dist.txt + - sample.per-base.bed.gz + - sample.per-base.bed.gz.csi + - sample.regions.bed.gz + - sample.thresholds.bed.gz + - picard/ + - wgs_metrics/ + - insert_size/ + - gc_bias/ + - hs_metrics/ + - samtools/ + - preseq/ + - rustqc_summary.json + - CITATIONS.md + + +Pass `--flat-output` to write everything directly into the output directory +instead. + +## Targeted mode + +Passing `--targets targets.bed` switches the run into targeted mode and adds +[`CollectHsMetrics`](/dna/picard/#collecthsmetrics). `--baits` defaults to the +same intervals; give it separately when the capture baits differ from the +regions you want reported. + +Intervals are merged on load. Overlapping targets would otherwise inflate the +reported territory and count the same base twice. + +## Memory + +The depth engine holds one array of four bytes per base for each contig being +processed, so the largest contig sets the cost per worker: roughly 1 GB for +GRCh38 chr1. `--max-depth-workers` bounds how many are live at once, defaulting +to a 4 GB budget divided by the largest contig. Raise it if you have the memory +and want more parallelism; lower it on a shared machine. + +## JSON summary + +`--json-summary` writes a machine-readable summary carrying genome length, +covered bases, mean, median and maximum coverage, the percentage of the +reference at or above each requested threshold, and the duplicate rate. The +coverage thresholds are a list rather than a map so that the order you asked +for survives. diff --git a/docs/src/content/docs/dna/picard.mdx b/docs/src/content/docs/dna/picard.mdx new file mode 100644 index 00000000..9cef0c28 --- /dev/null +++ b/docs/src/content/docs/dna/picard.mdx @@ -0,0 +1,118 @@ +--- +title: Picard metrics +description: CollectWgsMetrics, CollectInsertSizeMetrics, CollectGcBiasMetrics and CollectHsMetrics, and exactly which columns RustQC reproduces. +--- + +import { Aside } from "@astrojs/starlight/components"; + + + +RustQC reproduces four Picard collectors, validated against Picard 3.4.0. Three +match byte for byte; the fourth matches on every column that does not require a +Monte Carlo simulation. + +## CollectWgsMetrics + +Written to `picard/wgs_metrics/{sample}.wgs_metrics.txt`. Needs `--reference`, +because `GENOME_TERRITORY` counts the reference's non-N bases and cannot be +taken from the alignment header. + +Every column and all 251 histogram lines match Picard, except +`HET_SNP_SENSITIVITY` and `HET_SNP_Q`, which come from Picard's Monte Carlo +`TheoreticalSensitivity` and are written as `?`. + +### The exclusion breakdown + +This is what separates Picard's coverage from a plain depth count, and reading +it is the point of the tool. Unmapped, secondary and supplementary records +never enter the calculation. Every other record's reference-consuming bases +form the denominator of all the `PCT_EXC_*` columns. Exclusions then apply in a +fixed order: + +1. `PCT_EXC_DUPE`, the whole read, when duplicate-flagged; +2. `PCT_EXC_MAPQ`, the whole read, below the mapping quality floor; +3. `PCT_EXC_UNPAIRED`, the whole read, when unpaired; +4. `PCT_EXC_BASEQ`, per base, below the base quality floor; +5. `PCT_EXC_OVERLAP`, per base, where the mate already covered it; +6. `PCT_EXC_CAPPED`, per base, for depth beyond `--coverage-cap`. + +What survives is the high quality coverage the histogram reports. The figures +reconcile: on the test data, 670989 aligned bases less 201120 duplicate, 4933 +low quality, 217866 overlapping and 105814 capped leaves the 141256 the +histogram holds. + +`SD_COVERAGE` is the sample standard deviation over **every** base of the +territory, uncovered ones included, which is why it can dwarf the mean on a +targeted library. + +## CollectInsertSizeMetrics + +Written to `picard/insert_size/{sample}.insert_size_metrics.txt`. Matches +Picard byte for byte, metrics row and histogram. + +A pair counts when it is paired, neither secondary, supplementary, duplicate +nor unmapped, has a mapped mate, and carries a positive `TLEN`, which is what +counts each pair once. Proper-pair is deliberately not required. + +`MEAN_INSERT_SIZE` and `STANDARD_DEVIATION` are computed over the histogram +trimmed to `--deviations` median absolute deviations either side of the median, +while `MIN` and `MAX` are over the untrimmed set, so a single far outlier moves +the reported maximum but not the mean. + +## CollectGcBiasMetrics + +Written to `picard/gc_bias/{sample}.gc_bias.detail_metrics.txt` and +`.summary_metrics.txt`. Needs `--reference`. Both files match Picard byte for +byte. + +`NORMALIZED_COVERAGE` is the read density in a GC bin relative to the +genome-wide density, so 1.0 means a bin is covered exactly in proportion to how +much of the reference sits at that GC content. `AT_DROPOUT` and `GC_DROPOUT` +accumulate, over the bins where reads are under-represented, how many +percentage points of the reference are being missed. + + + +## CollectHsMetrics + +Written to `picard/hs_metrics/{sample}.hs_metrics.txt`, and only when +`--targets` is given. + +All 58 computable columns match Picard, including `HS_LIBRARY_SIZE`, which +solves the Lander-Waterman equation the same way Picard's estimator does. + +Seven columns are not computed and are written the way Picard writes its own +uncomputable values: + +| Column | Why | +| --- | --- | +| `HET_SNP_SENSITIVITY`, `HET_SNP_Q` | Monte Carlo theoretical sensitivity | +| `HS_PENALTY_10X` … `HS_PENALTY_100X` | derived from the same simulation | +| `FOLD_80_BASE_PENALTY` | derived from the same simulation | +| `AT_DROPOUT`, `GC_DROPOUT` | per-target GC binning, not implemented | + + + +## Reproducibility of the reference outputs + +The fixtures RustQC is validated against are regenerated by +`tests/create_dna_test_data.sh`, which pins the tool versions and forces the +JVM locale to English. A French default locale makes Picard write `3,531312` +where an English one writes `3.531312`, which would make the reference outputs +depend on the machine that produced them. diff --git a/docs/src/content/docs/dna/qualimap.mdx b/docs/src/content/docs/dna/qualimap.mdx new file mode 100644 index 00000000..5d350b15 --- /dev/null +++ b/docs/src/content/docs/dna/qualimap.mdx @@ -0,0 +1,69 @@ +--- +title: Qualimap bamqc +description: The bamqc outputs RustQC produces, how Qualimap's coverage differs from every other tool here, and which figures are not reproduced. +--- + +import { Aside } from "@astrojs/starlight/components"; + + + +RustQC writes `qualimap/genome_results.txt`, the +`raw_data_qualimapReport/` tables and an HTML summary. + + + +## Two figures that surprise people + +**Mean mapping quality reads about 2.4, not about 60.** It is the mean of the +per-window means, and a window with no reads contributes zero. On a targeted or +low-coverage library, most windows are empty, so the figure is closer to the +fraction of the genome covered than to the quality of the alignments. The +per-position histogram, which only counts covered positions, is the one to read +for that. + +**Base composition is reported in reference orientation.** Reverse-strand reads +are reverse-complemented before counting, so the A and T columns are not the +counts of A and T in the sequencer's output. + +## What matches Qualimap and what does not + +`genome_results.txt` matches on every line but four, and three of the raw +tables match byte for byte. The residuals, each with its cause: + +| Figure | Difference | +| --- | --- | +| `mean mapping quality` | fourth decimal; 393 of 397 windows match exactly | +| `std coverageData` | fourth decimal, same cause | +| `homopolymer indels` | differs outright, see below | +| coverage histogram and what derives from it | about five reference positions of 40001 sit one deeper | +| `genome_fraction_coverage` | last two digits of the double, Qualimap accumulates per window | +| `insert_size_histogram` | one extra row: Qualimap trims the largest insert from the plotted table while still counting it in the statistics | + +Qualimap classifies an indel as a homopolymer indel against a reference context +RustQC does not reconstruct. It reports two polyC indels on the test data, and +no rule derived from the read alone produces them, because the deleted bases +are not in the read. RustQC uses a run-of-four rule instead, so this one figure +will differ. + +Qualimap's GC content distribution and duplication rate histogram are not +written at all. The first is computed over a 679-read subsample whose selection +rule is not documented; the second uses a definition that does not match a +read-start-position count. Emitting tables under those names with different +numbers would be worse than leaving them out. + +## The HTML report + +RustQC writes its own summary page rather than a copy of Qualimap's, which +ships a bundle of images, CSS and JavaScript. It carries the same numbers as +`genome_results.txt`; the raw tables remain the machine-readable source. diff --git a/docs/src/content/docs/usage/cli-reference.mdx b/docs/src/content/docs/usage/cli-reference.mdx index deca6b40..3c638844 100644 --- a/docs/src/content/docs/usage/cli-reference.mdx +++ b/docs/src/content/docs/usage/cli-reference.mdx @@ -246,6 +246,70 @@ Preseq runs by default and can be skipped entirely with `--skip-preseq`. --- +## `dna` + +DNA quality control: depth of coverage, Picard metrics, samtools-compatible +outputs and library complexity, in a single pass. Needs no annotation. See the +[DNA overview](/dna/overview/) for what each output contains. + +### Synopsis + +```bash +rustqc dna ... [OPTIONS] +``` + +### Shared options + +`-o/--outdir`, `--sample-name`, `--flat-output`, `-c/--config`, +`-j/--json-summary`, `-t/--threads`, `-p/--paired`, `-q/--quiet`, +`-v/--verbose`, `--skip-dup-check`, `--skip-preseq` and the `--preseq-*` family +all behave exactly as they do for `rna`, with the same short flags and the same +`RUSTQC_*` environment variables. + + + +### DNA-specific options + +| Option | Default | Description | +| ------------------------------- | ---------------------- | -------------------------------------------------------------------- | +| `-r, --reference ` | none | Required for CRAM, `CollectWgsMetrics` and `CollectGcBiasMetrics` | +| `--targets ` | none | Switches on targeted mode and `CollectHsMetrics` | +| `--baits ` | same as `--targets` | Capture bait intervals, when they differ from the targets | +| `--depth-thresholds ` | `1,5,10,15,20,30,50` | Coverage thresholds to report | +| `--window-size ` | none | Fixed-width windows for per-window depth, mosdepth's `--by` | +| `--coverage-cap ` | `250` | Picard `COVERAGE_CAP` | +| `--min-base-quality ` | `20` | Picard `MINIMUM_BASE_QUALITY` | +| `--skip-per-base` | off | Suppress `per-base.bed.gz`, by far the largest output | +| `--skip-gc-bias` | off | Skip `CollectGcBiasMetrics` | +| `--max-depth-workers ` | derived from RAM | Cap on concurrently live per-contig depth arrays | + +### Examples + +```bash +# Whole genome, with the reference so every metric is available +rustqc dna sample.bam --reference genome.fasta --outdir results/ + +# Per-window depth and a custom threshold set +rustqc dna sample.bam -r genome.fasta --window-size 500 --depth-thresholds 1,10,30 + +# Targeted (exome or panel) mode +rustqc dna sample.bam -r genome.fasta --targets exome.bed --outdir results/ + +# Large genome on a shared machine: bound the depth memory explicitly +rustqc dna sample.bam -r genome.fasta --threads 16 --max-depth-workers 2 +``` + + + +--- + ## Exit codes | Code | Meaning | diff --git a/log.txt b/log.txt new file mode 100644 index 00000000..fc2bc6cd --- /dev/null +++ b/log.txt @@ -0,0 +1,12 @@ +@ 2026-08-29 12:05:10: Read BED file (reference gene model) ... +@ 2026-08-29 12:05:10: Total 0 transcripts loaded +@ 2026-08-29 12:05:10: Get BAM file(s) ... +@ 2026-08-29 12:05:10: Processing test.bam ... +@ 2026-08-29 12:05:10: Running R script ... +@ 2026-08-29 12:05:30: Read BED file (reference gene model) ... +@ 2026-08-29 12:05:30: Total 8 transcripts loaded +@ 2026-08-29 12:05:30: Get BAM file(s) ... +@ 2026-08-29 12:05:30: Processing test.bam ... +@ 2026-08-29 12:05:30: Running R script ... +@ 2026-08-29 12:18:07: Total 8 transcripts loaded +@ 2026-08-29 12:18:23: Total 8 transcripts loaded diff --git a/src/citations.rs b/src/citations.rs index 316ed4d1..0fb4c376 100644 --- a/src/citations.rs +++ b/src/citations.rs @@ -3,7 +3,7 @@ //! Writes a Markdown file alongside results documenting which upstream tools //! RustQC replicated in this run, their validated versions, and citation info. -use crate::config::RnaConfig; +use crate::config::{DnaConfig, RnaConfig}; use anyhow::{Context, Result}; use std::io::Write; use std::path::Path; @@ -48,6 +48,14 @@ const PRESEQ: Citation = Citation { doi: "10.1038/nmeth.2375", }; +const MOSDEPTH: Citation = Citation { + heading: "mosdepth (v0.3.14)", + description: "RustQC reimplements the depth of coverage analysis of mosdepth.", + reference: "Pedersen BS, Quinlan AR. Mosdepth: quick coverage calculation for genomes and exomes. *Bioinformatics*. 2018;34(5):867-868.", + url: "https://github.com/brentp/mosdepth", + doi: "10.1093/bioinformatics/btx699", +}; + const SAMTOOLS: Citation = Citation { heading: "Samtools (v1.22.1)", description: "RustQC produces Samtools-compatible flagstat, idxstats, and stats output.", @@ -56,6 +64,19 @@ const SAMTOOLS: Citation = Citation { doi: "10.1093/gigascience/giab008", }; +/// Same tool as [`SAMTOOLS`], different validated version. +/// +/// The `rna` pipeline's outputs were checked against samtools 1.22.1 and the +/// `dna` pipeline's against 1.24, so each cites the version it was actually +/// compared with rather than both claiming the newer one. +const SAMTOOLS_DNA: Citation = Citation { + heading: "Samtools (v1.24)", + description: "RustQC produces Samtools-compatible flagstat, idxstats, and stats output.", + reference: "Danecek P, Bonfield JK, Liddle J, et al. Twelve years of Samtools and BCFtools. *GigaScience*. 2021;10(2):giab008.", + url: "http://www.htslib.org/", + doi: "10.1093/gigascience/giab008", +}; + const QUALIMAP: Citation = Citation { heading: "Qualimap (v2.3)", description: "RustQC produces gene body coverage output compatible with Qualimap rnaseq.", @@ -79,21 +100,7 @@ pub fn write_citations(path: &Path, config: &RnaConfig, version: &str, commit: & .with_context(|| format!("Failed to create citations file: {}", path.display()))?; let mut w = std::io::BufWriter::new(file); - writeln!(w, "# RustQC Citations\n")?; - writeln!( - w, - "This file was generated by [RustQC](https://github.com/seqeralabs/RustQC) v{version} ({commit})." - )?; - writeln!( - w, - "It documents the upstream tools whose behaviour this run replicated." - )?; - writeln!( - w, - "Please cite both RustQC and the relevant upstream tools listed below.\n" - )?; - writeln!(w, "## RustQC (v{version})\n")?; - writeln!(w, "- Repository: \n")?; + write_header(&mut w, version, commit)?; if config.any_dupradar_output() { write_citation(&mut w, &DUPRADAR)?; @@ -118,6 +125,60 @@ pub fn write_citations(path: &Path, config: &RnaConfig, version: &str, commit: & Ok(()) } +/// Write `CITATIONS.md` for a `dna` run. +/// +/// Shares the header and the per-tool blocks with [`write_citations`]; only +/// the set of tools differs, because the two pipelines replicate different +/// upstream programs. +pub fn write_dna_citations( + path: &Path, + config: &DnaConfig, + version: &str, + commit: &str, +) -> Result<()> { + let file = std::fs::File::create(path) + .with_context(|| format!("Failed to create citations file: {}", path.display()))?; + let mut w = std::io::BufWriter::new(file); + + write_header(&mut w, version, commit)?; + + if config.mosdepth.enabled { + write_citation(&mut w, &MOSDEPTH)?; + } + if config.samtools.enabled { + write_citation(&mut w, &SAMTOOLS_DNA)?; + } + if config.qualimap.enabled { + write_citation(&mut w, &QUALIMAP)?; + } + if config.preseq.enabled { + write_citation(&mut w, &PRESEQ)?; + } + + w.flush()?; + Ok(()) +} + +/// Shared preamble of both citation files. +fn write_header(w: &mut W, version: &str, commit: &str) -> Result<()> { + writeln!(w, "# RustQC Citations\n")?; + writeln!( + w, + "This file was generated by [RustQC](https://github.com/seqeralabs/RustQC) v{version} ({commit})." + )?; + writeln!( + w, + "It documents the upstream tools whose behaviour this run replicated." + )?; + writeln!( + w, + "Please cite both RustQC and the relevant upstream tools listed below.\n" + )?; + writeln!(w, "## RustQC (v{version})\n")?; + writeln!(w, "- Repository: \n")?; + Ok(()) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/cli.rs b/src/cli.rs index 6e6459e8..2f8965a5 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -32,6 +32,13 @@ pub enum Commands { /// analyses in one pass. Requires a GTF annotation and duplicate-marked /// (not removed) alignments. Rna(RnaArgs), + + /// DNA QC — single-pass analysis of BAM/SAM/CRAM files. + /// + /// Runs depth of coverage, samtools stats and library complexity + /// estimation in one pass. Needs no gene annotation. Pass `--targets` + /// to switch to targeted (exome or panel) mode. + Dna(DnaArgs), } /// Arguments for the `rna` subcommand. @@ -369,6 +376,278 @@ pub struct RnaArgs { pub preseq_seg_len: Option, } +/// Arguments for the `dna` subcommand. +/// +/// Shared options keep the same long name, short flag and `RUSTQC_*` +/// environment variable as their `rna` counterparts, so wrapper scripts and +/// muscle memory carry over between the two pipelines. The differences are +/// deliberate: there is no `--gtf` and no `--stranded`, and `--mapq` defaults +/// to 0 rather than 30 because that is mosdepth's default. +#[derive(Parser, Debug)] +#[command( + next_line_help = false, + term_width = 120, + help_template = "\ +{about-with-newline} +{usage-heading} {usage} + +{all-args}" +)] +pub struct DnaArgs { + // ── Input / Output ────────────────────────────────────────────────── + /// Duplicate-marked alignment file(s) + #[arg(value_name = "INPUT", num_args = 1.., required = true, help_heading = "Input / Output")] + pub input: Vec, + + /// Reference FASTA (required for CRAM and for GC bias) + #[arg( + short, + long, + value_name = "FASTA", + env = "RUSTQC_REFERENCE", + help_heading = "Input / Output" + )] + pub reference: Option, + + /// Target intervals BED; switches on targeted (exome or panel) mode + #[arg( + long, + value_name = "BED", + env = "RUSTQC_TARGETS", + help_heading = "Input / Output" + )] + pub targets: Option, + + /// Capture bait intervals BED [default: same as --targets] + #[arg( + long, + value_name = "BED", + env = "RUSTQC_BAITS", + requires = "targets", + help_heading = "Input / Output" + )] + pub baits: Option, + + /// Output directory [default: .] + #[arg( + short, + long, + default_value = ".", + hide_default_value = true, + env = "RUSTQC_OUTDIR", + help_heading = "Input / Output" + )] + pub outdir: String, + + /// Override sample name for output filenames (default: derived from BAM filename) + #[arg( + long, + value_name = "NAME", + env = "RUSTQC_SAMPLE_NAME", + help_heading = "Input / Output" + )] + pub sample_name: Option, + + /// Write outputs to a flat directory (no subdirs) + #[arg( + long, + default_value_t = false, + env = "RUSTQC_FLAT_OUTPUT", + help_heading = "Input / Output" + )] + pub flat_output: bool, + + /// YAML configuration file (see also: RUSTQC_CONFIG env var) + #[arg(short, long, value_name = "CONFIG", help_heading = "Input / Output")] + pub config: Option, + + /// JSON summary path (use "-" for stdout) + #[arg(short = 'j', long = "json-summary", value_name = "PATH", num_args = 0..=1, default_missing_value = "", env = "RUSTQC_JSON_SUMMARY", help_heading = "Input / Output")] + pub json_summary: Option, + + // ── Library ───────────────────────────────────────────────────────── + /// Paired-end reads + #[arg(short, long, env = "RUSTQC_PAIRED", help_heading = "Library")] + pub paired: bool, + + // ── General ───────────────────────────────────────────────────────── + /// Number of threads [default: 1] + #[arg( + short, + long, + default_value_t = 1, + hide_default_value = true, + env = "RUSTQC_THREADS", + help_heading = "General" + )] + pub threads: usize, + + /// MAPQ cutoff; reads below it are ignored [default: 0] + #[arg( + short = 'Q', + long = "mapq", + default_value_t = 0, + hide_default_value = true, + env = "RUSTQC_MAPQ", + help_heading = "General" + )] + pub mapq_cut: u8, + + /// Skip duplicate-marking check + #[arg( + long, + default_value_t = false, + env = "RUSTQC_SKIP_DUP_CHECK", + help_heading = "General" + )] + pub skip_dup_check: bool, + + /// Suppress output except warnings/errors + #[arg( + short = 'q', + long, + conflicts_with = "verbose", + env = "RUSTQC_QUIET", + help_heading = "General" + )] + pub quiet: bool, + + /// Show additional detail + #[arg( + short = 'v', + long, + conflicts_with = "quiet", + env = "RUSTQC_VERBOSE", + help_heading = "General" + )] + pub verbose: bool, + + // ── Tool parameters ───────────────────────────────────────────────── + /// Coverage thresholds to report [default: 1,5,10,15,20,30,50] + #[arg( + long = "depth-thresholds", + value_name = "N,...", + value_delimiter = ',', + default_values_t = vec![1u32, 5, 10, 15, 20, 30, 50], + hide_default_value = true, + env = "RUSTQC_DEPTH_THRESHOLDS", + help_heading = "Tool parameters" + )] + pub depth_thresholds: Vec, + + /// Fixed-width window size for per-window depth + #[arg( + long = "window-size", + value_name = "N", + env = "RUSTQC_WINDOW_SIZE", + help_heading = "Tool parameters" + )] + pub window_size: Option, + + /// Picard COVERAGE_CAP [default: 250] + #[arg( + long = "coverage-cap", + value_name = "N", + default_value_t = 250, + hide_default_value = true, + env = "RUSTQC_COVERAGE_CAP", + help_heading = "Tool parameters" + )] + pub coverage_cap: u32, + + /// Picard MINIMUM_BASE_QUALITY [default: 20] + #[arg( + long = "min-base-quality", + value_name = "N", + default_value_t = 20, + hide_default_value = true, + env = "RUSTQC_MIN_BASE_QUALITY", + help_heading = "Tool parameters" + )] + pub min_base_quality: u8, + + /// Skip the per-base depth output, by far the largest file + #[arg( + long, + default_value_t = false, + env = "RUSTQC_SKIP_PER_BASE", + help_heading = "Tool parameters" + )] + pub skip_per_base: bool, + + /// Skip GC bias metrics + #[arg( + long, + default_value_t = false, + env = "RUSTQC_SKIP_GC_BIAS", + help_heading = "Tool parameters" + )] + pub skip_gc_bias: bool, + + /// Cap on concurrently live per-contig depth arrays [default: derived from RAM] + #[arg( + long = "max-depth-workers", + value_name = "N", + env = "RUSTQC_MAX_DEPTH_WORKERS", + help_heading = "Tool parameters" + )] + pub max_depth_workers: Option, + + /// Skip preseq library complexity analysis + #[arg( + long, + default_value_t = false, + env = "RUSTQC_SKIP_PRESEQ", + help_heading = "Tool parameters" + )] + pub skip_preseq: bool, + + /// preseq: random seed for bootstrap CIs + #[arg( + long = "preseq-seed", + value_name = "N", + env = "RUSTQC_PRESEQ_SEED", + help_heading = "Tool parameters" + )] + pub preseq_seed: Option, + + /// preseq: max extrapolation depth + #[arg( + long = "preseq-max-extrap", + value_name = "N", + env = "RUSTQC_PRESEQ_MAX_EXTRAP", + help_heading = "Tool parameters" + )] + pub preseq_max_extrap: Option, + + /// preseq: step size between points + #[arg( + long = "preseq-step-size", + value_name = "N", + env = "RUSTQC_PRESEQ_STEP_SIZE", + help_heading = "Tool parameters" + )] + pub preseq_step_size: Option, + + /// preseq: bootstrap replicates for CIs + #[arg( + long = "preseq-n-bootstraps", + value_name = "N", + env = "RUSTQC_PRESEQ_N_BOOTSTRAPS", + help_heading = "Tool parameters" + )] + pub preseq_n_bootstraps: Option, + + /// preseq: max segment length for PE merging + #[arg( + long = "preseq-seg-len", + value_name = "N", + env = "RUSTQC_PRESEQ_SEG_LEN", + help_heading = "Tool parameters" + )] + pub preseq_seg_len: Option, +} + /// Parse command-line arguments and return the Cli struct. /// /// Sets a `long_version` that includes the git commit, build timestamp, @@ -413,7 +692,6 @@ mod tests { assert_eq!(args.min_intron, None); assert_eq!(args.inner_distance_step, None); } - #[allow(unreachable_patterns)] _ => panic!("Expected Rna subcommand"), } } @@ -435,7 +713,6 @@ mod tests { assert_eq!(args.input, vec!["a.bam", "b.bam", "c.bam"]); assert_eq!(args.gtf, "genes.gtf"); } - #[allow(unreachable_patterns)] _ => panic!("Expected Rna subcommand"), } } @@ -470,7 +747,6 @@ mod tests { assert_eq!(args.reference, Some("genome.fa".to_string())); assert_eq!(args.mapq_cut, 20); } - #[allow(unreachable_patterns)] _ => panic!("Expected Rna subcommand"), } } @@ -512,7 +788,6 @@ mod tests { assert_eq!(args.inner_distance_upper_bound, Some(500)); assert_eq!(args.inner_distance_step, Some(10)); } - #[allow(unreachable_patterns)] _ => panic!("Expected Rna subcommand"), } } @@ -542,7 +817,6 @@ mod tests { assert_eq!(args.preseq_n_bootstraps, Some(200)); assert_eq!(args.preseq_seg_len, Some(100_000_000)); } - #[allow(unreachable_patterns)] _ => panic!("Expected Rna subcommand"), } } @@ -568,7 +842,6 @@ mod tests { assert_eq!(args.tin_seed, Some(2)); assert_eq!(args.junction_saturation_seed, Some(3)); } - #[allow(unreachable_patterns)] _ => panic!("Expected Rna subcommand"), } } @@ -587,8 +860,80 @@ mod tests { Commands::Rna(args) => { assert!(args.skip_preseq); } - #[allow(unreachable_patterns)] _ => panic!("Expected Rna subcommand"), } } + + #[test] + fn test_dna_default_args() { + let cli = Cli::parse_from(["rustqc", "dna", "test.bam"]); + match cli.command { + Commands::Dna(args) => { + assert_eq!(args.input, vec!["test.bam"]); + assert_eq!(args.outdir, "."); + assert_eq!(args.threads, 1); + assert_eq!(args.mapq_cut, 0); + assert_eq!(args.coverage_cap, 250); + assert_eq!(args.min_base_quality, 20); + assert_eq!(args.depth_thresholds, vec![1, 5, 10, 15, 20, 30, 50]); + assert_eq!(args.window_size, None); + assert!(args.targets.is_none()); + assert!(args.baits.is_none()); + assert!(!args.skip_per_base); + assert!(!args.skip_gc_bias); + } + _ => panic!("Expected Dna subcommand"), + } + } + + #[test] + fn test_dna_no_gtf_required() { + assert!(Cli::try_parse_from(["rustqc", "dna", "test.bam"]).is_ok()); + } + + #[test] + fn test_dna_targeted_args() { + let cli = Cli::parse_from([ + "rustqc", + "dna", + "a.bam", + "b.bam", + "--targets", + "t.bed", + "--baits", + "b.bed", + "--depth-thresholds", + "1,10,100", + "--window-size", + "500", + "--reference", + "genome.fa", + "-Q", + "20", + "--threads", + "4", + ]); + match cli.command { + Commands::Dna(args) => { + assert_eq!(args.input, vec!["a.bam", "b.bam"]); + assert_eq!(args.targets, Some("t.bed".to_string())); + assert_eq!(args.baits, Some("b.bed".to_string())); + assert_eq!(args.depth_thresholds, vec![1, 10, 100]); + assert_eq!(args.window_size, Some(500)); + assert_eq!(args.reference, Some("genome.fa".to_string())); + assert_eq!(args.mapq_cut, 20); + assert_eq!(args.threads, 4); + } + _ => panic!("Expected Dna subcommand"), + } + } + + #[test] + fn test_dna_baits_without_targets_is_rejected() { + let result = Cli::try_parse_from(["rustqc", "dna", "test.bam", "--baits", "b.bed"]); + assert!( + result.is_err(), + "--baits without --targets must be rejected" + ); + } } diff --git a/src/rna/bam_flags.rs b/src/common/bam_flags.rs similarity index 100% rename from src/rna/bam_flags.rs rename to src/common/bam_flags.rs diff --git a/src/rna/rseqc/bam_stat.rs b/src/common/bam_stat.rs similarity index 100% rename from src/rna/rseqc/bam_stat.rs rename to src/common/bam_stat.rs diff --git a/src/common/bam_stat_accum.rs b/src/common/bam_stat_accum.rs new file mode 100644 index 00000000..f4f08408 --- /dev/null +++ b/src/common/bam_stat_accum.rs @@ -0,0 +1,1313 @@ +//! Read-level alignment statistics accumulator. +//! +//! [`BamStatAccum`] gathers, in a single pass over the records, every counter +//! consumed by RSeQC `bam_stat` and by the samtools-compatible `stats`, +//! `flagstat` and `idxstats` writers. It needs no annotation and no library +//! protocol, so both the `rna` and `dna` pipelines drive the same struct: each +//! parallel worker owns one, and they are merged before conversion. + +use std::collections::HashMap; + +use rust_htslib::bam; + +use crate::common::bam_flags::*; +use crate::common::bam_stat::{BamStatResult, GcDepthBin}; + +/// Default GC-depth bin size in base pairs (matches upstream samtools default). +const GCD_BIN_SIZE: u64 = 20_000; + +// =================================================================== +// Merge helpers for Vec<[u64; N]> per-cycle arrays +// =================================================================== + +/// Merge two `Vec<[u64; N]>` arrays element-wise, extending target if shorter. +fn merge_vec_arrays(target: &mut Vec<[u64; N]>, source: Vec<[u64; N]>) { + if source.len() > target.len() { + target.resize(source.len(), [0u64; N]); + } + for (i, arr) in source.into_iter().enumerate() { + for j in 0..N { + target[i][j] += arr[j]; + } + } +} + +/// bam_stat accumulator — simple flag/MAPQ counting. +/// +/// Also collects the additional counters needed for samtools-compatible +/// flagstat, idxstats, and stats output. +#[derive(Debug)] +pub struct BamStatAccum { + // --- RSeQC bam_stat fields (original) --- + /// Total BAM records seen (primary + secondary + supplementary + unmapped). + pub total_records: u64, + /// Records with QC-fail flag (0x200). + pub qc_failed: u64, + /// Records with duplicate flag (0x400). + pub duplicates: u64, + /// Secondary alignment records (0x100). RSeQC calls these "non-primary". + pub non_primary: u64, + /// Unmapped reads (0x4). + pub unmapped: u64, + /// Mapped reads with MAPQ < cutoff. + pub non_unique: u64, + /// Mapped reads with MAPQ >= cutoff (uniquely mapped). + pub unique: u64, + /// Among unique reads: read1 in a pair. + pub read_1: u64, + /// Among unique reads: read2 in a pair. + pub read_2: u64, + /// Among unique reads: forward strand. + pub forward: u64, + /// Among unique reads: reverse strand. + pub reverse: u64, + /// Among unique reads: has splice junction (CIGAR N). + pub splice: u64, + /// Among unique reads: no splice junctions. + pub non_splice: u64, + /// Among unique reads: in proper pairs (0x2). + pub proper_pairs: u64, + /// Among proper-paired unique reads: mates on different chromosomes. + pub proper_pair_diff_chrom: u64, + + // --- samtools flagstat additional fields --- + /// Secondary alignments (0x100) — counted independently of QC/dup. + pub secondary: u64, + /// Supplementary alignments (0x800) — counted independently of QC/dup. + pub supplementary: u64, + /// All mapped records (not 0x4), regardless of QC/dup. + pub mapped: u64, + /// Paired reads (0x1), regardless of QC/dup. + pub paired_flagstat: u64, + /// Read1 in pair (0x40), regardless of QC/dup — for flagstat. + pub read1_flagstat: u64, + /// Read2 in pair (0x80), regardless of QC/dup — for flagstat. + pub read2_flagstat: u64, + /// First fragments for samtools stats: primary reads that are not "last fragments". + pub first_fragments: u64, + /// Last fragments for samtools stats: primary reads with 0x80 flag. + pub last_fragments: u64, + /// Properly paired reads (0x1 + 0x2), regardless of QC/dup. + pub properly_paired: u64, + /// Both mates mapped (paired + both !unmapped). + pub both_mapped: u64, + /// Singletons (paired, this mapped, mate unmapped). + pub singletons: u64, + /// Paired, both mapped, different reference. + pub mate_diff_chr: u64, + /// Paired, both mapped, different reference, MAPQ >= 5. + pub mate_diff_chr_mapq5: u64, + + // --- samtools idxstats additional fields --- + /// Per-reference (tid) mapped and unmapped counts. + pub chrom_counts: HashMap, + /// Unmapped reads with no reference (tid < 0). + pub unplaced_unmapped: u64, + + // --- samtools stats SN additional fields --- + /// Sum of query sequence lengths for all primary reads (non-secondary, non-supplementary). + pub total_len: u64, + /// Sum of first fragment (read1 or unpaired) sequence lengths. + pub total_first_fragment_len: u64, + /// Sum of last fragment (read2) sequence lengths. + pub total_last_fragment_len: u64, + /// Sum of query lengths for mapped primary reads. + pub bases_mapped: u64, + /// Sum of M/=/X CIGAR operations for mapped primary reads. + pub bases_mapped_cigar: u64, + /// Sum of query lengths for duplicate-flagged primary reads. + pub bases_duplicated: u64, + /// Maximum query sequence length (among primary reads). + pub max_len: u64, + /// Maximum first-fragment sequence length. + pub max_first_fragment_len: u64, + /// Maximum last-fragment sequence length. + pub max_last_fragment_len: u64, + /// Sum of average per-read base qualities (for average-of-averages). + pub quality_sum: f64, + /// Number of reads contributing to quality_sum (primary, non-QC-fail). + pub quality_count: u64, + /// Sum of NM tag values across mapped primary reads. + pub mismatches: u64, + /// Insert size with orientation: abs_tlen → [total, inward, outward, other]. + /// Only one mate per pair contributes (upstream mate), capped at 8000. + pub is_hist: HashMap, + /// Inward-oriented pairs (FR). + pub inward_pairs: u64, + /// Outward-oriented pairs (RF). + pub outward_pairs: u64, + /// Other orientation pairs (FF, RR). + pub other_orientation: u64, + /// Total primary reads (non-secondary, non-supplementary). + pub primary_count: u64, + /// Primary mapped reads count (non-secondary, non-supplementary, !unmapped). + pub primary_mapped: u64, + /// Primary duplicate reads. + pub primary_duplicates: u64, + /// Primary mapped reads with MAPQ = 0 (matching upstream samtools stats). + pub reads_mq0: u64, + /// Primary non-QC-fail mapped paired reads where mate is also mapped. + pub reads_mapped_and_paired: u64, + + // --- samtools stats histogram/distribution fields --- + /// Read length histogram (all primary reads): length → count. + pub rl_hist: HashMap, + /// First fragment read length histogram: length → count. + pub frl_hist: HashMap, + /// Last fragment read length histogram: length → count. + pub lrl_hist: HashMap, + /// MAPQ histogram: primary, mapped, !qcfail, !dup (quality 0-255). + pub mapq_hist: [u64; 256], + /// Per-cycle quality for first fragments (primary, mapped, !qcfail, !dup). + /// Outer: cycle index. Inner: quality value → count (64 buckets covers Q0-Q63). + pub ffq: Vec<[u64; 64]>, + /// Per-cycle quality for last fragments. + pub lfq: Vec<[u64; 64]>, + /// GC content step-function for first fragments, 200 bins (matching samtools ngc=200). + /// Each bin i stores the number of reads with gc_count * 199 / seq_len <= i. + pub gcf: [u64; 200], + /// GC content step-function for last fragments, 200 bins. + pub gcl: [u64; 200], + /// Per-cycle base composition for first fragments (primary, mapped, !qcfail, !dup). + /// [A, C, G, T, N, Other] per cycle. + pub fbc: Vec<[u64; 6]>, + /// Per-cycle base composition for last fragments. + pub lbc: Vec<[u64; 6]>, + /// Per-cycle base composition (read-oriented) for first fragments. + /// Reverse strand reads contribute in reversed cycle order. + pub fbc_ro: Vec<[u64; 6]>, + /// Per-cycle base composition (read-oriented) for last fragments. + pub lbc_ro: Vec<[u64; 6]>, + /// Per-cycle base composition (reverse-complemented for reverse-strand reads, + /// combined first+last fragments). Used for GCT output. [A, C, G, T] only. + pub gcc_rc: Vec<[u64; 4]>, + /// Total base counters for first fragments: [A, C, G, T, N]. + pub ftc: [u64; 5], + /// Total base counters for last fragments: [A, C, G, T, N]. + pub ltc: [u64; 5], + /// Indel distribution by size: length → [insertions, deletions]. + pub id_hist: HashMap, + /// Indels per cycle: cycle → [ins_fwd, ins_rev, del_fwd, del_rev]. + pub ic: Vec<[u64; 4]>, + /// CRC32 checksum sums: [names, sequences, qualities]. + /// Each is the wrapping u32 sum of per-read CRC32 values. + pub chk: [u32; 3], + /// Coverage distribution: depth → number of reference positions at that depth. + /// Populated from a round buffer pileup during sorted BAM processing. + pub cov_hist: HashMap, + /// Circular buffer for coverage pileup, matching upstream samtools design. + /// `cov_buf[cov_buf_idx]` corresponds to reference position `cov_buf_pos`. + /// The buffer grows dynamically to accommodate `max_read_length * 5`. + cov_buf: Vec, + /// Index into `cov_buf` corresponding to `cov_buf_pos`. + cov_buf_idx: usize, + /// Reference position of the element at `cov_buf[cov_buf_idx]`. + cov_buf_pos: i64, + /// Current chromosome tid for round buffer tracking. + cov_buf_tid: i32, + + // --- GC-depth (GCD section) fields --- + /// Accumulated GC-depth bins (one per `GCD_BIN_SIZE`-bp genomic window). + gcd_bins: Vec, + /// Start position of the current GCD bin. + gcd_pos: i64, + /// Chromosome tid of the current GCD bin. + gcd_tid: i32, +} + +impl Default for BamStatAccum { + fn default() -> Self { + Self { + total_records: 0, + qc_failed: 0, + duplicates: 0, + non_primary: 0, + unmapped: 0, + non_unique: 0, + unique: 0, + read_1: 0, + read_2: 0, + forward: 0, + reverse: 0, + splice: 0, + non_splice: 0, + proper_pairs: 0, + proper_pair_diff_chrom: 0, + secondary: 0, + supplementary: 0, + mapped: 0, + paired_flagstat: 0, + read1_flagstat: 0, + read2_flagstat: 0, + first_fragments: 0, + last_fragments: 0, + properly_paired: 0, + both_mapped: 0, + singletons: 0, + mate_diff_chr: 0, + mate_diff_chr_mapq5: 0, + chrom_counts: HashMap::new(), + unplaced_unmapped: 0, + total_len: 0, + total_first_fragment_len: 0, + total_last_fragment_len: 0, + bases_mapped: 0, + bases_mapped_cigar: 0, + bases_duplicated: 0, + max_len: 0, + max_first_fragment_len: 0, + max_last_fragment_len: 0, + quality_sum: 0.0, + quality_count: 0, + mismatches: 0, + is_hist: HashMap::new(), + inward_pairs: 0, + outward_pairs: 0, + other_orientation: 0, + primary_count: 0, + primary_mapped: 0, + primary_duplicates: 0, + reads_mq0: 0, + reads_mapped_and_paired: 0, + rl_hist: HashMap::new(), + frl_hist: HashMap::new(), + lrl_hist: HashMap::new(), + mapq_hist: [0u64; 256], + ffq: Vec::new(), + lfq: Vec::new(), + gcf: [0u64; 200], + gcl: [0u64; 200], + fbc: Vec::new(), + lbc: Vec::new(), + fbc_ro: Vec::new(), + lbc_ro: Vec::new(), + gcc_rc: Vec::new(), + ftc: [0u64; 5], + ltc: [0u64; 5], + id_hist: HashMap::new(), + ic: Vec::new(), + chk: [0u32; 3], + cov_hist: HashMap::new(), + cov_buf: vec![0u32; 1500], // matches upstream samtools: nbases * 5 = 300 * 5 + cov_buf_idx: 0, + cov_buf_pos: 0, + cov_buf_tid: -1, + gcd_bins: Vec::new(), + gcd_pos: -1, + gcd_tid: -1, + } + } +} + +impl BamStatAccum { + /// Process a single BAM record. Called for EVERY record (before counting filters). + /// + /// Collects counters for: + /// - RSeQC bam_stat (original cascade with early returns) + /// - samtools flagstat (counts all records independently) + /// - samtools idxstats (per-reference mapped/unmapped counts) + /// - samtools stats SN section (sequence lengths, quality, insert size, etc.) + pub fn process_read(&mut self, record: &bam::Record, mapq_cut: u8) { + let flags = record.flags(); + self.total_records += 1; + + let is_secondary = flags & BAM_FSECONDARY != 0; + let is_supplementary = flags & BAM_FSUPPLEMENTARY != 0; + let is_unmapped = flags & BAM_FUNMAP != 0; + let is_paired = flags & BAM_FPAIRED != 0; + let is_dup = flags & BAM_FDUP != 0; + let is_qcfail = flags & BAM_FQCFAIL != 0; + let is_primary = !is_secondary && !is_supplementary; + let is_mapped = !is_unmapped; + let tid = record.tid(); + let mapq = record.mapq(); + + // ================================================================= + // samtools flagstat counters (count ALL records, no early returns) + // ================================================================= + if is_secondary { + self.secondary += 1; + } + if is_supplementary { + self.supplementary += 1; + } + if is_mapped { + self.mapped += 1; + } + // samtools stats: "1st fragments" / "last fragments" count primary reads only + // For paired reads: read2 flag -> last, everything else -> 1st + // For SE reads (no PAIRED flag): all counted as 1st fragments + if is_primary { + if flags & BAM_FREAD2 != 0 { + self.last_fragments += 1; + } else { + self.first_fragments += 1; + } + } + // samtools flagstat: paired-read metrics count PRIMARY reads only + // (secondary/supplementary are excluded from paired/read1/read2/properly-paired counts) + if is_paired && is_primary { + self.paired_flagstat += 1; + if flags & BAM_FREAD1 != 0 { + self.read1_flagstat += 1; + } + if flags & BAM_FREAD2 != 0 { + self.read2_flagstat += 1; + } + if flags & BAM_FPROPER_PAIR != 0 { + self.properly_paired += 1; + } + let mate_unmapped = flags & BAM_FMUNMAP != 0; + if is_mapped && !mate_unmapped { + self.both_mapped += 1; + if tid != record.mtid() { + self.mate_diff_chr += 1; + if mapq >= 5 { + self.mate_diff_chr_mapq5 += 1; + } + } + } + if is_mapped && mate_unmapped { + self.singletons += 1; + } + } + + // ================================================================= + // samtools idxstats counters (per-reference) + // ================================================================= + if is_unmapped { + if tid >= 0 { + // Unmapped read placed on a reference (has tid) + self.chrom_counts.entry(tid).or_insert((0, 0)).1 += 1; + } else { + self.unplaced_unmapped += 1; + } + } else if tid >= 0 { + // Mapped read + self.chrom_counts.entry(tid).or_insert((0, 0)).0 += 1; + } + + // ================================================================= + // CHK checksums: computed on ALL reads (including secondary and + // supplementary). Matches samtools stats.c update_checksum() which + // is called before the secondary-read early return. + // ================================================================= + { + let qname = record.qname(); + let name_crc = crc32fast::hash(qname); + self.chk[0] = self.chk[0].wrapping_add(name_crc); + + let seq_len = record.seq_len(); + if seq_len > 0 { + // SAFETY: We access the raw BAM record data to compute CRC32 + // checksums matching samtools' approach. The pointer arithmetic + // replicates htslib's bam_get_seq() macro: + // data + l_qname + (n_cigar << 2) + // The seq_len > 0 guard above ensures sequence data exists. + // The slice length seq_len.div_ceil(2) matches the BAM spec's + // 4-bit encoded sequence format: (seq_len+1)/2 bytes. + let seq_bytes = unsafe { + let inner = record.inner(); + let data = inner.data; + let seq_offset = + inner.core.l_qname as isize + ((inner.core.n_cigar as isize) << 2); + let seq_nbytes = seq_len.div_ceil(2); + std::slice::from_raw_parts(data.offset(seq_offset), seq_nbytes) + }; + let seq_crc = crc32fast::hash(seq_bytes); + self.chk[1] = self.chk[1].wrapping_add(seq_crc); + + let qual = record.qual(); + let qual_crc = crc32fast::hash(qual); + self.chk[2] = self.chk[2].wrapping_add(qual_crc); + } + } + + // Track gc_count from the primary-read per-cycle loop so the GCD + // section below can reuse it without re-scanning the sequence. + let mut primary_gc_count: u64 = 0; + + // ================================================================= + // samtools stats SN counters (primary reads only) + // ================================================================= + if is_primary { + self.primary_count += 1; + let seq_len = record.seq_len() as u64; + let mate_unmapped = flags & BAM_FMUNMAP != 0; + + self.total_len += seq_len; + let is_last_fragment = is_paired && flags & BAM_FREAD2 != 0; + if is_last_fragment { + self.total_last_fragment_len += seq_len; + if seq_len > self.max_last_fragment_len { + self.max_last_fragment_len = seq_len; + } + } else { + self.total_first_fragment_len += seq_len; + if seq_len > self.max_first_fragment_len { + self.max_first_fragment_len = seq_len; + } + } + if seq_len > self.max_len { + self.max_len = seq_len; + } + + // RL/FRL/LRL: read length histograms (all primary reads) + *self.rl_hist.entry(seq_len).or_insert(0) += 1; + if is_last_fragment { + *self.lrl_hist.entry(seq_len).or_insert(0) += 1; + } else { + *self.frl_hist.entry(seq_len).or_insert(0) += 1; + } + + if is_dup { + self.primary_duplicates += 1; + self.bases_duplicated += seq_len; + } + // "reads mapped and paired" for samtools stats: primary, non-QC-fail, + // mapped, paired, mate also mapped + if is_mapped && is_paired && !is_qcfail && !mate_unmapped { + self.reads_mapped_and_paired += 1; + } + if is_mapped { + self.primary_mapped += 1; + self.bases_mapped += seq_len; + + // samtools stats: reads MQ0 counts primary mapped reads with MAPQ=0 + // (upstream stats.c: MQ0 is counted inside collect_orig_read_stats, + // which is only called for IS_ORIGINAL reads = non-secondary, non-supplementary) + if record.mapq() == 0 { + self.reads_mq0 += 1; + } + + // NOTE: bases_mapped_cigar is now computed in the IC/ID CIGAR + // loop below (for all mapped non-secondary reads) to avoid a + // separate full CIGAR traversal here. + + // NM tag (edit distance) + if let Ok(rust_htslib::bam::record::Aux::U8(nm)) = record.aux(b"NM") { + self.mismatches += u64::from(nm); + } else if let Ok(rust_htslib::bam::record::Aux::U16(nm)) = record.aux(b"NM") { + self.mismatches += u64::from(nm); + } else if let Ok(rust_htslib::bam::record::Aux::U32(nm)) = record.aux(b"NM") { + self.mismatches += u64::from(nm); + } else if let Ok(rust_htslib::bam::record::Aux::I8(nm)) = record.aux(b"NM") { + if nm > 0 { + self.mismatches += nm as u64; + } + } else if let Ok(rust_htslib::bam::record::Aux::I16(nm)) = record.aux(b"NM") { + if nm > 0 { + self.mismatches += nm as u64; + } + } else if let Ok(rust_htslib::bam::record::Aux::I32(nm)) = record.aux(b"NM") { + if nm > 0 { + self.mismatches += nm as u64; + } + } + + // Insert size + orientation for paired primary reads where both + // mates are mapped. Matches samtools stats gate: + // IS_PAIRED_AND_MAPPED && IS_ORIGINAL + // if (isize > 0 || tid == mtid) + // Both mates contribute; samtools divides by 2 at output. + // We do the same in write_insert_size() and the SN section. + if is_paired && !mate_unmapped { + let tid = record.tid(); + let mtid = record.mtid(); + let tlen = record.insert_size(); + let abs_tlen = tlen.unsigned_abs(); + + if abs_tlen > 0 || tid == mtid { + let pos = record.pos(); + let mpos = record.mpos(); + + // Compute orientation (only meaningful for same-chromosome) + let pos_fst = mpos - pos; + let is_fst: i64 = if flags & BAM_FREAD1 != 0 { 1 } else { -1 }; + let is_fwd: i64 = if flags & BAM_FREVERSE != 0 { -1 } else { 1 }; + let is_mfwd: i64 = if flags & BAM_FMREVERSE != 0 { -1 } else { 1 }; + + // orientation_idx: 1=inward, 2=outward, 3=other + let orientation_idx = if is_fwd * is_mfwd > 0 { + self.other_orientation += 1; + 3usize + } else if is_fst * pos_fst > 0 { + if is_fst * is_fwd > 0 { + self.inward_pairs += 1; + 1usize + } else { + self.outward_pairs += 1; + 2usize + } + } else if is_fst * pos_fst < 0 { + if is_fst * is_fwd > 0 { + self.outward_pairs += 1; + 2usize + } else { + self.inward_pairs += 1; + 1usize + } + } else { + self.inward_pairs += 1; + 1usize + }; + + if abs_tlen > 0 { + // Cap at MAX_INSERT_SIZE (8000), matching + // samtools stats which accumulates overflow + // into the cap bucket. + let capped = abs_tlen.min(8000); + let entry = self.is_hist.entry(capped).or_insert([0; 4]); + entry[0] += 1; // total + entry[orientation_idx] += 1; + } + } + } + } + + // Average quality for primary non-QC-fail reads. + // Upstream samtools stats computes per-BASE quality average: + // sum of all individual base qualities / total bases. + // (Not a per-read average of averages.) + if !is_qcfail { + let quals = record.qual(); + if !quals.is_empty() { + let base_qual_sum: f64 = quals.iter().map(|&q| f64::from(q)).sum::(); + self.quality_sum += base_qual_sum; + self.quality_count += quals.len() as u64; + } + } + + // ============================================================= + // MAPQ histogram: primary + mapped + !qcfail + !dup + // (matches samtools stats.c:1239 five-flag exclusion) + // ============================================================= + if is_mapped && !is_qcfail && !is_dup { + self.mapq_hist[mapq as usize] += 1; + } + + // ============================================================= + // Per-cycle quality & base composition histograms: + // FFQ/LFQ, FBC/LBC, GCF/GCL, FTC/LTC, FBC_RO/LBC_RO + // + // Upstream samtools stats includes duplicates, unmapped, and + // qcfail reads in these histograms (collect_orig_read_stats + // has no such checks). Only secondary+supplementary are + // excluded (via IS_ORIGINAL), which is already handled by + // the outer is_primary guard. + // ============================================================= + { + let is_reverse = flags & BAM_FREVERSE != 0; + + let seq = record.seq(); + let quals = record.qual(); + let read_len = seq.len(); + + // Determine which arrays to use (first vs last fragment) + // If paired: read2 = last, read1 = first. If SE: all = first. + let (qual_arr, base_arr, base_ro_arr, gc_arr, tc_arr) = if is_last_fragment { + ( + &mut self.lfq, + &mut self.lbc, + &mut self.lbc_ro, + &mut self.gcl, + &mut self.ltc, + ) + } else { + ( + &mut self.ffq, + &mut self.fbc, + &mut self.fbc_ro, + &mut self.gcf, + &mut self.ftc, + ) + }; + + // Ensure per-cycle arrays are large enough + if read_len > qual_arr.len() { + qual_arr.resize(read_len, [0u64; 64]); + } + if read_len > base_arr.len() { + base_arr.resize(read_len, [0u64; 6]); + } + if read_len > base_ro_arr.len() { + base_ro_arr.resize(read_len, [0u64; 6]); + } + if read_len > self.gcc_rc.len() { + self.gcc_rc.resize(read_len, [0u64; 4]); + } + + let mut gc_count: u64 = 0; + + // Pre-built lookup tables for the per-cycle inner loop, + // avoiding branches and match overhead on every base. + // + // BAM 4-bit encoding: A=1, C=2, G=4, T=8, N=15, others=0,3,5..14 + // BASE_IDX[nibble] → 0=A, 1=C, 2=G, 3=T, 4=N, 5=Other + const BASE_IDX: [u8; 16] = [5, 0, 1, 5, 2, 5, 5, 5, 3, 5, 5, 5, 5, 5, 5, 4]; + // RC_IDX[base_idx] → reverse-complement base_idx (A↔T, C↔G) + // Only meaningful for base_idx 0-3 (ACGT). Index 4/5 not used. + const RC_IDX: [u8; 6] = [3, 2, 1, 0, 4, 5]; // A→T, C→G, G→C, T→A + + // Hoist the is_reverse branch outside the inner loop so the + // compiler can version the loop and potentially auto-vectorize + // each variant independently. + if !is_reverse { + for i in 0..read_len { + let q = quals[i] as usize; + qual_arr[i][q.min(63)] += 1; + + let base_idx = BASE_IDX[seq.encoded_base(i) as usize] as usize; + base_arr[i][base_idx] += 1; + base_ro_arr[i][base_idx] += 1; + if base_idx < 4 { + self.gcc_rc[i][base_idx] += 1; + } + if base_idx == 1 || base_idx == 2 { + gc_count += 1; + } + if base_idx < 5 { + tc_arr[base_idx] += 1; + } + } + } else { + for i in 0..read_len { + let ro_cycle = read_len - 1 - i; + let q = quals[i] as usize; + qual_arr[ro_cycle][q.min(63)] += 1; + + let base_idx = BASE_IDX[seq.encoded_base(i) as usize] as usize; + base_arr[i][base_idx] += 1; + base_ro_arr[ro_cycle][base_idx] += 1; + if base_idx < 4 { + self.gcc_rc[ro_cycle][RC_IDX[base_idx] as usize] += 1; + } + if base_idx == 1 || base_idx == 2 { + gc_count += 1; + } + if base_idx < 5 { + tc_arr[base_idx] += 1; + } + } + } + + // Save gc_count for GCD section below (avoids re-scanning the sequence). + primary_gc_count = gc_count; + + // GC content: cumulative step function with ngc=200 bins. + // Matches samtools stats.c:925-941. For a read with gc_count G/C + // bases out of read_len total, increment bins gc_idx_min..gc_idx_max. + let ngc: usize = 200; + if let (Some(gc_idx_min), Some(gc_idx_max)) = ( + (gc_count as usize * (ngc - 1)).checked_div(read_len), + ((gc_count as usize + 1) * (ngc - 1)).checked_div(read_len), + ) { + let gc_idx_max = gc_idx_max.min(ngc - 1); + for item in gc_arr.iter_mut().take(gc_idx_max).skip(gc_idx_min) { + *item += 1; + } + } + } + } // if is_primary + + // ============================================================= + // Indel distribution (ID) and indels per cycle (IC) from CIGAR. + // + // Upstream samtools stats calls count_indels() AFTER the + // secondary-read early return (line 1206-1210) and the + // IS_UNMAPPED return (line 1255), but OUTSIDE IS_ORIGINAL(). + // This means: all mapped, non-secondary reads are included + // (supplementary, duplicate, qcfail all contribute). + // + // IC uses first-fragment/last-fragment read order (not + // forward/reverse strand) and read-oriented cycle indices, + // matching upstream count_indels(). + // ============================================================= + // ============================================================= + // Combined single-CIGAR-pass block for IC/ID (indel distribution), + // bases_mapped_cigar, and COV (coverage ring-buffer pileup). + // + // Both IC/ID and COV apply to the same read set (mapped, + // non-secondary). Merging them into one CIGAR traversal + // eliminates two redundant record.cigar() calls per read. + // + // IC/ID: Upstream samtools stats calls count_indels() outside + // IS_ORIGINAL() — supplementary/dup/qcfail all contribute. + // IC uses first/last-fragment order and read-oriented cycles. + // + // COV: Circular-buffer pileup; buffer flushed up to read start + // before CIGAR walk; M/=/X blocks inserted as ranges. + // Buffer grown to max_read_len * 5 as needed. + // ============================================================= + if is_mapped && !is_secondary { + use rust_htslib::bam::record::Cigar as C; + let is_reverse = flags & BAM_FREVERSE != 0; + let read_len = record.seq_len(); + let tid = record.tid(); + let pos = record.pos(); // 0-based + + // Upstream order: paired ? (read1?FIRST:0)+(read2?LAST:0) : FIRST + let order: u32 = if is_paired { + (if flags & BAM_FREAD1 != 0 { 1 } else { 0 }) + + (if flags & BAM_FREAD2 != 0 { 2 } else { 0 }) + } else { + 1 // unpaired → FIRST + }; + + // COV buffer setup (must happen before CIGAR walk). + // Skip reads with no sequence (upstream samtools early-return). + let do_cov = read_len > 0; + let buf_size = if do_cov { + // Grow buffer to max_read_len * 5 if needed. + // When growing, linearise the circular data just like + // upstream samtools: copy [idx..old_size] then [0..idx] + // into a fresh buffer, and reset idx to 0. + let need = read_len * 5; + if need > self.cov_buf.len() { + let old_size = self.cov_buf.len(); + let mut new_buf = vec![0u32; need]; + let head = old_size - self.cov_buf_idx; + new_buf[..head].copy_from_slice(&self.cov_buf[self.cov_buf_idx..]); + new_buf[head..head + self.cov_buf_idx] + .copy_from_slice(&self.cov_buf[..self.cov_buf_idx]); + self.cov_buf = new_buf; + self.cov_buf_idx = 0; + } + let bs = self.cov_buf.len(); + // Flush entire buffer on chromosome change + if tid != self.cov_buf_tid { + self.flush_cov_buf_all(); + self.cov_buf_tid = tid; + self.cov_buf_pos = pos; + self.cov_buf_idx = 0; + } + // Flush positions from cov_buf_pos up to read start + self.cov_buf_flush_to(pos, bs); + bs + } else { + 0 + }; + + // Single CIGAR traversal serving IC/ID + bases_mapped_cigar + COV + let cigar = record.cigar(); + let mut icycle: usize = 0; + let mut cigar_mapped: u64 = 0; + let mut ref_pos = pos; + + for op in cigar.iter() { + match op { + C::Ins(n) => { + let ncig = *n as usize; + let len = *n as u64; + cigar_mapped += len; // I counts toward bases_mapped_cigar + + // ID: indel size distribution + let id_entry = self.id_hist.entry(len).or_insert([0; 2]); + id_entry[0] += 1; // insertions + + // IC: indels per cycle (read-oriented index) + let idx = if is_reverse { + read_len.saturating_sub(icycle + ncig) + } else { + icycle + }; + if idx >= self.ic.len() { + self.ic.resize(idx + 1, [0u64; 4]); + } + if order == 1 { + self.ic[idx][0] += 1; // ins_1st + } + if order == 2 { + self.ic[idx][1] += 1; // ins_2nd + } + + icycle += ncig; // I advances query cycle; ref unchanged + // COV: I consumes no reference positions + } + C::Del(n) => { + let len = *n as u64; + // ID: indel size distribution + let id_entry = self.id_hist.entry(len).or_insert([0; 2]); + id_entry[1] += 1; // deletions + + // IC: indels per cycle (read-oriented index) + let idx = if is_reverse { + if icycle == 0 { + // Discard meaningless deletions at cycle 0 + // (upstream: "if (idx<0) continue;") + ref_pos += *n as i64; // still advance ref for COV + continue; + } + read_len.saturating_sub(icycle + 1) + } else { + if icycle == 0 { + ref_pos += *n as i64; + continue; + } + icycle - 1 + }; + if idx >= self.ic.len() { + self.ic.resize(idx + 1, [0u64; 4]); + } + if order == 1 { + self.ic[idx][2] += 1; // del_1st + } + if order == 2 { + self.ic[idx][3] += 1; // del_2nd + } + // D does NOT advance query cycle; does advance ref + ref_pos += *n as i64; + } + C::Match(n) | C::Equal(n) | C::Diff(n) => { + let len = *n as u64; + cigar_mapped += len; // M/=/X count toward bases_mapped_cigar + icycle += *n as usize; + // COV: M/=/X consumes reference positions + if do_cov { + let end = ref_pos + *n as i64; + self.cov_buf_insert(ref_pos, end, buf_size); + ref_pos = end; + } else { + ref_pos += *n as i64; + } + } + C::RefSkip(n) => { + ref_pos += *n as i64; // N advances ref (COV skips it) + } + C::SoftClip(n) => { + icycle += *n as usize; // S advances query cycle + // COV: S consumes no reference positions + } + C::HardClip(_) | C::Pad(_) => {} + } + } + self.bases_mapped_cigar += cigar_mapped; + } // if is_mapped && !is_secondary (IC/ID + COV combined) + + // ============================================================= + // GCD: GC-depth accumulation (no-reference path). + // + // Matches upstream samtools stats without --ref-seq: bins of + // GCD_BIN_SIZE bp, depth incremented for each read, GC fraction + // accumulated from the read's sequence. + // + // Included reads: mapped, non-secondary (same as COV). + // + // NOTE: gc_count_for_gcd is set from the primary-read per-cycle + // loop above (when is_primary is true), or computed here only for + // non-primary mapped reads, avoiding a redundant full sequence scan. + // ============================================================= + if is_mapped && !is_secondary { + let tid = record.tid(); + let pos = record.pos(); + let seq_len = record.seq_len(); + + if seq_len > 0 { + // Start a new bin on: first read, chromosome change, or + // read beyond current bin boundary. + let new_bin = self.gcd_pos < 0 + || tid != self.gcd_tid + || pos - self.gcd_pos > GCD_BIN_SIZE as i64; + + if new_bin { + self.gcd_bins.push(GcDepthBin { gc: 0.0, depth: 0 }); + self.gcd_pos = pos; + self.gcd_tid = tid; + } + + // Increment depth and accumulate GC fraction from read seq. + if let Some(bin) = self.gcd_bins.last_mut() { + bin.depth += 1; + // For primary reads, gc_count was already computed in the + // per-cycle base loop above. For non-primary mapped reads + // (supplementary, etc.) compute it here from the sequence. + let gc_count: u32 = if is_primary { + primary_gc_count as u32 + } else { + let seq = record.seq(); + let mut count: u32 = 0; + for i in 0..seq_len { + let base = seq.encoded_base(i); + if base == 2 || base == 4 { + count += 1; + } + } + count + }; + bin.gc += gc_count as f32 / seq_len as f32; + } + } + } // if is_mapped && !is_secondary (GCD) + + // ================================================================= + // RSeQC bam_stat cascade (original logic, with early returns) + // ================================================================= + + // 1. QC-failed + if is_qcfail { + self.qc_failed += 1; + return; + } + + // 2. Duplicate + if is_dup { + self.duplicates += 1; + return; + } + + // 3. Secondary (non-primary) — NOT supplementary + if is_secondary { + self.non_primary += 1; + return; + } + + // 4. Unmapped + if is_unmapped { + self.unmapped += 1; + return; + } + + // 5. MAPQ classification + if mapq < mapq_cut { + self.non_unique += 1; + return; + } + + // Uniquely mapped + self.unique += 1; + + if flags & BAM_FREAD1 != 0 { + self.read_1 += 1; + } + if flags & BAM_FREAD2 != 0 { + self.read_2 += 1; + } + if flags & BAM_FREVERSE != 0 { + self.reverse += 1; + } else { + self.forward += 1; + } + + // Splice detection: CIGAR N operation + let has_splice = record + .cigar() + .iter() + .any(|op| matches!(op, rust_htslib::bam::record::Cigar::RefSkip(_))); + if has_splice { + self.splice += 1; + } else { + self.non_splice += 1; + } + + // Proper pair analysis + if is_paired && flags & BAM_FPROPER_PAIR != 0 { + self.proper_pairs += 1; + if tid != record.mtid() { + self.proper_pair_diff_chrom += 1; + } + } + } + + /// Flush all remaining positions in the coverage round buffer into cov_hist. + /// Must be called after processing all reads (or when switching chromosomes). + /// Flush the circular buffer from `cov_buf_pos` up to (but not including) `pos`. + /// Each slot's depth is recorded in `cov_hist` and the slot is zeroed. + /// Matches upstream `round_buffer_flush` logic from samtools stats.c. + fn cov_buf_flush_to(&mut self, pos: i64, buf_size: usize) { + if pos - self.cov_buf_pos >= buf_size as i64 { + // Gap exceeds buffer size. Match upstream samtools exactly: + // flush `size - 1` positions (from cov_buf_pos to + // cov_buf_pos + size - 2), leaving the LAST slot untouched. + // Then advance idx by `size - 1` and jump pos. + // + // Upstream (stats.c round_buffer_flush lines 334-366): + // pos = rbuf.pos + size - 1; // cap at last slot + // ito = lidx2ridx(start, size, rbuf.pos, pos-1); + // // flush from start to ito (size-1 slots) + // rbuf.start = lidx2ridx(start, size, rbuf.pos, pos); + // rbuf.pos = new_pos; + let flush_count = buf_size - 1; // flush all but the last slot + for _ in 0..flush_count { + let depth = self.cov_buf[self.cov_buf_idx]; + if depth > 0 { + *self.cov_hist.entry(depth).or_insert(0) += 1; + self.cov_buf[self.cov_buf_idx] = 0; + } + self.cov_buf_idx += 1; + if self.cov_buf_idx >= buf_size { + self.cov_buf_idx = 0; + } + } + // idx now points to the ONE unflushed slot (the last position + // in the old window). Jump pos to the new read position. + self.cov_buf_pos = pos; + } else { + // Normal case: flush slot by slot. + while self.cov_buf_pos < pos { + let depth = self.cov_buf[self.cov_buf_idx]; + if depth > 0 { + *self.cov_hist.entry(depth).or_insert(0) += 1; + self.cov_buf[self.cov_buf_idx] = 0; + } + self.cov_buf_idx += 1; + if self.cov_buf_idx >= buf_size { + self.cov_buf_idx = 0; + } + self.cov_buf_pos += 1; + } + } + } + + /// Insert a contiguous reference range `[from, to)` into the circular buffer, + /// incrementing depth for each position. The range must fit within `buf_size`. + fn cov_buf_insert(&mut self, from: i64, to: i64, buf_size: usize) { + for ref_pos in from..to { + // Map ref_pos to buffer index: offset from cov_buf_idx by (ref_pos - cov_buf_pos) + let offset = (ref_pos - self.cov_buf_pos) as usize; + let idx = (self.cov_buf_idx + offset) % buf_size; + self.cov_buf[idx] += 1; + } + } + + /// Flush the entire circular buffer and reset tracking state. + pub fn flush_cov_buf_all(&mut self) { + for slot in self.cov_buf.iter_mut() { + if *slot > 0 { + *self.cov_hist.entry(*slot).or_insert(0) += 1; + *slot = 0; + } + } + self.cov_buf_idx = 0; + self.cov_buf_pos = 0; + self.cov_buf_tid = -1; + } + + /// Merge another accumulator into this one. + pub fn merge(&mut self, mut other: BamStatAccum) { + // Flush any remaining positions in the other's round buffer into its + // cov_hist before merging. Without this, positions still in the + // round buffer would be silently lost during parallel merges. + other.flush_cov_buf_all(); + + // RSeQC bam_stat fields + self.total_records += other.total_records; + self.qc_failed += other.qc_failed; + self.duplicates += other.duplicates; + self.non_primary += other.non_primary; + self.unmapped += other.unmapped; + self.non_unique += other.non_unique; + self.unique += other.unique; + self.read_1 += other.read_1; + self.read_2 += other.read_2; + self.forward += other.forward; + self.reverse += other.reverse; + self.splice += other.splice; + self.non_splice += other.non_splice; + self.proper_pairs += other.proper_pairs; + self.proper_pair_diff_chrom += other.proper_pair_diff_chrom; + + // samtools flagstat fields + self.secondary += other.secondary; + self.supplementary += other.supplementary; + self.mapped += other.mapped; + self.paired_flagstat += other.paired_flagstat; + self.read1_flagstat += other.read1_flagstat; + self.read2_flagstat += other.read2_flagstat; + self.first_fragments += other.first_fragments; + self.last_fragments += other.last_fragments; + self.properly_paired += other.properly_paired; + self.both_mapped += other.both_mapped; + self.singletons += other.singletons; + self.mate_diff_chr += other.mate_diff_chr; + self.mate_diff_chr_mapq5 += other.mate_diff_chr_mapq5; + + // samtools idxstats fields + for (tid, (m, u)) in other.chrom_counts { + let entry = self.chrom_counts.entry(tid).or_insert((0, 0)); + entry.0 += m; + entry.1 += u; + } + self.unplaced_unmapped += other.unplaced_unmapped; + + // samtools stats SN fields + self.total_len += other.total_len; + self.total_first_fragment_len += other.total_first_fragment_len; + self.total_last_fragment_len += other.total_last_fragment_len; + self.bases_mapped += other.bases_mapped; + self.bases_mapped_cigar += other.bases_mapped_cigar; + self.bases_duplicated += other.bases_duplicated; + if other.max_len > self.max_len { + self.max_len = other.max_len; + } + if other.max_first_fragment_len > self.max_first_fragment_len { + self.max_first_fragment_len = other.max_first_fragment_len; + } + if other.max_last_fragment_len > self.max_last_fragment_len { + self.max_last_fragment_len = other.max_last_fragment_len; + } + self.quality_sum += other.quality_sum; + self.quality_count += other.quality_count; + self.mismatches += other.mismatches; + for (isize_val, counts) in other.is_hist { + let entry = self.is_hist.entry(isize_val).or_insert([0; 4]); + for i in 0..4 { + entry[i] += counts[i]; + } + } + self.inward_pairs += other.inward_pairs; + self.outward_pairs += other.outward_pairs; + self.other_orientation += other.other_orientation; + self.primary_count += other.primary_count; + self.primary_mapped += other.primary_mapped; + self.primary_duplicates += other.primary_duplicates; + self.reads_mq0 += other.reads_mq0; + self.reads_mapped_and_paired += other.reads_mapped_and_paired; + + // Histogram/distribution fields + for (len, count) in other.rl_hist { + *self.rl_hist.entry(len).or_insert(0) += count; + } + for (len, count) in other.frl_hist { + *self.frl_hist.entry(len).or_insert(0) += count; + } + for (len, count) in other.lrl_hist { + *self.lrl_hist.entry(len).or_insert(0) += count; + } + for i in 0..256 { + self.mapq_hist[i] += other.mapq_hist[i]; + } + + // Per-cycle quality arrays (FFQ/LFQ) + merge_vec_arrays(&mut self.ffq, other.ffq); + merge_vec_arrays(&mut self.lfq, other.lfq); + + // GC content distributions (200 bins) + for i in 0..200 { + self.gcf[i] += other.gcf[i]; + self.gcl[i] += other.gcl[i]; + } + + // Per-cycle base composition (FBC/LBC and read-oriented) + merge_vec_arrays(&mut self.fbc, other.fbc); + merge_vec_arrays(&mut self.lbc, other.lbc); + merge_vec_arrays(&mut self.fbc_ro, other.fbc_ro); + merge_vec_arrays(&mut self.lbc_ro, other.lbc_ro); + merge_vec_arrays(&mut self.gcc_rc, other.gcc_rc); + + // Total base counters + for i in 0..5 { + self.ftc[i] += other.ftc[i]; + self.ltc[i] += other.ltc[i]; + } + + // Indel distribution + for (len, counts) in other.id_hist { + let entry = self.id_hist.entry(len).or_insert([0; 2]); + entry[0] += counts[0]; + entry[1] += counts[1]; + } + + // Indels per cycle + merge_vec_arrays(&mut self.ic, other.ic); + + // CHK checksums (wrapping u32 addition) + for i in 0..3 { + self.chk[i] = self.chk[i].wrapping_add(other.chk[i]); + } + + // COV histogram (additive merge) + for (depth, count) in other.cov_hist { + *self.cov_hist.entry(depth).or_insert(0) += count; + } + + // GCD bins (concatenate — bins from different chromosome workers + // are independent and will be sorted during output). + self.gcd_bins.append(&mut other.gcd_bins); + } +} + +impl BamStatAccum { + /// Convert accumulated counters into a `BamStatResult` for output. + pub fn into_result(mut self) -> BamStatResult { + // Flush remaining positions in the coverage round buffer + self.flush_cov_buf_all(); + BamStatResult { + // RSeQC bam_stat fields + total_records: self.total_records, + qc_failed: self.qc_failed, + duplicates: self.duplicates, + non_primary: self.non_primary, + unmapped: self.unmapped, + non_unique: self.non_unique, + unique: self.unique, + read_1: self.read_1, + read_2: self.read_2, + forward: self.forward, + reverse: self.reverse, + splice: self.splice, + non_splice: self.non_splice, + proper_pairs: self.proper_pairs, + proper_pair_diff_chrom: self.proper_pair_diff_chrom, + // samtools flagstat fields + secondary: self.secondary, + supplementary: self.supplementary, + mapped: self.mapped, + paired_flagstat: self.paired_flagstat, + read1_flagstat: self.read1_flagstat, + read2_flagstat: self.read2_flagstat, + first_fragments: self.first_fragments, + last_fragments: self.last_fragments, + properly_paired: self.properly_paired, + both_mapped: self.both_mapped, + singletons: self.singletons, + mate_diff_chr: self.mate_diff_chr, + mate_diff_chr_mapq5: self.mate_diff_chr_mapq5, + // samtools idxstats fields + chrom_counts: self.chrom_counts, + unplaced_unmapped: self.unplaced_unmapped, + // samtools stats SN fields + total_len: self.total_len, + total_first_fragment_len: self.total_first_fragment_len, + total_last_fragment_len: self.total_last_fragment_len, + bases_mapped: self.bases_mapped, + bases_mapped_cigar: self.bases_mapped_cigar, + bases_duplicated: self.bases_duplicated, + max_len: self.max_len, + max_first_fragment_len: self.max_first_fragment_len, + max_last_fragment_len: self.max_last_fragment_len, + quality_sum: self.quality_sum, + quality_count: self.quality_count, + mismatches: self.mismatches, + is_hist: self.is_hist, + inward_pairs: self.inward_pairs, + outward_pairs: self.outward_pairs, + other_orientation: self.other_orientation, + primary_count: self.primary_count, + primary_mapped: self.primary_mapped, + primary_duplicates: self.primary_duplicates, + reads_mq0: self.reads_mq0, + reads_mapped_and_paired: self.reads_mapped_and_paired, + // Histogram/distribution fields + rl_hist: self.rl_hist, + frl_hist: self.frl_hist, + lrl_hist: self.lrl_hist, + mapq_hist: self.mapq_hist, + ffq: self.ffq, + lfq: self.lfq, + gcf: self.gcf, + gcl: self.gcl, + fbc: self.fbc, + lbc: self.lbc, + fbc_ro: self.fbc_ro, + lbc_ro: self.lbc_ro, + gcc_rc: self.gcc_rc, + ftc: self.ftc, + ltc: self.ltc, + id_hist: self.id_hist, + ic: self.ic, + chk: self.chk, + cov_hist: self.cov_hist, + gcd_bins: self.gcd_bins, + } + } +} diff --git a/src/rna/cpp_rng.rs b/src/common/cpp_rng.rs similarity index 100% rename from src/rna/cpp_rng.rs rename to src/common/cpp_rng.rs diff --git a/src/common/mod.rs b/src/common/mod.rs new file mode 100644 index 00000000..31c3a7d3 --- /dev/null +++ b/src/common/mod.rs @@ -0,0 +1,13 @@ +//! Analysis modules shared between the `rna` and `dna` pipelines. +//! +//! Nothing in this module is specific to a library preparation or an assay: +//! BAM flag helpers, the C++ RNG shim used for preseq bootstrap +//! reproducibility, the preseq `lc_extrap` implementation, read-level +//! alignment statistics, and the samtools-compatible output writers. + +pub mod bam_flags; +pub mod bam_stat; +pub mod bam_stat_accum; +pub mod cpp_rng; +pub mod preseq; +pub mod samtools; diff --git a/src/rna/preseq.rs b/src/common/preseq.rs similarity index 100% rename from src/rna/preseq.rs rename to src/common/preseq.rs diff --git a/src/rna/rseqc/flagstat.rs b/src/common/samtools/flagstat.rs similarity index 99% rename from src/rna/rseqc/flagstat.rs rename to src/common/samtools/flagstat.rs index cb1e370d..b611ae03 100644 --- a/src/rna/rseqc/flagstat.rs +++ b/src/common/samtools/flagstat.rs @@ -8,7 +8,7 @@ use std::path::Path; use anyhow::{Context, Result}; use log::debug; -use super::bam_stat::BamStatResult; +use crate::common::bam_stat::BamStatResult; // ============================================================================ // Output formatting diff --git a/src/rna/rseqc/idxstats.rs b/src/common/samtools/idxstats.rs similarity index 98% rename from src/rna/rseqc/idxstats.rs rename to src/common/samtools/idxstats.rs index 93c018fe..61f05d4f 100644 --- a/src/rna/rseqc/idxstats.rs +++ b/src/common/samtools/idxstats.rs @@ -8,7 +8,7 @@ use std::path::Path; use anyhow::{Context, Result}; use log::debug; -use super::bam_stat::BamStatResult; +use crate::common::bam_stat::BamStatResult; // ============================================================================ // Output formatting diff --git a/src/common/samtools/mod.rs b/src/common/samtools/mod.rs new file mode 100644 index 00000000..f9b1d860 --- /dev/null +++ b/src/common/samtools/mod.rs @@ -0,0 +1,10 @@ +//! samtools-compatible output writers. +//! +//! Reproduce the exact output formats of `samtools stats`, `samtools flagstat` +//! and `samtools idxstats` from the counters gathered in +//! [`crate::common::bam_stat::BamStatResult`], so that MultiQC and +//! `plot-bamstats` parse RustQC output as if samtools had produced it. + +pub mod flagstat; +pub mod idxstats; +pub mod stats; diff --git a/src/rna/rseqc/stats.rs b/src/common/samtools/stats.rs similarity index 99% rename from src/rna/rseqc/stats.rs rename to src/common/samtools/stats.rs index 20abf8c0..32f42067 100644 --- a/src/rna/rseqc/stats.rs +++ b/src/common/samtools/stats.rs @@ -10,7 +10,7 @@ use std::path::Path; use anyhow::{Context, Result}; use log::debug; -use super::bam_stat::{BamStatResult, GcDepthBin}; +use crate::common::bam_stat::{BamStatResult, GcDepthBin}; // ============================================================================ // Output formatting @@ -62,7 +62,7 @@ pub fn write_stats(result: &BamStatResult, output_path: &Path) -> Result<()> { writeln!(out, "# This file was produced by samtools stats and RustQC")?; writeln!( out, - "# The command line was: rustqc rna (samtools stats compatible output)" + "# The command line was: rustqc (samtools stats compatible output)" )?; // Derived values diff --git a/src/config.rs b/src/config.rs index 4952a1fd..0045116d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -30,6 +30,10 @@ pub struct Config { /// RNA-Seq QC configuration (matches the `rna` subcommand). #[serde(default)] pub rna: RnaConfig, + + /// DNA QC configuration (matches the `dna` subcommand). + #[serde(default)] + pub dna: DnaConfig, } /// RNA-Seq QC configuration. @@ -908,6 +912,314 @@ impl RnaConfig { } } +// =================================================================== +// DNA QC configuration +// =================================================================== + +/// DNA QC configuration. +/// +/// Contains all settings for the `rustqc dna` subcommand. Tool-specific +/// settings are nested under their tool name (e.g. `mosdepth:`, `samtools:`, +/// `preseq:`). +/// +/// The shared settings are declared here rather than inherited from the root +/// [`Config`], mirroring [`RnaConfig`], so the two pipelines can be configured +/// independently in one file. +/// +/// Example: +/// ```yaml +/// dna: +/// flat_output: true +/// mosdepth: +/// window_size: 500 +/// thresholds: [1, 10, 30] +/// ``` +#[derive(Debug, Deserialize, Default)] +#[serde(default)] +pub struct DnaConfig { + /// Prefix to prepend to alignment file chromosome names before matching + /// interval-file names (for example a targets BED using `chr1` against an + /// alignment using `1`). + #[serde(default)] + pub chromosome_prefix: Option, + + /// Chromosome name mapping from interval-file names to alignment file names. + /// + /// Applied after `chromosome_prefix`, so explicit mappings override it. + #[serde(default)] + pub chromosome_mapping: HashMap, + + /// Override the sample name used in output filenames. + /// + /// The CLI `--sample-name` flag takes precedence over this setting. + #[serde(default)] + pub sample_name: Option, + + /// Write all output files to a flat directory (no subdirectories). + /// + /// By default (`false`), outputs are organised by tool: `mosdepth/`, + /// `samtools/`, `preseq/`. The CLI `--flat-output` flag enables flat + /// output regardless of this setting (either source being `true` produces + /// flat output). + #[serde(default)] + pub flat_output: bool, + + /// mosdepth-compatible depth of coverage configuration. + #[serde(default)] + pub mosdepth: MosdepthConfig, + + /// samtools-compatible output configuration (stats, flagstat, idxstats). + #[serde(default)] + pub samtools: SamtoolsConfig, + + /// Picard CollectWgsMetrics configuration. + #[serde(default)] + pub wgs_metrics: WgsMetricsConfig, + + /// Picard CollectInsertSizeMetrics configuration. + #[serde(default)] + pub insert_size: InsertSizeConfig, + + /// Picard CollectGcBiasMetrics configuration. + #[serde(default)] + pub gc_bias: GcBiasConfig, + + /// Picard CollectHsMetrics configuration, used in targeted mode. + #[serde(default)] + pub hs_metrics: HsMetricsConfig, + + /// Qualimap bamqc configuration. + #[serde(default)] + pub qualimap: BamqcConfig, + + /// preseq lc_extrap library complexity extrapolation configuration. + /// + /// Reuses the same type as the `rna` pipeline; the implementation is shared. + #[serde(default)] + pub preseq: PreseqConfig, +} + +/// Configuration for the Picard-compatible whole-genome coverage metrics. +/// +/// Requires a reference FASTA: `GENOME_TERRITORY` counts the reference's +/// non-N bases, so without one the analysis is skipped. +/// +/// Example: +/// ```yaml +/// wgs_metrics: +/// enabled: true +/// coverage_cap: 250 +/// min_base_quality: 20 +/// min_mapping_quality: 20 +/// ``` +#[derive(Debug, Deserialize)] +#[serde(default)] +pub struct WgsMetricsConfig { + /// Whether to compute whole-genome coverage metrics. Defaults to true. + pub enabled: bool, + /// Depth beyond this is reported as excluded rather than counted. + pub coverage_cap: u32, + /// Bases below this quality are excluded. + pub min_base_quality: u8, + /// Reads below this mapping quality are excluded. + pub min_mapping_quality: u8, +} + +impl Default for WgsMetricsConfig { + fn default() -> Self { + Self { + enabled: true, + coverage_cap: 250, + min_base_quality: 20, + min_mapping_quality: 20, + } + } +} + +/// Configuration for the Qualimap-compatible bamqc report. +/// +/// Named apart from the `rna` pipeline's [`QualimapConfig`], which configures +/// a different Qualimap analysis entirely: gene body coverage rather than +/// bamqc. +/// +/// Example: +/// ```yaml +/// qualimap: +/// enabled: true +/// num_windows: 400 +/// ``` +#[derive(Debug, Deserialize)] +#[serde(default)] +pub struct BamqcConfig { + /// Whether to produce the bamqc outputs. Defaults to true. + pub enabled: bool, + /// Target number of windows the reference is split into. The realised + /// count is usually a little lower, because the window width is rounded up + /// first. + pub num_windows: usize, +} + +impl Default for BamqcConfig { + fn default() -> Self { + Self { + enabled: true, + num_windows: 400, + } + } +} + +/// Configuration for the Picard-compatible GC bias metrics. +/// +/// Requires a reference FASTA: the analysis bins reference windows by GC. +/// +/// Example: +/// ```yaml +/// gc_bias: +/// enabled: true +/// window_size: 100 +/// ``` +#[derive(Debug, Deserialize)] +#[serde(default)] +pub struct GcBiasConfig { + /// Whether to compute GC bias metrics. Defaults to true. + pub enabled: bool, + /// Width of the sliding reference windows GC is computed over. + pub window_size: usize, +} + +impl Default for GcBiasConfig { + fn default() -> Self { + Self { + enabled: true, + window_size: 100, + } + } +} + +/// Configuration for the Picard-compatible targeted sequencing metrics. +/// +/// Only takes effect when `--targets` is given. +/// +/// Example: +/// ```yaml +/// hs_metrics: +/// enabled: true +/// min_base_quality: 20 +/// min_mapping_quality: 20 +/// ``` +#[derive(Debug, Deserialize)] +#[serde(default)] +pub struct HsMetricsConfig { + /// Whether to compute targeted metrics. Defaults to true. + pub enabled: bool, + /// Bases below this quality are excluded. + pub min_base_quality: u8, + /// Reads below this mapping quality are excluded. + pub min_mapping_quality: u8, +} + +impl Default for HsMetricsConfig { + fn default() -> Self { + Self { + enabled: true, + min_base_quality: 20, + min_mapping_quality: 20, + } + } +} + +/// Configuration for the Picard-compatible insert size metrics. +/// +/// Example: +/// ```yaml +/// insert_size: +/// enabled: true +/// deviations: 10.0 +/// ``` +#[derive(Debug, Deserialize)] +#[serde(default)] +pub struct InsertSizeConfig { + /// Whether to compute insert size metrics. Defaults to true. + pub enabled: bool, + /// Median absolute deviations either side of the median that survive + /// trimming before the mean and standard deviation are computed. + pub deviations: f64, +} + +impl Default for InsertSizeConfig { + fn default() -> Self { + Self { + enabled: true, + deviations: 10.0, + } + } +} + +/// Configuration for the mosdepth-compatible depth of coverage analysis. +/// +/// Example: +/// ```yaml +/// mosdepth: +/// enabled: true +/// window_size: 500 +/// thresholds: [1, 10, 30] +/// skip_per_base: false +/// ``` +#[derive(Debug, Deserialize)] +#[serde(default)] +pub struct MosdepthConfig { + /// Whether to compute depth of coverage. Defaults to true. + pub enabled: bool, + + /// Fixed-width window size for the per-window depth output. + /// + /// `None` (the default) means no `regions` output is written, matching + /// mosdepth run without `--by`. + pub window_size: Option, + + /// Coverage thresholds reported in the thresholds output and used for the + /// percent-of-bases-at-least-NX summary figures. + pub thresholds: Vec, + + /// Skip the per-base depth output, by far the largest file produced. + pub skip_per_base: bool, +} + +impl Default for MosdepthConfig { + fn default() -> Self { + Self { + enabled: true, + window_size: None, + thresholds: vec![1, 5, 10, 15, 20, 30, 50], + skip_per_base: false, + } + } +} + +/// Configuration for the samtools-compatible outputs of the DNA pipeline. +/// +/// A single toggle covers `stats`, `flagstat` and `idxstats` because all three +/// are produced from one accumulator in the same pass; disabling them +/// individually would save no work. +/// +/// Example: +/// ```yaml +/// samtools: +/// enabled: true +/// ``` +#[derive(Debug, Deserialize)] +#[serde(default)] +pub struct SamtoolsConfig { + /// Whether to write the samtools-compatible outputs. Defaults to true. + pub enabled: bool, +} + +impl Default for SamtoolsConfig { + fn default() -> Self { + Self { enabled: true } + } +} + #[cfg(test)] mod tests { use super::*; @@ -1294,4 +1606,44 @@ preseq: std::env::set_var("RUSTQC_CONFIG", val); } } + + #[test] + fn test_dna_config_defaults() { + let config = Config::default(); + assert!(config.dna.mosdepth.enabled); + assert!(config.dna.samtools.enabled); + assert!(config.dna.preseq.enabled); + assert!(!config.dna.flat_output); + assert_eq!( + config.dna.mosdepth.thresholds, + vec![1, 5, 10, 15, 20, 30, 50] + ); + assert_eq!(config.dna.mosdepth.window_size, None); + } + + #[test] + fn test_dna_config_from_yaml() { + let yaml = "dna:\n flat_output: true\n mosdepth:\n window_size: 500\n thresholds: [1, 30]\n preseq:\n enabled: false\n"; + let config: Config = serde_yaml_ng::from_str(yaml).unwrap(); + assert!(config.dna.flat_output); + assert_eq!(config.dna.mosdepth.window_size, Some(500)); + assert_eq!(config.dna.mosdepth.thresholds, vec![1, 30]); + assert!(!config.dna.preseq.enabled); + // A dna-only config leaves the rna side untouched. + assert!(config.rna.preseq.enabled); + } + + #[test] + fn test_dna_config_deep_merge() { + let mut merged: Value = serde_yaml_ng::from_str( + "dna:\n mosdepth:\n window_size: 100\n thresholds: [1]\n", + ) + .unwrap(); + let overlay: Value = + serde_yaml_ng::from_str("dna:\n mosdepth:\n window_size: 500\n").unwrap(); + deep_merge(&mut merged, overlay); + let config: Config = serde_yaml_ng::from_value(merged).unwrap(); + assert_eq!(config.dna.mosdepth.window_size, Some(500)); + assert_eq!(config.dna.mosdepth.thresholds, vec![1]); + } } diff --git a/src/dna/depth.rs b/src/dna/depth.rs new file mode 100644 index 00000000..ce39f2c2 --- /dev/null +++ b/src/dna/depth.rs @@ -0,0 +1,444 @@ +//! Per-contig depth of coverage accumulation. +//! +//! One [`DepthAccum`] covers one contig. Aligned blocks are recorded as +//! increments in a delta array the length of the contig, and a prefix sum at +//! the end turns that into per-base depth in a single linear pass. +//! +//! # Upstream semantics +//! +//! The filters and the CIGAR walk reproduce mosdepth 0.3.14 run without +//! `--fast-mode`, whose help text describes that flag as "dont look at +//! internal cigar operations or correct mate overlaps". Default mode +//! therefore does both, and so does this module: +//! +//! - records carrying any bit of [`MOSDEPTH_DEFAULT_EXCLUDE`] are skipped +//! (mosdepth's `-F` default of 1796); +//! - records with `MAPQ` below the cutoff are skipped (mosdepth's `-Q`, +//! default 0); +//! - `M`, `=` and `X` cover the reference, `D` and `N` advance without +//! covering, and `I`, `S`, `H` and `P` do not advance at all; +//! - a base covered by both mates of one pair counts once. +//! +//! That last rule is not a detail. On the project's test dataset, correcting +//! mate overlaps takes total covered bases from 469875 down to 247878, which +//! is exactly the gap between mosdepth's `--fast-mode` and its default. + +use std::collections::{BTreeMap, HashMap}; + +use rust_htslib::bam; +use rust_htslib::bam::record::Cigar; + +use crate::common::bam_flags::*; + +/// Bit mask matching mosdepth's `-F` default: `UNMAP | SECONDARY | QCFAIL | DUP`. +pub const MOSDEPTH_DEFAULT_EXCLUDE: u16 = BAM_FUNMAP | BAM_FSECONDARY | BAM_FQCFAIL | BAM_FDUP; + +/// Accumulates per-base depth for a single contig. +#[derive(Debug)] +pub struct DepthAccum { + /// Delta array of length `contig_len + 1`; a `+1` at a block start and a + /// `-1` one past its end, summed into depth by [`DepthAccum::into_depths`]. + deltas: Vec, + /// Contig length in bases. + len: usize, + /// Records with `MAPQ` strictly below this value are ignored. + mapq_cut: u8, + /// Records carrying any of these flag bits are ignored. + exclude_flags: u16, + /// Aligned blocks already counted for a pair whose second mate is still + /// ahead, keyed by read name. + pending: HashMap, Vec<(usize, usize)>>, + /// Read names indexed by the position their outstanding mate is expected + /// at, so stale entries can be evicted without scanning `pending`. + pending_by_pos: BTreeMap>>, +} + +impl DepthAccum { + /// Allocate for one contig of `length` bases. + pub fn new(length: u64, mapq_cut: u8, exclude_flags: u16) -> Self { + let len = length as usize; + Self { + deltas: vec![0i32; len + 1], + len, + mapq_cut, + exclude_flags, + pending: HashMap::new(), + pending_by_pos: BTreeMap::new(), + } + } + + /// Add one record's aligned blocks. Records failing the filters are ignored. + /// + /// Records are expected in coordinate order, which is what the per-contig + /// worker feeds. That ordering is what makes the pending-mate bookkeeping + /// bounded: once the read position passes the position an outstanding mate + /// was announced at, that entry can never be claimed and is dropped. + pub fn process_read(&mut self, record: &bam::Record) { + if !self.passes_filters(record) { + return; + } + let pos = record.pos(); + self.evict_unclaimable(pos); + + let blocks = Self::aligned_blocks(record, self.len); + if blocks.is_empty() { + return; + } + + // A record can only overlap its own mate, and only on the same contig. + let paired_here = record.flags() & BAM_FPAIRED != 0 + && record.flags() & BAM_FMUNMAP == 0 + && record.mtid() == record.tid(); + + if paired_here { + if let Some(mate_blocks) = self.pending.remove(record.qname()) { + // Second mate of the pair: shared bases are already counted. + self.add_blocks_excluding(&blocks, &mate_blocks); + return; + } + if record.mpos() >= pos { + let qname = record.qname().to_vec(); + self.pending.insert(qname.clone(), blocks.clone()); + self.pending_by_pos + .entry(record.mpos()) + .or_default() + .push(qname); + } + } + + for &(start, end) in &blocks { + self.add_block_usize(start, end); + } + } + + /// Number of pairs still waiting for their second mate. Test-only: the + /// bookkeeping is an implementation detail, but an unbounded map would be + /// a memory leak on a real chromosome, so it is worth asserting on. + #[cfg(test)] + pub fn pending_mates_len(&self) -> usize { + self.pending.len() + } + + /// Drop pending entries whose outstanding mate lies behind `pos` and can + /// therefore never arrive (it was filtered out, or the file is truncated). + fn evict_unclaimable(&mut self, pos: i64) { + while let Some((&mate_pos, _)) = self.pending_by_pos.iter().next() { + if mate_pos >= pos { + break; + } + // Safe: the key came from `iter().next()` on this same map. + let qnames = self.pending_by_pos.remove(&mate_pos).unwrap_or_default(); + for qname in qnames { + self.pending.remove(&qname); + } + } + } + + /// The record's reference-covering blocks as half-open `[start, end)` + /// intervals, clamped to `len`. + fn aligned_blocks(record: &bam::Record, len: usize) -> Vec<(usize, usize)> { + let mut blocks = Vec::new(); + let mut pos = record.pos(); + for op in record.cigar().iter() { + match op { + // Reference-consuming and query-consuming: covers the reference. + Cigar::Match(n) | Cigar::Equal(n) | Cigar::Diff(n) => { + let n = i64::from(*n); + let start = pos.max(0) as usize; + let end = ((pos + n).max(0) as usize).min(len); + if start < end { + blocks.push((start, end)); + } + pos += n; + } + // Reference-consuming only: advances without covering. + Cigar::Del(n) | Cigar::RefSkip(n) => pos += i64::from(*n), + // Neither reference-consuming nor covering. + Cigar::Ins(_) | Cigar::SoftClip(_) | Cigar::HardClip(_) | Cigar::Pad(_) => {} + } + } + blocks + } + + /// Add `blocks`, skipping any part already covered by `exclude`. + /// + /// Both sides are in ascending order and non-overlapping within themselves, + /// because each comes from one record's CIGAR walk. + fn add_blocks_excluding(&mut self, blocks: &[(usize, usize)], exclude: &[(usize, usize)]) { + for &(start, end) in blocks { + let mut cursor = start; + for &(ex_start, ex_end) in exclude { + if ex_end <= cursor { + continue; + } + if ex_start >= end { + break; + } + if ex_start > cursor { + self.add_block_usize(cursor, ex_start.min(end)); + } + cursor = cursor.max(ex_end); + if cursor >= end { + break; + } + } + if cursor < end { + self.add_block_usize(cursor, end); + } + } + } + + /// Consume the delta array and return per-base depth for the contig. + pub fn into_depths(self) -> Vec { + let mut depths = Vec::with_capacity(self.len); + let mut running = 0i32; + for delta in self.deltas.iter().take(self.len) { + running += delta; + // `running` cannot go negative: every `-1` is emitted only after + // its matching `+1`, and both are clamped to the same range. + depths.push(running.max(0) as u32); + } + depths + } + + /// Whether a record contributes to depth at all. + fn passes_filters(&self, record: &bam::Record) -> bool { + record.flags() & self.exclude_flags == 0 && record.mapq() >= self.mapq_cut + } + + /// Record a half-open aligned block `[start, end)`, already clamped. + fn add_block_usize(&mut self, start: usize, end: usize) { + if start >= end { + return; + } + self.deltas[start] += 1; + self.deltas[end] -= 1; + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rust_htslib::bam::record::{Cigar, CigarString, Record}; + + /// Build a minimal mapped record at `pos` with the given CIGAR, MAPQ and flags. + /// + /// `seq` and `qual` must both be as long as the query-consuming part of the + /// CIGAR, otherwise the record is malformed and every assertion made against + /// it is meaningless, so the helper asserts that itself. + fn rec(pos: i64, cigar: Vec, mapq: u8, flags: u16) -> Record { + let query_len: usize = cigar + .iter() + .map(|op| match op { + Cigar::Match(n) | Cigar::Ins(n) | Cigar::SoftClip(n) => *n as usize, + Cigar::Equal(n) | Cigar::Diff(n) => *n as usize, + _ => 0, + }) + .sum(); + let seq = vec![b'A'; query_len]; + let qual = vec![30u8; query_len]; + assert_eq!(seq.len(), qual.len(), "malformed test record"); + + let mut r = Record::new(); + r.set(b"q", Some(&CigarString(cigar)), &seq, &qual); + r.set_tid(0); + r.set_pos(pos); + r.set_mapq(mapq); + r.set_flags(flags); + r + } + + /// Build a paired record whose mate sits at `mate_pos` on the same contig. + fn pair_rec(qname: &[u8], pos: i64, mate_pos: i64, cigar: Vec, read2: bool) -> Record { + let mut r = rec( + pos, + cigar, + 60, + BAM_FPAIRED | BAM_FPROPER_PAIR | if read2 { BAM_FREAD2 } else { BAM_FREAD1 }, + ); + r.set_qname(qname); + r.set_mtid(0); + r.set_mpos(mate_pos); + r + } + + #[test] + fn match_block_covers_exactly_its_span() { + let mut d = DepthAccum::new(20, 0, MOSDEPTH_DEFAULT_EXCLUDE); + d.process_read(&rec(5, vec![Cigar::Match(4)], 60, 0)); + assert_eq!(&d.into_depths()[4..10], &[0, 1, 1, 1, 1, 0]); + } + + #[test] + fn deletion_and_skip_advance_without_covering() { + let mut d = DepthAccum::new(20, 0, MOSDEPTH_DEFAULT_EXCLUDE); + d.process_read(&rec( + 0, + vec![Cigar::Match(2), Cigar::Del(3), Cigar::Match(2)], + 60, + 0, + )); + assert_eq!(&d.into_depths()[0..8], &[1, 1, 0, 0, 0, 1, 1, 0]); + } + + #[test] + fn ref_skip_advances_without_covering() { + let mut d = DepthAccum::new(20, 0, MOSDEPTH_DEFAULT_EXCLUDE); + d.process_read(&rec( + 0, + vec![Cigar::Match(2), Cigar::RefSkip(3), Cigar::Match(2)], + 60, + 0, + )); + assert_eq!(&d.into_depths()[0..8], &[1, 1, 0, 0, 0, 1, 1, 0]); + } + + #[test] + fn insertion_and_soft_clip_do_not_advance_the_reference() { + let mut d = DepthAccum::new(20, 0, MOSDEPTH_DEFAULT_EXCLUDE); + d.process_read(&rec( + 0, + vec![ + Cigar::SoftClip(3), + Cigar::Match(2), + Cigar::Ins(4), + Cigar::Match(2), + ], + 60, + 0, + )); + assert_eq!(&d.into_depths()[0..6], &[1, 1, 1, 1, 0, 0]); + } + + #[test] + fn duplicate_flagged_reads_are_excluded_by_default() { + let mut d = DepthAccum::new(20, 0, MOSDEPTH_DEFAULT_EXCLUDE); + d.process_read(&rec(0, vec![Cigar::Match(4)], 60, BAM_FDUP)); + assert_eq!(d.into_depths().iter().sum::(), 0); + } + + #[test] + fn secondary_qcfail_and_unmapped_reads_are_excluded_by_default() { + for flag in [BAM_FSECONDARY, BAM_FQCFAIL, BAM_FUNMAP] { + let mut d = DepthAccum::new(20, 0, MOSDEPTH_DEFAULT_EXCLUDE); + d.process_read(&rec(0, vec![Cigar::Match(4)], 60, flag)); + assert_eq!( + d.into_depths().iter().sum::(), + 0, + "flag {flag:#x} should be excluded" + ); + } + } + + #[test] + fn reads_below_the_mapq_cutoff_are_excluded() { + let mut d = DepthAccum::new(20, 30, MOSDEPTH_DEFAULT_EXCLUDE); + d.process_read(&rec(0, vec![Cigar::Match(4)], 29, 0)); + assert_eq!(d.into_depths().iter().sum::(), 0); + + let mut d = DepthAccum::new(20, 30, MOSDEPTH_DEFAULT_EXCLUDE); + d.process_read(&rec(0, vec![Cigar::Match(4)], 30, 0)); + assert_eq!(d.into_depths().iter().sum::(), 4); + } + + #[test] + fn a_read_running_past_the_contig_end_is_clipped_not_panicking() { + let mut d = DepthAccum::new(6, 0, MOSDEPTH_DEFAULT_EXCLUDE); + d.process_read(&rec(4, vec![Cigar::Match(10)], 60, 0)); + assert_eq!(d.into_depths(), vec![0, 0, 0, 0, 1, 1]); + } + + #[test] + fn overlapping_mates_cover_a_base_once() { + let mut d = DepthAccum::new(20, 0, MOSDEPTH_DEFAULT_EXCLUDE); + d.process_read(&pair_rec(b"pair1", 0, 0, vec![Cigar::Match(4)], false)); + d.process_read(&pair_rec(b"pair1", 0, 0, vec![Cigar::Match(4)], true)); + assert_eq!( + &d.into_depths()[0..5], + &[1, 1, 1, 1, 0], + "a base covered by both mates counts once" + ); + } + + #[test] + fn partially_overlapping_mates_count_the_shared_bases_once() { + let mut d = DepthAccum::new(20, 0, MOSDEPTH_DEFAULT_EXCLUDE); + d.process_read(&pair_rec(b"pair1", 0, 2, vec![Cigar::Match(4)], false)); + d.process_read(&pair_rec(b"pair1", 2, 0, vec![Cigar::Match(4)], true)); + // Mate 1 covers 0..4, mate 2 covers 2..6; bases 2 and 3 are shared. + assert_eq!(&d.into_depths()[0..7], &[1, 1, 1, 1, 1, 1, 0]); + } + + #[test] + fn non_overlapping_mates_each_contribute() { + let mut d = DepthAccum::new(20, 0, MOSDEPTH_DEFAULT_EXCLUDE); + d.process_read(&pair_rec(b"pair2", 0, 8, vec![Cigar::Match(4)], false)); + d.process_read(&pair_rec(b"pair2", 8, 0, vec![Cigar::Match(4)], true)); + assert_eq!( + &d.into_depths()[0..13], + &[1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0] + ); + } + + #[test] + fn reads_from_different_pairs_at_the_same_locus_both_count() { + let mut d = DepthAccum::new(20, 0, MOSDEPTH_DEFAULT_EXCLUDE); + d.process_read(&pair_rec(b"pairA", 0, 0, vec![Cigar::Match(4)], false)); + d.process_read(&pair_rec(b"pairB", 0, 0, vec![Cigar::Match(4)], false)); + assert_eq!(d.into_depths()[0], 2); + } + + #[test] + fn the_pending_mate_map_is_emptied_once_both_mates_are_seen() { + let mut d = DepthAccum::new(20, 0, MOSDEPTH_DEFAULT_EXCLUDE); + d.process_read(&pair_rec(b"pair1", 0, 0, vec![Cigar::Match(4)], false)); + d.process_read(&pair_rec(b"pair1", 0, 0, vec![Cigar::Match(4)], true)); + assert_eq!(d.pending_mates_len(), 0, "the entry must be dropped"); + } + + #[test] + fn a_pending_mate_that_never_arrives_is_evicted() { + let mut d = DepthAccum::new(200, 0, MOSDEPTH_DEFAULT_EXCLUDE); + // Its mate is announced at 10 but never turns up (filtered, say). + d.process_read(&pair_rec(b"orphan", 0, 10, vec![Cigar::Match(4)], false)); + assert_eq!(d.pending_mates_len(), 1); + // Walking past position 10 makes the entry unclaimable. + d.process_read(&pair_rec(b"later", 50, 50, vec![Cigar::Match(4)], false)); + assert_eq!( + d.pending_mates_len(), + 1, + "only the unclaimable one is dropped" + ); + } + + /// Engine-level parity check against mosdepth 0.3.14 on the committed + /// fixture. `tests/expected/dna/test.mosdepth.summary.txt` records + /// `total 40001 247878 6.20 0 867` for this BAM, so the total covered + /// bases and the maximum depth are both pinned here. Getting this right + /// requires the flag filter, the CIGAR walk and the mate-overlap + /// correction to all be right at once. + #[test] + fn total_covered_bases_match_mosdepth_on_the_fixture() { + use rust_htslib::bam::Read; + + let bam_path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/dna/test.dna.bam"); + let mut bam = bam::Reader::from_path(bam_path).unwrap(); + let header = bam.header().to_owned(); + let contig_len = header.target_len(0).unwrap(); + + let mut accum = DepthAccum::new(contig_len, 0, MOSDEPTH_DEFAULT_EXCLUDE); + let mut record = Record::new(); + while let Some(result) = bam.read(&mut record) { + result.unwrap(); + accum.process_read(&record); + } + + let depths = accum.into_depths(); + let total: u64 = depths.iter().map(|d| u64::from(*d)).sum(); + let max = depths.iter().copied().max().unwrap(); + + assert_eq!(depths.len(), 40001, "contig length"); + assert_eq!(total, 247878, "total covered bases must match mosdepth"); + assert_eq!(max, 867, "maximum depth must match mosdepth"); + } +} diff --git a/src/dna/gc_bias.rs b/src/dna/gc_bias.rs new file mode 100644 index 00000000..bcaec0b8 --- /dev/null +++ b/src/dna/gc_bias.rs @@ -0,0 +1,514 @@ +//! Picard `CollectGcBiasMetrics` reimplementation. +//! +//! # Upstream semantics +//! +//! These rules come from Picard 3.4.0's own source, `GcBiasUtils` and +//! `GcBiasMetricsCollector`, after black-box inference from its output failed +//! to reproduce them. They are unusual enough to be worth stating. +//! +//! **Windows.** GC is computed over sliding windows of `window_size` bases at +//! every reference position `i` for `1 <= i < len - window_size`. Note both +//! bounds: the window at position 0 is skipped, and so is the last one that +//! would fit. On a 40001 base reference with 100 base windows that gives +//! 39900 windows, not the 39902 a naive reading produces. A window holding +//! more than [`MAX_NS_PER_WINDOW`] `N` bases is marked unusable and its reads +//! are dropped. The GC value is `gc_count * 100 / window_size` in integer +//! arithmetic, truncating rather than rounding. +//! +//! **Read assignment.** A read is assigned to the window at its alignment +//! start, except on the reverse strand, where it is assigned to +//! `alignment_end - window_size`. That is not the read's 5' end; it is the +//! window that would start where the read's far end finishes. Positions are +//! one-based, and a read landing at position 0 or lower is dropped. +//! +//! **Which reads count.** Only unmapped reads and reads with an empty +//! sequence are skipped. Secondary and supplementary alignments and +//! duplicates all contribute, which is what `READS_USED ALL` means. On the +//! project fixture that is the difference between 5640 and 5642 read starts. +//! +//! **Dropout.** For each GC bin, `(window_share - read_share) * 100` is +//! accumulated when positive, into `AT_DROPOUT` for bins at or below 50 and +//! `GC_DROPOUT` above. + +use std::io::Write; +use std::path::Path; + +use anyhow::{Context, Result}; +use rust_htslib::bam; +use rust_htslib::bam::record::Cigar; + +use crate::common::bam_flags::*; + +/// Number of GC bins, one per whole percent from 0 to 100 inclusive. +pub const BINS: usize = 101; + +/// Picard's `SCAN_WINDOW_SIZE` default. +pub const DEFAULT_WINDOW_SIZE: usize = 100; + +/// A window holding more than this many `N` bases is unusable. +pub const MAX_NS_PER_WINDOW: usize = 4; + +/// Accumulates GC bias for one contig. +#[derive(Debug)] +pub struct GcBiasAccum { + /// GC percent per one-based reference position, or `-1` when the window + /// there holds too many `N` bases or does not exist. + gc: Vec, + window_size: usize, + windows_by_gc: [u64; BINS], + reads_by_gc: [u64; BINS], + bases_by_gc: [u64; BINS], + errors_by_gc: [u64; BINS], + total_clusters: u64, + total_aligned_reads: u64, +} + +impl GcBiasAccum { + /// Build the window GC table for one contig's reference bases. + pub fn new(reference: &[u8], window_size: usize) -> Self { + let len = reference.len(); + let mut gc = vec![-1i8; len + 1]; + let mut windows_by_gc = [0u64; BINS]; + + if len > window_size { + // Prefix sums make each window a constant-time lookup. + let mut gc_prefix = vec![0u32; len + 1]; + let mut n_prefix = vec![0u32; len + 1]; + for (i, base) in reference.iter().enumerate() { + let upper = base.to_ascii_uppercase(); + gc_prefix[i + 1] = gc_prefix[i] + u32::from(upper == b'G' || upper == b'C'); + n_prefix[i + 1] = n_prefix[i] + u32::from(upper == b'N'); + } + + let last_window_start = len - window_size; + for i in 1..last_window_start { + let end = i + window_size; + let ns = (n_prefix[end] - n_prefix[i]) as usize; + if ns > MAX_NS_PER_WINDOW { + continue; + } + let gc_count = gc_prefix[end] - gc_prefix[i]; + let percent = (gc_count as usize * 100 / window_size) as i8; + gc[i] = percent; + windows_by_gc[percent as usize] += 1; + } + } + + Self { + gc, + window_size, + windows_by_gc, + reads_by_gc: [0; BINS], + bases_by_gc: [0; BINS], + errors_by_gc: [0; BINS], + total_clusters: 0, + total_aligned_reads: 0, + } + } + + /// Offer one record, with the contig's reference bases for mismatch counting. + pub fn process_read(&mut self, record: &bam::Record, reference: &[u8]) { + if record.seq_len() == 0 { + return; + } + + // A cluster is a template, counted once, at the unpaired read or the + // first of the pair. Unmapped reads count towards clusters even though + // they reach nothing else, so this precedes the mapped check. + if record.flags() & BAM_FPAIRED == 0 || record.flags() & BAM_FREAD1 != 0 { + self.total_clusters += 1; + } + if record.flags() & BAM_FUNMAP != 0 { + return; + } + self.total_aligned_reads += 1; + + // One-based, and the reverse strand is assigned by the far end rather + // than the near one. + let position = if record.flags() & BAM_FREVERSE != 0 { + alignment_end(record) - self.window_size as i64 + } else { + record.pos() + 1 + }; + if position <= 0 { + return; + } + let Some(&percent) = self.gc.get(position as usize) else { + return; + }; + if percent < 0 { + return; + } + + let bin = percent as usize; + self.reads_by_gc[bin] += 1; + self.bases_by_gc[bin] += record.seq_len() as u64; + self.errors_by_gc[bin] += count_errors(record, reference); + } + + /// Fold another contig's counters in. Window tables are per contig and add + /// up the same way. + pub fn merge(&mut self, other: &GcBiasAccum) { + for bin in 0..BINS { + self.windows_by_gc[bin] += other.windows_by_gc[bin]; + self.reads_by_gc[bin] += other.reads_by_gc[bin]; + self.bases_by_gc[bin] += other.bases_by_gc[bin]; + self.errors_by_gc[bin] += other.errors_by_gc[bin]; + } + self.total_clusters += other.total_clusters; + self.total_aligned_reads += other.total_aligned_reads; + } + + /// Summarise into the reported detail rows and summary figures. + pub fn into_result(self, window_size: usize) -> GcBiasResult { + let total_reads: u64 = self.reads_by_gc.iter().sum(); + let total_windows: u64 = self.windows_by_gc.iter().sum(); + let global_rate = if total_windows == 0 { + 0.0 + } else { + total_reads as f64 / total_windows as f64 + }; + + let mut rows = Vec::with_capacity(BINS); + let mut at_dropout = 0.0; + let mut gc_dropout = 0.0; + + for bin in 0..BINS { + let windows = self.windows_by_gc[bin]; + let reads = self.reads_by_gc[bin]; + let bases = self.bases_by_gc[bin]; + let errors = self.errors_by_gc[bin]; + + let normalized = if windows == 0 || global_rate == 0.0 { + 0.0 + } else { + (reads as f64 / windows as f64) / global_rate + }; + let error_bar = if windows == 0 || global_rate == 0.0 { + 0.0 + } else { + ((reads as f64).sqrt() / windows as f64) / global_rate + }; + // Mean quality as the phred score of the observed error rate. + let mean_base_quality = if bases == 0 || errors == 0 { + 0 + } else { + (-10.0 * (errors as f64 / bases as f64).log10()).round() as i32 + }; + + if total_reads > 0 && total_windows > 0 { + let read_share = reads as f64 / total_reads as f64; + let window_share = windows as f64 / total_windows as f64; + let dropout = (window_share - read_share) * 100.0; + if dropout > 0.0 { + if bin <= 50 { + at_dropout += dropout; + } else { + gc_dropout += dropout; + } + } + } + + rows.push(GcBiasDetail { + gc: bin as u32, + windows, + read_starts: reads, + mean_base_quality, + normalized_coverage: normalized, + error_bar_width: error_bar, + }); + } + + GcBiasResult { + rows, + window_size, + total_clusters: self.total_clusters, + aligned_reads: self.total_aligned_reads, + at_dropout, + gc_dropout, + } + } +} + +/// One-based inclusive end of a record's alignment. +fn alignment_end(record: &bam::Record) -> i64 { + let mut end = record.pos(); + for op in record.cigar().iter() { + match op { + Cigar::Match(n) + | Cigar::Equal(n) + | Cigar::Diff(n) + | Cigar::Del(n) + | Cigar::RefSkip(n) => end += i64::from(*n), + _ => {} + } + } + end +} + +/// Mismatches against the reference, plus inserted and deleted bases, which is +/// what Picard counts towards the per-bin error rate. +fn count_errors(record: &bam::Record, reference: &[u8]) -> u64 { + let sequence = record.seq(); + let mut errors = 0u64; + let mut ref_pos = record.pos(); + let mut query_pos = 0i64; + + for op in record.cigar().iter() { + match op { + Cigar::Match(n) | Cigar::Equal(n) | Cigar::Diff(n) => { + for k in 0..i64::from(*n) { + let r = ref_pos + k; + let q = query_pos + k; + if r < 0 || r as usize >= reference.len() { + continue; + } + let ref_base = reference[r as usize].to_ascii_uppercase(); + let read_base = sequence[q as usize].to_ascii_uppercase(); + // htsjdk's basesEqual is a plain comparison after + // uppercasing, so an N on either side is a mismatch rather + // than a free pass. + if ref_base != read_base { + errors += 1; + } + } + ref_pos += i64::from(*n); + query_pos += i64::from(*n); + } + Cigar::Ins(n) => { + errors += u64::from(*n); + query_pos += i64::from(*n); + } + Cigar::Del(n) => { + errors += u64::from(*n); + ref_pos += i64::from(*n); + } + Cigar::RefSkip(n) => ref_pos += i64::from(*n), + Cigar::SoftClip(n) => query_pos += i64::from(*n), + Cigar::HardClip(_) | Cigar::Pad(_) => {} + } + } + errors +} + +/// One GC bin's detail row. +#[derive(Debug, Clone)] +pub struct GcBiasDetail { + /// GC percent this row describes. + pub gc: u32, + /// Reference windows at this GC. + pub windows: u64, + /// Reads assigned to a window at this GC. + pub read_starts: u64, + /// Phred score of the observed error rate for those reads. + pub mean_base_quality: i32, + /// Read density here relative to the genome-wide density. + pub normalized_coverage: f64, + /// One standard error of `normalized_coverage`. + pub error_bar_width: f64, +} + +/// The complete GC bias result. +#[derive(Debug, Clone)] +pub struct GcBiasResult { + /// One row per GC bin, ascending. + pub rows: Vec, + /// Window size the bins were computed over. + pub window_size: usize, + /// Templates seen. + pub total_clusters: u64, + /// Mapped reads seen. + pub aligned_reads: u64, + /// Illumina-style AT dropout. + pub at_dropout: f64, + /// Illumina-style GC dropout. + pub gc_dropout: f64, +} + +impl GcBiasResult { + /// Mean normalised coverage across a GC range, as the `GC_NC_x_y` columns + /// report it. + /// + /// The mean is weighted by how many reference windows each bin holds, not + /// a plain average over bins. That distinction matters at the extremes, + /// where most bins hold no windows at all and would otherwise drag the + /// figure towards zero. + fn mean_normalized(&self, low: u32, high: u32) -> f64 { + let mut weighted = 0.0; + let mut windows = 0u64; + for row in self.rows.iter().filter(|r| r.gc >= low && r.gc <= high) { + weighted += row.normalized_coverage * row.windows as f64; + windows += row.windows; + } + if windows == 0 { + 0.0 + } else { + weighted / windows as f64 + } + } +} + +/// Format a float the way Picard's metrics writer does. +fn fmt_picard(value: f64) -> String { + if !value.is_finite() { + return "?".to_string(); + } + if value == value.trunc() && value.abs() < 1e15 { + return format!("{}", value as i64); + } + let text = format!("{value:.6}"); + text.trim_end_matches('0').trim_end_matches('.').to_string() +} + +/// Write the per-GC-bin detail metrics. +pub fn write_detail_metrics(result: &GcBiasResult, path: &Path) -> Result<()> { + let mut out = std::fs::File::create(path) + .map(std::io::BufWriter::new) + .with_context(|| { + format!( + "Failed to create GC bias detail metrics: {}", + path.display() + ) + })?; + + writeln!(out, "## METRICS CLASS\tpicard.analysis.GcBiasDetailMetrics")?; + writeln!( + out, + "ACCUMULATION_LEVEL\tREADS_USED\tGC\tWINDOWS\tREAD_STARTS\tMEAN_BASE_QUALITY\t\ + NORMALIZED_COVERAGE\tERROR_BAR_WIDTH\tSAMPLE\tLIBRARY\tREAD_GROUP" + )?; + for row in &result.rows { + writeln!( + out, + "All Reads\tALL\t{}\t{}\t{}\t{}\t{}\t{}\t\t\t", + row.gc, + row.windows, + row.read_starts, + row.mean_base_quality, + fmt_picard(row.normalized_coverage), + fmt_picard(row.error_bar_width), + )?; + } + // Picard leaves two blank lines at the end of the GC bias tables, one more + // than it writes after the insert size or WGS tables. The fixtures are the + // specification, so this matches them rather than being tidied. + writeln!(out)?; + writeln!(out)?; + out.flush()?; + Ok(()) +} + +/// Write the GC bias summary metrics. +pub fn write_summary_metrics(result: &GcBiasResult, path: &Path) -> Result<()> { + let mut out = std::fs::File::create(path) + .map(std::io::BufWriter::new) + .with_context(|| { + format!( + "Failed to create GC bias summary metrics: {}", + path.display() + ) + })?; + + writeln!( + out, + "## METRICS CLASS\tpicard.analysis.GcBiasSummaryMetrics" + )?; + writeln!( + out, + "ACCUMULATION_LEVEL\tREADS_USED\tWINDOW_SIZE\tTOTAL_CLUSTERS\tALIGNED_READS\t\ + AT_DROPOUT\tGC_DROPOUT\tGC_NC_0_19\tGC_NC_20_39\tGC_NC_40_59\tGC_NC_60_79\t\ + GC_NC_80_100\tSAMPLE\tLIBRARY\tREAD_GROUP" + )?; + writeln!( + out, + "All Reads\tALL\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t\t\t", + result.window_size, + result.total_clusters, + result.aligned_reads, + fmt_picard(result.at_dropout), + fmt_picard(result.gc_dropout), + fmt_picard(result.mean_normalized(0, 19)), + fmt_picard(result.mean_normalized(20, 39)), + fmt_picard(result.mean_normalized(40, 59)), + fmt_picard(result.mean_normalized(60, 79)), + fmt_picard(result.mean_normalized(80, 100)), + )?; + writeln!(out)?; + writeln!(out)?; + out.flush()?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn the_first_and_last_possible_windows_are_both_skipped() { + // 10 bases, window size 4: naive windows would be starts 0 through 6, + // Picard's loop runs 1 through 5. + let reference = b"ACGTACGTAC".to_vec(); + let accum = GcBiasAccum::new(&reference, 4); + let windows: u64 = accum.windows_by_gc.iter().sum(); + assert_eq!(windows, 5, "starts 1 through 5 inclusive"); + assert_eq!(accum.gc[0], -1, "the window at 0 is never computed"); + assert_eq!(accum.gc[6], -1, "nor the last one that would fit"); + } + + #[test] + fn gc_percent_truncates_rather_than_rounds() { + // 3 of 8 bases are G or C: 3 * 100 / 8 is 37.5, truncated to 37. + let reference = b"GGCAAAAAAAAA".to_vec(); + let accum = GcBiasAccum::new(&reference, 8); + // Window at position 1 is GCAAAAAA, two of eight, 25 percent. + assert_eq!(accum.gc[1], 25); + } + + #[test] + fn windows_with_too_many_ns_are_unusable() { + let reference = b"ACGTNNNNNGCTAGCTAGC".to_vec(); + let accum = GcBiasAccum::new(&reference, 8); + // The window at 1 holds five Ns, one more than the limit. + assert_eq!(accum.gc[1], -1); + } + + #[test] + fn dropout_splits_at_fifty_percent_gc() { + let mut accum = GcBiasAccum::new(&b"A".repeat(200), 100); + // Hand-place windows and reads so the shares are unambiguous. + accum.windows_by_gc = [0; BINS]; + accum.reads_by_gc = [0; BINS]; + accum.windows_by_gc[30] = 50; + accum.windows_by_gc[70] = 50; + accum.reads_by_gc[30] = 100; + accum.reads_by_gc[70] = 0; + let result = accum.into_result(100); + // Bin 70 has half the windows and none of the reads: 50 points of + // dropout, and being above 50 percent GC it lands in GC_DROPOUT. + assert!( + (result.gc_dropout - 50.0).abs() < 1e-9, + "{}", + result.gc_dropout + ); + assert!( + (result.at_dropout - 0.0).abs() < 1e-9, + "{}", + result.at_dropout + ); + } + + #[test] + fn normalized_coverage_is_relative_to_the_genome_wide_rate() { + let mut accum = GcBiasAccum::new(&b"A".repeat(200), 100); + accum.windows_by_gc = [0; BINS]; + accum.reads_by_gc = [0; BINS]; + accum.windows_by_gc[10] = 100; + accum.windows_by_gc[20] = 100; + accum.reads_by_gc[10] = 150; + accum.reads_by_gc[20] = 50; + let result = accum.into_result(100); + // Global rate is 200 reads over 200 windows, so 1 read per window. + assert!((result.rows[10].normalized_coverage - 1.5).abs() < 1e-9); + assert!((result.rows[20].normalized_coverage - 0.5).abs() < 1e-9); + } +} diff --git a/src/dna/hs_metrics.rs b/src/dna/hs_metrics.rs new file mode 100644 index 00000000..ba82e745 --- /dev/null +++ b/src/dna/hs_metrics.rs @@ -0,0 +1,677 @@ +//! Picard `CollectHsMetrics` reimplementation for targeted sequencing. +//! +//! # Upstream semantics +//! +//! Taken from Picard 3.4.0's `TargetMetricsCollector`, because this collector +//! does not filter the way [`crate::dna::wgs_metrics`] does and the difference +//! is not guessable from the outputs. +//! +//! Secondary alignments are excluded outright, so `TOTAL_READS` is 5642 on the +//! project fixture rather than the 5644 records it holds. Then, per record: +//! +//! 1. `PF_BASES` accumulates the read length of every non-supplementary read; +//! 2. mapped reads add their reference-aligned bases to `PF_BASES_ALIGNED`, +//! and to `PF_UQ_BASES_ALIGNED` when not duplicate-flagged; +//! 3. the bait counters are taken **before** any filtering, so the assay +//! metrics are not skewed by duplicates or mapping quality; +//! 4. duplicates are charged to `PCT_EXC_DUPE` and dropped; +//! 5. reads below the mapping quality floor are dropped; +//! 6. **overlap clipping happens next, at the read level**, charging +//! `PCT_EXC_OVERLAP` with the number of aligned bases clipped; +//! 7. only then, per surviving base: below the base quality floor charges +//! `PCT_EXC_BASEQ`; off-target charges `PCT_EXC_OFF_TARGET`; the rest are +//! `ON_TARGET_BASES`. +//! +//! Step 6 before step 7 is the crux. `CollectWgsMetrics` applies base quality +//! first and reconciles overlaps per locus afterwards, which is why the two +//! collectors report different `PCT_EXC_BASEQ` and `PCT_EXC_OVERLAP` on the +//! same file: 0.003982 against 0.007352, and 0.330968 against 0.324694. +//! +//! Only the left-most mate of an overlapping pair is clipped, and everything +//! from the mate's alignment start onwards goes, per htsjdk's +//! `getNumOverlappingAlignedBasesToClip`. +//! +//! # What is not reproduced +//! +//! `HET_SNP_SENSITIVITY` and `HET_SNP_Q` come from the same Monte Carlo +//! simulation left out of `CollectWgsMetrics`, and `HS_PENALTY_*X` and +//! `FOLD_80_BASE_PENALTY` derive from it. All are written as Picard writes +//! them when it cannot compute them: `-1` for the penalties, `?` for the rest. + +use std::io::Write; +use std::path::Path; + +use anyhow::{Context, Result}; +use rust_htslib::bam; +use rust_htslib::bam::record::Cigar; + +use crate::common::bam_flags::*; +use crate::dna::intervals::IntervalSet; + +/// Coverage levels reported as `PCT_TARGET_BASES_xX`, in output order. +pub const TARGET_COVERAGE_LEVELS: [u32; 17] = [ + 1, 2, 10, 20, 30, 40, 50, 100, 250, 500, 1000, 2500, 5000, 10000, 25000, 50000, 100000, +]; + +/// Penalty levels reported as `HS_PENALTY_xX`, in output order. +pub const PENALTY_LEVELS: [u32; 6] = [10, 20, 30, 40, 50, 100]; + +/// Accumulates targeted-sequencing metrics for one contig. +#[derive(Debug)] +pub struct HsAccum { + bait_mask: Vec, + target_mask: Vec, + /// High quality on-target depth per reference base. + depth: Vec, + min_mapping_quality: u8, + min_base_quality: u8, + counters: HsCounters, +} + +/// The raw counters, summed across contigs. +#[derive(Debug, Clone, Default)] +pub struct HsCounters { + /// Records seen, secondary alignments excluded. + pub total_reads: u64, + /// Read length of every non-supplementary record. + pub pf_bases: u64, + /// Reference-aligned bases of mapped records. + pub pf_bases_aligned: u64, + /// Reference-aligned bases of mapped, non-duplicate records. + pub pf_uq_bases_aligned: u64, + /// Non-duplicate records. + pub pf_unique_reads: u64, + /// Non-duplicate mapped records. + pub pf_uq_reads_aligned: u64, + /// Aligned bases falling on a bait. + pub on_bait_bases: u64, + /// Aligned bases of bait-overlapping reads that miss the baits themselves. + pub near_bait_bases: u64, + /// Aligned bases of reads that touch no bait at all. + pub off_bait_bases: u64, + /// Bases dropped because their read was duplicate-flagged. + pub excluded_dupe: u64, + /// Bases clipped as overlapping a mate. + pub excluded_overlap: u64, + /// Bases dropped for low base quality. + pub excluded_baseq: u64, + /// Bases dropped for falling outside the targets. + pub excluded_off_target: u64, + /// High quality bases on target. + pub on_target_bases: u64, + /// First-of-pair records over a bait, with a mapped mate. + pub selected_pairs: u64, + /// The same, excluding duplicates. + pub selected_unique_pairs: u64, +} + +impl HsCounters { + /// Add another contig's counters. + pub fn merge(&mut self, other: &HsCounters) { + self.total_reads += other.total_reads; + self.pf_bases += other.pf_bases; + self.pf_bases_aligned += other.pf_bases_aligned; + self.pf_uq_bases_aligned += other.pf_uq_bases_aligned; + self.pf_unique_reads += other.pf_unique_reads; + self.pf_uq_reads_aligned += other.pf_uq_reads_aligned; + self.on_bait_bases += other.on_bait_bases; + self.near_bait_bases += other.near_bait_bases; + self.off_bait_bases += other.off_bait_bases; + self.excluded_dupe += other.excluded_dupe; + self.excluded_overlap += other.excluded_overlap; + self.excluded_baseq += other.excluded_baseq; + self.excluded_off_target += other.excluded_off_target; + self.on_target_bases += other.on_target_bases; + self.selected_pairs += other.selected_pairs; + self.selected_unique_pairs += other.selected_unique_pairs; + } +} + +impl HsAccum { + /// Prepare for one contig. + pub fn new( + contig: &str, + length: u64, + baits: &IntervalSet, + targets: &IntervalSet, + min_mapping_quality: u8, + min_base_quality: u8, + ) -> Self { + Self { + bait_mask: baits.mask(contig, length), + target_mask: targets.mask(contig, length), + depth: vec![0; length as usize], + min_mapping_quality, + min_base_quality, + counters: HsCounters::default(), + } + } + + /// Offer one record. + pub fn process_read(&mut self, record: &bam::Record) { + let flags = record.flags(); + // Secondary alignments are not part of this collector's read set. + if flags & BAM_FSECONDARY != 0 || flags & BAM_FQCFAIL != 0 { + return; + } + self.counters.total_reads += 1; + + if flags & BAM_FSUPPLEMENTARY == 0 { + self.counters.pf_bases += record.seq_len() as u64; + } + if flags & BAM_FDUP == 0 { + self.counters.pf_unique_reads += 1; + } + if flags & BAM_FUNMAP != 0 { + return; + } + + let blocks = aligned_blocks(record, self.depth.len()); + let aligned: u64 = blocks.iter().map(|(start, end)| end - start).sum(); + self.counters.pf_bases_aligned += aligned; + if flags & BAM_FDUP == 0 { + self.counters.pf_uq_bases_aligned += aligned; + self.counters.pf_uq_reads_aligned += 1; + } + + // Bait metrics come before duplicate, mapping quality and overlap + // filtering, so that the assay is measured rather than the library. + let on_bait: u64 = blocks + .iter() + .map(|(start, end)| { + (*start..*end) + .filter(|p| self.bait_mask.get(*p as usize).copied().unwrap_or(false)) + .count() as u64 + }) + .sum(); + if on_bait > 0 { + self.counters.on_bait_bases += on_bait; + self.counters.near_bait_bases += aligned - on_bait; + } else { + self.counters.off_bait_bases += aligned; + } + + // HS_LIBRARY_SIZE counts templates over a bait, once each. + if flags & BAM_FSUPPLEMENTARY == 0 + && flags & BAM_FPAIRED != 0 + && flags & BAM_FREAD1 != 0 + && flags & BAM_FMUNMAP == 0 + && on_bait > 0 + { + self.counters.selected_pairs += 1; + if flags & BAM_FDUP == 0 { + self.counters.selected_unique_pairs += 1; + } + } + + if flags & BAM_FDUP != 0 { + self.counters.excluded_dupe += aligned; + return; + } + if record.mapq() < self.min_mapping_quality { + return; + } + + // Overlap clipping, at the read level and before any base is examined. + // + // Two different quantities are at play. The counter Picard reports is + // htsjdk's count of *read* bases clipped, insertions included. What is + // actually removed from the alignment is every base at or past the + // mate's start, in *reference* coordinates. They coincide only for a + // gapless read, so they are tracked separately. + self.counters.excluded_overlap += overlapping_bases_to_clip(record); + let clip_from = overlap_clip_reference_start(record); + + let qualities = record.qual(); + let mut ref_pos = record.pos(); + let mut query_pos = 0i64; + for op in record.cigar().iter() { + match op { + Cigar::Match(n) | Cigar::Equal(n) | Cigar::Diff(n) => { + for k in 0..i64::from(*n) { + let r = ref_pos + k; + if r < 0 || r as usize >= self.depth.len() { + continue; + } + if let Some(from) = clip_from { + if r >= from { + continue; + } + } + let quality = qualities + .get((query_pos + k) as usize) + .copied() + .unwrap_or(0); + if quality < self.min_base_quality { + self.counters.excluded_baseq += 1; + } else if !self.target_mask[r as usize] { + self.counters.excluded_off_target += 1; + } else { + self.counters.on_target_bases += 1; + self.depth[r as usize] += 1; + } + } + ref_pos += i64::from(*n); + query_pos += i64::from(*n); + } + Cigar::Del(n) | Cigar::RefSkip(n) => ref_pos += i64::from(*n), + Cigar::Ins(n) | Cigar::SoftClip(n) => query_pos += i64::from(*n), + Cigar::HardClip(_) | Cigar::Pad(_) => {} + } + } + } + + /// Consume the accumulator, returning its counters and per-base depths. + pub fn into_parts(self) -> (HsCounters, Vec, Vec) { + (self.counters, self.depth, self.target_mask) + } +} + +/// A record's reference-covering blocks as half-open `[start, end)`. +fn aligned_blocks(record: &bam::Record, contig_len: usize) -> Vec<(u64, u64)> { + let mut blocks = Vec::new(); + let mut ref_pos = record.pos(); + for op in record.cigar().iter() { + match op { + Cigar::Match(n) | Cigar::Equal(n) | Cigar::Diff(n) => { + let start = ref_pos.max(0) as u64; + let end = ((ref_pos + i64::from(*n)).max(0) as u64).min(contig_len as u64); + if start < end { + blocks.push((start, end)); + } + ref_pos += i64::from(*n); + } + Cigar::Del(n) | Cigar::RefSkip(n) => ref_pos += i64::from(*n), + _ => {} + } + } + blocks +} + +/// The reference position from which this read's alignment is clipped away +/// because its mate covers it, or `None` when nothing is clipped. +/// +/// This is the mate's alignment start: the left-most read of an overlapping +/// pair loses everything from there onwards. +fn overlap_clip_reference_start(record: &bam::Record) -> Option { + if overlapping_bases_to_clip(record) == 0 { + return None; + } + Some(record.mpos()) +} + +/// Read bases to clip because a mate covers them, per htsjdk's +/// `getNumOverlappingAlignedBasesToClip`. +/// +/// Only the left-most mate of the pair is clipped, and everything from the +/// mate's alignment start onwards goes. A pair sharing a start is broken by +/// clipping the second of the pair. +fn overlapping_bases_to_clip(record: &bam::Record) -> u64 { + let flags = record.flags(); + if flags & BAM_FPAIRED == 0 || flags & BAM_FUNMAP != 0 || flags & BAM_FMUNMAP != 0 { + return 0; + } + let start = record.pos(); + let mate_start = record.mpos(); + if mate_start < start { + return 0; + } + if mate_start == start && flags & BAM_FREAD1 != 0 { + return 0; + } + + let mut clipped: i64 = 0; + let mut ref_pos = start; + for op in record.cigar().iter() { + let ref_len = match op { + Cigar::Match(n) + | Cigar::Equal(n) + | Cigar::Diff(n) + | Cigar::Del(n) + | Cigar::RefSkip(n) => i64::from(*n), + _ => 0, + }; + if mate_start < ref_pos + ref_len { + match op { + // Only M takes the partial path: htsjdk's MATCH_OR_MISMATCH is + // the M operator alone, so = and X fall through to the branch + // below and lose their whole element. + Cigar::Match(_) => { + clipped += if mate_start < ref_pos { + ref_len + } else { + ref_pos + ref_len - mate_start + }; + } + Cigar::SoftClip(_) | Cigar::HardClip(_) | Cigar::Pad(_) | Cigar::RefSkip(_) => {} + // Everything else loses its read-consuming bases outright, + // which covers insertions as well as = and X. + Cigar::Equal(n) | Cigar::Diff(n) | Cigar::Ins(n) => clipped += i64::from(*n), + Cigar::Del(_) => {} + } + } + ref_pos += ref_len; + } + // Left-most but not actually overlapping. + clipped.max(0) as u64 +} + +/// Estimate library size from observed and unique templates. +/// +/// Solves the Lander-Waterman equation `C/X = 1 - exp(-N/X)` by bisection, +/// exactly as Picard's `DuplicationMetrics.estimateLibrarySize` does, down to +/// the forty iterations and the starting bracket. +pub fn estimate_library_size(read_pairs: u64, unique_read_pairs: u64) -> Option { + if read_pairs == 0 || read_pairs <= unique_read_pairs || unique_read_pairs == 0 { + return None; + } + let n = read_pairs as f64; + let c = unique_read_pairs as f64; + let f = |x: f64| c / x - 1.0 + (-n / x).exp(); + + let mut low = 1.0; + let mut high = 100.0; + while f(high * c) > 0.0 { + high *= 10.0; + } + for _ in 0..40 { + let mid = (low + high) / 2.0; + let value = f(mid * c); + if value == 0.0 { + break; + } else if value > 0.0 { + low = mid; + } else { + high = mid; + } + } + Some((c * (low + high) / 2.0) as u64) +} + +/// The computed `CollectHsMetrics` figures. +#[derive(Debug, Clone)] +pub struct HsMetricsResult { + /// Name of the bait set. + pub bait_set: String, + /// Bases covered by baits. + pub bait_territory: u64, + /// Bases covered by targets. + pub target_territory: u64, + /// Total reference length. + pub genome_size: u64, + /// Raw counters. + pub counters: HsCounters, + /// High quality depth of every target base, target order. + pub target_depths: Vec, + /// Number of targets with no coverage at all. + pub zero_coverage_targets: u64, + /// Number of targets. + pub target_count: u64, + /// Estimated library size, when it can be estimated. + pub library_size: Option, +} + +impl HsMetricsResult { + /// Mean high quality coverage over the targets. + pub fn mean_target_coverage(&self) -> f64 { + if self.target_territory == 0 { + 0.0 + } else { + self.counters.on_target_bases as f64 / self.target_territory as f64 + } + } + + /// Mean aligned coverage over the baits. + pub fn mean_bait_coverage(&self) -> f64 { + if self.bait_territory == 0 { + 0.0 + } else { + self.counters.pf_bases_aligned as f64 / self.bait_territory as f64 + } + } + + /// Fraction of the targets at or above each level. + pub fn target_coverage_fractions(&self) -> Vec { + TARGET_COVERAGE_LEVELS + .iter() + .map(|level| { + if self.target_territory == 0 { + return 0.0; + } + let at_or_above = self.target_depths.iter().filter(|d| **d >= *level).count(); + at_or_above as f64 / self.target_territory as f64 + }) + .collect() + } + + /// Median, minimum and maximum high quality target coverage. + pub fn target_coverage_bounds(&self) -> (u32, u32, u32) { + if self.target_depths.is_empty() { + return (0, 0, 0); + } + let mut sorted = self.target_depths.clone(); + sorted.sort_unstable(); + let median = sorted[sorted.len() / 2]; + (median, sorted[0], sorted[sorted.len() - 1]) + } +} + +/// Format a float the way Picard's metrics writer does. +fn fmt_picard(value: f64) -> String { + if !value.is_finite() { + return "?".to_string(); + } + if value == value.trunc() && value.abs() < 1e15 { + return format!("{}", value as i64); + } + let text = format!("{value:.6}"); + text.trim_end_matches('0').trim_end_matches('.').to_string() +} + +/// Write a Picard-compatible `hs_metrics.txt`. +pub fn write_hs_metrics(result: &HsMetricsResult, path: &Path) -> Result<()> { + let mut out = std::fs::File::create(path) + .map(std::io::BufWriter::new) + .with_context(|| format!("Failed to create HS metrics: {}", path.display()))?; + + let c = &result.counters; + let aligned = c.pf_bases_aligned as f64; + let frac = |n: u64| { + if aligned == 0.0 { + 0.0 + } else { + n as f64 / aligned + } + }; + let selected = c.on_bait_bases + c.near_bait_bases; + let (median, min, max) = result.target_coverage_bounds(); + + writeln!(out, "## METRICS CLASS\tpicard.analysis.directed.HsMetrics")?; + + let mut header = String::from( + "BAIT_SET\tBAIT_TERRITORY\tBAIT_DESIGN_EFFICIENCY\tON_BAIT_BASES\tNEAR_BAIT_BASES\t\ + OFF_BAIT_BASES\tPCT_SELECTED_BASES\tPCT_OFF_BAIT\tON_BAIT_VS_SELECTED\t\ + MEAN_BAIT_COVERAGE\tPCT_USABLE_BASES_ON_BAIT\tPCT_USABLE_BASES_ON_TARGET\t\ + FOLD_ENRICHMENT\tHS_LIBRARY_SIZE", + ); + for level in PENALTY_LEVELS { + header.push_str(&format!("\tHS_PENALTY_{level}X")); + } + header.push_str( + "\tTARGET_TERRITORY\tGENOME_SIZE\tTOTAL_READS\tPF_READS\tPF_BASES\tPF_UNIQUE_READS\t\ + PF_UQ_READS_ALIGNED\tPF_BASES_ALIGNED\tPF_UQ_BASES_ALIGNED\tON_TARGET_BASES\t\ + PCT_PF_READS\tPCT_PF_UQ_READS\tPCT_PF_UQ_READS_ALIGNED\tMEAN_TARGET_COVERAGE\t\ + MEDIAN_TARGET_COVERAGE\tMAX_TARGET_COVERAGE\tMIN_TARGET_COVERAGE\tZERO_CVG_TARGETS_PCT\t\ + PCT_EXC_DUPE\tPCT_EXC_ADAPTER\tPCT_EXC_MAPQ\tPCT_EXC_BASEQ\tPCT_EXC_OVERLAP\t\ + PCT_EXC_OFF_TARGET\tFOLD_80_BASE_PENALTY", + ); + for level in TARGET_COVERAGE_LEVELS { + header.push_str(&format!("\tPCT_TARGET_BASES_{level}X")); + } + header.push_str( + "\tAT_DROPOUT\tGC_DROPOUT\tHET_SNP_SENSITIVITY\tHET_SNP_Q\tSAMPLE\tLIBRARY\tREAD_GROUP", + ); + writeln!(out, "{header}")?; + + write!( + out, + "{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}", + result.bait_set, + result.bait_territory, + // Every bait base is intended as a target here; Picard reports the + // fraction of bait territory that is also target territory. + fmt_picard(if result.bait_territory == 0 { + 0.0 + } else { + result.target_territory.min(result.bait_territory) as f64 / result.bait_territory as f64 + }), + c.on_bait_bases, + c.near_bait_bases, + c.off_bait_bases, + fmt_picard(frac(selected)), + fmt_picard(frac(c.off_bait_bases)), + fmt_picard(if selected == 0 { + 0.0 + } else { + c.on_bait_bases as f64 / selected as f64 + }), + fmt_picard(result.mean_bait_coverage()), + fmt_picard(if c.pf_bases == 0 { + 0.0 + } else { + c.on_bait_bases as f64 / c.pf_bases as f64 + }), + fmt_picard(if c.pf_bases == 0 { + 0.0 + } else { + c.on_target_bases as f64 / c.pf_bases as f64 + }), + fmt_picard(fold_enrichment(result)), + result + .library_size + .map(|v| v.to_string()) + .unwrap_or_default(), + )?; + // The penalties derive from the theoretical sensitivity simulation, which + // is out of scope; Picard writes -1 when it cannot compute them. + for _ in PENALTY_LEVELS { + write!(out, "\t-1")?; + } + write!( + out, + "\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t?", + result.target_territory, + result.genome_size, + c.total_reads, + c.total_reads, + c.pf_bases, + c.pf_unique_reads, + c.pf_uq_reads_aligned, + c.pf_bases_aligned, + c.pf_uq_bases_aligned, + c.on_target_bases, + fmt_picard(1.0), + fmt_picard(if c.total_reads == 0 { + 0.0 + } else { + c.pf_unique_reads as f64 / c.total_reads as f64 + }), + fmt_picard(if c.pf_unique_reads == 0 { + 0.0 + } else { + c.pf_uq_reads_aligned as f64 / c.pf_unique_reads as f64 + }), + fmt_picard(result.mean_target_coverage()), + median, + max, + min, + fmt_picard(if result.target_count == 0 { + 0.0 + } else { + result.zero_coverage_targets as f64 / result.target_count as f64 + }), + fmt_picard(frac(c.excluded_dupe)), + fmt_picard(0.0), + fmt_picard(frac(0)), + fmt_picard(frac(c.excluded_baseq)), + fmt_picard(frac(c.excluded_overlap)), + fmt_picard(frac(c.excluded_off_target)), + )?; + for fraction in result.target_coverage_fractions() { + write!(out, "\t{}", fmt_picard(fraction))?; + } + // AT and GC dropout over targets, and the two simulated columns, are not + // computed; see the module documentation. + writeln!(out, "\t?\t?\t?\t?\t\t\t")?; + writeln!(out)?; + + out.flush()?; + Ok(()) +} + +/// Enrichment of the selected territory relative to uniform coverage. +/// +/// Picard computes this from the *selected* bases against the bait territory, +/// not from on-target bases against the target territory. On the project +/// fixture every aligned base is on bait, so the figure reduces to +/// `GENOME_SIZE / BAIT_TERRITORY`, which is exactly the 1.142886 it reports. +fn fold_enrichment(result: &HsMetricsResult) -> f64 { + let c = &result.counters; + if c.pf_bases_aligned == 0 || result.bait_territory == 0 || result.genome_size == 0 { + return 0.0; + } + let selected = (c.on_bait_bases + c.near_bait_bases) as f64 / c.pf_bases_aligned as f64; + selected / (result.bait_territory as f64 / result.genome_size as f64) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn library_size_solves_the_lander_waterman_equation() { + // The project fixture's counts, checked against Picard's own answer. + assert_eq!(estimate_library_size(2820, 1992), Some(3807)); + } + + #[test] + fn library_size_is_absent_when_nothing_is_duplicated() { + assert_eq!(estimate_library_size(100, 100), None); + assert_eq!(estimate_library_size(0, 0), None); + } + + #[test] + fn target_coverage_fractions_are_at_or_above_each_level() { + let result = HsMetricsResult { + bait_set: "t".into(), + bait_territory: 4, + target_territory: 4, + genome_size: 100, + counters: HsCounters::default(), + target_depths: vec![0, 1, 10, 300], + zero_coverage_targets: 0, + target_count: 1, + library_size: None, + }; + let f = result.target_coverage_fractions(); + assert!((f[0] - 0.75).abs() < 1e-12, "1X"); + assert!((f[2] - 0.5).abs() < 1e-12, "10X"); + assert!((f[8] - 0.25).abs() < 1e-12, "250X"); + } + + #[test] + fn coverage_bounds_come_from_the_target_bases_only() { + let result = HsMetricsResult { + bait_set: "t".into(), + bait_territory: 5, + target_territory: 5, + genome_size: 100, + counters: HsCounters::default(), + target_depths: vec![0, 3, 7, 9, 40], + zero_coverage_targets: 0, + target_count: 1, + library_size: None, + }; + assert_eq!(result.target_coverage_bounds(), (7, 0, 40)); + } +} diff --git a/src/dna/insert_size.rs b/src/dna/insert_size.rs new file mode 100644 index 00000000..5ebd179e --- /dev/null +++ b/src/dna/insert_size.rs @@ -0,0 +1,489 @@ +//! Picard `CollectInsertSizeMetrics` reimplementation. +//! +//! # Upstream semantics +//! +//! Every rule below was measured against Picard 3.4.0 output on +//! `tests/data/dna/test.dna.bam`, not recalled from documentation. +//! +//! A record contributes when it is paired, is neither secondary, +//! supplementary, duplicate-flagged nor unmapped, has a mapped mate, and +//! carries a positive `TLEN`. Taking only the positive `TLEN` of the two is +//! what counts each pair once. Proper-pair is deliberately **not** required: +//! requiring it drops one pair and shortens the maximum from 300 to 239 on the +//! project fixture. +//! +//! Pairs are grouped by orientation (`FR`, `RF`, `TANDEM`), each group +//! reported on its own row with its own histogram, exactly as Picard does. +//! +//! `MEAN_INSERT_SIZE` and `STANDARD_DEVIATION` are computed over the +//! histogram trimmed to `DEVIATIONS` median absolute deviations either side of +//! the median, and the standard deviation uses the `n - 1` denominator. +//! `MIN_INSERT_SIZE` and `MAX_INSERT_SIZE` are over the untrimmed set. +//! +//! `WIDTH_OF_XX_PERCENT` is the width of the smallest window centred on the +//! median that covers at least `XX` percent of pairs: grow `i` from zero until +//! the bins from `median - i` to `median + i` cover the target, then report +//! `2i + 1`. + +use std::collections::BTreeMap; +use std::io::Write; +use std::path::Path; + +use anyhow::{Context, Result}; +use rust_htslib::bam; + +use crate::common::bam_flags::*; + +/// Percentiles Picard reports a width for, in output order. +pub const WIDTH_PERCENTILES: [u32; 11] = [10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 99]; + +/// Picard's `DEVIATIONS` default: how many median absolute deviations either +/// side of the median survive trimming before the mean and standard deviation +/// are computed. +pub const DEFAULT_DEVIATIONS: f64 = 10.0; + +/// Relative orientation of the two mates of a pair. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum PairOrientation { + /// Forward-reverse, the usual Illumina paired-end arrangement. + Fr, + /// Reverse-forward, seen in mate-pair and some capture libraries. + Rf, + /// Both mates on the same strand. + Tandem, +} + +impl PairOrientation { + /// The label Picard writes in the `PAIR_ORIENTATION` column. + pub fn label(&self) -> &'static str { + match self { + PairOrientation::Fr => "FR", + PairOrientation::Rf => "RF", + PairOrientation::Tandem => "TANDEM", + } + } + + /// The prefix Picard uses for this orientation's histogram column. + fn histogram_column(&self) -> &'static str { + match self { + PairOrientation::Fr => "fr", + PairOrientation::Rf => "rf", + PairOrientation::Tandem => "tandem", + } + } +} + +/// Accumulates insert sizes, one histogram per orientation. +#[derive(Debug, Default)] +pub struct InsertSizeAccum { + histograms: BTreeMap>, +} + +impl InsertSizeAccum { + /// A new, empty accumulator. + pub fn new() -> Self { + Self::default() + } + + /// Offer one record. Records that do not represent a countable pair are + /// ignored. + pub fn process_read(&mut self, record: &bam::Record) { + let flags = record.flags(); + if flags & BAM_FPAIRED == 0 { + return; + } + let excluded = BAM_FUNMAP | BAM_FMUNMAP | BAM_FSECONDARY | BAM_FSUPPLEMENTARY | BAM_FDUP; + if flags & excluded != 0 { + return; + } + // Only the mate carrying the positive TLEN counts, so each pair is + // counted once. + let insert_size = record.insert_size(); + if insert_size <= 0 { + return; + } + + let orientation = orientation_of(flags); + *self + .histograms + .entry(orientation) + .or_default() + .entry(insert_size as u64) + .or_insert(0) += 1; + } + + /// Fold another accumulator into this one. + pub fn merge(&mut self, other: InsertSizeAccum) { + for (orientation, histogram) in other.histograms { + let target = self.histograms.entry(orientation).or_default(); + for (size, count) in histogram { + *target.entry(size).or_insert(0) += count; + } + } + } + + /// Summarise each orientation, in Picard's output order: most pairs first. + pub fn into_result(self, deviations: f64) -> InsertSizeResult { + let mut rows: Vec = self + .histograms + .into_iter() + .map(|(orientation, histogram)| InsertSizeRow::new(orientation, histogram, deviations)) + .collect(); + rows.sort_by_key(|row| std::cmp::Reverse(row.read_pairs)); + InsertSizeResult { rows } + } +} + +/// Which orientation a record's flags describe. +fn orientation_of(flags: u16) -> PairOrientation { + let read_reverse = flags & BAM_FREVERSE != 0; + let mate_reverse = flags & BAM_FMREVERSE != 0; + if read_reverse == mate_reverse { + PairOrientation::Tandem + } else if read_reverse { + // This record carries the positive TLEN, so it is the leftmost mate. + // Leftmost on the reverse strand means reverse-forward. + PairOrientation::Rf + } else { + PairOrientation::Fr + } +} + +/// One orientation's metrics and histogram. +#[derive(Debug, Clone)] +pub struct InsertSizeRow { + /// The orientation this row describes. + pub orientation: PairOrientation, + /// Insert size histogram, size to pair count. + pub histogram: BTreeMap, + /// Number of pairs counted. + pub read_pairs: u64, + /// Median insert size. + pub median: u64, + /// Most frequent insert size; ties go to the smaller size. + pub mode: u64, + /// Median absolute deviation from the median. + pub median_absolute_deviation: u64, + /// Smallest insert size seen, before trimming. + pub min: u64, + /// Largest insert size seen, before trimming. + pub max: u64, + /// Mean over the trimmed histogram. + pub mean: f64, + /// Standard deviation over the trimmed histogram, `n - 1` denominator. + pub standard_deviation: f64, + /// Width of the smallest median-centred window covering each percentile, + /// in the order of [`WIDTH_PERCENTILES`]. + pub widths: Vec, +} + +impl InsertSizeRow { + fn new(orientation: PairOrientation, histogram: BTreeMap, deviations: f64) -> Self { + let read_pairs: u64 = histogram.values().sum(); + let median = quantile(&histogram, read_pairs / 2); + let mode = histogram + .iter() + .max_by_key(|(size, count)| (**count, std::cmp::Reverse(**size))) + .map(|(size, _)| *size) + .unwrap_or(0); + + // Median absolute deviation, itself a median over |size - median|. + let mut deviation_histogram: BTreeMap = BTreeMap::new(); + for (size, count) in &histogram { + let deviation = size.abs_diff(median); + *deviation_histogram.entry(deviation).or_insert(0) += count; + } + let median_absolute_deviation = quantile(&deviation_histogram, read_pairs / 2); + + let min = histogram.keys().copied().min().unwrap_or(0); + let max = histogram.keys().copied().max().unwrap_or(0); + + // Trim to `deviations` MADs either side before the mean and SD. + let span = deviations * median_absolute_deviation as f64; + let low = (median as f64 - span).max(0.0); + let high = median as f64 + span; + let trimmed: Vec<(u64, u64)> = histogram + .iter() + .filter(|(size, _)| **size as f64 >= low && **size as f64 <= high) + .map(|(size, count)| (*size, *count)) + .collect(); + + let n: u64 = trimmed.iter().map(|(_, count)| count).sum(); + let mean = if n == 0 { + 0.0 + } else { + trimmed + .iter() + .map(|(size, count)| *size as f64 * *count as f64) + .sum::() + / n as f64 + }; + let standard_deviation = if n < 2 { + 0.0 + } else { + let variance = trimmed + .iter() + .map(|(size, count)| { + let diff = *size as f64 - mean; + diff * diff * *count as f64 + }) + .sum::() + / (n - 1) as f64; + variance.sqrt() + }; + + let widths = WIDTH_PERCENTILES + .iter() + .map(|pct| width_of_percent(&histogram, median, read_pairs, *pct)) + .collect(); + + Self { + orientation, + histogram, + read_pairs, + median, + mode, + median_absolute_deviation, + min, + max, + mean, + standard_deviation, + widths, + } + } +} + +/// The value at `rank` when the histogram is expanded into a sorted list. +fn quantile(histogram: &BTreeMap, rank: u64) -> u64 { + let mut seen = 0u64; + for (value, count) in histogram { + seen += count; + if seen > rank { + return *value; + } + } + histogram.keys().next_back().copied().unwrap_or(0) +} + +/// Width of the smallest window centred on `median` covering `pct` percent of +/// `total` pairs. +fn width_of_percent(histogram: &BTreeMap, median: u64, total: u64, pct: u32) -> u64 { + if total == 0 { + return 0; + } + let target = total as f64 * pct as f64 / 100.0; + let mut covered = *histogram.get(&median).unwrap_or(&0) as f64; + let mut i = 0u64; + while covered < target { + i += 1; + covered += *histogram.get(&(median.saturating_sub(i))).unwrap_or(&0) as f64; + covered += *histogram.get(&(median + i)).unwrap_or(&0) as f64; + // Once the window spans the whole histogram there is nothing left to add. + if median + i > *histogram.keys().next_back().unwrap_or(&0) && median < i { + break; + } + } + 2 * i + 1 +} + +/// All orientations' metrics for one alignment file. +#[derive(Debug, Clone)] +pub struct InsertSizeResult { + /// One row per orientation seen, most pairs first. + pub rows: Vec, +} + +/// Format a float the way Picard's metrics writer does: up to six decimals, +/// trailing zeros removed, and a bare integer when there is no fraction. +fn fmt_picard(value: f64) -> String { + if value == value.trunc() && value.abs() < 1e15 { + return format!("{}", value as i64); + } + let text = format!("{value:.6}"); + let trimmed = text.trim_end_matches('0').trim_end_matches('.'); + trimmed.to_string() +} + +/// Write a Picard-compatible `insert_size_metrics.txt`. +/// +/// The `## htsjdk...StringHeader` preamble Picard writes is omitted: it holds +/// only the command line and a start timestamp, both of which are noise in a +/// reproducible pipeline. +pub fn write_insert_size_metrics(result: &InsertSizeResult, path: &Path) -> Result<()> { + let mut out = std::fs::File::create(path) + .map(std::io::BufWriter::new) + .with_context(|| format!("Failed to create insert size metrics: {}", path.display()))?; + + writeln!(out, "## METRICS CLASS\tpicard.analysis.InsertSizeMetrics")?; + write!( + out, + "MEDIAN_INSERT_SIZE\tMODE_INSERT_SIZE\tMEDIAN_ABSOLUTE_DEVIATION\tMIN_INSERT_SIZE\t\ + MAX_INSERT_SIZE\tMEAN_INSERT_SIZE\tSTANDARD_DEVIATION\tREAD_PAIRS\tPAIR_ORIENTATION" + )?; + for pct in WIDTH_PERCENTILES { + write!(out, "\tWIDTH_OF_{pct}_PERCENT")?; + } + writeln!(out, "\tSAMPLE\tLIBRARY\tREAD_GROUP")?; + + for row in &result.rows { + write!( + out, + "{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}", + row.median, + row.mode, + row.median_absolute_deviation, + row.min, + row.max, + fmt_picard(row.mean), + fmt_picard(row.standard_deviation), + row.read_pairs, + row.orientation.label(), + )?; + for width in &row.widths { + write!(out, "\t{width}")?; + } + // Trailing SAMPLE, LIBRARY and READ_GROUP columns are empty at the + // ALL_READS accumulation level, which is Picard's default. + writeln!(out, "\t\t\t")?; + } + + writeln!(out)?; + writeln!(out, "## HISTOGRAM\tjava.lang.Integer")?; + write!(out, "insert_size")?; + for row in &result.rows { + write!( + out, + "\tAll_Reads.{}_count", + row.orientation.histogram_column() + )?; + } + writeln!(out)?; + + // One row per insert size seen in any orientation, ascending. + let mut sizes: Vec = result + .rows + .iter() + .flat_map(|row| row.histogram.keys().copied()) + .collect(); + sizes.sort_unstable(); + sizes.dedup(); + for size in sizes { + write!(out, "{size}")?; + for row in &result.rows { + write!(out, "\t{}", row.histogram.get(&size).copied().unwrap_or(0))?; + } + writeln!(out)?; + } + writeln!(out)?; + + out.flush()?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn hist(pairs: &[(u64, u64)]) -> BTreeMap { + pairs.iter().copied().collect() + } + + #[test] + fn orientation_follows_the_strand_flags() { + // The record carrying the positive TLEN is the leftmost mate, so its + // own strand decides between FR and RF. + assert_eq!( + orientation_of(BAM_FPAIRED | BAM_FMREVERSE), + PairOrientation::Fr, + "leftmost forward, mate reverse" + ); + assert_eq!( + orientation_of(BAM_FPAIRED | BAM_FREVERSE), + PairOrientation::Rf, + "leftmost reverse, mate forward" + ); + // Same strand either way round is tandem, including neither reversed. + assert_eq!( + orientation_of(BAM_FPAIRED), + PairOrientation::Tandem, + "both forward" + ); + assert_eq!( + orientation_of(BAM_FPAIRED | BAM_FREVERSE | BAM_FMREVERSE), + PairOrientation::Tandem, + "both reverse" + ); + } + + #[test] + fn mode_breaks_ties_towards_the_smaller_size() { + let row = InsertSizeRow::new( + PairOrientation::Fr, + hist(&[(100, 5), (200, 5)]), + DEFAULT_DEVIATIONS, + ); + assert_eq!(row.mode, 100); + } + + #[test] + fn standard_deviation_uses_the_sample_denominator() { + // Values 1, 2, 3: mean 2, sample variance 1, so SD is exactly 1. + let row = InsertSizeRow::new( + PairOrientation::Fr, + hist(&[(1, 1), (2, 1), (3, 1)]), + DEFAULT_DEVIATIONS, + ); + assert!((row.mean - 2.0).abs() < 1e-12); + assert!( + (row.standard_deviation - 1.0).abs() < 1e-12, + "got {}", + row.standard_deviation + ); + } + + #[test] + fn trimming_excludes_outliers_beyond_the_deviation_span() { + // Median 10, MAD 0, so a span of zero keeps only the median bin. + let row = InsertSizeRow::new( + PairOrientation::Fr, + hist(&[(10, 9), (1000, 1)]), + DEFAULT_DEVIATIONS, + ); + assert_eq!(row.max, 1000, "the untrimmed maximum is still reported"); + assert!( + (row.mean - 10.0).abs() < 1e-12, + "the outlier must not reach the mean, got {}", + row.mean + ); + } + + #[test] + fn width_grows_symmetrically_around_the_median() { + // Ten pairs at the median, five either side one apart. + let h = hist(&[(9, 5), (10, 10), (11, 5)]); + assert_eq!(width_of_percent(&h, 10, 20, 50), 1, "the median bin alone"); + assert_eq!(width_of_percent(&h, 10, 20, 90), 3, "one bin either side"); + } + + #[test] + fn picard_float_formatting_drops_trailing_zeros() { + assert_eq!(fmt_picard(124.442269), "124.442269"); + assert_eq!(fmt_picard(3.5), "3.5"); + assert_eq!(fmt_picard(40001.0), "40001"); + assert_eq!(fmt_picard(0.0), "0"); + } + + #[test] + fn rows_are_ordered_by_pair_count() { + let mut accum = InsertSizeAccum::new(); + accum + .histograms + .insert(PairOrientation::Rf, hist(&[(100, 1)])); + accum + .histograms + .insert(PairOrientation::Fr, hist(&[(100, 50)])); + let result = accum.into_result(DEFAULT_DEVIATIONS); + assert_eq!(result.rows[0].orientation, PairOrientation::Fr); + assert_eq!(result.rows[1].orientation, PairOrientation::Rf); + } +} diff --git a/src/dna/intervals.rs b/src/dna/intervals.rs new file mode 100644 index 00000000..f89af490 --- /dev/null +++ b/src/dna/intervals.rs @@ -0,0 +1,262 @@ +//! BED interval parsing and merging for targeted mode. +//! +//! Picard consumes `.interval_list` files, RustQC accepts BED. The two differ +//! in a way that is easy to get wrong: BED is zero-based half-open, an +//! interval list is one-based inclusive, so `chr22 1 15000` in BED is +//! `chr22 2 15000` in an interval list. Everything here works in BED's +//! convention internally and converts only at the edges. + +use std::collections::HashMap; +use std::path::Path; + +use anyhow::{bail, Context, Result}; + +/// A half-open interval `[start, end)` on one contig, zero-based. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct Interval { + /// Zero-based inclusive start. + pub start: u64, + /// Zero-based exclusive end. + pub end: u64, +} + +impl Interval { + /// Number of bases covered. + pub fn len(&self) -> u64 { + self.end.saturating_sub(self.start) + } + + /// Whether the interval covers no bases. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Whether `position` falls inside. + pub fn contains(&self, position: u64) -> bool { + position >= self.start && position < self.end + } +} + +/// Merged, sorted intervals grouped by contig. +#[derive(Debug, Clone, Default)] +pub struct IntervalSet { + /// Non-overlapping intervals per contig, ascending. + by_contig: HashMap>, + /// A name for the set, used as `BAIT_SET` in the metrics. + name: String, +} + +impl IntervalSet { + /// Read a BED file, merging any overlapping or touching intervals. + /// + /// Merging matters: overlapping targets would otherwise inflate the + /// territory and double-count on-target bases. + pub fn from_bed(path: &Path) -> Result { + let text = std::fs::read_to_string(path) + .with_context(|| format!("Failed to read BED file: {}", path.display()))?; + + let name = path + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("targets") + .to_string(); + + let mut raw: HashMap> = HashMap::new(); + for (number, line) in text.lines().enumerate() { + let line = line.trim(); + if line.is_empty() + || line.starts_with('#') + || line.starts_with("track") + || line.starts_with("browser") + { + continue; + } + let fields: Vec<&str> = line.split('\t').collect(); + if fields.len() < 3 { + bail!( + "{}: line {} has {} fields, a BED interval needs at least 3", + path.display(), + number + 1, + fields.len() + ); + } + let start: u64 = fields[1].parse().with_context(|| { + format!("{}: line {} has a bad start", path.display(), number + 1) + })?; + let end: u64 = fields[2].parse().with_context(|| { + format!("{}: line {} has a bad end", path.display(), number + 1) + })?; + if end <= start { + bail!( + "{}: line {} ends at or before it starts", + path.display(), + number + 1 + ); + } + raw.entry(fields[0].to_string()) + .or_default() + .push(Interval { start, end }); + } + + let by_contig = raw + .into_iter() + .map(|(contig, intervals)| (contig, merge(intervals))) + .collect(); + + Ok(Self { by_contig, name }) + } + + /// Build directly from intervals, for tests and for deriving one set from + /// another. + pub fn from_intervals(name: &str, by_contig: HashMap>) -> Self { + Self { + by_contig: by_contig + .into_iter() + .map(|(contig, intervals)| (contig, merge(intervals))) + .collect(), + name: name.to_string(), + } + } + + /// The set's name, reported as `BAIT_SET`. + pub fn name(&self) -> &str { + &self.name + } + + /// Total bases covered across every contig. + pub fn territory(&self) -> u64 { + self.by_contig + .values() + .flat_map(|intervals| intervals.iter()) + .map(|interval| interval.len()) + .sum() + } + + /// Intervals on one contig, ascending, or an empty slice. + pub fn on(&self, contig: &str) -> &[Interval] { + self.by_contig + .get(contig) + .map(|v| v.as_slice()) + .unwrap_or(&[]) + } + + /// Total number of intervals. + pub fn len(&self) -> usize { + self.by_contig.values().map(|v| v.len()).sum() + } + + /// Whether the set holds no intervals. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// A per-base membership mask for one contig, for fast position lookup in + /// the inner loop. + pub fn mask(&self, contig: &str, length: u64) -> Vec { + let mut mask = vec![false; length as usize]; + for interval in self.on(contig) { + let start = interval.start.min(length) as usize; + let end = interval.end.min(length) as usize; + mask[start..end].fill(true); + } + mask + } +} + +/// Sort and merge overlapping or adjacent intervals. +fn merge(mut intervals: Vec) -> Vec { + intervals.sort(); + let mut merged: Vec = Vec::with_capacity(intervals.len()); + for interval in intervals { + match merged.last_mut() { + Some(last) if interval.start <= last.end => { + last.end = last.end.max(interval.end); + } + _ => merged.push(interval), + } + } + merged +} + +#[cfg(test)] +mod tests { + use super::*; + + fn write_bed(name: &str, contents: &str) -> std::path::PathBuf { + let dir = std::env::temp_dir().join("rustqc-interval-tests"); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join(name); + std::fs::write(&path, contents).unwrap(); + path + } + + #[test] + fn overlapping_intervals_are_merged() { + let path = write_bed( + "overlap.bed", + "chr1\t100\t200\nchr1\t150\t300\nchr1\t400\t500\n", + ); + let set = IntervalSet::from_bed(&path).unwrap(); + assert_eq!( + set.on("chr1"), + &[ + Interval { + start: 100, + end: 300 + }, + Interval { + start: 400, + end: 500 + }, + ] + ); + assert_eq!(set.territory(), 300, "merged, not 100 + 150 + 100"); + } + + #[test] + fn touching_intervals_are_merged_too() { + let path = write_bed("touch.bed", "chr1\t100\t200\nchr1\t200\t300\n"); + let set = IntervalSet::from_bed(&path).unwrap(); + assert_eq!( + set.on("chr1"), + &[Interval { + start: 100, + end: 300 + }] + ); + } + + #[test] + fn comments_and_track_lines_are_ignored() { + let path = write_bed( + "comments.bed", + "# a comment\ntrack name=x\nchr1\t10\t20\n\nbrowser position chr1\n", + ); + let set = IntervalSet::from_bed(&path).unwrap(); + assert_eq!(set.len(), 1); + } + + #[test] + fn a_backwards_interval_is_an_error_rather_than_silently_empty() { + let path = write_bed("backwards.bed", "chr1\t200\t100\n"); + let result = IntervalSet::from_bed(&path); + assert!(result.is_err(), "an end before the start must be rejected"); + } + + #[test] + fn the_mask_marks_exactly_the_covered_bases() { + let path = write_bed("mask.bed", "chr1\t2\t5\n"); + let set = IntervalSet::from_bed(&path).unwrap(); + assert_eq!( + set.mask("chr1", 8), + vec![false, false, true, true, true, false, false, false] + ); + } + + #[test] + fn intervals_beyond_the_contig_end_do_not_overflow_the_mask() { + let path = write_bed("beyond.bed", "chr1\t2\t100\n"); + let set = IntervalSet::from_bed(&path).unwrap(); + assert_eq!(set.mask("chr1", 4), vec![false, false, true, true]); + } +} diff --git a/src/dna/mod.rs b/src/dna/mod.rs new file mode 100644 index 00000000..d924de1c --- /dev/null +++ b/src/dna/mod.rs @@ -0,0 +1,15 @@ +//! DNA quality control and analysis modules. +//! +//! Contains the depth of coverage engine and the mosdepth-compatible outputs +//! built on top of it. Read-level statistics, the samtools-compatible writers +//! and preseq are shared with the RNA pipeline and live in [`crate::common`]. + +pub mod depth; +pub mod gc_bias; +pub mod hs_metrics; +pub mod insert_size; +pub mod intervals; +pub mod mosdepth; +pub mod qualimap; +pub mod qualimap_output; +pub mod wgs_metrics; diff --git a/src/dna/mosdepth/mod.rs b/src/dna/mosdepth/mod.rs new file mode 100644 index 00000000..b8fe1f8f --- /dev/null +++ b/src/dna/mosdepth/mod.rs @@ -0,0 +1,427 @@ +//! mosdepth-compatible depth of coverage results. +//! +//! [`ContigDepth::from_depths`] turns one contig's per-base depth vector into +//! everything the six mosdepth outputs need, in a single pass over the vector, +//! so the depth vector can be dropped as soon as the contig is done. +//! +//! # Output formats +//! +//! These were derived from mosdepth 0.3.14 output committed under +//! `tests/expected/dna/`, not from documentation, and every rule below was +//! checked against every row of those fixtures. +//! +//! `{prefix}.mosdepth.summary.txt` carries the header +//! `chrom length bases mean min max`, one row per contig, then one +//! `{contig}_region` row per contig when windows were requested, then `total` +//! and `total_region`. `mean` is `bases / length` to two decimals. +//! +//! `{prefix}.mosdepth.global.dist.txt` and `.region.dist.txt` carry +//! `chrom depth proportion` rows in descending depth order, where `proportion` +//! is the fraction at depth **at or above** `depth`, formatted to two +//! decimals, ending at depth 0 with `1.00`. Which depths get a row is the +//! non-obvious part: +//! +//! - depths 0 through [`DIST_DENSE_MAX`] always get a row, even when no base +//! sits at that exact depth; +//! - above that, only depths that actually occur; +//! - the maximum observed depth never gets a row. +//! +//! The global distribution is over bases and their exact depth; the region +//! distribution is over windows and their **rounded** mean depth. + +use std::collections::BTreeMap; + +pub mod output; + +/// Highest depth that always gets a distribution row, matching the size of +/// mosdepth's internal fixed depth array. +pub const DIST_DENSE_MAX: u32 = 300; + +/// A run of consecutive bases sharing one depth, as written to `per-base.bed.gz`. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DepthRun { + /// Zero-based, inclusive start. + pub start: u64, + /// Zero-based, exclusive end. + pub end: u64, + /// Depth shared by every base in the run. + pub depth: u32, +} + +/// A fixed-width window and its mean depth, as written to `regions.bed.gz`. +#[derive(Debug, Clone, PartialEq)] +pub struct WindowDepth { + /// Zero-based, inclusive start. + pub start: u64, + /// Zero-based, exclusive end. + pub end: u64, + /// Mean depth over the window. + pub mean: f64, +} + +/// One window's per-threshold counts, as written to `thresholds.bed.gz`. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ThresholdRow { + /// Zero-based, inclusive start. + pub start: u64, + /// Zero-based, exclusive end. + pub end: u64, + /// Bases at or above each requested threshold, in the requested order. + pub counts: Vec, +} + +/// Everything the mosdepth outputs need about one contig. +#[derive(Debug, Clone)] +pub struct ContigDepth { + /// Contig name as it appears in the alignment header. + pub name: String, + /// Contig length in bases. + pub length: u64, + /// Sum of per-base depth over the contig. + pub total_bases: u64, + /// Lowest per-base depth seen. + pub min: u32, + /// Highest per-base depth seen. + pub max: u32, + /// Base count per exact depth. + pub histogram: BTreeMap, + /// Collapsed runs of equal depth. + pub runs: Vec, + /// Per-window mean depth; empty when no window size was requested. + pub windows: Vec, + /// Per-window threshold counts; empty when no thresholds were requested. + pub thresholds: Vec, +} + +impl ContigDepth { + /// Summarise one contig's per-base depths in a single pass. + pub fn from_depths( + name: &str, + depths: &[u32], + window_size: Option, + thresholds: &[u32], + ) -> Self { + let length = depths.len() as u64; + let mut histogram: BTreeMap = BTreeMap::new(); + let mut runs: Vec = Vec::new(); + let mut total_bases = 0u64; + + for (i, &depth) in depths.iter().enumerate() { + total_bases += u64::from(depth); + *histogram.entry(depth).or_insert(0) += 1; + match runs.last_mut() { + Some(run) if run.depth == depth => run.end = i as u64 + 1, + _ => runs.push(DepthRun { + start: i as u64, + end: i as u64 + 1, + depth, + }), + } + } + + let min = depths.iter().copied().min().unwrap_or(0); + let max = depths.iter().copied().max().unwrap_or(0); + + let (windows, threshold_rows) = match window_size { + Some(size) if size > 0 => Self::windowed(depths, u64::from(size), thresholds), + _ => (Vec::new(), Vec::new()), + }; + + Self { + name: name.to_string(), + length, + total_bases, + min, + max, + histogram, + runs, + windows, + thresholds: threshold_rows, + } + } + + /// Split the contig into fixed-width windows, computing each window's mean + /// depth and its per-threshold base counts. + fn windowed( + depths: &[u32], + size: u64, + thresholds: &[u32], + ) -> (Vec, Vec) { + let mut windows = Vec::new(); + let mut rows = Vec::new(); + for (index, chunk) in depths.chunks(size as usize).enumerate() { + let start = index as u64 * size; + let end = start + chunk.len() as u64; + let sum: u64 = chunk.iter().map(|d| u64::from(*d)).sum(); + windows.push(WindowDepth { + start, + end, + mean: sum as f64 / chunk.len() as f64, + }); + if !thresholds.is_empty() { + let counts = thresholds + .iter() + .map(|t| chunk.iter().filter(|d| *d >= t).count() as u64) + .collect(); + rows.push(ThresholdRow { start, end, counts }); + } + } + (windows, rows) + } + + /// Mean depth over the contig. + pub fn mean(&self) -> f64 { + if self.length == 0 { + 0.0 + } else { + self.total_bases as f64 / self.length as f64 + } + } + + /// Histogram of window mean depths, rounded to the nearest integer, which + /// is what the region distribution is built from. + pub fn region_histogram(&self) -> BTreeMap { + let mut hist = BTreeMap::new(); + for window in &self.windows { + let key = window.mean.round().max(0.0) as u32; + *hist.entry(key).or_insert(0) += 1; + } + hist + } +} + +/// The mosdepth result for one alignment file. +#[derive(Debug, Clone)] +pub struct MosdepthResult { + /// Per-contig results, in alignment-header order. + pub contigs: Vec, + /// Window size, when per-window output was requested. + pub window_size: Option, + /// Requested coverage thresholds, in the order they are reported. + pub thresholds: Vec, +} + +impl MosdepthResult { + /// Total length across all contigs. + pub fn total_length(&self) -> u64 { + self.contigs.iter().map(|c| c.length).sum() + } + + /// Total covered bases across all contigs. + pub fn total_bases(&self) -> u64 { + self.contigs.iter().map(|c| c.total_bases).sum() + } + + /// Mean depth across all contigs. + pub fn mean(&self) -> f64 { + let length = self.total_length(); + if length == 0 { + 0.0 + } else { + self.total_bases() as f64 / length as f64 + } + } + + /// Lowest depth across all contigs. + pub fn min(&self) -> u32 { + self.contigs.iter().map(|c| c.min).min().unwrap_or(0) + } + + /// Highest depth across all contigs. + pub fn max(&self) -> u32 { + self.contigs.iter().map(|c| c.max).max().unwrap_or(0) + } +} + +/// Merge histograms element-wise. +pub fn merge_histograms<'a>( + parts: impl IntoIterator>, +) -> BTreeMap { + let mut merged = BTreeMap::new(); + for part in parts { + for (depth, count) in part { + *merged.entry(*depth).or_insert(0) += count; + } + } + merged +} + +/// The depths that get a distribution row, in descending order. +/// +/// The rule was derived from the committed fixtures and holds for both +/// distribution files: every depth from 0 up to `min(DIST_DENSE_MAX, max)` +/// gets a row whether or not anything sits at it, and above +/// [`DIST_DENSE_MAX`] only depths that actually occur and lie strictly below +/// the maximum do. +/// +/// The consequence worth stating plainly: the maximum observed depth gets a +/// row when it falls inside the dense range and no row when it does not. On +/// the project fixture the global distribution tops out at 866 with a maximum +/// of 867, while the region distribution does emit its maximum of 204. +pub fn dist_rows(histogram: &BTreeMap) -> Vec { + let observed_max = histogram + .iter() + .filter(|(_, count)| **count > 0) + .map(|(depth, _)| *depth) + .max() + .unwrap_or(0); + + let mut depths: Vec = histogram + .iter() + .filter(|(depth, count)| **count > 0 && **depth > DIST_DENSE_MAX && **depth < observed_max) + .map(|(depth, _)| *depth) + .collect(); + depths.extend(0..=DIST_DENSE_MAX.min(observed_max)); + depths.sort_unstable_by(|a, b| b.cmp(a)); + depths.dedup(); + depths +} + +/// Cumulative proportion at or above each depth in `rows`, given `histogram` +/// and a total to divide by. +pub fn dist_proportions(histogram: &BTreeMap, rows: &[u32], total: u64) -> Vec { + if total == 0 { + return vec![0.0; rows.len()]; + } + rows.iter() + .map(|threshold| { + let at_or_above: u64 = histogram + .iter() + .filter(|(depth, _)| *depth >= threshold) + .map(|(_, count)| count) + .sum(); + at_or_above as f64 / total as f64 + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn runs_collapse_equal_neighbours() { + let c = ContigDepth::from_depths("chr1", &[0, 0, 0, 2, 2, 1], None, &[]); + assert_eq!( + c.runs, + vec![ + DepthRun { + start: 0, + end: 3, + depth: 0 + }, + DepthRun { + start: 3, + end: 5, + depth: 2 + }, + DepthRun { + start: 5, + end: 6, + depth: 1 + }, + ] + ); + } + + #[test] + fn summary_figures_are_computed_over_the_whole_contig() { + let c = ContigDepth::from_depths("chr1", &[0, 0, 3, 5], None, &[]); + assert_eq!(c.length, 4); + assert_eq!(c.total_bases, 8); + assert_eq!(c.min, 0); + assert_eq!(c.max, 5); + assert!((c.mean() - 2.0).abs() < 1e-12); + } + + #[test] + fn windows_cover_the_tail_even_when_shorter_than_the_window() { + let c = ContigDepth::from_depths("chr1", &[4, 4, 4, 4, 10], Some(4), &[]); + assert_eq!(c.windows.len(), 2); + assert_eq!( + c.windows[0], + WindowDepth { + start: 0, + end: 4, + mean: 4.0 + } + ); + assert_eq!( + c.windows[1], + WindowDepth { + start: 4, + end: 5, + mean: 10.0 + } + ); + } + + #[test] + fn threshold_counts_are_at_or_above_each_threshold() { + let c = ContigDepth::from_depths("chr1", &[0, 1, 5, 10], Some(4), &[1, 5, 20]); + assert_eq!(c.thresholds.len(), 1); + assert_eq!(c.thresholds[0].counts, vec![3, 2, 0]); + } + + #[test] + fn dist_rows_emit_a_maximum_that_falls_inside_the_dense_range() { + let mut hist = BTreeMap::new(); + hist.insert(0u32, 10u64); + hist.insert(204, 1); // the maximum, but below DIST_DENSE_MAX + let rows = dist_rows(&hist); + assert_eq!( + rows.first(), + Some(&204), + "a maximum inside the dense range is emitted" + ); + assert_eq!(rows.len(), 205, "0 through 204 inclusive"); + } + + #[test] + fn dist_rows_skip_a_maximum_above_the_dense_range() { + let mut hist = BTreeMap::new(); + hist.insert(0u32, 10u64); + hist.insert(5, 2); + hist.insert(400, 1); + hist.insert(500, 1); // the maximum, never emitted + let rows = dist_rows(&hist); + assert!(!rows.contains(&500), "the maximum depth gets no row"); + assert!( + rows.contains(&400), + "an observed depth above the dense range does" + ); + assert!( + rows.contains(&7), + "an unobserved depth inside the dense range does" + ); + assert!( + !rows.contains(&350), + "an unobserved depth above the dense range does not" + ); + assert_eq!(rows.first(), Some(&400), "descending order"); + assert_eq!(rows.last(), Some(&0), "down to zero"); + } + + #[test] + fn dist_proportions_are_cumulative_from_the_top() { + let mut hist = BTreeMap::new(); + hist.insert(0u32, 2u64); + hist.insert(1, 1); + hist.insert(3, 1); + let rows = vec![3u32, 2, 1, 0]; + let props = dist_proportions(&hist, &rows, 4); + assert!((props[0] - 0.25).abs() < 1e-12); + assert!((props[1] - 0.25).abs() < 1e-12); + assert!((props[2] - 0.50).abs() < 1e-12); + assert!((props[3] - 1.00).abs() < 1e-12); + } + + #[test] + fn region_histogram_rounds_window_means() { + let c = ContigDepth::from_depths("chr1", &[1, 2, 2, 3], Some(2), &[]); + // Windows: mean 1.5 rounds to 2, mean 2.5 rounds to 3 (away from zero). + let hist = c.region_histogram(); + assert_eq!(hist.get(&2), Some(&1)); + assert_eq!(hist.get(&3), Some(&1)); + } +} diff --git a/src/dna/mosdepth/output.rs b/src/dna/mosdepth/output.rs new file mode 100644 index 00000000..78e4ccb0 --- /dev/null +++ b/src/dna/mosdepth/output.rs @@ -0,0 +1,337 @@ +//! Writers for the six mosdepth-compatible output files. +//! +//! Formats are documented in the parent module. Compressed outputs are written +//! as bgzf, which is what mosdepth writes and what both `tabix` and `gunzip` +//! read. Parity against the fixtures is therefore asserted on the decompressed +//! bytes: two bgzf writers at the same level need not emit identical +//! compressed bytes, so comparing the `.gz` byte for byte would be testing the +//! compressor rather than this code. + +use std::io::Write; +use std::path::Path; + +use anyhow::{bail, Context, Result}; +use rust_htslib::bgzf; + +use super::{dist_proportions, dist_rows, merge_histograms, MosdepthResult}; + +/// Write `{prefix}.mosdepth.summary.txt`. +pub fn write_summary(result: &MosdepthResult, path: &Path) -> Result<()> { + let mut out = std::fs::File::create(path) + .map(std::io::BufWriter::new) + .with_context(|| format!("Failed to create summary file: {}", path.display()))?; + + writeln!(out, "chrom\tlength\tbases\tmean\tmin\tmax")?; + for contig in &result.contigs { + writeln!( + out, + "{}\t{}\t{}\t{:.2}\t{}\t{}", + contig.name, + contig.length, + contig.total_bases, + contig.mean(), + contig.min, + contig.max + )?; + if result.window_size.is_some() { + writeln!( + out, + "{}_region\t{}\t{}\t{:.2}\t{}\t{}", + contig.name, + contig.length, + contig.total_bases, + contig.mean(), + contig.min, + contig.max + )?; + } + } + writeln!( + out, + "total\t{}\t{}\t{:.2}\t{}\t{}", + result.total_length(), + result.total_bases(), + result.mean(), + result.min(), + result.max() + )?; + if result.window_size.is_some() { + writeln!( + out, + "total_region\t{}\t{}\t{:.2}\t{}\t{}", + result.total_length(), + result.total_bases(), + result.mean(), + result.min(), + result.max() + )?; + } + out.flush()?; + Ok(()) +} + +/// Write `{prefix}.mosdepth.global.dist.txt`, the distribution over bases. +pub fn write_global_dist(result: &MosdepthResult, path: &Path) -> Result<()> { + let per_contig: Vec<_> = result + .contigs + .iter() + .map(|c| (c.name.as_str(), c.histogram.clone(), c.length)) + .collect(); + write_dist(&per_contig, path) +} + +/// Write `{prefix}.mosdepth.region.dist.txt`, the distribution over windows +/// and their rounded mean depth. +pub fn write_region_dist(result: &MosdepthResult, path: &Path) -> Result<()> { + let per_contig: Vec<_> = result + .contigs + .iter() + .map(|c| { + let hist = c.region_histogram(); + let total = hist.values().sum::(); + (c.name.as_str(), hist, total) + }) + .collect(); + write_dist(&per_contig, path) +} + +/// Shared body of both distribution writers. +fn write_dist( + per_contig: &[(&str, std::collections::BTreeMap, u64)], + path: &Path, +) -> Result<()> { + let mut out = std::fs::File::create(path) + .map(std::io::BufWriter::new) + .with_context(|| format!("Failed to create distribution file: {}", path.display()))?; + + for (name, histogram, total) in per_contig { + let rows = dist_rows(histogram); + for (depth, proportion) in rows.iter().zip(dist_proportions(histogram, &rows, *total)) { + writeln!(out, "{name}\t{depth}\t{proportion:.2}")?; + } + } + + let merged = merge_histograms(per_contig.iter().map(|(_, h, _)| h)); + let total: u64 = per_contig.iter().map(|(_, _, t)| t).sum(); + let rows = dist_rows(&merged); + for (depth, proportion) in rows.iter().zip(dist_proportions(&merged, &rows, total)) { + writeln!(out, "total\t{depth}\t{proportion:.2}")?; + } + + out.flush()?; + Ok(()) +} + +/// Write `{prefix}.per-base.bed.gz`, one line per run of equal depth. +pub fn write_per_base(result: &MosdepthResult, path: &Path) -> Result<()> { + let mut lines = Vec::new(); + for contig in &result.contigs { + for run in &contig.runs { + lines.push(format!( + "{}\t{}\t{}\t{}\n", + contig.name, run.start, run.end, run.depth + )); + } + } + write_bgzf(path, &lines.concat()) +} + +/// Write `{prefix}.regions.bed.gz`, one line per window with its mean depth. +pub fn write_regions(result: &MosdepthResult, path: &Path) -> Result<()> { + let mut lines = Vec::new(); + for contig in &result.contigs { + for window in &contig.windows { + lines.push(format!( + "{}\t{}\t{}\t{:.2}\n", + contig.name, window.start, window.end, window.mean + )); + } + } + write_bgzf(path, &lines.concat()) +} + +/// Write `{prefix}.thresholds.bed.gz`, one line per window with the number of +/// bases at or above each requested threshold. +pub fn write_thresholds(result: &MosdepthResult, path: &Path) -> Result<()> { + let mut body = String::from("#chrom\tstart\tend\tregion"); + for threshold in &result.thresholds { + body.push_str(&format!("\t{threshold}X")); + } + body.push('\n'); + + for contig in &result.contigs { + for row in &contig.thresholds { + body.push_str(&format!( + "{}\t{}\t{}\tunknown", + contig.name, row.start, row.end + )); + for count in &row.counts { + body.push_str(&format!("\t{count}")); + } + body.push('\n'); + } + } + write_bgzf(path, &body) +} + +/// Write `contents` to `path` as bgzf, then build its `.csi` index. +fn write_bgzf(path: &Path, contents: &str) -> Result<()> { + { + let mut writer = bgzf::Writer::from_path(path) + .with_context(|| format!("Failed to create bgzf file: {}", path.display()))?; + writer + .write_all(contents.as_bytes()) + .with_context(|| format!("Failed to write bgzf file: {}", path.display()))?; + // The writer must be dropped, and the bgzf stream closed, before the + // indexer reads the file back. + } + build_csi_index(path) +} + +/// Build the `.csi` companion index for a bgzf-compressed BED file. +/// +/// mosdepth writes one alongside each of its BED outputs, and `tabix` needs it +/// to seek into them. CSI rather than TBI because CSI carries no 512 Mb +/// coordinate ceiling, which matters on large contigs. +fn build_csi_index(path: &Path) -> Result<()> { + use std::ffi::CString; + + let path_c = CString::new(path.as_os_str().as_encoded_bytes()).with_context(|| { + format!( + "Path is not representable as a C string: {}", + path.display() + ) + })?; + + // SAFETY: `path_c` is a valid NUL-terminated string that outlives the + // call, `tbx_conf_bed` is a static provided by htslib, and the file was + // closed above. A min_shift of 14 selects CSI, matching what mosdepth and + // `tabix --csi` produce. + let ret = unsafe { + rust_htslib::htslib::tbx_index_build( + path_c.as_ptr(), + 14, + &raw const rust_htslib::htslib::tbx_conf_bed, + ) + }; + if ret < 0 { + bail!("Failed to build the CSI index for {}", path.display()); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::dna::mosdepth::ContigDepth; + use std::io::Read; + + fn scratch(name: &str) -> std::path::PathBuf { + let dir = std::env::temp_dir().join("rustqc-mosdepth-tests"); + std::fs::create_dir_all(&dir).unwrap(); + dir.join(name) + } + + fn result_with_windows() -> MosdepthResult { + let depths = vec![0u32, 0, 2, 2, 4, 4]; + MosdepthResult { + contigs: vec![ContigDepth::from_depths("chr1", &depths, Some(3), &[1, 4])], + window_size: Some(3), + thresholds: vec![1, 4], + } + } + + fn read_bgzf(path: &std::path::Path) -> String { + let mut reader = bgzf::Reader::from_path(path).unwrap(); + let mut buf = Vec::new(); + reader.read_to_end(&mut buf).unwrap(); + String::from_utf8(buf).unwrap() + } + + #[test] + fn summary_has_region_rows_only_when_windows_were_requested() { + let path = scratch("summary_windows.txt"); + write_summary(&result_with_windows(), &path).unwrap(); + let text = std::fs::read_to_string(&path).unwrap(); + assert_eq!( + text, + "chrom\tlength\tbases\tmean\tmin\tmax\n\ + chr1\t6\t12\t2.00\t0\t4\n\ + chr1_region\t6\t12\t2.00\t0\t4\n\ + total\t6\t12\t2.00\t0\t4\n\ + total_region\t6\t12\t2.00\t0\t4\n" + ); + + let depths = vec![0u32, 0, 2, 2, 4, 4]; + let no_windows = MosdepthResult { + contigs: vec![ContigDepth::from_depths("chr1", &depths, None, &[])], + window_size: None, + thresholds: vec![], + }; + let path = scratch("summary_nowindows.txt"); + write_summary(&no_windows, &path).unwrap(); + let text = std::fs::read_to_string(&path).unwrap(); + assert!(!text.contains("_region"), "no windows means no region rows"); + } + + #[test] + fn per_base_writes_one_line_per_run() { + let path = scratch("per-base.bed.gz"); + write_per_base(&result_with_windows(), &path).unwrap(); + assert_eq!( + read_bgzf(&path), + "chr1\t0\t2\t0\nchr1\t2\t4\t2\nchr1\t4\t6\t4\n" + ); + } + + #[test] + fn regions_carry_two_decimal_means() { + let path = scratch("regions.bed.gz"); + write_regions(&result_with_windows(), &path).unwrap(); + assert_eq!(read_bgzf(&path), "chr1\t0\t3\t0.67\nchr1\t3\t6\t3.33\n"); + } + + #[test] + fn thresholds_carry_a_header_and_one_column_per_threshold() { + let path = scratch("thresholds.bed.gz"); + write_thresholds(&result_with_windows(), &path).unwrap(); + assert_eq!( + read_bgzf(&path), + "#chrom\tstart\tend\tregion\t1X\t4X\n\ + chr1\t0\t3\tunknown\t1\t0\n\ + chr1\t3\t6\tunknown\t3\t2\n" + ); + } + + #[test] + fn global_dist_is_descending_and_ends_at_one() { + let path = scratch("global.dist.txt"); + write_global_dist(&result_with_windows(), &path).unwrap(); + let text = std::fs::read_to_string(&path).unwrap(); + let chr1: Vec<&str> = text.lines().filter(|l| l.starts_with("chr1\t")).collect(); + assert_eq!( + *chr1.first().unwrap(), + "chr1\t4\t0.33", + "descending from the maximum" + ); + assert_eq!(*chr1.last().unwrap(), "chr1\t0\t1.00", "down to zero"); + assert_eq!( + chr1.len(), + 5, + "depths 4 down to 0, all inside the dense range" + ); + assert!(text.contains("total\t0\t1.00")); + } + + #[test] + fn compressed_outputs_get_a_loadable_csi_index() { + let path = scratch("indexed.per-base.bed.gz"); + let index = scratch("indexed.per-base.bed.gz.csi"); + let _ = std::fs::remove_file(&index); + write_per_base(&result_with_windows(), &path).unwrap(); + assert!(index.exists(), "the .csi companion index must be written"); + // htslib refuses to open a malformed index, so opening it is the check. + let tbx = rust_htslib::tbx::Reader::from_path(&path); + assert!(tbx.is_ok(), "htslib could not open the indexed file"); + } +} diff --git a/src/dna/qualimap.rs b/src/dna/qualimap.rs new file mode 100644 index 00000000..ae2e5905 --- /dev/null +++ b/src/dna/qualimap.rs @@ -0,0 +1,648 @@ +//! Qualimap `bamqc` reimplementation. +//! +//! # Upstream semantics +//! +//! Derived by reproducing Qualimap 2.3's own output on the project fixture +//! until each figure matched. Several rules are surprising and none of them +//! are guessable, so they are recorded here. +//! +//! **Windows.** The reference is split into `ceil(len / ceil(len / 400))` +//! windows, which is 397 windows of 101 bases on the 40001 base fixture, not +//! the round 400 the option name suggests. +//! +//! **Coverage.** Every primary mapped record contributes, with no duplicate, +//! mapping quality or base quality filtering and **no mate-overlap +//! correction**. Deletions count as covered. That is why Qualimap reports 16.77 +//! mean coverage where mosdepth reports 6.20 on the same file: they are +//! measuring different things, and neither is wrong. +//! +//! **Mapping quality.** The global figure is the mean of the per-window means, +//! where a window with no reads contributes zero. That is why it reads 2.4178 +//! rather than about 60. The per-position histogram truncates the mean rather +//! than rounding it. +//! +//! **Base composition.** Bases are counted in reference orientation, so +//! reverse-strand reads are reverse-complemented, but the clipped span that +//! selects which positions count is taken in *sequencing* orientation. Mixing +//! the two orientations is what Qualimap does; matching it means doing the +//! same. +//! +//! **Mismatches** are the `NM` tag less inserted bases only. Deleted bases are +//! not subtracted, which is what puts the fixture at 1350 rather than 1340. + +use rust_htslib::bam; +use rust_htslib::bam::record::{Aux, Cigar}; +use std::collections::BTreeMap; + +use crate::common::bam_flags::*; + +/// Qualimap's default target number of windows. +pub const DEFAULT_NUM_WINDOWS: usize = 400; + +/// Highest coverage level reported in the genome fraction table. +const MAX_FRACTION_LEVEL: u32 = 51; + +/// Per-contig accumulation for one alignment file. +#[derive(Debug)] +pub struct QualimapAccum { + contig: String, + length: u64, + window_size: u64, + /// Coverage per reference base, counting `M`, `=`, `X` and `D`. + coverage: Vec, + /// Sum of mapping quality over the reads covering each base. + mapq_sum: Vec, + /// Per-window sum of insert sizes and the number of reads contributing. + insert_window_sum: Vec, + insert_window_count: Vec, + counters: QualimapCounters, +} + +/// Read-level counters, summed across contigs. +#[derive(Debug, Clone, Default)] +pub struct QualimapCounters { + /// Records seen, secondary alignments excluded and counted separately. + pub reads: u64, + /// Secondary alignments. + pub secondary: u64, + /// Mapped records. + pub mapped: u64, + /// Duplicate-flagged records. + pub duplicates: u64, + /// Mapped first-in-pair records with a mapped mate. + pub paired_first: u64, + /// Mapped second-in-pair records with a mapped mate. + pub paired_second: u64, + /// Mapped paired records whose mate is also mapped. + pub paired_both: u64, + /// Mapped paired records whose mate is not mapped. + pub singletons: u64, + /// Reference-consuming aligned bases, `M`, `=` and `X`. + pub sequenced_bases: u64, + /// Those plus deleted bases. + pub mapped_bases: u64, + /// Sum of the `NM` tag over mapped records. + pub edit_distance: u64, + /// Inserted bases. + pub insertions: u64, + /// Deleted bases. + pub deletions: u64, + /// Records carrying at least one insertion. + pub reads_with_insertion: u64, + /// Records carrying at least one deletion. + pub reads_with_deletion: u64, + /// Base composition in reference orientation, indexed by [`base_index`]. + pub base_counts: [u64; 5], + /// Insert size histogram over positive `TLEN` values. + pub insert_sizes: BTreeMap, + /// Per read position base composition, in reference orientation. + pub nucleotide_by_position: Vec<[u64; 5]>, + /// Per read position count of clipped bases. + pub clipping_by_position: Vec, + /// Total clipped bases, the denominator of the clipping profile. + pub clipped_bases: u64, + /// Homopolymer indel counts, indexed by [`base_index`], plus non-polymer. + pub homopolymer_indels: [u64; 5], + /// Indels not adjacent to a homopolymer run. + pub non_polymer_indels: u64, +} + +/// Index of a base in the fixed `A, C, G, T, N` ordering. +fn base_index(base: u8) -> usize { + match base.to_ascii_uppercase() { + b'A' => 0, + b'C' => 1, + b'G' => 2, + b'T' => 3, + _ => 4, + } +} + +/// The complement of a base, leaving anything unrecognised alone. +fn complement(base: u8) -> u8 { + match base.to_ascii_uppercase() { + b'A' => b'T', + b'C' => b'G', + b'G' => b'C', + b'T' => b'A', + other => other, + } +} + +impl QualimapCounters { + /// Add another contig's counters. + pub fn merge(&mut self, other: &QualimapCounters) { + self.reads += other.reads; + self.secondary += other.secondary; + self.mapped += other.mapped; + self.duplicates += other.duplicates; + self.paired_first += other.paired_first; + self.paired_second += other.paired_second; + self.paired_both += other.paired_both; + self.singletons += other.singletons; + self.sequenced_bases += other.sequenced_bases; + self.mapped_bases += other.mapped_bases; + self.edit_distance += other.edit_distance; + self.insertions += other.insertions; + self.deletions += other.deletions; + self.reads_with_insertion += other.reads_with_insertion; + self.reads_with_deletion += other.reads_with_deletion; + self.clipped_bases += other.clipped_bases; + self.non_polymer_indels += other.non_polymer_indels; + for (target, source) in self.base_counts.iter_mut().zip(&other.base_counts) { + *target += source; + } + for (target, source) in self + .homopolymer_indels + .iter_mut() + .zip(&other.homopolymer_indels) + { + *target += source; + } + for (size, count) in &other.insert_sizes { + *self.insert_sizes.entry(*size).or_insert(0) += count; + } + if self.nucleotide_by_position.len() < other.nucleotide_by_position.len() { + self.nucleotide_by_position + .resize(other.nucleotide_by_position.len(), [0; 5]); + } + for (position, counts) in other.nucleotide_by_position.iter().enumerate() { + for (target, source) in self.nucleotide_by_position[position].iter_mut().zip(counts) { + *target += source; + } + } + if self.clipping_by_position.len() < other.clipping_by_position.len() { + self.clipping_by_position + .resize(other.clipping_by_position.len(), 0); + } + for (position, count) in other.clipping_by_position.iter().enumerate() { + self.clipping_by_position[position] += count; + } + } + + /// Mismatches, which Qualimap takes as `NM` less inserted bases only. + pub fn mismatches(&self) -> u64 { + self.edit_distance.saturating_sub(self.insertions) + } + + /// Mismatches, insertions and deletions over sequenced bases. + pub fn general_error_rate(&self) -> f64 { + if self.sequenced_bases == 0 { + return 0.0; + } + (self.mismatches() + self.insertions + self.deletions) as f64 / self.sequenced_bases as f64 + } + + /// Fraction of indels adjacent to a homopolymer run. + pub fn homopolymer_fraction(&self) -> f64 { + let poly: u64 = self.homopolymer_indels.iter().sum(); + let total = poly + self.non_polymer_indels; + if total == 0 { + 0.0 + } else { + poly as f64 / total as f64 + } + } + + /// Mean, population standard deviation and median insert size. + pub fn insert_size_stats(&self) -> (f64, f64, u64) { + let n: u64 = self.insert_sizes.values().sum(); + if n == 0 { + return (0.0, 0.0, 0); + } + let mean = self + .insert_sizes + .iter() + .map(|(size, count)| *size as f64 * *count as f64) + .sum::() + / n as f64; + let variance = self + .insert_sizes + .iter() + .map(|(size, count)| { + let diff = *size as f64 - mean; + diff * diff * *count as f64 + }) + .sum::() + / n as f64; + let mut seen = 0u64; + let mut median = 0u64; + for (size, count) in &self.insert_sizes { + seen += count; + if seen > n / 2 { + median = *size; + break; + } + } + (mean, variance.sqrt(), median) + } +} + +impl QualimapAccum { + /// Prepare for one contig, splitting it into Qualimap's window grid. + pub fn new(contig: &str, length: u64, num_windows: usize) -> Self { + let window_size = length.div_ceil(num_windows as u64).max(1); + let windows = length.div_ceil(window_size) as usize; + Self { + contig: contig.to_string(), + length, + window_size, + coverage: vec![0; length as usize], + mapq_sum: vec![0; length as usize], + insert_window_sum: vec![0; windows], + insert_window_count: vec![0; windows], + counters: QualimapCounters::default(), + } + } + + /// Number of windows this contig is split into. + pub fn window_count(&self) -> usize { + self.insert_window_sum.len() + } + + /// Width of each window; the last one may be shorter. + pub fn window_size(&self) -> u64 { + self.window_size + } + + /// Offer one record. + pub fn process_read(&mut self, record: &bam::Record) { + let flags = record.flags(); + if flags & BAM_FSECONDARY != 0 { + self.counters.secondary += 1; + return; + } + self.counters.reads += 1; + if flags & BAM_FUNMAP != 0 { + return; + } + self.counters.mapped += 1; + if flags & BAM_FDUP != 0 { + self.counters.duplicates += 1; + } + + if flags & BAM_FPAIRED != 0 { + if flags & BAM_FMUNMAP != 0 { + self.counters.singletons += 1; + } else { + self.counters.paired_both += 1; + if flags & BAM_FREAD1 != 0 { + self.counters.paired_first += 1; + } + if flags & BAM_FREAD2 != 0 { + self.counters.paired_second += 1; + } + } + } + + let mapq = u64::from(record.mapq()); + let sequence = record.seq().as_bytes(); + let reverse = flags & BAM_FREVERSE != 0; + + // Bases in reference orientation: reverse-complemented for a + // reverse-strand read. + let oriented: Vec = if reverse { + sequence.iter().rev().map(|b| complement(*b)).collect() + } else { + sequence.clone() + }; + + let cigar = record.cigar(); + let ops: Vec = cigar.iter().copied().collect(); + + // The clipped span is taken in sequencing orientation, unlike the + // bases. That asymmetry is Qualimap's, and reproducing it is the only + // way the composition figures agree. + let leading_clip = match ops.first() { + Some(Cigar::SoftClip(n)) | Some(Cigar::HardClip(n)) => *n as usize, + _ => 0, + }; + let trailing_clip = match ops.last() { + Some(Cigar::SoftClip(n)) | Some(Cigar::HardClip(n)) => *n as usize, + _ => 0, + }; + + let read_len = sequence.len(); + if self.counters.nucleotide_by_position.len() < read_len { + self.counters + .nucleotide_by_position + .resize(read_len, [0; 5]); + self.counters.clipping_by_position.resize(read_len, 0); + } + for position in 0..leading_clip.min(read_len) { + self.counters.clipping_by_position[position] += 1; + self.counters.clipped_bases += 1; + } + for offset in 0..trailing_clip.min(read_len) { + let position = read_len - 1 - offset; + self.counters.clipping_by_position[position] += 1; + self.counters.clipped_bases += 1; + } + for position in leading_clip..read_len.saturating_sub(trailing_clip) { + let base = oriented.get(position).copied().unwrap_or(b'N'); + self.counters.nucleotide_by_position[position][base_index(base)] += 1; + } + + if let Ok(Aux::U8(nm)) = record.aux(b"NM") { + self.counters.edit_distance += u64::from(nm); + } else if let Ok(Aux::U16(nm)) = record.aux(b"NM") { + self.counters.edit_distance += u64::from(nm); + } else if let Ok(Aux::U32(nm)) = record.aux(b"NM") { + self.counters.edit_distance += u64::from(nm); + } else if let Ok(Aux::I32(nm)) = record.aux(b"NM") { + self.counters.edit_distance += nm.max(0) as u64; + } + + let mut reference_position = record.pos(); + let mut query_position = 0usize; + let mut had_insertion = false; + let mut had_deletion = false; + + for op in &ops { + match op { + Cigar::Match(n) | Cigar::Equal(n) | Cigar::Diff(n) => { + let n = *n as usize; + for k in 0..n { + let position = reference_position + k as i64; + if position >= 0 && (position as usize) < self.coverage.len() { + self.coverage[position as usize] += 1; + self.mapq_sum[position as usize] += mapq; + } + let base = oriented.get(query_position + k).copied().unwrap_or(b'N'); + self.counters.base_counts[base_index(base)] += 1; + } + self.counters.sequenced_bases += n as u64; + self.counters.mapped_bases += n as u64; + reference_position += n as i64; + query_position += n; + } + Cigar::Del(n) => { + let n = *n as usize; + for k in 0..n { + let position = reference_position + k as i64; + if position >= 0 && (position as usize) < self.coverage.len() { + self.coverage[position as usize] += 1; + self.mapq_sum[position as usize] += mapq; + } + } + self.counters.mapped_bases += n as u64; + self.counters.deletions += n as u64; + had_deletion = true; + self.classify_indel(&oriented, query_position); + reference_position += n as i64; + } + Cigar::Ins(n) => { + self.counters.insertions += u64::from(*n); + had_insertion = true; + self.classify_indel(&oriented, query_position); + query_position += *n as usize; + } + Cigar::RefSkip(n) => reference_position += i64::from(*n), + Cigar::SoftClip(n) => query_position += *n as usize, + Cigar::HardClip(_) | Cigar::Pad(_) => {} + } + } + if had_insertion { + self.counters.reads_with_insertion += 1; + } + if had_deletion { + self.counters.reads_with_deletion += 1; + } + + let insert_size = record.insert_size(); + if insert_size > 0 { + *self + .counters + .insert_sizes + .entry(insert_size as u64) + .or_insert(0) += 1; + let window = (record.pos().max(0) as u64 / self.window_size) as usize; + if window < self.insert_window_sum.len() { + self.insert_window_sum[window] += insert_size; + self.insert_window_count[window] += 1; + } + } + } + + /// Charge an indel to a homopolymer bucket when the bases either side of + /// it repeat, and to the non-polymer bucket otherwise. + fn classify_indel(&mut self, oriented: &[u8], query_position: usize) { + const RUN: usize = 4; + let start = query_position.saturating_sub(RUN); + let window = &oriented[start..query_position.min(oriented.len())]; + if window.len() == RUN && window.iter().all(|b| *b == window[0]) { + self.counters.homopolymer_indels[base_index(window[0])] += 1; + } else { + self.counters.non_polymer_indels += 1; + } + } + + /// Consume the accumulator into its per-contig result. + pub fn into_result(self) -> ContigQualimap { + let window_size = self.window_size; + let windows = self.insert_window_sum.len(); + let mut window_coverage = Vec::with_capacity(windows); + let mut window_coverage_sd = Vec::with_capacity(windows); + let mut window_mapq = Vec::with_capacity(windows); + let mut window_insert = Vec::with_capacity(windows); + let mut midpoints = Vec::with_capacity(windows); + + for window in 0..windows { + let start = window as u64 * window_size; + let end = ((window as u64 + 1) * window_size).min(self.length); + let span = &self.coverage[start as usize..end as usize]; + let mapq_span = &self.mapq_sum[start as usize..end as usize]; + + let mean = span.iter().map(|c| f64::from(*c)).sum::() / span.len() as f64; + let variance = span + .iter() + .map(|c| { + let diff = f64::from(*c) - mean; + diff * diff + }) + .sum::() + / span.len() as f64; + let covered: u64 = span.iter().map(|c| u64::from(*c)).sum(); + let mapq_total: u64 = mapq_span.iter().sum(); + + window_coverage.push(mean); + window_coverage_sd.push(variance.sqrt()); + window_mapq.push(if covered == 0 { + 0.0 + } else { + mapq_total as f64 / covered as f64 + }); + window_insert.push(if self.insert_window_count[window] == 0 { + 0.0 + } else { + self.insert_window_sum[window] as f64 / self.insert_window_count[window] as f64 + }); + midpoints.push((start + end + 1) as f64 / 2.0); + } + + let mut coverage_histogram: BTreeMap = BTreeMap::new(); + let mut mapq_histogram: BTreeMap = BTreeMap::new(); + for (position, depth) in self.coverage.iter().enumerate() { + *coverage_histogram.entry(*depth).or_insert(0) += 1; + if *depth > 0 { + // Truncated, not rounded: this is what Qualimap does. + let mean = self.mapq_sum[position] / u64::from(*depth); + *mapq_histogram.entry(mean as u32).or_insert(0) += 1; + } + } + + ContigQualimap { + name: self.contig, + length: self.length, + coverage: self.coverage, + window_size, + midpoints, + window_coverage, + window_coverage_sd, + window_mapq, + window_insert, + coverage_histogram, + mapq_histogram, + counters: self.counters, + } + } +} + +/// One contig's Qualimap result. +#[derive(Debug, Clone)] +pub struct ContigQualimap { + /// Contig name. + pub name: String, + /// Contig length. + pub length: u64, + /// Per-base coverage. + pub coverage: Vec, + /// Window width. + pub window_size: u64, + /// Window midpoints, as Qualimap reports positions. + pub midpoints: Vec, + /// Mean coverage per window. + pub window_coverage: Vec, + /// Coverage standard deviation per window. + pub window_coverage_sd: Vec, + /// Mean mapping quality per window, zero where uncovered. + pub window_mapq: Vec, + /// Mean insert size per window. + pub window_insert: Vec, + /// Bases at each exact coverage. + pub coverage_histogram: BTreeMap, + /// Covered bases at each truncated mean mapping quality. + pub mapq_histogram: BTreeMap, + /// Read-level counters gathered on this contig. + pub counters: QualimapCounters, +} + +impl ContigQualimap { + /// Mean coverage over the contig. + pub fn mean_coverage(&self) -> f64 { + if self.length == 0 { + 0.0 + } else { + self.coverage.iter().map(|c| f64::from(*c)).sum::() / self.length as f64 + } + } + + /// Population standard deviation of per-base coverage. + pub fn coverage_sd(&self) -> f64 { + if self.length == 0 { + return 0.0; + } + let mean = self.mean_coverage(); + let variance = self + .coverage + .iter() + .map(|c| { + let diff = f64::from(*c) - mean; + diff * diff + }) + .sum::() + / self.length as f64; + variance.sqrt() + } + + /// Mean of the per-window mapping qualities, uncovered windows included. + pub fn mean_mapping_quality(&self) -> f64 { + if self.window_mapq.is_empty() { + 0.0 + } else { + self.window_mapq.iter().sum::() / self.window_mapq.len() as f64 + } + } + + /// Percentage of the contig at or above each coverage level. + pub fn genome_fraction(&self) -> Vec<(u32, f64)> { + (1..=MAX_FRACTION_LEVEL) + .map(|level| { + let at_or_above = self.coverage.iter().filter(|c| **c >= level).count(); + (level, 100.0 * at_or_above as f64 / self.length as f64) + }) + .collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn window_grid_matches_qualimaps_arithmetic() { + // 40001 bases into 400 windows: 101 bases each, and 397 of them. + let accum = QualimapAccum::new("chr22", 40001, DEFAULT_NUM_WINDOWS); + assert_eq!(accum.window_size(), 101); + assert_eq!(accum.window_count(), 397); + } + + #[test] + fn a_short_contig_still_gets_one_window() { + let accum = QualimapAccum::new("small", 10, DEFAULT_NUM_WINDOWS); + assert_eq!(accum.window_size(), 1); + assert_eq!(accum.window_count(), 10); + } + + #[test] + fn mismatches_subtract_insertions_but_not_deletions() { + let mut counters = QualimapCounters { + edit_distance: 1352, + insertions: 2, + deletions: 10, + ..Default::default() + }; + assert_eq!(counters.mismatches(), 1350, "deletions are not subtracted"); + counters.deletions = 0; + assert_eq!(counters.mismatches(), 1350); + } + + #[test] + fn insert_size_statistics_use_the_population_denominator() { + let mut counters = QualimapCounters::default(); + for size in [1u64, 2, 3] { + counters.insert_sizes.insert(size, 1); + } + let (mean, sd, median) = counters.insert_size_stats(); + assert!((mean - 2.0).abs() < 1e-12); + // Population variance of 1, 2, 3 is 2/3. + assert!((sd - (2.0f64 / 3.0).sqrt()).abs() < 1e-12, "got {sd}"); + assert_eq!(median, 2); + } + + #[test] + fn base_indexing_folds_anything_unknown_into_n() { + assert_eq!(base_index(b'A'), 0); + assert_eq!(base_index(b'c'), 1); + assert_eq!(base_index(b'N'), 4); + assert_eq!(base_index(b'R'), 4, "ambiguity codes are counted as N"); + } + + #[test] + fn complement_leaves_unknown_bases_alone() { + assert_eq!(complement(b'A'), b'T'); + assert_eq!(complement(b'g'), b'C'); + assert_eq!(complement(b'N'), b'N'); + assert_eq!(complement(b'R'), b'R'); + } +} diff --git a/src/dna/qualimap_output.rs b/src/dna/qualimap_output.rs new file mode 100644 index 00000000..31e09978 --- /dev/null +++ b/src/dna/qualimap_output.rs @@ -0,0 +1,703 @@ +//! Writers for the Qualimap `bamqc` outputs. +//! +//! The formats are reproduced from Qualimap 2.3's own output. Two details are +//! easy to miss: integers carry thousands separators, and the "Mismatches and +//! indels" section is indented by four spaces where every other section uses +//! five. +//! +//! # Figures that do not match exactly +//! +//! - `mean mapping quality` differs in the fourth decimal, 2.4179 against +//! 2.4178 on the project fixture. It is the mean of the per-window means; +//! 393 of the 397 windows match exactly and the four that do not differ by +//! at most 0.053, which is consistent with Qualimap accumulating them +//! differently at window boundaries. +//! - `std coverageData` differs in the fourth decimal, 154.9340 against +//! 154.9323, for the same reason. +//! - `homopolymer indels` is computed here as an indel flanked by a run of +//! four identical bases. Qualimap's own definition was not recovered: no +//! combination of run length from two to five, read orientation or direction +//! reproduces its split of 7 homopolymer against 5 other indels, so this +//! figure differs. +//! - The coverage histogram differs in 10 bins of roughly 590, always by one +//! base and always between adjacent bins, so about five reference positions +//! out of 40001 sit one deeper here than in Qualimap. That carries into the +//! `coverageData >= NX` lines, which agree to within 0.003 percentage +//! points. + +use std::io::Write; +use std::path::Path; + +use anyhow::{Context, Result}; + +use super::qualimap::ContigQualimap; + +/// Format an integer with thousands separators, as Qualimap does. +fn thousands(value: u64) -> String { + let digits = value.to_string(); + let mut out = String::with_capacity(digits.len() + digits.len() / 3); + for (i, c) in digits.chars().enumerate() { + if i > 0 && (digits.len() - i).is_multiple_of(3) { + out.push(','); + } + out.push(c); + } + out +} + +/// Format a percentage rounded to `places` decimals, trailing zeros removed. +fn trimmed(value: f64, places: usize) -> String { + let text = format!("{value:.places$}"); + if text.contains('.') { + text.trim_end_matches('0').trim_end_matches('.').to_string() + } else { + text + } +} + +/// Percentage of `part` in `whole`, guarding against an empty denominator. +fn pct(part: u64, whole: u64) -> f64 { + if whole == 0 { + 0.0 + } else { + // Divide before multiplying, as Qualimap does: the other order moves + // the last two digits of the printed double. + part as f64 / whole as f64 * 100.0 + } +} + +/// Format a double the way Java's `Double.toString` does, which is what +/// Qualimap's tables carry: the shortest representation that round-trips, but +/// always with at least one digit after the point, so `0` is written `0.0`. +fn java_double(value: f64) -> String { + let text = format!("{value}"); + if text.contains('.') || text.contains('e') || text.contains("NaN") || text.contains("inf") { + text + } else { + format!("{text}.0") + } +} + +/// Write `genome_results.txt`. +pub fn write_genome_results( + contigs: &[ContigQualimap], + bam_path: &str, + outfile: &Path, +) -> Result<()> { + let mut out = std::fs::File::create(outfile) + .map(std::io::BufWriter::new) + .with_context(|| format!("Failed to create genome results: {}", outfile.display()))?; + + let total_length: u64 = contigs.iter().map(|c| c.length).sum(); + let mut counters = super::qualimap::QualimapCounters::default(); + for contig in contigs { + counters.merge(&contig.counters); + } + let windows: usize = contigs.iter().map(|c| c.midpoints.len()).sum(); + + writeln!(out, "BamQC report")?; + writeln!(out, "-----------------------------------")?; + writeln!(out)?; + writeln!(out, ">>>>>>> Input")?; + writeln!(out)?; + writeln!(out, " bam file = {bam_path}")?; + writeln!(out, " outfile = {}", outfile.display())?; + writeln!(out)?; + writeln!(out)?; + + writeln!(out, ">>>>>>> Reference")?; + writeln!(out)?; + writeln!(out, " number of bases = {} bp", thousands(total_length))?; + writeln!(out, " number of contigs = {}", contigs.len())?; + writeln!(out)?; + writeln!(out)?; + + writeln!(out, ">>>>>>> Globals")?; + writeln!(out)?; + writeln!(out, " number of windows = {windows}")?; + writeln!(out)?; + writeln!(out, " number of reads = {}", thousands(counters.reads))?; + writeln!( + out, + " number of mapped reads = {} ({}%)", + thousands(counters.mapped), + trimmed(pct(counters.mapped, counters.reads), 2) + )?; + writeln!( + out, + " number of secondary alignments = {}", + thousands(counters.secondary) + )?; + writeln!(out)?; + writeln!( + out, + " number of mapped paired reads (first in pair) = {}", + thousands(counters.paired_first) + )?; + writeln!( + out, + " number of mapped paired reads (second in pair) = {}", + thousands(counters.paired_second) + )?; + writeln!( + out, + " number of mapped paired reads (both in pair) = {}", + thousands(counters.paired_both) + )?; + writeln!( + out, + " number of mapped paired reads (singletons) = {}", + thousands(counters.singletons) + )?; + writeln!(out)?; + writeln!( + out, + " number of mapped bases = {} bp", + thousands(counters.mapped_bases) + )?; + writeln!( + out, + " number of sequenced bases = {} bp", + thousands(counters.sequenced_bases) + )?; + // Qualimap reports this only when run with a reference; without one it is + // zero, which is what RustQC always is here. + writeln!(out, " number of aligned bases = 0 bp")?; + writeln!( + out, + " number of duplicated reads (flagged) = {}", + thousands(counters.duplicates) + )?; + writeln!(out)?; + writeln!(out)?; + + let (insert_mean, insert_sd, insert_median) = counters.insert_size_stats(); + writeln!(out, ">>>>>>> Insert size")?; + writeln!(out)?; + writeln!(out, " mean insert size = {insert_mean:.4}")?; + writeln!(out, " std insert size = {insert_sd:.4}")?; + writeln!(out, " median insert size = {insert_median}")?; + writeln!(out)?; + writeln!(out)?; + + let mean_mapq = if contigs.is_empty() { + 0.0 + } else { + contigs + .iter() + .map(|c| c.mean_mapping_quality()) + .sum::() + / contigs.len() as f64 + }; + writeln!(out, ">>>>>>> Mapping quality")?; + writeln!(out)?; + writeln!(out, " mean mapping quality = {mean_mapq:.4}")?; + writeln!(out)?; + writeln!(out)?; + + let bases: u64 = counters.base_counts.iter().sum(); + writeln!(out, ">>>>>>> ACTG content")?; + writeln!(out)?; + for (label, index) in [("A", 0), ("C", 1), ("T", 3), ("G", 2), ("N", 4)] { + writeln!( + out, + " number of {label}'s = {} bp ({}%)", + thousands(counters.base_counts[index]), + trimmed(pct(counters.base_counts[index], bases), 2) + )?; + } + writeln!(out)?; + let gc = counters.base_counts[1] + counters.base_counts[2]; + writeln!(out, " GC percentage = {}%", trimmed(pct(gc, bases), 2))?; + writeln!(out)?; + writeln!(out)?; + + // Note the four-space indent: this section is the odd one out. + writeln!(out, ">>>>>>> Mismatches and indels")?; + writeln!(out)?; + writeln!( + out, + " general error rate = {}", + trimmed(counters.general_error_rate(), 4) + )?; + writeln!( + out, + " number of mismatches = {}", + thousands(counters.mismatches()) + )?; + writeln!( + out, + " number of insertions = {}", + thousands(counters.insertions) + )?; + writeln!( + out, + " mapped reads with insertion percentage = {}%", + trimmed(pct(counters.reads_with_insertion, counters.mapped), 2) + )?; + writeln!( + out, + " number of deletions = {}", + thousands(counters.deletions) + )?; + writeln!( + out, + " mapped reads with deletion percentage = {}%", + trimmed(pct(counters.reads_with_deletion, counters.mapped), 2) + )?; + writeln!( + out, + " homopolymer indels = {}%", + trimmed(100.0 * counters.homopolymer_fraction(), 2) + )?; + writeln!(out)?; + writeln!(out)?; + + let mean_coverage = if total_length == 0 { + 0.0 + } else { + contigs + .iter() + .map(|c| c.coverage.iter().map(|d| f64::from(*d)).sum::()) + .sum::() + / total_length as f64 + }; + let coverage_sd = { + let variance = contigs + .iter() + .flat_map(|c| c.coverage.iter()) + .map(|d| { + let diff = f64::from(*d) - mean_coverage; + diff * diff + }) + .sum::() + / total_length.max(1) as f64; + variance.sqrt() + }; + + writeln!(out, ">>>>>>> Coverage")?; + writeln!(out)?; + writeln!(out, " mean coverageData = {mean_coverage:.4}X")?; + writeln!(out, " std coverageData = {coverage_sd:.4}X")?; + writeln!(out)?; + for (level, fraction) in genome_fraction(contigs, total_length) { + writeln!( + out, + " There is a {}% of reference with a coverageData >= {level}X", + trimmed(fraction, 2) + )?; + } + writeln!(out)?; + writeln!(out)?; + + writeln!(out, ">>>>>>> Coverage per contig")?; + writeln!(out)?; + for contig in contigs { + let covered: u64 = contig.coverage.iter().map(|d| u64::from(*d)).sum(); + writeln!( + out, + "\t{}\t{}\t{}\t{}\t{}", + contig.name, + contig.length, + covered, + contig.mean_coverage(), + contig.coverage_sd() + )?; + } + writeln!(out)?; + writeln!(out)?; + + out.flush()?; + Ok(()) +} + +/// Percentage of the whole reference at or above each level from 1 to 51. +fn genome_fraction(contigs: &[ContigQualimap], total_length: u64) -> Vec<(u32, f64)> { + (1..=51) + .map(|level| { + let at_or_above: u64 = contigs + .iter() + .map(|c| c.coverage.iter().filter(|d| **d >= level).count() as u64) + .sum(); + (level, pct(at_or_above, total_length)) + }) + .collect() +} + +/// Write the twelve `raw_data_qualimapReport` tables RustQC reproduces. +/// +/// Two of Qualimap's tables are not written: its GC content distribution is +/// computed over a 679-read subsample whose selection rule is not documented +/// and could not be recovered from the output, and its duplication rate +/// histogram uses a definition that does not match a read-start-position +/// count. Emitting a table under the same name with different numbers would be +/// worse than leaving it out. +pub fn write_raw_data(contigs: &[ContigQualimap], dir: &Path) -> Result<()> { + std::fs::create_dir_all(dir) + .with_context(|| format!("Failed to create raw data directory: {}", dir.display()))?; + + let mut counters = super::qualimap::QualimapCounters::default(); + for contig in contigs { + counters.merge(&contig.counters); + } + + // Per-window tables, positions given as window midpoints. + table( + dir, + "coverage_across_reference.txt", + "#Position (bp)\tCoverage\tStd", + |out| { + for contig in contigs { + for i in 0..contig.midpoints.len() { + writeln!( + out, + "{}\t{}\t{}", + java_double(contig.midpoints[i]), + java_double(contig.window_coverage[i]), + java_double(contig.window_coverage_sd[i]) + )?; + } + } + Ok(()) + }, + )?; + + table( + dir, + "mapping_quality_across_reference.txt", + "#Position (bp)\tmapping quality", + |out| { + for contig in contigs { + for i in 0..contig.midpoints.len() { + writeln!( + out, + "{}\t{}", + java_double(contig.midpoints[i]), + java_double(contig.window_mapq[i]) + )?; + } + } + Ok(()) + }, + )?; + + table( + dir, + "insert_size_across_reference.txt", + "#Position (bp)\tinsert size", + |out| { + for contig in contigs { + for i in 0..contig.midpoints.len() { + writeln!( + out, + "{}\t{}", + java_double(contig.midpoints[i]), + java_double(contig.window_insert[i]) + )?; + } + } + Ok(()) + }, + )?; + + // Histograms. + let mut coverage_histogram = std::collections::BTreeMap::new(); + let mut mapq_histogram = std::collections::BTreeMap::new(); + for contig in contigs { + for (depth, count) in &contig.coverage_histogram { + *coverage_histogram.entry(*depth).or_insert(0u64) += count; + } + for (quality, count) in &contig.mapq_histogram { + *mapq_histogram.entry(*quality).or_insert(0u64) += count; + } + } + + table( + dir, + "coverage_histogram.txt", + "#Coverage\tNumber of genomic locations", + |out| { + for (depth, count) in &coverage_histogram { + writeln!( + out, + "{}\t{}", + java_double(*depth as f64), + java_double(*count as f64) + )?; + } + Ok(()) + }, + )?; + + table( + dir, + "mapping_quality_histogram.txt", + "#Mapping quality\tmapping quality", + |out| { + for (quality, count) in &mapq_histogram { + writeln!( + out, + "{}\t{}", + java_double(*quality as f64), + java_double(*count as f64) + )?; + } + Ok(()) + }, + )?; + + table( + dir, + "insert_size_histogram.txt", + "#Insert size (bp)\tinsert size", + |out| { + for (size, count) in &counters.insert_sizes { + writeln!( + out, + "{}\t{}", + java_double(*size as f64), + java_double(*count as f64) + )?; + } + Ok(()) + }, + )?; + + let total_length: u64 = contigs.iter().map(|c| c.length).sum(); + table( + dir, + "genome_fraction_coverage.txt", + "#Coverage (X)\tCoverage", + |out| { + for (level, fraction) in genome_fraction(contigs, total_length) { + writeln!( + out, + "{}\t{}", + java_double(level as f64), + java_double(fraction) + )?; + } + Ok(()) + }, + )?; + + table( + dir, + "mapped_reads_clipping_profile.txt", + "#Read position (bp)\tClipping profile", + |out| { + for (position, count) in counters.clipping_by_position.iter().enumerate() { + writeln!( + out, + "{}\t{}", + java_double(position as f64), + java_double(pct(*count, counters.clipped_bases)) + )?; + } + Ok(()) + }, + )?; + + table( + dir, + "mapped_reads_nucleotide_content.txt", + "# Position (bp)\tA\tC\tG\tT\tN", + |out| { + for (position, counts) in counters.nucleotide_by_position.iter().enumerate() { + let total: u64 = counts.iter().sum(); + writeln!( + out, + "{}\t{}\t{}\t{}\t{}\t{}", + java_double(position as f64), + java_double(pct(counts[0], total)), + java_double(pct(counts[1], total)), + java_double(pct(counts[2], total)), + java_double(pct(counts[3], total)), + java_double(pct(counts[4], total)), + )?; + } + Ok(()) + }, + )?; + + table( + dir, + "homopolymer_indels.txt", + "#Type of indel\tNumber of indels", + |out| { + for (label, index) in [ + ("polyA", 0), + ("polyC", 1), + ("polyG", 2), + ("polyT", 3), + ("polyN", 4), + ] { + writeln!(out, "{label}\t{}", counters.homopolymer_indels[index])?; + } + writeln!(out, "Non-poly\t{}", counters.non_polymer_indels)?; + Ok(()) + }, + )?; + + Ok(()) +} + +/// Write one raw data table with its header line. +fn table(dir: &Path, name: &str, header: &str, body: F) -> Result<()> +where + F: FnOnce(&mut dyn Write) -> Result<()>, +{ + let path = dir.join(name); + let mut out = std::fs::File::create(&path) + .map(std::io::BufWriter::new) + .with_context(|| format!("Failed to create {}", path.display()))?; + writeln!(out, "{header}")?; + body(&mut out)?; + out.flush()?; + Ok(()) +} + +/// Write `qualimapReport.html`. +/// +/// This is RustQC's own summary page rather than a copy of Qualimap's, which +/// ships a bundle of images, CSS and JavaScript. The numbers are the same ones +/// `genome_results.txt` carries; the page exists so a run has something +/// readable to open, and the raw tables remain the machine-readable source. +pub fn write_html_report(contigs: &[ContigQualimap], sample_name: &str, path: &Path) -> Result<()> { + let mut out = std::fs::File::create(path) + .map(std::io::BufWriter::new) + .with_context(|| format!("Failed to create the report: {}", path.display()))?; + + let total_length: u64 = contigs.iter().map(|c| c.length).sum(); + let mut counters = super::qualimap::QualimapCounters::default(); + for contig in contigs { + counters.merge(&contig.counters); + } + let mean_coverage = if total_length == 0 { + 0.0 + } else { + contigs + .iter() + .map(|c| c.coverage.iter().map(|d| f64::from(*d)).sum::()) + .sum::() + / total_length as f64 + }; + let (insert_mean, insert_sd, insert_median) = counters.insert_size_stats(); + + writeln!(out, "")?; + writeln!(out, "")?; + writeln!(out, "BamQC report: {}", escape(sample_name))?; + writeln!( + out, + "" + )?; + writeln!(out, "

BamQC report

")?; + writeln!( + out, + "

Sample: {}

", + escape(sample_name) + )?; + + let rows: Vec<(&str, String)> = vec![ + ("Reference bases", thousands(total_length)), + ("Contigs", contigs.len().to_string()), + ("Reads", thousands(counters.reads)), + ("Mapped reads", thousands(counters.mapped)), + ("Duplicated reads (flagged)", thousands(counters.duplicates)), + ("Mapped bases", thousands(counters.mapped_bases)), + ("Sequenced bases", thousands(counters.sequenced_bases)), + ("Mean coverage", format!("{mean_coverage:.4}X")), + ("Mean insert size", format!("{insert_mean:.4}")), + ("Std insert size", format!("{insert_sd:.4}")), + ("Median insert size", insert_median.to_string()), + ("Mismatches", thousands(counters.mismatches())), + ("Insertions", thousands(counters.insertions)), + ("Deletions", thousands(counters.deletions)), + ]; + writeln!(out, "

Summary

")?; + for (label, value) in rows { + writeln!( + out, + "" + )?; + } + writeln!(out, "
{label}{value}
")?; + + writeln!(out, "

Coverage per contig

")?; + writeln!( + out, + "" + )?; + for contig in contigs { + let covered: u64 = contig.coverage.iter().map(|d| u64::from(*d)).sum(); + writeln!( + out, + "", + escape(&contig.name), + thousands(contig.length), + thousands(covered), + contig.mean_coverage(), + contig.coverage_sd(), + )?; + } + writeln!(out, "
ContigLengthMapped bases Mean coverageStd
{}{}{} {:.4}{:.4}
")?; + writeln!( + out, + "

Per-window and per-position tables are in \ + raw_data_qualimapReport/.

" + )?; + writeln!(out, "")?; + + out.flush()?; + Ok(()) +} + +/// Escape the few characters that would otherwise close a tag or attribute. +fn escape(text: &str) -> String { + text.replace('&', "&") + .replace('<', "<") + .replace('>', ">") + .replace('"', """) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn doubles_are_written_the_way_java_writes_them() { + assert_eq!(java_double(0.0), "0.0"); + assert_eq!(java_double(51.0), "51.0"); + assert_eq!(java_double(2.5), "2.5"); + assert_eq!(java_double(2.9524261893452746), "2.9524261893452746"); + } + + #[test] + fn thousands_separators_match_qualimaps_formatting() { + assert_eq!(thousands(0), "0"); + assert_eq!(thousands(999), "999"); + assert_eq!(thousands(1_000), "1,000"); + assert_eq!(thousands(40_001), "40,001"); + assert_eq!(thousands(670_999), "670,999"); + } + + #[test] + fn percentages_drop_trailing_zeros() { + assert_eq!(trimmed(2.95, 2), "2.95"); + assert_eq!(trimmed(2.50, 2), "2.5"); + assert_eq!(trimmed(2.0, 2), "2"); + assert_eq!(trimmed(15.2, 2), "15.2"); + } + + #[test] + fn html_escaping_covers_the_characters_that_break_markup() { + assert_eq!(escape("ac&d\"e"), "a<b>c&d"e"); + assert_eq!(escape("plain"), "plain"); + } + + #[test] + fn a_zero_denominator_gives_zero_rather_than_a_nan() { + assert_eq!(pct(5, 0), 0.0); + assert_eq!(pct(0, 10), 0.0); + } +} diff --git a/src/dna/wgs_metrics.rs b/src/dna/wgs_metrics.rs new file mode 100644 index 00000000..0fdc192c --- /dev/null +++ b/src/dna/wgs_metrics.rs @@ -0,0 +1,536 @@ +//! Picard `CollectWgsMetrics` reimplementation. +//! +//! # Upstream semantics +//! +//! Every rule below was derived by reproducing Picard 3.4.0's own output on +//! `tests/data/dna/test.dna.bam` until every exclusion fraction matched, not +//! recalled from documentation. +//! +//! Records that are unmapped, secondary or supplementary never enter the +//! calculation at all. Every other record's reference-consuming bases (`M`, +//! `=`, `X`) form the **denominator** of all the `PCT_EXC_*` columns: 670989 +//! bases on the project fixture. +//! +//! Exclusions then apply in a fixed order, each counted against that same +//! denominator: +//! +//! 1. `PCT_EXC_DUPE`, the whole read, when it is duplicate-flagged; +//! 2. `PCT_EXC_MAPQ`, the whole read, when `MAPQ` is below the minimum; +//! 3. `PCT_EXC_UNPAIRED`, the whole read, when it is not paired; +//! 4. `PCT_EXC_BASEQ`, per base, when the base quality is below the minimum; +//! 5. `PCT_EXC_OVERLAP`, per base, where the mate of the same pair already +//! counted that reference position; +//! 6. `PCT_EXC_CAPPED`, per base, for depth beyond `COVERAGE_CAP`. +//! +//! What survives is the "high quality coverage" the histogram reports, and +//! `MEAN_COVERAGE` is that total over `GENOME_TERRITORY`. `SD_COVERAGE` is the +//! sample standard deviation, `n - 1` denominator, over every base of the +//! territory including the uncovered ones. +//! +//! # What is not reproduced +//! +//! `HET_SNP_SENSITIVITY` and `HET_SNP_Q` come from Picard's +//! `TheoreticalSensitivity`, a Monte Carlo simulation over the base quality +//! and depth distributions. Reproducing its draws bit for bit would mean +//! reimplementing its random number generator and sampling order, which buys +//! nothing for quality control. Both columns are written as `?`, the same +//! marker Picard itself uses for a value it cannot compute. + +use std::collections::{HashMap, HashSet}; +use std::io::Write; +use std::path::Path; + +use anyhow::{Context, Result}; +use rust_htslib::bam; +use rust_htslib::bam::record::Cigar; + +use crate::common::bam_flags::*; + +/// Coverage levels reported as `PCT_xX` columns, in output order. +pub const COVERAGE_LEVELS: [u32; 14] = [1, 5, 10, 15, 20, 25, 30, 40, 50, 60, 70, 80, 90, 100]; + +/// Picard's `COVERAGE_CAP` default. +pub const DEFAULT_COVERAGE_CAP: u32 = 250; + +/// Picard's `MINIMUM_BASE_QUALITY` default. +pub const DEFAULT_MIN_BASE_QUALITY: u8 = 20; + +/// Picard's `MINIMUM_MAPPING_QUALITY` default. +pub const DEFAULT_MIN_MAPPING_QUALITY: u8 = 20; + +/// Accumulates Picard-style high quality coverage for one contig. +#[derive(Debug)] +pub struct WgsAccum { + depth: Vec, + min_mapping_quality: u8, + min_base_quality: u8, + /// Reference-aligned bases of every record that reached the calculation. + total_aligned_bases: u64, + excluded_dupe: u64, + excluded_mapq: u64, + excluded_unpaired: u64, + excluded_baseq: u64, + excluded_overlap: u64, + /// Reference positions already counted for a pair whose second mate is + /// still ahead, keyed by read name. + pending: HashMap, HashSet>, +} + +impl WgsAccum { + /// Allocate for one contig of `length` bases. + pub fn new(length: u64, min_mapping_quality: u8, min_base_quality: u8) -> Self { + Self { + depth: vec![0; length as usize], + min_mapping_quality, + min_base_quality, + total_aligned_bases: 0, + excluded_dupe: 0, + excluded_mapq: 0, + excluded_unpaired: 0, + excluded_baseq: 0, + excluded_overlap: 0, + pending: HashMap::new(), + } + } + + /// Offer one record. + pub fn process_read(&mut self, record: &bam::Record) { + let flags = record.flags(); + // These never reach the calculation, not even the denominator. + if flags & (BAM_FUNMAP | BAM_FSECONDARY | BAM_FSUPPLEMENTARY) != 0 { + return; + } + + let blocks = aligned_positions(record, self.depth.len()); + let aligned = blocks.len() as u64; + if aligned == 0 { + return; + } + self.total_aligned_bases += aligned; + + // Whole-read exclusions, in Picard's order. + if flags & BAM_FDUP != 0 { + self.excluded_dupe += aligned; + return; + } + if record.mapq() < self.min_mapping_quality { + self.excluded_mapq += aligned; + return; + } + if flags & BAM_FPAIRED == 0 { + self.excluded_unpaired += aligned; + return; + } + + // Per-base exclusions. + let qualities = record.qual(); + let mut kept: Vec = Vec::with_capacity(blocks.len()); + for &(ref_pos, query_pos) in &blocks { + let quality = qualities.get(query_pos as usize).copied().unwrap_or(0); + if quality < self.min_base_quality { + self.excluded_baseq += 1; + continue; + } + kept.push(ref_pos); + } + + let same_contig_mate = record.mtid() == record.tid(); + if let Some(mate_positions) = self.pending.remove(record.qname()) { + let before = kept.len(); + kept.retain(|pos| !mate_positions.contains(pos)); + self.excluded_overlap += (before - kept.len()) as u64; + } else if same_contig_mate && record.mpos() >= record.pos() { + self.pending + .insert(record.qname().to_vec(), kept.iter().copied().collect()); + } + + for pos in kept { + self.depth[pos as usize] += 1; + } + } + + /// Fold another contig worker's counters in. Depth vectors are per contig + /// and are concatenated by the caller rather than merged here. + pub fn merge_counters(&mut self, other: &WgsAccum) { + self.total_aligned_bases += other.total_aligned_bases; + self.excluded_dupe += other.excluded_dupe; + self.excluded_mapq += other.excluded_mapq; + self.excluded_unpaired += other.excluded_unpaired; + self.excluded_baseq += other.excluded_baseq; + self.excluded_overlap += other.excluded_overlap; + } + + /// The uncapped per-base depths for this contig. + pub fn depths(&self) -> &[u32] { + &self.depth + } + + /// Consume the accumulator, returning its counters and depths. + pub fn into_parts(self) -> (WgsCounters, Vec) { + ( + WgsCounters { + total_aligned_bases: self.total_aligned_bases, + excluded_dupe: self.excluded_dupe, + excluded_mapq: self.excluded_mapq, + excluded_unpaired: self.excluded_unpaired, + excluded_baseq: self.excluded_baseq, + excluded_overlap: self.excluded_overlap, + }, + self.depth, + ) + } +} + +/// Exclusion counters, summed across contigs. +#[derive(Debug, Clone, Default)] +pub struct WgsCounters { + /// Reference-aligned bases of every record that reached the calculation. + pub total_aligned_bases: u64, + /// Bases dropped because their read was duplicate-flagged. + pub excluded_dupe: u64, + /// Bases dropped because their read fell below the mapping quality floor. + pub excluded_mapq: u64, + /// Bases dropped because their read was unpaired. + pub excluded_unpaired: u64, + /// Bases dropped for low base quality. + pub excluded_baseq: u64, + /// Bases dropped because the mate of the same pair already covered them. + pub excluded_overlap: u64, +} + +impl WgsCounters { + /// Add another set of counters. + pub fn merge(&mut self, other: &WgsCounters) { + self.total_aligned_bases += other.total_aligned_bases; + self.excluded_dupe += other.excluded_dupe; + self.excluded_mapq += other.excluded_mapq; + self.excluded_unpaired += other.excluded_unpaired; + self.excluded_baseq += other.excluded_baseq; + self.excluded_overlap += other.excluded_overlap; + } +} + +/// A record's reference-covering positions, paired with the query offset that +/// produced each one so base qualities can be looked up. +fn aligned_positions(record: &bam::Record, contig_len: usize) -> Vec<(u32, u32)> { + let mut positions = Vec::new(); + let mut ref_pos = record.pos(); + let mut query_pos: i64 = 0; + for op in record.cigar().iter() { + match op { + Cigar::Match(n) | Cigar::Equal(n) | Cigar::Diff(n) => { + for k in 0..i64::from(*n) { + let r = ref_pos + k; + if r >= 0 && (r as usize) < contig_len { + positions.push((r as u32, (query_pos + k) as u32)); + } + } + ref_pos += i64::from(*n); + query_pos += i64::from(*n); + } + Cigar::Del(n) | Cigar::RefSkip(n) => ref_pos += i64::from(*n), + Cigar::Ins(n) | Cigar::SoftClip(n) => query_pos += i64::from(*n), + Cigar::HardClip(_) | Cigar::Pad(_) => {} + } + } + positions +} + +/// The computed `CollectWgsMetrics` figures. +#[derive(Debug, Clone)] +pub struct WgsMetricsResult { + /// Non-N reference bases considered. + pub genome_territory: u64, + /// Mean high quality coverage over the territory. + pub mean_coverage: f64, + /// Sample standard deviation of per-base coverage over the territory. + pub sd_coverage: f64, + /// Median per-base coverage. + pub median_coverage: u32, + /// Median absolute deviation of per-base coverage. + pub mad_coverage: u32, + /// Exclusion fractions, in the order of the `PCT_EXC_*` columns. + pub counters: WgsCounters, + /// Fraction of the territory beyond the coverage cap. + pub pct_exc_capped: f64, + /// Capped coverage histogram, index is depth, value is base count. + pub histogram: Vec, + /// The coverage cap applied. + pub coverage_cap: u32, +} + +impl WgsMetricsResult { + /// Summarise per-base depths and counters into the reported figures. + pub fn new( + depths: &[u32], + counters: WgsCounters, + genome_territory: u64, + coverage_cap: u32, + ) -> Self { + let mut histogram = vec![0u64; coverage_cap as usize + 1]; + let mut capped_excess = 0u64; + for &depth in depths { + if depth > coverage_cap { + capped_excess += u64::from(depth - coverage_cap); + histogram[coverage_cap as usize] += 1; + } else { + histogram[depth as usize] += 1; + } + } + + let total: u64 = histogram + .iter() + .enumerate() + .map(|(depth, count)| depth as u64 * count) + .sum(); + let mean = if genome_territory == 0 { + 0.0 + } else { + total as f64 / genome_territory as f64 + }; + + // Sample standard deviation over every base of the territory. + let sd = if genome_territory < 2 { + 0.0 + } else { + let sum_sq: f64 = histogram + .iter() + .enumerate() + .map(|(depth, count)| { + let diff = depth as f64 - mean; + diff * diff * *count as f64 + }) + .sum(); + (sum_sq / (genome_territory - 1) as f64).sqrt() + }; + + let median = histogram_quantile(&histogram, genome_territory / 2); + let mut deviations = vec![0u64; coverage_cap as usize + 1]; + for (depth, count) in histogram.iter().enumerate() { + let deviation = (depth as u32).abs_diff(median) as usize; + deviations[deviation.min(coverage_cap as usize)] += count; + } + let mad = histogram_quantile(&deviations, genome_territory / 2); + + let pct_exc_capped = if counters.total_aligned_bases == 0 { + 0.0 + } else { + capped_excess as f64 / counters.total_aligned_bases as f64 + }; + + Self { + genome_territory, + mean_coverage: mean, + sd_coverage: sd, + median_coverage: median, + mad_coverage: mad, + counters, + pct_exc_capped, + histogram, + coverage_cap, + } + } + + /// Fraction of `total_aligned_bases` a given exclusion accounts for. + fn fraction(&self, excluded: u64) -> f64 { + if self.counters.total_aligned_bases == 0 { + 0.0 + } else { + excluded as f64 / self.counters.total_aligned_bases as f64 + } + } + + /// Every `PCT_EXC_*` value, summing to `PCT_EXC_TOTAL`. + pub fn exclusion_fractions(&self) -> [f64; 7] { + let dupe = self.fraction(self.counters.excluded_dupe); + let mapq = self.fraction(self.counters.excluded_mapq); + let unpaired = self.fraction(self.counters.excluded_unpaired); + let baseq = self.fraction(self.counters.excluded_baseq); + let overlap = self.fraction(self.counters.excluded_overlap); + let capped = self.pct_exc_capped; + let total = dupe + mapq + unpaired + baseq + overlap + capped; + [dupe, mapq, unpaired, baseq, overlap, capped, total] + } + + /// Fraction of the territory at or above each level in [`COVERAGE_LEVELS`]. + pub fn coverage_fractions(&self) -> Vec { + COVERAGE_LEVELS + .iter() + .map(|level| { + if self.genome_territory == 0 { + return 0.0; + } + let at_or_above: u64 = self + .histogram + .iter() + .enumerate() + .filter(|(depth, _)| *depth as u32 >= *level) + .map(|(_, count)| count) + .sum(); + at_or_above as f64 / self.genome_territory as f64 + }) + .collect() + } +} + +/// The value at `rank` when a histogram indexed by value is expanded. +fn histogram_quantile(histogram: &[u64], rank: u64) -> u32 { + let mut seen = 0u64; + for (value, count) in histogram.iter().enumerate() { + seen += count; + if seen > rank { + return value as u32; + } + } + 0 +} + +/// Format a float the way Picard's metrics writer does. +fn fmt_picard(value: f64) -> String { + if !value.is_finite() { + return "?".to_string(); + } + if value == value.trunc() && value.abs() < 1e15 { + return format!("{}", value as i64); + } + let text = format!("{value:.6}"); + text.trim_end_matches('0').trim_end_matches('.').to_string() +} + +/// Write a Picard-compatible `wgs_metrics.txt`. +pub fn write_wgs_metrics(result: &WgsMetricsResult, path: &Path) -> Result<()> { + let mut out = std::fs::File::create(path) + .map(std::io::BufWriter::new) + .with_context(|| format!("Failed to create WGS metrics: {}", path.display()))?; + + writeln!(out, "## METRICS CLASS\tpicard.analysis.WgsMetrics")?; + write!( + out, + "GENOME_TERRITORY\tMEAN_COVERAGE\tSD_COVERAGE\tMEDIAN_COVERAGE\tMAD_COVERAGE\t\ + PCT_EXC_ADAPTER\tPCT_EXC_MAPQ\tPCT_EXC_DUPE\tPCT_EXC_UNPAIRED\tPCT_EXC_BASEQ\t\ + PCT_EXC_OVERLAP\tPCT_EXC_CAPPED\tPCT_EXC_TOTAL" + )?; + for level in COVERAGE_LEVELS { + write!(out, "\tPCT_{level}X")?; + } + writeln!( + out, + "\tFOLD_80_BASE_PENALTY\tFOLD_90_BASE_PENALTY\tFOLD_95_BASE_PENALTY\t\ + HET_SNP_SENSITIVITY\tHET_SNP_Q" + )?; + + let [dupe, mapq, unpaired, baseq, overlap, capped, total] = result.exclusion_fractions(); + write!( + out, + "{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}", + result.genome_territory, + fmt_picard(result.mean_coverage), + fmt_picard(result.sd_coverage), + result.median_coverage, + result.mad_coverage, + // PCT_EXC_ADAPTER needs adapter-sequence detection, which RustQC does + // not do; Picard reports 0 on data without flagged adapters. + fmt_picard(0.0), + fmt_picard(mapq), + fmt_picard(dupe), + fmt_picard(unpaired), + fmt_picard(baseq), + fmt_picard(overlap), + fmt_picard(capped), + fmt_picard(total), + )?; + for fraction in result.coverage_fractions() { + write!(out, "\t{}", fmt_picard(fraction))?; + } + // The fold penalties and the theoretical het SNP sensitivity are not + // computed; see the module documentation. + writeln!(out, "\t?\t?\t?\t?\t?")?; + writeln!(out)?; + + writeln!(out, "## HISTOGRAM\tjava.lang.Integer")?; + writeln!(out, "coverage\thigh_quality_coverage_count")?; + for (depth, count) in result.histogram.iter().enumerate() { + writeln!(out, "{depth}\t{count}")?; + } + writeln!(out)?; + + out.flush()?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn counters(total: u64) -> WgsCounters { + WgsCounters { + total_aligned_bases: total, + ..Default::default() + } + } + + #[test] + fn depth_beyond_the_cap_lands_in_the_top_bin_and_counts_as_excluded() { + let result = WgsMetricsResult::new(&[300, 1, 0], counters(1000), 3, 250); + assert_eq!(result.histogram[250], 1, "the capped base"); + assert_eq!(result.histogram[1], 1); + assert_eq!(result.histogram[0], 1); + // 300 - 250 = 50 bases beyond the cap. + assert!((result.pct_exc_capped - 50.0 / 1000.0).abs() < 1e-12); + } + + #[test] + fn standard_deviation_uses_the_sample_denominator_over_the_territory() { + // Depths 1, 2, 3: mean 2, sample variance 1, so SD is exactly 1. + let result = WgsMetricsResult::new(&[1, 2, 3], counters(6), 3, 250); + assert!((result.mean_coverage - 2.0).abs() < 1e-12); + assert!( + (result.sd_coverage - 1.0).abs() < 1e-12, + "got {}", + result.sd_coverage + ); + } + + #[test] + fn uncovered_bases_pull_the_median_down() { + let mut depths = vec![0u32; 90]; + depths.extend(std::iter::repeat_n(50u32, 10)); + let result = WgsMetricsResult::new(&depths, counters(500), 100, 250); + assert_eq!(result.median_coverage, 0, "90 percent of bases are at zero"); + } + + #[test] + fn exclusion_fractions_sum_to_the_total() { + let c = WgsCounters { + total_aligned_bases: 1000, + excluded_dupe: 100, + excluded_mapq: 50, + excluded_unpaired: 25, + excluded_baseq: 10, + excluded_overlap: 200, + }; + let result = WgsMetricsResult::new(&[1, 1, 1], c, 3, 250); + let f = result.exclusion_fractions(); + let summed: f64 = f[..6].iter().sum(); + assert!((f[6] - summed).abs() < 1e-12, "PCT_EXC_TOTAL is the sum"); + assert!((f[0] - 0.1).abs() < 1e-12, "dupe"); + assert!((f[4] - 0.2).abs() < 1e-12, "overlap"); + } + + #[test] + fn coverage_fractions_are_at_or_above_each_level() { + let result = WgsMetricsResult::new(&[0, 1, 5, 100], counters(106), 4, 250); + let f = result.coverage_fractions(); + assert!((f[0] - 0.75).abs() < 1e-12, "PCT_1X: three of four bases"); + assert!((f[1] - 0.5).abs() < 1e-12, "PCT_5X: two of four"); + assert!((f[13] - 0.25).abs() < 1e-12, "PCT_100X: one of four"); + } + + #[test] + fn unrepresentable_values_are_written_as_a_question_mark() { + assert_eq!(fmt_picard(f64::NAN), "?"); + assert_eq!(fmt_picard(f64::INFINITY), "?"); + assert_eq!(fmt_picard(3.531312), "3.531312"); + assert_eq!(fmt_picard(0.0), "0"); + } +} diff --git a/src/lib.rs b/src/lib.rs index 9a228cae..2ce25ddf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,9 +23,12 @@ //! - [`config`] — configuration types that mirror the CLI's YAML config file. //! - [`summary`] — serializable types for the JSON run summary. //! - [`cpu`] — CPU feature detection and binary-target identification. +//! - [`common`] — analyses shared by every pipeline: BAM flag helpers, +//! read-level statistics ([`common::bam_stat`], [`common::bam_stat_accum`]), +//! the samtools-compatible writers ([`common::samtools`]), and preseq +//! library complexity extrapolation ([`common::preseq`]). //! - [`rna`] — the RNA-Seq analysis modules: -//! - [`rna::dupradar`], [`rna::featurecounts`], [`rna::qualimap`], -//! [`rna::preseq`], [`rna::rseqc`]. +//! - [`rna::dupradar`], [`rna::featurecounts`], [`rna::qualimap`], [`rna::rseqc`]. //! //! [`Strandedness`] lives at the crate root because it is used across most //! analysis modules. @@ -65,8 +68,10 @@ use clap::ValueEnum; use serde::Deserialize; +pub mod common; pub mod config; pub mod cpu; +pub mod dna; pub mod gtf; pub mod io; pub mod rna; diff --git a/src/main.rs b/src/main.rs index 4c66c173..75736e3e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,14 +18,15 @@ use indexmap::IndexMap; use log::debug; use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use std::collections::{HashMap, HashSet}; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::time::{Instant, SystemTime, UNIX_EPOCH}; use rustqc::io::{format_count, format_duration, format_pct}; -use rustqc::{config, cpu, gtf, rna, summary}; +use rustqc::{common, config, cpu, gtf, rna, summary}; use ui::{Ui, Verbosity}; +use rust_htslib::bam; use rust_htslib::bam::Read as BamRead; use rna::rseqc::accumulators::{RseqcAccumulators, RseqcAnnotations, RseqcConfig}; @@ -73,9 +74,13 @@ fn main() -> Result<()> { let cli = cli::parse_args(); // Determine verbosity from CLI flags - let verbosity = match &cli.command { - cli::Commands::Rna(args) if args.quiet => Verbosity::Quiet, - cli::Commands::Rna(args) if args.verbose => Verbosity::Verbose, + let (quiet, verbose) = match &cli.command { + cli::Commands::Rna(args) => (args.quiet, args.verbose), + cli::Commands::Dna(args) => (args.quiet, args.verbose), + }; + let verbosity = match (quiet, verbose) { + (true, _) => Verbosity::Quiet, + (_, true) => Verbosity::Verbose, _ => Verbosity::Normal, }; @@ -94,7 +99,807 @@ fn main() -> Result<()> { match cli.command { cli::Commands::Rna(args) => run_rna(args, &ui), + cli::Commands::Dna(args) => run_dna(args, &ui), + } +} + +/// Run the DNA QC pipeline: depth of coverage, samtools-compatible outputs +/// and library complexity estimation in a single pass over each input. +/// +/// Contigs are processed in parallel, one worker per contig, each holding its +/// own depth array. Input files are processed one after another so that the +/// per-contig parallelism gets the whole thread budget. +fn run_dna(args: cli::DnaArgs, ui: &Ui) -> Result<()> { + let run_start = Instant::now(); + let timestamp_start = format_utc_now(); + + let (merged, config_paths) = config::load_merged_config(args.config.as_deref())?; + let mut config = merged.dna; + + // CLI flags override the configuration file. + if !args.depth_thresholds.is_empty() { + config.mosdepth.thresholds = args.depth_thresholds.clone(); + } + if let Some(window) = args.window_size { + config.mosdepth.window_size = Some(window); + } + if args.skip_per_base { + config.mosdepth.skip_per_base = true; + } + if args.skip_preseq { + config.preseq.enabled = false; + } + if let Some(seed) = args.preseq_seed { + config.preseq.seed = seed; + } + if let Some(val) = args.preseq_max_extrap { + config.preseq.max_extrap = val; + } + if let Some(val) = args.preseq_step_size { + config.preseq.step_size = val; + } + if let Some(val) = args.preseq_n_bootstraps { + config.preseq.n_bootstraps = val; + } + if let Some(val) = args.preseq_seg_len { + config.preseq.max_segment_length = val; + } + + let flat_output = args.flat_output || config.flat_output; + let outdir = Path::new(&args.outdir); + std::fs::create_dir_all(outdir) + .with_context(|| format!("Failed to create output directory: {}", outdir.display()))?; + + ui.header( + env!("CARGO_PKG_VERSION"), + env!("GIT_SHORT_HASH"), + env!("BUILD_TIMESTAMP"), + Some(&rustqc::cpu::cpu_info_line()), + ); + for (path, source) in &config_paths { + ui.config("Config", &format!("{} ({source})", path.display())); + } + ui.config("Output dir", &args.outdir); + ui.config("Threads", &args.threads.to_string()); + if let Some(ref targets) = args.targets { + ui.config("Targets", targets); + } + if let Some(ref baits) = args.baits { + ui.config("Baits", baits); + } + + let mut inputs = Vec::new(); + for bam_path in &args.input { + let bam_start = Instant::now(); + let name = Path::new(bam_path) + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or(bam_path.as_str()) + .to_string(); + + match process_single_dna_bam(bam_path, &args, &config, outdir, flat_output, ui) { + Ok(mut summary) => { + summary.runtime_seconds = bam_start.elapsed().as_secs_f64(); + ui.bam_result_ok(&name, bam_start.elapsed()); + inputs.push(summary); + } + Err(e) => { + ui.bam_result_err(&name, &format!("{e:#}")); + inputs.push(summary::InputSummary { + bam_file: bam_path.clone(), + status: "failed".to_string(), + error: Some(format!("{e:#}")), + runtime_seconds: bam_start.elapsed().as_secs_f64(), + counting: None, + dupradar: None, + dna: None, + outputs: Vec::new(), + }); + } + } + } + + if let Some(ref json_path) = args.json_summary { + let summary = summary::RunSummary { + version: env!("CARGO_PKG_VERSION").to_string(), + commit: env!("GIT_SHORT_HASH").to_string(), + binary_target: cpu::binary_target().to_string(), + cpu_features: cpu::detected_features() + .iter() + .map(|s| s.to_string()) + .collect(), + timestamp_start, + timestamp_end: format_utc_now(), + runtime_seconds: run_start.elapsed().as_secs_f64(), + inputs, + }; + let json = serde_json::to_string_pretty(&summary)?; + if json_path == "-" { + println!("{json}"); + } else { + let path = if json_path.is_empty() { + outdir.join("rustqc_summary.json") + } else { + PathBuf::from(json_path) + }; + std::fs::write(&path, json) + .with_context(|| format!("Failed to write JSON summary: {}", path.display()))?; + } + } + + let citations_path = outdir.join("CITATIONS.md"); + citations::write_dna_citations( + &citations_path, + &config, + env!("CARGO_PKG_VERSION"), + env!("GIT_SHORT_HASH"), + )?; + ui.output_item("citations", &citations_path.display().to_string()); + + ui.finish("DNA QC", run_start.elapsed()); + Ok(()) +} + +/// Process one alignment file through the DNA pipeline. +fn process_single_dna_bam( + bam_path: &str, + args: &cli::DnaArgs, + config: &config::DnaConfig, + outdir: &Path, + flat_output: bool, + ui: &Ui, +) -> Result { + use rustqc::common::bam_stat_accum::BamStatAccum; + use rustqc::common::preseq::PreseqAccum; + use rustqc::dna::depth::{DepthAccum, MOSDEPTH_DEFAULT_EXCLUDE}; + use rustqc::dna::gc_bias::{self, GcBiasAccum}; + use rustqc::dna::hs_metrics::{self, HsAccum, HsCounters, HsMetricsResult}; + use rustqc::dna::insert_size::{self, InsertSizeAccum}; + use rustqc::dna::intervals::IntervalSet; + use rustqc::dna::mosdepth::{output as mos_out, ContigDepth, MosdepthResult}; + use rustqc::dna::qualimap::{ContigQualimap, QualimapAccum}; + use rustqc::dna::qualimap_output; + use rustqc::dna::wgs_metrics::{self, WgsAccum, WgsCounters, WgsMetricsResult}; + + let sample_name = args + .sample_name + .clone() + .or_else(|| config.sample_name.clone()) + .unwrap_or_else(|| { + Path::new(bam_path) + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("sample") + .to_string() + }); + + let is_cram = bam_path.ends_with(".cram"); + ensure!( + !is_cram || args.reference.is_some(), + "CRAM input requires --reference" + ); + + // Read the header once to learn the contigs. + let header = { + let reader = bam::IndexedReader::from_path(bam_path) + .with_context(|| format!("Failed to open alignment file: {bam_path}"))?; + reader.header().to_owned() + }; + let mut contigs: Vec<(u32, String, u64)> = (0..header.target_count()) + .map(|tid| { + let name = String::from_utf8_lossy(header.tid2name(tid)).to_string(); + let len = header.target_len(tid).unwrap_or(0); + (tid, name, len) + }) + .collect(); + // Longest first, so the biggest depth arrays are allocated while the pool + // is emptiest. + contigs.sort_by_key(|contig| std::cmp::Reverse(contig.2)); + + let largest = contigs.first().map(|c| c.2).unwrap_or(0); + let workers = depth_worker_budget(args.threads, args.max_depth_workers, largest); + ui.config("Depth workers", &workers.to_string()); + + let pool = rayon::ThreadPoolBuilder::new() + .num_threads(workers) + .build() + .context("Failed to build rayon thread pool")?; + + let thresholds = config.mosdepth.thresholds.clone(); + let window_size = config.mosdepth.window_size; + let preseq_enabled = config.preseq.enabled; + let seg_len = config.preseq.max_segment_length; + let mapq_cut = args.mapq_cut; + // CollectWgsMetrics needs the reference to count non-N bases, so without + // one it is skipped rather than reported against a wrong territory. + let wgs_enabled = config.wgs_metrics.enabled && args.reference.is_some(); + if config.wgs_metrics.enabled && args.reference.is_none() { + ui.warn("CollectWgsMetrics needs --reference to size the genome territory, skipping"); + } + let insert_size_enabled = config.insert_size.enabled; + // GC bias bins reference windows, so it needs the reference just as the + // WGS metrics do. + let gc_bias_enabled = config.gc_bias.enabled && args.reference.is_some(); + if config.gc_bias.enabled && args.reference.is_none() { + ui.warn("CollectGcBiasMetrics needs --reference to bin the genome, skipping"); + } + let gc_window = config.gc_bias.window_size; + + // Targeted mode is switched on by --targets alone; --baits defaults to it. + let targets = match args.targets.as_deref() { + Some(path) => Some(IntervalSet::from_bed(Path::new(path))?), + None => None, + }; + let baits = match args.baits.as_deref() { + Some(path) => Some(IntervalSet::from_bed(Path::new(path))?), + None => targets.clone(), + }; + let hs_enabled = config.hs_metrics.enabled && targets.is_some(); + let hs_min_mapq = config.hs_metrics.min_mapping_quality; + let hs_min_baseq = config.hs_metrics.min_base_quality; + let qualimap_enabled = config.qualimap.enabled; + let qualimap_windows = config.qualimap.num_windows; + let wgs_min_mapq = config.wgs_metrics.min_mapping_quality; + let wgs_min_baseq = config.wgs_metrics.min_base_quality; + let coverage_cap = config.wgs_metrics.coverage_cap; + + /// What one contig worker hands back: its depth summary, the read-level + /// counters, and the optional per-tool accumulators. + type ContigOutput = ( + ContigDepth, + BamStatAccum, + Option, + Option<(WgsCounters, Vec)>, + Option, + Option, + Option<(HsCounters, Vec, Vec, String)>, + Option, + ); + + let results: Vec> = pool.install(|| { + contigs + .par_iter() + .map(|(tid, name, len)| -> Result { + let mut reader = bam::IndexedReader::from_path(bam_path) + .with_context(|| format!("Failed to open alignment file: {bam_path}"))?; + if let Some(reference) = args.reference.as_deref() { + reader + .set_reference(reference) + .with_context(|| format!("Failed to set reference: {reference}"))?; + } + reader + .fetch(*tid) + .with_context(|| format!("Failed to fetch contig {name}"))?; + + let mut depth = DepthAccum::new(*len, mapq_cut, MOSDEPTH_DEFAULT_EXCLUDE); + let mut bam_stat = BamStatAccum::default(); + let mut preseq = preseq_enabled.then(|| PreseqAccum::new(seg_len)); + // Picard filters differently from mosdepth, so its coverage + // needs its own accumulator rather than a correction applied + // to a shared one. + let mut wgs = wgs_enabled.then(|| WgsAccum::new(*len, wgs_min_mapq, wgs_min_baseq)); + let mut insert_sizes = insert_size_enabled.then(InsertSizeAccum::new); + let mut qualimap = + qualimap_enabled.then(|| QualimapAccum::new(name, *len, qualimap_windows)); + + // GC bias and the targeted metrics both need per-contig + // context, fetched once here rather than per record. + let reference_bases: Option> = if gc_bias_enabled { + let reader = rust_htslib::faidx::Reader::from_path( + args.reference.as_deref().unwrap_or_default(), + ) + .with_context(|| "Failed to open the reference FASTA index")?; + let length = reader.fetch_seq_len(name) as usize; + Some( + reader + .fetch_seq(name, 0, length.saturating_sub(1)) + .map(|s| s.to_vec()) + .with_context(|| format!("Failed to read reference for {name}"))?, + ) + } else { + None + }; + let mut gc = reference_bases + .as_ref() + .map(|bases| GcBiasAccum::new(bases, gc_window)); + let mut hs = hs_enabled.then(|| { + HsAccum::new( + name, + *len, + baits.as_ref().unwrap_or_else(|| targets.as_ref().unwrap()), + targets.as_ref().unwrap(), + hs_min_mapq, + hs_min_baseq, + ) + }); + + let mut record = bam::Record::new(); + while let Some(result) = reader.read(&mut record) { + result.context("Failed to read record")?; + depth.process_read(&record); + bam_stat.process_read(&record, mapq_cut); + if let Some(accum) = preseq.as_mut() { + accum.process_read(&record); + } + if let Some(accum) = wgs.as_mut() { + accum.process_read(&record); + } + if let Some(accum) = insert_sizes.as_mut() { + accum.process_read(&record); + } + if let (Some(accum), Some(bases)) = (gc.as_mut(), reference_bases.as_ref()) { + accum.process_read(&record, bases); + } + if let Some(accum) = hs.as_mut() { + accum.process_read(&record); + } + if let Some(accum) = qualimap.as_mut() { + accum.process_read(&record); + } + } + + let depths = depth.into_depths(); + let contig = ContigDepth::from_depths(name, &depths, window_size, &thresholds); + Ok(( + contig, + bam_stat, + preseq, + wgs.map(|accum| accum.into_parts()), + insert_sizes, + gc, + hs.map(|accum| { + let (counters, depths, mask) = accum.into_parts(); + (counters, depths, mask, name.clone()) + }), + qualimap.map(|accum| accum.into_result()), + )) + }) + .collect() + }); + + let mut per_contig = Vec::new(); + let mut bam_stat_total = BamStatAccum::default(); + let mut preseq_total: Option = None; + let mut wgs_counters = WgsCounters::default(); + let mut wgs_depths: Vec = Vec::new(); + let mut saw_wgs = false; + let mut insert_size_total: Option = None; + let mut gc_total: Option = None; + let mut hs_counters = HsCounters::default(); + let mut hs_target_depths: Vec = Vec::new(); + let mut hs_target_count = 0u64; + let mut hs_zero_targets = 0u64; + let mut qualimap_contigs: Vec = Vec::new(); + for result in results { + let (contig, bam_stat, preseq, wgs, insert_sizes, gc, hs, qualimap) = result?; + per_contig.push(contig); + bam_stat_total.merge(bam_stat); + match (preseq_total.as_mut(), preseq) { + (Some(total), Some(part)) => total.merge(part), + (None, part) => preseq_total = part, + _ => {} + } + if let Some((counters, depths)) = wgs { + saw_wgs = true; + wgs_counters.merge(&counters); + // Depths concatenate rather than merge: each worker owns a + // distinct contig and the metrics span all of them. + wgs_depths.extend(depths); + } + match (insert_size_total.as_mut(), insert_sizes) { + (Some(total), Some(part)) => total.merge(part), + (None, part) => insert_size_total = part, + _ => {} + } + match (gc_total.as_mut(), gc) { + (Some(total), Some(part)) => total.merge(&part), + (None, part) => gc_total = part, + _ => {} + } + if let Some(part) = qualimap { + qualimap_contigs.push(part); + } + if let Some((counters, depths, mask, contig_name)) = hs { + hs_counters.merge(&counters); + for (depth, on_target) in depths.iter().zip(mask.iter()) { + if *on_target { + hs_target_depths.push(*depth); + } + } + if let Some(set) = targets.as_ref() { + for interval in set.on(&contig_name) { + hs_target_count += 1; + if (interval.start..interval.end) + .all(|p| depths.get(p as usize).copied().unwrap_or(0) == 0) + { + hs_zero_targets += 1; + } + } + } + } + } + + // Unmapped records carry no contig, so they need their own pass; flagstat + // and idxstats both report them. + { + let mut reader = bam::IndexedReader::from_path(bam_path) + .with_context(|| format!("Failed to open alignment file: {bam_path}"))?; + if let Some(reference) = args.reference.as_deref() { + reader.set_reference(reference).ok(); + } + if reader.fetch(bam::FetchDefinition::Unmapped).is_ok() { + // Unmapped records reach no contig worker, yet they still count + // towards several metrics: flagstat and idxstats report them, HS + // metrics count them in TOTAL_READS and PF_BASES, and GC bias + // counts them as clusters. Both accumulators short-circuit on an + // unmapped record, so an empty contig is enough context here. + let mut hs_unmapped = hs_enabled.then(|| { + HsAccum::new( + "", + 0, + baits.as_ref().unwrap_or_else(|| targets.as_ref().unwrap()), + targets.as_ref().unwrap(), + hs_min_mapq, + hs_min_baseq, + ) + }); + let mut gc_unmapped = gc_bias_enabled.then(|| GcBiasAccum::new(&[], gc_window)); + let mut qualimap_unmapped = + qualimap_enabled.then(|| QualimapAccum::new("", 0, qualimap_windows)); + + let mut record = bam::Record::new(); + while let Some(result) = reader.read(&mut record) { + result.context("Failed to read unmapped record")?; + bam_stat_total.process_read(&record, mapq_cut); + if let Some(accum) = hs_unmapped.as_mut() { + accum.process_read(&record); + } + if let Some(accum) = gc_unmapped.as_mut() { + accum.process_read(&record, &[]); + } + if let Some(accum) = qualimap_unmapped.as_mut() { + accum.process_read(&record); + } + } + + if let Some(accum) = hs_unmapped { + let (counters, _, _) = accum.into_parts(); + hs_counters.merge(&counters); + } + if let (Some(total), Some(part)) = (gc_total.as_mut(), gc_unmapped) { + total.merge(&part); + } + if let (Some(first), Some(part)) = (qualimap_contigs.first_mut(), qualimap_unmapped) { + // The unmapped pass only moves read counters, so folding it + // into the first contig keeps the totals right without + // inventing a contig for reads that have none. + first.counters.merge(&part.into_result().counters); + } + } + } + + // Workers ran longest-contig-first; outputs go out in header order. + let order: Vec = (0..header.target_count()) + .map(|tid| String::from_utf8_lossy(header.tid2name(tid)).to_string()) + .collect(); + per_contig.sort_by_key(|contig| { + order + .iter() + .position(|name| name == &contig.name) + .unwrap_or(usize::MAX) + }); + + let result = MosdepthResult { + contigs: per_contig, + window_size, + thresholds: thresholds.clone(), + }; + + let bam_stat_result = bam_stat_total.into_result(); + ensure!( + args.skip_dup_check || bam_stat_result.duplicates > 0, + "No duplicate-flagged reads found in {bam_path}. RustQC expects \ + duplicate-marked (not removed) input. Pass --skip-dup-check to override." + ); + + let dir = |name: &str| -> PathBuf { + if flat_output { + outdir.to_path_buf() + } else { + outdir.join(name) + } + }; + let mut written: Vec = Vec::new(); + let mut record_output = |tool: &str, path: PathBuf| { + ui.output_item(tool, &path.display().to_string()); + written.push(summary::OutputFile { + tool: tool.to_string(), + path: path.display().to_string(), + }); + }; + + if config.mosdepth.enabled { + let mos_dir = dir("mosdepth"); + std::fs::create_dir_all(&mos_dir)?; + // Built with format! rather than with_extension: a sample name that + // contains a dot (test.dna, say) would otherwise lose its last segment. + let prefix = |suffix: &str| mos_dir.join(format!("{sample_name}.{suffix}")); + + let path = prefix("mosdepth.summary.txt"); + mos_out::write_summary(&result, &path)?; + record_output("mosdepth", path); + + let path = prefix("mosdepth.global.dist.txt"); + mos_out::write_global_dist(&result, &path)?; + record_output("mosdepth", path); + + if !config.mosdepth.skip_per_base { + let path = prefix("per-base.bed.gz"); + mos_out::write_per_base(&result, &path)?; + record_output("mosdepth", path); + } + + if window_size.is_some() { + let path = prefix("mosdepth.region.dist.txt"); + mos_out::write_region_dist(&result, &path)?; + record_output("mosdepth", path); + + let path = prefix("regions.bed.gz"); + mos_out::write_regions(&result, &path)?; + record_output("mosdepth", path); + + if !thresholds.is_empty() { + let path = prefix("thresholds.bed.gz"); + mos_out::write_thresholds(&result, &path)?; + record_output("mosdepth", path); + } + } + } + + if config.samtools.enabled { + let sam_dir = dir("samtools"); + std::fs::create_dir_all(&sam_dir)?; + + let path = sam_dir.join(format!("{sample_name}.stats.txt")); + common::samtools::stats::write_stats(&bam_stat_result, &path)?; + record_output("samtools stats", path); + + let path = sam_dir.join(format!("{sample_name}.flagstat.txt")); + common::samtools::flagstat::write_flagstat(&bam_stat_result, &path)?; + record_output("samtools flagstat", path); + + let refs: Vec<(String, u64)> = (0..header.target_count()) + .map(|tid| { + ( + String::from_utf8_lossy(header.tid2name(tid)).to_string(), + header.target_len(tid).unwrap_or(0), + ) + }) + .collect(); + let path = sam_dir.join(format!("{sample_name}.idxstats.txt")); + common::samtools::idxstats::write_idxstats(&bam_stat_result, &refs, &path)?; + record_output("samtools idxstats", path); + } + + if saw_wgs { + let territory = match args.reference.as_deref() { + Some(reference) => genome_territory(reference)?, + // Unreachable: saw_wgs implies a reference was given. + None => wgs_depths.len() as u64, + }; + let result = WgsMetricsResult::new(&wgs_depths, wgs_counters, territory, coverage_cap); + let dir_path = dir("picard").join("wgs_metrics"); + std::fs::create_dir_all(&dir_path)?; + let path = dir_path.join(format!("{sample_name}.wgs_metrics.txt")); + wgs_metrics::write_wgs_metrics(&result, &path)?; + record_output("picard CollectWgsMetrics", path); + } + + if let Some(accum) = insert_size_total { + let result = accum.into_result(config.insert_size.deviations); + if result.rows.is_empty() { + ui.warn("no paired records with a usable insert size, skipping insert size metrics"); + } else { + let dir_path = dir("picard").join("insert_size"); + std::fs::create_dir_all(&dir_path)?; + let path = dir_path.join(format!("{sample_name}.insert_size_metrics.txt")); + insert_size::write_insert_size_metrics(&result, &path)?; + record_output("picard CollectInsertSizeMetrics", path); + } + } + + if let Some(accum) = gc_total { + let result = accum.into_result(gc_window); + let dir_path = dir("picard").join("gc_bias"); + std::fs::create_dir_all(&dir_path)?; + let detail = dir_path.join(format!("{sample_name}.gc_bias.detail_metrics.txt")); + gc_bias::write_detail_metrics(&result, &detail)?; + record_output("picard CollectGcBiasMetrics", detail); + let summary = dir_path.join(format!("{sample_name}.gc_bias.summary_metrics.txt")); + gc_bias::write_summary_metrics(&result, &summary)?; + record_output("picard CollectGcBiasMetrics", summary); + } + + if hs_enabled { + let target_set = targets.as_ref().expect("hs_enabled implies --targets"); + let bait_set = baits.as_ref().unwrap_or(target_set); + let library_size = hs_metrics::estimate_library_size( + hs_counters.selected_pairs, + hs_counters.selected_unique_pairs, + ); + let result = HsMetricsResult { + bait_set: bait_set.name().to_string(), + bait_territory: bait_set.territory(), + target_territory: target_set.territory(), + genome_size: contigs.iter().map(|(_, _, len)| len).sum(), + counters: hs_counters, + target_depths: hs_target_depths, + zero_coverage_targets: hs_zero_targets, + target_count: hs_target_count, + library_size, + }; + let dir_path = dir("picard").join("hs_metrics"); + std::fs::create_dir_all(&dir_path)?; + let path = dir_path.join(format!("{sample_name}.hs_metrics.txt")); + hs_metrics::write_hs_metrics(&result, &path)?; + record_output("picard CollectHsMetrics", path); + } + + if !qualimap_contigs.is_empty() { + // Restore header order, since the workers ran longest contig first. + qualimap_contigs.sort_by_key(|c| { + order + .iter() + .position(|name| *name == c.name) + .unwrap_or(usize::MAX) + }); + let dir_path = dir("qualimap"); + std::fs::create_dir_all(&dir_path)?; + let results = dir_path.join("genome_results.txt"); + qualimap_output::write_genome_results(&qualimap_contigs, bam_path, &results)?; + record_output("qualimap", results); + qualimap_output::write_raw_data( + &qualimap_contigs, + &dir_path.join("raw_data_qualimapReport"), + )?; + record_output("qualimap", dir_path.join("raw_data_qualimapReport")); + let report = dir_path.join("qualimapReport.html"); + qualimap_output::write_html_report(&qualimap_contigs, &sample_name, &report)?; + record_output("qualimap", report); + } + + if let Some(mut accum) = preseq_total { + let preseq_dir = dir("preseq"); + std::fs::create_dir_all(&preseq_dir)?; + accum.finalize(); + let total_reads = accum.total_fragments; + let n_distinct = accum.n_distinct(); + let histogram = accum.into_histogram(); + match common::preseq::estimate_complexity( + &histogram, + total_reads, + n_distinct, + &config.preseq, + ) { + Ok(preseq_result) => { + let path = preseq_dir.join(format!("{sample_name}.lc_extrap.txt")); + common::preseq::write_output( + &preseq_result, + &path, + config.preseq.confidence_level, + )?; + record_output("preseq", path); + } + Err(e) => ui.warn(&format!("preseq: {e:#}")), + } + } + + Ok(summary::InputSummary { + bam_file: bam_path.to_string(), + status: "success".to_string(), + error: None, + runtime_seconds: 0.0, + counting: None, + dupradar: None, + dna: Some(dna_summary(&result, &bam_stat_result, &thresholds)), + outputs: written, + }) +} + +/// Build the JSON summary block for a `dna` run. +fn dna_summary( + result: &rustqc::dna::mosdepth::MosdepthResult, + bam_stat: &rustqc::common::bam_stat::BamStatResult, + thresholds: &[u32], +) -> summary::DnaSummary { + let genome_length = result.total_length(); + let histogram = + rustqc::dna::mosdepth::merge_histograms(result.contigs.iter().map(|c| &c.histogram)); + + let coverage_thresholds = thresholds + .iter() + .map(|threshold| { + let at_or_above: u64 = histogram + .iter() + .filter(|(depth, _)| *depth >= threshold) + .map(|(_, count)| count) + .sum(); + summary::CoverageThreshold { + threshold: *threshold, + pct_bases: if genome_length == 0 { + 0.0 + } else { + at_or_above as f64 * 100.0 / genome_length as f64 + }, + } + }) + .collect(); + + // Median: walk the depth histogram until half the reference is behind us. + let mut seen = 0u64; + let mut median = 0u32; + for (depth, count) in &histogram { + seen += count; + if seen * 2 >= genome_length { + median = *depth; + break; + } + } + + let duplicate_pct = if bam_stat.total_records == 0 { + 0.0 + } else { + bam_stat.duplicates as f64 * 100.0 / bam_stat.total_records as f64 + }; + + summary::DnaSummary { + genome_length, + covered_bases: result.total_bases(), + mean_coverage: result.mean(), + median_coverage: median, + max_coverage: result.max(), + coverage_thresholds, + total_reads: bam_stat.total_records, + duplicates: bam_stat.duplicates, + duplicate_pct, + } +} + +/// Count the reference's non-N bases, which is Picard's `GENOME_TERRITORY`. +/// +/// The whole reference is read once. Picard does the same, and the figure +/// cannot be taken from the alignment header, which records contig lengths +/// including their N runs. +fn genome_territory(reference: &str) -> Result { + use std::io::BufRead; + + let reader = rustqc::io::open_reader(reference) + .with_context(|| format!("Failed to open reference FASTA: {reference}"))?; + let mut territory = 0u64; + for line in reader.lines() { + let line = line.with_context(|| format!("Failed to read reference FASTA: {reference}"))?; + if line.starts_with('>') { + continue; + } + territory += line.bytes().filter(|b| !matches!(b, b'N' | b'n')).count() as u64; + } + Ok(territory) +} + +/// How many contig depth arrays may be live at once. +/// +/// Each worker holds four bytes per base of its contig, so the largest contig +/// sets the per-worker cost: about 1 GB for GRCh38 chr1. There is no portable +/// way to ask the operating system how much memory is free, so the budget is a +/// fixed 4 GB unless the user overrides it with `--max-depth-workers`. +fn depth_worker_budget(threads: usize, override_value: Option, largest: u64) -> usize { + const BUDGET_BYTES: u64 = 4 * 1024 * 1024 * 1024; + if let Some(value) = override_value { + return value.max(1); } + let per_worker = largest.saturating_mul(4).max(1); + let affordable = (BUDGET_BYTES / per_worker).max(1) as usize; + threads.min(affordable).max(1) } /// Reconstruct the command line for the featureCounts-compatible header comment. @@ -618,6 +1423,7 @@ fn run_rna(args: cli::RnaArgs, ui: &Ui) -> Result<()> { runtime_seconds: 0.0, counting: None, dupradar: None, + dna: None, outputs: vec![], }); } @@ -902,6 +1708,7 @@ impl BamResult { runtime_seconds: self.duration.as_secs_f64(), counting, dupradar, + dna: None, outputs: self .outputs .iter() @@ -1562,7 +2369,7 @@ fn write_rseqc_outputs( if params.config.flagstat.enabled { std::fs::create_dir_all(&samtools_dir)?; let flagstat_path = samtools_dir.join(format!("{}.flagstat", sample_name)); - rna::rseqc::flagstat::write_flagstat(result, &flagstat_path)?; + common::samtools::flagstat::write_flagstat(result, &flagstat_path)?; let p = flagstat_path.display().to_string(); ui.output_item("flagstat", &p); written.push(("flagstat".into(), p)); @@ -1572,7 +2379,7 @@ fn write_rseqc_outputs( if params.config.idxstats.enabled { std::fs::create_dir_all(&samtools_dir)?; let idxstats_path = samtools_dir.join(format!("{}.idxstats", sample_name)); - rna::rseqc::idxstats::write_idxstats(result, bam_header_refs, &idxstats_path)?; + common::samtools::idxstats::write_idxstats(result, bam_header_refs, &idxstats_path)?; let p = idxstats_path.display().to_string(); ui.output_item("idxstats", &p); written.push(("idxstats".into(), p)); @@ -1582,7 +2389,7 @@ fn write_rseqc_outputs( if params.config.samtools_stats.enabled { std::fs::create_dir_all(&samtools_dir)?; let stats_path = samtools_dir.join(format!("{}.stats", sample_name)); - rna::rseqc::stats::write_stats(result, &stats_path)?; + common::samtools::stats::write_stats(result, &stats_path)?; let p = stats_path.display().to_string(); ui.output_item("stats", &p); written.push(("samtools stats".into(), p)); diff --git a/src/rna/mod.rs b/src/rna/mod.rs index 7dbcc094..29b50b64 100644 --- a/src/rna/mod.rs +++ b/src/rna/mod.rs @@ -3,10 +3,12 @@ //! Contains dupRadar duplication rate analysis, featureCounts-compatible output, //! and RSeQC tool reimplementations. -pub mod bam_flags; -pub mod cpp_rng; pub mod dupradar; pub mod featurecounts; -pub mod preseq; pub mod qualimap; pub mod rseqc; + +// These analyses are not RNA-specific and now live in `crate::common`. +// Re-exported here so existing `crate::rna::...` paths and the published +// 0.2.x library surface keep resolving. Drop the shims at 1.0. +pub use crate::common::{bam_flags, cpp_rng, preseq}; diff --git a/src/rna/rseqc/accumulators.rs b/src/rna/rseqc/accumulators.rs index b91a409e..20f2b964 100644 --- a/src/rna/rseqc/accumulators.rs +++ b/src/rna/rseqc/accumulators.rs @@ -11,10 +11,8 @@ use anyhow::Result; use indexmap::IndexMap; use rust_htslib::bam; -use super::bam_stat::{BamStatResult, GcDepthBin}; - -/// Default GC-depth bin size in base pairs (matches upstream samtools default). -const GCD_BIN_SIZE: u64 = 20_000; +// BamStatAccum is read-level and assay-agnostic; it lives in `crate::common` +// and is shared with the dna pipeline. Re-exported so existing paths resolve. use super::common::{self, KnownJunctionSet, ReferenceJunctions}; use super::infer_experiment::{GeneModel, InferExperimentResult}; use super::inner_distance::{ @@ -25,6 +23,7 @@ use super::junction_saturation::SaturationResult; use super::read_distribution::{ChromIntervals, ReadDistributionResult, RegionSets}; use super::read_duplication::ReadDuplicationResult; use super::tin::TinAccum; +pub use crate::common::bam_stat_accum::BamStatAccum; use crate::rna::preseq::PreseqAccum; use crate::rna::bam_flags::*; @@ -116,1200 +115,6 @@ pub struct RseqcConfig { // Per-tool accumulators // =================================================================== -/// bam_stat accumulator — simple flag/MAPQ counting. -/// -/// Also collects the additional counters needed for samtools-compatible -/// flagstat, idxstats, and stats output. -#[derive(Debug)] -pub struct BamStatAccum { - // --- RSeQC bam_stat fields (original) --- - /// Total BAM records seen (primary + secondary + supplementary + unmapped). - pub total_records: u64, - /// Records with QC-fail flag (0x200). - pub qc_failed: u64, - /// Records with duplicate flag (0x400). - pub duplicates: u64, - /// Secondary alignment records (0x100). RSeQC calls these "non-primary". - pub non_primary: u64, - /// Unmapped reads (0x4). - pub unmapped: u64, - /// Mapped reads with MAPQ < cutoff. - pub non_unique: u64, - /// Mapped reads with MAPQ >= cutoff (uniquely mapped). - pub unique: u64, - /// Among unique reads: read1 in a pair. - pub read_1: u64, - /// Among unique reads: read2 in a pair. - pub read_2: u64, - /// Among unique reads: forward strand. - pub forward: u64, - /// Among unique reads: reverse strand. - pub reverse: u64, - /// Among unique reads: has splice junction (CIGAR N). - pub splice: u64, - /// Among unique reads: no splice junctions. - pub non_splice: u64, - /// Among unique reads: in proper pairs (0x2). - pub proper_pairs: u64, - /// Among proper-paired unique reads: mates on different chromosomes. - pub proper_pair_diff_chrom: u64, - - // --- samtools flagstat additional fields --- - /// Secondary alignments (0x100) — counted independently of QC/dup. - pub secondary: u64, - /// Supplementary alignments (0x800) — counted independently of QC/dup. - pub supplementary: u64, - /// All mapped records (not 0x4), regardless of QC/dup. - pub mapped: u64, - /// Paired reads (0x1), regardless of QC/dup. - pub paired_flagstat: u64, - /// Read1 in pair (0x40), regardless of QC/dup — for flagstat. - pub read1_flagstat: u64, - /// Read2 in pair (0x80), regardless of QC/dup — for flagstat. - pub read2_flagstat: u64, - /// First fragments for samtools stats: primary reads that are not "last fragments". - pub first_fragments: u64, - /// Last fragments for samtools stats: primary reads with 0x80 flag. - pub last_fragments: u64, - /// Properly paired reads (0x1 + 0x2), regardless of QC/dup. - pub properly_paired: u64, - /// Both mates mapped (paired + both !unmapped). - pub both_mapped: u64, - /// Singletons (paired, this mapped, mate unmapped). - pub singletons: u64, - /// Paired, both mapped, different reference. - pub mate_diff_chr: u64, - /// Paired, both mapped, different reference, MAPQ >= 5. - pub mate_diff_chr_mapq5: u64, - - // --- samtools idxstats additional fields --- - /// Per-reference (tid) mapped and unmapped counts. - pub chrom_counts: HashMap, - /// Unmapped reads with no reference (tid < 0). - pub unplaced_unmapped: u64, - - // --- samtools stats SN additional fields --- - /// Sum of query sequence lengths for all primary reads (non-secondary, non-supplementary). - pub total_len: u64, - /// Sum of first fragment (read1 or unpaired) sequence lengths. - pub total_first_fragment_len: u64, - /// Sum of last fragment (read2) sequence lengths. - pub total_last_fragment_len: u64, - /// Sum of query lengths for mapped primary reads. - pub bases_mapped: u64, - /// Sum of M/=/X CIGAR operations for mapped primary reads. - pub bases_mapped_cigar: u64, - /// Sum of query lengths for duplicate-flagged primary reads. - pub bases_duplicated: u64, - /// Maximum query sequence length (among primary reads). - pub max_len: u64, - /// Maximum first-fragment sequence length. - pub max_first_fragment_len: u64, - /// Maximum last-fragment sequence length. - pub max_last_fragment_len: u64, - /// Sum of average per-read base qualities (for average-of-averages). - pub quality_sum: f64, - /// Number of reads contributing to quality_sum (primary, non-QC-fail). - pub quality_count: u64, - /// Sum of NM tag values across mapped primary reads. - pub mismatches: u64, - /// Insert size with orientation: abs_tlen → [total, inward, outward, other]. - /// Only one mate per pair contributes (upstream mate), capped at 8000. - pub is_hist: HashMap, - /// Inward-oriented pairs (FR). - pub inward_pairs: u64, - /// Outward-oriented pairs (RF). - pub outward_pairs: u64, - /// Other orientation pairs (FF, RR). - pub other_orientation: u64, - /// Total primary reads (non-secondary, non-supplementary). - pub primary_count: u64, - /// Primary mapped reads count (non-secondary, non-supplementary, !unmapped). - pub primary_mapped: u64, - /// Primary duplicate reads. - pub primary_duplicates: u64, - /// Primary mapped reads with MAPQ = 0 (matching upstream samtools stats). - pub reads_mq0: u64, - /// Primary non-QC-fail mapped paired reads where mate is also mapped. - pub reads_mapped_and_paired: u64, - - // --- samtools stats histogram/distribution fields --- - /// Read length histogram (all primary reads): length → count. - pub rl_hist: HashMap, - /// First fragment read length histogram: length → count. - pub frl_hist: HashMap, - /// Last fragment read length histogram: length → count. - pub lrl_hist: HashMap, - /// MAPQ histogram: primary, mapped, !qcfail, !dup (quality 0-255). - pub mapq_hist: [u64; 256], - /// Per-cycle quality for first fragments (primary, mapped, !qcfail, !dup). - /// Outer: cycle index. Inner: quality value → count (64 buckets covers Q0-Q63). - pub ffq: Vec<[u64; 64]>, - /// Per-cycle quality for last fragments. - pub lfq: Vec<[u64; 64]>, - /// GC content step-function for first fragments, 200 bins (matching samtools ngc=200). - /// Each bin i stores the number of reads with gc_count * 199 / seq_len <= i. - pub gcf: [u64; 200], - /// GC content step-function for last fragments, 200 bins. - pub gcl: [u64; 200], - /// Per-cycle base composition for first fragments (primary, mapped, !qcfail, !dup). - /// [A, C, G, T, N, Other] per cycle. - pub fbc: Vec<[u64; 6]>, - /// Per-cycle base composition for last fragments. - pub lbc: Vec<[u64; 6]>, - /// Per-cycle base composition (read-oriented) for first fragments. - /// Reverse strand reads contribute in reversed cycle order. - pub fbc_ro: Vec<[u64; 6]>, - /// Per-cycle base composition (read-oriented) for last fragments. - pub lbc_ro: Vec<[u64; 6]>, - /// Per-cycle base composition (reverse-complemented for reverse-strand reads, - /// combined first+last fragments). Used for GCT output. [A, C, G, T] only. - pub gcc_rc: Vec<[u64; 4]>, - /// Total base counters for first fragments: [A, C, G, T, N]. - pub ftc: [u64; 5], - /// Total base counters for last fragments: [A, C, G, T, N]. - pub ltc: [u64; 5], - /// Indel distribution by size: length → [insertions, deletions]. - pub id_hist: HashMap, - /// Indels per cycle: cycle → [ins_fwd, ins_rev, del_fwd, del_rev]. - pub ic: Vec<[u64; 4]>, - /// CRC32 checksum sums: [names, sequences, qualities]. - /// Each is the wrapping u32 sum of per-read CRC32 values. - pub chk: [u32; 3], - /// Coverage distribution: depth → number of reference positions at that depth. - /// Populated from a round buffer pileup during sorted BAM processing. - pub cov_hist: HashMap, - /// Circular buffer for coverage pileup, matching upstream samtools design. - /// `cov_buf[cov_buf_idx]` corresponds to reference position `cov_buf_pos`. - /// The buffer grows dynamically to accommodate `max_read_length * 5`. - cov_buf: Vec, - /// Index into `cov_buf` corresponding to `cov_buf_pos`. - cov_buf_idx: usize, - /// Reference position of the element at `cov_buf[cov_buf_idx]`. - cov_buf_pos: i64, - /// Current chromosome tid for round buffer tracking. - cov_buf_tid: i32, - - // --- GC-depth (GCD section) fields --- - /// Accumulated GC-depth bins (one per `GCD_BIN_SIZE`-bp genomic window). - gcd_bins: Vec, - /// Start position of the current GCD bin. - gcd_pos: i64, - /// Chromosome tid of the current GCD bin. - gcd_tid: i32, -} - -impl Default for BamStatAccum { - fn default() -> Self { - Self { - total_records: 0, - qc_failed: 0, - duplicates: 0, - non_primary: 0, - unmapped: 0, - non_unique: 0, - unique: 0, - read_1: 0, - read_2: 0, - forward: 0, - reverse: 0, - splice: 0, - non_splice: 0, - proper_pairs: 0, - proper_pair_diff_chrom: 0, - secondary: 0, - supplementary: 0, - mapped: 0, - paired_flagstat: 0, - read1_flagstat: 0, - read2_flagstat: 0, - first_fragments: 0, - last_fragments: 0, - properly_paired: 0, - both_mapped: 0, - singletons: 0, - mate_diff_chr: 0, - mate_diff_chr_mapq5: 0, - chrom_counts: HashMap::new(), - unplaced_unmapped: 0, - total_len: 0, - total_first_fragment_len: 0, - total_last_fragment_len: 0, - bases_mapped: 0, - bases_mapped_cigar: 0, - bases_duplicated: 0, - max_len: 0, - max_first_fragment_len: 0, - max_last_fragment_len: 0, - quality_sum: 0.0, - quality_count: 0, - mismatches: 0, - is_hist: HashMap::new(), - inward_pairs: 0, - outward_pairs: 0, - other_orientation: 0, - primary_count: 0, - primary_mapped: 0, - primary_duplicates: 0, - reads_mq0: 0, - reads_mapped_and_paired: 0, - rl_hist: HashMap::new(), - frl_hist: HashMap::new(), - lrl_hist: HashMap::new(), - mapq_hist: [0u64; 256], - ffq: Vec::new(), - lfq: Vec::new(), - gcf: [0u64; 200], - gcl: [0u64; 200], - fbc: Vec::new(), - lbc: Vec::new(), - fbc_ro: Vec::new(), - lbc_ro: Vec::new(), - gcc_rc: Vec::new(), - ftc: [0u64; 5], - ltc: [0u64; 5], - id_hist: HashMap::new(), - ic: Vec::new(), - chk: [0u32; 3], - cov_hist: HashMap::new(), - cov_buf: vec![0u32; 1500], // matches upstream samtools: nbases * 5 = 300 * 5 - cov_buf_idx: 0, - cov_buf_pos: 0, - cov_buf_tid: -1, - gcd_bins: Vec::new(), - gcd_pos: -1, - gcd_tid: -1, - } - } -} - -impl BamStatAccum { - /// Process a single BAM record. Called for EVERY record (before counting filters). - /// - /// Collects counters for: - /// - RSeQC bam_stat (original cascade with early returns) - /// - samtools flagstat (counts all records independently) - /// - samtools idxstats (per-reference mapped/unmapped counts) - /// - samtools stats SN section (sequence lengths, quality, insert size, etc.) - pub fn process_read(&mut self, record: &bam::Record, mapq_cut: u8) { - let flags = record.flags(); - self.total_records += 1; - - let is_secondary = flags & BAM_FSECONDARY != 0; - let is_supplementary = flags & BAM_FSUPPLEMENTARY != 0; - let is_unmapped = flags & BAM_FUNMAP != 0; - let is_paired = flags & BAM_FPAIRED != 0; - let is_dup = flags & BAM_FDUP != 0; - let is_qcfail = flags & BAM_FQCFAIL != 0; - let is_primary = !is_secondary && !is_supplementary; - let is_mapped = !is_unmapped; - let tid = record.tid(); - let mapq = record.mapq(); - - // ================================================================= - // samtools flagstat counters (count ALL records, no early returns) - // ================================================================= - if is_secondary { - self.secondary += 1; - } - if is_supplementary { - self.supplementary += 1; - } - if is_mapped { - self.mapped += 1; - } - // samtools stats: "1st fragments" / "last fragments" count primary reads only - // For paired reads: read2 flag -> last, everything else -> 1st - // For SE reads (no PAIRED flag): all counted as 1st fragments - if is_primary { - if flags & BAM_FREAD2 != 0 { - self.last_fragments += 1; - } else { - self.first_fragments += 1; - } - } - // samtools flagstat: paired-read metrics count PRIMARY reads only - // (secondary/supplementary are excluded from paired/read1/read2/properly-paired counts) - if is_paired && is_primary { - self.paired_flagstat += 1; - if flags & BAM_FREAD1 != 0 { - self.read1_flagstat += 1; - } - if flags & BAM_FREAD2 != 0 { - self.read2_flagstat += 1; - } - if flags & BAM_FPROPER_PAIR != 0 { - self.properly_paired += 1; - } - let mate_unmapped = flags & BAM_FMUNMAP != 0; - if is_mapped && !mate_unmapped { - self.both_mapped += 1; - if tid != record.mtid() { - self.mate_diff_chr += 1; - if mapq >= 5 { - self.mate_diff_chr_mapq5 += 1; - } - } - } - if is_mapped && mate_unmapped { - self.singletons += 1; - } - } - - // ================================================================= - // samtools idxstats counters (per-reference) - // ================================================================= - if is_unmapped { - if tid >= 0 { - // Unmapped read placed on a reference (has tid) - self.chrom_counts.entry(tid).or_insert((0, 0)).1 += 1; - } else { - self.unplaced_unmapped += 1; - } - } else if tid >= 0 { - // Mapped read - self.chrom_counts.entry(tid).or_insert((0, 0)).0 += 1; - } - - // ================================================================= - // CHK checksums: computed on ALL reads (including secondary and - // supplementary). Matches samtools stats.c update_checksum() which - // is called before the secondary-read early return. - // ================================================================= - { - let qname = record.qname(); - let name_crc = crc32fast::hash(qname); - self.chk[0] = self.chk[0].wrapping_add(name_crc); - - let seq_len = record.seq_len(); - if seq_len > 0 { - // SAFETY: We access the raw BAM record data to compute CRC32 - // checksums matching samtools' approach. The pointer arithmetic - // replicates htslib's bam_get_seq() macro: - // data + l_qname + (n_cigar << 2) - // The seq_len > 0 guard above ensures sequence data exists. - // The slice length seq_len.div_ceil(2) matches the BAM spec's - // 4-bit encoded sequence format: (seq_len+1)/2 bytes. - let seq_bytes = unsafe { - let inner = record.inner(); - let data = inner.data; - let seq_offset = - inner.core.l_qname as isize + ((inner.core.n_cigar as isize) << 2); - let seq_nbytes = seq_len.div_ceil(2); - std::slice::from_raw_parts(data.offset(seq_offset), seq_nbytes) - }; - let seq_crc = crc32fast::hash(seq_bytes); - self.chk[1] = self.chk[1].wrapping_add(seq_crc); - - let qual = record.qual(); - let qual_crc = crc32fast::hash(qual); - self.chk[2] = self.chk[2].wrapping_add(qual_crc); - } - } - - // Track gc_count from the primary-read per-cycle loop so the GCD - // section below can reuse it without re-scanning the sequence. - let mut primary_gc_count: u64 = 0; - - // ================================================================= - // samtools stats SN counters (primary reads only) - // ================================================================= - if is_primary { - self.primary_count += 1; - let seq_len = record.seq_len() as u64; - let mate_unmapped = flags & BAM_FMUNMAP != 0; - - self.total_len += seq_len; - let is_last_fragment = is_paired && flags & BAM_FREAD2 != 0; - if is_last_fragment { - self.total_last_fragment_len += seq_len; - if seq_len > self.max_last_fragment_len { - self.max_last_fragment_len = seq_len; - } - } else { - self.total_first_fragment_len += seq_len; - if seq_len > self.max_first_fragment_len { - self.max_first_fragment_len = seq_len; - } - } - if seq_len > self.max_len { - self.max_len = seq_len; - } - - // RL/FRL/LRL: read length histograms (all primary reads) - *self.rl_hist.entry(seq_len).or_insert(0) += 1; - if is_last_fragment { - *self.lrl_hist.entry(seq_len).or_insert(0) += 1; - } else { - *self.frl_hist.entry(seq_len).or_insert(0) += 1; - } - - if is_dup { - self.primary_duplicates += 1; - self.bases_duplicated += seq_len; - } - // "reads mapped and paired" for samtools stats: primary, non-QC-fail, - // mapped, paired, mate also mapped - if is_mapped && is_paired && !is_qcfail && !mate_unmapped { - self.reads_mapped_and_paired += 1; - } - if is_mapped { - self.primary_mapped += 1; - self.bases_mapped += seq_len; - - // samtools stats: reads MQ0 counts primary mapped reads with MAPQ=0 - // (upstream stats.c: MQ0 is counted inside collect_orig_read_stats, - // which is only called for IS_ORIGINAL reads = non-secondary, non-supplementary) - if record.mapq() == 0 { - self.reads_mq0 += 1; - } - - // NOTE: bases_mapped_cigar is now computed in the IC/ID CIGAR - // loop below (for all mapped non-secondary reads) to avoid a - // separate full CIGAR traversal here. - - // NM tag (edit distance) - if let Ok(rust_htslib::bam::record::Aux::U8(nm)) = record.aux(b"NM") { - self.mismatches += u64::from(nm); - } else if let Ok(rust_htslib::bam::record::Aux::U16(nm)) = record.aux(b"NM") { - self.mismatches += u64::from(nm); - } else if let Ok(rust_htslib::bam::record::Aux::U32(nm)) = record.aux(b"NM") { - self.mismatches += u64::from(nm); - } else if let Ok(rust_htslib::bam::record::Aux::I8(nm)) = record.aux(b"NM") { - if nm > 0 { - self.mismatches += nm as u64; - } - } else if let Ok(rust_htslib::bam::record::Aux::I16(nm)) = record.aux(b"NM") { - if nm > 0 { - self.mismatches += nm as u64; - } - } else if let Ok(rust_htslib::bam::record::Aux::I32(nm)) = record.aux(b"NM") { - if nm > 0 { - self.mismatches += nm as u64; - } - } - - // Insert size + orientation for paired primary reads where both - // mates are mapped. Matches samtools stats gate: - // IS_PAIRED_AND_MAPPED && IS_ORIGINAL - // if (isize > 0 || tid == mtid) - // Both mates contribute; samtools divides by 2 at output. - // We do the same in write_insert_size() and the SN section. - if is_paired && !mate_unmapped { - let tid = record.tid(); - let mtid = record.mtid(); - let tlen = record.insert_size(); - let abs_tlen = tlen.unsigned_abs(); - - if abs_tlen > 0 || tid == mtid { - let pos = record.pos(); - let mpos = record.mpos(); - - // Compute orientation (only meaningful for same-chromosome) - let pos_fst = mpos - pos; - let is_fst: i64 = if flags & BAM_FREAD1 != 0 { 1 } else { -1 }; - let is_fwd: i64 = if flags & BAM_FREVERSE != 0 { -1 } else { 1 }; - let is_mfwd: i64 = if flags & BAM_FMREVERSE != 0 { -1 } else { 1 }; - - // orientation_idx: 1=inward, 2=outward, 3=other - let orientation_idx = if is_fwd * is_mfwd > 0 { - self.other_orientation += 1; - 3usize - } else if is_fst * pos_fst > 0 { - if is_fst * is_fwd > 0 { - self.inward_pairs += 1; - 1usize - } else { - self.outward_pairs += 1; - 2usize - } - } else if is_fst * pos_fst < 0 { - if is_fst * is_fwd > 0 { - self.outward_pairs += 1; - 2usize - } else { - self.inward_pairs += 1; - 1usize - } - } else { - self.inward_pairs += 1; - 1usize - }; - - if abs_tlen > 0 { - // Cap at MAX_INSERT_SIZE (8000), matching - // samtools stats which accumulates overflow - // into the cap bucket. - let capped = abs_tlen.min(8000); - let entry = self.is_hist.entry(capped).or_insert([0; 4]); - entry[0] += 1; // total - entry[orientation_idx] += 1; - } - } - } - } - - // Average quality for primary non-QC-fail reads. - // Upstream samtools stats computes per-BASE quality average: - // sum of all individual base qualities / total bases. - // (Not a per-read average of averages.) - if !is_qcfail { - let quals = record.qual(); - if !quals.is_empty() { - let base_qual_sum: f64 = quals.iter().map(|&q| f64::from(q)).sum::(); - self.quality_sum += base_qual_sum; - self.quality_count += quals.len() as u64; - } - } - - // ============================================================= - // MAPQ histogram: primary + mapped + !qcfail + !dup - // (matches samtools stats.c:1239 five-flag exclusion) - // ============================================================= - if is_mapped && !is_qcfail && !is_dup { - self.mapq_hist[mapq as usize] += 1; - } - - // ============================================================= - // Per-cycle quality & base composition histograms: - // FFQ/LFQ, FBC/LBC, GCF/GCL, FTC/LTC, FBC_RO/LBC_RO - // - // Upstream samtools stats includes duplicates, unmapped, and - // qcfail reads in these histograms (collect_orig_read_stats - // has no such checks). Only secondary+supplementary are - // excluded (via IS_ORIGINAL), which is already handled by - // the outer is_primary guard. - // ============================================================= - { - let is_reverse = flags & BAM_FREVERSE != 0; - - let seq = record.seq(); - let quals = record.qual(); - let read_len = seq.len(); - - // Determine which arrays to use (first vs last fragment) - // If paired: read2 = last, read1 = first. If SE: all = first. - let (qual_arr, base_arr, base_ro_arr, gc_arr, tc_arr) = if is_last_fragment { - ( - &mut self.lfq, - &mut self.lbc, - &mut self.lbc_ro, - &mut self.gcl, - &mut self.ltc, - ) - } else { - ( - &mut self.ffq, - &mut self.fbc, - &mut self.fbc_ro, - &mut self.gcf, - &mut self.ftc, - ) - }; - - // Ensure per-cycle arrays are large enough - if read_len > qual_arr.len() { - qual_arr.resize(read_len, [0u64; 64]); - } - if read_len > base_arr.len() { - base_arr.resize(read_len, [0u64; 6]); - } - if read_len > base_ro_arr.len() { - base_ro_arr.resize(read_len, [0u64; 6]); - } - if read_len > self.gcc_rc.len() { - self.gcc_rc.resize(read_len, [0u64; 4]); - } - - let mut gc_count: u64 = 0; - - // Pre-built lookup tables for the per-cycle inner loop, - // avoiding branches and match overhead on every base. - // - // BAM 4-bit encoding: A=1, C=2, G=4, T=8, N=15, others=0,3,5..14 - // BASE_IDX[nibble] → 0=A, 1=C, 2=G, 3=T, 4=N, 5=Other - const BASE_IDX: [u8; 16] = [5, 0, 1, 5, 2, 5, 5, 5, 3, 5, 5, 5, 5, 5, 5, 4]; - // RC_IDX[base_idx] → reverse-complement base_idx (A↔T, C↔G) - // Only meaningful for base_idx 0-3 (ACGT). Index 4/5 not used. - const RC_IDX: [u8; 6] = [3, 2, 1, 0, 4, 5]; // A→T, C→G, G→C, T→A - - // Hoist the is_reverse branch outside the inner loop so the - // compiler can version the loop and potentially auto-vectorize - // each variant independently. - if !is_reverse { - for i in 0..read_len { - let q = quals[i] as usize; - qual_arr[i][q.min(63)] += 1; - - let base_idx = BASE_IDX[seq.encoded_base(i) as usize] as usize; - base_arr[i][base_idx] += 1; - base_ro_arr[i][base_idx] += 1; - if base_idx < 4 { - self.gcc_rc[i][base_idx] += 1; - } - if base_idx == 1 || base_idx == 2 { - gc_count += 1; - } - if base_idx < 5 { - tc_arr[base_idx] += 1; - } - } - } else { - for i in 0..read_len { - let ro_cycle = read_len - 1 - i; - let q = quals[i] as usize; - qual_arr[ro_cycle][q.min(63)] += 1; - - let base_idx = BASE_IDX[seq.encoded_base(i) as usize] as usize; - base_arr[i][base_idx] += 1; - base_ro_arr[ro_cycle][base_idx] += 1; - if base_idx < 4 { - self.gcc_rc[ro_cycle][RC_IDX[base_idx] as usize] += 1; - } - if base_idx == 1 || base_idx == 2 { - gc_count += 1; - } - if base_idx < 5 { - tc_arr[base_idx] += 1; - } - } - } - - // Save gc_count for GCD section below (avoids re-scanning the sequence). - primary_gc_count = gc_count; - - // GC content: cumulative step function with ngc=200 bins. - // Matches samtools stats.c:925-941. For a read with gc_count G/C - // bases out of read_len total, increment bins gc_idx_min..gc_idx_max. - let ngc: usize = 200; - if let (Some(gc_idx_min), Some(gc_idx_max)) = ( - (gc_count as usize * (ngc - 1)).checked_div(read_len), - ((gc_count as usize + 1) * (ngc - 1)).checked_div(read_len), - ) { - let gc_idx_max = gc_idx_max.min(ngc - 1); - for item in gc_arr.iter_mut().take(gc_idx_max).skip(gc_idx_min) { - *item += 1; - } - } - } - } // if is_primary - - // ============================================================= - // Indel distribution (ID) and indels per cycle (IC) from CIGAR. - // - // Upstream samtools stats calls count_indels() AFTER the - // secondary-read early return (line 1206-1210) and the - // IS_UNMAPPED return (line 1255), but OUTSIDE IS_ORIGINAL(). - // This means: all mapped, non-secondary reads are included - // (supplementary, duplicate, qcfail all contribute). - // - // IC uses first-fragment/last-fragment read order (not - // forward/reverse strand) and read-oriented cycle indices, - // matching upstream count_indels(). - // ============================================================= - // ============================================================= - // Combined single-CIGAR-pass block for IC/ID (indel distribution), - // bases_mapped_cigar, and COV (coverage ring-buffer pileup). - // - // Both IC/ID and COV apply to the same read set (mapped, - // non-secondary). Merging them into one CIGAR traversal - // eliminates two redundant record.cigar() calls per read. - // - // IC/ID: Upstream samtools stats calls count_indels() outside - // IS_ORIGINAL() — supplementary/dup/qcfail all contribute. - // IC uses first/last-fragment order and read-oriented cycles. - // - // COV: Circular-buffer pileup; buffer flushed up to read start - // before CIGAR walk; M/=/X blocks inserted as ranges. - // Buffer grown to max_read_len * 5 as needed. - // ============================================================= - if is_mapped && !is_secondary { - use rust_htslib::bam::record::Cigar as C; - let is_reverse = flags & BAM_FREVERSE != 0; - let read_len = record.seq_len(); - let tid = record.tid(); - let pos = record.pos(); // 0-based - - // Upstream order: paired ? (read1?FIRST:0)+(read2?LAST:0) : FIRST - let order: u32 = if is_paired { - (if flags & BAM_FREAD1 != 0 { 1 } else { 0 }) - + (if flags & BAM_FREAD2 != 0 { 2 } else { 0 }) - } else { - 1 // unpaired → FIRST - }; - - // COV buffer setup (must happen before CIGAR walk). - // Skip reads with no sequence (upstream samtools early-return). - let do_cov = read_len > 0; - let buf_size = if do_cov { - // Grow buffer to max_read_len * 5 if needed. - // When growing, linearise the circular data just like - // upstream samtools: copy [idx..old_size] then [0..idx] - // into a fresh buffer, and reset idx to 0. - let need = read_len * 5; - if need > self.cov_buf.len() { - let old_size = self.cov_buf.len(); - let mut new_buf = vec![0u32; need]; - let head = old_size - self.cov_buf_idx; - new_buf[..head].copy_from_slice(&self.cov_buf[self.cov_buf_idx..]); - new_buf[head..head + self.cov_buf_idx] - .copy_from_slice(&self.cov_buf[..self.cov_buf_idx]); - self.cov_buf = new_buf; - self.cov_buf_idx = 0; - } - let bs = self.cov_buf.len(); - // Flush entire buffer on chromosome change - if tid != self.cov_buf_tid { - self.flush_cov_buf_all(); - self.cov_buf_tid = tid; - self.cov_buf_pos = pos; - self.cov_buf_idx = 0; - } - // Flush positions from cov_buf_pos up to read start - self.cov_buf_flush_to(pos, bs); - bs - } else { - 0 - }; - - // Single CIGAR traversal serving IC/ID + bases_mapped_cigar + COV - let cigar = record.cigar(); - let mut icycle: usize = 0; - let mut cigar_mapped: u64 = 0; - let mut ref_pos = pos; - - for op in cigar.iter() { - match op { - C::Ins(n) => { - let ncig = *n as usize; - let len = *n as u64; - cigar_mapped += len; // I counts toward bases_mapped_cigar - - // ID: indel size distribution - let id_entry = self.id_hist.entry(len).or_insert([0; 2]); - id_entry[0] += 1; // insertions - - // IC: indels per cycle (read-oriented index) - let idx = if is_reverse { - read_len.saturating_sub(icycle + ncig) - } else { - icycle - }; - if idx >= self.ic.len() { - self.ic.resize(idx + 1, [0u64; 4]); - } - if order == 1 { - self.ic[idx][0] += 1; // ins_1st - } - if order == 2 { - self.ic[idx][1] += 1; // ins_2nd - } - - icycle += ncig; // I advances query cycle; ref unchanged - // COV: I consumes no reference positions - } - C::Del(n) => { - let len = *n as u64; - // ID: indel size distribution - let id_entry = self.id_hist.entry(len).or_insert([0; 2]); - id_entry[1] += 1; // deletions - - // IC: indels per cycle (read-oriented index) - let idx = if is_reverse { - if icycle == 0 { - // Discard meaningless deletions at cycle 0 - // (upstream: "if (idx<0) continue;") - ref_pos += *n as i64; // still advance ref for COV - continue; - } - read_len.saturating_sub(icycle + 1) - } else { - if icycle == 0 { - ref_pos += *n as i64; - continue; - } - icycle - 1 - }; - if idx >= self.ic.len() { - self.ic.resize(idx + 1, [0u64; 4]); - } - if order == 1 { - self.ic[idx][2] += 1; // del_1st - } - if order == 2 { - self.ic[idx][3] += 1; // del_2nd - } - // D does NOT advance query cycle; does advance ref - ref_pos += *n as i64; - } - C::Match(n) | C::Equal(n) | C::Diff(n) => { - let len = *n as u64; - cigar_mapped += len; // M/=/X count toward bases_mapped_cigar - icycle += *n as usize; - // COV: M/=/X consumes reference positions - if do_cov { - let end = ref_pos + *n as i64; - self.cov_buf_insert(ref_pos, end, buf_size); - ref_pos = end; - } else { - ref_pos += *n as i64; - } - } - C::RefSkip(n) => { - ref_pos += *n as i64; // N advances ref (COV skips it) - } - C::SoftClip(n) => { - icycle += *n as usize; // S advances query cycle - // COV: S consumes no reference positions - } - C::HardClip(_) | C::Pad(_) => {} - } - } - self.bases_mapped_cigar += cigar_mapped; - } // if is_mapped && !is_secondary (IC/ID + COV combined) - - // ============================================================= - // GCD: GC-depth accumulation (no-reference path). - // - // Matches upstream samtools stats without --ref-seq: bins of - // GCD_BIN_SIZE bp, depth incremented for each read, GC fraction - // accumulated from the read's sequence. - // - // Included reads: mapped, non-secondary (same as COV). - // - // NOTE: gc_count_for_gcd is set from the primary-read per-cycle - // loop above (when is_primary is true), or computed here only for - // non-primary mapped reads, avoiding a redundant full sequence scan. - // ============================================================= - if is_mapped && !is_secondary { - let tid = record.tid(); - let pos = record.pos(); - let seq_len = record.seq_len(); - - if seq_len > 0 { - // Start a new bin on: first read, chromosome change, or - // read beyond current bin boundary. - let new_bin = self.gcd_pos < 0 - || tid != self.gcd_tid - || pos - self.gcd_pos > GCD_BIN_SIZE as i64; - - if new_bin { - self.gcd_bins.push(GcDepthBin { gc: 0.0, depth: 0 }); - self.gcd_pos = pos; - self.gcd_tid = tid; - } - - // Increment depth and accumulate GC fraction from read seq. - if let Some(bin) = self.gcd_bins.last_mut() { - bin.depth += 1; - // For primary reads, gc_count was already computed in the - // per-cycle base loop above. For non-primary mapped reads - // (supplementary, etc.) compute it here from the sequence. - let gc_count: u32 = if is_primary { - primary_gc_count as u32 - } else { - let seq = record.seq(); - let mut count: u32 = 0; - for i in 0..seq_len { - let base = seq.encoded_base(i); - if base == 2 || base == 4 { - count += 1; - } - } - count - }; - bin.gc += gc_count as f32 / seq_len as f32; - } - } - } // if is_mapped && !is_secondary (GCD) - - // ================================================================= - // RSeQC bam_stat cascade (original logic, with early returns) - // ================================================================= - - // 1. QC-failed - if is_qcfail { - self.qc_failed += 1; - return; - } - - // 2. Duplicate - if is_dup { - self.duplicates += 1; - return; - } - - // 3. Secondary (non-primary) — NOT supplementary - if is_secondary { - self.non_primary += 1; - return; - } - - // 4. Unmapped - if is_unmapped { - self.unmapped += 1; - return; - } - - // 5. MAPQ classification - if mapq < mapq_cut { - self.non_unique += 1; - return; - } - - // Uniquely mapped - self.unique += 1; - - if flags & BAM_FREAD1 != 0 { - self.read_1 += 1; - } - if flags & BAM_FREAD2 != 0 { - self.read_2 += 1; - } - if flags & BAM_FREVERSE != 0 { - self.reverse += 1; - } else { - self.forward += 1; - } - - // Splice detection: CIGAR N operation - let has_splice = record - .cigar() - .iter() - .any(|op| matches!(op, rust_htslib::bam::record::Cigar::RefSkip(_))); - if has_splice { - self.splice += 1; - } else { - self.non_splice += 1; - } - - // Proper pair analysis - if is_paired && flags & BAM_FPROPER_PAIR != 0 { - self.proper_pairs += 1; - if tid != record.mtid() { - self.proper_pair_diff_chrom += 1; - } - } - } - - /// Flush all remaining positions in the coverage round buffer into cov_hist. - /// Must be called after processing all reads (or when switching chromosomes). - /// Flush the circular buffer from `cov_buf_pos` up to (but not including) `pos`. - /// Each slot's depth is recorded in `cov_hist` and the slot is zeroed. - /// Matches upstream `round_buffer_flush` logic from samtools stats.c. - fn cov_buf_flush_to(&mut self, pos: i64, buf_size: usize) { - if pos - self.cov_buf_pos >= buf_size as i64 { - // Gap exceeds buffer size. Match upstream samtools exactly: - // flush `size - 1` positions (from cov_buf_pos to - // cov_buf_pos + size - 2), leaving the LAST slot untouched. - // Then advance idx by `size - 1` and jump pos. - // - // Upstream (stats.c round_buffer_flush lines 334-366): - // pos = rbuf.pos + size - 1; // cap at last slot - // ito = lidx2ridx(start, size, rbuf.pos, pos-1); - // // flush from start to ito (size-1 slots) - // rbuf.start = lidx2ridx(start, size, rbuf.pos, pos); - // rbuf.pos = new_pos; - let flush_count = buf_size - 1; // flush all but the last slot - for _ in 0..flush_count { - let depth = self.cov_buf[self.cov_buf_idx]; - if depth > 0 { - *self.cov_hist.entry(depth).or_insert(0) += 1; - self.cov_buf[self.cov_buf_idx] = 0; - } - self.cov_buf_idx += 1; - if self.cov_buf_idx >= buf_size { - self.cov_buf_idx = 0; - } - } - // idx now points to the ONE unflushed slot (the last position - // in the old window). Jump pos to the new read position. - self.cov_buf_pos = pos; - } else { - // Normal case: flush slot by slot. - while self.cov_buf_pos < pos { - let depth = self.cov_buf[self.cov_buf_idx]; - if depth > 0 { - *self.cov_hist.entry(depth).or_insert(0) += 1; - self.cov_buf[self.cov_buf_idx] = 0; - } - self.cov_buf_idx += 1; - if self.cov_buf_idx >= buf_size { - self.cov_buf_idx = 0; - } - self.cov_buf_pos += 1; - } - } - } - - /// Insert a contiguous reference range `[from, to)` into the circular buffer, - /// incrementing depth for each position. The range must fit within `buf_size`. - fn cov_buf_insert(&mut self, from: i64, to: i64, buf_size: usize) { - for ref_pos in from..to { - // Map ref_pos to buffer index: offset from cov_buf_idx by (ref_pos - cov_buf_pos) - let offset = (ref_pos - self.cov_buf_pos) as usize; - let idx = (self.cov_buf_idx + offset) % buf_size; - self.cov_buf[idx] += 1; - } - } - - /// Flush the entire circular buffer and reset tracking state. - pub fn flush_cov_buf_all(&mut self) { - for slot in self.cov_buf.iter_mut() { - if *slot > 0 { - *self.cov_hist.entry(*slot).or_insert(0) += 1; - *slot = 0; - } - } - self.cov_buf_idx = 0; - self.cov_buf_pos = 0; - self.cov_buf_tid = -1; - } - - /// Merge another accumulator into this one. - pub fn merge(&mut self, mut other: BamStatAccum) { - // Flush any remaining positions in the other's round buffer into its - // cov_hist before merging. Without this, positions still in the - // round buffer would be silently lost during parallel merges. - other.flush_cov_buf_all(); - - // RSeQC bam_stat fields - self.total_records += other.total_records; - self.qc_failed += other.qc_failed; - self.duplicates += other.duplicates; - self.non_primary += other.non_primary; - self.unmapped += other.unmapped; - self.non_unique += other.non_unique; - self.unique += other.unique; - self.read_1 += other.read_1; - self.read_2 += other.read_2; - self.forward += other.forward; - self.reverse += other.reverse; - self.splice += other.splice; - self.non_splice += other.non_splice; - self.proper_pairs += other.proper_pairs; - self.proper_pair_diff_chrom += other.proper_pair_diff_chrom; - - // samtools flagstat fields - self.secondary += other.secondary; - self.supplementary += other.supplementary; - self.mapped += other.mapped; - self.paired_flagstat += other.paired_flagstat; - self.read1_flagstat += other.read1_flagstat; - self.read2_flagstat += other.read2_flagstat; - self.first_fragments += other.first_fragments; - self.last_fragments += other.last_fragments; - self.properly_paired += other.properly_paired; - self.both_mapped += other.both_mapped; - self.singletons += other.singletons; - self.mate_diff_chr += other.mate_diff_chr; - self.mate_diff_chr_mapq5 += other.mate_diff_chr_mapq5; - - // samtools idxstats fields - for (tid, (m, u)) in other.chrom_counts { - let entry = self.chrom_counts.entry(tid).or_insert((0, 0)); - entry.0 += m; - entry.1 += u; - } - self.unplaced_unmapped += other.unplaced_unmapped; - - // samtools stats SN fields - self.total_len += other.total_len; - self.total_first_fragment_len += other.total_first_fragment_len; - self.total_last_fragment_len += other.total_last_fragment_len; - self.bases_mapped += other.bases_mapped; - self.bases_mapped_cigar += other.bases_mapped_cigar; - self.bases_duplicated += other.bases_duplicated; - if other.max_len > self.max_len { - self.max_len = other.max_len; - } - if other.max_first_fragment_len > self.max_first_fragment_len { - self.max_first_fragment_len = other.max_first_fragment_len; - } - if other.max_last_fragment_len > self.max_last_fragment_len { - self.max_last_fragment_len = other.max_last_fragment_len; - } - self.quality_sum += other.quality_sum; - self.quality_count += other.quality_count; - self.mismatches += other.mismatches; - for (isize_val, counts) in other.is_hist { - let entry = self.is_hist.entry(isize_val).or_insert([0; 4]); - for i in 0..4 { - entry[i] += counts[i]; - } - } - self.inward_pairs += other.inward_pairs; - self.outward_pairs += other.outward_pairs; - self.other_orientation += other.other_orientation; - self.primary_count += other.primary_count; - self.primary_mapped += other.primary_mapped; - self.primary_duplicates += other.primary_duplicates; - self.reads_mq0 += other.reads_mq0; - self.reads_mapped_and_paired += other.reads_mapped_and_paired; - - // Histogram/distribution fields - for (len, count) in other.rl_hist { - *self.rl_hist.entry(len).or_insert(0) += count; - } - for (len, count) in other.frl_hist { - *self.frl_hist.entry(len).or_insert(0) += count; - } - for (len, count) in other.lrl_hist { - *self.lrl_hist.entry(len).or_insert(0) += count; - } - for i in 0..256 { - self.mapq_hist[i] += other.mapq_hist[i]; - } - - // Per-cycle quality arrays (FFQ/LFQ) - merge_vec_arrays(&mut self.ffq, other.ffq); - merge_vec_arrays(&mut self.lfq, other.lfq); - - // GC content distributions (200 bins) - for i in 0..200 { - self.gcf[i] += other.gcf[i]; - self.gcl[i] += other.gcl[i]; - } - - // Per-cycle base composition (FBC/LBC and read-oriented) - merge_vec_arrays(&mut self.fbc, other.fbc); - merge_vec_arrays(&mut self.lbc, other.lbc); - merge_vec_arrays(&mut self.fbc_ro, other.fbc_ro); - merge_vec_arrays(&mut self.lbc_ro, other.lbc_ro); - merge_vec_arrays(&mut self.gcc_rc, other.gcc_rc); - - // Total base counters - for i in 0..5 { - self.ftc[i] += other.ftc[i]; - self.ltc[i] += other.ltc[i]; - } - - // Indel distribution - for (len, counts) in other.id_hist { - let entry = self.id_hist.entry(len).or_insert([0; 2]); - entry[0] += counts[0]; - entry[1] += counts[1]; - } - - // Indels per cycle - merge_vec_arrays(&mut self.ic, other.ic); - - // CHK checksums (wrapping u32 addition) - for i in 0..3 { - self.chk[i] = self.chk[i].wrapping_add(other.chk[i]); - } - - // COV histogram (additive merge) - for (depth, count) in other.cov_hist { - *self.cov_hist.entry(depth).or_insert(0) += count; - } - - // GCD bins (concatenate — bins from different chromosome workers - // are independent and will be sorted during output). - self.gcd_bins.append(&mut other.gcd_bins); - } -} - // ------------------------------------------------------------------- // infer_experiment accumulator // ------------------------------------------------------------------- @@ -2327,112 +1132,10 @@ fn point_in(region_map: &HashMap, chrom: &str, point: u6 region_map.get(chrom).is_some_and(|ci| ci.contains(point)) } -// =================================================================== -// Merge helpers for Vec<[u64; N]> per-cycle arrays -// =================================================================== - -/// Merge two `Vec<[u64; N]>` arrays element-wise, extending target if shorter. -fn merge_vec_arrays(target: &mut Vec<[u64; N]>, source: Vec<[u64; N]>) { - if source.len() > target.len() { - target.resize(source.len(), [0u64; N]); - } - for (i, arr) in source.into_iter().enumerate() { - for j in 0..N { - target[i][j] += arr[j]; - } - } -} - // =================================================================== // Converter methods: accumulator → result types for output functions // =================================================================== -impl BamStatAccum { - /// Convert accumulated counters into a `BamStatResult` for output. - pub fn into_result(mut self) -> BamStatResult { - // Flush remaining positions in the coverage round buffer - self.flush_cov_buf_all(); - BamStatResult { - // RSeQC bam_stat fields - total_records: self.total_records, - qc_failed: self.qc_failed, - duplicates: self.duplicates, - non_primary: self.non_primary, - unmapped: self.unmapped, - non_unique: self.non_unique, - unique: self.unique, - read_1: self.read_1, - read_2: self.read_2, - forward: self.forward, - reverse: self.reverse, - splice: self.splice, - non_splice: self.non_splice, - proper_pairs: self.proper_pairs, - proper_pair_diff_chrom: self.proper_pair_diff_chrom, - // samtools flagstat fields - secondary: self.secondary, - supplementary: self.supplementary, - mapped: self.mapped, - paired_flagstat: self.paired_flagstat, - read1_flagstat: self.read1_flagstat, - read2_flagstat: self.read2_flagstat, - first_fragments: self.first_fragments, - last_fragments: self.last_fragments, - properly_paired: self.properly_paired, - both_mapped: self.both_mapped, - singletons: self.singletons, - mate_diff_chr: self.mate_diff_chr, - mate_diff_chr_mapq5: self.mate_diff_chr_mapq5, - // samtools idxstats fields - chrom_counts: self.chrom_counts, - unplaced_unmapped: self.unplaced_unmapped, - // samtools stats SN fields - total_len: self.total_len, - total_first_fragment_len: self.total_first_fragment_len, - total_last_fragment_len: self.total_last_fragment_len, - bases_mapped: self.bases_mapped, - bases_mapped_cigar: self.bases_mapped_cigar, - bases_duplicated: self.bases_duplicated, - max_len: self.max_len, - max_first_fragment_len: self.max_first_fragment_len, - max_last_fragment_len: self.max_last_fragment_len, - quality_sum: self.quality_sum, - quality_count: self.quality_count, - mismatches: self.mismatches, - is_hist: self.is_hist, - inward_pairs: self.inward_pairs, - outward_pairs: self.outward_pairs, - other_orientation: self.other_orientation, - primary_count: self.primary_count, - primary_mapped: self.primary_mapped, - primary_duplicates: self.primary_duplicates, - reads_mq0: self.reads_mq0, - reads_mapped_and_paired: self.reads_mapped_and_paired, - // Histogram/distribution fields - rl_hist: self.rl_hist, - frl_hist: self.frl_hist, - lrl_hist: self.lrl_hist, - mapq_hist: self.mapq_hist, - ffq: self.ffq, - lfq: self.lfq, - gcf: self.gcf, - gcl: self.gcl, - fbc: self.fbc, - lbc: self.lbc, - fbc_ro: self.fbc_ro, - lbc_ro: self.lbc_ro, - gcc_rc: self.gcc_rc, - ftc: self.ftc, - ltc: self.ltc, - id_hist: self.id_hist, - ic: self.ic, - chk: self.chk, - cov_hist: self.cov_hist, - gcd_bins: self.gcd_bins, - } - } -} - impl InferExpAccum { /// Convert accumulated strand counts into an `InferExperimentResult`. pub fn into_result(self) -> InferExperimentResult { diff --git a/src/rna/rseqc/genebody_coverage.rs b/src/rna/rseqc/genebody_coverage.rs new file mode 100644 index 00000000..0bb20202 --- /dev/null +++ b/src/rna/rseqc/genebody_coverage.rs @@ -0,0 +1,386 @@ +//! RSeQC `geneBody_coverage.py` reimplementation. +//! +//! Measures how evenly reads cover the gene body from the 5' to the 3' end, +//! which is the standard way RNA degradation and 3' bias show up. +//! +//! # Upstream semantics +//! +//! Taken from RSeQC 5.0.4's `geneBody_coverage.py`, whose behaviour is not +//! obvious from its output: +//! +//! Each transcript's mRNA bases are laid out end to end, exon by exon, in +//! one-based genome coordinates. Transcripts shorter than +//! [`DEFAULT_MIN_MRNA_LENGTH`] are skipped entirely. The remainder are reduced +//! to exactly 100 positions by [`percentile_positions`], which is a linear +//! interpolation between neighbouring bases, rounded. Those are *positions*, +//! not windows: coverage is read at 100 single bases, and everything between +//! them is never looked at. +//! +//! A base counts a read when the read is not deleted at that position and is +//! not QC-failed, secondary, unmapped or duplicate-flagged. +//! +//! A transcript on the minus strand has its 100 values reversed, so index 0 is +//! always the 5' end. The reversal happens per transcript, before summing, so +//! a gene set mixing strands still aggregates 5' to 3'. + +use std::collections::BTreeMap; + +/// Transcripts with fewer mRNA bases than this are skipped. +pub const DEFAULT_MIN_MRNA_LENGTH: usize = 100; + +/// Number of points each transcript is reduced to. +pub const POINTS: usize = 100; + +/// One transcript reduced to the positions its coverage is read at. +#[derive(Debug, Clone)] +pub struct TranscriptPoints { + /// Contig the transcript sits on. + pub chrom: String, + /// `+` or `-`. + pub reverse: bool, + /// One-based genome positions, in the order the exons were concatenated. + /// Ascending for a normal transcript, but not for one whose exons + /// overlap, because RSeQC never sorts them. + pub positions: Vec, +} + +/// Round to the nearest integer, ties going to the even one. +/// +/// RSeQC interpolates with Python's `round`, which rounds half to even. +/// Rounding away from zero instead moves an interpolated position by one base +/// wherever the interpolation lands exactly halfway, which on the project +/// fixture changes the 23rd percentile's coverage by a whole read. +fn round_half_to_even(value: f64) -> i64 { + let floor = value.floor(); + if (value - floor - 0.5).abs() < 1e-9 { + if (floor as i64) % 2 == 0 { + floor as i64 + } else { + floor as i64 + 1 + } + } else { + value.round() as i64 + } +} + +/// Reduce a sorted list of values to 100 percentile points. +/// +/// This is RSeQC's `mystat.percentile_list`. A list shorter than 100 is +/// returned unchanged rather than padded, which is why transcripts below the +/// minimum length are dropped beforehand: they would otherwise contribute +/// fewer than 100 values and shift every later index. +pub fn percentile_positions(sorted: &[i64]) -> Vec { + if sorted.is_empty() { + return Vec::new(); + } + if sorted.len() < POINTS { + return sorted.to_vec(); + } + let last = (sorted.len() - 1) as f64; + (1..=POINTS) + .map(|i| { + let k = last * i as f64 / 100.0; + let floor = k.floor(); + let ceil = k.ceil(); + if (floor - ceil).abs() < f64::EPSILON { + sorted[k as usize] + } else { + let low = sorted[floor as usize] as f64 * (ceil - k); + let high = sorted[ceil as usize] as f64 * (k - floor); + round_half_to_even(low + high) + } + }) + .collect() +} + +/// Build the percentile points for one transcript from its exons. +/// +/// Exons are half-open zero-based, as BED and the GTF parser give them; the +/// positions returned are one-based, as RSeQC works in, and are in the order +/// the exons were given rather than sorted. +pub fn transcript_points( + chrom: &str, + reverse: bool, + exons: &[(i64, i64)], + min_length: usize, +) -> Option { + // Exons are concatenated in the order given and deliberately **not** + // sorted. RSeQC does not sort either, and its percentile interpolation + // assumes a sorted list, so a transcript with overlapping exons feeds it a + // list that dips backwards. The interpolated positions then repeat, and + // the curve for that transcript is computed over fewer than 100 distinct + // bases. Sorting here would be the more sensible thing and would disagree + // with the tool this reproduces; see `GeneBodyCoverage::add_transcript` + // for what the repeats then do. + let mut bases: Vec = Vec::new(); + for (start, end) in exons { + bases.extend((*start + 1)..=*end); + } + if bases.len() < min_length { + return None; + } + Some(TranscriptPoints { + chrom: chrom.to_string(), + reverse, + positions: percentile_positions(&bases), + }) +} + +/// Aggregated coverage across every transcript, 5' to 3'. +#[derive(Debug, Clone)] +pub struct GeneBodyCoverage { + /// One total per percentile point. + pub totals: Vec, + /// Transcripts that contributed. + pub transcripts: u64, +} + +impl Default for GeneBodyCoverage { + fn default() -> Self { + Self { + totals: vec![0; POINTS], + transcripts: 0, + } + } +} + +impl GeneBodyCoverage { + /// Fold one transcript's per-position coverage in. + /// + /// Only the **distinct** positions contribute, which matters on short + /// transcripts where the interpolation repeats a base. RSeQC keeps its + /// coverage in a dictionary keyed by position, so repeats collapse and the + /// list it aggregates is shorter than 100; it then adds those values at + /// indices 0 upwards, which quietly compresses that transcript's curve + /// towards the 5' end. + /// + /// That is a defect in RSeQC rather than a design decision, and it is + /// reproduced here on purpose: the point of this module is to agree with + /// the tool people already have. On the project fixture one gene of eight + /// has 100 points across 88 distinct positions, and correcting the + /// behaviour moves the aggregate curve. Anyone deciding to diverge should + /// do so knowingly, which is why this is spelled out rather than hidden. + pub fn add_transcript(&mut self, points: &TranscriptPoints, coverage: &BTreeMap) { + let mut distinct: Vec = points.positions.clone(); + distinct.sort_unstable(); + distinct.dedup(); + + let mut values: Vec = distinct + .iter() + .map(|p| coverage.get(p).copied().unwrap_or(0)) + .collect(); + if points.reverse { + values.reverse(); + } + for (index, value) in values.iter().enumerate() { + if index < self.totals.len() { + self.totals[index] += value; + } + } + self.transcripts += 1; + } + + /// Pearson's moment coefficient of skewness over the aggregated curve, + /// which RSeQC reports alongside it. + pub fn skewness(&self) -> f64 { + let n = self.totals.len(); + if n < 2 { + return 0.0; + } + let values: Vec = self.totals.iter().map(|v| *v as f64).collect(); + let mean = values.iter().sum::() / n as f64; + let variance = values.iter().map(|v| (v - mean).powi(2)).sum::() / n as f64; + let sd = variance.sqrt(); + if sd == 0.0 { + return 0.0; + } + values + .iter() + .map(|v| ((v - mean) / sd).powi(3)) + .sum::() + / n as f64 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_list_shorter_than_a_hundred_is_returned_unchanged() { + let values = vec![1i64, 2, 3]; + assert_eq!(percentile_positions(&values), values); + } + + #[test] + fn a_hundred_values_map_to_themselves_offset_by_one() { + // With exactly 100 values the interpolation lands on each in turn, + // starting at the second, because the points run from 1 to 100 rather + // than 0 to 99. + let values: Vec = (1..=100).collect(); + let points = percentile_positions(&values); + assert_eq!(points.len(), 100); + assert_eq!(points[0], 2); + assert_eq!(points[99], 100); + } + + #[test] + fn interpolation_rounds_between_neighbours() { + let values: Vec = (0..200).map(|i| i * 10).collect(); + let points = percentile_positions(&values); + assert_eq!(points.len(), 100); + // Ascending and inside the input range. + assert!(points.windows(2).all(|w| w[0] <= w[1])); + assert!(*points.first().unwrap() >= 0); + assert!(*points.last().unwrap() <= 1990); + } + + #[test] + fn exons_are_laid_out_end_to_end_in_one_based_coordinates() { + // Two exons of 60 bases each: 120 mRNA bases, so the transcript is + // kept and reduced to 100 points. + let points = transcript_points("chr1", false, &[(0, 60), (100, 160)], 100).unwrap(); + assert_eq!(points.positions.len(), 100); + assert_eq!(*points.positions.first().unwrap(), 2, "one-based"); + // Non-overlapping exons concatenate in ascending order, so these do + // come out sorted; overlapping ones do not. + assert!(points.positions.windows(2).all(|w| w[0] <= w[1])); + } + + #[test] + fn overlapping_exons_leave_the_positions_unsorted() { + // Two exons covering 1..=600 and 401..=900. Concatenated in order the + // base list dips backwards at the junction, and because RSeQC never + // sorts it the interpolated positions dip too. That is the mechanism + // behind the repeated positions seen on real annotations; whether a + // given transcript actually repeats one depends on where the + // interpolation lands. + let points = transcript_points("chr1", false, &[(0, 600), (400, 900)], 100).unwrap(); + assert_eq!(points.positions.len(), 100); + assert!( + points.positions.windows(2).any(|w| w[0] > w[1]), + "the concatenation is not sorted, so the points should dip" + ); + } + + #[test] + fn interpolation_can_land_inside_an_intron() { + // Two exons either side of a 40 base intron. The percentile points are + // interpolated between neighbouring *mRNA* bases, and the pair + // straddling the junction interpolates between genome positions 60 and + // 101, so a point can fall in the intron and coverage is then read at + // a base the transcript does not contain. + // + // This is RSeQC's behaviour, not a defect introduced here, and + // reproducing it is the point. It is asserted so that anyone + // "correcting" it later sees the intent. + let points = transcript_points("chr1", false, &[(0, 60), (100, 160)], 100).unwrap(); + let intronic: Vec = points + .positions + .iter() + .copied() + .filter(|p| (61..=100).contains(p)) + .collect(); + assert!( + !intronic.is_empty(), + "expected at least one interpolated point in the intron" + ); + } + + #[test] + fn a_transcript_below_the_minimum_length_is_dropped() { + assert!(transcript_points("chr1", false, &[(0, 50)], 100).is_none()); + assert!(transcript_points("chr1", false, &[(0, 100)], 100).is_some()); + } + + #[test] + fn a_minus_strand_transcript_is_reversed_before_aggregating() { + let points = TranscriptPoints { + chrom: "chr1".into(), + reverse: true, + positions: vec![10, 20, 30], + }; + let coverage: BTreeMap = [(10, 1), (20, 2), (30, 3)].into_iter().collect(); + let mut aggregate = GeneBodyCoverage { + totals: vec![0; 3], + transcripts: 0, + }; + aggregate.add_transcript(&points, &coverage); + assert_eq!( + aggregate.totals, + vec![3, 2, 1], + "index 0 is the 5' end, which for a minus-strand gene is the last position" + ); + } + + #[test] + fn a_plus_strand_transcript_keeps_its_order() { + let points = TranscriptPoints { + chrom: "chr1".into(), + reverse: false, + positions: vec![10, 20, 30], + }; + let coverage: BTreeMap = [(10, 1), (20, 2), (30, 3)].into_iter().collect(); + let mut aggregate = GeneBodyCoverage { + totals: vec![0; 3], + transcripts: 0, + }; + aggregate.add_transcript(&points, &coverage); + assert_eq!(aggregate.totals, vec![1, 2, 3]); + } + + #[test] + fn an_uncovered_position_contributes_zero_rather_than_being_skipped() { + let points = TranscriptPoints { + chrom: "chr1".into(), + reverse: false, + positions: vec![10, 20, 30], + }; + let coverage: BTreeMap = [(10, 5)].into_iter().collect(); + let mut aggregate = GeneBodyCoverage { + totals: vec![0; 3], + transcripts: 0, + }; + aggregate.add_transcript(&points, &coverage); + assert_eq!(aggregate.totals, vec![5, 0, 0]); + } + + #[test] + fn interpolation_ties_round_to_the_even_position() { + assert_eq!(round_half_to_even(10.5), 10, "10 is even"); + assert_eq!(round_half_to_even(11.5), 12, "12 is even"); + assert_eq!(round_half_to_even(10.4), 10); + assert_eq!(round_half_to_even(10.6), 11); + } + + #[test] + fn repeated_positions_collapse_and_shorten_the_curve() { + // Three points across two distinct positions: RSeQC aggregates two + // values at indices 0 and 1, leaving index 2 untouched. + let points = TranscriptPoints { + chrom: "chr1".into(), + reverse: false, + positions: vec![10, 10, 20], + }; + let coverage: BTreeMap = [(10, 5), (20, 7)].into_iter().collect(); + let mut aggregate = GeneBodyCoverage { + totals: vec![0; 3], + transcripts: 0, + }; + aggregate.add_transcript(&points, &coverage); + assert_eq!( + aggregate.totals, + vec![5, 7, 0], + "the repeat collapses rather than contributing twice" + ); + } + + #[test] + fn a_flat_curve_has_no_skew() { + let aggregate = GeneBodyCoverage { + totals: vec![10; 100], + transcripts: 1, + }; + assert_eq!(aggregate.skewness(), 0.0); + } +} diff --git a/src/rna/rseqc/mod.rs b/src/rna/rseqc/mod.rs index a4730ccb..2513d674 100644 --- a/src/rna/rseqc/mod.rs +++ b/src/rna/rseqc/mod.rs @@ -7,14 +7,36 @@ pub mod accumulators; pub mod common; pub mod plots; -pub mod bam_stat; -pub mod flagstat; -pub mod idxstats; +pub mod genebody_coverage; pub mod infer_experiment; pub mod inner_distance; pub mod junction_annotation; pub mod junction_saturation; pub mod read_distribution; pub mod read_duplication; -pub mod stats; +pub mod read_gc; pub mod tin; + +// bam_stat and the samtools writers are read-level and assay-agnostic; they +// now live in `crate::common`. Re-exported so existing +// `crate::rna::rseqc::...` paths and the published 0.2.x library surface +// keep resolving. Drop the shims at 1.0. +pub use crate::common::bam_stat; +pub use crate::common::samtools::{flagstat, idxstats, stats}; + +#[cfg(test)] +mod compat_tests { + //! Guards the re-export shims that keep the published 0.2.x paths alive. + //! These are compile-time assertions; there is nothing to observe at runtime. + + #[test] + fn moved_modules_are_still_reachable_from_their_old_paths() { + let _: fn( + &crate::rna::rseqc::bam_stat::BamStatResult, + &std::path::Path, + ) -> anyhow::Result<()> = crate::rna::rseqc::flagstat::write_flagstat; + let _ = crate::rna::rseqc::accumulators::BamStatAccum::default(); + let _: u16 = crate::rna::bam_flags::BAM_FDUP; + let _: Option<&crate::rna::preseq::PreseqAccum> = None; + } +} diff --git a/src/rna/rseqc/read_gc.rs b/src/rna/rseqc/read_gc.rs new file mode 100644 index 00000000..6d9a75ca --- /dev/null +++ b/src/rna/rseqc/read_gc.rs @@ -0,0 +1,220 @@ +//! RSeQC `read_GC.py` reimplementation. +//! +//! Reports the distribution of GC content across reads, which is how library +//! preparation bias and contamination show up before any annotation is +//! involved. +//! +//! # Upstream semantics +//! +//! Taken from RSeQC 5.0.4's `qcmodule.SAM.readGC`. A read is skipped when it +//! is unmapped, QC-failed, or below the mapping quality cutoff, which defaults +//! to 30. Secondary alignments are not excluded. +//! +//! GC percentage is `(C + G)` over the **full sequence length**, so an `N` pads +//! the denominator without ever counting as GC and pulls the percentage down. +//! Dividing by the called bases instead is the more defensible measurement and +//! the wrong one to report under this name. The result is formatted to two +//! decimals, and reads sharing a formatted value share a bin. + +use std::collections::BTreeMap; + +use rust_htslib::bam; + +use crate::common::bam_flags::*; + +/// The histogram key for a percentage: the value scaled by 100 and rounded +/// half to even. +/// +/// RSeQC formats with `%4.2f`, which rounds ties to the even digit. A read +/// with 9 of 32 bases G or C is exactly 28.125 percent and lands in the 28.12 +/// bin, not 28.13. Rounding away from zero puts it in the wrong bin and, on +/// the project fixture, moves fourteen reads. +fn bin_key(percent: f64) -> u32 { + let scaled = percent * 100.0; + let floor = scaled.floor(); + let value = if (scaled - floor - 0.5).abs() < 1e-9 { + if (floor as i64) % 2 == 0 { + floor + } else { + floor + 1.0 + } + } else { + scaled.round() + }; + value.max(0.0) as u32 +} + +/// Accumulates the read GC distribution. +/// +/// Percentages are keyed by their value scaled by 100 and rounded, so the map +/// is exact rather than relying on float keys. +#[derive(Debug, Default, Clone)] +pub struct ReadGcAccum { + /// Read counts keyed by GC percentage times 100. + counts: BTreeMap, + /// Mapped reads seen. + pub reads: u64, + /// Reads whose sequence held no called base at all. + pub reads_without_bases: u64, +} + +impl ReadGcAccum { + /// A new, empty accumulator. + pub fn new() -> Self { + Self::default() + } + + /// Offer one record. + pub fn process_read(&mut self, record: &bam::Record, mapq_cut: u8) { + let flags = record.flags(); + if flags & BAM_FUNMAP != 0 || flags & BAM_FQCFAIL != 0 || record.mapq() < mapq_cut { + return; + } + self.reads += 1; + + let sequence = record.seq().as_bytes(); + if sequence.is_empty() { + self.reads_without_bases += 1; + return; + } + let gc = sequence + .iter() + .filter(|b| matches!(b.to_ascii_uppercase(), b'G' | b'C')) + .count() as f64; + + let percent = gc / sequence.len() as f64 * 100.0; + *self.counts.entry(bin_key(percent)).or_insert(0) += 1; + } + + /// Fold another accumulator in. + pub fn merge(&mut self, other: ReadGcAccum) { + for (key, count) in other.counts { + *self.counts.entry(key).or_insert(0) += count; + } + self.reads += other.reads; + self.reads_without_bases += other.reads_without_bases; + } + + /// The distribution as `(percentage, count)` pairs, ascending. + pub fn distribution(&self) -> Vec<(f64, u64)> { + self.counts + .iter() + .map(|(key, count)| (f64::from(*key) / 100.0, *count)) + .collect() + } + + /// Reads that landed in the histogram. + pub fn counted(&self) -> u64 { + self.counts.values().sum() + } + + /// Mean GC percentage across the counted reads. + pub fn mean(&self) -> f64 { + let total = self.counted(); + if total == 0 { + return 0.0; + } + self.counts + .iter() + .map(|(key, count)| f64::from(*key) / 100.0 * *count as f64) + .sum::() + / total as f64 + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rust_htslib::bam::record::{Cigar, CigarString, Record}; + + /// RSeQC's default mapping quality cutoff. + const CUTOFF: u8 = 30; + + fn record(sequence: &[u8], flags: u16, mapq: u8) -> Record { + let mut r = Record::new(); + let cigar = CigarString(vec![Cigar::Match(sequence.len() as u32)]); + let quality = vec![30u8; sequence.len()]; + r.set(b"q", Some(&cigar), sequence, &quality); + r.set_flags(flags); + r.set_mapq(mapq); + r + } + + #[test] + fn n_pads_the_denominator_rather_than_being_excluded() { + let mut accum = ReadGcAccum::new(); + // Two of five bases are G or C, and the N counts towards the length, + // so this is 40 percent rather than the 50 that dropping it would give. + accum.process_read(&record(b"ACGTN", 0, 60), CUTOFF); + assert_eq!(accum.distribution(), vec![(40.0, 1)]); + } + + #[test] + fn unmapped_and_qc_failed_reads_are_skipped() { + let mut accum = ReadGcAccum::new(); + accum.process_read(&record(b"GGGG", BAM_FUNMAP, 60), CUTOFF); + accum.process_read(&record(b"GGGG", BAM_FQCFAIL, 60), CUTOFF); + assert_eq!(accum.reads, 0); + assert!(accum.distribution().is_empty()); + } + + #[test] + fn reads_below_the_mapping_quality_cutoff_are_skipped() { + let mut accum = ReadGcAccum::new(); + accum.process_read(&record(b"GGCC", 0, 29), CUTOFF); + assert_eq!(accum.reads, 0, "29 is below the default cutoff of 30"); + accum.process_read(&record(b"GGCC", 0, 30), CUTOFF); + assert_eq!(accum.reads, 1, "30 itself passes"); + } + + #[test] + fn secondary_alignments_still_count() { + let mut accum = ReadGcAccum::new(); + accum.process_read(&record(b"GGCC", BAM_FSECONDARY, 60), CUTOFF); + assert_eq!( + accum.reads, 1, + "RSeQC excludes neither secondaries nor duplicates" + ); + assert_eq!(accum.distribution(), vec![(100.0, 1)]); + } + + #[test] + fn ties_round_to_the_even_digit_as_printf_does() { + // 9 of 32 bases is exactly 28.125 percent, which "%4.2f" writes as + // 28.12; rounding away from zero would give 28.13. + let mut sequence = vec![b'G'; 9]; + sequence.extend(std::iter::repeat_n(b'A', 23)); + let mut accum = ReadGcAccum::new(); + accum.process_read(&record(&sequence, 0, 60), CUTOFF); + assert_eq!(accum.distribution(), vec![(28.12, 1)]); + } + + #[test] + fn percentages_are_kept_to_two_decimals() { + let mut accum = ReadGcAccum::new(); + // One G in three bases is 33.333..., reported as 33.33. + accum.process_read(&record(b"AGT", 0, 60), CUTOFF); + assert_eq!(accum.distribution(), vec![(33.33, 1)]); + } + + #[test] + fn merging_adds_the_distributions() { + let mut a = ReadGcAccum::new(); + a.process_read(&record(b"GGCC", 0, 60), CUTOFF); + let mut b = ReadGcAccum::new(); + b.process_read(&record(b"GGCC", 0, 60), CUTOFF); + b.process_read(&record(b"AATT", 0, 60), CUTOFF); + a.merge(b); + assert_eq!(a.distribution(), vec![(0.0, 1), (100.0, 2)]); + assert_eq!(a.reads, 3); + } + + #[test] + fn the_mean_weights_by_read_count() { + let mut accum = ReadGcAccum::new(); + accum.process_read(&record(b"GGCC", 0, 60), CUTOFF); + accum.process_read(&record(b"AATT", 0, 60), CUTOFF); + accum.process_read(&record(b"AATT", 0, 60), CUTOFF); + assert!((accum.mean() - 100.0 / 3.0).abs() < 1e-9); + } +} diff --git a/src/summary.rs b/src/summary.rs index 2f1320a6..05f18254 100644 --- a/src/summary.rs +++ b/src/summary.rs @@ -44,6 +44,11 @@ pub struct InputSummary { /// dupRadar summary (if successful and enabled). #[serde(skip_serializing_if = "Option::is_none")] pub dupradar: Option, + /// DNA depth-of-coverage summary (if this was a `dna` run). + /// + /// An input carries either the RNA fields above or this one, never both. + #[serde(skip_serializing_if = "Option::is_none")] + pub dna: Option, /// List of output files written. pub outputs: Vec, } @@ -90,6 +95,39 @@ pub struct DupradarSummary { pub slope: Option, } +/// Depth of coverage summary for a single alignment file. +#[derive(Debug, Serialize)] +pub struct DnaSummary { + /// Total reference bases across all contigs. + pub genome_length: u64, + /// Sum of per-base depth, that is total bases covered. + pub covered_bases: u64, + /// Mean depth across the reference. + pub mean_coverage: f64, + /// Median per-base depth. + pub median_coverage: u32, + /// Highest per-base depth seen. + pub max_coverage: u32, + /// Percentage of reference bases at or above each requested threshold, + /// in the order the thresholds were requested. + pub coverage_thresholds: Vec, + /// Total records seen. + pub total_reads: u64, + /// Duplicate-flagged records. + pub duplicates: u64, + /// Duplicate rate as a percentage of total records. + pub duplicate_pct: f64, +} + +/// Percentage of the reference covered at or above one depth threshold. +#[derive(Debug, Serialize)] +pub struct CoverageThreshold { + /// The threshold itself, in reads (for example 10 for 10X). + pub threshold: u32, + /// Percentage of reference bases at or above it. + pub pct_bases: f64, +} + /// A single output file written during processing. #[derive(Debug, Serialize)] pub struct OutputFile { diff --git a/tests/create_dna_test_data.sh b/tests/create_dna_test_data.sh new file mode 100755 index 00000000..d9762868 --- /dev/null +++ b/tests/create_dna_test_data.sh @@ -0,0 +1,141 @@ +#!/usr/bin/env bash +# Regenerate the DNA test inputs and the reference outputs they are compared against. +# +# Inputs come from nf-core/test-datasets (a real human chr22 slice, 40 kb). +# The upstream BAM is not duplicate-marked, so this script marks duplicates +# with samtools; RustQC requires duplicate-marked input. +# +# The reference outputs are produced by the pinned tool versions recorded in +# tests/expected/dna/VERSIONS.txt. Regenerating with a different version will +# make the parity tests fail, which is the intended behaviour: fixtures and +# tool versions travel together. +set -euo pipefail + +MOSDEPTH_VERSION="0.3.14" +PICARD_VERSION="3.4.0" +QUALIMAP_VERSION="2.3" +SAMTOOLS_VERSION="1.24" + +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +data="$here/data/dna" +expected="$here/expected/dna" +base="https://raw.githubusercontent.com/nf-core/test-datasets/modules/data/genomics/homo_sapiens" + +have() { command -v "$1" >/dev/null || { echo "missing tool: $1" >&2; exit 1; }; } +have samtools; have mosdepth; have curl; have java; have unzip + +check_version() { + local tool="$1" want="$2" got + got="$($tool --version 2>&1 | head -1 | grep -oE '[0-9]+\.[0-9]+(\.[0-9]+)?' | head -1)" + if [[ "$got" != "$want" ]]; then + echo "$tool version $got does not match the pinned $want" >&2 + echo "Install the pinned version, or update VERSIONS.txt and the fixtures together." >&2 + exit 1 + fi +} +check_version samtools "$SAMTOOLS_VERSION" +check_version mosdepth "$MOSDEPTH_VERSION" + +mkdir -p "$data" "$expected" +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT + +curl -sSfL -o "$tmp/upstream.bam" "$base/illumina/bam/test.paired_end.sorted.bam" +curl -sSfL -o "$data/genome.fasta" "$base/genome/genome.fasta" +curl -sSfL -o "$data/genome.fasta.fai" "$base/genome/genome.fasta.fai" +curl -sSfL -o "$data/targets.bed" "$base/genome/genome.multi_intervals.bed" + +# Mark duplicates: name-sort, add mate tags, coordinate-sort, then markdup. +samtools sort -n -o "$tmp/ns.bam" "$tmp/upstream.bam" +samtools fixmate -m "$tmp/ns.bam" "$tmp/fm.bam" +samtools sort -o "$tmp/cs.bam" "$tmp/fm.bam" +samtools markdup -S "$tmp/cs.bam" "$data/test.dna.bam" +samtools index "$data/test.dna.bam" + +mosdepth --by 500 --thresholds 1,5,10,15,20,30,50 "$expected/test" "$data/test.dna.bam" + +# Picard is a jar rather than a command, so it is fetched by version instead of +# version-checked. The JVM locale is pinned: a French default locale writes +# "3,531312" where an English one writes "3.531312", which would make the +# fixtures depend on the machine that produced them. +picard_jar="$tmp/picard-$PICARD_VERSION.jar" +curl -sSfL -o "$picard_jar" \ + "https://github.com/broadinstitute/picard/releases/download/$PICARD_VERSION/picard.jar" +picard() { + java -Duser.language=en -Duser.country=US -jar "$picard_jar" "$@" 2>/dev/null +} + +picard CollectWgsMetrics \ + -I "$data/test.dna.bam" \ + -O "$expected/test.wgs_metrics.txt" \ + -R "$data/genome.fasta" + +picard CollectInsertSizeMetrics \ + -I "$data/test.dna.bam" \ + -O "$expected/test.insert_size_metrics.txt" \ + -H "$tmp/insert_size_histogram.pdf" + +# The chart output needs R, so it goes to the scratch directory and is not +# compared against; only the two metrics tables are fixtures. +# Picard consumes interval lists rather than BED, so the targets are converted +# with Picard's own tool. The two conventions differ: BED is zero-based +# half-open, an interval list one-based inclusive. +picard CreateSequenceDictionary -R "$data/genome.fasta" -O "$tmp/genome.dict" +picard BedToIntervalList \ + -I "$data/targets.bed" \ + -O "$tmp/targets.interval_list" \ + -SD "$tmp/genome.dict" + +picard CollectHsMetrics \ + -I "$data/test.dna.bam" \ + -O "$expected/test.hs_metrics.txt" \ + -R "$data/genome.fasta" \ + -BI "$tmp/targets.interval_list" \ + -TI "$tmp/targets.interval_list" + +picard CollectGcBiasMetrics \ + -I "$data/test.dna.bam" \ + -O "$expected/test.gc_bias.detail_metrics.txt" \ + -S "$expected/test.gc_bias.summary_metrics.txt" \ + -CHART "$tmp/gc_bias.pdf" \ + -R "$data/genome.fasta" + +# Picard stamps a start time and the full command line, absolute paths and all, +# into the first four lines of every metrics file. Those are dropped: they would +# change on every regeneration and say nothing about the numbers. The +# "## METRICS CLASS" and "## HISTOGRAM" markers further down are part of the +# format and are kept. +for f in "$expected/test.wgs_metrics.txt" "$expected/test.insert_size_metrics.txt" \ + "$expected/test.gc_bias.detail_metrics.txt" "$expected/test.gc_bias.summary_metrics.txt" \ + "$expected/test.hs_metrics.txt"; do + sed -e '/^## htsjdk\.samtools\.metrics\.StringHeader$/d' -e '/^# /d' "$f" \ + | sed -e '/./,$!d' > "$f.tmp" && mv "$f.tmp" "$f" +done +samtools stats "$data/test.dna.bam" > "$expected/test.stats.txt" +samtools flagstat "$data/test.dna.bam" > "$expected/test.flagstat.txt" +samtools idxstats "$data/test.dna.bam" > "$expected/test.idxstats.txt" + +# Qualimap ships as a zip rather than a single jar, and its launcher passes +# -XX:MaxPermSize, which modern JVMs reject, so the main class is invoked +# directly. The locale is pinned for the same reason as Picard's. +curl -sSfL -o "$tmp/qualimap.zip" \ + "https://bitbucket.org/kokonech/qualimap/downloads/qualimap_v$QUALIMAP_VERSION.zip" +unzip -q -o "$tmp/qualimap.zip" -d "$tmp" +qm_dir="$tmp/qualimap_v$QUALIMAP_VERSION" +java -Duser.language=en -Duser.country=US -Xmx2G \ + -cp "$qm_dir/qualimap.jar:$qm_dir/lib/*" \ + org.bioinfo.ngs.qc.qualimap.main.NgsSmartMain bamqc \ + -bam "$data/test.dna.bam" -outdir "$tmp/qualimap" -nt 1 >/dev/null 2>&1 + +mkdir -p "$expected/qualimap" +# The Input section records the absolute paths it was run with, which would +# make the fixture depend on the machine that produced it. +grep -v "bam file =\|outfile =" "$tmp/qualimap/genome_results.txt" \ + > "$expected/qualimap/genome_results.txt" +cp -R "$tmp/qualimap/raw_data_qualimapReport" "$expected/qualimap/" + +printf 'mosdepth\t%s\nsamtools\t%s\npicard\t%s\nqualimap\t%s\n' \ + "$MOSDEPTH_VERSION" "$SAMTOOLS_VERSION" "$PICARD_VERSION" "$QUALIMAP_VERSION" \ + > "$expected/VERSIONS.txt" + +echo "Regenerated $(find "$data" "$expected" -type f | wc -l | tr -d ' ') files." diff --git a/tests/data/dna/genome.fasta b/tests/data/dna/genome.fasta new file mode 100644 index 00000000..b0ea69be --- /dev/null +++ b/tests/data/dna/genome.fasta @@ -0,0 +1,668 @@ +>chr22 +ACTCAAGATAATGATGAGTAAAGAATATATTTCTAACAACAAAAAGGAAATTTGATAGTA +TTTCTAAAGACAAAAAGGAAATTTGTATTCACATTCAGTTAGTCATTCCACCAGAATGAC +TTCATCACACAATATTTTGTGACAAGAACCTGAACAGCCTCATGTTTTACAATATTCTTT +TCATCTTTTATTATATGCACCAAAATTTTCTTTTTTAAATTTTCTTGAACCTCTAAATCT +ACTTTAAAAATTTACCTGATACACTTTTTAAATGGACAAATGCTGAAGGTAGCTGTGTAT +ACAAATGTGACTAGAAGGAAAAAGATGATGTAGAAATACAATAACTCCTTGAGTTGATCA +TTCTGATTGGCATTTATAGAGTAGAAATGTTTTGTAATTACAGAGGAAAAAAGATGGCCT +TTCCTTCAACAGTTATGAGCCGTCAGAATTTTCAAAAATATTGCATTTTGACAATGTAGT +TTCTAGTTTGACAATGATATATTTATCTTCAAAACCAGGAAAATGTAGATAAGGATTTGG +TTTTATAATATTTAAATTCTTATTAAAATGTATAATAAAATTGTTTTCCCCATCACTTTA +TTCTTCTGTAAGTTATTTTACGTTTAAAATGTAAACAAATAAAAATAAGTAAATAAACAG +TAGCAGCTTCTTTTCCTGGTGAATCGAGGATTGAGTATGTATTATATCTTTCCTGGACTA +TTGGAATAACCTCTCCCTCCTTCCACAGAGAAGCCATAATAATCTTTATGAAATACAAAT +CAAATCATGGTATTCATTCTTTAAATAGTTATCAATAAAAATAAAATCCCAACTTTATAC +CCTGTTCTGCAAATTTTAACGTGGTCTGAATTCAGCTTACATTTCTTCTTTCCCTTGTCT +ATTGCCCATCAGGCTCACTGGCCTTATTCCTTCACACCAAACTAGTTATTTCCGGGGTGG +GAGGAAGGCTTGCAGTGTTTTCTCCATCTGCAATAGTCTTTCCCAAATCTTAGTGTGGAT +AAAGTTTCCTTCTTGTTACTTGAATCACAAATACTATGTTCTTAGTCATTCTCTGTTACA +TCATCCAGAGTACATTATATCAATTTTCCAATATTTTTATTTATTTGATTTCCCACTATA +ACAGAGGCTCTGTTAGTGCAGGGTCTTTTACTCTTTTGTAATCCCAACAGCAAGAACAAA +ACAAGGTACATAGTACATATTTAATAAATACCTGTTGAACAAATATGTGCCAGTAATATT +TCTTCATGCTGCTGAATAAGTTAACAGCATATAAACACATACAAACCAAGTGGCATGGAT +GTCTGCTTTGATTTTTAGCCATTTAAAAATATACGTAACCCATCCTAAGGGGTTTATATT +TGTTTTGCATAATACATTAATATGTACTCATTATTCATTACACAGTTAATATATCTATAT +TTGCAGGGAATATACATTGCTTGGAATTATACAAAAAAATATTATTTTTCGTTTTCTAAT +ATTCAGGATACAGTGTTTTAATGGGGGTGTTTCTTCATTCTTTTTTTCTTACTGGTTTTT +ACTTTTTAAATTTGAAAGCCTTGCAGTGATCATAAGGATCTGTTCAGGCAAAGAACATGA +AAGAGTTTAAATTTTTATCATTTTAGTGTTTCTTATTCTCTATATCAAAAACATTCACAG +GTAAGTTAACAAGATCCTCATCAGGAGGAAAAGTAAATTGTTCACTACCATCCTCTAGTA +TCCTAATCTGGTCTTGTTGTTGGCTAACTTCAGCAGTTACTATTCTGTGATTGGTGTAAT +ATTAACCAAATAAATTACTGGATTTGTTCCACAAATATTATATCTTAGATTGGTTCTTTC +CTGTCTCTGAAAATAAAGTCTTGCAATGAGAATAAATTATTTTACAACAGTTAATTAGCA +ATGTAAAGTTTATTGAAAATGTATTTGCTTTTTTTGTAAATCATCTGTGAATCCAGAGGG +GAAAAATATGACAAAGAAAGCTATATAAGATATTATTTTATTTTACAGAGTAACAGACTA +GCTAGAGACAATGAATTAAGGGAAAATGACAAAGAACAGCTCAAAGCAATTTCTACACGA +GATCCTCTCTCTGAAATCACTGCGCAGGAGAAAGATTTTCTATGGACCACAGGTAAGTGC +TAAAATGGAGATTCTCTGTTTCTTTTTCTTTATTACAGAAAAAATAACTGACTTTGGCTG +ATCTCAGCATGTTTTTACCATACCTATTAGAATAAATGAAGCAGAATTTACATGATTTTT +AAACTATAAACATTGCCTTTTTAAAAACAATGGCTGTAAATTGATATTTGTAGAAAATCA +TACTACATTTGTAGTTGGCACATTAAATGCTTTTTCTTACTCTGAATTCCTGATATGACT +TTCTTTAGGATTGTTTAAAATATTCTAGTAGTTTTAGGTCAATTTAGATGTGATTTAGTT +GCTCTAGATATTATAATTTTTAGGGGTTCCCTTTCATTTTTTTCTTACGTTTCTTCAAAT +AGTATAATGCCTTATTTTCATTTATGAAGAAATTACCCTGCTGTTGGTGATACGGGTATA +TTTAAATAAACCAGTTGCAGTGCATTTTTGCAGAAAGTCCATTAAGACATAAATTTTGTC +CAGTAACCACAGTAGAAGTGGTGACTCTATGATTCATTCATGTTGCATAAGTAGGTGAAA +AATATGAGCTATATTCTGTCTGTTAAATGGAATTCTAGAGATGAAGTAGCCCAGGTAAAT +GTATGTTTGAGATTACTAGATAACTGTTGTACAAATTGGTATGTCACTTAAATTGTTTTC +TCTCAGAAAGTCCACATAAATAAATGAAATAGACTAATAATAGTAATATGGTGTAGAAAA +AACTCCCTTAACATTATTTCCATAGATAAAACTAATTAGAACTGTAAATTCTAAGGAGAT +TATTTATCTAAACTAATTTTAAAATCAGAAGTTAAGGCAGTGTTTTAGATGGCTCATTCA +CAACTATCTTTCCCCTTTAAATATGATTTATTGTCTTTCTCATACACAGATGTATTGCTT +GGTAAAAGATTGGCCTCCAATCAAACCTGAACAGGCTATGGAACTTCTGGACTGTAATTA +CCCAGATCCTATGGTTCGAAGTTTTGCTGTTCAGTGCTTGGAAAAATATTTAACAGATAA +CAAACTTTCTCAGTATTTAATTCAGCTAGTACAGGTAAAATAATGTAAAATAGTGAATAA +TGTTTAATTACAATAATAATTTATTTTAGATCCATACAACTTCCTTTTAAAAAACCTACT +GCACTAACTAGTTTTATGCTTAAAAAAAATTATTACCAGTAATATCCACTTTCTTTCTGA +AAAAATTTTCTTTAGATCGGCCATGCAGAAACTGAACCTGATTTGTTTTTTTTGAATCAC +CTAGGTCCTAAAATATGAACAATATTTGGATAACTTGCTTGTGAGATTTTTACTGAAGAA +AGCATTGACTAATCAAAGGATTGGGCACTTTTTCTTTTGGCATTTAAAGTAAGTCTAATT +ATTTTCCCATTAAATTCTTAAGGTACATATTACTTGCTTTCTTAATAGATTTATAAATAT +GTATTACTTATATACTTTTGTTTATGTTTGGCTGGAAGAGTTTTCCATACTAAAACTATT +TTGTACCAGTGATGAGCTTCTCAACTTTTGCTCTTTGAAATTTAAAAAGTAATAAATTCA +AAACTAAATTTCAGTCATGAATGAGAGCTTAAATATTTTTAAAGATTTTTGTTCTACTTA +AGTAAAATTTTCTAGGTCCAGATGAATATTGCTGTAGGTTTCACTGTGTGTATGGATTAA +AATATCCCCAAAAAAAGAAAAAAAATGTTTTACCTTGAGATTCAGAACAATAATGTCAAA +CTCCCGTGGTTCTTACTGAAAAACAAGCTAATTAAGAATAAAAAATGTTTTGTAGAATGT +GATATATGCAGTACTCAAAAGTTACAGGTCATAAACCATATAACTTTTCATAAATTTAGA +AACAGATTTATATCTAATATGATATTTTAAGTGTTAAAATTTAAAAATGGAACCCAGAAG +TTAAGTTGAAAACAAGAAGCGTAGACGTGTGTCAGAAGAGTCAAACAGCATTCACTGAGC +GCTTTGTTCCCTCCCTCTTCATTTGATTATTTTTGTGCTCAATTTCCTTTTTTCATGCTT +TTATATCTTGTACTGAGATTAGTCAATGAAAACTAGTTGAAATAAACCTAAAAACTAGAT +GTTTATTTAATCACATATTCAGGAACTACCTGAAACTCATGGTGGTTTTGCTTCTAAATT +ACAGGTTTTGAATAATGTTATTATTAGTATGATTGTAACATTTATTGGATTTCAAAAATG +AGTGTTTAAATTGTTTAGCAAAGATTATTTGTATACTGATTTAAGACTATATATATATTT +TTCTAATTTTGCATGATTCTTTTAGATCTGAGATGCACAATAAAACACTTAGCCAGAGGT +TTGGCCTGCTTTTGGAGTCCTATTGTCGTGCATGTGGGATGTATTTGAAGCACCTGAATA +GGCAAGTCGAGGCAATGGAAAAGCTCATTAACTTAACTGACATTGTCAAACAGGAGAAGA +AGGATGAAACACAAAAGTTGTGTGACTCTAGTCTGTGTTTGAGACTCTTTTCACTGCAGT +GGGGCAGAGTTGTTTAGAAGCCCAGTGTATATACAGATCATGGTCCTTGGAATCAAGCAG +ATTAGGATTTGGAACCAAGTTCCACTGCCTCTCATCTGTGTAGTGTTAGACACGTTATGC +AGGCTCTCAAGACTCATTTTCTTTGTCTGTAAAATGGGAATAATACCTGCTTCGTAAGGC +CATTGTGAGAATTAAATTACATGAGATATGCAAAGAACCTATCACAATCCTTGGAACACA +GAAGGTGCCCAATAAATGTTAGATCCCTTTACTTTCCCTTCCTTTCTCTTATTCAGGTCC +CTAAGTATTTACAGTGATTATTTCCTTATTCTGTCATTTATTATCTCTCAGTAATGACCC +TGAAAATGAGTGGAAAGAAGTTAGTTTTTACATTTCCAAGTTTAAAATGGATTTCGAGTC +ACTCAGTAAATATATCACACCCTCTAGTCATCTGCTGTCTAGCTTAGTGTAACTAAGAGT +AGGAAATACAATGTAAACTTTTTTTTTTGAGACAGGGTCTGGCTCTTTTGCCCGGCCTGG +AATGCAGTGGTGCAATTTCGGCTCACTGCAGCCTTGACCTCCTGGGTTCAAGCCATCCTC +CCACCTCAGCCTCCTGAGTAGCTAGGACTATAGGAGCATGCCACCACTCCCAGCTAATTT +TTGTATTTTTAGTAGAGACAGTGTTCTATTCTGCTTTATATTAAAAGCCCCTTAGAAAAT +GGGAACCTGGTGAATATATAATGAATTGTAAAATATTTTAATGTGTAACTTTTTCAACTG +TGAAACTGACTACTGATTTTTTGATGAAAACAGCTGCTGATAAAGTATTTTGTGTAAAGT +GTAGTTCTTATTAATCAGGAAAATGATGACTTGATTAGACTGTATATGCCCTCTTGGATT +TTATTTTAAATGGATTGGTGACTTTCACATAGGTAAAACACAGTCCATCTGTATTCTTTT +TTCCATCAAAAAGCGAGTGATTTAGAATTATAAAAAAATTTGTGAGCAGCCTATTTGAAA +GGCATCATGGAAATTTCACAGCACAATAACATGGATTTGTTTTTTTCTTAATGATGTAAA +TCCGTTTAATTCATATTTTGATCAATAGCCCATGCTTGCCAACTCTGAAGAAATTTAATT +TCCAGCAGTATTTTAAAGCTAGCCTGTTAACTTTTTCTGAATATTTAAAGTTCCTCTTTT +TTCTATGTCTGCACAAACTGCAGACCTGGGCTGGACCCACATACTCAAGAGTCCACCTTA +AGAAATTATTTTGATGTCCAAGACATCACTAAAATATTTCAGTTTAAAGATAACATGTGG +TGTTAATAGATTGTGGTGCTTTTACTATTTAAAGACAACTTTCATACTTCAGATGTTTTT +GAGAAGAGGGGAATGTGAGGGGAGGGGGCAGAACAGGGAGGAGTTTGAATGAATTACATT +CTTTATATCCATCCTGCTCATTTGGGGCATGTCTTTAAGAGAAGGCTGAAAGTTGTGAGA +GTATATTGTATACCGTAAGAGAATCAACTCTTCATCATGGATGGGATTGTGAAGGCTGAA +CTGTAAAAGTCAGCATTGACAGCATCCTCAATTAATAATTCTTGGTGACAGAATAATACA +GCTGGGCTGTTTTATAAATATAAACAATACCATTTTTAATTATTACATTAAAAATTTTAA +ATATATCTATGTGCCATGGCCTGGGAAGCCTGTTTTCTATTTTCATAAAAATTATTTTTA +CTGTATGAAAAGATTATGGGGTTTAGCTCAAAATATCTGTGGTCCTGATAAAATTGGATT +GGTAACTCTACCTCAGAAGGAAAATGGGAAAAAAAAATAGATGAGTCACAATTCAATACT +TCAAGCTCAGAAACTGTGCAGATCACTGAATTTTAGATTTATAAAGTCAGAGTTGGCATG +CGTTGTTTTTAATGATATGGAAGACCTTAAGAAAAAAACTTGGCTGAAGTTTAATCGTTG +GTCCAGCCATTTGAAAAAGGCAATAGTTCGAGGAGGTTTCCGAATTCGGCATTTGAAATT +CATTTTGTTCTCTCTTCTTCATTATTAGTGCATTTGGTGTGTGTATACTTGCACACAATT +CTGTTTGTGTACACACTGCTTGCTAAGCCCTAGTCAAGAGGCATCTTTTATAAAAGGTGT +AAAGAAATATCAAGGTTCTAAAATTCGGAAGAGTTTAGAATTTATTAGGAGTTTCCCAAG +TTGGGATGTTAGTCTTTAAATAAACTTCATGCACCTATTCCACTTAAGGTTTTGCACCTC +CTTTTTATTAGTGCAGTGCCATTTCTTCTGCTTGATTTTAGGTATGTTAATATTCCAGCC +TTGCTAGTTAGCATAAAGTGACAGGTGTGAGCCATGAGGAAATTTTCTGACTTAATTTTT +ATACAACTACATATGAGTTTTAGTGGAGAAAAAAAATTAGTCCCTTGTGCATATATAGTA +GTTAGGTAAATGATTTTTCTACCAACAGTGTACTCCATTCCTCATGTAGGTAAGTACAGA +AAAGGTTTTTAAATGTATTTTGTTAGCCAGTTAAAGTCTATGAATCTATCTGCAACCTTA +TTTAATCTGTCACTACAATAATTTTGTGGTTATGCTAAGAACCATGTATACTTTTAGGTA +TTCTTATTTTTGTCAATTTTTCTAGGTTAGCAAGGAGGCAGAAAAGCTTCACTGTTTCAT +ATTAAAATATAATTAGACTAAACTTAATTCTAGTATGAATTTCCAAAATCATTATCTATT +TATTTCATTTTTATTTAATTTTGTTTTTAGTTCATTTTTAAAAGTCCCTTGTTCAATTTA +ATTTATGTTCCTAAGAGTGGTTGGAGAACTTGGCCTTCATCTGATTTCAAAAACATTTTG +AGTTTCAAATGAAGTTAATGGTTTCAGTGTGATTCAGTCCTCAGACCTAATTGGGTTGAA +TAAAATCTAAAAGAATATACCCTTTTGGAGCATAACATTTTAATACCTTGAGGAATGTGG +CACTACCAAAAGAAGACTACTAACACGTCAGATGTTCACCTGGAAGCTTTAACAAGAAAT +TCGAACCACCCTTTTGGCCCCATTAATTGTAGCAAGTTTATTTCTCTATATTTTGTCATT +CAGTGAATTGAAGTCCTGTGGTATACTGCATTCATTAGAAGAAAAACGTTTTTAATGTCC +TTTTAATGATGGCCCAGAAAGCATTTGACACAGCAAGATGCATGTATTATTATATTGAGA +ATACAGAATAATAACAGTATCACTAAATTTAAGACCTCTTCCCAGTCTTGCTGTTCCTAG +CAAGAAGTTTGGCCCGTGACTGCACTTACTGTTTATGCTCATCAGAAACTGTCAATGTCT +GCTTTTCTTTAACTCTGCAGTCTGTAACATCATGCTGTTTATTAAAAAAAAAGAAAAATT +ACTTTGACTTGTGTCCAAACAATCCTTAGTGTACTACATAAGCAAAAAACTGTGATAATT +CTCTTTTGCCATTCCTTTTGAAAAGCAAGCCAGTGTTGCTAAAATCAAAATTTAGCTGAA +TTTGAGTTCTTTTCAGTAATGACTAAGAATACTTGATTGAAAATCTGAAACTATTATACC +TTAAAAGCCAATTTTTCTGCCCCAGTAAAGTGATGAATATTAAAGAAATGTATGTTTAAA +TATTTACTTCCTTTAAGCATAAAGAATTATATGCTTGTATTTTAAGAAATATATGTATGT +ATACATACATATGAATGTATGTATATGCAATAGGTAAGTGGACTTTTTTCCAAGTCATTT +GAAGATCAGAACCTAGAAATGAAGTTAGGCTACAAGCAAACTGGTTTTGCTTTCAGTTCT +CATAAACATTGCAAAAGGTAAGTGTGGGCTTTTCTTTGACCATTAATGCACATAGGCATT +AACAACTTAGTATTTCTGAGCAATTAAGCAAATAATTACTTACATTTTATTTATTTGCCA +AATGGTTTAAATAATTTTGAATTGACTTTGCTCTCCAGGGATAATATCTCTCTTTGCTGG +AATGATTCAGGTAGCTCCTATCTAAATGGAAAACTGTGGTAATTGAAACACACACTTTAC +ATTTTAAATTAGCAGTTTTGAATTTGTTAGGGAAAAAAATCCCAGCAATTGCATATTGTT +AGGTAGAAGTCAAATTTACAAAGAAACGGAATAGAGATGTGCCCTTGAGAAAAGTGTAGA +ATCTCAATGTGCAGATGATTTAAAATGTGCGTGCATATAAAATGTTCATGTGTACTTACA +TACTTTATTACAGAGAAGTCTTTGGTATACAAAATAGTTTACCACAACCTTTTAAACAGC +AGGTTCTGGGCCTTAAATGCGTATCACATTTAGCCAAGAGAACTCGGGTAGGGGCATGGA +AAATGAACTGCAGCTCCCTATCCCTAGCCTCTATACCAGCTGTTCAATGAAAAGTACCAA +GGCTCACTGAATGTTATAACCTAGCAGATTGTTACATAAATGATCTAACATTTTTGAGCA +CCGCTACTGGATGCTAGAAGCTAAGCTAAAGTGTTTCACATGCCCTACTTTGCTTATTCT +ATAAAATAACTGCGTGAAAGAACAGGTTATCCCCATTTTATAGATGAGAAAAGAAAGGTT +TACACAGGTTAGCTTATTTGCCCAAAGTTGTGATTATGGCCTACAAAGTCAAATAAATCC +TACTCTGAGACACATGTTCTTTCCACCATTGCACACTAGAAAGGAAAACACCAAGATTAT +TCATTACTGATCAAGTCAATATTGCTGTATTCAGCTAATTTAGTAATATGTGTCTTGAAA +TTAATTGCTAAAAGGGATTAAACTGACTTAGAATCAGTTTTTTGTTTGATTACATCTACA +TACAAAAGTAGCTTCAAATGTCTCATTCTACTGTCCATAATTTAAGATTTTTGAGTATAA +TACAATTTTAAAGATACTTTGAGGCACTTTGGAAAATCAGACCAAAATCTCTTTTCCACT +CACAGATTCGGCTTAATCAATCTGGAAAGCATTTGTTGAGAGCCTTATGACATCATTTAA +TAACCACGGTTGATTCATTAATTAAAGTACAGACAATTGTTGACTATCCATGTGGGACTT +TTCTATTAGGTTGACGCAAAAATAATTGCGGTTTTTCGCCATTAAAGGTTAACAGCGAAA +ACTGGAATTACTTTTGCACCAGCCTAATACGATGTGGATCATCTGAGATGAATGTTGAAA +TCCAGTATAGCTTCTTCATATTTCTGGCCCATTTTTCCCACCAGAAAGTGCACAAAGTGA +AATGAGCTTATGAAAAGCTTAATTAACTAGAAAAATGTTACTGAAAGAAAAATTACATGG +TACATGACAAGGCTAAATACTAGTAACTCTAAACTTAGTGAATTTTCTAGGCAGCAGCTT +TCCTCTGCTGTCTAGACTGGTAAAGAACAAACTAAGGCCAGGCGCAGTGGCTCATGCCTG +TAATCCCAGCACTTTGGGAGGCTGAGGCGGCCAAATCACCTGAGGTCAGGAGTTCAAGAC +CAGCCTGATCAACATGGTGAAACCCTGTCTACACTAAAAATATAAAAATTAGCTGGGCGT +GGTGGTGCACACCTGTAATCCCAGCTACTTAGGAAGCTGAAGCAGGAGAATTGCTTGAAC +CCAGGAGGCAGAGGTTGCAGTGAGCCAAGATCACGCCACTGTGCTCCAGCCTGGGCTACA +AGAGCAAAACTCCATCTCAAAAAGGAAAAAAAAGAAAAAAACTATAATAAATATGTTAGG +TCCATGTTTTCTTAAGTTTTCTACCGGATTTTTATCTTCGTATAGTGAACGAACTGTTAA +GAACTTTTTTATGAGAAATATTTTAGTATGACTATATTGCATAGAGTTAGGCTGATGGTT +CAGTGTTCAGTAGGTTAGATACCCTCATTGTTTATTTCCATATTGACTGGTTCTAGCTAG +AGCTGAAATTAGGCAAAGAATATCTTGAACTCATTTTGCTATACAGGAAAAAAGTGCTTC +CTTAGCTCATTTGGAAAGAGATTGAGATTAGAAAAGATGGTTAATTTGTATGTATTTATA +GAAATAAATAGAATACAAAATGAGGCTTTTAAATTTTTTCCCACATGAAAATATGATACT +TTAATCATTACGTTTTACATTGTTAGTTTGCAGACAGGCATAATTAGGTCCTCAGTTGCA +GAAATCACAGACATCTGAAGGCCAGCCCTTTAATTTGGCCACCGTCTTAAGATTTCTCTG +CTCCTTCCTTTGCTCCTCCTCCTACTGCACAGTTTGAACTGATGCTGTTCTATATAAGGT +ACTTTTCCACCTACCTCATCTCTGACTACAGTGCTATATTTTTCACACAGTAAGGACAGG +TGTTGTGTTAATCTCACCATGCCAACAATCAGGGCACCACCTAGCAGAGTCAGTGAAGGC +CAAAATAAACAGTGGAAGATAGCCATTTGGTCATACTTTTTTATAAGAATGACATCTTCA +GATTGGCTGGCTGGACTGTAGAAGCATGAAAAGGGGGTTCCATTTTTGTGATCGAAGAAT +TCTTTTATGTCCAGAGCACTGTTGAGCAAATCATTTCTATCTTGGTGGCACTTAGGTGTG +TAAAAGCACTAGGAATATGGAAGAGGGAAAAAGATAAAGGCACTGTCACCAATACCAAAT +ACTTAACAGTTTCTAATTATGAAATAGCTTCAGGCTGAAGTTATTAGTGGGCAGTTTCAA +TCTTAGAAGGTGGTAAAATATTACATAGCTCATGGGAAAGGGTTGATTGGAGGGCCACAG +TGAAATGGCCATTTCCAGTCATTAAGCAAGGATGTGGAAGAGAATTCTTAGTTTATATGA +CATTGCAGGAGAGTCAGTGACCAATTTCATAAGGAATATGACTCCTCCCTACATGCAGGT +TCTTGGACTCTTGGACAGTATGAATCCGTTTGTCCATTGAACAAAAATGTATTGAGCCTT +ACTATGAGCTTTCAACACCTAGTAATGCCTCTGTGGTCTCTGTCTTGATCTCCTGTAGCA +AAATATTACCCTGAAGAAAAGCACGTTGAGGCTTTTGCTCTAGACTCACAGACAGGGAGC +CCCACCTGGACTTTGGTTCCTGGGAGACAGAACCAGTGGAGAAGGGAGCTCTGTCAGCTG +GTGACTTTTTTCAAAAAAGCTTGAGGTTTATTACCATATCCATTAGGTACTTGAGGTACT +GTGCTAAAGGCCTACAAACTGTTTGAAATCTTAAAAATCATTGCATCCAAAATAGAAAAC +AAAAGTCATCAGATTGAAATTGATGCTTAAAGACAATAAAGTGTAACATGTCAACTAATC +TAACACAACTCAACTTTTATAGTTAGGTATAAATATAAATTTTAAATCATATGAAAGACT +ATACTTTCAGGGATCATTTCTATAATTCGTTAAATCATATGAACCCATTGTGTAACTTAT +TAAAATAAAAATAATCTTTACATTTATTTGATAAGAAAAAATTACTCGCTTGATTCAAGG +GAGACTGTGGTACACTGTAGCATATGTTATATGGCGCGGAGTGGAATCTCCAAAAGAAAG +ACTCCCCACAAATGACTACTCATTGGCTCAGCCTATAAATTCCAGACACCAAGTTGTGAA +ATTGGAATAATTTCTCTCCTTTCTATATACCCCATTTCTCCACCAAGAAGAAAGCTTCAT +TTATCCTGATTTGATCACTATAAAAATGTTCACTCCAAAAAAATAGATTTATCCCTAAAG +ACAGCCCTGGGTTATTTATGTACCCTGCTAGGGACAGTCTGGCAGGGAAAGGTTGCTGTC +ATAAGAACTCTTTAAACTTTACAATACCTTGGGATTTATCTGGACAGCCTCTTCATTATA +ATGTAGGAGAGCTTTCTGAGCTGAATGGGTGAGGTTCACAAACACCCGAAGACACGAGTA +CTTCCCGTGACCACGGCAGTGCACACCACAGGTGAAGGCACAGTCCAGCCAGTCGTCCAT +GATATCTGTGTGGATGGCAGTGCAGGTTGATTCTTCTCTCCGAATGCTTCAATTTGAAAA +AAAAAAAAATGTTCTTCACTTACTAGAAAATTTCGTTCTACATTTTGGTGCGGTTATGAG +CTTATGTACACAATTAGCTGGGATTACAGGCGCTCAGCTGCCATGTCCAGCTAATTTTTG +TATTTTTAGTAGAGACAGGGTGTTGGCCAGGCTCGTCTCCAACTCCTGACCTCAAGTGAT +CCACCCACCTTGGCCTCCCAAAGTGCTGGGATTACAGGCATGAGCCACTGCACCTGGCCC +AAATACTATGTTTTATCAATTCTAAAGTGCACTTTAGTATTTACATTTTAATATAACTAA +AATCAATATGTATTTTGCAATCAATGGCATCTTGCTATTATTTGAAAACATTTCTTTAAT +AGTCTGTAAAATAATGGAACATGCCCAGATGCAGTGGCTTATGCCTGTAATCCCAGCACT +TTGAAGGGTCAAGATAGGAGGATCGCTTGAGCCCAGGAGCTGGAGACCAGCCTGGCCAAT +ATAGTGACAGAATAAATAAATAAGTAAATAAAATAATGGAAAATCTCACAAATGGTGATG +TTTTAGGTTCGACAAAATACATTAACTAGCCCATTTAGTTTTCTGAAATTATTTTGATGT +TATTGCTTACAATATTTGTTCTGTGGTACACAACCATAGGATTAATAATATTGATGAAAA +TAATAAAAGAATAATAAGCATGTATTGAGCTCTTCCTGTGTGAAGTTCTGGACAAATCCT +CATAAAGCCTTAAAAGGCAGATACTAGGCTGGGCACGGTGGCTCATGCCTGTAATCCCAG +CACTTTGGGAGGCCGAGGCAGGCAGATCACGCGGTCAGGAGATTGAGACCATCCTGGCTA +ACATGATGAAACACGGTCTCTACTAAAAATACAAAAAATTAGCCAGGCATGGTGGCACGT +GCCTGTAGTCCCAGCTACTCGGGAGGCTGAGGCAGGAAAATCGCTTGAACCTGGGAGGCT +GAGGTTGCAGTGAGCCAAGATCGCACCACTGCTCTCCAGCCTGGGCGACAGAGCAAGACT +CTGTCTTAAAAAAAAAAAAAAAAAAAAGAAAGAAACAGGCAGATACTAGCCCAGGCACGG +TGGCTCATGCCTGTAATCCCACACCTTCGAAGGCCCAGGCGGGTGGATTATCTGAGGTCA +GGAGTTTGAGACCAGCCTGACCAACATTGTGAAACCCTGTCTCTACTAAAAATACAAAAA +TATTAGCCAGGTGTGGTGACAGGTGCCTGTAATTCCAGCTACTCAGGAGGCTAAGGCAGG +AGAATCGCTTGAACCCGGGAGGCGGAGGTTGCAGTGAGCTGAGATTGTGCCACTTTACTC +CAGCCTAGGTGACAGAGGAAGACTCTGTCTCAAAAAAAACAAACAAACAACAACAACAAC +ATCAAAAAGAAACCTATAGTAATAAAATTGAAATAGAAGGAGGTTTGCAATCAAAATGAC +TGACTAGGAATGAAATAGGAAACATAATATTTTGCATCTGCATAGGGAAGTCTGAGATTG +GCTGATCTTGTTCTCTTCTGTAGGGGAAATACTAGTCCAGAACTTGGGGTGCCTGCCAAG +AGGGGAGCAGCCACAGTAGGAAAGGGGGACTCTGGAATGCTAGGGTTCTGGGGTCTGTGG +ACACAGGAGGCAGAGGACATGTGTTAAGATGTTTTAAGAAATGAATGTTGAACTGGATAT +GAAAATATTTTTCAGCCGGGCGCAGTGGCTCACGTCTGTAATCCCAGTACTTTGGGAGGC +TGAGGCGGGTGGATCATGAGGTCAGGAGATCGAGACCATCCTGGCTAACACGGTGAAACC +CCGTCCGTCTCTACTGAAAATACAAAAAGTTAGCCAGGCGTGGTGGCGGAGGCCTGTAAT +CCCAGTTACTCTGGCGGCTGAAGCAGGAGAATGGCGTGAACCTGGGAGACGGAGCTTGCA +GTGAGCCGAGATTGCACCAGTGCACTCTAGCCTGGGCGACAGAGGGAGACTCCATCTAAA +AAAAAAAAAAAAAGAAAGAAAATATTTTTCACTATAGAGAGGCATATGTCCCCTGAACTT +GCCGGGATCCACCTTTCCTGCTGGTGCATTCTGTGAGTTAGAAGAAAACTTCCAAAGAGC +CATTTTTTCCACCCTGTCTACTGTATAAAATTGCTTCTCAAACATGTGCTGCATTGCAGA +GGATTACCATTGTTTTGCTAACCAGCGTCTGGTCTTTCTTATGTGGCGCTGCAATTACTA +GTGTCAAACCCTGTTGGTAATACCCAGAGGACGGTGTCTGAAGTCTTTACTCAATATTCA +CATTTGGCCGGGTGTGGTGGCTCACACCTGTAATCCCAGCACTTTCGGAAGCAGAGGCAG +GCGGATCACTTGAGGTCAGGAGTTCAAGACCAGCCTGGCCAACATGGTGAAACTCCATCT +CTACTAAAAATACAAAAATTAGCCGGGTATGGTGGCGGGTGCCTGTAATCTCAGCTACTA +GGGAGGCTGAGACAGGAGAATCACTTGAACCCAGGAGGTGGAGGTTACAGTGAGCCAAGA +TTGTGCCACTGTACTCCAGCCTGGGGGAAAATTCACATTTGTAGAGAGTTTAAATTCTTT +TTTGATACGGAGTCTCGCTCTGTTGCCCGGGCTGGAGTGCAGTGGCAGGGTCTTGACTCA +CTACAACCTCTGCCTCCCAGGCTCAAGGGATTCTCCTGCTTTAGCCTCCTGAGTAGTTGG +GATTACAGGCACCCACCAAAACACCTGGGCAATTTTTGTATTTTTATTAGAGACAGGGTT +TCACCATGTTGTCCAGGCTGATCTGAAACTCCTGACCTCAGGTGATCTGCCTGCCCTGGC +CTCCCAAAGTGCTGGGATTACAGGCATGAGCCACCACGCCCGGCCGAGAGTTTAAATTCT +TAAGTCCTACACTCCAATGTGTGGGAAGTATTCGTGCTATGCTTTTATAACTAAATCATC +TCAGTATTTCTATTTCTAGCCCCCTTTTTCTGCCTGATGGTAAGATACTTAATCTAGTCA +ATTCCAGGTAAACTTTGGCCTTTTATGATTTTTCCTGATCAGGCCAAACCTCAACCAAGT +CCCTTCTTGATCTTCTCCTTCACCTCCTTCTCTCATTCACCCGACAATTAGCCTCCAGTC +CACGGGCTGATGCAGCATCTTGGTGTCCTGTGGTCTGAGGTCATTTTCTGTCTTTCTCAA +GCCTCAGCTAAAGTTTACAATCCTACCTTTTCTCATGACCTTGAAATGCCCTAAGGTTCA +GGGGCTTCATGGTTGCTGCTTCATGGGGGAACCTGGCTGTTCTCTGAGGCTGCTCGGCCG +CGAACACCCCATCAACTACCCGGGGCCCATCTACGCCCGAGGCCTCAGCCATTCCTGCTC +TACAGCTCTGCTGTCCCATTGGCACAGGGAACTTCTTGGGGCCCCAGGGTTCCAGATTGG +AAGCAGAGAATCTCCTCTGTTCTCAGACCCCCAAACTTTGTTGTGGATTCTAATTGTCCT +TTCCCCCATCTCACTCCTTGGAACCCACTGGGAGGTGAGTAGAATCCCTGTCAGAGATTC +TACCACCATCTCCCTCATTCTTACCCTAACTTTCTTCCTCTTCCTCCCTAGTTAGGAAAG +AGGATCTTTAGCCTGCGGCGGGGGGGTGGGGGTGGGGATGCTTGATGTTTCAGGGGAAAA +GGTGACTCAGCTACTTTTGGAATATCTGTCATACCTGTCTACTGGTGCAATGAGCTGGGA +TCACACCACTACACTCCAGCCTGGGTGACAGAGCAAGATTCCATCTCAAAAATAAATAAA +TAAATAAATAAAGACTCTGGAGAAACAACTCAATACACATGAGAAGAGGCTGGCCCATGT +AGGGAAAGGACTGGCAAACTATGACAACTCTTTTCTGTTGTTTTGTTTTCAATAGTCTCT +TCACAGTTCTTTTCACAGTTTGGAATTGATACCTTTTTCTCTTCATCAGAACTCCAATGT +TTTTGTAGATTGAAGTCTTTTTTTTTTTTTTTCTTGAGAAAGGGTCTCACTTTGTCACCC +AGGCTGGAGTGCAGTGGACCAATCACTGCTCACTGCAGCCTCGACTTCCTGGGCTCAAGA +AATCCTTCCACCTCAGCCCCCCAGTAGCTAGGACTACAGGTGTTCACCACCATGCCCAGT +TAATTTTTATTTTTTAATGTATTATTATTATTATTATTATTATTATTATTATTATTATTA +TTATTTTGAGATGGAGTCTTGCTCTGTTGCCCAGGCTGGAGTGCAGCGGCACCATCTCGG +CTCACTGCAACCTCTGCCTCCTGGGTTCAAGAGATTCTCTTGCCTCAGCCTTCCAAGTAA +GTGGGACTACAGGTGCATGCCCCCACACCTGGGTAATTTATTTTTTTGTAGAAAAGGGGT +ATCAGTGTGCTGTCCAGGCTGGTCTCAAACTCCTAACCTCGAGTGATCTGCCTGCCTTGG +CCTTCCAAACTACTGGGATTAGAGGTAATGAGTCACCATGACTGGCCTACGTATAGCCCA +AATGGATGAGCAGTTCCCAAGGCTCATTCCCAGCCTCCACTATCCAAGTCAGCCTCTCAT +CTCCTTCATTTCCCAGGACTTAGTTCTCATTTTCCTCCCCTGTTTTCTCCGGATTGTGGC +TATTGTTCCCTGGTTGCTAGATCAACCTGGAGCACAGTAAAGCAGTGTCACAAAGCTGGA +AGGGGTCTGGGATGAGTCCACCAGCTACAAGTTCTTATAGAAAACGTACTCCGGGGATGG +CCGGGCCCAGTGGCTCATGCCTGTAATCCCAGCACTTTGGGAGGCCGAGGCGGGCGGATC +CCCTGAGGTTGGGAGTTCGAGACCAGCCTGACCAACATGGAGAAACCCCGTCTCTACTAA +AAATACAAAATTAGCTGGGTGTGGTGGCACATGCCTGTAATCCCAGCTACTAGGGAGGCT +GAGGCAGGGGAATCGCTTGAACCTGGGAGGCGGAGGTTGCGGTGAGCCAAGATTATGCCA +TTGCACTCCAGCCTGGGCAACAAGAGTGAAACTCCATCTCAAAAAAAAAAAAAAAAAAGA +AAATGTACTCCAGGAATTGTCATTTCTGAAATTCAACAGCTTCTGGAATTGAAGCAAACA +GCTCATCTTGGAAGAGAAATATGTAGCCAACTCCAAAGCCAAAGCCTTTGAGTATTGAGA +CCTAGCATGCTAGGAGACCTTGATCCTGTAACCTCAGAAGAAGAATCTGGATCTGGCCAA +ATTGAGGTCAAATTCTGCTCAACTTCTCCATAGTCAGTAGGAGAAAAAAACCAACTTGAT +GTTTGAGTCATATGTTTTGACAACTAAAGAGGACACTTATGCTGGGGTCGGTGGTTCATG +CCTGTAATCCCAGCACTTTGGGAGGTCGAGGCGGGTGAATCATTTGAGGTCAGGGGTTCG +AGACCAGCCTGGCCAACATGGTGAAACCCCGTCTCTACAAAAAATTCAAAAAAATTGGCT +GGGGGCAGTGGCTCATGCCTGTAATCCCAGCACTTTGGGAGGCTGAGATGGGTGGATCAC +GAGGTCAGGAGTTCAAGACCAGCCTGGCCATTATGGTGAGACCCTGTCTCTACTAAAAAT +ACAAAAATGATCCGGGCATGGTGGCGCACGCCTGTGGTCCCAGCTACTCAGGAGGCTGAG +ACAGAAGAATCTCTTGAACCTGGGAGGTGGAGGTTGCAGTGAGCCGAGATCACGCCACTG +CACTCCAGGCTGGGTGACAGAGTGAGATGTCATCTCAAAAAATAAATAAATAAATAAATA +AAATTAGTCTGACTTAGTGGCGGGCCCCTGTAATCCCAGCTACTGGGAGGCTGAGGCAGG +AGAATCACTTGAACCCGGGAGGTGGATGCAGTGAGCCAAGATCATGCCACTGCACTCTAG +CCTGGGCGAGTGAGACTCCATCTCAAAAAAAAAAAAAAAAAAAAAAGACACTTAAAGATG +ACATTAAAGAGGATACTTAGATTCTAGACAAAATCAAGATATAGCAAATTGGGGTGGGAC +ACACCTGTAATCTCAGCATTTGGGGAGGCCGAGGCAGGTGGATCACCTGAGGTCCAAAGT +TTGAGACCACCCTGACCAACATGGCGAAACCCCGTCTCTACTAAAAATACAAAAATTAGC +CAGGCATGGTGGTGGACACCTGTAGTCCCAGCTACTCAGGAGGCTGAGGCAGGAGAATCA +TTTGAGCCCAGGAGGCAGAGGTTGCAGTGAGCTGAGACTGCACTGCTGCACTGGTGCCTG +GGCCACACCAGTCACTATGCCTGGGTGACAGAGCAAGACTCTGTCTCAAAATAAATAAAT +AAATAAATAAAATTTTGTTTTGCTGTGTTGCGGCTAATATGCGTGCTATAAGACAATGGT +TTCTTGAGTCTCATTCTCTCTGCATATGCCTAAAGCTTTTTTATTTTTATGATTCTAAAA +GATTGTACCTTCTCATCTCCTAGATTCTGTCCCATAGGTTCTGATTTTTCCTAGAGTAAC +TTGGAAGTTAAAAAAGTGGAAAAAGCTTTGCGTATTAGGTGCCAAACCCACTCAGCTCTG +CTCAAACCCCTTCTTTAATGCCCAAGGTTGTCCAATCCTAGCCCTTCCCCCTACCCTCAG +CTTTCTCCTCACCTACACAGCAACCTTAGTATAGTCCTAAAGTATGTGTTCTTATCTTCT +GTTATCTATGCCAAGGATGTTTGCTGGTTTTGTTTTGTTTTGTTGAGACAGGGTCTTGCT +CTGTCTCTTAGGCTGGAGTGCAGTGGCACAATCACAGCTCACTGCAACCTCGATCTCCTG +GGCTTAAGTGATCCCCCCACTCAGCCTCCTGAGTAGCTGGGACTACAGGTATGCATCACC +ACGCCTGGCTAATTTTTTTTTTTTTTTTTTTTTTTGAGGCAGAGTTTTGCTCTTGTTGCC +CAGGCTGGGGTACAATAGTGTCATCTCAGCTCACCACAACCTCTGCCTCCCAGGTTCAAG +CAATTCTCCTGCCTCAGCCTCTCAAATAGCTGGGATTACAGGCATGTGCCATCACATCCG +GCTACTGTTTTGTATTTTTAGTAGAGATGGGGTTTCTCCACGTTGGCCAGGCTGGTCTTG +AACTCCTGACCTCAGCTGATCCACCCACCTTGGGCTCCCAAAGTGCTGGGATTAAAGGCT +TGAGCCACCATGCCCGGCCCATGCCTGGCTAATTTTTTTTAATTTTTATTTTTGTAGAGA +TAGGGTCTCACTATGTTGTCCAGGCTAGTCTTGAACTCCTGGACTCAAGCGATCTTCCTG +TCTCAGCCTCCCAAAGTGCAGGAATTATAGGCATGAGCCACTTTGCCAGGCAAGGATTTT +TTTCTTTTTAAGTTACATTTCTGCCTGCCACCACAGCAGCTCTTTCTCCTGCTCTCTCTC +TCTCTCTGTGCTTTAAGATGATAGTCCCTTCTTTTTTTTCAAATAACCACAACAGGAAGG +ACTGACCACTCTTGTAAGCTGCAACTGATGTTTTCAGACTCCTAAAGTGACATCTAGACA +TAAGTCCATATATGTCAGAATATCATGCAGGGAATGCTCAAATAGTTGGGAAGAGATTGC +TGCACTGTGTTTTGCACGCCCAAAGCCCACATAGGTACTCAGTTTAAAAATCTTAATAGA +ATTGAATCCTGCTCTTATCATAGGAAAGGAAGAGCATCTGATAGAAACACAAAATGAAAA +GGTCAAGAACTGGCTGGGCACAGTGGCTCTCGCCTGTAATCCCAGCACTTTGGGAGGCTG +AGGCGGGAGGATCATGAGGTCAGGAGTTCGAGACCAGCCTGGTCAATATGGTGAAACCCC +GTCTCTACTAAAAATACAAAAAATAGCTGGGCGTGGTGGCGCGCACCTGTAGTCCCAGCT +ATTCAGGAGGCTGAGGCAGGAAAATCGCTTGAACCTGGGAGGCGGAGGTTGCAGTGAGCC +AAGATCACGCCACTGCACACCAGCCTGGGCAACAGAGCAAGACTCCGTCTCTCAAAAAAA +AAAAACAAAAAAAGTCGAGAACTGGAAAGGAACTAAGCGCATGAAAAGAAATTTTATGTT +CCTTCATGTTTTTATTTAAAGAAAGTGAATCAAGTACCAAACACGGAATAAAGGCAAACA +TTCATTTTTGGGGTGATTGTTCCCTTCTTGGCAATCCCTGTTTTATTGAGGGTATCACTA +GTTATTCAATCCAAGGATTTTTTTTGTTTCCACAGGAGGTGGGTGTTTCTTTGTCTTCTT +AGAGTCAGGATTCCAGATCTCCTGATGTGTGGGACTTTTCTTGGCCACTACGATTTCATC +TACAGTCACGAGCTGTAGCACCACCTCAGCCACTGCTCGAAATCCTTGGGCTTTGACTAT +TAGGGTGTCCCACACCCCTTCCTGGGCCACATTTATTATCCCTTCAGTTCCCACACCCAT +TAGGAGGTTCCCACCTTGGTGCACTCCACTCATTTCTGCCATCACGTCTGAGACAGCTAA +GCCTGCATTCTCTGCCAAAGTTTTAGGAAGATACTTCAGGGCCCAGGCAAATGCTAGGAA +TGCAGGCCCACTGGGCCCTTCCAATCTGCTTCCTTTATCAGAAAGCATTTTTGCCAAAGC +CATTTCTGTGGCCCCAGCTCCTGGAATCAGTCTGGGATCTTGACATAGCTGGAAATAGGC +ATCAATGCCGTGGTAGACGGCCTGCTCTGCACTCCGCAGCCCCTGGGTGGTGGCTCCCCT +GAGAACCACAGTGAGGGCAGGTGTGCCTGTACATTCCCATTCAAATACCACAGCCAAACC +ATCTCCCAGCTCCTGCCTGTAAACCCTCTGGCACTTGCCTGGCCTCTGGGGAGGGAGCAG +ACGAGGCAGCAGAGGTGTGTCCAACACCTCACTCAGGTAAATGATCTCCATCCAAGACCT +AGCTTGAATCACCACGATGCCATACTTGTCCGCCAGTGTGAGGGTCTCCTCGTCGACCTC +CCCCAACACCACTGCCACATTAATTCCTGCAGCTGCTAGCTGGCCTACTTGCTTTTCTAG +TAATTGATCGCTTCCTTTACTAAATTGAGCTAGATCAGCAGGACTAGAAAGACGGGCCGT +TGCTGGTGCATTTGGATGGGCAGGACCAAAGGGGCAAGCAAAGAGAGCCACCCTGGCACC +ACTTAACACTGTGGCCATTTGCCCACAGAGCTTCCCAGATATTGCTAACCCCGGGAGGAG +GCAGGAATCCTCCAGTGTCCCCCCGGGCAGCGCGCACACCCCAACACGCTCAGGCTTGAA +GCTGCCGTCTAGTTCCTTGATAGCCCAGCAGGCGTGGGCCACCAGCTTGGTCAAGTGGTC +CATGGGGGACAGGGTGTGGGTATTCATCACAGAATGGAGGGCCCAGGATGGATCTTCCAA +AGGCCCCAGAGATTGGATGGCCAGGGAGGGCAGTGTGGCCAGGACCTCTGCAGTGGCCGT +GGCGTAGGCCTCCCGGAGCTGCGGGCGAGGCAGGCCAGCCTTCAGCAGCTGCTCTGCCTG +TTCCAGCAAGGCTTCCGTCAGCAGAACCACGAAGGCTGTGCCGTCCCCACTATTCTCTGC +CTGGGTTTGTCCTGCTTCCCGGAGGAGCCATGCTGCTGGGTGCTCCAGCTCCAGGGCCCT +GAGGATGGCAGTGGCACACCCCGTGCACACTGTTTCTCCTTTCATGGTCACCAGGAACTT +CTGCCGGCCGTGGGGGCCATAGCAAGGCCGGATGACACTGGCCAGGGTCTGGACTGCAGC +CAAGCTGCTCAGCAGGTGGGGCTCCTCCTCTTCTGGACTCCTCGGGCTCTCCCTTGGGTT +CAGTGCCAGCCGCTGGGGCAGCTCCAGGGCTGAAGGGACTGTGCTGTCCATGGCCCGCAG +AGAGAGGAGAGGCCACCGTGGGTTGCAGAGATGCTCTAGAAACAGCAGCTGGGGCACTCC +TGACACCGATCGTTGAAAGTACTCAAGAGGTCAGTGGAAGCAAGGAGCCAAATGCCCATT +GATTGGTATCTGAAGACATCAGCACGGACCAGCACTCCACTGTGGGTCCAAGGATGAGCT +CCAAAGAGCCCAGTCCTAAAGCCACCCCAGGGTTGATTCTGTAAAGGAACTGGGTCTTGG +GGCCTCTCAACCTTGGTGGCTGAAATGGGATCTTTAACTGATGAAGTCACAAAGTGGAAA +ATGGAACCAGGATAGAGAATGAGGTCACAGAAGGCTGGTTAGAACTGAGGAGGCCCTACC +AGCAGGCAAAAGTCAGGCCTTGTCCAGCAATGGAGGTACATGCACCTCTGCACCAGGTTT +GAGACTTGTTTAAACGTAAGAGACAATGAGGAGGAGATCAAGTGAAAAACTACCCATTTC +ACCCTATCTGGAGTGCAGGGGCATAACCATGGTTCACTGCAGGCCCAGCTCCCTGGTCTC +AAGCAGTCCTCCTGCTCAGGTTCCCAAGTACCTGGGACTACAGGCACACACCACCACACC +TAGCTAGTTTTTTTATTTTTTGTAGAGACAGTGTTTCTGTCTGTTGTCCAGGCAGGTCTC +GAATTCCTAGCCTCAAGAGAGCCTTCCACCTTGGCCTCCCAAAGTGCTAGGACTACAGGT +GTGAGCCACCACCTCACCCACCCTTTTTTTTTTTTTTTTTTTGAGACAGAGTCACACTCT +GTTGCCCAGGCTGGAGTGCAGTGGTACAATCTTAGCTCACTGCAACCTCCACCTCCCAGG +TTCAAGCAGTTCTCCTGCCTCAGCCTCTCAGTAGCTGGGATTACAGGTGCCAGCCACCAC +GCCCGGCTAATTTTTTATATTTTTAGTAGAGATAGGGGGATTTCACCATGTTGGCCATGG +TTGGCCAGGTTAGTCTCAAACTCCTGGCCTCAAGTGATCCGCCCACCTCGGCCTCCAAAA +GTGCTGGGATTACAGGTGTGAGCCACTGCACCTGGCCTTTTTTTTTTATTTGAGAAGGAA +CTGAGAGATGATGTCTGTGTTTTGTTTTGTTTTGGTGTTACTTTCTCTTGCAGTACTGTG +TAATATTAGCCATGTTTTGCTGTCTGCCTTTGACTTTTTGGGTATCTTATCAGTTTGTGC +TTGTGTATCAGGTTTCTTAGGGTGTCTGTTGGTCTTTCAGGGTGCAGGTGTGGGAGGCTG +CACAGCGTGCATGCCTGTGCCACGACTCCCAACTCTGCCTCCCTGGCAGAGGCAGGGCAA +GACAAGTGGGGAAGGATGCTGACAGCTCACAGACAAATAGAAGTGAACCCAGAGGGGTGA +AAAGCAACCAGCCTCCCAGCGGTCAGGGAGGTAGAAGCCTAAATGGGGTCCTGAGATTTA +AATGCGAATCGCCTTCCCATCCTAACCTTCAATGCTTACAATTTAAGTCTCTTTTTTTCA +TTCTCTCTCCTTTCCTCACTTGTCTCCTCTTTCCTCCTATAGAGCCTACTCGGGTAATGA +TGCTTCTGCTTTAGTTTAACACATATTTAGTCTGGGCGTGGTGGCTCATGCATGTAATCC +CTGCACGTTGGGAGGCTGAGGCGGGAGGATTGCTTAAGCTCAGGAGGTTGAGGCTTCAGT +GAGCCATGATTGCACCACTGCATTCCAGCTAGGGCAACAGAGTGAGACTTGTCTCAAAAA +AAATAGGGGAAAGGTCATTTGGAATCCTAGTCCAGAGATAACCATTGTTTACAACTTGAT +GAACATTACTACTTTGCACATATTATATGCATACATAATTATAGATTTACACCATTTTAC +ATAAGATTATGATACATATATGCTATTCTGTGATCATTTCCCCCTCAACATTATCTTGGC +TCAGAGAAATGTTTCTTTTTTTGTTTGGACATGGAGTTTCGGAGTTTCGCTCTTGTCGCC +CAGGCTGGAGTACAATGGCGCAATCTCGGCTCACCCTCGGCTCACCACAGCCTCTGCCTC +CCGGGTTCAAGCAATTCTCTTGCCTCAGCCTCCTGAGTAGCTGGGACTGAGTAGCCATGT +GCCACCATGCCCGGCTAATTTTGTGTTTTTAGTAGAGACAGGGTTTCTCCATGTTAGTCA +GGCTGGTCTCAAACTCCTGACCTCAGGGGATCCACCCGCCTCGGCCTCCCAAAAGTGCTG +GGATTACAGGCGTGTGCCACTGTGCCTGGTCTGTGAGCCACTGTGCCCGGCCTGAGAAAT +GTTTCTTTTTTTCTTTCTTTTTTTTTTTTTAAGCAGAAACACATTCATTTATTAACCAAA +GGGATGATCCTAATGAATCCAACACACTTTGAAATAGCTGCATGTAAAATGTTTGTGATA +AAGATAATTGAACACAGTAATGAAAAAAAAAAAAGAAAGAAAGAAACGGTATGGAGATTT +GCTCATTGAACTGAGCTTGGTCATTCTCTTAGTTAACTCCTGTCCAAAGTGATGATGGAA +TCTTTATTGTACTTTTTCATAGATCCGAGTACAGGCGACATGGTTCATGACACAGTCCAC +CACTAATTTCCCATCTTTCAATGTTCTTGTTATTGTGCTTTCCTTCCCATCCCACTCCTG +ATGCTGAACCAATGCACCATCTGTAAAGTTGCACACAGTCTGAGTTTTTCTGCCATCAGC +TGTGGTTTCTTCAAACTTCTCTCCCAGGGTACAAGAAAACTGTGTTGTTTTCAAAGTGCT +CTCAGTTTTTATGGTGAGGTTTTTGCCATCACAAGTGATGATACAATCTGGCTTGGCCAT +TGCGCCCATTTTTTGCAAAGCTATTTCCTCCTAGCTCCTTCATGTATTCATCAAAGCCTT +CGCTGTCCACCAGGCGCCATCTTCCTTCCAGCTGCTGAACTGTGGCCATGGTGGGTGCAG +GGGGGCTGGTGTGCAGAGCAGGGTCTGCGTCGGCGTGGCAGCGTGCTGTCGAGAAATGTT +TCTAAGGAGATCTTATTTGGTCTGAGAACCATGAATGATTATTTTGAGCACTTTTGATTC +TGGAGACTCCATTTGGATCAGGCATGGTCCTCCAAATTCAGGCTTCTGAAAGCCTGTACC +TCAGAGTAGGCTTGATGTTCCATAAAAGATGTGGTTATGAGTGCAAAGATGACTTGCCTG +TATTGTTATACAAATGTAAAATGTAACAATCAACAAAAATGTAGCAAAGTATGCATGTAT +ACATTTTCTCTAAAGATACAGTTTCTTTTTTGAAAAAATAAACACATTAGGCAGGTGTGA +TGGCGGGTGCCTGTTATCCCAGCTACTCCGGAGGCTAAGGCACGAGAATCTCTTGAACCT +GGGAGGTGGACAAATTGCAGTGAGCCAAGATTGCGCCACTATACTCCAGCCTGGGCAATA +GAGCGAGACTCAGTCTCAAAAAATAAATAAATAAATAAATAAATAAATAAATAAATAAAA +TAAACACTACCGGCCAGTGGCCATGGCTCGAGCCTATAATCCCAGCACTTTGGGAGGCCT +GAGCCAGGTGGAGTTCAGGCATTCAAGACCAGCTTGGGCAATATGACAAGACCCCTGTCT +CTACTAAAAATACAAAACAATAGCCGGCCGTGGTGGTGTGTGCCTGTAGTCAGCTGCTTG +GGAGGCTGAGGTGGGAGGATTGCTTGAGCCCTGAAGGTGGAAGTTGCAGTGAGCTGAGAT +AGTGCCATTGCACTCCAGCCTGGGTGACAGAGTGAGACCCTGTCTCAAAAAATAAAATAA +AATAAACACTCCTATAAAGGATCCTCTTAGCTCTTTTTCTAACACCTAATCTACATTTTC +ATATTCATTTCAGTTACCCTACAACTGTTCACTGAGCTGCTGTTGAATAGGGGAAATAAG +GCAGATAACTACTGCCATCTCCGCTGGAGGGACGATACAGACATTAATCTGGGCACTTTG +ATTACAGGCAATGAGAGCTGTGAGTGGGGAAAGCACAAGGTTGGCAGAAGCATTTAGGGG +GACACAGCCATTCTCACGGAGGGCAGAGGTCTAAAGCAAGAGCTGAATAAAAAGTAGGAA +CTGGCCTCGTGGAAAGGGGAAGGGTGATGGGACAGCCTGGTGGTTTGTAGCCCACTGGAA +GGAGTTCTGAAAACTGGTGGTCAGGTGAGAAGGAAAGCTGGGGAAGAGATGAGCACGTTC +GCCAGAGGGTAGCAGGGGCTCTCCGGACCTAGTGAGTCAAGCCAAGGAATTAAGGCTTCA +GCCTGCAGGGTGATGAATAGGGCTGTCTATTCCATTTCTTCCTTCTTTCTTTCTTTTCTT +TCTTTTTTTGAGACAGCGTCTCACTCTGTCACCCAGGCTGGAGTGCAGTGGCACGATCCT +GGCTCACTGCAACCTCTGCCTCCCTGATTCAAGCAATTCTCCTGCTTCAGCCTCCAGAAT +AGCCGGGATTACGGGTGCCTGCTACCACGCCTGGCTAATTTTGTATTTTTAGTAGAGGCG +AGGTTTCACCATGTTGGTCAGGCTGGTCTCGAACTCCTGACCTCAAGTGATCTGCCTACC +TCGGCCTCCCAAAGTGCTGGGATTACAGGTGTAAACCACCGTGCCTGGCCTGAAAATTTC +TAGTTTATGATACTTGCCAGCAGAATGTGTTCTGTCACCCTCTTCTGAATAGATATGGTT +GTCTGCTATGACTTCTCCCACTGCTGCCCTTCCCCCTGAATCCACAGATGCATTTCTTTT +AAAACTATGATCTTGTACACAATGGATGTAAATATTTAATCTTTCTATTTGTATGTTTTT +CCATGTTTCTTTTCTTTCTTTCTCTTTTTTTTTTTTTTTTTTTTTTTTTTGGAGGTGGTG +TCTGCCTCTATTGCCCACAGGCTGGAGTGCACTGGTACAATCTCGGCTCACTGCACCCTC +CGCCTCCTAGGTTCAAGGGATTCTGCTGCCTGAGCCTCCTGAGTAGCTGGGACTACAGGT +GTGCACCACCACGCCCGGCTAGTTTTTATATTTTTAACAGAGACAGGGTTTCACCATATT +GGCCAGGCTGGTCTCGAACTCCTGACCTCGTGATCCTCTCACCTCGTCCTCCCAAAGTGC +TGGGATTACAGGCATGAGCCACCGTGCCCGGCCTCCATGTTTATTTTCTAGTTGCTTACT +TGTCCTTTTGTGTTTATCCTTGTTAACTACTACTGCCAGGCTTAAAGTATAGACCCCTAG +AGGGCAAGATTTGTATCTATATAAAATGTACTGCAAAACATCTACTTAAGCCTCACATTC +TTAAACACAAATTACTTTTGAAGATGACTGTTCTGTTTGTTTCCTTCCTGGTTTCTTCCT +TTAACTTTTCCACCAAACAGGTACATGATATACTTTACTGAAATAACTTATATAGCAATA +TGAATTTTTTTTTTGAGGCGGAGTTTCGCTCTTGTTGCCCAGGCTAGAGTGCAATGGCGT +GATCTTGGCTCACTGCAACCTCCGCCTCCTGGGTTCAAACAATTCTCCTGTCTCAGCCTC +CAGAATAGCGGGGATTACAGGCGCACACCACCATGCCAGGCTAATTTTTGTATTTTTAGT +AGAGACGGGGGTTCACCATGTTGGCCACGCTGGTCTCGAACTCCTGACCTCAGGTGATCC +GCCTGCCTTGGCCTCCCAAAGTGCTGGGACTACAGGCATGAGCCACCGTGCCCGGCAAAT +TTGAGGTGGAGGTTGCAGTGAGCTGAGATCGCATCACTGCACTCTAGCCTAGGTGACAGA +GCAAGACTGTCTCCCACTTCAGCCTCCCAAGTAGCTGGGACTACAAGCATGTGCCACCAG +ACCTGGTTAATTTTTTTTTTTTTTTTTTTTGAGACGGAGTCTCGCTCCATCACCCAGGCT +GGAGTGCAGTGGCGCGATCTCAGCTCACTGCAAGCTCCCCCTCCCGGGTACACGCCACTC +TCCTGCCTCAGCCTCCCGAGTAGCTGGGACTACAGGCACCTGCCAGCACGCCCGGCTAAC +TTTTTGCATTTTTAGTAGAGACAGGGTTTCACCGTGTTAGCCAGGATGGTCTCGATCTCC +TGACCTCATGATCCACCTGCCTTGGCCTCTCAAAGTGCTGGGATTATAGGCGTGAGCCAC +CGCGCCCAGCCAGGCCTGGTTAATTTTCTTTGGTATTTTTTTGTAGAGACGGAGGTCTCA +CTATGTTGCCCAGGCTGGTCTCGAACTCCTGAGCTCAAGTGATCCACCTGCCTTGGCCTT +CCAAAGTGCTAGGATTACAGGCATGAGCCACGGTGCCCAGCCTACAGTGCAACTTTAATA +ATAACAATATGAACACAAAAATTCTAAGATCTAAAATTTAAGCTTTCAGTAGTCCTTCTA +TAACTGTGAAAGTTTGGTTCCTAAAAAGCCCTGAGGAATTTATGGGAAAACAAGAGAGAC +AACATTTAGTAGTGAACCTGTGCATTCTAAATAAAGACAATATCAATGACGTGTTATAGG +TCTTCAATTAGTAAGAATGAATATTGGACTATGAATTTTTATTCACTGTCACTTGTTTGC +TAGATGCTTTGAGAATCTTCCTTGCCTATATTTTCCTGAGATGTTGGTTTTTCTTTGTCA +CAGATAACAATGCTCATTCCCTCCCCATTAAAAACTAAATATATATATATATATATATAT +GATTAAACGATTACTACATGTGCTTTGAAATATTCAAATATTTTAGACAGTAAAAGTCCC +TTGTAATTCAACCCTTTGCAGATGATTGGTTAACAGGTTAGTACACATCTACCTAAATTT +AAAATCCCATATTTAACATGTATACTTATTAGAAAGTACACATTCTAATATTTTTCTATT +GTATTTGGTACTATTTTCAGATGCTCCTGCCTTTTTCTTTCGTAATTTTGAAGGACCTCA +GCTCCCTGCCTCCTAGATTTTTGCTACTATGGTCTCAGAGCTGTGTAATTTGGATGACTG +AGATGGAAAAACCTCTGGAAAACCTTTATTTATGTTGAATAAGTATTCCTTGAATCCTTC +CTCAGCATCCTGGGTTATATTTGATTTGCTCTGCTCATGATAACTTCATGCCAAGGAGAC +TGCTATCAGTTCTCTTAAAACAGATCCCAACTCCCTGCTCATAGTGGCCAAAGGAATGGA +GATTTCAGGCTGAGTTTACTTACGTGCATCATCTTCATCTATCCAGAAGCATCCCTGCAC +AAAACCTCTGTTTCTACCCTTCCATTCACTCGGCTCACTTTTCTGCTCTTAGTACCCTTT +GTTTCTTGTGAACTCTCCAGCAGGAGTGACTTGCAATTTGTATCCACTGACACTTAAGTT +CTCGGAAGTGCTGGAGAAGTGTATGGAAGTAAATTATCCTGATGTATAATTTTGTGCATG +TGAAACTCACCGTGGAAGTGCCTATCTAATTTCAGTATGGAACACAGCTAAACATTTGGA +TCAATAATCCAGTTTTGAAACCACACTTCATTTAAAGTACAATGTGCTGAAAAAAATGAA +AAAAGGGTGCTTTCAAATTTGTACTTAGTAAACTTTCACTAGATCACATCATATGTTTAT +CACTAGTCATGTTGTATTTCTATGTGTAATCGCCAGGCACTTTTAATTTCTAGTTTGCAT +TTACCATGCCAGCCTCCTCCTCAATCCCAAATTTCCTTTGGTTATAAATTTAGTAAATTT +GAAAGAGCCAGCAGGGATTAAACCCTGAAGGTATTCAAATGACTATCTGACGTTATTCCT +CATTTCAGCCATTTCGAAAAATTATGCTTTCATTTAGAATAGGCTCTGGGAATCAAAGTG +TGTGTATTTTGCCCAAGTAGAAGACACAGTTTAAAGTTAACATCCTAGCTACTAGAAGGG +AAAGCAAACAACATCGCTGCAAAAGGAGCCTATTTTTTTTTTACCTTACACTAAAACTAC +ATTGTGAAGATCAAACGAAATCAAGATGAGAGTGTGCCTCTTAACGCCAGGTCCAAAGTA +GATGCTTATTAAATGATAGTTTACCCCAATCCTTCACAAATGGTTGATAGGTCTTACTAT +TTCCCCCCTATTCAAATCTAGAATTTTTTCACTCCCATATACTAATCGATAGTTAATGGA +AAGCACAGAATAGATCATCGTCCAAGTGTTAGGTATTAGCCTGAGGAATCCGGAATCCCA +TATTTGTAACTGTCCTTCTTGAGAAAGTGCATTTTTCAGGCGGATTCTAGCCCCATTTTT +CCTTTTACCATTTTTACATGTTATGAGAGGTGGCTTAGAAATACTTCGATTTTTGCCTCT +TCATCACAACACACTGAACGTTAAAATCAAGTGGTTGGGTTTTTATTGGCTTATTTTGTC +TCTAACCGTTTTATTTCTCGAGCTGTCATCGTTCTTTTCGTCTTACATCCTTATGAACCT +TTTCTGGATTAAAAAAATGACGTTATAATAAGGAAACTGTAACTGGCGTTGGATTAGAAC +GAAGTTGACTCCATTCCTTTTCCTCCCCGTAGTGTGGGCGATACGAGGAAAGACCTCGGC +AAGAACCAGCGAAGCCCCGGCTGCCCTCGCCCTGCGGGCGCACACTTGCTCCTCGCGCCG +GGCTGCGCCGGGCGCCCGCGCCGCCTCGGCGTGTGTCCGCGGCTCCCTCCCGCCCTCGCC +CGCAGTCCCCCGATCCCGATCCCGGATCTCTGGGTCCACAGCTTGGCTCCCTCCCGAGCC +GGAGCCGGAGCCGGAGCCGAAGTCGCGGCTGGGCCCGGCCGCCCCGTCACAGGGGGAGGG +AACCCATGGGGAGGGGGAGGGGCGGTGAGGTCAGCGGCGGCGGCGCGTCCGCGGGCGGCG +GGAGCTTCGCATGCGCGGAGCGAGGCCCGTGAGTGGCAGCGGCGGCGCGCGGGGGGCGGG +CGAGGGGCCGAGAGTGGGGGAGCGGGCGGGGGCCGTCGAGGAGGCGTTGTGTGGGCGCGA +CGGCTGCGAGTTGGGGAGGTCTGTGGTGCGGGTCGCCCCGGGGGATCCCCGGCGCGGGCC +TCGCGCGACGGCCACGGTCGCGCGGCGTGTGTGGGGGGTCCACGCACACCCGCAAAACTT +CCTCCTCCCCTGCTCCGGGAGAGCGAGCGAGCGTGTGTGAGAGCGAGTGTGAGGAGCGAG +CCGCGGCCCGACGCCCAGCGCCGCCGCTGGAGCAGCTGTCAAAACTTCGCCGCCGCCCGG +GCCCCGCGGCCCGCCCTCCCCGCGCCGGGCCCCTTTCTCTTCCTGCTGCGGGCGGCCCGG +GGGAGGGGCCGCGGGCGGAGACCCCGGAGGCCGGCGCCCCTCACGCCGCCCGCCCGCCCG +CTCCCCGCCCGGCCCCTGCGCGCGTGCGTGTCCTGCTCGCTCCATGTTGCCGCCTCTCCC +GGTACCTGCTGCTGCTCCCGGGGCTTCGGGAAATGCGAGAGTCTGAGCCGGGGAGGAGGA +ACCCGAGCAGCGGCGGCGGCGGCCGCGGCGGCGGGAGCCCCCCAAGAGGAGGACCGGGAT +CCATGTGTCTTTCCTGGTGACTAGGATGTCGTCGGAGGAGAACAAGTGCGTGGAGCAGCC +GCAGCCACCACCCCCCGAGGAGCCTGGAGCCCCGGCCCCGAGCCCCCCAGCCGCAGACAA +AAGACCTCGGGGCCGGCCTCGCAAGGCGCTTCCCCTTTCCAGAGAGCCAGAAAGAAGTAA +GTTGAGTGCGAGGGAGCCAGGCCGGGAGCCAGCGGCGGCGCCGGGCCGGAGCTGCCACCG +GGCGCCCGCCCCGCGGCCTCCACGCCTTGGCGCCCCCCGGCGGGATGGGGGCGGGGCGGG +CCCGCGGGCGGCGGCAGCTCCCGGCCCCGGCCCCACGCCCCTCGGTAGCCGCCCGCGCCC +GGCCTCCCCCGCTCCGCGCCGCCCGCCCGGGCTCCCGTCGGCGCCCGGCTTCGCACACTT +TACTTTTCAGTCGGGCCTTTTCAGTGGGTCTTCTCCGCGACTCTTCTTTTGGAGAAATTT +CTCGTAGCCGCGTCTTGGCCTAGCTGGATCATTGAGAAAACAAGCCCGGAGCGCGCGCAG +GTAGTCCCCGGACGGACTCCGAGCGAACCGCCGAGCCGTGGGCGCTCGGGAAACTCGGAG +CTGTCAAAACGCCCGGGCCAGGTGGTCTCGGGGCGCGGGCTGGGGGCGAGAAGAAAGCGG +CCGGGCGAGTGCAGCTTTTGTTTGTCAGCGACTCGTTCGTGGAACTTTTCCTGGTCCCAA +ACCTGTGTTTTCTTCTTTTGATGATATATTAGGAAGCCATTTGGCTTCTTCCTTCCCCCT +CCCCCAACACCCAGCACCGCACTCCCGGGCTCCGAAAGCACAAGTCCTGTGGGAACCCCC +AGCTTCGGGGAACGGCCTGCCTAAGTTTTGGAGACGTAGCCAGCGTCCCCTCGTAAGGCA +GAATACCAAGAGCACTTATTCAGAGAGAGTGCAGATGTAAATGTCGTTTCCCTCGTAAGT +CTTAGCTGTAAGGGGCTTGGGAATAGGGTCGCCTGCCTTTGACCGACCGTACTGTAGGGC +TGGACACCGGCTTATTAGAGGACCAGAAATGTCTTCTTACAGAACGGTTATTTGACGGCT +TTGCTTGTAAATTAAGACACCGTTTTAGTGCCAGCGAGCTGCTCGGCTTCTGTGGCTCTC +GCGTGTGCCGTGGAAGAACTGTGAATGTCTTTCGAAGTTGTAGAATGGCGTGTGTGCTTA +CTCATTTCATGAGATGATATTCTCATTGAACTGTCGGGAGTGGAAGGGTGCGCTGGGACG +TGAAGGAAGCCAGCACGTTTATGGATAGGCTGTTTCTTTGGTTCGGGTGCATTCACTTAG +TAATAGTGTTGTTTGGTGATTTGTAGTAAAAATAGTAGCGTGAACTGAGGCATAGCAGAG +CTGGGTTGTGGGAACCCATTAAGCTCTTGACTTGAATGTGCTCTTTTCTTGCCCCGCTGT +CCTTTTACTATGAAAATGATTCAGGGCCTTCAACTTGCCTCCATATTTTATTGCCAGCTC +TTACCTAGCTATGATAATCGTGAGGGAGGCAAGTACAGGATGTGTGTACGTTATTACATT +AGCTTCTTCGTGATACAAAGTTAGGACTTACTTATGCCACTTGCGTTGTAATACAATGGC +AAATATAAAATGCCCTTATTCTATATTAACTGAAATTTGGAGAAGGAAGTGGAGGTTTAA +GTAATTTTTAGACGTCTAAGCCACTTTTTTGCATCCTTTAAAGCAACTCAGGACAAGCCA +TATTGGGGGTTTTACCTTGATTGCCTCCCATTTCACTATTTGCAAAGCATTTCTTCATCT +CTTACTGAACATTAATTTGCAATTTTTTTTTTTAATTTGCATTTGAATTCTTACTCCAGA +AAGATTAGATCTGTGTTGTCACACCCCACACCCCATACTCCTGTAAGGGCGTGCTTGTGC +ACGCGCACACGCTCACACGCACGCGCACACTCGCACACACCCTACTTTTGAAATGAGCTC +ATTTGTATTAGTGCAGCTCCTGAGTGCACTGGACGATTAGGGTATTGCCACTTTATTATT +TTAATTCTTAATCTCATATTATGAAGAAATAGGTAGCCTTTGGAGAAGATAAAAAATTTC +TGCTGAATAACAGTATAATCTAACTATGAAACATCAAAACTTTTGGAAATATTTAGAACA +AATGTAAGTCTGTAGAGAGCTTTTTCTTTTAGATTTGAAAACTAGTACTGCTTTCTTTAT +AGGAAAGTAAAGTCTACTGGTAAATTTCACGGGTCTAAACTTTTTAGAGCTTTTTTTTGA +AATTGTGTCTTTTGAAGGGAGTGGAATCTCCAGTTGTTTTTAGAAACATGTAAATGGAAA +CTAACATATGAATTGGAAAGCAAAGAGAAAGTTTTTCAATTGTGTATCTCTATACTGTAT +AAGAATCCATGCAGAAAAGACCCTGTAGTTGGATAGTAAAGACCCTGAAGGTGAAACTTA +TGTGTAACCAGTGTAAATTAGGTTTGTAACCAGTGAAATTATGTGAAATTGCAAATAATT +CACCTGAGAAATGAAAATTAATCTTCTTTGCTAAATGCCATAGAGATATTTTAAGTTGCT +AATGTTACTTAGATGTTCATTAACTTAGTGAGTTACATTAAGTAGAGAAGATGCCTTTTT +TTTTTTTCTGTACGAAGTCTTGCTCTGTAGCCCAGTGTAGTGGTATGATCTCGGCTCACC +ACAACCTCCGCCTCCTGTATTCAAGCGACTCTCCTGCCTCAGCCTCCAGAGTAGCTGGGA +TTACAGGTGTGCACCATCGCACCTAGCTAATTTTTTGTATTTTTAGCAGAGACAGCATTT +CACCATGTTGGCCAGGCTGTTCTTGAACCCCCGACCTCAGGTAATCCACCCTCCTTAGCC +TCCCAAAGTGCCAGGATTACAGGCGTGAGCCACTGCACCCTGCTGAGAAGATGCCTTTTG +ACAATGAAGTGGATTTGTATATTTATCTTTGGCTTAAAAAAACATGCACCACCAATTACA +CTTTCCTCAAGTTTAAATTTTTAATAATTAGGAAAATAAAGCATTTTCTTGTCTTATAGT +GTTAGCTAGATTGTTTTTGTGTATTTTGTCATGAATAAAAAGCATAGCTATATAGTTACT +GCTTTTACATTAACTATAAATATCTTAAAATTTTACTACCTAAAATCAGGAAACTTGAAC +TGAAGCTACTAATCTTAGAGTTGGAAAAGTAAATACATAGAGGTTTCCTGTTGTACAAAT +GTCAAGTGGCACAGTGAAATTTACATTCATTTGAAAGTTTTCCTTAACTGTAAAAAGTAT +CAAATTACTTGATACTTTGGAGTAGTTCATCATCTTTATCAGAGGCACAGGTCTTAACCA +TTGGCAAGCCTCTGTCAGAATATGCACATATTAAAGATCTGATTATTTTTGTGTTAATGT +TAAAAAATTTTTCTGAAGCTTTTATCTTATTTTTTCCATCCTTACACCGTAAATTCACAT +TACCAAGTTGGGAAGCCAAAGAAACATTCTACTCTACTATGTTTCTTACCAGTTCATGAA +AGTTGATGTTAGAAATGGGTGTGGGTGTGGGGGATGGGGGTGGTTGTACAGAAGCAGCAG +GTGGTAGGGATAGGATTTCTGAAGCACTATCCTTGGCCTTTTTTGAGTAAACTCTTTATA +CCCTGAGCCACTTTCTTTTCAGAGGGCAATTGCTATTATTAGAGAGCCACCTTAAGCATT +ATTGTTGTAGAAAAATTAGGCACAACCAGTGATTGTCATTACAAGGACCAGCAAAAATGG +CTAGGTTGCTACTCTGTATTTGTAACGCCCTTCCCCCAACAAAATTTCTCCTTTTCATAT +CTGTGAATTAGAAATAAGTGATAGAAAACTGTACTGCATTACAATATATACCATTTAATA +AAACAAGTTTATAGTTGAGAGCACTATTCATGCTTTTTGAGATAATGCAAATTTGTAATT +TTTATGATAGCAATTCTTAATAATTTATTGTCCAAGAGATTTGATAAAATTTTTGATAGT +TATTGGTCTCTGGGACTCAATAGGCACTGAAATGTTTTAATTCAGTTGAAAAGTTGGTTC +AGGATTGCTACCCTCTCTTACCTGTTAGGAGGTTGTTGTTTAACCTGACCTGAAATTCCC +ATGAATAAGAACCTGTTTTTTTTTTTTTTTTCTTTGACAGAGTCTTGCTCTGTCGCCCAG +GCTGCAGTGCAGTGGTGCGATCTTGGCTCGCTGCAAGTTCCGCCTCCCAGGTTCAAGCGA +TTCTCCTGTCTCAGCCTCCCAAGTAGCTGGAGTAGCTGGGACTGCAGGCACGTACCACCA +TGCCTGACTAATTTTTGTATTTTTAGTAGAGACGGGGTTTCACCGTGTTAGCCAGGATGG +TCGCAATCTCTTGACCTCATGATCTGCCTGCCTTGGCCTCCCAAAGTGCTGGGATTACAG +GTGTGAGCCACCGCACCTGGCCCAGGGAATTTCTAATATTTGAGAAGATGTTATTTTTAG +TCTATTATACAAATTTATATATTGTTTACTAATATATAAATTTACATATTGGTTACTAAT +ATGTAAACACCAATTTACATATTGGTTACTAATATGTAAACTTGATAAACATGGATTTCC +ATGGAAATTTAAAAGTATCACAACAATTTGTTTTCCCATTCTGAAACTTGTGATTTATTA +CATTTTCCTACTATTTCAGTTAATTCCATAATGCCAGATTTGTTGTCAATTTGCCGAGTG +ACAAGCCACACTGCTTCCTCTCATTCCTCTATTCCGCAAAACTGCAAAGTTTCCCAGACC +ACAGTCAGGTTTCTCTGGGTTGTCCAACTCTGTAAACTTACAGAGTGGTTGTCCAACTCT +GTAAACTTACAGAGTGGTTGTCCAACTCTGTAAACTTACAGAGTGGTTGTCCAACTCTGT +AAACTTAAGTCACTTTAAGTTTATGACGGAGGGGCTTCGTGAAACTTCATTGACCTTCCA +AGGTGAAAATTGGTCAGTTTTCAGTTATAAAGGACATTAAGGATGGGTGTGGTGGCTGAT +ACATGTAATCCCAGCACTTTCGGGAGACTGAGTCAGGAGGATCACTTAATCCTCATTTAA +AAGGAGTTTGAGACCAGCCTGGGCAACAAAGTGAGGCCTTGTCTCTACAAAAAAATTAGC +TGGGTGTGGTGGTAGGCACTTGTAATCCCAACTACTCTGGAGACTGAGCTGAGAGAAGAT +TGTGTGAGGCTTGGAGGTTGAGGCTGCAGTGAACGGACATCACACCACTACACTCTAGTC +AGGTGACAGAGCAAGACTCTAAATAAATAGGAACATTAGATGGTCTCTCTGCACTCTTGC +CTGGTGGGGACGTGTTAGATACCCTCGTTAGGTTGTGATTTAGTTTTTAATCTGTGAGAT +GTTTGGGTCAAACAATTTTTAGCTGCCATGGAATAAACTTTCCAGTCAGCGTGTGAGTTT +GTGTTTGCCTTTACTTTTTTTTTTCTATATTGTTTTGGTCTATTTTTATCTTTTAATTTC +AGAAAGCTGATTAATCTCTTCCTTTTCTCTTTAAAAATTTTCTTTATCATGTTTGTGCTA +CAGTGGTTATTTTGAGAACTTGTTGGCAGGATAAGTTGCAAAAGTTATGAAGTAGAATAG +GGATGATTTCTGTTTTTGTTTTTTTTTTTTTCAGACAGAGTCTCACTCTCTTGCCTAGGC +TGGAGTGCAGTGGCGTGATCCTGGCTCACTGCAGCCGCCGCCCTCCGGATTCAAGTGATT +TGCCTGGCTCAGCCTCCCAAAAAGCTGGGATTACAGGTGCATGCCACCACACCCAGCTAA +TTTTTGTGTTTTTAGTAGAGATGGGTGTTCACCATGTTGGCCAGGCTGGTCTCAAACTCC +TGACCTCAGGTGATCTGCCTGCCTCCGCACTCCCAAAGTGCTGGGATTACAGACGTGAGC +CACCATGCCTGGCTGAGATTATTTCTTTTTTTATTATAGCCATTGCTTGTAGATATATGC +TGGTGGTTATCTGTAAAAATGTAATAGAAAGGCCGGGCACGGTGGCTCACACCGGTAATC +CCAGCACTTTGGGAGGCTGAGGTGGGCGGATCACAAGGTCAGGAGTGGGAGACCAGCCTG +GCCAATATGGTGAAACCCCGTCTCTACCAAAAATACAAAAATTAGCTGGGCATAGTGGCG +GGCACCTATAGTCCCAGTGACTCGGGAAGCTGAGGCAGGACAATCGCTTGAACCCAGGAG +GCAGAGGTTGCAGTGAGCTGAGATCGTGCTATTATTGCACACCAGCCTGGGCGACAGAGT +GAGACTCCGTCTCAAAAAGAAAAAAGTAATAGACCAATCTTGAATTTATAATTGGAAGTG +TTGATCCCTTTATTTGCAGAATTTATTTATTTGTGACGCAGCTGTTGCTACCTCGCCTTT +TCTTTTGTTGAGCTTAATCTCATGTCAAGTCATTCAACCAACTCAAAAGCGATGAAGACA +TTATTGAATCAACCTGAACTAAATCAGACCTAGGCTTCTTAAAATATACAGCTTAATGCT +TCCAAATGATTTAGAAAACTAAAAAACCTAGCTACGCTGTAGGACACACAGTGGCCAATA +ATACAGGACCCCCAAACTGGCCAGTGGACCACTGCAACCACTATTTACTTCCTCCGTGTT +TAGGAATGTTCAACGCTCCAAGCCCCATAGGCTGATTCAAGAAGATAAAGTGAGACTCAA +GGAATTTCGAAGTGGAACAATACACCAAAGCCTTAAACCTGAAATGACTCTCCTTTTCTG +GGGGGTGAGGGGGAAAGAAAAAGAAAAAGTTTCTAGGGCTCTCGGGGTGGCCTGGATGCC +AGGGTCCCAGAAGTGGCCTTTTCTAGCTCCTGTAACTAAACCTGGCGGAAAACTCCCCGC +CTGCTCACTCCACCCCCACCCGCCCAAGAATGCGTCTTCCCGTCTTCGGTGGCCCTACCC +AGAATCCCAAAATGTGGGTTCCAACCCGGGCCCTGAATGTCTTCTCAAATCCCCGGGACC +CAGGTTCCGGTGCGTGCCTTGCGTGCCGGGTCTTGCCCCTCGGGCGGTACCACCCAGGCA +GCCCTAAATCCAGCCTCCCGGGCCCCCAGCAGCGCCCTCCGCCCCTCCACTATCCGGTCC +GGCTCGAAGTCGGGGCCAAATCCAGAGACAAGAGGGCTGTGCCTGAAACTGAGCAGTTTC +ACCACTCGGCACTCCTGGCGGAAACTTCCCTTTAAAAAAAAGAAAAGAAAAGAAAAGCAA +CAGCACTTTTGGGCTAGCATTTCAATCCTTCCTGCCCTTTAGAGTTCCCAGTTCTGCTTC +CAGCTGGCTTTGGGTGTTCCACTAGAATTGAGTTGTAAAGATATTCTTTAAGTGTTTATA +GAACATTAAGACTTAAAAAAAATCTTTAAAATTAGAGGAGGGAAAAAGCCACCTTATCGC +ACACATCCAGGAAATGCAGCCCCGTGCATCCCTGCTCAGGGATGAGCAGGCGCCCCAGGA +CTCCCGGAGACAGATTTTTGGGCACCCGAGGGAGTCACCGGGCGCGTGTCGGGGTCCGCG +GTGAGGCCCAGCCCCTCCGGCGGTCCCTTAGACGCGCCCTCTGCCCGGCCGGTGTGGACC +GTCCCGGCCATTGTTTACGGGGGATGCCCGTCCAGACGCATTGTTTTGGCCGTTTCCAAC +TTGCCCCGGCCCTTTCCGGGGCATCGCGGGGGACCCTACACCGACGTCCCCCCTCCGCCC +GCGCCCCAAGGGCTGACTGGGCAAATTGGCAGATCCGCCCCGCGGGGCGACCCAACTTTT +CGGAACAGCCCCCCACCGCCCACCCCTGCAGATCCCCGGACCCCCGCTCCCGGCGGAGAT +TCAGGGAACCCCGCATCCCAAGCCCTTCTAAATCGTGCGGCCTGAGTGTGACGGCCAAGA +GCGGATGCAGCCCGGGATCGCCCGCACCTTCCCGTGGGCGG diff --git a/tests/data/dna/genome.fasta.fai b/tests/data/dna/genome.fasta.fai new file mode 100644 index 00000000..b542e338 --- /dev/null +++ b/tests/data/dna/genome.fasta.fai @@ -0,0 +1 @@ +chr22 40001 7 60 61 diff --git a/tests/data/dna/targets.bed b/tests/data/dna/targets.bed new file mode 100644 index 00000000..54498e32 --- /dev/null +++ b/tests/data/dna/targets.bed @@ -0,0 +1,2 @@ +chr22 1 15000 +chr22 20000 40001 diff --git a/tests/data/dna/test.dna.bam b/tests/data/dna/test.dna.bam new file mode 100644 index 00000000..d0c6a339 Binary files /dev/null and b/tests/data/dna/test.dna.bam differ diff --git a/tests/data/dna/test.dna.bam.bai b/tests/data/dna/test.dna.bam.bai new file mode 100644 index 00000000..ce91243c Binary files /dev/null and b/tests/data/dna/test.dna.bam.bai differ diff --git a/tests/data/test_genes.bed b/tests/data/test_genes.bed new file mode 100644 index 00000000..3d13e0c5 --- /dev/null +++ b/tests/data/test_genes.bed @@ -0,0 +1,8 @@ +chr1 999 3800 GENE_HIGH 0 + 999 3800 0 3 501,501,801, 0,1000,2000, +chr1 4999 6500 GENE_MED 0 + 4999 6500 0 2 601,501, 0,1000, +chr1 7999 8500 GENE_LOW 0 - 7999 8500 0 1 501, 0, +chr1 9999 10300 GENE_VLOW 0 - 9999 10300 0 1 301, 0, +chr1 11999 12400 GENE_ZERO 0 + 11999 12400 0 1 401, 0, +chr2 999 2600 GENE_MED2 0 + 999 2600 0 2 401,601, 0,1000, +chr2 3999 4800 GENE_OVERLAP 0 - 3999 4800 0 3 301,401,301, 0,200,500, +chr2 5999 6100 GENE_SMALL 0 + 5999 6100 0 1 101, 0, diff --git a/tests/dna_integration_test.rs b/tests/dna_integration_test.rs new file mode 100644 index 00000000..54c507ec --- /dev/null +++ b/tests/dna_integration_test.rs @@ -0,0 +1,1431 @@ +//! Parity tests for the DNA pipeline against the committed reference outputs. +//! +//! The fixtures under `tests/expected/dna/` are the output of the upstream +//! tools themselves, at the versions pinned in `VERSIONS.txt`. A failure here +//! is a defect in RustQC, not a reason to regenerate the fixture. +//! +//! Compressed outputs are compared on their decompressed bytes. Two bgzf +//! writers at the same compression level need not emit identical compressed +//! bytes, so comparing the `.gz` files directly would test the compressor +//! rather than this code. + +use std::collections::BTreeMap; +use std::io::Read; +use std::path::{Path, PathBuf}; + +use rust_htslib::bam::Read as BamRead; +use rust_htslib::{bam, bgzf}; + +use rustqc::dna::depth::{DepthAccum, MOSDEPTH_DEFAULT_EXCLUDE}; +use rustqc::dna::gc_bias::{self, GcBiasAccum}; +use rustqc::dna::hs_metrics::{self, HsAccum, HsCounters, HsMetricsResult}; +use rustqc::dna::insert_size::{self, InsertSizeAccum}; +use rustqc::dna::intervals::IntervalSet; +use rustqc::dna::mosdepth::{output, ContigDepth, MosdepthResult}; +use rustqc::dna::qualimap::{self, ContigQualimap, QualimapAccum}; +use rustqc::dna::qualimap_output; +use rustqc::dna::wgs_metrics::{self, WgsAccum, WgsMetricsResult}; + +/// Window size and thresholds the fixtures were generated with. +const WINDOW_SIZE: u32 = 500; +const THRESHOLDS: [u32; 7] = [1, 5, 10, 15, 20, 30, 50]; + +fn fixture(name: &str) -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests/expected/dna") + .join(name) +} + +fn scratch(name: &str) -> PathBuf { + let dir = std::env::temp_dir().join("rustqc-dna-parity"); + std::fs::create_dir_all(&dir).unwrap(); + dir.join(name) +} + +/// Run the depth engine over the committed test BAM and summarise it exactly +/// as the fixtures were generated. +fn compute() -> MosdepthResult { + let bam_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/data/dna/test.dna.bam"); + let reader = bam::Reader::from_path(&bam_path).unwrap(); + let header = reader.header().to_owned(); + + let mut contigs = Vec::new(); + for tid in 0..header.target_count() { + let name = String::from_utf8(header.tid2name(tid).to_vec()).unwrap(); + let length = header.target_len(tid).unwrap(); + let mut accum = DepthAccum::new(length, 0, MOSDEPTH_DEFAULT_EXCLUDE); + + let mut record = bam::Record::new(); + let mut per_contig = bam::Reader::from_path(&bam_path).unwrap(); + while let Some(result) = per_contig.read(&mut record) { + result.unwrap(); + if record.tid() == tid as i32 { + accum.process_read(&record); + } + } + let depths = accum.into_depths(); + contigs.push(ContigDepth::from_depths( + &name, + &depths, + Some(WINDOW_SIZE), + &THRESHOLDS, + )); + } + + MosdepthResult { + contigs, + window_size: Some(WINDOW_SIZE), + thresholds: THRESHOLDS.to_vec(), + } +} + +fn read_bgzf(path: &Path) -> String { + let mut reader = bgzf::Reader::from_path(path).unwrap(); + let mut buf = Vec::new(); + reader.read_to_end(&mut buf).unwrap(); + String::from_utf8(buf).unwrap() +} + +/// Compare line by line so a failure names the offending row. +fn assert_same_lines(actual: &str, expected: &str, what: &str) { + let a: Vec<&str> = actual.lines().collect(); + let e: Vec<&str> = expected.lines().collect(); + for (i, (got, want)) in a.iter().zip(e.iter()).enumerate() { + assert_eq!(got, want, "{what}: line {} differs", i + 1); + } + assert_eq!(a.len(), e.len(), "{what}: line count differs"); +} + +#[test] +fn fixture_tool_versions_are_the_pinned_ones() { + let versions = std::fs::read_to_string(fixture("VERSIONS.txt")).unwrap(); + assert!( + versions.contains("mosdepth\t0.3.14"), + "unexpected mosdepth fixture version: {versions}" + ); + assert!( + versions.contains("samtools\t1.24"), + "unexpected samtools fixture version: {versions}" + ); +} + +#[test] +fn summary_matches_mosdepth() { + let path = scratch("test.mosdepth.summary.txt"); + output::write_summary(&compute(), &path).unwrap(); + assert_same_lines( + &std::fs::read_to_string(&path).unwrap(), + &std::fs::read_to_string(fixture("test.mosdepth.summary.txt")).unwrap(), + "summary", + ); +} + +#[test] +fn global_dist_matches_mosdepth() { + let path = scratch("test.mosdepth.global.dist.txt"); + output::write_global_dist(&compute(), &path).unwrap(); + assert_same_lines( + &std::fs::read_to_string(&path).unwrap(), + &std::fs::read_to_string(fixture("test.mosdepth.global.dist.txt")).unwrap(), + "global dist", + ); +} + +#[test] +fn region_dist_matches_mosdepth() { + let path = scratch("test.mosdepth.region.dist.txt"); + output::write_region_dist(&compute(), &path).unwrap(); + assert_same_lines( + &std::fs::read_to_string(&path).unwrap(), + &std::fs::read_to_string(fixture("test.mosdepth.region.dist.txt")).unwrap(), + "region dist", + ); +} + +#[test] +fn per_base_matches_mosdepth() { + let path = scratch("test.per-base.bed.gz"); + output::write_per_base(&compute(), &path).unwrap(); + assert_same_lines( + &read_bgzf(&path), + &read_bgzf(&fixture("test.per-base.bed.gz")), + "per-base", + ); +} + +#[test] +fn regions_match_mosdepth() { + let path = scratch("test.regions.bed.gz"); + output::write_regions(&compute(), &path).unwrap(); + assert_same_lines( + &read_bgzf(&path), + &read_bgzf(&fixture("test.regions.bed.gz")), + "regions", + ); +} + +#[test] +fn thresholds_match_mosdepth() { + let path = scratch("test.thresholds.bed.gz"); + output::write_thresholds(&compute(), &path).unwrap(); + assert_same_lines( + &read_bgzf(&path), + &read_bgzf(&fixture("test.thresholds.bed.gz")), + "thresholds", + ); +} + +/// The depth histogram is the input to both distribution files, so pinning it +/// separately makes a distribution failure easy to attribute. +#[test] +fn depth_histogram_matches_the_per_base_fixture() { + let result = compute(); + let mut expected: BTreeMap = BTreeMap::new(); + for line in read_bgzf(&fixture("test.per-base.bed.gz")).lines() { + let fields: Vec<&str> = line.split('\t').collect(); + let start: u64 = fields[1].parse().unwrap(); + let end: u64 = fields[2].parse().unwrap(); + let depth: u32 = fields[3].parse().unwrap(); + *expected.entry(depth).or_insert(0) += end - start; + } + assert_eq!(result.contigs[0].histogram, expected); +} + +// =================================================================== +// End-to-end parity: the binary, not just the library +// =================================================================== + +/// Run `rustqc dna` once into a scratch directory shared by every end-to-end +/// test, with the same window size and thresholds the fixtures were made with. +fn run_binary() -> &'static Path { + static OUTDIR: std::sync::OnceLock = std::sync::OnceLock::new(); + OUTDIR.get_or_init(|| { + let root = Path::new(env!("CARGO_MANIFEST_DIR")); + let outdir = std::env::temp_dir().join("rustqc-dna-e2e"); + let _ = std::fs::remove_dir_all(&outdir); + std::fs::create_dir_all(&outdir).unwrap(); + + let status = std::process::Command::new(env!("CARGO_BIN_EXE_rustqc")) + .arg("dna") + .arg(root.join("tests/data/dna/test.dna.bam")) + .arg("--outdir") + .arg(&outdir) + .arg("--window-size") + .arg(WINDOW_SIZE.to_string()) + .arg("--reference") + .arg(root.join("tests/data/dna/genome.fasta")) + .arg("--quiet") + .status() + .expect("failed to run the rustqc binary"); + assert!(status.success(), "rustqc dna exited with {status}"); + outdir + }) +} + +/// The sample name is the BAM file stem, dots included. +const SAMPLE: &str = "test.dna"; + +fn produced(subdir: &str, name: &str) -> PathBuf { + run_binary().join(subdir).join(name) +} + +#[test] +fn binary_writes_every_mosdepth_output_byte_for_byte() { + for suffix in [ + "mosdepth.summary.txt", + "mosdepth.global.dist.txt", + "mosdepth.region.dist.txt", + ] { + let got = std::fs::read_to_string(produced("mosdepth", &format!("{SAMPLE}.{suffix}"))) + .unwrap_or_else(|e| panic!("reading {suffix}: {e}")); + let want = std::fs::read_to_string(fixture(&format!("test.{suffix}"))).unwrap(); + assert_same_lines(&got, &want, suffix); + } + for suffix in ["per-base.bed.gz", "regions.bed.gz", "thresholds.bed.gz"] { + let got = read_bgzf(&produced("mosdepth", &format!("{SAMPLE}.{suffix}"))); + let want = read_bgzf(&fixture(&format!("test.{suffix}"))); + assert_same_lines(&got, &want, suffix); + } +} + +#[test] +fn binary_writes_flagstat_and_idxstats_byte_for_byte() { + for suffix in ["flagstat", "idxstats"] { + let got = std::fs::read_to_string(produced("samtools", &format!("{SAMPLE}.{suffix}.txt"))) + .unwrap(); + let want = std::fs::read_to_string(fixture(&format!("test.{suffix}.txt"))).unwrap(); + assert_eq!(got, want, "{suffix} must match samtools exactly"); + } +} + +/// `samtools stats` output is compared on its data lines only. RustQC writes +/// its own `#` header, naming itself rather than reproducing samtools' command +/// line and version banner, which is deliberate and shared with the `rna` +/// pipeline. Everything below the header must match exactly. +#[test] +fn binary_writes_samtools_stats_data_lines_byte_for_byte() { + let got = + std::fs::read_to_string(produced("samtools", &format!("{SAMPLE}.stats.txt"))).unwrap(); + let want = std::fs::read_to_string(fixture("test.stats.txt")).unwrap(); + let strip = |s: &str| { + s.lines() + .filter(|l| !l.starts_with('#')) + .collect::>() + .join("\n") + }; + assert_same_lines(&strip(&got), &strip(&want), "samtools stats data lines"); +} + +#[test] +fn the_stats_header_does_not_claim_the_wrong_subcommand() { + let got = + std::fs::read_to_string(produced("samtools", &format!("{SAMPLE}.stats.txt"))).unwrap(); + assert!( + !got.contains("rustqc rna"), + "the dna pipeline must not label its output as rna output" + ); +} + +#[test] +fn binary_refuses_input_without_duplicate_marks() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")); + let outdir = std::env::temp_dir().join("rustqc-dna-nodup"); + let _ = std::fs::remove_dir_all(&outdir); + let output = std::process::Command::new(env!("CARGO_BIN_EXE_rustqc")) + .arg("dna") + .arg(root.join("tests/data/test_nodup.bam")) + .arg("--outdir") + .arg(&outdir) + .arg("--json-summary") + .arg("-") + .output() + .expect("failed to run the rustqc binary"); + let combined = format!( + "{}{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + assert!( + combined.contains("duplicate-flagged") || combined.contains("failed"), + "expected a duplicate-marking complaint, got: {combined}" + ); +} + +/// The JSON summary is the machine-readable face of a run, so its DNA block is +/// pinned against the same figures the mosdepth fixtures carry. +#[test] +fn json_summary_carries_the_dna_block() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")); + let outdir = std::env::temp_dir().join("rustqc-dna-json"); + let _ = std::fs::remove_dir_all(&outdir); + std::fs::create_dir_all(&outdir).unwrap(); + let json_path = outdir.join("summary.json"); + + let status = std::process::Command::new(env!("CARGO_BIN_EXE_rustqc")) + .arg("dna") + .arg(root.join("tests/data/dna/test.dna.bam")) + .arg("--outdir") + .arg(&outdir) + .arg("--window-size") + .arg(WINDOW_SIZE.to_string()) + .arg("--json-summary") + .arg(&json_path) + .arg("--quiet") + .status() + .expect("failed to run the rustqc binary"); + assert!(status.success()); + + let text = std::fs::read_to_string(&json_path).unwrap(); + // Checked as text rather than parsed: the point is that these exact + // figures reach the summary, and pulling in a JSON parser for one test + // would not make the assertion any stronger. + for needle in [ + "\"genome_length\": 40001", + "\"covered_bases\": 247878", + "\"max_coverage\": 867", + "\"total_reads\": 5644", + "\"duplicates\": 1656", + ] { + assert!( + text.contains(needle), + "summary is missing {needle}:\n{text}" + ); + } + assert!( + !text.contains("\"dupradar\""), + "a dna run must not emit the rna summary blocks" + ); +} + +/// The citations file names the tools this pipeline actually replicated. +#[test] +fn citations_name_the_dna_tools_only() { + let citations = std::fs::read_to_string(run_binary().join("CITATIONS.md")).unwrap(); + assert!(citations.contains("mosdepth"), "mosdepth must be cited"); + assert!(citations.contains("Samtools"), "samtools must be cited"); + assert!( + !citations.contains("dupRadar") && !citations.contains("RSeQC"), + "a dna run must not cite the rna-only tools" + ); +} + +/// The `.csi` companion indexes are not compared byte for byte: an index is +/// binary metadata over the compressed blocks, and two writers answering the +/// same queries need not produce the same bytes. What matters is that a region +/// query returns the same rows through our index as through mosdepth's. +/// +/// The query goes through the `tabix` binary rather than rust-htslib's tabix +/// reader, which ends a fetched region by yielding a `TabixTruncatedRecord` +/// instead of stopping, and does so at different points for the two files. The +/// test is skipped where `tabix` is not installed, the same way the fixtures +/// themselves depend on the upstream tools being present. +#[test] +fn csi_indexes_answer_region_queries_like_mosdepths() { + if std::process::Command::new("tabix") + .arg("--version") + .output() + .is_err() + { + eprintln!("skipping: tabix is not installed"); + return; + } + + let query = |path: &Path| -> String { + let out = std::process::Command::new("tabix") + .arg(path) + .arg("chr22:2000-2500") + .output() + .unwrap_or_else(|e| panic!("querying {}: {e}", path.display())); + assert!( + out.status.success(), + "tabix failed on {}: {}", + path.display(), + String::from_utf8_lossy(&out.stderr) + ); + String::from_utf8(out.stdout).unwrap() + }; + + for suffix in ["per-base.bed.gz", "regions.bed.gz", "thresholds.bed.gz"] { + let ours = produced("mosdepth", &format!("{SAMPLE}.{suffix}")); + let index = ours.with_file_name(format!("{SAMPLE}.{suffix}.csi")); + assert!( + index.exists(), + "{suffix} must have a .csi companion at {}", + index.display() + ); + + let mine = query(&ours); + let theirs = query(&fixture(&format!("test.{suffix}"))); + assert!(!mine.is_empty(), "{suffix}: the query returned nothing"); + assert_eq!(mine, theirs, "{suffix}: region query results differ"); + } +} + +// =================================================================== +// Picard CollectInsertSizeMetrics +// =================================================================== + +/// Drive the insert size accumulator over the whole test BAM. +fn insert_size_result() -> insert_size::InsertSizeResult { + let bam_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/data/dna/test.dna.bam"); + let mut reader = bam::Reader::from_path(&bam_path).unwrap(); + let mut accum = InsertSizeAccum::new(); + let mut record = bam::Record::new(); + while let Some(result) = reader.read(&mut record) { + result.unwrap(); + accum.process_read(&record); + } + accum.into_result(insert_size::DEFAULT_DEVIATIONS) +} + +#[test] +fn insert_size_metrics_match_picard() { + let path = scratch("test.insert_size_metrics.txt"); + insert_size::write_insert_size_metrics(&insert_size_result(), &path).unwrap(); + assert_same_lines( + &std::fs::read_to_string(&path).unwrap(), + &std::fs::read_to_string(fixture("test.insert_size_metrics.txt")).unwrap(), + "insert size metrics", + ); +} + +/// The headline figures, pinned separately so a failure in the metrics row is +/// easy to tell apart from a failure in the histogram below it. +#[test] +fn insert_size_headline_figures_match_picard() { + let result = insert_size_result(); + let fr = result + .rows + .iter() + .find(|r| r.orientation == insert_size::PairOrientation::Fr) + .expect("the fixture library is FR"); + assert_eq!(fr.read_pairs, 1992, "read pairs"); + assert_eq!(fr.median, 122, "median insert size"); + assert_eq!(fr.mode, 96, "mode"); + assert_eq!(fr.median_absolute_deviation, 23, "MAD"); + assert_eq!(fr.min, 32, "minimum"); + assert_eq!(fr.max, 300, "maximum"); + assert!((fr.mean - 124.442269).abs() < 1e-6, "mean was {}", fr.mean); + assert!( + (fr.standard_deviation - 32.720214).abs() < 1e-6, + "standard deviation was {}", + fr.standard_deviation + ); + assert_eq!( + fr.widths, + vec![9, 19, 27, 37, 47, 57, 69, 83, 103, 127, 181], + "the eleven percentile widths" + ); +} + +// =================================================================== +// Picard CollectWgsMetrics +// =================================================================== + +fn wgs_result() -> WgsMetricsResult { + let bam_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/data/dna/test.dna.bam"); + let mut reader = bam::Reader::from_path(&bam_path).unwrap(); + let header = reader.header().to_owned(); + let length = header.target_len(0).unwrap(); + + let mut accum = WgsAccum::new( + length, + wgs_metrics::DEFAULT_MIN_MAPPING_QUALITY, + wgs_metrics::DEFAULT_MIN_BASE_QUALITY, + ); + let mut record = bam::Record::new(); + while let Some(result) = reader.read(&mut record) { + result.unwrap(); + accum.process_read(&record); + } + let (counters, depths) = accum.into_parts(); + // The fixture reference carries no N bases, so the territory is its length. + WgsMetricsResult::new(&depths, counters, length, wgs_metrics::DEFAULT_COVERAGE_CAP) +} + +/// The exclusion breakdown is the heart of this tool: it is what separates +/// Picard's coverage from a plain depth count, and each fraction is a +/// different rule. They are pinned individually so a failure names the rule +/// that broke. +#[test] +fn wgs_exclusion_fractions_match_picard() { + let result = wgs_result(); + assert_eq!( + result.counters.total_aligned_bases, 670_989, + "the denominator is every reference-aligned base of every primary mapped record" + ); + let [dupe, mapq, unpaired, baseq, overlap, capped, total] = result.exclusion_fractions(); + let close = |got: f64, want: f64, what: &str| { + assert!((got - want).abs() < 1e-6, "{what}: got {got}, want {want}"); + }; + close(dupe, 0.299737, "PCT_EXC_DUPE"); + close(mapq, 0.0, "PCT_EXC_MAPQ"); + close(unpaired, 0.0, "PCT_EXC_UNPAIRED"); + close(baseq, 0.007352, "PCT_EXC_BASEQ"); + close(overlap, 0.324694, "PCT_EXC_OVERLAP"); + close(capped, 0.157699, "PCT_EXC_CAPPED"); + close(total, 0.789481, "PCT_EXC_TOTAL"); +} + +#[test] +fn wgs_headline_figures_match_picard() { + let result = wgs_result(); + assert_eq!(result.genome_territory, 40_001); + assert_eq!(result.median_coverage, 0); + assert_eq!(result.mad_coverage, 0); + assert!( + (result.mean_coverage - 3.531312).abs() < 1e-6, + "mean was {}", + result.mean_coverage + ); + assert!( + (result.sd_coverage - 27.339314).abs() < 1e-6, + "standard deviation was {}", + result.sd_coverage + ); + let f = result.coverage_fractions(); + assert!((f[0] - 0.029124).abs() < 1e-6, "PCT_1X was {}", f[0]); + assert!((f[13] - 0.01505).abs() < 1e-6, "PCT_100X was {}", f[13]); +} + +/// The whole file, except the five columns RustQC does not compute. +/// +/// `FOLD_80/90/95_BASE_PENALTY` are `?` in the fixture too, because Picard +/// could not compute them on this data. `HET_SNP_SENSITIVITY` and `HET_SNP_Q` +/// come from a Monte Carlo simulation that is out of scope, so RustQC writes +/// `?` where Picard writes a sampled value. Those two positions are the only +/// permitted difference. +#[test] +fn wgs_metrics_file_matches_picard_except_the_simulated_columns() { + let path = scratch("test.wgs_metrics.txt"); + wgs_metrics::write_wgs_metrics(&wgs_result(), &path).unwrap(); + let got = std::fs::read_to_string(&path).unwrap(); + let want = std::fs::read_to_string(fixture("test.wgs_metrics.txt")).unwrap(); + + let got_lines: Vec<&str> = got.lines().collect(); + let want_lines: Vec<&str> = want.lines().collect(); + assert_eq!( + got_lines.len(), + want_lines.len(), + "line count differs: {} versus {}", + got_lines.len(), + want_lines.len() + ); + + for (i, (a, b)) in got_lines.iter().zip(want_lines.iter()).enumerate() { + if i == 2 { + // The metrics row: compare every column but the last two. + let ours: Vec<&str> = a.split('\t').collect(); + let theirs: Vec<&str> = b.split('\t').collect(); + assert_eq!(ours.len(), theirs.len(), "column count differs"); + let simulated = ours.len() - 2; + for (col, (x, y)) in ours.iter().zip(theirs.iter()).enumerate() { + if col >= simulated { + continue; + } + assert_eq!(x, y, "column {col} of the metrics row differs"); + } + assert_eq!( + &ours[simulated..], + &["?", "?"], + "the simulated columns must be written as ?" + ); + } else { + assert_eq!(a, b, "line {} differs", i + 1); + } + } +} + +#[test] +fn binary_writes_insert_size_metrics_byte_for_byte() { + let got = std::fs::read_to_string(produced( + "picard/insert_size", + &format!("{SAMPLE}.insert_size_metrics.txt"), + )) + .unwrap(); + let want = std::fs::read_to_string(fixture("test.insert_size_metrics.txt")).unwrap(); + assert_same_lines(&got, &want, "insert size metrics from the binary"); +} + +/// As with the library-level check, the two Monte Carlo columns are the only +/// permitted difference. +#[test] +fn binary_writes_wgs_metrics_bar_the_simulated_columns() { + let got = std::fs::read_to_string(produced( + "picard/wgs_metrics", + &format!("{SAMPLE}.wgs_metrics.txt"), + )) + .unwrap(); + let want = std::fs::read_to_string(fixture("test.wgs_metrics.txt")).unwrap(); + + let got_lines: Vec<&str> = got.lines().collect(); + let want_lines: Vec<&str> = want.lines().collect(); + assert_eq!(got_lines.len(), want_lines.len(), "line count differs"); + for (i, (a, b)) in got_lines.iter().zip(want_lines.iter()).enumerate() { + if i == 2 { + let ours: Vec<&str> = a.split('\t').collect(); + let theirs: Vec<&str> = b.split('\t').collect(); + let simulated = ours.len() - 2; + assert_eq!(&ours[..simulated], &theirs[..simulated], "metrics row"); + } else { + assert_eq!(a, b, "line {} differs", i + 1); + } + } +} + +/// Without a reference there is no way to size the genome territory, so the +/// analysis is skipped rather than reported against a wrong denominator. +#[test] +fn wgs_metrics_are_skipped_without_a_reference() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")); + let outdir = std::env::temp_dir().join("rustqc-dna-noref"); + let _ = std::fs::remove_dir_all(&outdir); + let status = std::process::Command::new(env!("CARGO_BIN_EXE_rustqc")) + .arg("dna") + .arg(root.join("tests/data/dna/test.dna.bam")) + .arg("--outdir") + .arg(&outdir) + .arg("--quiet") + .status() + .unwrap(); + assert!(status.success()); + assert!( + !outdir.join("picard/wgs_metrics").exists(), + "no reference means no WGS metrics" + ); + assert!( + outdir.join("picard/insert_size").exists(), + "insert size needs no reference and must still be written" + ); +} + +// =================================================================== +// Picard CollectGcBiasMetrics +// =================================================================== + +fn gc_bias_result() -> gc_bias::GcBiasResult { + let root = Path::new(env!("CARGO_MANIFEST_DIR")); + let reference: Vec = { + let text = std::fs::read_to_string(root.join("tests/data/dna/genome.fasta")).unwrap(); + text.lines() + .filter(|l| !l.starts_with('>')) + .flat_map(|l| l.bytes()) + .collect() + }; + + let mut accum = GcBiasAccum::new(&reference, gc_bias::DEFAULT_WINDOW_SIZE); + let mut reader = bam::Reader::from_path(root.join("tests/data/dna/test.dna.bam")).unwrap(); + let mut record = bam::Record::new(); + while let Some(result) = reader.read(&mut record) { + result.unwrap(); + accum.process_read(&record, &reference); + } + accum.into_result(gc_bias::DEFAULT_WINDOW_SIZE) +} + +/// The window table and the read assignment are the two rules that black-box +/// inference could not recover, so they are pinned before anything derived +/// from them. +#[test] +fn gc_bias_windows_and_read_starts_match_picard() { + let result = gc_bias_result(); + let windows: u64 = result.rows.iter().map(|r| r.windows).sum(); + let read_starts: u64 = result.rows.iter().map(|r| r.read_starts).sum(); + assert_eq!( + windows, 39_900, + "sliding windows run from position 1 to len - window_size - 1" + ); + assert_eq!( + read_starts, 5_642, + "secondary alignments count towards read starts" + ); + assert_eq!(result.total_clusters, 2_822); + assert_eq!(result.aligned_reads, 5_642); +} + +#[test] +fn gc_bias_detail_metrics_match_picard() { + let path = scratch("test.gc_bias.detail_metrics.txt"); + gc_bias::write_detail_metrics(&gc_bias_result(), &path).unwrap(); + assert_same_lines( + &std::fs::read_to_string(&path).unwrap(), + &std::fs::read_to_string(fixture("test.gc_bias.detail_metrics.txt")).unwrap(), + "GC bias detail metrics", + ); +} + +#[test] +fn gc_bias_summary_metrics_match_picard() { + let path = scratch("test.gc_bias.summary_metrics.txt"); + gc_bias::write_summary_metrics(&gc_bias_result(), &path).unwrap(); + assert_same_lines( + &std::fs::read_to_string(&path).unwrap(), + &std::fs::read_to_string(fixture("test.gc_bias.summary_metrics.txt")).unwrap(), + "GC bias summary metrics", + ); +} + +// =================================================================== +// Picard CollectHsMetrics +// =================================================================== + +/// Read one column out of the fixture's single metrics row. +fn hs_fixture_column(name: &str) -> String { + let text = std::fs::read_to_string(fixture("test.hs_metrics.txt")).unwrap(); + let mut lines = text + .lines() + .filter(|l| !l.starts_with('#') && !l.is_empty()); + let header: Vec<&str> = lines.next().unwrap().split('\t').collect(); + let values: Vec<&str> = lines.next().unwrap().split('\t').collect(); + let index = header + .iter() + .position(|h| *h == name) + .unwrap_or_else(|| panic!("no column named {name}")); + values[index].to_string() +} + +fn hs_result() -> HsMetricsResult { + let root = Path::new(env!("CARGO_MANIFEST_DIR")); + let targets = IntervalSet::from_bed(&root.join("tests/data/dna/targets.bed")).unwrap(); + + let mut reader = bam::Reader::from_path(root.join("tests/data/dna/test.dna.bam")).unwrap(); + let header = reader.header().to_owned(); + let contig = String::from_utf8(header.tid2name(0).to_vec()).unwrap(); + let length = header.target_len(0).unwrap(); + + let mut accum = HsAccum::new(&contig, length, &targets, &targets, 20, 20); + let mut record = bam::Record::new(); + while let Some(result) = reader.read(&mut record) { + result.unwrap(); + accum.process_read(&record); + } + let (counters, depths, target_mask) = accum.into_parts(); + + let target_depths: Vec = depths + .iter() + .zip(target_mask.iter()) + .filter(|(_, on_target)| **on_target) + .map(|(depth, _)| *depth) + .collect(); + + let zero_coverage_targets = targets + .on(&contig) + .iter() + .filter(|interval| (interval.start..interval.end).all(|p| depths[p as usize] == 0)) + .count() as u64; + + let library_size = + hs_metrics::estimate_library_size(counters.selected_pairs, counters.selected_unique_pairs); + + HsMetricsResult { + bait_set: targets.name().to_string(), + bait_territory: targets.territory(), + target_territory: targets.territory(), + genome_size: length, + counters, + target_depths, + zero_coverage_targets, + target_count: targets.len() as u64, + library_size, + } +} + +/// The counters are the part that had to be taken from Picard's source, so +/// each is pinned against the fixture individually. +#[test] +fn hs_counters_match_picard() { + let result = hs_result(); + let c: &HsCounters = &result.counters; + let want = |name: &str| -> u64 { hs_fixture_column(name).parse().unwrap() }; + + assert_eq!(result.bait_territory, want("BAIT_TERRITORY")); + assert_eq!(result.target_territory, want("TARGET_TERRITORY")); + assert_eq!(result.genome_size, want("GENOME_SIZE")); + assert_eq!(c.total_reads, want("TOTAL_READS"), "secondary excluded"); + assert_eq!(c.pf_bases, want("PF_BASES")); + assert_eq!(c.pf_unique_reads, want("PF_UNIQUE_READS")); + assert_eq!(c.pf_uq_reads_aligned, want("PF_UQ_READS_ALIGNED")); + assert_eq!(c.pf_bases_aligned, want("PF_BASES_ALIGNED")); + assert_eq!(c.pf_uq_bases_aligned, want("PF_UQ_BASES_ALIGNED")); + assert_eq!(c.on_bait_bases, want("ON_BAIT_BASES")); + assert_eq!(c.near_bait_bases, want("NEAR_BAIT_BASES")); + assert_eq!(c.off_bait_bases, want("OFF_BAIT_BASES")); + assert_eq!( + c.on_target_bases, + want("ON_TARGET_BASES"), + "overlap clipping runs before the base quality filter" + ); + assert_eq!(result.library_size, Some(want("HS_LIBRARY_SIZE"))); +} + +/// The exclusion fractions are where HsMetrics parts company with +/// CollectWgsMetrics, so they get their own assertions. +#[test] +fn hs_exclusion_fractions_match_picard() { + let result = hs_result(); + let aligned = result.counters.pf_bases_aligned as f64; + let want = |name: &str| -> f64 { hs_fixture_column(name).parse().unwrap() }; + let close = |got: f64, name: &str| { + let expected = want(name); + assert!( + (got - expected).abs() < 1e-6, + "{name}: got {got}, want {expected}" + ); + }; + close( + result.counters.excluded_dupe as f64 / aligned, + "PCT_EXC_DUPE", + ); + close( + result.counters.excluded_overlap as f64 / aligned, + "PCT_EXC_OVERLAP", + ); + close( + result.counters.excluded_baseq as f64 / aligned, + "PCT_EXC_BASEQ", + ); + close( + result.counters.excluded_off_target as f64 / aligned, + "PCT_EXC_OFF_TARGET", + ); +} + +#[test] +fn hs_target_coverage_matches_picard() { + let result = hs_result(); + let want = |name: &str| -> f64 { hs_fixture_column(name).parse().unwrap() }; + assert!( + (result.mean_target_coverage() - want("MEAN_TARGET_COVERAGE")).abs() < 1e-6, + "mean target coverage was {}", + result.mean_target_coverage() + ); + assert!( + (result.mean_bait_coverage() - want("MEAN_BAIT_COVERAGE")).abs() < 1e-6, + "mean bait coverage was {}", + result.mean_bait_coverage() + ); + let (median, min, max) = result.target_coverage_bounds(); + assert_eq!(u64::from(median), want("MEDIAN_TARGET_COVERAGE") as u64); + assert_eq!(u64::from(min), want("MIN_TARGET_COVERAGE") as u64); + assert_eq!(u64::from(max), want("MAX_TARGET_COVERAGE") as u64); + + let fractions = result.target_coverage_fractions(); + for (level, got) in hs_metrics::TARGET_COVERAGE_LEVELS.iter().zip(fractions) { + let expected = want(&format!("PCT_TARGET_BASES_{level}X")); + assert!( + (got - expected).abs() < 1e-6, + "PCT_TARGET_BASES_{level}X: got {got}, want {expected}" + ); + } +} + +/// A second binary run, this time in targeted mode with a reference, so the +/// GC bias and targeted outputs are produced. +fn run_binary_targeted() -> &'static Path { + static OUTDIR: std::sync::OnceLock = std::sync::OnceLock::new(); + OUTDIR.get_or_init(|| { + let root = Path::new(env!("CARGO_MANIFEST_DIR")); + let outdir = std::env::temp_dir().join("rustqc-dna-targeted"); + let _ = std::fs::remove_dir_all(&outdir); + std::fs::create_dir_all(&outdir).unwrap(); + let status = std::process::Command::new(env!("CARGO_BIN_EXE_rustqc")) + .arg("dna") + .arg(root.join("tests/data/dna/test.dna.bam")) + .arg("--reference") + .arg(root.join("tests/data/dna/genome.fasta")) + .arg("--targets") + .arg(root.join("tests/data/dna/targets.bed")) + .arg("--outdir") + .arg(&outdir) + .arg("--quiet") + .status() + .expect("failed to run the rustqc binary"); + assert!(status.success(), "rustqc dna exited with {status}"); + outdir + }) +} + +#[test] +fn binary_writes_gc_bias_metrics_byte_for_byte() { + for suffix in ["gc_bias.detail_metrics", "gc_bias.summary_metrics"] { + let got = std::fs::read_to_string( + run_binary_targeted() + .join("picard/gc_bias") + .join(format!("{SAMPLE}.{suffix}.txt")), + ) + .unwrap(); + let want = std::fs::read_to_string(fixture(&format!("test.{suffix}.txt"))).unwrap(); + assert_same_lines(&got, &want, suffix); + } +} + +/// Every HS metrics column but the seven that need Picard's theoretical +/// sensitivity simulation or its per-target GC dropout, which RustQC does not +/// compute and writes as Picard writes its own uncomputable values. +#[test] +fn binary_writes_hs_metrics_bar_the_simulated_columns() { + let path = run_binary_targeted() + .join("picard/hs_metrics") + .join(format!("{SAMPLE}.hs_metrics.txt")); + let parse = |text: &str| -> std::collections::HashMap { + let mut lines = text + .lines() + .filter(|l| !l.starts_with('#') && !l.trim().is_empty()); + let header: Vec<&str> = lines.next().unwrap().split('\t').collect(); + let values: Vec<&str> = lines.next().unwrap().split('\t').collect(); + header + .iter() + .zip(values.iter()) + .map(|(h, v)| (h.to_string(), v.to_string())) + .collect() + }; + + let ours = parse(&std::fs::read_to_string(&path).unwrap()); + let theirs = parse(&std::fs::read_to_string(fixture("test.hs_metrics.txt")).unwrap()); + + let uncomputed: Vec = [ + "HET_SNP_SENSITIVITY", + "HET_SNP_Q", + "AT_DROPOUT", + "GC_DROPOUT", + "FOLD_80_BASE_PENALTY", + ] + .iter() + .map(|s| s.to_string()) + .chain( + hs_metrics::PENALTY_LEVELS + .iter() + .map(|n| format!("HS_PENALTY_{n}X")), + ) + .collect(); + + let mut compared = 0; + for (column, want) in &theirs { + if uncomputed.contains(column) { + continue; + } + compared += 1; + let got = ours + .get(column) + .unwrap_or_else(|| panic!("we do not emit column {column}")); + assert_eq!(got, want, "column {column} differs"); + } + assert!( + compared >= 55, + "expected to compare most of the columns, only did {compared}" + ); +} + +/// Targeted outputs appear only when targets are given. +#[test] +fn hs_metrics_are_absent_without_targets() { + assert!( + !run_binary().join("picard/hs_metrics").exists(), + "no targets means no targeted metrics" + ); + assert!( + run_binary_targeted().join("picard/hs_metrics").exists(), + "targets must produce them" + ); +} + +// =================================================================== +// Qualimap bamqc +// =================================================================== + +fn qualimap_result() -> ContigQualimap { + let root = Path::new(env!("CARGO_MANIFEST_DIR")); + let mut reader = bam::Reader::from_path(root.join("tests/data/dna/test.dna.bam")).unwrap(); + let header = reader.header().to_owned(); + let contig = String::from_utf8(header.tid2name(0).to_vec()).unwrap(); + let length = header.target_len(0).unwrap(); + + let mut accum = QualimapAccum::new(&contig, length, qualimap::DEFAULT_NUM_WINDOWS); + let mut record = bam::Record::new(); + while let Some(result) = reader.read(&mut record) { + result.unwrap(); + accum.process_read(&record); + } + accum.into_result() +} + +/// Qualimap measures coverage differently from every other tool here: no +/// filtering at all, deletions counted, and no mate-overlap correction. The +/// figures are pinned so that difference stays deliberate. +#[test] +fn qualimap_globals_match() { + let r = qualimap_result(); + let c = &r.counters; + assert_eq!(r.midpoints.len(), 397, "window count"); + assert_eq!(c.reads, 5642, "secondary alignments are counted separately"); + assert_eq!(c.secondary, 2); + assert_eq!(c.mapped, 5640); + assert_eq!(c.duplicates, 1656); + assert_eq!(c.paired_first, 2820); + assert_eq!(c.paired_second, 2820); + assert_eq!(c.paired_both, 5640); + assert_eq!(c.singletons, 0); + assert_eq!(c.sequenced_bases, 670_989); + assert_eq!(c.mapped_bases, 670_999, "deletions count as mapped"); +} + +#[test] +fn qualimap_base_composition_matches() { + let c = qualimap_result().counters; + // A, C, G, T, N in reference orientation. + assert_eq!(c.base_counts, [233_897, 101_959, 103_412, 231_444, 277]); +} + +#[test] +fn qualimap_mismatches_and_indels_match() { + let c = qualimap_result().counters; + assert_eq!( + c.mismatches(), + 1350, + "NM less insertions, not less deletions" + ); + assert_eq!(c.insertions, 2); + assert_eq!(c.deletions, 10); + assert_eq!(c.reads_with_insertion, 2); + assert_eq!(c.reads_with_deletion, 10); + let rate = c.general_error_rate(); + assert!((rate - 0.002).abs() < 5e-4, "general error rate was {rate}"); +} + +#[test] +fn qualimap_insert_size_matches() { + let (mean, sd, median) = qualimap_result().counters.insert_size_stats(); + assert!((mean - 125.6844).abs() < 1e-4, "mean was {mean}"); + assert!((sd - 32.4421).abs() < 1e-4, "sd was {sd}"); + assert_eq!(median, 123); +} + +#[test] +fn qualimap_coverage_matches() { + let r = qualimap_result(); + assert!( + (r.mean_coverage() - 16.7746).abs() < 1e-4, + "mean coverage was {}", + r.mean_coverage() + ); + assert_eq!(r.coverage_histogram.get(&0), Some(&38_820)); + assert_eq!(r.coverage_histogram.get(&1), Some(&40)); + let fraction = r.genome_fraction(); + assert!( + (fraction[0].1 - 2.9524261893452746).abs() < 1e-9, + "1X fraction was {}", + fraction[0].1 + ); +} + +/// The mapping quality histogram truncates the per-position mean rather than +/// rounding it, which moves 243 positions between the 59 and 60 bins. +#[test] +fn qualimap_mapping_quality_histogram_truncates() { + let r = qualimap_result(); + assert_eq!(r.mapq_histogram.get(&59), Some(&248)); + assert_eq!(r.mapq_histogram.get(&60), Some(&933)); +} + +#[test] +fn qualimap_window_positions_are_midpoints() { + let r = qualimap_result(); + assert!((r.midpoints[0] - 51.0).abs() < 1e-9); + assert!((r.midpoints[1] - 152.0).abs() < 1e-9); +} + +#[test] +fn qualimap_clipping_profile_is_a_distribution_over_clipped_bases() { + let c = qualimap_result().counters; + assert_eq!(c.clipped_bases, 863, "the profile's denominator"); + let first = 100.0 * c.clipping_by_position[0] as f64 / c.clipped_bases as f64; + assert!( + (first - 1.8539976825028968).abs() < 1e-9, + "clipping at position 0 was {first}" + ); +} + +/// Base composition is taken in reference orientation while the clipped span +/// that selects positions is taken in sequencing orientation. Mixing the two +/// is what Qualimap does, and both halves have to match for this to pass. +#[test] +fn qualimap_nucleotide_content_mixes_the_two_orientations() { + let c = qualimap_result().counters; + let first = c.nucleotide_by_position[0]; + let total: u64 = first.iter().sum(); + assert_eq!(total, 5624, "clipped positions are excluded"); + let pct = |i: usize| 100.0 * first[i] as f64 / total as f64; + assert!( + (pct(0) - 36.575391180654336).abs() < 1e-9, + "A was {}", + pct(0) + ); + assert!( + (pct(1) - 12.820056899004268).abs() < 1e-9, + "C was {}", + pct(1) + ); + assert!( + (pct(2) - 18.509957325746797).abs() < 1e-9, + "G was {}", + pct(2) + ); + assert!( + (pct(3) - 32.059032716927454).abs() < 1e-9, + "T was {}", + pct(3) + ); + assert!( + (pct(4) - 0.03556187766714083).abs() < 1e-9, + "N was {}", + pct(4) + ); +} + +/// The whole `genome_results.txt`, minus the two lines that record the +/// absolute paths the run used. +/// +/// Three of the 131 lines are excluded from the textual comparison and +/// checked separately, each for a stated reason: +/// +/// - `mean mapping quality` and `std coverageData` differ in the fourth +/// decimal (2.4179 against 2.4178, 154.9340 against 154.9323). Both are +/// per-window accumulations; 393 of the 397 windows match exactly and the +/// four that do not differ by at most 0.053. They are asserted numerically +/// with a tolerance. +/// - `homopolymer indels` differs outright. Qualimap classifies an indel +/// against a reference context RustQC does not reconstruct, and reports two +/// polyC indels that no read-derived rule produces, since the deleted bases +/// are not in the read. That line is asserted only to be present and +/// well-formed. +#[test] +fn qualimap_genome_results_match() { + let path = scratch("genome_results.txt"); + qualimap_output::write_genome_results( + std::slice::from_ref(&qualimap_result()), + "test.dna.bam", + &path, + ) + .unwrap(); + let strip = |s: &str| { + s.lines() + .filter(|l| !l.contains("bam file =") && !l.contains("outfile =")) + .collect::>() + .join("\n") + }; + let got = strip(&std::fs::read_to_string(&path).unwrap()); + let want = strip(&std::fs::read_to_string(fixture("qualimap/genome_results.txt")).unwrap()); + + let number = |text: &str, key: &str| -> f64 { + text.lines() + .find(|l| l.contains(key)) + .and_then(|l| l.split('=').nth(1)) + .map(|v| v.trim().trim_end_matches('X').parse().unwrap()) + .unwrap_or_else(|| panic!("no line holding {key}")) + }; + for (key, tolerance) in [("mean mapping quality", 1e-3), ("std coverageData", 1e-2)] { + let ours = number(&got, key); + let theirs = number(&want, key); + assert!( + (ours - theirs).abs() < tolerance, + "{key}: got {ours}, want {theirs}" + ); + } + + let homopolymer = got + .lines() + .find(|l| l.contains("homopolymer indels")) + .expect("the homopolymer line must still be written"); + assert!( + homopolymer.trim_end().ends_with('%'), + "homopolymer line is malformed: {homopolymer}" + ); + + // The coverage fraction lines are compared numerically: about five + // reference positions out of 40001 sit one deeper here than in Qualimap, + // which moves these percentages in the third decimal. + let fractions = |text: &str| -> Vec { + text.lines() + .filter(|l| l.contains("of reference with a coverageData")) + .map(|l| { + l.split("There is a") + .nth(1) + .and_then(|r| r.split('%').next()) + .unwrap() + .trim() + .parse() + .unwrap() + }) + .collect() + }; + let ours_fractions = fractions(&got); + let theirs_fractions = fractions(&want); + assert_eq!( + ours_fractions.len(), + theirs_fractions.len(), + "fraction lines" + ); + for (i, (a, b)) in ours_fractions.iter().zip(&theirs_fractions).enumerate() { + assert!( + (a - b).abs() < 0.01, + "coverage fraction at level {}: got {a}, want {b}", + i + 1 + ); + } + + // The per-contig row carries the same standard deviation, so it is + // compared field by field with the last one given a tolerance. + let contig_row = |text: &str| -> Vec { + text.lines() + .find(|l| l.starts_with('\t')) + .map(|l| l.trim().split('\t').map(str::to_string).collect()) + .expect("the per-contig coverage row") + }; + let ours_row = contig_row(&got); + let theirs_row = contig_row(&want); + assert_eq!( + ours_row[..4], + theirs_row[..4], + "per-contig name, length, bases and mean" + ); + let ours_sd: f64 = ours_row[4].parse().unwrap(); + let theirs_sd: f64 = theirs_row[4].parse().unwrap(); + assert!( + (ours_sd - theirs_sd).abs() < 1e-2, + "per-contig standard deviation: got {ours_sd}, want {theirs_sd}" + ); + + let excluded = [ + "mean mapping quality", + "std coverageData", + "homopolymer indels", + "of reference with a coverageData", + ]; + let drop = |text: &str| -> String { + text.lines() + .filter(|l| !l.starts_with('\t') && !excluded.iter().any(|k| l.contains(k))) + .collect::>() + .join("\n") + }; + assert_same_lines(&drop(&got), &drop(&want), "genome_results.txt"); +} + +/// The raw data tables, compared as numbers rather than as text. +/// +/// Three match byte for byte. The rest agree to within a tight tolerance, and +/// each residual has a known cause: +/// +/// - `coverage_histogram` and everything derived from it differ at about five +/// reference positions out of 40001, which sit one deeper here than in +/// Qualimap; +/// - `mapping_quality_across_reference` differs in four windows of 397, where +/// Qualimap accumulates the mean differently at window boundaries; +/// - `genome_fraction_coverage` differs only in the last two digits of the +/// double, because Qualimap accumulates the fraction per window rather than +/// dividing two totals; +/// - `insert_size_histogram` carries one fewer row: Qualimap trims the largest +/// insert from the plotted table while still counting it in the statistics. +#[test] +fn qualimap_raw_data_tables_match() { + let dir = run_binary_targeted().join("qualimap/raw_data_qualimapReport"); + + let numbers = |path: &Path| -> Vec> { + std::fs::read_to_string(path) + .unwrap_or_else(|e| panic!("reading {}: {e}", path.display())) + .lines() + .filter(|l| !l.starts_with('#')) + .map(|l| { + l.split('\t') + .map(|v| v.trim().parse::().unwrap_or(f64::NAN)) + .collect() + }) + .collect() + }; + + // Tables that reproduce exactly. + for name in [ + "mapped_reads_clipping_profile.txt", + "mapped_reads_nucleotide_content.txt", + "mapping_quality_histogram.txt", + ] { + let got = std::fs::read_to_string(dir.join(name)).unwrap(); + let want = + std::fs::read_to_string(fixture(&format!("qualimap/raw_data_qualimapReport/{name}"))) + .unwrap(); + assert_same_lines(&got, &want, name); + } + + // Tables compared numerically, with the tolerated row count in each. + for (name, tolerance, max_differing_rows) in [ + ("coverage_across_reference.txt", 1e-6, 20usize), + ("coverage_histogram.txt", 1.5, 10), + ("genome_fraction_coverage.txt", 1e-6, 52), + ("insert_size_across_reference.txt", 1e-6, 5), + ("mapping_quality_across_reference.txt", 0.1, 5), + ] { + let ours = numbers(&dir.join(name)); + let theirs = numbers(&fixture(&format!( + "qualimap/raw_data_qualimapReport/{name}" + ))); + assert_eq!(ours.len(), theirs.len(), "{name}: row count"); + + let mut differing = 0; + for (row, (a, b)) in ours.iter().zip(&theirs).enumerate() { + assert_eq!(a.len(), b.len(), "{name}: row {row} column count"); + if a.iter().zip(b).any(|(x, y)| (x - y).abs() > tolerance) { + differing += 1; + assert!( + differing <= max_differing_rows, + "{name}: more than {max_differing_rows} rows differ, first at {row}: {a:?} against {b:?}" + ); + } + } + } + + // The insert size histogram is the one table with a different row count. + let ours = numbers(&dir.join("insert_size_histogram.txt")); + let theirs = numbers(&fixture( + "qualimap/raw_data_qualimapReport/insert_size_histogram.txt", + )); + assert!( + ours.len() == theirs.len() + 1, + "expected exactly one extra row, got {} against {}", + ours.len(), + theirs.len() + ); + for (a, b) in ours.iter().zip(&theirs) { + assert_eq!(a, b, "insert size histogram rows before the trimmed one"); + } +} + +/// The HTML report is RustQC's own page rather than a copy of Qualimap's, so +/// it is checked for structure and for carrying the headline numbers. +#[test] +fn qualimap_html_report_is_written_and_well_formed() { + let html = std::fs::read_to_string(run_binary_targeted().join("qualimap/qualimapReport.html")) + .unwrap(); + assert!(html.starts_with(""), "missing doctype"); + assert!(html.trim_end().ends_with(""), "unclosed document"); + assert!(html.contains("BamQC report"), "missing the title"); + assert!(html.contains("40,001"), "missing the reference length"); + assert!(html.contains("5,642"), "missing the read count"); + assert!(html.contains("16.7746X"), "missing the mean coverage"); +} + +// =================================================================== +// RSeQC read_GC +// =================================================================== + +/// RSeQC's `read_GC.py` distribution, against its own output. +/// +/// The fixture is the DNA alignment rather than the RNA one: the RNA test BAM +/// carries synthetic all-A sequences, so every read there is 0 percent GC and +/// the histogram collapses to a single row that would prove nothing. +#[test] +fn read_gc_distribution_matches_rseqc() { + use rustqc::rna::rseqc::read_gc::ReadGcAccum; + + let root = Path::new(env!("CARGO_MANIFEST_DIR")); + let mut reader = bam::Reader::from_path(root.join("tests/data/dna/test.dna.bam")).unwrap(); + let mut accum = ReadGcAccum::new(); + let mut record = bam::Record::new(); + while let Some(result) = reader.read(&mut record) { + result.unwrap(); + accum.process_read(&record, 30); + } + + let want: std::collections::BTreeMap = + std::fs::read_to_string(root.join("tests/expected/rseqc/read_gc.GC.xls")) + .unwrap() + .lines() + .skip(1) + .filter_map(|l| l.split_once('\t')) + .map(|(percent, count)| (percent.to_string(), count.parse().unwrap())) + .collect(); + + let got: std::collections::BTreeMap = accum + .distribution() + .into_iter() + .map(|(percent, count)| (format!("{percent:.2}"), count)) + .collect(); + + assert_eq!( + got.len(), + want.len(), + "distinct GC values: got {}, want {}", + got.len(), + want.len() + ); + for (percent, count) in &want { + assert_eq!( + got.get(percent), + Some(count), + "GC {percent}%: got {:?}, want {count}", + got.get(percent) + ); + } + + let total: u64 = want.values().sum(); + assert_eq!(accum.counted(), total, "every read must land in a bin"); + assert_eq!(accum.reads, 5642, "every mapped read counts"); +} diff --git a/tests/expected/dna/VERSIONS.txt b/tests/expected/dna/VERSIONS.txt new file mode 100644 index 00000000..599b49d3 --- /dev/null +++ b/tests/expected/dna/VERSIONS.txt @@ -0,0 +1,4 @@ +mosdepth 0.3.14 +samtools 1.24 +picard 3.4.0 +qualimap 2.3 diff --git a/tests/expected/dna/qualimap/genome_results.txt b/tests/expected/dna/qualimap/genome_results.txt new file mode 100644 index 00000000..7bff7717 --- /dev/null +++ b/tests/expected/dna/qualimap/genome_results.txt @@ -0,0 +1,129 @@ +BamQC report +----------------------------------- + +>>>>>>> Input + + + +>>>>>>> Reference + + number of bases = 40,001 bp + number of contigs = 1 + + +>>>>>>> Globals + + number of windows = 397 + + number of reads = 5,642 + number of mapped reads = 5,640 (99.96%) + number of secondary alignments = 2 + + number of mapped paired reads (first in pair) = 2,820 + number of mapped paired reads (second in pair) = 2,820 + number of mapped paired reads (both in pair) = 5,640 + number of mapped paired reads (singletons) = 0 + + number of mapped bases = 670,999 bp + number of sequenced bases = 670,989 bp + number of aligned bases = 0 bp + number of duplicated reads (flagged) = 1,656 + + +>>>>>>> Insert size + + mean insert size = 125.6844 + std insert size = 32.4421 + median insert size = 123 + + +>>>>>>> Mapping quality + + mean mapping quality = 2.4178 + + +>>>>>>> ACTG content + + number of A's = 233,897 bp (34.86%) + number of C's = 101,959 bp (15.2%) + number of T's = 231,444 bp (34.49%) + number of G's = 103,412 bp (15.41%) + number of N's = 277 bp (0.04%) + + GC percentage = 30.61% + + +>>>>>>> Mismatches and indels + + general error rate = 0.002 + number of mismatches = 1,350 + number of insertions = 2 + mapped reads with insertion percentage = 0.04% + number of deletions = 10 + mapped reads with deletion percentage = 0.18% + homopolymer indels = 58.33% + + +>>>>>>> Coverage + + mean coverageData = 16.7746X + std coverageData = 154.9323X + + There is a 2.95% of reference with a coverageData >= 1X + There is a 2.85% of reference with a coverageData >= 2X + There is a 2.64% of reference with a coverageData >= 3X + There is a 2.56% of reference with a coverageData >= 4X + There is a 2.53% of reference with a coverageData >= 5X + There is a 2.5% of reference with a coverageData >= 6X + There is a 2.5% of reference with a coverageData >= 7X + There is a 2.47% of reference with a coverageData >= 8X + There is a 2.45% of reference with a coverageData >= 9X + There is a 2.43% of reference with a coverageData >= 10X + There is a 2.4% of reference with a coverageData >= 11X + There is a 2.4% of reference with a coverageData >= 12X + There is a 2.39% of reference with a coverageData >= 13X + There is a 2.39% of reference with a coverageData >= 14X + There is a 2.38% of reference with a coverageData >= 15X + There is a 2.37% of reference with a coverageData >= 16X + There is a 2.35% of reference with a coverageData >= 17X + There is a 2.35% of reference with a coverageData >= 18X + There is a 2.35% of reference with a coverageData >= 19X + There is a 2.34% of reference with a coverageData >= 20X + There is a 2.33% of reference with a coverageData >= 21X + There is a 2.3% of reference with a coverageData >= 22X + There is a 2.27% of reference with a coverageData >= 23X + There is a 2.27% of reference with a coverageData >= 24X + There is a 2.25% of reference with a coverageData >= 25X + There is a 2.25% of reference with a coverageData >= 26X + There is a 2.01% of reference with a coverageData >= 27X + There is a 2.01% of reference with a coverageData >= 28X + There is a 2.01% of reference with a coverageData >= 29X + There is a 2.01% of reference with a coverageData >= 30X + There is a 2% of reference with a coverageData >= 31X + There is a 2% of reference with a coverageData >= 32X + There is a 2% of reference with a coverageData >= 33X + There is a 2% of reference with a coverageData >= 34X + There is a 2% of reference with a coverageData >= 35X + There is a 2% of reference with a coverageData >= 36X + There is a 2% of reference with a coverageData >= 37X + There is a 1.99% of reference with a coverageData >= 38X + There is a 1.99% of reference with a coverageData >= 39X + There is a 1.99% of reference with a coverageData >= 40X + There is a 1.99% of reference with a coverageData >= 41X + There is a 1.99% of reference with a coverageData >= 42X + There is a 1.99% of reference with a coverageData >= 43X + There is a 1.98% of reference with a coverageData >= 44X + There is a 1.98% of reference with a coverageData >= 45X + There is a 1.98% of reference with a coverageData >= 46X + There is a 1.97% of reference with a coverageData >= 47X + There is a 1.97% of reference with a coverageData >= 48X + There is a 1.97% of reference with a coverageData >= 49X + There is a 1.97% of reference with a coverageData >= 50X + There is a 1.96% of reference with a coverageData >= 51X + + +>>>>>>> Coverage per contig + + chr22 40001 670999 16.774555636109096 154.9323026692165 + + diff --git a/tests/expected/dna/qualimap/raw_data_qualimapReport/coverage_across_reference.txt b/tests/expected/dna/qualimap/raw_data_qualimapReport/coverage_across_reference.txt new file mode 100644 index 00000000..fba351b4 --- /dev/null +++ b/tests/expected/dna/qualimap/raw_data_qualimapReport/coverage_across_reference.txt @@ -0,0 +1,398 @@ +#Position (bp) Coverage Std +51.0 0.0 0.0 +152.0 0.0 0.0 +253.0 0.0 0.0 +354.0 0.0 0.0 +455.0 0.0 0.0 +556.0 0.0 0.0 +657.0 0.0 0.0 +758.0 0.0 0.0 +859.0 0.0 0.0 +960.0 0.0 0.0 +1061.0 0.0 0.0 +1162.0 0.0 0.0 +1263.0 0.0 0.0 +1364.0 0.0 0.0 +1465.0 0.0 0.0 +1566.0 0.0 0.0 +1667.0 0.0 0.0 +1768.0 0.0 0.0 +1869.0 0.0 0.0 +1970.0 227.46534653465346 256.9118776376194 +2071.0 606.3366336633663 264.0358511510687 +2172.0 0.2376237623762376 0.9227595290837431 +2273.0 0.0 0.0 +2374.0 0.0 0.0 +2475.0 0.0 0.0 +2576.0 0.0 0.0 +2677.0 2.712871287128713 12.447115853454042 +2778.0 321.7227722772277 71.30127128690422 +2879.0 55.04950495049505 80.90717086284566 +2980.0 1118.5544554455446 498.66584378958237 +3081.0 1075.930693069307 557.8362902614648 +3182.0 8.782178217821782 10.598430670305266 +3283.0 22.455445544554454 8.687079286771445 +3384.0 39.37623762376238 98.50043976380698 +3485.0 1870.4554455445545 652.9420698152736 +3586.0 1218.6039603960396 745.6865864077984 +3687.0 5.108910891089109 16.80140258171535 +3788.0 0.0 0.0 +3889.0 0.0 0.0 +3990.0 0.0 0.0 +4091.0 0.0 0.0 +4192.0 0.0 0.0 +4293.0 0.0 0.0 +4394.0 0.0 0.0 +4495.0 34.613861386138616 39.22062500448679 +4596.0 36.148514851485146 37.67277032237444 +4697.0 0.0 0.0 +4798.0 0.0 0.0 +4899.0 0.0 0.0 +5000.0 0.0 0.0 +5101.0 0.0 0.0 +5202.0 0.0 0.0 +5303.0 0.0 0.0 +5404.0 0.0 0.0 +5505.0 0.0 0.0 +5606.0 0.0 0.0 +5707.0 0.0 0.0 +5808.0 0.0 0.0 +5909.0 0.0 0.0 +6010.0 0.0 0.0 +6111.0 0.0 0.0 +6212.0 0.0 0.0 +6313.0 0.0 0.0 +6414.0 0.0 0.0 +6515.0 0.0 0.0 +6616.0 0.0 0.0 +6717.0 0.0 0.0 +6818.0 0.0 0.0 +6919.0 0.0 0.0 +7020.0 0.0 0.0 +7121.0 0.0 0.0 +7222.0 0.0 0.0 +7323.0 0.0 0.0 +7424.0 0.0 0.0 +7525.0 0.0 0.0 +7626.0 0.0 0.0 +7727.0 0.0 0.0 +7828.0 0.0 0.0 +7929.0 0.0 0.0 +8030.0 0.0 0.0 +8131.0 0.0 0.0 +8232.0 0.0 0.0 +8333.0 0.0 0.0 +8434.0 0.0 0.0 +8535.0 0.0 0.0 +8636.0 0.0 0.0 +8737.0 0.0 0.0 +8838.0 0.0 0.0 +8939.0 0.0 0.0 +9040.0 0.0 0.0 +9141.0 0.0 0.0 +9242.0 0.0 0.0 +9343.0 0.0 0.0 +9444.0 0.0 0.0 +9545.0 0.0 0.0 +9646.0 0.0 0.0 +9747.0 0.0 0.0 +9848.0 0.0 0.0 +9949.0 0.0 0.0 +10050.0 0.0 0.0 +10151.0 0.0 0.0 +10252.0 0.0 0.0 +10353.0 0.0 0.0 +10454.0 0.0 0.0 +10555.0 0.0 0.0 +10656.0 0.0 0.0 +10757.0 0.0 0.0 +10858.0 0.0 0.0 +10959.0 0.0 0.0 +11060.0 0.0 0.0 +11161.0 0.0 0.0 +11262.0 0.0 0.0 +11363.0 0.0 0.0 +11464.0 0.0 0.0 +11565.0 0.0 0.0 +11666.0 0.0 0.0 +11767.0 0.0 0.0 +11868.0 0.0 0.0 +11969.0 0.0 0.0 +12070.0 0.0 0.0 +12171.0 0.0 0.0 +12272.0 0.0 0.0 +12373.0 0.0 0.0 +12474.0 0.0 0.0 +12575.0 0.0 0.0 +12676.0 0.0 0.0 +12777.0 0.0 0.0 +12878.0 0.0 0.0 +12979.0 0.0 0.0 +13080.0 0.0 0.0 +13181.0 0.0 0.0 +13282.0 0.0 0.0 +13383.0 0.0 0.0 +13484.0 0.0 0.0 +13585.0 0.0 0.0 +13686.0 0.0 0.0 +13787.0 0.0 0.0 +13888.0 0.0 0.0 +13989.0 0.0 0.0 +14090.0 0.0 0.0 +14191.0 0.0 0.0 +14292.0 0.0 0.0 +14393.0 0.0 0.0 +14494.0 0.0 0.0 +14595.0 0.0 0.0 +14696.0 0.0 0.0 +14797.0 0.0 0.0 +14898.0 0.0 0.0 +14999.0 0.0 0.0 +15100.0 0.0 0.0 +15201.0 0.0 0.0 +15302.0 0.0 0.0 +15403.0 0.0 0.0 +15504.0 0.0 0.0 +15605.0 0.0 0.0 +15706.0 0.0 0.0 +15807.0 0.0 0.0 +15908.0 0.0 0.0 +16009.0 0.0 0.0 +16110.0 0.0 0.0 +16211.0 0.0 0.0 +16312.0 0.0 0.0 +16413.0 0.0 0.0 +16514.0 0.0 0.0 +16615.0 0.0 0.0 +16716.0 0.0 0.0 +16817.0 0.0 0.0 +16918.0 0.0 0.0 +17019.0 0.0 0.0 +17120.0 0.0 0.0 +17221.0 0.0 0.0 +17322.0 0.0 0.0 +17423.0 0.0 0.0 +17524.0 0.0 0.0 +17625.0 0.0 0.0 +17726.0 0.0 0.0 +17827.0 0.0 0.0 +17928.0 0.0 0.0 +18029.0 0.0 0.0 +18130.0 0.0 0.0 +18231.0 0.0 0.0 +18332.0 0.0 0.0 +18433.0 0.0 0.0 +18534.0 0.0 0.0 +18635.0 0.0 0.0 +18736.0 0.0 0.0 +18837.0 0.0 0.0 +18938.0 0.0 0.0 +19039.0 0.0 0.0 +19140.0 0.0 0.0 +19241.0 0.0 0.0 +19342.0 0.0 0.0 +19443.0 0.0 0.0 +19544.0 0.0 0.0 +19645.0 0.0 0.0 +19746.0 0.0 0.0 +19847.0 0.0 0.0 +19948.0 0.0 0.0 +20049.0 0.0 0.0 +20150.0 0.0 0.0 +20251.0 0.0 0.0 +20352.0 0.0 0.0 +20453.0 0.0 0.0 +20554.0 0.0 0.0 +20655.0 0.0 0.0 +20756.0 0.0 0.0 +20857.0 0.0 0.0 +20958.0 0.0 0.0 +21059.0 0.0 0.0 +21160.0 0.0 0.0 +21261.0 0.0 0.0 +21362.0 0.0 0.0 +21463.0 0.0 0.0 +21564.0 0.0 0.0 +21665.0 0.0 0.0 +21766.0 0.0 0.0 +21867.0 0.0 0.0 +21968.0 0.0 0.0 +22069.0 0.0 0.0 +22170.0 0.0 0.0 +22271.0 0.0 0.0 +22372.0 0.0 0.0 +22473.0 0.0 0.0 +22574.0 0.0 0.0 +22675.0 0.0 0.0 +22776.0 0.0 0.0 +22877.0 0.0 0.0 +22978.0 0.0 0.0 +23079.0 0.0 0.0 +23180.0 0.0 0.0 +23281.0 0.0 0.0 +23382.0 0.0 0.0 +23483.0 0.0 0.0 +23584.0 0.0 0.0 +23685.0 0.0 0.0 +23786.0 0.0 0.0 +23887.0 0.0 0.0 +23988.0 0.0 0.0 +24089.0 0.0 0.0 +24190.0 0.0 0.0 +24291.0 0.0 0.0 +24392.0 0.0 0.0 +24493.0 0.0 0.0 +24594.0 0.0 0.0 +24695.0 0.0 0.0 +24796.0 0.0 0.0 +24897.0 0.0 0.0 +24998.0 0.0 0.0 +25099.0 0.0 0.0 +25200.0 0.0 0.0 +25301.0 0.0 0.0 +25402.0 0.0 0.0 +25503.0 0.0 0.0 +25604.0 0.0 0.0 +25705.0 0.0 0.0 +25806.0 0.0 0.0 +25907.0 0.0 0.0 +26008.0 0.0 0.0 +26109.0 0.0 0.0 +26210.0 0.0 0.0 +26311.0 0.0 0.0 +26412.0 0.0 0.0 +26513.0 0.0 0.0 +26614.0 0.0 0.0 +26715.0 0.0 0.0 +26816.0 0.0 0.0 +26917.0 0.0 0.0 +27018.0 0.0 0.0 +27119.0 0.0 0.0 +27220.0 0.0 0.0 +27321.0 0.0 0.0 +27422.0 0.0 0.0 +27523.0 0.0 0.0 +27624.0 0.0 0.0 +27725.0 0.0 0.0 +27826.0 0.0 0.0 +27927.0 0.0 0.0 +28028.0 0.0 0.0 +28129.0 0.0 0.0 +28230.0 0.0 0.0 +28331.0 0.0 0.0 +28432.0 0.0 0.0 +28533.0 0.0 0.0 +28634.0 0.0 0.0 +28735.0 0.0 0.0 +28836.0 0.0 0.0 +28937.0 0.0 0.0 +29038.0 0.0 0.0 +29139.0 0.0 0.0 +29240.0 0.0 0.0 +29341.0 0.0 0.0 +29442.0 0.0 0.0 +29543.0 0.0 0.0 +29644.0 0.0 0.0 +29745.0 0.0 0.0 +29846.0 0.0 0.0 +29947.0 0.0 0.0 +30048.0 0.0 0.0 +30149.0 0.0 0.0 +30250.0 0.0 0.0 +30351.0 0.0 0.0 +30452.0 0.0 0.0 +30553.0 0.0 0.0 +30654.0 0.0 0.0 +30755.0 0.0 0.0 +30856.0 0.0 0.0 +30957.0 0.0 0.0 +31058.0 0.0 0.0 +31159.0 0.0 0.0 +31260.0 0.0 0.0 +31361.0 0.0 0.0 +31462.0 0.0 0.0 +31563.0 0.0 0.0 +31664.0 0.0 0.0 +31765.0 0.0 0.0 +31866.0 0.0 0.0 +31967.0 0.0 0.0 +32068.0 0.0 0.0 +32169.0 0.0 0.0 +32270.0 0.0 0.0 +32371.0 0.0 0.0 +32472.0 0.0 0.0 +32573.0 0.0 0.0 +32674.0 0.0 0.0 +32775.0 0.0 0.0 +32876.0 0.0 0.0 +32977.0 0.0 0.0 +33078.0 0.0 0.0 +33179.0 0.0 0.0 +33280.0 0.0 0.0 +33381.0 0.0 0.0 +33482.0 0.0 0.0 +33583.0 0.0 0.0 +33684.0 0.0 0.0 +33785.0 0.0 0.0 +33886.0 0.0 0.0 +33987.0 0.0 0.0 +34088.0 0.0 0.0 +34189.0 0.0 0.0 +34290.0 0.0 0.0 +34391.0 0.0 0.0 +34492.0 0.0 0.0 +34593.0 0.0 0.0 +34694.0 0.0 0.0 +34795.0 0.0 0.0 +34896.0 0.0 0.0 +34997.0 0.0 0.0 +35098.0 0.0 0.0 +35199.0 0.0 0.0 +35300.0 0.0 0.0 +35401.0 0.0 0.0 +35502.0 0.0 0.0 +35603.0 0.0 0.0 +35704.0 0.0 0.0 +35805.0 0.0 0.0 +35906.0 0.0 0.0 +36007.0 0.0 0.0 +36108.0 0.0 0.0 +36209.0 0.0 0.0 +36310.0 0.0 0.0 +36411.0 0.0 0.0 +36512.0 0.0 0.0 +36613.0 0.0 0.0 +36714.0 0.0 0.0 +36815.0 0.0 0.0 +36916.0 0.0 0.0 +37017.0 0.0 0.0 +37118.0 0.0 0.0 +37219.0 0.0 0.0 +37320.0 0.0 0.0 +37421.0 0.0 0.0 +37522.0 0.0 0.0 +37623.0 0.0 0.0 +37724.0 0.0 0.0 +37825.0 0.0 0.0 +37926.0 0.0 0.0 +38027.0 0.0 0.0 +38128.0 0.0 0.0 +38229.0 0.0 0.0 +38330.0 0.0 0.0 +38431.0 0.0 0.0 +38532.0 0.0 0.0 +38633.0 0.0 0.0 +38734.0 0.0 0.0 +38835.0 0.0 0.0 +38936.0 0.0 0.0 +39037.0 0.0 0.0 +39138.0 0.0 0.0 +39239.0 0.0 0.0 +39340.0 0.0 0.0 +39441.0 0.0 0.0 +39542.0 0.0 0.0 +39643.0 0.0 0.0 +39744.0 0.0 0.0 +39845.0 0.0 0.0 +39946.0 0.0 0.0 +39999.0 0.0 0.0 diff --git a/tests/expected/dna/qualimap/raw_data_qualimapReport/coverage_histogram.txt b/tests/expected/dna/qualimap/raw_data_qualimapReport/coverage_histogram.txt new file mode 100644 index 00000000..6ef768dd --- /dev/null +++ b/tests/expected/dna/qualimap/raw_data_qualimapReport/coverage_histogram.txt @@ -0,0 +1,590 @@ +#Coverage Number of genomic locations +0.0 38820.0 +1.0 40.0 +2.0 83.0 +3.0 32.0 +4.0 14.0 +5.0 10.0 +6.0 1.0 +7.0 12.0 +8.0 8.0 +9.0 9.0 +10.0 10.0 +11.0 1.0 +12.0 5.0 +13.0 1.0 +14.0 4.0 +15.0 1.0 +16.0 9.0 +17.0 1.0 +18.0 1.0 +19.0 2.0 +20.0 5.0 +21.0 13.0 +22.0 9.0 +23.0 2.0 +24.0 6.0 +25.0 1.0 +26.0 98.0 +30.0 2.0 +32.0 1.0 +36.0 1.0 +37.0 1.0 +40.0 2.0 +41.0 1.0 +43.0 2.0 +45.0 2.0 +46.0 1.0 +48.0 1.0 +50.0 5.0 +52.0 5.0 +54.0 3.0 +55.0 1.0 +56.0 2.0 +57.0 1.0 +58.0 2.0 +59.0 1.0 +60.0 1.0 +63.0 1.0 +64.0 1.0 +66.0 5.0 +68.0 1.0 +70.0 1.0 +71.0 1.0 +72.0 3.0 +73.0 1.0 +74.0 7.0 +78.0 6.0 +80.0 6.0 +81.0 1.0 +82.0 7.0 +83.0 1.0 +84.0 2.0 +85.0 1.0 +86.0 4.0 +87.0 1.0 +88.0 23.0 +90.0 7.0 +92.0 7.0 +93.0 1.0 +94.0 1.0 +95.0 1.0 +98.0 1.0 +100.0 1.0 +101.0 2.0 +103.0 1.0 +104.0 1.0 +111.0 1.0 +114.0 1.0 +115.0 2.0 +116.0 1.0 +120.0 1.0 +121.0 1.0 +125.0 1.0 +126.0 1.0 +129.0 2.0 +130.0 1.0 +135.0 1.0 +136.0 1.0 +140.0 1.0 +143.0 1.0 +144.0 1.0 +145.0 1.0 +147.0 1.0 +148.0 1.0 +151.0 1.0 +154.0 1.0 +158.0 2.0 +159.0 1.0 +160.0 1.0 +164.0 1.0 +166.0 1.0 +170.0 2.0 +172.0 1.0 +173.0 1.0 +176.0 1.0 +178.0 1.0 +180.0 2.0 +184.0 1.0 +188.0 1.0 +190.0 2.0 +191.0 1.0 +192.0 2.0 +197.0 1.0 +200.0 2.0 +205.0 1.0 +208.0 2.0 +210.0 1.0 +212.0 1.0 +214.0 1.0 +215.0 1.0 +218.0 1.0 +224.0 1.0 +226.0 1.0 +229.0 1.0 +231.0 2.0 +232.0 1.0 +236.0 1.0 +240.0 2.0 +241.0 1.0 +242.0 1.0 +244.0 3.0 +245.0 1.0 +247.0 1.0 +250.0 1.0 +252.0 1.0 +254.0 1.0 +258.0 2.0 +259.0 1.0 +262.0 1.0 +263.0 1.0 +264.0 1.0 +265.0 1.0 +271.0 1.0 +274.0 1.0 +275.0 1.0 +278.0 1.0 +280.0 1.0 +281.0 2.0 +284.0 1.0 +286.0 2.0 +288.0 2.0 +289.0 1.0 +292.0 1.0 +293.0 1.0 +294.0 1.0 +296.0 1.0 +300.0 1.0 +302.0 1.0 +304.0 2.0 +306.0 1.0 +308.0 1.0 +310.0 1.0 +311.0 1.0 +314.0 1.0 +315.0 1.0 +317.0 1.0 +318.0 2.0 +320.0 2.0 +324.0 1.0 +325.0 1.0 +326.0 3.0 +329.0 1.0 +330.0 1.0 +331.0 1.0 +332.0 1.0 +333.0 1.0 +334.0 2.0 +338.0 1.0 +339.0 1.0 +340.0 1.0 +342.0 1.0 +343.0 1.0 +344.0 3.0 +345.0 1.0 +348.0 3.0 +349.0 1.0 +350.0 1.0 +352.0 1.0 +356.0 3.0 +357.0 1.0 +358.0 4.0 +360.0 1.0 +362.0 9.0 +364.0 7.0 +366.0 3.0 +367.0 2.0 +368.0 28.0 +374.0 2.0 +375.0 2.0 +387.0 2.0 +388.0 1.0 +389.0 1.0 +399.0 1.0 +401.0 1.0 +403.0 1.0 +406.0 1.0 +415.0 1.0 +419.0 1.0 +425.0 1.0 +426.0 1.0 +430.0 1.0 +432.0 1.0 +436.0 1.0 +445.0 1.0 +447.0 1.0 +454.0 1.0 +458.0 2.0 +459.0 1.0 +460.0 1.0 +463.0 1.0 +476.0 1.0 +477.0 1.0 +480.0 1.0 +481.0 1.0 +483.0 2.0 +489.0 1.0 +492.0 1.0 +500.0 1.0 +501.0 1.0 +504.0 1.0 +508.0 1.0 +511.0 1.0 +512.0 1.0 +515.0 1.0 +525.0 3.0 +529.0 1.0 +533.0 1.0 +539.0 1.0 +540.0 1.0 +541.0 1.0 +546.0 1.0 +549.0 1.0 +550.0 1.0 +553.0 1.0 +559.0 1.0 +563.0 2.0 +565.0 1.0 +569.0 1.0 +575.0 1.0 +577.0 1.0 +578.0 1.0 +579.0 1.0 +591.0 2.0 +592.0 2.0 +593.0 2.0 +601.0 1.0 +603.0 1.0 +605.0 1.0 +610.0 1.0 +611.0 1.0 +613.0 2.0 +617.0 1.0 +622.0 1.0 +625.0 1.0 +628.0 1.0 +637.0 2.0 +639.0 1.0 +640.0 1.0 +643.0 1.0 +652.0 2.0 +657.0 1.0 +661.0 1.0 +663.0 2.0 +665.0 1.0 +669.0 1.0 +671.0 1.0 +674.0 1.0 +675.0 1.0 +679.0 1.0 +685.0 1.0 +687.0 1.0 +689.0 1.0 +692.0 1.0 +694.0 1.0 +697.0 2.0 +698.0 1.0 +699.0 1.0 +705.0 1.0 +711.0 1.0 +714.0 1.0 +719.0 2.0 +724.0 1.0 +727.0 1.0 +728.0 1.0 +732.0 1.0 +733.0 1.0 +735.0 1.0 +738.0 1.0 +741.0 1.0 +746.0 1.0 +752.0 1.0 +755.0 3.0 +756.0 1.0 +757.0 1.0 +763.0 1.0 +765.0 1.0 +767.0 1.0 +769.0 1.0 +770.0 1.0 +771.0 2.0 +773.0 2.0 +774.0 1.0 +775.0 1.0 +779.0 3.0 +781.0 1.0 +782.0 1.0 +785.0 2.0 +788.0 1.0 +789.0 2.0 +792.0 1.0 +793.0 5.0 +794.0 4.0 +795.0 7.0 +796.0 9.0 +797.0 8.0 +799.0 1.0 +801.0 1.0 +806.0 1.0 +807.0 1.0 +817.0 1.0 +820.0 1.0 +824.0 1.0 +825.0 1.0 +847.0 1.0 +850.0 1.0 +851.0 1.0 +853.0 1.0 +868.0 1.0 +873.0 1.0 +874.0 1.0 +875.0 1.0 +892.0 1.0 +893.0 1.0 +902.0 1.0 +906.0 1.0 +908.0 1.0 +916.0 1.0 +925.0 1.0 +927.0 1.0 +935.0 1.0 +937.0 1.0 +944.0 1.0 +955.0 1.0 +965.0 2.0 +967.0 1.0 +986.0 1.0 +988.0 1.0 +999.0 2.0 +1008.0 1.0 +1013.0 1.0 +1019.0 1.0 +1026.0 1.0 +1038.0 1.0 +1041.0 1.0 +1043.0 1.0 +1051.0 1.0 +1061.0 1.0 +1062.0 1.0 +1071.0 1.0 +1078.0 1.0 +1091.0 1.0 +1092.0 1.0 +1101.0 1.0 +1105.0 1.0 +1110.0 1.0 +1115.0 1.0 +1133.0 1.0 +1135.0 1.0 +1139.0 1.0 +1151.0 1.0 +1152.0 1.0 +1157.0 1.0 +1168.0 1.0 +1173.0 1.0 +1183.0 1.0 +1185.0 1.0 +1191.0 1.0 +1195.0 1.0 +1196.0 1.0 +1211.0 1.0 +1216.0 1.0 +1228.0 2.0 +1229.0 1.0 +1239.0 1.0 +1249.0 1.0 +1258.0 1.0 +1264.0 1.0 +1266.0 1.0 +1273.0 1.0 +1283.0 1.0 +1285.0 1.0 +1292.0 1.0 +1293.0 1.0 +1307.0 1.0 +1312.0 2.0 +1317.0 1.0 +1323.0 1.0 +1335.0 1.0 +1342.0 1.0 +1345.0 2.0 +1361.0 1.0 +1374.0 1.0 +1377.0 1.0 +1379.0 1.0 +1386.0 1.0 +1401.0 1.0 +1408.0 1.0 +1411.0 1.0 +1412.0 1.0 +1417.0 1.0 +1427.0 1.0 +1428.0 1.0 +1440.0 1.0 +1441.0 1.0 +1447.0 1.0 +1451.0 1.0 +1457.0 1.0 +1458.0 1.0 +1465.0 1.0 +1480.0 1.0 +1483.0 1.0 +1489.0 1.0 +1491.0 1.0 +1501.0 1.0 +1503.0 1.0 +1507.0 1.0 +1512.0 1.0 +1513.0 1.0 +1519.0 1.0 +1527.0 1.0 +1532.0 1.0 +1545.0 1.0 +1550.0 1.0 +1552.0 1.0 +1558.0 1.0 +1561.0 1.0 +1562.0 1.0 +1567.0 1.0 +1568.0 2.0 +1577.0 1.0 +1580.0 1.0 +1584.0 1.0 +1590.0 1.0 +1591.0 1.0 +1602.0 2.0 +1605.0 1.0 +1608.0 1.0 +1612.0 2.0 +1615.0 1.0 +1622.0 2.0 +1624.0 1.0 +1632.0 1.0 +1633.0 1.0 +1640.0 1.0 +1642.0 1.0 +1644.0 1.0 +1646.0 1.0 +1647.0 1.0 +1650.0 1.0 +1657.0 1.0 +1658.0 1.0 +1660.0 1.0 +1666.0 1.0 +1672.0 1.0 +1676.0 1.0 +1680.0 1.0 +1682.0 2.0 +1684.0 1.0 +1687.0 1.0 +1689.0 1.0 +1693.0 1.0 +1696.0 1.0 +1700.0 2.0 +1708.0 2.0 +1712.0 1.0 +1714.0 1.0 +1716.0 1.0 +1718.0 1.0 +1720.0 1.0 +1728.0 2.0 +1734.0 2.0 +1738.0 1.0 +1740.0 2.0 +1744.0 1.0 +1746.0 1.0 +1748.0 2.0 +1754.0 2.0 +1760.0 2.0 +1762.0 1.0 +1764.0 2.0 +1768.0 3.0 +1772.0 2.0 +1777.0 1.0 +1809.0 1.0 +1812.0 1.0 +1832.0 1.0 +1842.0 1.0 +1855.0 1.0 +1878.0 1.0 +1879.0 1.0 +1902.0 1.0 +1909.0 1.0 +1932.0 2.0 +1936.0 1.0 +1952.0 1.0 +1959.0 1.0 +1979.0 1.0 +1996.0 1.0 +1999.0 1.0 +2016.0 1.0 +2019.0 1.0 +2037.0 1.0 +2049.0 1.0 +2065.0 1.0 +2077.0 1.0 +2091.0 1.0 +2092.0 1.0 +2103.0 1.0 +2115.0 1.0 +2133.0 1.0 +2136.0 1.0 +2147.0 1.0 +2151.0 1.0 +2163.0 1.0 +2174.0 1.0 +2192.0 2.0 +2210.0 1.0 +2221.0 1.0 +2224.0 1.0 +2238.0 1.0 +2246.0 1.0 +2264.0 1.0 +2269.0 1.0 +2278.0 1.0 +2282.0 1.0 +2297.0 1.0 +2298.0 1.0 +2304.0 1.0 +2314.0 1.0 +2320.0 1.0 +2324.0 1.0 +2332.0 1.0 +2336.0 1.0 +2342.0 1.0 +2343.0 1.0 +2352.0 1.0 +2366.0 1.0 +2372.0 1.0 +2384.0 2.0 +2388.0 1.0 +2392.0 1.0 +2394.0 1.0 +2401.0 1.0 +2409.0 1.0 +2410.0 1.0 +2416.0 1.0 +2418.0 1.0 +2427.0 1.0 +2430.0 1.0 +2432.0 1.0 +2444.0 1.0 +2447.0 1.0 +2459.0 1.0 +2460.0 1.0 +2473.0 1.0 +2480.0 1.0 +2481.0 1.0 +2489.0 1.0 +2498.0 1.0 +2507.0 1.0 +2509.0 1.0 +2513.0 1.0 +2515.0 1.0 +2517.0 1.0 +2525.0 1.0 +2527.0 3.0 +2528.0 1.0 +2529.0 2.0 +2531.0 1.0 +2532.0 2.0 diff --git a/tests/expected/dna/qualimap/raw_data_qualimapReport/duplication_rate_histogram.txt b/tests/expected/dna/qualimap/raw_data_qualimapReport/duplication_rate_histogram.txt new file mode 100644 index 00000000..67204929 --- /dev/null +++ b/tests/expected/dna/qualimap/raw_data_qualimapReport/duplication_rate_histogram.txt @@ -0,0 +1,51 @@ +#Duplication rate Coverage +1.0 15.0 +2.0 41.0 +3.0 3.0 +4.0 31.0 +5.0 6.0 +6.0 14.0 +7.0 2.0 +8.0 28.0 +9.0 7.0 +10.0 27.0 +11.0 6.0 +12.0 20.0 +13.0 1.0 +14.0 14.0 +15.0 6.0 +16.0 16.0 +17.0 8.0 +18.0 19.0 +19.0 4.0 +20.0 12.0 +21.0 10.0 +22.0 6.0 +23.0 8.0 +24.0 7.0 +25.0 5.0 +26.0 3.0 +27.0 4.0 +28.0 2.0 +29.0 7.0 +30.0 6.0 +31.0 3.0 +32.0 2.0 +33.0 3.0 +34.0 4.0 +35.0 4.0 +36.0 3.0 +37.0 2.0 +38.0 0.0 +39.0 4.0 +40.0 1.0 +41.0 1.0 +42.0 1.0 +43.0 2.0 +44.0 1.0 +45.0 0.0 +46.0 3.0 +47.0 1.0 +48.0 1.0 +49.0 1.0 +50.0 4.0 diff --git a/tests/expected/dna/qualimap/raw_data_qualimapReport/genome_fraction_coverage.txt b/tests/expected/dna/qualimap/raw_data_qualimapReport/genome_fraction_coverage.txt new file mode 100644 index 00000000..e0eade2e --- /dev/null +++ b/tests/expected/dna/qualimap/raw_data_qualimapReport/genome_fraction_coverage.txt @@ -0,0 +1,52 @@ +#Coverage (X) Coverage +1.0 2.9524261893452746 +2.0 2.8524286892827746 +3.0 2.6449338766530843 +4.0 2.564935876603087 +5.0 2.529936751581218 +6.0 2.504937376565593 +7.0 2.5024374390640247 +8.0 2.4724381890452776 +9.0 2.4524386890327747 +10.0 2.429939251518718 +11.0 2.404939876503093 +12.0 2.4024399390015247 +13.0 2.3899402514937123 +14.0 2.387440313992144 +15.0 2.3774405639858998 +16.0 2.3749406264843316 +17.0 2.3524411889702748 +18.0 2.3499412514687066 +19.0 2.3474413139671384 +20.0 2.3424414389640162 +21.0 2.3299417514562037 +22.0 2.2974425639358884 +23.0 2.2749431264218316 +24.0 2.2699432514187095 +25.0 2.2549436264093288 +26.0 2.2524436889077606 +27.0 2.007449813754633 +28.0 2.007449813754633 +29.0 2.007449813754633 +30.0 2.007449813754633 +31.0 2.0024499387515107 +32.0 2.0024499387515107 +33.0 1.9999500012499425 +34.0 1.9999500012499425 +35.0 1.9999500012499425 +36.0 1.9999500012499425 +37.0 1.9974500637483743 +38.0 1.9949501262468061 +39.0 1.9949501262468061 +40.0 1.9949501262468061 +41.0 1.989950251243684 +42.0 1.9874503137421158 +43.0 1.9874503137421158 +44.0 1.9824504387389936 +45.0 1.9824504387389936 +46.0 1.9774505637358715 +47.0 1.9749506262343033 +48.0 1.9749506262343033 +49.0 1.972450688732735 +50.0 1.972450688732735 +51.0 1.9599510012249226 diff --git a/tests/expected/dna/qualimap/raw_data_qualimapReport/homopolymer_indels.txt b/tests/expected/dna/qualimap/raw_data_qualimapReport/homopolymer_indels.txt new file mode 100644 index 00000000..f3769464 --- /dev/null +++ b/tests/expected/dna/qualimap/raw_data_qualimapReport/homopolymer_indels.txt @@ -0,0 +1,7 @@ +#Type of indel Number of indels +polyA 2 +polyC 2 +polyG 1 +polyT 2 +polyN 0 +Non-poly 5 diff --git a/tests/expected/dna/qualimap/raw_data_qualimapReport/insert_size_across_reference.txt b/tests/expected/dna/qualimap/raw_data_qualimapReport/insert_size_across_reference.txt new file mode 100644 index 00000000..b1444ee9 --- /dev/null +++ b/tests/expected/dna/qualimap/raw_data_qualimapReport/insert_size_across_reference.txt @@ -0,0 +1,398 @@ +#Position (bp) insert size +51.0 0.0 +152.0 0.0 +253.0 0.0 +354.0 0.0 +455.0 0.0 +556.0 0.0 +657.0 0.0 +758.0 0.0 +859.0 0.0 +960.0 0.0 +1061.0 0.0 +1162.0 0.0 +1263.0 0.0 +1364.0 0.0 +1465.0 0.0 +1566.0 0.0 +1667.0 0.0 +1768.0 0.0 +1869.0 0.0 +1970.0 108.91666666666667 +2071.0 77.975 +2172.0 0.0 +2273.0 0.0 +2374.0 0.0 +2475.0 0.0 +2576.0 0.0 +2677.0 105.26829268292683 +2778.0 94.76223776223776 +2879.0 162.94767441860466 +2980.0 128.54441260744986 +3081.0 83.66666666666667 +3182.0 118.23076923076923 +3283.0 0.0 +3384.0 144.00704225352112 +3485.0 128.55276381909547 +3586.0 0.0 +3687.0 0.0 +3788.0 0.0 +3889.0 0.0 +3990.0 0.0 +4091.0 0.0 +4192.0 0.0 +4293.0 0.0 +4394.0 0.0 +4495.0 77.80434782608695 +4596.0 0.0 +4697.0 0.0 +4798.0 0.0 +4899.0 0.0 +5000.0 0.0 +5101.0 0.0 +5202.0 0.0 +5303.0 0.0 +5404.0 0.0 +5505.0 0.0 +5606.0 0.0 +5707.0 0.0 +5808.0 0.0 +5909.0 0.0 +6010.0 0.0 +6111.0 0.0 +6212.0 0.0 +6313.0 0.0 +6414.0 0.0 +6515.0 0.0 +6616.0 0.0 +6717.0 0.0 +6818.0 0.0 +6919.0 0.0 +7020.0 0.0 +7121.0 0.0 +7222.0 0.0 +7323.0 0.0 +7424.0 0.0 +7525.0 0.0 +7626.0 0.0 +7727.0 0.0 +7828.0 0.0 +7929.0 0.0 +8030.0 0.0 +8131.0 0.0 +8232.0 0.0 +8333.0 0.0 +8434.0 0.0 +8535.0 0.0 +8636.0 0.0 +8737.0 0.0 +8838.0 0.0 +8939.0 0.0 +9040.0 0.0 +9141.0 0.0 +9242.0 0.0 +9343.0 0.0 +9444.0 0.0 +9545.0 0.0 +9646.0 0.0 +9747.0 0.0 +9848.0 0.0 +9949.0 0.0 +10050.0 0.0 +10151.0 0.0 +10252.0 0.0 +10353.0 0.0 +10454.0 0.0 +10555.0 0.0 +10656.0 0.0 +10757.0 0.0 +10858.0 0.0 +10959.0 0.0 +11060.0 0.0 +11161.0 0.0 +11262.0 0.0 +11363.0 0.0 +11464.0 0.0 +11565.0 0.0 +11666.0 0.0 +11767.0 0.0 +11868.0 0.0 +11969.0 0.0 +12070.0 0.0 +12171.0 0.0 +12272.0 0.0 +12373.0 0.0 +12474.0 0.0 +12575.0 0.0 +12676.0 0.0 +12777.0 0.0 +12878.0 0.0 +12979.0 0.0 +13080.0 0.0 +13181.0 0.0 +13282.0 0.0 +13383.0 0.0 +13484.0 0.0 +13585.0 0.0 +13686.0 0.0 +13787.0 0.0 +13888.0 0.0 +13989.0 0.0 +14090.0 0.0 +14191.0 0.0 +14292.0 0.0 +14393.0 0.0 +14494.0 0.0 +14595.0 0.0 +14696.0 0.0 +14797.0 0.0 +14898.0 0.0 +14999.0 0.0 +15100.0 0.0 +15201.0 0.0 +15302.0 0.0 +15403.0 0.0 +15504.0 0.0 +15605.0 0.0 +15706.0 0.0 +15807.0 0.0 +15908.0 0.0 +16009.0 0.0 +16110.0 0.0 +16211.0 0.0 +16312.0 0.0 +16413.0 0.0 +16514.0 0.0 +16615.0 0.0 +16716.0 0.0 +16817.0 0.0 +16918.0 0.0 +17019.0 0.0 +17120.0 0.0 +17221.0 0.0 +17322.0 0.0 +17423.0 0.0 +17524.0 0.0 +17625.0 0.0 +17726.0 0.0 +17827.0 0.0 +17928.0 0.0 +18029.0 0.0 +18130.0 0.0 +18231.0 0.0 +18332.0 0.0 +18433.0 0.0 +18534.0 0.0 +18635.0 0.0 +18736.0 0.0 +18837.0 0.0 +18938.0 0.0 +19039.0 0.0 +19140.0 0.0 +19241.0 0.0 +19342.0 0.0 +19443.0 0.0 +19544.0 0.0 +19645.0 0.0 +19746.0 0.0 +19847.0 0.0 +19948.0 0.0 +20049.0 0.0 +20150.0 0.0 +20251.0 0.0 +20352.0 0.0 +20453.0 0.0 +20554.0 0.0 +20655.0 0.0 +20756.0 0.0 +20857.0 0.0 +20958.0 0.0 +21059.0 0.0 +21160.0 0.0 +21261.0 0.0 +21362.0 0.0 +21463.0 0.0 +21564.0 0.0 +21665.0 0.0 +21766.0 0.0 +21867.0 0.0 +21968.0 0.0 +22069.0 0.0 +22170.0 0.0 +22271.0 0.0 +22372.0 0.0 +22473.0 0.0 +22574.0 0.0 +22675.0 0.0 +22776.0 0.0 +22877.0 0.0 +22978.0 0.0 +23079.0 0.0 +23180.0 0.0 +23281.0 0.0 +23382.0 0.0 +23483.0 0.0 +23584.0 0.0 +23685.0 0.0 +23786.0 0.0 +23887.0 0.0 +23988.0 0.0 +24089.0 0.0 +24190.0 0.0 +24291.0 0.0 +24392.0 0.0 +24493.0 0.0 +24594.0 0.0 +24695.0 0.0 +24796.0 0.0 +24897.0 0.0 +24998.0 0.0 +25099.0 0.0 +25200.0 0.0 +25301.0 0.0 +25402.0 0.0 +25503.0 0.0 +25604.0 0.0 +25705.0 0.0 +25806.0 0.0 +25907.0 0.0 +26008.0 0.0 +26109.0 0.0 +26210.0 0.0 +26311.0 0.0 +26412.0 0.0 +26513.0 0.0 +26614.0 0.0 +26715.0 0.0 +26816.0 0.0 +26917.0 0.0 +27018.0 0.0 +27119.0 0.0 +27220.0 0.0 +27321.0 0.0 +27422.0 0.0 +27523.0 0.0 +27624.0 0.0 +27725.0 0.0 +27826.0 0.0 +27927.0 0.0 +28028.0 0.0 +28129.0 0.0 +28230.0 0.0 +28331.0 0.0 +28432.0 0.0 +28533.0 0.0 +28634.0 0.0 +28735.0 0.0 +28836.0 0.0 +28937.0 0.0 +29038.0 0.0 +29139.0 0.0 +29240.0 0.0 +29341.0 0.0 +29442.0 0.0 +29543.0 0.0 +29644.0 0.0 +29745.0 0.0 +29846.0 0.0 +29947.0 0.0 +30048.0 0.0 +30149.0 0.0 +30250.0 0.0 +30351.0 0.0 +30452.0 0.0 +30553.0 0.0 +30654.0 0.0 +30755.0 0.0 +30856.0 0.0 +30957.0 0.0 +31058.0 0.0 +31159.0 0.0 +31260.0 0.0 +31361.0 0.0 +31462.0 0.0 +31563.0 0.0 +31664.0 0.0 +31765.0 0.0 +31866.0 0.0 +31967.0 0.0 +32068.0 0.0 +32169.0 0.0 +32270.0 0.0 +32371.0 0.0 +32472.0 0.0 +32573.0 0.0 +32674.0 0.0 +32775.0 0.0 +32876.0 0.0 +32977.0 0.0 +33078.0 0.0 +33179.0 0.0 +33280.0 0.0 +33381.0 0.0 +33482.0 0.0 +33583.0 0.0 +33684.0 0.0 +33785.0 0.0 +33886.0 0.0 +33987.0 0.0 +34088.0 0.0 +34189.0 0.0 +34290.0 0.0 +34391.0 0.0 +34492.0 0.0 +34593.0 0.0 +34694.0 0.0 +34795.0 0.0 +34896.0 0.0 +34997.0 0.0 +35098.0 0.0 +35199.0 0.0 +35300.0 0.0 +35401.0 0.0 +35502.0 0.0 +35603.0 0.0 +35704.0 0.0 +35805.0 0.0 +35906.0 0.0 +36007.0 0.0 +36108.0 0.0 +36209.0 0.0 +36310.0 0.0 +36411.0 0.0 +36512.0 0.0 +36613.0 0.0 +36714.0 0.0 +36815.0 0.0 +36916.0 0.0 +37017.0 0.0 +37118.0 0.0 +37219.0 0.0 +37320.0 0.0 +37421.0 0.0 +37522.0 0.0 +37623.0 0.0 +37724.0 0.0 +37825.0 0.0 +37926.0 0.0 +38027.0 0.0 +38128.0 0.0 +38229.0 0.0 +38330.0 0.0 +38431.0 0.0 +38532.0 0.0 +38633.0 0.0 +38734.0 0.0 +38835.0 0.0 +38936.0 0.0 +39037.0 0.0 +39138.0 0.0 +39239.0 0.0 +39340.0 0.0 +39441.0 0.0 +39542.0 0.0 +39643.0 0.0 +39744.0 0.0 +39845.0 0.0 +39946.0 0.0 +39999.0 0.0 diff --git a/tests/expected/dna/qualimap/raw_data_qualimapReport/insert_size_histogram.txt b/tests/expected/dna/qualimap/raw_data_qualimapReport/insert_size_histogram.txt new file mode 100644 index 00000000..e9b8c479 --- /dev/null +++ b/tests/expected/dna/qualimap/raw_data_qualimapReport/insert_size_histogram.txt @@ -0,0 +1,171 @@ +#Insert size (bp) insert size +32.0 1.0 +41.0 1.0 +49.0 3.0 +51.0 1.0 +52.0 2.0 +54.0 1.0 +58.0 1.0 +59.0 2.0 +60.0 1.0 +61.0 4.0 +62.0 1.0 +63.0 5.0 +65.0 5.0 +66.0 2.0 +67.0 6.0 +68.0 3.0 +69.0 5.0 +70.0 10.0 +71.0 11.0 +72.0 7.0 +73.0 8.0 +74.0 4.0 +75.0 12.0 +76.0 11.0 +77.0 19.0 +78.0 15.0 +79.0 13.0 +80.0 17.0 +81.0 24.0 +82.0 18.0 +83.0 19.0 +84.0 25.0 +85.0 15.0 +86.0 24.0 +87.0 30.0 +88.0 29.0 +89.0 21.0 +90.0 16.0 +91.0 24.0 +92.0 30.0 +93.0 23.0 +94.0 21.0 +95.0 43.0 +96.0 54.0 +97.0 34.0 +98.0 28.0 +99.0 24.0 +100.0 44.0 +101.0 24.0 +102.0 27.0 +103.0 22.0 +104.0 33.0 +105.0 26.0 +106.0 28.0 +107.0 35.0 +108.0 26.0 +109.0 24.0 +110.0 34.0 +111.0 29.0 +112.0 22.0 +113.0 36.0 +114.0 30.0 +115.0 49.0 +116.0 36.0 +117.0 33.0 +118.0 34.0 +119.0 38.0 +120.0 14.0 +121.0 39.0 +122.0 30.0 +123.0 28.0 +124.0 36.0 +125.0 36.0 +126.0 25.0 +127.0 32.0 +128.0 31.0 +129.0 28.0 +130.0 39.0 +131.0 45.0 +132.0 25.0 +133.0 18.0 +134.0 25.0 +135.0 31.0 +136.0 30.0 +137.0 29.0 +138.0 34.0 +139.0 32.0 +140.0 28.0 +141.0 41.0 +142.0 27.0 +143.0 23.0 +144.0 26.0 +145.0 31.0 +146.0 21.0 +147.0 29.0 +148.0 18.0 +149.0 17.0 +150.0 19.0 +151.0 20.0 +152.0 28.0 +153.0 28.0 +154.0 18.0 +155.0 23.0 +156.0 20.0 +157.0 29.0 +158.0 16.0 +159.0 15.0 +160.0 14.0 +161.0 18.0 +162.0 19.0 +163.0 15.0 +164.0 9.0 +165.0 11.0 +166.0 21.0 +167.0 9.0 +168.0 17.0 +169.0 16.0 +170.0 17.0 +171.0 13.0 +172.0 14.0 +173.0 21.0 +174.0 9.0 +175.0 9.0 +176.0 7.0 +177.0 9.0 +178.0 9.0 +179.0 9.0 +180.0 2.0 +181.0 8.0 +182.0 8.0 +183.0 3.0 +184.0 12.0 +185.0 10.0 +186.0 5.0 +187.0 7.0 +188.0 1.0 +189.0 5.0 +190.0 8.0 +191.0 10.0 +192.0 8.0 +193.0 2.0 +194.0 6.0 +195.0 1.0 +196.0 2.0 +197.0 3.0 +198.0 2.0 +199.0 4.0 +200.0 7.0 +201.0 2.0 +202.0 6.0 +203.0 4.0 +204.0 4.0 +205.0 2.0 +206.0 4.0 +207.0 4.0 +209.0 1.0 +210.0 1.0 +212.0 2.0 +213.0 4.0 +214.0 3.0 +215.0 1.0 +216.0 4.0 +218.0 2.0 +220.0 1.0 +221.0 2.0 +223.0 1.0 +224.0 1.0 +231.0 1.0 +236.0 1.0 +239.0 1.0 diff --git a/tests/expected/dna/qualimap/raw_data_qualimapReport/mapped_reads_clipping_profile.txt b/tests/expected/dna/qualimap/raw_data_qualimapReport/mapped_reads_clipping_profile.txt new file mode 100644 index 00000000..27a26cd9 --- /dev/null +++ b/tests/expected/dna/qualimap/raw_data_qualimapReport/mapped_reads_clipping_profile.txt @@ -0,0 +1,144 @@ +#Read position (bp) Clipping profile +0.0 1.8539976825028968 +1.0 1.8539976825028968 +2.0 1.8539976825028968 +3.0 1.738122827346466 +4.0 1.6222479721900347 +5.0 1.5063731170336037 +6.0 1.5063731170336037 +7.0 1.3904982618771726 +8.0 1.3904982618771726 +9.0 1.3904982618771726 +10.0 1.3904982618771726 +11.0 1.3904982618771726 +12.0 1.3904982618771726 +13.0 1.1587485515643106 +14.0 1.1587485515643106 +15.0 1.1587485515643106 +16.0 1.1587485515643106 +17.0 1.1587485515643106 +18.0 1.1587485515643106 +19.0 1.1587485515643106 +20.0 1.1587485515643106 +21.0 1.1587485515643106 +22.0 1.1587485515643106 +23.0 1.1587485515643106 +24.0 1.1587485515643106 +25.0 1.1587485515643106 +26.0 1.0428736964078795 +27.0 1.0428736964078795 +28.0 1.0428736964078795 +29.0 1.0428736964078795 +30.0 1.0428736964078795 +31.0 1.0428736964078795 +32.0 0.9269988412514484 +33.0 0.9269988412514484 +34.0 0.9269988412514484 +35.0 0.9269988412514484 +36.0 0.9269988412514484 +37.0 1.1587485515643106 +38.0 1.1587485515643106 +39.0 1.1587485515643106 +40.0 1.1587485515643106 +41.0 1.0428736964078795 +42.0 1.0428736964078795 +43.0 1.0428736964078795 +44.0 1.0428736964078795 +45.0 1.0428736964078795 +46.0 1.0428736964078795 +47.0 1.0428736964078795 +48.0 0.9269988412514484 +49.0 0.9269988412514484 +50.0 0.9269988412514484 +51.0 0.9269988412514484 +52.0 0.9269988412514484 +53.0 0.9269988412514484 +54.0 0.8111239860950173 +55.0 0.8111239860950173 +56.0 0.8111239860950173 +57.0 0.8111239860950173 +58.0 0.8111239860950173 +59.0 0.8111239860950173 +60.0 0.8111239860950173 +61.0 0.8111239860950173 +62.0 0.8111239860950173 +63.0 0.8111239860950173 +64.0 0.6952491309385863 +65.0 0.6952491309385863 +66.0 0.6952491309385863 +67.0 0.6952491309385863 +68.0 0.5793742757821553 +69.0 0.5793742757821553 +70.0 0.4634994206257242 +71.0 0.5793742757821553 +72.0 0.5793742757821553 +73.0 0.6952491309385863 +74.0 0.6952491309385863 +75.0 0.6952491309385863 +76.0 0.5793742757821553 +77.0 0.5793742757821553 +78.0 0.5793742757821553 +79.0 0.5793742757821553 +80.0 0.5793742757821553 +81.0 0.5793742757821553 +82.0 0.4634994206257242 +83.0 0.4634994206257242 +84.0 0.4634994206257242 +85.0 0.4634994206257242 +86.0 0.4634994206257242 +87.0 0.4634994206257242 +88.0 0.4634994206257242 +89.0 0.4634994206257242 +90.0 0.4634994206257242 +91.0 0.5793742757821553 +92.0 0.34762456546929316 +93.0 0.34762456546929316 +94.0 0.34762456546929316 +95.0 0.34762456546929316 +96.0 0.34762456546929316 +97.0 0.34762456546929316 +98.0 0.2317497103128621 +99.0 0.2317497103128621 +100.0 0.2317497103128621 +101.0 0.2317497103128621 +102.0 0.2317497103128621 +103.0 0.2317497103128621 +104.0 0.11587485515643105 +105.0 0.11587485515643105 +106.0 0.11587485515643105 +107.0 0.0 +108.0 0.0 +109.0 0.0 +110.0 0.0 +111.0 0.0 +112.0 0.0 +113.0 0.0 +114.0 0.0 +115.0 0.11587485515643105 +116.0 0.11587485515643105 +117.0 0.11587485515643105 +118.0 0.11587485515643105 +119.0 0.11587485515643105 +120.0 0.0 +121.0 0.0 +122.0 0.0 +123.0 0.0 +124.0 0.0 +125.0 0.11587485515643105 +126.0 0.2317497103128621 +127.0 0.4634994206257242 +128.0 0.34762456546929316 +129.0 0.34762456546929316 +130.0 0.4634994206257242 +131.0 0.4634994206257242 +132.0 0.4634994206257242 +133.0 0.4634994206257242 +134.0 0.4634994206257242 +135.0 0.4634994206257242 +136.0 0.4634994206257242 +137.0 0.4634994206257242 +138.0 0.34762456546929316 +139.0 0.34762456546929316 +140.0 0.5793742757821553 +141.0 0.6952491309385863 +142.0 0.6952491309385863 diff --git a/tests/expected/dna/qualimap/raw_data_qualimapReport/mapped_reads_gc-content_distribution.txt b/tests/expected/dna/qualimap/raw_data_qualimapReport/mapped_reads_gc-content_distribution.txt new file mode 100644 index 00000000..ffdab0f1 --- /dev/null +++ b/tests/expected/dna/qualimap/raw_data_qualimapReport/mapped_reads_gc-content_distribution.txt @@ -0,0 +1,101 @@ +#GC Content (%) Sample +1.0 0.0 +2.0 0.0 +3.0 0.0 +4.0 0.0 +5.0 0.0 +6.0 0.0 +7.0 0.0 +8.0 0.0 +9.0 0.0 +10.0 0.0 +11.0 0.0 +12.0 0.0 +13.0 0.0 +14.0 0.0 +15.0 0.0 +16.0 0.0 +17.0 0.0 +18.0 0.0014727540500736377 +19.0 0.0 +20.0 0.0 +21.0 0.0 +22.0 0.0 +23.0 0.010309278350515464 +24.0 0.025036818851251842 +25.0 0.042709867452135494 +26.0 0.05301914580265096 +27.0 0.050073637702503684 +28.0 0.09131075110456553 +29.0 0.11192930780559647 +30.0 0.13991163475699558 +31.0 0.10751104565537553 +32.0 0.06774668630338734 +33.0 0.04860088365243005 +34.0 0.022091310751104563 +35.0 0.030927835051546393 +36.0 0.05891016200294552 +37.0 0.05301914580265096 +38.0 0.04860088365243005 +39.0 0.014727540500736377 +40.0 0.0029455081001472753 +41.0 0.0 +42.0 0.004418262150220913 +43.0 0.0014727540500736377 +44.0 0.0014727540500736377 +45.0 0.004418262150220913 +46.0 0.0029455081001472753 +47.0 0.004418262150220913 +48.0 0.0 +49.0 0.0 +50.0 0.0 +51.0 0.0 +52.0 0.0 +53.0 0.0 +54.0 0.0 +55.0 0.0 +56.0 0.0 +57.0 0.0 +58.0 0.0 +59.0 0.0 +60.0 0.0 +61.0 0.0 +62.0 0.0 +63.0 0.0 +64.0 0.0 +65.0 0.0 +66.0 0.0 +67.0 0.0 +68.0 0.0 +69.0 0.0 +70.0 0.0 +71.0 0.0 +72.0 0.0 +73.0 0.0 +74.0 0.0 +75.0 0.0 +76.0 0.0 +77.0 0.0 +78.0 0.0 +79.0 0.0 +80.0 0.0 +81.0 0.0 +82.0 0.0 +83.0 0.0 +84.0 0.0 +85.0 0.0 +86.0 0.0 +87.0 0.0 +88.0 0.0 +89.0 0.0 +90.0 0.0 +91.0 0.0 +92.0 0.0 +93.0 0.0 +94.0 0.0 +95.0 0.0 +96.0 0.0 +97.0 0.0 +98.0 0.0 +99.0 0.0 +100.0 0.0 diff --git a/tests/expected/dna/qualimap/raw_data_qualimapReport/mapped_reads_nucleotide_content.txt b/tests/expected/dna/qualimap/raw_data_qualimapReport/mapped_reads_nucleotide_content.txt new file mode 100644 index 00000000..23604a2d --- /dev/null +++ b/tests/expected/dna/qualimap/raw_data_qualimapReport/mapped_reads_nucleotide_content.txt @@ -0,0 +1,144 @@ +# Position (bp) A C G T N +0.0 36.575391180654336 12.820056899004268 18.509957325746797 32.059032716927454 0.03556187766714083 +1.0 36.21977240398293 13.78022759601707 19.221194879089616 30.743243243243246 0.03556187766714083 +2.0 37.03769559032717 13.264580369843529 17.798719772403985 31.89900426742532 0.0 +3.0 36.92444444444444 13.137777777777778 17.262222222222224 32.65777777777778 0.017777777777777778 +4.0 38.019907571987204 12.371134020618557 17.774617845716318 31.834340561677926 0.0 +5.0 37.79989337124578 12.262306735382975 17.807001954860493 32.113026479473966 0.01777145903678692 +6.0 36.200462057934956 12.937622178780877 18.269059889816955 32.592855873467215 0.0 +7.0 35.69651741293532 13.592750533049042 17.608386638237384 33.04904051172708 0.053304904051172705 +8.0 36.247334754797436 12.082444918265814 17.928216062544422 33.724235963041934 0.017768301350390904 +9.0 37.65103056147832 12.064676616915424 18.176972281449892 32.08955223880597 0.017768301350390904 +10.0 36.49609097370291 12.722103766879886 17.235252309879176 33.546552949538025 0.0 +11.0 36.44278606965174 12.686567164179104 17.093105899076047 33.70646766169154 0.07107320540156362 +12.0 35.18123667377399 14.339019189765459 17.555081734186214 32.88912579957356 0.03553660270078181 +13.0 35.861456483126105 12.966252220248666 17.779751332149203 33.37477797513321 0.017761989342806393 +14.0 36.802841918294845 13.978685612788633 18.02841918294849 31.119005328596806 0.07104795737122557 +15.0 37.24689165186501 13.001776198934282 18.17051509769094 31.49200710479574 0.08880994671403197 +16.0 37.47779751332149 13.037300177619892 17.460035523978686 31.97158081705151 0.05328596802841918 +17.0 35.09769094138544 14.103019538188278 17.72646536412078 33.01953818827709 0.05328596802841918 +18.0 35.150976909413856 14.777975133214921 16.44760213143872 33.587921847246896 0.035523978685612786 +19.0 34.04973357015986 13.321492007104796 15.86145648312611 36.731793960923625 0.035523978685612786 +20.0 35.13321492007105 12.735346358792185 17.140319715808168 34.97335701598579 0.017761989342806393 +21.0 34.795737122557725 13.0550621669627 17.08703374777975 35.0088809946714 0.05328596802841918 +22.0 34.209591474245116 13.232682060390763 16.838365896980463 35.66607460035524 0.05328596802841918 +23.0 34.08525754884547 13.161634103019537 17.708703374777976 35.0088809946714 0.035523978685612786 +24.0 34.795737122557725 12.984014209591473 16.518650088809945 35.61278863232682 0.08880994671403197 +25.0 34.849023090586144 13.534635879218474 17.05150976909414 34.52930728241563 0.035523978685612786 +26.0 34.36334576451785 13.549991120582488 17.474693660095898 34.5586929497425 0.05327650506126798 +27.0 33.24453915823122 13.461196945480378 17.95418220564731 35.25128751553898 0.0887941751021133 +28.0 34.203516249334044 14.473450541644468 17.989699875688157 33.31557449831291 0.01775883502042266 +29.0 34.469898774640384 13.514473450541646 16.78209909429941 35.21576984549814 0.01775883502042266 +30.0 35.168738898756665 13.765541740674955 17.67317939609236 33.30373001776199 0.08880994671403197 +31.0 32.770870337477795 14.174067495559504 16.660746003552397 36.34103019538188 0.05328596802841918 +32.0 34.949387320191796 13.052743740010655 16.480198898952228 35.39335819570236 0.12431184514295864 +33.0 33.83658969804618 13.570159857904084 16.607460035523978 35.89698046181172 0.08880994671403197 +34.0 33.37477797513321 14.08525754884547 16.69626998223801 35.772646536412076 0.07104795737122557 +35.0 32.45115452930728 13.623445825932503 17.33570159857904 36.53641207815275 0.05328596802841918 +36.0 34.280639431616336 12.060390763765541 16.571936056838364 37.03374777975133 0.05328596802841918 +37.0 34.02629708599858 13.521677327647478 18.105899076048328 34.310589907604836 0.03553660270078181 +38.0 33.972992181947404 13.450604122245913 17.35963041933191 35.127931769722814 0.08884150675195451 +39.0 33.546552949538025 14.978678038379531 16.91542288557214 34.50604122245913 0.053304904051172705 +40.0 35.199004975124375 14.55223880597015 15.884861407249467 34.310589907604836 0.053304904051172705 +41.0 33.91968727789623 13.699360341151387 16.417910447761194 35.90973702914002 0.053304904051172705 +42.0 34.00852878464819 13.983653162757639 16.080312722103766 35.891968727789624 0.03553660270078181 +43.0 34.09737029140014 14.232409381663114 15.689410092395168 35.87420042643924 0.10660980810234541 +44.0 35.09239516702203 14.800995024875622 15.15636105188344 34.86140724946695 0.08884150675195451 +45.0 34.06788697352053 15.141283099342456 15.265683312599965 35.489603696463476 0.03554291807357384 +46.0 34.3700017771459 14.554824951128486 15.354540607783898 35.614003909720985 0.10662875422072153 +47.0 34.97423138439666 14.9457970499378 15.016882886084948 34.97423138439666 0.08885729518393459 +48.0 35.60767590618337 14.57000710732054 15.618336886993603 34.1684434968017 0.03553660270078181 +49.0 36.374955531839205 14.585556741373177 15.91960156527926 33.04873710423337 0.0711490572749911 +50.0 34.293845606545716 15.635005336179294 16.22198505869797 33.76022767698328 0.08893632159373889 +51.0 35.355871886121 16.601423487544483 15.640569395017796 32.36654804270462 0.03558718861209965 +52.0 34.98931623931624 15.206552706552706 17.11182336182336 32.67450142450142 0.017806267806267807 +53.0 35.57692307692308 15.918803418803417 15.776353276353278 32.65669515669516 0.07122507122507123 +54.0 33.8913624220837 16.812110418521815 17.008014247551202 32.181656277827244 0.10685663401602849 +55.0 36.02849510240427 15.85040071237756 16.046304541406943 32.00356188780054 0.07123775601068566 +56.0 35.636687444345505 15.45859305431879 15.796972395369547 33.018699910952805 0.08904719501335707 +57.0 36.349065004452356 14.56812110418522 14.95992876224399 33.998219056099735 0.1246660730186999 +58.0 34.90112239444147 16.443969356850168 14.840548726171388 33.74309638339569 0.07126313914127917 +59.0 34.694241397753615 16.616152611873776 15.207701907648422 33.41059012301658 0.07131395970761277 +60.0 34.706616729088644 16.800428036383092 15.337970394150169 33.083645443196005 0.07133939718209381 +61.0 36.470798356849436 15.395606358278263 16.038578317556706 32.04143597070905 0.05358099660653688 +62.0 35.644095050920136 14.632839020904056 15.436841164909772 34.16115776308737 0.12506700017866715 +63.0 33.56005011634151 15.285484159656345 16.144621442634687 34.938249507785926 0.07159477358152855 +64.0 35.36148890479599 15.085898353614887 16.35647816750179 33.08876163206872 0.10737294201861132 +65.0 34.38508425959125 15.561133022588741 16.20652563642883 33.811401936177845 0.035855145213338116 +66.0 36.54467168998924 16.397560100466453 14.872622891998565 32.131324004305704 0.05382131324004305 +67.0 35.2391226177634 16.50485436893204 15.24631427544049 32.991729593671344 0.017979144192736426 +68.0 35.871130309575236 16.180705543556513 15.712742980561556 32.19942404607632 0.03599712023038157 +69.0 35.7194374323837 15.380454381536243 16.300036062026685 32.58204111071043 0.018031013342949875 +70.0 35.10041613895423 15.469513298353538 16.55509317893975 32.85688438574272 0.01809299800977022 +71.0 36.26453488372093 16.333575581395348 15.715843023255813 31.64970930232558 0.036337209302325583 +72.0 36.48451730418943 15.282331511839708 15.100182149362476 33.114754098360656 0.018214936247723135 +73.0 36.01315549059017 15.055728119861136 15.073999634569708 33.83884524027042 0.01827151470856934 +74.0 35.02287282708142 14.217749313815187 15.279048490393413 35.42543458371455 0.05489478499542544 +75.0 33.651902223855906 15.585370336335233 14.648042639220732 36.05954787722845 0.05513692335967653 +76.0 33.94833948339483 15.77490774907749 15.33210332103321 34.92619926199262 0.01845018450184502 +77.0 34.46674098848012 16.257896692679303 15.663322185061315 33.537718320327016 0.07432181345224824 +78.0 35.48206278026906 15.041106128550075 14.31240657698057 35.1270553064275 0.03736920777279522 +79.0 34.209538114908 16.522718738265116 13.875328576793091 35.317311303041684 0.07510326699211416 +80.0 34.03250188964474 16.93121693121693 14.13454270597128 34.863945578231295 0.03779289493575208 +81.0 33.44774980930587 16.113653699466056 15.408085430968727 35.01144164759725 0.01906941266209001 +82.0 33.36532923785755 16.37550393549626 13.419082357458246 36.80168938375888 0.03839508542906508 +83.0 34.480758073873524 16.418487719976792 12.531425256236705 36.54999033069039 0.019338619222587505 +84.0 35.344659246240965 15.52431165787932 12.90763522749463 36.1452841241945 0.07810974419058778 +85.0 35.867216656845414 16.519347868788056 13.94617953250835 33.64761343547437 0.019642506383814574 +86.0 36.149117588736864 16.141185802101923 12.71068808249058 34.97917906008328 0.019829466587348802 +87.0 34.85851896447923 15.79369857515553 13.064419024683927 36.283363435681316 0.0 +88.0 36.44670050761422 15.472081218274111 12.101522842639593 35.93908629441624 0.04060913705583756 +89.0 35.65431087446242 14.396887159533073 12.840466926070038 37.10833503993447 0.0 +90.0 34.50834879406308 16.14100185528757 13.749742321170894 35.600907029478456 0.0 +91.0 36.3579604578564 15.691987513007282 12.11238293444329 35.796045785639954 0.04162330905306972 +92.0 37.0339161575732 15.820518222034968 13.313671792711185 33.831893827680645 0.0 +93.0 35.99234205488194 16.507126143373753 13.316315677515423 34.1629440544565 0.021272069772388852 +94.0 35.142673246084534 15.85496674533362 14.353143102338553 34.62776228277194 0.021454623471358077 +95.0 36.29807692307692 15.887237762237763 13.439685314685315 34.375 0.0 +96.0 34.355416293643685 16.02506714413608 15.085049239033124 34.51208594449418 0.022381378692927483 +97.0 34.97727272727273 16.545454545454547 14.295454545454545 34.18181818181818 0.0 +98.0 35.627157652474104 17.12313003452244 13.73993095512083 33.50978135788262 0.0 +99.0 35.070979753316266 17.989294856876892 12.357458692110775 34.58226669769607 0.0 +100.0 36.08933238298883 16.39344262295082 12.21192682347351 35.28153955808981 0.023758612497030172 +101.0 35.90483056957462 15.957702475366498 12.32876712328767 35.80869983177121 0.0 +102.0 35.64645726807889 16.43535427319211 13.562210859508156 34.35597759922084 0.0 +103.0 36.697021904996305 15.850356879153335 12.552301255230125 34.90031996062023 0.0 +104.0 35.867933966983486 16.983491745872936 11.85592796398199 35.29264632316158 0.0 +105.0 37.78509883426254 16.016218955904712 12.316269640141916 33.88241256969083 0.0 +106.0 34.70437017994858 16.73521850899743 12.313624678663238 36.221079691516714 0.025706940874035987 +107.0 33.58638743455498 16.910994764397905 14.319371727748692 35.13089005235602 0.052356020942408384 +108.0 33.70488322717622 15.233545647558385 13.641188959660298 37.39384288747346 0.02653927813163482 +109.0 33.70967741935484 16.93548387096774 13.064516129032258 36.26344086021505 0.026881720430107527 +110.0 33.433734939759034 18.209200438116103 12.568455640744796 35.788608981380065 0.0 +111.0 33.36115748469672 17.50139120756817 13.439065108514189 35.67056204785754 0.02782415136338342 +112.0 33.183098591549296 16.788732394366196 13.521126760563378 36.50704225352113 0.0 +113.0 31.6561242093157 17.30879815986199 14.40483036227717 36.63024726854514 0.0 +114.0 31.71445289643066 18.461088355763604 14.160327677004094 35.63487419543593 0.029256875365710942 +115.0 33.373493975903614 16.355421686746986 13.283132530120481 36.95783132530121 0.030120481927710847 +116.0 31.434729064039406 16.163793103448278 13.023399014778326 39.37807881773399 0.0 +117.0 34.41231929604023 16.939032055311127 12.664990571967316 35.98365807668134 0.0 +118.0 32.85163776493256 19.20359666024406 12.363519588953114 35.51701991008349 0.06422607578676942 +119.0 33.20171108917407 17.17670286278381 13.063507732806844 36.55807831523528 0.0 +120.0 32.38126868150116 18.631683825971436 13.218199933576885 35.76884755895052 0.0 +121.0 31.67405386975793 18.888510057961135 13.092396863279918 36.34503920900102 0.0 +122.0 30.838844413505047 18.58684302123216 12.39122868082144 38.18308388444135 0.0 +123.0 30.45793397231097 19.772807951721692 12.70855520056798 37.06070287539936 0.0 +124.0 32.27686703096539 18.907103825136613 13.07832422586521 35.73770491803279 0.0 +125.0 31.773952095808383 18.07634730538922 13.09880239520958 37.050898203592816 0.0 +126.0 35.673407096528045 16.673025562762305 11.942006867607784 35.673407096528045 0.03815337657382679 +127.0 31.75596402033633 17.52053187328901 14.704732107938993 35.97966366836136 0.03910833007430582 +128.0 32.61217948717949 17.067307692307693 12.660256410256409 37.66025641025641 0.0 +129.0 33.77049180327869 17.745901639344265 11.721311475409836 36.721311475409834 0.040983606557377046 +130.0 34.36440677966102 17.71186440677966 12.711864406779661 35.21186440677966 0.0 +131.0 34.008810572687224 18.590308370044053 12.334801762114537 35.06607929515418 0.0 +132.0 34.27927927927928 15.855855855855856 13.558558558558559 36.306306306306304 0.0 +133.0 30.691708657810352 19.606046724690792 14.567109482363719 35.13513513513514 0.0 +134.0 32.114392873886544 18.612283169245195 13.783403656821378 35.48992030004688 0.0 +135.0 30.90294543698696 18.976339932399807 12.988894253983583 37.13182037662965 0.0 +136.0 30.432620586772753 17.951268025857782 14.172053704624565 37.444057682744905 0.0 +137.0 30.824372759856633 20.225294418842807 13.312852022529443 35.63748079877112 0.0 +138.0 32.608695652173914 18.928950159066808 12.672322375397668 35.79003181336161 0.0 +139.0 33.69923161361142 18.551042810098792 11.85510428100988 35.89462129527991 0.0 +140.0 31.916099773242628 16.383219954648524 13.662131519274375 38.038548752834465 0.0 +141.0 32.42117787031529 16.299821534800714 13.741820345032718 37.537180249851275 0.0 +142.0 32.32944068838353 16.59496004917025 10.633066994468347 40.073755377996314 0.36877688998156116 diff --git a/tests/expected/dna/qualimap/raw_data_qualimapReport/mapping_quality_across_reference.txt b/tests/expected/dna/qualimap/raw_data_qualimapReport/mapping_quality_across_reference.txt new file mode 100644 index 00000000..502c4d0c --- /dev/null +++ b/tests/expected/dna/qualimap/raw_data_qualimapReport/mapping_quality_across_reference.txt @@ -0,0 +1,398 @@ +#Position (bp) mapping quality +51.0 0.0 +152.0 0.0 +253.0 0.0 +354.0 0.0 +455.0 0.0 +556.0 0.0 +657.0 0.0 +758.0 0.0 +859.0 0.0 +960.0 0.0 +1061.0 0.0 +1162.0 0.0 +1263.0 0.0 +1364.0 0.0 +1465.0 0.0 +1566.0 0.0 +1667.0 0.0 +1768.0 0.0 +1869.0 0.0 +1970.0 59.99982589013668 +2071.0 59.98494448073155 +2172.0 60.0 +2273.0 0.0 +2374.0 0.0 +2475.0 0.0 +2576.0 0.0 +2677.0 60.0 +2778.0 60.0 +2879.0 60.0 +2980.0 59.99787561739869 +3081.0 60.0 +3182.0 60.0 +3283.0 59.94708994708995 +3384.0 59.94719637918028 +3485.0 59.992187003747695 +3586.0 59.99595381827932 +3687.0 60.0 +3788.0 0.0 +3889.0 0.0 +3990.0 0.0 +4091.0 0.0 +4192.0 0.0 +4293.0 0.0 +4394.0 0.0 +4495.0 60.0 +4596.0 60.0 +4697.0 0.0 +4798.0 0.0 +4899.0 0.0 +5000.0 0.0 +5101.0 0.0 +5202.0 0.0 +5303.0 0.0 +5404.0 0.0 +5505.0 0.0 +5606.0 0.0 +5707.0 0.0 +5808.0 0.0 +5909.0 0.0 +6010.0 0.0 +6111.0 0.0 +6212.0 0.0 +6313.0 0.0 +6414.0 0.0 +6515.0 0.0 +6616.0 0.0 +6717.0 0.0 +6818.0 0.0 +6919.0 0.0 +7020.0 0.0 +7121.0 0.0 +7222.0 0.0 +7323.0 0.0 +7424.0 0.0 +7525.0 0.0 +7626.0 0.0 +7727.0 0.0 +7828.0 0.0 +7929.0 0.0 +8030.0 0.0 +8131.0 0.0 +8232.0 0.0 +8333.0 0.0 +8434.0 0.0 +8535.0 0.0 +8636.0 0.0 +8737.0 0.0 +8838.0 0.0 +8939.0 0.0 +9040.0 0.0 +9141.0 0.0 +9242.0 0.0 +9343.0 0.0 +9444.0 0.0 +9545.0 0.0 +9646.0 0.0 +9747.0 0.0 +9848.0 0.0 +9949.0 0.0 +10050.0 0.0 +10151.0 0.0 +10252.0 0.0 +10353.0 0.0 +10454.0 0.0 +10555.0 0.0 +10656.0 0.0 +10757.0 0.0 +10858.0 0.0 +10959.0 0.0 +11060.0 0.0 +11161.0 0.0 +11262.0 0.0 +11363.0 0.0 +11464.0 0.0 +11565.0 0.0 +11666.0 0.0 +11767.0 0.0 +11868.0 0.0 +11969.0 0.0 +12070.0 0.0 +12171.0 0.0 +12272.0 0.0 +12373.0 0.0 +12474.0 0.0 +12575.0 0.0 +12676.0 0.0 +12777.0 0.0 +12878.0 0.0 +12979.0 0.0 +13080.0 0.0 +13181.0 0.0 +13282.0 0.0 +13383.0 0.0 +13484.0 0.0 +13585.0 0.0 +13686.0 0.0 +13787.0 0.0 +13888.0 0.0 +13989.0 0.0 +14090.0 0.0 +14191.0 0.0 +14292.0 0.0 +14393.0 0.0 +14494.0 0.0 +14595.0 0.0 +14696.0 0.0 +14797.0 0.0 +14898.0 0.0 +14999.0 0.0 +15100.0 0.0 +15201.0 0.0 +15302.0 0.0 +15403.0 0.0 +15504.0 0.0 +15605.0 0.0 +15706.0 0.0 +15807.0 0.0 +15908.0 0.0 +16009.0 0.0 +16110.0 0.0 +16211.0 0.0 +16312.0 0.0 +16413.0 0.0 +16514.0 0.0 +16615.0 0.0 +16716.0 0.0 +16817.0 0.0 +16918.0 0.0 +17019.0 0.0 +17120.0 0.0 +17221.0 0.0 +17322.0 0.0 +17423.0 0.0 +17524.0 0.0 +17625.0 0.0 +17726.0 0.0 +17827.0 0.0 +17928.0 0.0 +18029.0 0.0 +18130.0 0.0 +18231.0 0.0 +18332.0 0.0 +18433.0 0.0 +18534.0 0.0 +18635.0 0.0 +18736.0 0.0 +18837.0 0.0 +18938.0 0.0 +19039.0 0.0 +19140.0 0.0 +19241.0 0.0 +19342.0 0.0 +19443.0 0.0 +19544.0 0.0 +19645.0 0.0 +19746.0 0.0 +19847.0 0.0 +19948.0 0.0 +20049.0 0.0 +20150.0 0.0 +20251.0 0.0 +20352.0 0.0 +20453.0 0.0 +20554.0 0.0 +20655.0 0.0 +20756.0 0.0 +20857.0 0.0 +20958.0 0.0 +21059.0 0.0 +21160.0 0.0 +21261.0 0.0 +21362.0 0.0 +21463.0 0.0 +21564.0 0.0 +21665.0 0.0 +21766.0 0.0 +21867.0 0.0 +21968.0 0.0 +22069.0 0.0 +22170.0 0.0 +22271.0 0.0 +22372.0 0.0 +22473.0 0.0 +22574.0 0.0 +22675.0 0.0 +22776.0 0.0 +22877.0 0.0 +22978.0 0.0 +23079.0 0.0 +23180.0 0.0 +23281.0 0.0 +23382.0 0.0 +23483.0 0.0 +23584.0 0.0 +23685.0 0.0 +23786.0 0.0 +23887.0 0.0 +23988.0 0.0 +24089.0 0.0 +24190.0 0.0 +24291.0 0.0 +24392.0 0.0 +24493.0 0.0 +24594.0 0.0 +24695.0 0.0 +24796.0 0.0 +24897.0 0.0 +24998.0 0.0 +25099.0 0.0 +25200.0 0.0 +25301.0 0.0 +25402.0 0.0 +25503.0 0.0 +25604.0 0.0 +25705.0 0.0 +25806.0 0.0 +25907.0 0.0 +26008.0 0.0 +26109.0 0.0 +26210.0 0.0 +26311.0 0.0 +26412.0 0.0 +26513.0 0.0 +26614.0 0.0 +26715.0 0.0 +26816.0 0.0 +26917.0 0.0 +27018.0 0.0 +27119.0 0.0 +27220.0 0.0 +27321.0 0.0 +27422.0 0.0 +27523.0 0.0 +27624.0 0.0 +27725.0 0.0 +27826.0 0.0 +27927.0 0.0 +28028.0 0.0 +28129.0 0.0 +28230.0 0.0 +28331.0 0.0 +28432.0 0.0 +28533.0 0.0 +28634.0 0.0 +28735.0 0.0 +28836.0 0.0 +28937.0 0.0 +29038.0 0.0 +29139.0 0.0 +29240.0 0.0 +29341.0 0.0 +29442.0 0.0 +29543.0 0.0 +29644.0 0.0 +29745.0 0.0 +29846.0 0.0 +29947.0 0.0 +30048.0 0.0 +30149.0 0.0 +30250.0 0.0 +30351.0 0.0 +30452.0 0.0 +30553.0 0.0 +30654.0 0.0 +30755.0 0.0 +30856.0 0.0 +30957.0 0.0 +31058.0 0.0 +31159.0 0.0 +31260.0 0.0 +31361.0 0.0 +31462.0 0.0 +31563.0 0.0 +31664.0 0.0 +31765.0 0.0 +31866.0 0.0 +31967.0 0.0 +32068.0 0.0 +32169.0 0.0 +32270.0 0.0 +32371.0 0.0 +32472.0 0.0 +32573.0 0.0 +32674.0 0.0 +32775.0 0.0 +32876.0 0.0 +32977.0 0.0 +33078.0 0.0 +33179.0 0.0 +33280.0 0.0 +33381.0 0.0 +33482.0 0.0 +33583.0 0.0 +33684.0 0.0 +33785.0 0.0 +33886.0 0.0 +33987.0 0.0 +34088.0 0.0 +34189.0 0.0 +34290.0 0.0 +34391.0 0.0 +34492.0 0.0 +34593.0 0.0 +34694.0 0.0 +34795.0 0.0 +34896.0 0.0 +34997.0 0.0 +35098.0 0.0 +35199.0 0.0 +35300.0 0.0 +35401.0 0.0 +35502.0 0.0 +35603.0 0.0 +35704.0 0.0 +35805.0 0.0 +35906.0 0.0 +36007.0 0.0 +36108.0 0.0 +36209.0 0.0 +36310.0 0.0 +36411.0 0.0 +36512.0 0.0 +36613.0 0.0 +36714.0 0.0 +36815.0 0.0 +36916.0 0.0 +37017.0 0.0 +37118.0 0.0 +37219.0 0.0 +37320.0 0.0 +37421.0 0.0 +37522.0 0.0 +37623.0 0.0 +37724.0 0.0 +37825.0 0.0 +37926.0 0.0 +38027.0 0.0 +38128.0 0.0 +38229.0 0.0 +38330.0 0.0 +38431.0 0.0 +38532.0 0.0 +38633.0 0.0 +38734.0 0.0 +38835.0 0.0 +38936.0 0.0 +39037.0 0.0 +39138.0 0.0 +39239.0 0.0 +39340.0 0.0 +39441.0 0.0 +39542.0 0.0 +39643.0 0.0 +39744.0 0.0 +39845.0 0.0 +39946.0 0.0 +39999.0 0.0 diff --git a/tests/expected/dna/qualimap/raw_data_qualimapReport/mapping_quality_histogram.txt b/tests/expected/dna/qualimap/raw_data_qualimapReport/mapping_quality_histogram.txt new file mode 100644 index 00000000..30307309 --- /dev/null +++ b/tests/expected/dna/qualimap/raw_data_qualimapReport/mapping_quality_histogram.txt @@ -0,0 +1,3 @@ +#Mapping quality mapping quality +59.0 248.0 +60.0 933.0 diff --git a/tests/expected/dna/test.flagstat.txt b/tests/expected/dna/test.flagstat.txt new file mode 100644 index 00000000..1d2e97e5 --- /dev/null +++ b/tests/expected/dna/test.flagstat.txt @@ -0,0 +1,16 @@ +5644 + 0 in total (QC-passed reads + QC-failed reads) +5642 + 0 primary +2 + 0 secondary +0 + 0 supplementary +1656 + 0 duplicates +1656 + 0 primary duplicates +5642 + 0 mapped (99.96% : N/A) +5640 + 0 primary mapped (99.96% : N/A) +5642 + 0 paired in sequencing +2821 + 0 read1 +2821 + 0 read2 +5638 + 0 properly paired (99.93% : N/A) +5640 + 0 with itself and mate mapped +0 + 0 singletons (0.00% : N/A) +0 + 0 with mate mapped to a different chr +0 + 0 with mate mapped to a different chr (mapQ>=5) diff --git a/tests/expected/dna/test.gc_bias.detail_metrics.txt b/tests/expected/dna/test.gc_bias.detail_metrics.txt new file mode 100644 index 00000000..e6da30c8 --- /dev/null +++ b/tests/expected/dna/test.gc_bias.detail_metrics.txt @@ -0,0 +1,105 @@ +## METRICS CLASS picard.analysis.GcBiasDetailMetrics +ACCUMULATION_LEVEL READS_USED GC WINDOWS READ_STARTS MEAN_BASE_QUALITY NORMALIZED_COVERAGE ERROR_BAR_WIDTH SAMPLE LIBRARY READ_GROUP +All Reads ALL 0 0 0 0 0 0 +All Reads ALL 1 0 0 0 0 0 +All Reads ALL 2 0 0 0 0 0 +All Reads ALL 3 0 0 0 0 0 +All Reads ALL 4 0 0 0 0 0 +All Reads ALL 5 0 0 0 0 0 +All Reads ALL 6 0 0 0 0 0 +All Reads ALL 7 0 0 0 0 0 +All Reads ALL 8 0 0 0 0 0 +All Reads ALL 9 0 0 0 0 0 +All Reads ALL 10 0 0 0 0 0 +All Reads ALL 11 0 0 0 0 0 +All Reads ALL 12 0 0 0 0 0 +All Reads ALL 13 0 0 0 0 0 +All Reads ALL 14 6 0 0 0 0 +All Reads ALL 15 8 0 0 0 0 +All Reads ALL 16 10 0 0 0 0 +All Reads ALL 17 38 0 0 0 0 +All Reads ALL 18 69 0 0 0 0 +All Reads ALL 19 106 73 27 4.870312 0.570027 +All Reads ALL 20 157 80 26 3.603547 0.402889 +All Reads ALL 21 223 179 27 5.676596 0.424289 +All Reads ALL 22 319 71 27 1.57401 0.186801 +All Reads ALL 23 397 43 30 0.765981 0.116811 +All Reads ALL 24 429 96 28 1.582537 0.161517 +All Reads ALL 25 493 76 31 1.090201 0.125055 +All Reads ALL 26 741 85 25 0.811224 0.08799 +All Reads ALL 27 1014 243 26 1.69476 0.108719 +All Reads ALL 28 1015 485 28 3.379213 0.153442 +All Reads ALL 29 1079 841 26 5.512065 0.190071 +All Reads ALL 30 1048 766 27 5.169009 0.186764 +All Reads ALL 31 1142 486 26 3.009608 0.136519 +All Reads ALL 32 1223 353 26 2.041212 0.108643 +All Reads ALL 33 1119 339 28 2.142444 0.116362 +All Reads ALL 34 1281 327 28 1.805255 0.099831 +All Reads ALL 35 1194 239 30 1.415577 0.091566 +All Reads ALL 36 1191 376 28 2.232626 0.115139 +All Reads ALL 37 1171 85 24 0.513336 0.055679 +All Reads ALL 38 1135 87 28 0.54208 0.058117 +All Reads ALL 39 1140 95 26 0.58933 0.060464 +All Reads ALL 40 1166 39 26 0.236541 0.037877 +All Reads ALL 41 1070 72 29 0.47587 0.056082 +All Reads ALL 42 973 28 20 0.20351 0.03846 +All Reads ALL 43 1040 11 0 0.0748 0.022553 +All Reads ALL 44 1078 17 25 0.111524 0.027049 +All Reads ALL 45 924 23 21 0.176034 0.036706 +All Reads ALL 46 1022 8 0 0.055358 0.019572 +All Reads ALL 47 932 16 31 0.121407 0.030352 +All Reads ALL 48 906 3 0 0.023417 0.01352 +All Reads ALL 49 1068 0 0 0 0 +All Reads ALL 50 1027 0 0 0 0 +All Reads ALL 51 1081 0 0 0 0 +All Reads ALL 52 915 0 0 0 0 +All Reads ALL 53 816 0 0 0 0 +All Reads ALL 54 754 0 0 0 0 +All Reads ALL 55 751 0 0 0 0 +All Reads ALL 56 783 0 0 0 0 +All Reads ALL 57 721 0 0 0 0 +All Reads ALL 58 574 0 0 0 0 +All Reads ALL 59 563 0 0 0 0 +All Reads ALL 60 494 0 0 0 0 +All Reads ALL 61 331 0 0 0 0 +All Reads ALL 62 246 0 0 0 0 +All Reads ALL 63 247 0 0 0 0 +All Reads ALL 64 233 0 0 0 0 +All Reads ALL 65 217 0 0 0 0 +All Reads ALL 66 203 0 0 0 0 +All Reads ALL 67 181 0 0 0 0 +All Reads ALL 68 125 0 0 0 0 +All Reads ALL 69 103 0 0 0 0 +All Reads ALL 70 140 0 0 0 0 +All Reads ALL 71 137 0 0 0 0 +All Reads ALL 72 124 0 0 0 0 +All Reads ALL 73 119 0 0 0 0 +All Reads ALL 74 105 0 0 0 0 +All Reads ALL 75 111 0 0 0 0 +All Reads ALL 76 85 0 0 0 0 +All Reads ALL 77 71 0 0 0 0 +All Reads ALL 78 89 0 0 0 0 +All Reads ALL 79 96 0 0 0 0 +All Reads ALL 80 78 0 0 0 0 +All Reads ALL 81 50 0 0 0 0 +All Reads ALL 82 73 0 0 0 0 +All Reads ALL 83 65 0 0 0 0 +All Reads ALL 84 34 0 0 0 0 +All Reads ALL 85 39 0 0 0 0 +All Reads ALL 86 22 0 0 0 0 +All Reads ALL 87 28 0 0 0 0 +All Reads ALL 88 28 0 0 0 0 +All Reads ALL 89 30 0 0 0 0 +All Reads ALL 90 51 0 0 0 0 +All Reads ALL 91 26 0 0 0 0 +All Reads ALL 92 7 0 0 0 0 +All Reads ALL 93 0 0 0 0 0 +All Reads ALL 94 0 0 0 0 0 +All Reads ALL 95 0 0 0 0 0 +All Reads ALL 96 0 0 0 0 0 +All Reads ALL 97 0 0 0 0 0 +All Reads ALL 98 0 0 0 0 0 +All Reads ALL 99 0 0 0 0 0 +All Reads ALL 100 0 0 0 0 0 + + diff --git a/tests/expected/dna/test.gc_bias.summary_metrics.txt b/tests/expected/dna/test.gc_bias.summary_metrics.txt new file mode 100644 index 00000000..8a21261c --- /dev/null +++ b/tests/expected/dna/test.gc_bias.summary_metrics.txt @@ -0,0 +1,5 @@ +## METRICS CLASS picard.analysis.GcBiasSummaryMetrics +ACCUMULATION_LEVEL READS_USED WINDOW_SIZE TOTAL_CLUSTERS ALIGNED_READS AT_DROPOUT GC_DROPOUT GC_NC_0_19 GC_NC_20_39 GC_NC_40_59 GC_NC_60_79 GC_NC_80_100 SAMPLE LIBRARY READ_GROUP +All Reads ALL 100 2822 5642 29.055038 27.433584 2.178283 2.161449 0.084487 0 0 + + diff --git a/tests/expected/dna/test.hs_metrics.txt b/tests/expected/dna/test.hs_metrics.txt new file mode 100644 index 00000000..0f498da1 --- /dev/null +++ b/tests/expected/dna/test.hs_metrics.txt @@ -0,0 +1,870 @@ +## METRICS CLASS picard.analysis.directed.HsMetrics +BAIT_SET BAIT_TERRITORY BAIT_DESIGN_EFFICIENCY ON_BAIT_BASES NEAR_BAIT_BASES OFF_BAIT_BASES PCT_SELECTED_BASES PCT_OFF_BAIT ON_BAIT_VS_SELECTED MEAN_BAIT_COVERAGE PCT_USABLE_BASES_ON_BAIT PCT_USABLE_BASES_ON_TARGET FOLD_ENRICHMENT HS_LIBRARY_SIZE HS_PENALTY_10X HS_PENALTY_20X HS_PENALTY_30X HS_PENALTY_40X HS_PENALTY_50X HS_PENALTY_100X TARGET_TERRITORY GENOME_SIZE TOTAL_READS PF_READS PF_BASES PF_UNIQUE_READS PF_UQ_READS_ALIGNED PF_BASES_ALIGNED PF_UQ_BASES_ALIGNED ON_TARGET_BASES PCT_PF_READS PCT_PF_UQ_READS PCT_PF_UQ_READS_ALIGNED MEAN_TARGET_COVERAGE MEDIAN_TARGET_COVERAGE MAX_TARGET_COVERAGE MIN_TARGET_COVERAGE ZERO_CVG_TARGETS_PCT PCT_EXC_DUPE PCT_EXC_ADAPTER PCT_EXC_MAPQ PCT_EXC_BASEQ PCT_EXC_OVERLAP PCT_EXC_OFF_TARGET FOLD_80_BASE_PENALTY PCT_TARGET_BASES_1X PCT_TARGET_BASES_2X PCT_TARGET_BASES_10X PCT_TARGET_BASES_20X PCT_TARGET_BASES_30X PCT_TARGET_BASES_40X PCT_TARGET_BASES_50X PCT_TARGET_BASES_100X PCT_TARGET_BASES_250X PCT_TARGET_BASES_500X PCT_TARGET_BASES_1000X PCT_TARGET_BASES_2500X PCT_TARGET_BASES_5000X PCT_TARGET_BASES_10000X PCT_TARGET_BASES_25000X PCT_TARGET_BASES_50000X PCT_TARGET_BASES_100000X AT_DROPOUT GC_DROPOUT HET_SNP_SENSITIVITY HET_SNP_Q SAMPLE LIBRARY READ_GROUP +targets 35000 1 670989 0 0 1 0 1 19.171114 0.998301 0.364694 1.142886 3807 -1 -1 -1 -1 -1 -1 35000 40001 5642 5642 672131 3986 3984 670989 469869 245122 1 0.706487 0.999498 7.003486 0 862 0 0.5 0.299737 0 0 0.003982 0.330968 0 ? 0.033229 0.029943 0.026171 0.022171 0.020943 0.019486 0.019114 0.017171 0.010886 0.006 0 0 0 0 0 0 0 57.145714 0 0.031185 0 + +## HISTOGRAM java.lang.Integer +coverage_or_base_quality high_quality_coverage_count unfiltered_baseq_count +0 33837 0 +1 115 0 +2 34 0 +3 22 0 +4 18 23 +5 8 1 +6 19 1 +7 9 0 +8 13 8 +9 9 0 +10 93 1 +11 3 2 +12 5 0 +13 5 2481 +14 7 0 +15 2 0 +16 12 0 +17 5 15 +18 4 10 +19 4 0 +20 0 576 +21 4 1 +22 5 0 +23 1 0 +24 6 0 +25 4 5 +26 4 1154 +27 8 0 +28 6 0 +29 5 0 +30 18 0 +31 21 4527 +32 5 64 +33 0 1 +34 2 54518 +35 0 14 +36 2 1 +37 1 0 +38 1 126 +39 1 0 +40 1 24 +41 3 2 +42 0 94 +43 2 244 +44 1 9250 +45 0 174521 +46 2 0 +47 1 0 +48 1 0 +49 2 0 +50 1 0 +51 3 0 +52 1 0 +53 0 0 +54 1 0 +55 2 0 +56 1 0 +57 1 0 +58 4 0 +59 0 0 +60 0 0 +61 1 0 +62 3 0 +63 0 0 +64 2 0 +65 2 0 +66 0 0 +67 0 0 +68 2 0 +69 1 0 +70 2 0 +71 1 0 +72 2 0 +73 0 0 +74 1 0 +75 0 0 +76 2 0 +77 2 0 +78 2 0 +79 1 0 +80 2 0 +81 1 0 +82 2 0 +83 2 0 +84 1 0 +85 1 0 +86 1 0 +87 1 0 +88 0 0 +89 2 0 +90 0 0 +91 4 0 +92 2 0 +93 1 0 +94 3 0 +95 1 0 +96 1 0 +97 3 0 +98 1 0 +99 1 0 +100 1 0 +101 0 0 +102 0 0 +103 2 0 +104 2 0 +105 3 0 +106 2 0 +107 2 0 +108 1 0 +109 3 0 +110 1 0 +111 3 0 +112 3 0 +113 1 0 +114 1 0 +115 1 0 +116 0 0 +117 3 0 +118 6 0 +119 1 0 +120 3 0 +121 0 0 +122 4 0 +123 2 0 +124 2 0 +125 3 0 +126 2 0 +127 6 0 +128 3 0 +129 7 0 +130 11 0 +131 9 0 +132 15 0 +133 10 0 +134 2 0 +135 0 0 +136 0 0 +137 0 0 +138 1 0 +139 1 0 +140 1 0 +141 0 0 +142 2 0 +143 1 0 +144 0 0 +145 0 0 +146 1 0 +147 1 0 +148 0 0 +149 2 0 +150 0 0 +151 0 0 +152 1 0 +153 1 0 +154 0 0 +155 1 0 +156 1 0 +157 1 0 +158 1 0 +159 2 0 +160 3 0 +161 0 0 +162 0 0 +163 1 0 +164 1 0 +165 1 0 +166 1 0 +167 0 0 +168 0 0 +169 0 0 +170 3 0 +171 1 0 +172 1 0 +173 1 0 +174 0 0 +175 1 0 +176 1 0 +177 2 0 +178 1 0 +179 1 0 +180 0 0 +181 0 0 +182 2 0 +183 1 0 +184 0 0 +185 1 0 +186 1 0 +187 0 0 +188 0 0 +189 2 0 +190 2 0 +191 0 0 +192 0 0 +193 0 0 +194 1 0 +195 2 0 +196 2 0 +197 1 0 +198 0 0 +199 3 0 +200 0 0 +201 3 0 +202 0 0 +203 0 0 +204 0 0 +205 1 0 +206 0 0 +207 1 0 +208 0 0 +209 1 0 +210 1 0 +211 0 0 +212 2 0 +213 3 0 +214 1 0 +215 0 0 +216 3 0 +217 1 0 +218 0 0 +219 0 0 +220 2 0 +221 1 0 +222 0 0 +223 0 0 +224 0 0 +225 2 0 +226 2 0 +227 0 0 +228 2 0 +229 0 0 +230 2 0 +231 1 0 +232 3 0 +233 0 0 +234 2 0 +235 0 0 +236 0 0 +237 1 0 +238 1 0 +239 0 0 +240 1 0 +241 1 0 +242 1 0 +243 0 0 +244 5 0 +245 1 0 +246 1 0 +247 0 0 +248 2 0 +249 2 0 +250 1 0 +251 1 0 +252 3 0 +253 0 0 +254 1 0 +255 1 0 +256 2 0 +257 1 0 +258 5 0 +259 3 0 +260 2 0 +261 4 0 +262 8 0 +263 7 0 +264 6 0 +265 9 0 +266 4 0 +267 0 0 +268 0 0 +269 2 0 +270 0 0 +271 0 0 +272 1 0 +273 1 0 +274 1 0 +275 0 0 +276 0 0 +277 1 0 +278 0 0 +279 0 0 +280 1 0 +281 1 0 +282 0 0 +283 0 0 +284 2 0 +285 0 0 +286 1 0 +287 0 0 +288 0 0 +289 0 0 +290 0 0 +291 0 0 +292 0 0 +293 0 0 +294 0 0 +295 1 0 +296 1 0 +297 0 0 +298 1 0 +299 0 0 +300 0 0 +301 1 0 +302 0 0 +303 1 0 +304 1 0 +305 0 0 +306 1 0 +307 0 0 +308 0 0 +309 1 0 +310 0 0 +311 0 0 +312 0 0 +313 0 0 +314 1 0 +315 1 0 +316 0 0 +317 1 0 +318 1 0 +319 0 0 +320 0 0 +321 0 0 +322 0 0 +323 1 0 +324 0 0 +325 1 0 +326 0 0 +327 0 0 +328 0 0 +329 2 0 +330 0 0 +331 0 0 +332 1 0 +333 1 0 +334 0 0 +335 1 0 +336 2 0 +337 0 0 +338 0 0 +339 0 0 +340 1 0 +341 0 0 +342 0 0 +343 2 0 +344 0 0 +345 0 0 +346 1 0 +347 2 0 +348 0 0 +349 1 0 +350 0 0 +351 0 0 +352 2 0 +353 0 0 +354 0 0 +355 0 0 +356 0 0 +357 0 0 +358 1 0 +359 1 0 +360 0 0 +361 1 0 +362 0 0 +363 1 0 +364 0 0 +365 1 0 +366 0 0 +367 0 0 +368 2 0 +369 0 0 +370 0 0 +371 0 0 +372 1 0 +373 0 0 +374 0 0 +375 2 0 +376 0 0 +377 1 0 +378 0 0 +379 0 0 +380 2 0 +381 0 0 +382 0 0 +383 2 0 +384 0 0 +385 0 0 +386 1 0 +387 0 0 +388 0 0 +389 1 0 +390 1 0 +391 0 0 +392 0 0 +393 1 0 +394 1 0 +395 0 0 +396 0 0 +397 0 0 +398 1 0 +399 0 0 +400 0 0 +401 0 0 +402 1 0 +403 2 0 +404 0 0 +405 1 0 +406 1 0 +407 0 0 +408 2 0 +409 0 0 +410 0 0 +411 0 0 +412 0 0 +413 2 0 +414 0 0 +415 0 0 +416 0 0 +417 0 0 +418 1 0 +419 0 0 +420 1 0 +421 1 0 +422 1 0 +423 0 0 +424 0 0 +425 0 0 +426 1 0 +427 0 0 +428 1 0 +429 1 0 +430 1 0 +431 0 0 +432 0 0 +433 1 0 +434 0 0 +435 0 0 +436 1 0 +437 0 0 +438 0 0 +439 1 0 +440 0 0 +441 0 0 +442 1 0 +443 0 0 +444 0 0 +445 2 0 +446 2 0 +447 0 0 +448 0 0 +449 0 0 +450 0 0 +451 0 0 +452 0 0 +453 1 0 +454 1 0 +455 1 0 +456 0 0 +457 0 0 +458 0 0 +459 1 0 +460 0 0 +461 0 0 +462 0 0 +463 1 0 +464 0 0 +465 2 0 +466 1 0 +467 0 0 +468 2 0 +469 0 0 +470 0 0 +471 1 0 +472 0 0 +473 2 0 +474 0 0 +475 1 0 +476 0 0 +477 2 0 +478 0 0 +479 0 0 +480 0 0 +481 0 0 +482 0 0 +483 2 0 +484 1 0 +485 0 0 +486 1 0 +487 0 0 +488 0 0 +489 0 0 +490 1 0 +491 1 0 +492 1 0 +493 0 0 +494 0 0 +495 1 0 +496 0 0 +497 1 0 +498 1 0 +499 0 0 +500 0 0 +501 1 0 +502 0 0 +503 1 0 +504 0 0 +505 0 0 +506 0 0 +507 1 0 +508 1 0 +509 0 0 +510 1 0 +511 1 0 +512 0 0 +513 1 0 +514 2 0 +515 0 0 +516 0 0 +517 0 0 +518 0 0 +519 0 0 +520 1 0 +521 1 0 +522 0 0 +523 1 0 +524 3 0 +525 0 0 +526 1 0 +527 0 0 +528 0 0 +529 1 0 +530 1 0 +531 1 0 +532 0 0 +533 1 0 +534 0 0 +535 1 0 +536 1 0 +537 0 0 +538 0 0 +539 0 0 +540 0 0 +541 1 0 +542 1 0 +543 0 0 +544 1 0 +545 0 0 +546 0 0 +547 0 0 +548 1 0 +549 1 0 +550 0 0 +551 0 0 +552 1 0 +553 1 0 +554 0 0 +555 1 0 +556 1 0 +557 0 0 +558 1 0 +559 0 0 +560 0 0 +561 1 0 +562 0 0 +563 1 0 +564 0 0 +565 1 0 +566 0 0 +567 0 0 +568 1 0 +569 1 0 +570 2 0 +571 0 0 +572 3 0 +573 1 0 +574 1 0 +575 0 0 +576 1 0 +577 1 0 +578 0 0 +579 0 0 +580 0 0 +581 0 0 +582 2 0 +583 3 0 +584 0 0 +585 0 0 +586 0 0 +587 1 0 +588 1 0 +589 1 0 +590 0 0 +591 0 0 +592 1 0 +593 2 0 +594 0 0 +595 0 0 +596 3 0 +597 2 0 +598 1 0 +599 1 0 +600 1 0 +601 0 0 +602 2 0 +603 1 0 +604 0 0 +605 1 0 +606 0 0 +607 0 0 +608 0 0 +609 2 0 +610 1 0 +611 0 0 +612 2 0 +613 1 0 +614 1 0 +615 0 0 +616 3 0 +617 0 0 +618 0 0 +619 3 0 +620 1 0 +621 1 0 +622 0 0 +623 1 0 +624 0 0 +625 2 0 +626 0 0 +627 1 0 +628 2 0 +629 1 0 +630 1 0 +631 1 0 +632 2 0 +633 0 0 +634 1 0 +635 1 0 +636 1 0 +637 0 0 +638 2 0 +639 0 0 +640 3 0 +641 0 0 +642 2 0 +643 1 0 +644 1 0 +645 2 0 +646 2 0 +647 3 0 +648 1 0 +649 0 0 +650 2 0 +651 0 0 +652 0 0 +653 0 0 +654 1 0 +655 2 0 +656 1 0 +657 1 0 +658 1 0 +659 1 0 +660 0 0 +661 1 0 +662 0 0 +663 0 0 +664 0 0 +665 1 0 +666 0 0 +667 0 0 +668 0 0 +669 0 0 +670 0 0 +671 0 0 +672 1 0 +673 0 0 +674 0 0 +675 0 0 +676 0 0 +677 1 0 +678 2 0 +679 0 0 +680 0 0 +681 0 0 +682 1 0 +683 1 0 +684 0 0 +685 0 0 +686 0 0 +687 0 0 +688 0 0 +689 1 0 +690 0 0 +691 0 0 +692 0 0 +693 0 0 +694 0 0 +695 0 0 +696 1 0 +697 1 0 +698 0 0 +699 0 0 +700 0 0 +701 0 0 +702 1 0 +703 0 0 +704 1 0 +705 0 0 +706 0 0 +707 0 0 +708 0 0 +709 0 0 +710 1 0 +711 0 0 +712 0 0 +713 1 0 +714 2 0 +715 0 0 +716 0 0 +717 0 0 +718 0 0 +719 0 0 +720 1 0 +721 0 0 +722 0 0 +723 0 0 +724 0 0 +725 0 0 +726 0 0 +727 0 0 +728 0 0 +729 1 0 +730 0 0 +731 0 0 +732 1 0 +733 0 0 +734 1 0 +735 0 0 +736 0 0 +737 1 0 +738 0 0 +739 1 0 +740 0 0 +741 0 0 +742 0 0 +743 3 0 +744 0 0 +745 0 0 +746 0 0 +747 0 0 +748 0 0 +749 1 0 +750 0 0 +751 0 0 +752 1 0 +753 0 0 +754 1 0 +755 0 0 +756 0 0 +757 0 0 +758 0 0 +759 0 0 +760 0 0 +761 1 0 +762 0 0 +763 0 0 +764 0 0 +765 1 0 +766 0 0 +767 1 0 +768 1 0 +769 0 0 +770 1 0 +771 1 0 +772 0 0 +773 0 0 +774 0 0 +775 0 0 +776 0 0 +777 0 0 +778 0 0 +779 0 0 +780 1 0 +781 1 0 +782 0 0 +783 0 0 +784 2 0 +785 0 0 +786 0 0 +787 1 0 +788 0 0 +789 0 0 +790 1 0 +791 0 0 +792 0 0 +793 2 0 +794 1 0 +795 1 0 +796 2 0 +797 0 0 +798 0 0 +799 0 0 +800 0 0 +801 0 0 +802 1 0 +803 0 0 +804 1 0 +805 2 0 +806 0 0 +807 0 0 +808 0 0 +809 2 0 +810 2 0 +811 0 0 +812 1 0 +813 2 0 +814 0 0 +815 0 0 +816 0 0 +817 1 0 +818 0 0 +819 0 0 +820 0 0 +821 0 0 +822 2 0 +823 0 0 +824 0 0 +825 1 0 +826 0 0 +827 1 0 +828 1 0 +829 1 0 +830 0 0 +831 1 0 +832 0 0 +833 1 0 +834 1 0 +835 0 0 +836 0 0 +837 0 0 +838 1 0 +839 0 0 +840 0 0 +841 1 0 +842 1 0 +843 0 0 +844 0 0 +845 0 0 +846 0 0 +847 0 0 +848 2 0 +849 1 0 +850 1 0 +851 1 0 +852 3 0 +853 0 0 +854 0 0 +855 1 0 +856 0 0 +857 1 0 +858 1 0 +859 0 0 +860 1 0 +861 1 0 +862 2 0 + diff --git a/tests/expected/dna/test.idxstats.txt b/tests/expected/dna/test.idxstats.txt new file mode 100644 index 00000000..f56aa9fe --- /dev/null +++ b/tests/expected/dna/test.idxstats.txt @@ -0,0 +1,2 @@ +chr22 40001 5642 0 +* 0 0 2 diff --git a/tests/expected/dna/test.insert_size_metrics.txt b/tests/expected/dna/test.insert_size_metrics.txt new file mode 100644 index 00000000..b99c4e54 --- /dev/null +++ b/tests/expected/dna/test.insert_size_metrics.txt @@ -0,0 +1,178 @@ +## METRICS CLASS picard.analysis.InsertSizeMetrics +MEDIAN_INSERT_SIZE MODE_INSERT_SIZE MEDIAN_ABSOLUTE_DEVIATION MIN_INSERT_SIZE MAX_INSERT_SIZE MEAN_INSERT_SIZE STANDARD_DEVIATION READ_PAIRS PAIR_ORIENTATION WIDTH_OF_10_PERCENT WIDTH_OF_20_PERCENT WIDTH_OF_30_PERCENT WIDTH_OF_40_PERCENT WIDTH_OF_50_PERCENT WIDTH_OF_60_PERCENT WIDTH_OF_70_PERCENT WIDTH_OF_80_PERCENT WIDTH_OF_90_PERCENT WIDTH_OF_95_PERCENT WIDTH_OF_99_PERCENT SAMPLE LIBRARY READ_GROUP +122 96 23 32 300 124.442269 32.720214 1992 FR 9 19 27 37 47 57 69 83 103 127 181 + +## HISTOGRAM java.lang.Integer +insert_size All_Reads.fr_count +32 1 +41 1 +49 2 +51 1 +52 2 +54 1 +58 1 +59 2 +60 1 +61 4 +62 1 +63 4 +65 4 +66 2 +67 5 +68 3 +69 3 +70 6 +71 7 +72 6 +73 6 +74 3 +75 7 +76 7 +77 15 +78 11 +79 9 +80 14 +81 18 +82 15 +83 15 +84 17 +85 9 +86 16 +87 23 +88 24 +89 16 +90 12 +91 18 +92 21 +93 16 +94 18 +95 31 +96 37 +97 24 +98 22 +99 20 +100 33 +101 18 +102 20 +103 16 +104 24 +105 22 +106 22 +107 23 +108 22 +109 19 +110 28 +111 19 +112 17 +113 24 +114 20 +115 35 +116 22 +117 22 +118 25 +119 26 +120 11 +121 26 +122 21 +123 19 +124 27 +125 24 +126 21 +127 22 +128 22 +129 21 +130 24 +131 31 +132 20 +133 11 +134 19 +135 23 +136 21 +137 18 +138 23 +139 22 +140 21 +141 28 +142 18 +143 14 +144 17 +145 18 +146 15 +147 19 +148 14 +149 12 +150 13 +151 13 +152 18 +153 18 +154 11 +155 13 +156 14 +157 19 +158 11 +159 10 +160 8 +161 12 +162 11 +163 11 +164 6 +165 6 +166 14 +167 6 +168 12 +169 11 +170 12 +171 10 +172 10 +173 13 +174 6 +175 6 +176 5 +177 5 +178 5 +179 6 +180 2 +181 6 +182 5 +183 2 +184 7 +185 8 +186 3 +187 4 +188 1 +189 4 +190 5 +191 7 +192 5 +193 2 +194 4 +195 1 +196 1 +197 2 +198 1 +199 3 +200 5 +201 1 +202 4 +203 3 +204 3 +205 2 +206 3 +207 2 +209 1 +210 1 +212 1 +213 2 +214 3 +215 1 +216 2 +218 1 +220 1 +221 2 +223 1 +224 1 +231 1 +236 1 +239 1 +300 1 + diff --git a/tests/expected/dna/test.mosdepth.global.dist.txt b/tests/expected/dna/test.mosdepth.global.dist.txt new file mode 100644 index 00000000..2299da21 --- /dev/null +++ b/tests/expected/dna/test.mosdepth.global.dist.txt @@ -0,0 +1,1094 @@ +chr22 866 0.00 +chr22 865 0.00 +chr22 863 0.00 +chr22 862 0.00 +chr22 860 0.00 +chr22 859 0.00 +chr22 858 0.00 +chr22 857 0.00 +chr22 854 0.00 +chr22 851 0.00 +chr22 848 0.00 +chr22 846 0.00 +chr22 842 0.00 +chr22 840 0.00 +chr22 837 0.00 +chr22 833 0.00 +chr22 832 0.00 +chr22 831 0.00 +chr22 830 0.00 +chr22 827 0.00 +chr22 825 0.00 +chr22 822 0.00 +chr22 817 0.00 +chr22 816 0.00 +chr22 814 0.00 +chr22 812 0.00 +chr22 811 0.00 +chr22 808 0.00 +chr22 802 0.00 +chr22 801 0.00 +chr22 799 0.00 +chr22 798 0.00 +chr22 795 0.00 +chr22 792 0.00 +chr22 790 0.00 +chr22 788 0.00 +chr22 786 0.00 +chr22 784 0.00 +chr22 779 0.00 +chr22 777 0.00 +chr22 775 0.00 +chr22 773 0.00 +chr22 768 0.00 +chr22 764 0.00 +chr22 761 0.00 +chr22 757 0.00 +chr22 754 0.00 +chr22 750 0.00 +chr22 747 0.00 +chr22 745 0.00 +chr22 743 0.00 +chr22 738 0.00 +chr22 734 0.00 +chr22 733 0.00 +chr22 726 0.00 +chr22 724 0.00 +chr22 720 0.00 +chr22 716 0.00 +chr22 711 0.00 +chr22 706 0.00 +chr22 702 0.00 +chr22 695 0.00 +chr22 693 0.00 +chr22 690 0.00 +chr22 689 0.00 +chr22 684 0.00 +chr22 683 0.00 +chr22 677 0.00 +chr22 675 0.00 +chr22 668 0.00 +chr22 667 0.00 +chr22 666 0.00 +chr22 662 0.00 +chr22 661 0.00 +chr22 659 0.00 +chr22 658 0.00 +chr22 657 0.00 +chr22 656 0.00 +chr22 654 0.00 +chr22 652 0.00 +chr22 651 0.00 +chr22 649 0.00 +chr22 647 0.00 +chr22 645 0.00 +chr22 644 0.00 +chr22 641 0.00 +chr22 640 0.00 +chr22 639 0.00 +chr22 638 0.00 +chr22 637 0.00 +chr22 636 0.00 +chr22 635 0.00 +chr22 634 0.00 +chr22 633 0.00 +chr22 631 0.00 +chr22 628 0.00 +chr22 627 0.00 +chr22 626 0.00 +chr22 624 0.00 +chr22 623 0.00 +chr22 622 0.00 +chr22 618 0.00 +chr22 617 0.00 +chr22 616 0.00 +chr22 614 0.00 +chr22 613 0.00 +chr22 612 0.00 +chr22 611 0.00 +chr22 609 0.00 +chr22 608 0.00 +chr22 605 0.00 +chr22 604 0.00 +chr22 603 0.00 +chr22 602 0.00 +chr22 601 0.00 +chr22 600 0.00 +chr22 598 0.00 +chr22 596 0.00 +chr22 595 0.00 +chr22 594 0.00 +chr22 592 0.00 +chr22 590 0.00 +chr22 589 0.00 +chr22 588 0.00 +chr22 587 0.00 +chr22 583 0.00 +chr22 582 0.00 +chr22 579 0.00 +chr22 577 0.00 +chr22 576 0.00 +chr22 575 0.00 +chr22 574 0.00 +chr22 571 0.00 +chr22 565 0.00 +chr22 562 0.00 +chr22 561 0.00 +chr22 557 0.00 +chr22 555 0.00 +chr22 554 0.00 +chr22 552 0.00 +chr22 550 0.00 +chr22 549 0.00 +chr22 547 0.00 +chr22 545 0.00 +chr22 540 0.00 +chr22 539 0.00 +chr22 536 0.00 +chr22 532 0.00 +chr22 531 0.00 +chr22 527 0.00 +chr22 526 0.01 +chr22 520 0.01 +chr22 518 0.01 +chr22 517 0.01 +chr22 516 0.01 +chr22 514 0.01 +chr22 512 0.01 +chr22 506 0.01 +chr22 505 0.01 +chr22 503 0.01 +chr22 500 0.01 +chr22 499 0.01 +chr22 496 0.01 +chr22 494 0.01 +chr22 491 0.01 +chr22 490 0.01 +chr22 489 0.01 +chr22 488 0.01 +chr22 485 0.01 +chr22 483 0.01 +chr22 482 0.01 +chr22 481 0.01 +chr22 477 0.01 +chr22 474 0.01 +chr22 472 0.01 +chr22 469 0.01 +chr22 468 0.01 +chr22 466 0.01 +chr22 461 0.01 +chr22 460 0.01 +chr22 457 0.01 +chr22 455 0.01 +chr22 453 0.01 +chr22 451 0.01 +chr22 448 0.01 +chr22 445 0.01 +chr22 444 0.01 +chr22 441 0.01 +chr22 439 0.01 +chr22 437 0.01 +chr22 435 0.01 +chr22 434 0.01 +chr22 431 0.01 +chr22 426 0.01 +chr22 425 0.01 +chr22 422 0.01 +chr22 419 0.01 +chr22 418 0.01 +chr22 414 0.01 +chr22 413 0.01 +chr22 410 0.01 +chr22 408 0.01 +chr22 406 0.01 +chr22 405 0.01 +chr22 401 0.01 +chr22 397 0.01 +chr22 395 0.01 +chr22 394 0.01 +chr22 392 0.01 +chr22 391 0.01 +chr22 387 0.01 +chr22 385 0.01 +chr22 384 0.01 +chr22 383 0.01 +chr22 381 0.01 +chr22 379 0.01 +chr22 376 0.01 +chr22 374 0.01 +chr22 373 0.01 +chr22 368 0.01 +chr22 366 0.01 +chr22 365 0.01 +chr22 361 0.01 +chr22 360 0.01 +chr22 359 0.01 +chr22 353 0.01 +chr22 352 0.01 +chr22 350 0.01 +chr22 345 0.01 +chr22 344 0.01 +chr22 343 0.01 +chr22 338 0.01 +chr22 337 0.01 +chr22 335 0.01 +chr22 334 0.01 +chr22 330 0.01 +chr22 328 0.01 +chr22 327 0.01 +chr22 324 0.01 +chr22 320 0.01 +chr22 318 0.01 +chr22 315 0.01 +chr22 311 0.01 +chr22 310 0.01 +chr22 307 0.01 +chr22 305 0.01 +chr22 300 0.01 +chr22 299 0.01 +chr22 298 0.01 +chr22 297 0.01 +chr22 296 0.01 +chr22 295 0.01 +chr22 294 0.01 +chr22 293 0.01 +chr22 292 0.01 +chr22 291 0.01 +chr22 290 0.01 +chr22 289 0.01 +chr22 288 0.01 +chr22 287 0.01 +chr22 286 0.01 +chr22 285 0.01 +chr22 284 0.01 +chr22 283 0.01 +chr22 282 0.01 +chr22 281 0.01 +chr22 280 0.01 +chr22 279 0.01 +chr22 278 0.01 +chr22 277 0.01 +chr22 276 0.01 +chr22 275 0.01 +chr22 274 0.01 +chr22 273 0.01 +chr22 272 0.01 +chr22 271 0.01 +chr22 270 0.01 +chr22 269 0.01 +chr22 268 0.01 +chr22 267 0.01 +chr22 266 0.01 +chr22 265 0.01 +chr22 264 0.01 +chr22 263 0.01 +chr22 262 0.01 +chr22 261 0.01 +chr22 260 0.01 +chr22 259 0.01 +chr22 258 0.01 +chr22 257 0.01 +chr22 256 0.01 +chr22 255 0.01 +chr22 254 0.01 +chr22 253 0.01 +chr22 252 0.01 +chr22 251 0.01 +chr22 250 0.01 +chr22 249 0.01 +chr22 248 0.01 +chr22 247 0.01 +chr22 246 0.01 +chr22 245 0.01 +chr22 244 0.01 +chr22 243 0.01 +chr22 242 0.01 +chr22 241 0.01 +chr22 240 0.01 +chr22 239 0.01 +chr22 238 0.01 +chr22 237 0.01 +chr22 236 0.01 +chr22 235 0.01 +chr22 234 0.01 +chr22 233 0.01 +chr22 232 0.01 +chr22 231 0.01 +chr22 230 0.01 +chr22 229 0.01 +chr22 228 0.01 +chr22 227 0.01 +chr22 226 0.01 +chr22 225 0.01 +chr22 224 0.01 +chr22 223 0.01 +chr22 222 0.01 +chr22 221 0.01 +chr22 220 0.01 +chr22 219 0.01 +chr22 218 0.01 +chr22 217 0.01 +chr22 216 0.01 +chr22 215 0.01 +chr22 214 0.01 +chr22 213 0.01 +chr22 212 0.01 +chr22 211 0.01 +chr22 210 0.01 +chr22 209 0.01 +chr22 208 0.01 +chr22 207 0.01 +chr22 206 0.01 +chr22 205 0.01 +chr22 204 0.01 +chr22 203 0.01 +chr22 202 0.01 +chr22 201 0.01 +chr22 200 0.01 +chr22 199 0.01 +chr22 198 0.01 +chr22 197 0.01 +chr22 196 0.01 +chr22 195 0.01 +chr22 194 0.01 +chr22 193 0.01 +chr22 192 0.01 +chr22 191 0.01 +chr22 190 0.01 +chr22 189 0.01 +chr22 188 0.01 +chr22 187 0.01 +chr22 186 0.01 +chr22 185 0.01 +chr22 184 0.01 +chr22 183 0.01 +chr22 182 0.01 +chr22 181 0.01 +chr22 180 0.01 +chr22 179 0.01 +chr22 178 0.01 +chr22 177 0.01 +chr22 176 0.01 +chr22 175 0.01 +chr22 174 0.01 +chr22 173 0.01 +chr22 172 0.01 +chr22 171 0.01 +chr22 170 0.01 +chr22 169 0.01 +chr22 168 0.01 +chr22 167 0.01 +chr22 166 0.01 +chr22 165 0.01 +chr22 164 0.01 +chr22 163 0.01 +chr22 162 0.01 +chr22 161 0.01 +chr22 160 0.01 +chr22 159 0.01 +chr22 158 0.01 +chr22 157 0.01 +chr22 156 0.01 +chr22 155 0.01 +chr22 154 0.01 +chr22 153 0.01 +chr22 152 0.01 +chr22 151 0.01 +chr22 150 0.01 +chr22 149 0.01 +chr22 148 0.01 +chr22 147 0.01 +chr22 146 0.01 +chr22 145 0.01 +chr22 144 0.01 +chr22 143 0.01 +chr22 142 0.01 +chr22 141 0.01 +chr22 140 0.01 +chr22 139 0.01 +chr22 138 0.01 +chr22 137 0.01 +chr22 136 0.01 +chr22 135 0.01 +chr22 134 0.01 +chr22 133 0.01 +chr22 132 0.01 +chr22 131 0.01 +chr22 130 0.01 +chr22 129 0.01 +chr22 128 0.01 +chr22 127 0.01 +chr22 126 0.01 +chr22 125 0.01 +chr22 124 0.01 +chr22 123 0.01 +chr22 122 0.01 +chr22 121 0.01 +chr22 120 0.01 +chr22 119 0.01 +chr22 118 0.01 +chr22 117 0.01 +chr22 116 0.01 +chr22 115 0.01 +chr22 114 0.01 +chr22 113 0.01 +chr22 112 0.01 +chr22 111 0.01 +chr22 110 0.01 +chr22 109 0.01 +chr22 108 0.01 +chr22 107 0.01 +chr22 106 0.01 +chr22 105 0.01 +chr22 104 0.01 +chr22 103 0.02 +chr22 102 0.02 +chr22 101 0.02 +chr22 100 0.02 +chr22 99 0.02 +chr22 98 0.02 +chr22 97 0.02 +chr22 96 0.02 +chr22 95 0.02 +chr22 94 0.02 +chr22 93 0.02 +chr22 92 0.02 +chr22 91 0.02 +chr22 90 0.02 +chr22 89 0.02 +chr22 88 0.02 +chr22 87 0.02 +chr22 86 0.02 +chr22 85 0.02 +chr22 84 0.02 +chr22 83 0.02 +chr22 82 0.02 +chr22 81 0.02 +chr22 80 0.02 +chr22 79 0.02 +chr22 78 0.02 +chr22 77 0.02 +chr22 76 0.02 +chr22 75 0.02 +chr22 74 0.02 +chr22 73 0.02 +chr22 72 0.02 +chr22 71 0.02 +chr22 70 0.02 +chr22 69 0.02 +chr22 68 0.02 +chr22 67 0.02 +chr22 66 0.02 +chr22 65 0.02 +chr22 64 0.02 +chr22 63 0.02 +chr22 62 0.02 +chr22 61 0.02 +chr22 60 0.02 +chr22 59 0.02 +chr22 58 0.02 +chr22 57 0.02 +chr22 56 0.02 +chr22 55 0.02 +chr22 54 0.02 +chr22 53 0.02 +chr22 52 0.02 +chr22 51 0.02 +chr22 50 0.02 +chr22 49 0.02 +chr22 48 0.02 +chr22 47 0.02 +chr22 46 0.02 +chr22 45 0.02 +chr22 44 0.02 +chr22 43 0.02 +chr22 42 0.02 +chr22 41 0.02 +chr22 40 0.02 +chr22 39 0.02 +chr22 38 0.02 +chr22 37 0.02 +chr22 36 0.02 +chr22 35 0.02 +chr22 34 0.02 +chr22 33 0.02 +chr22 32 0.02 +chr22 31 0.02 +chr22 30 0.02 +chr22 29 0.02 +chr22 28 0.02 +chr22 27 0.02 +chr22 26 0.02 +chr22 25 0.02 +chr22 24 0.02 +chr22 23 0.02 +chr22 22 0.02 +chr22 21 0.02 +chr22 20 0.02 +chr22 19 0.02 +chr22 18 0.02 +chr22 17 0.02 +chr22 16 0.02 +chr22 15 0.02 +chr22 14 0.02 +chr22 13 0.02 +chr22 12 0.02 +chr22 11 0.02 +chr22 10 0.02 +chr22 9 0.02 +chr22 8 0.02 +chr22 7 0.02 +chr22 6 0.02 +chr22 5 0.02 +chr22 4 0.03 +chr22 3 0.03 +chr22 2 0.03 +chr22 1 0.03 +chr22 0 1.00 +total 866 0.00 +total 865 0.00 +total 863 0.00 +total 862 0.00 +total 860 0.00 +total 859 0.00 +total 858 0.00 +total 857 0.00 +total 854 0.00 +total 851 0.00 +total 848 0.00 +total 846 0.00 +total 842 0.00 +total 840 0.00 +total 837 0.00 +total 833 0.00 +total 832 0.00 +total 831 0.00 +total 830 0.00 +total 827 0.00 +total 825 0.00 +total 822 0.00 +total 817 0.00 +total 816 0.00 +total 814 0.00 +total 812 0.00 +total 811 0.00 +total 808 0.00 +total 802 0.00 +total 801 0.00 +total 799 0.00 +total 798 0.00 +total 795 0.00 +total 792 0.00 +total 790 0.00 +total 788 0.00 +total 786 0.00 +total 784 0.00 +total 779 0.00 +total 777 0.00 +total 775 0.00 +total 773 0.00 +total 768 0.00 +total 764 0.00 +total 761 0.00 +total 757 0.00 +total 754 0.00 +total 750 0.00 +total 747 0.00 +total 745 0.00 +total 743 0.00 +total 738 0.00 +total 734 0.00 +total 733 0.00 +total 726 0.00 +total 724 0.00 +total 720 0.00 +total 716 0.00 +total 711 0.00 +total 706 0.00 +total 702 0.00 +total 695 0.00 +total 693 0.00 +total 690 0.00 +total 689 0.00 +total 684 0.00 +total 683 0.00 +total 677 0.00 +total 675 0.00 +total 668 0.00 +total 667 0.00 +total 666 0.00 +total 662 0.00 +total 661 0.00 +total 659 0.00 +total 658 0.00 +total 657 0.00 +total 656 0.00 +total 654 0.00 +total 652 0.00 +total 651 0.00 +total 649 0.00 +total 647 0.00 +total 645 0.00 +total 644 0.00 +total 641 0.00 +total 640 0.00 +total 639 0.00 +total 638 0.00 +total 637 0.00 +total 636 0.00 +total 635 0.00 +total 634 0.00 +total 633 0.00 +total 631 0.00 +total 628 0.00 +total 627 0.00 +total 626 0.00 +total 624 0.00 +total 623 0.00 +total 622 0.00 +total 618 0.00 +total 617 0.00 +total 616 0.00 +total 614 0.00 +total 613 0.00 +total 612 0.00 +total 611 0.00 +total 609 0.00 +total 608 0.00 +total 605 0.00 +total 604 0.00 +total 603 0.00 +total 602 0.00 +total 601 0.00 +total 600 0.00 +total 598 0.00 +total 596 0.00 +total 595 0.00 +total 594 0.00 +total 592 0.00 +total 590 0.00 +total 589 0.00 +total 588 0.00 +total 587 0.00 +total 583 0.00 +total 582 0.00 +total 579 0.00 +total 577 0.00 +total 576 0.00 +total 575 0.00 +total 574 0.00 +total 571 0.00 +total 565 0.00 +total 562 0.00 +total 561 0.00 +total 557 0.00 +total 555 0.00 +total 554 0.00 +total 552 0.00 +total 550 0.00 +total 549 0.00 +total 547 0.00 +total 545 0.00 +total 540 0.00 +total 539 0.00 +total 536 0.00 +total 532 0.00 +total 531 0.00 +total 527 0.00 +total 526 0.01 +total 520 0.01 +total 518 0.01 +total 517 0.01 +total 516 0.01 +total 514 0.01 +total 512 0.01 +total 506 0.01 +total 505 0.01 +total 503 0.01 +total 500 0.01 +total 499 0.01 +total 496 0.01 +total 494 0.01 +total 491 0.01 +total 490 0.01 +total 489 0.01 +total 488 0.01 +total 485 0.01 +total 483 0.01 +total 482 0.01 +total 481 0.01 +total 477 0.01 +total 474 0.01 +total 472 0.01 +total 469 0.01 +total 468 0.01 +total 466 0.01 +total 461 0.01 +total 460 0.01 +total 457 0.01 +total 455 0.01 +total 453 0.01 +total 451 0.01 +total 448 0.01 +total 445 0.01 +total 444 0.01 +total 441 0.01 +total 439 0.01 +total 437 0.01 +total 435 0.01 +total 434 0.01 +total 431 0.01 +total 426 0.01 +total 425 0.01 +total 422 0.01 +total 419 0.01 +total 418 0.01 +total 414 0.01 +total 413 0.01 +total 410 0.01 +total 408 0.01 +total 406 0.01 +total 405 0.01 +total 401 0.01 +total 397 0.01 +total 395 0.01 +total 394 0.01 +total 392 0.01 +total 391 0.01 +total 387 0.01 +total 385 0.01 +total 384 0.01 +total 383 0.01 +total 381 0.01 +total 379 0.01 +total 376 0.01 +total 374 0.01 +total 373 0.01 +total 368 0.01 +total 366 0.01 +total 365 0.01 +total 361 0.01 +total 360 0.01 +total 359 0.01 +total 353 0.01 +total 352 0.01 +total 350 0.01 +total 345 0.01 +total 344 0.01 +total 343 0.01 +total 338 0.01 +total 337 0.01 +total 335 0.01 +total 334 0.01 +total 330 0.01 +total 328 0.01 +total 327 0.01 +total 324 0.01 +total 320 0.01 +total 318 0.01 +total 315 0.01 +total 311 0.01 +total 310 0.01 +total 307 0.01 +total 305 0.01 +total 300 0.01 +total 299 0.01 +total 298 0.01 +total 297 0.01 +total 296 0.01 +total 295 0.01 +total 294 0.01 +total 293 0.01 +total 292 0.01 +total 291 0.01 +total 290 0.01 +total 289 0.01 +total 288 0.01 +total 287 0.01 +total 286 0.01 +total 285 0.01 +total 284 0.01 +total 283 0.01 +total 282 0.01 +total 281 0.01 +total 280 0.01 +total 279 0.01 +total 278 0.01 +total 277 0.01 +total 276 0.01 +total 275 0.01 +total 274 0.01 +total 273 0.01 +total 272 0.01 +total 271 0.01 +total 270 0.01 +total 269 0.01 +total 268 0.01 +total 267 0.01 +total 266 0.01 +total 265 0.01 +total 264 0.01 +total 263 0.01 +total 262 0.01 +total 261 0.01 +total 260 0.01 +total 259 0.01 +total 258 0.01 +total 257 0.01 +total 256 0.01 +total 255 0.01 +total 254 0.01 +total 253 0.01 +total 252 0.01 +total 251 0.01 +total 250 0.01 +total 249 0.01 +total 248 0.01 +total 247 0.01 +total 246 0.01 +total 245 0.01 +total 244 0.01 +total 243 0.01 +total 242 0.01 +total 241 0.01 +total 240 0.01 +total 239 0.01 +total 238 0.01 +total 237 0.01 +total 236 0.01 +total 235 0.01 +total 234 0.01 +total 233 0.01 +total 232 0.01 +total 231 0.01 +total 230 0.01 +total 229 0.01 +total 228 0.01 +total 227 0.01 +total 226 0.01 +total 225 0.01 +total 224 0.01 +total 223 0.01 +total 222 0.01 +total 221 0.01 +total 220 0.01 +total 219 0.01 +total 218 0.01 +total 217 0.01 +total 216 0.01 +total 215 0.01 +total 214 0.01 +total 213 0.01 +total 212 0.01 +total 211 0.01 +total 210 0.01 +total 209 0.01 +total 208 0.01 +total 207 0.01 +total 206 0.01 +total 205 0.01 +total 204 0.01 +total 203 0.01 +total 202 0.01 +total 201 0.01 +total 200 0.01 +total 199 0.01 +total 198 0.01 +total 197 0.01 +total 196 0.01 +total 195 0.01 +total 194 0.01 +total 193 0.01 +total 192 0.01 +total 191 0.01 +total 190 0.01 +total 189 0.01 +total 188 0.01 +total 187 0.01 +total 186 0.01 +total 185 0.01 +total 184 0.01 +total 183 0.01 +total 182 0.01 +total 181 0.01 +total 180 0.01 +total 179 0.01 +total 178 0.01 +total 177 0.01 +total 176 0.01 +total 175 0.01 +total 174 0.01 +total 173 0.01 +total 172 0.01 +total 171 0.01 +total 170 0.01 +total 169 0.01 +total 168 0.01 +total 167 0.01 +total 166 0.01 +total 165 0.01 +total 164 0.01 +total 163 0.01 +total 162 0.01 +total 161 0.01 +total 160 0.01 +total 159 0.01 +total 158 0.01 +total 157 0.01 +total 156 0.01 +total 155 0.01 +total 154 0.01 +total 153 0.01 +total 152 0.01 +total 151 0.01 +total 150 0.01 +total 149 0.01 +total 148 0.01 +total 147 0.01 +total 146 0.01 +total 145 0.01 +total 144 0.01 +total 143 0.01 +total 142 0.01 +total 141 0.01 +total 140 0.01 +total 139 0.01 +total 138 0.01 +total 137 0.01 +total 136 0.01 +total 135 0.01 +total 134 0.01 +total 133 0.01 +total 132 0.01 +total 131 0.01 +total 130 0.01 +total 129 0.01 +total 128 0.01 +total 127 0.01 +total 126 0.01 +total 125 0.01 +total 124 0.01 +total 123 0.01 +total 122 0.01 +total 121 0.01 +total 120 0.01 +total 119 0.01 +total 118 0.01 +total 117 0.01 +total 116 0.01 +total 115 0.01 +total 114 0.01 +total 113 0.01 +total 112 0.01 +total 111 0.01 +total 110 0.01 +total 109 0.01 +total 108 0.01 +total 107 0.01 +total 106 0.01 +total 105 0.01 +total 104 0.01 +total 103 0.02 +total 102 0.02 +total 101 0.02 +total 100 0.02 +total 99 0.02 +total 98 0.02 +total 97 0.02 +total 96 0.02 +total 95 0.02 +total 94 0.02 +total 93 0.02 +total 92 0.02 +total 91 0.02 +total 90 0.02 +total 89 0.02 +total 88 0.02 +total 87 0.02 +total 86 0.02 +total 85 0.02 +total 84 0.02 +total 83 0.02 +total 82 0.02 +total 81 0.02 +total 80 0.02 +total 79 0.02 +total 78 0.02 +total 77 0.02 +total 76 0.02 +total 75 0.02 +total 74 0.02 +total 73 0.02 +total 72 0.02 +total 71 0.02 +total 70 0.02 +total 69 0.02 +total 68 0.02 +total 67 0.02 +total 66 0.02 +total 65 0.02 +total 64 0.02 +total 63 0.02 +total 62 0.02 +total 61 0.02 +total 60 0.02 +total 59 0.02 +total 58 0.02 +total 57 0.02 +total 56 0.02 +total 55 0.02 +total 54 0.02 +total 53 0.02 +total 52 0.02 +total 51 0.02 +total 50 0.02 +total 49 0.02 +total 48 0.02 +total 47 0.02 +total 46 0.02 +total 45 0.02 +total 44 0.02 +total 43 0.02 +total 42 0.02 +total 41 0.02 +total 40 0.02 +total 39 0.02 +total 38 0.02 +total 37 0.02 +total 36 0.02 +total 35 0.02 +total 34 0.02 +total 33 0.02 +total 32 0.02 +total 31 0.02 +total 30 0.02 +total 29 0.02 +total 28 0.02 +total 27 0.02 +total 26 0.02 +total 25 0.02 +total 24 0.02 +total 23 0.02 +total 22 0.02 +total 21 0.02 +total 20 0.02 +total 19 0.02 +total 18 0.02 +total 17 0.02 +total 16 0.02 +total 15 0.02 +total 14 0.02 +total 13 0.02 +total 12 0.02 +total 11 0.02 +total 10 0.02 +total 9 0.02 +total 8 0.02 +total 7 0.02 +total 6 0.02 +total 5 0.02 +total 4 0.03 +total 3 0.03 +total 2 0.03 +total 1 0.03 +total 0 1.00 diff --git a/tests/expected/dna/test.mosdepth.region.dist.txt b/tests/expected/dna/test.mosdepth.region.dist.txt new file mode 100644 index 00000000..6e1d6dc7 --- /dev/null +++ b/tests/expected/dna/test.mosdepth.region.dist.txt @@ -0,0 +1,410 @@ +chr22 204 0.01 +chr22 203 0.01 +chr22 202 0.01 +chr22 201 0.01 +chr22 200 0.01 +chr22 199 0.01 +chr22 198 0.01 +chr22 197 0.01 +chr22 196 0.01 +chr22 195 0.01 +chr22 194 0.01 +chr22 193 0.01 +chr22 192 0.01 +chr22 191 0.01 +chr22 190 0.01 +chr22 189 0.01 +chr22 188 0.01 +chr22 187 0.01 +chr22 186 0.01 +chr22 185 0.01 +chr22 184 0.01 +chr22 183 0.01 +chr22 182 0.01 +chr22 181 0.01 +chr22 180 0.01 +chr22 179 0.01 +chr22 178 0.01 +chr22 177 0.01 +chr22 176 0.01 +chr22 175 0.01 +chr22 174 0.01 +chr22 173 0.01 +chr22 172 0.01 +chr22 171 0.01 +chr22 170 0.01 +chr22 169 0.01 +chr22 168 0.01 +chr22 167 0.01 +chr22 166 0.01 +chr22 165 0.01 +chr22 164 0.01 +chr22 163 0.01 +chr22 162 0.01 +chr22 161 0.01 +chr22 160 0.01 +chr22 159 0.01 +chr22 158 0.01 +chr22 157 0.01 +chr22 156 0.01 +chr22 155 0.01 +chr22 154 0.01 +chr22 153 0.01 +chr22 152 0.01 +chr22 151 0.02 +chr22 150 0.02 +chr22 149 0.02 +chr22 148 0.02 +chr22 147 0.02 +chr22 146 0.02 +chr22 145 0.02 +chr22 144 0.02 +chr22 143 0.02 +chr22 142 0.02 +chr22 141 0.02 +chr22 140 0.02 +chr22 139 0.02 +chr22 138 0.02 +chr22 137 0.02 +chr22 136 0.02 +chr22 135 0.02 +chr22 134 0.02 +chr22 133 0.02 +chr22 132 0.02 +chr22 131 0.02 +chr22 130 0.02 +chr22 129 0.02 +chr22 128 0.02 +chr22 127 0.02 +chr22 126 0.02 +chr22 125 0.02 +chr22 124 0.02 +chr22 123 0.02 +chr22 122 0.02 +chr22 121 0.02 +chr22 120 0.02 +chr22 119 0.02 +chr22 118 0.02 +chr22 117 0.02 +chr22 116 0.02 +chr22 115 0.02 +chr22 114 0.02 +chr22 113 0.02 +chr22 112 0.02 +chr22 111 0.02 +chr22 110 0.02 +chr22 109 0.02 +chr22 108 0.02 +chr22 107 0.02 +chr22 106 0.02 +chr22 105 0.02 +chr22 104 0.02 +chr22 103 0.02 +chr22 102 0.02 +chr22 101 0.02 +chr22 100 0.02 +chr22 99 0.02 +chr22 98 0.02 +chr22 97 0.02 +chr22 96 0.02 +chr22 95 0.02 +chr22 94 0.02 +chr22 93 0.02 +chr22 92 0.02 +chr22 91 0.02 +chr22 90 0.02 +chr22 89 0.02 +chr22 88 0.02 +chr22 87 0.02 +chr22 86 0.02 +chr22 85 0.02 +chr22 84 0.02 +chr22 83 0.02 +chr22 82 0.02 +chr22 81 0.02 +chr22 80 0.04 +chr22 79 0.04 +chr22 78 0.04 +chr22 77 0.04 +chr22 76 0.04 +chr22 75 0.04 +chr22 74 0.04 +chr22 73 0.04 +chr22 72 0.04 +chr22 71 0.04 +chr22 70 0.04 +chr22 69 0.04 +chr22 68 0.04 +chr22 67 0.04 +chr22 66 0.04 +chr22 65 0.04 +chr22 64 0.04 +chr22 63 0.04 +chr22 62 0.04 +chr22 61 0.04 +chr22 60 0.04 +chr22 59 0.04 +chr22 58 0.04 +chr22 57 0.04 +chr22 56 0.04 +chr22 55 0.04 +chr22 54 0.04 +chr22 53 0.04 +chr22 52 0.04 +chr22 51 0.04 +chr22 50 0.04 +chr22 49 0.05 +chr22 48 0.05 +chr22 47 0.05 +chr22 46 0.05 +chr22 45 0.05 +chr22 44 0.05 +chr22 43 0.05 +chr22 42 0.05 +chr22 41 0.05 +chr22 40 0.05 +chr22 39 0.05 +chr22 38 0.05 +chr22 37 0.05 +chr22 36 0.05 +chr22 35 0.05 +chr22 34 0.05 +chr22 33 0.05 +chr22 32 0.05 +chr22 31 0.05 +chr22 30 0.05 +chr22 29 0.05 +chr22 28 0.05 +chr22 27 0.05 +chr22 26 0.05 +chr22 25 0.05 +chr22 24 0.05 +chr22 23 0.05 +chr22 22 0.05 +chr22 21 0.05 +chr22 20 0.05 +chr22 19 0.05 +chr22 18 0.05 +chr22 17 0.05 +chr22 16 0.05 +chr22 15 0.05 +chr22 14 0.05 +chr22 13 0.05 +chr22 12 0.05 +chr22 11 0.05 +chr22 10 0.05 +chr22 9 0.05 +chr22 8 0.05 +chr22 7 0.06 +chr22 6 0.06 +chr22 5 0.07 +chr22 4 0.07 +chr22 3 0.07 +chr22 2 0.07 +chr22 1 0.07 +chr22 0 1.00 +total 204 0.01 +total 203 0.01 +total 202 0.01 +total 201 0.01 +total 200 0.01 +total 199 0.01 +total 198 0.01 +total 197 0.01 +total 196 0.01 +total 195 0.01 +total 194 0.01 +total 193 0.01 +total 192 0.01 +total 191 0.01 +total 190 0.01 +total 189 0.01 +total 188 0.01 +total 187 0.01 +total 186 0.01 +total 185 0.01 +total 184 0.01 +total 183 0.01 +total 182 0.01 +total 181 0.01 +total 180 0.01 +total 179 0.01 +total 178 0.01 +total 177 0.01 +total 176 0.01 +total 175 0.01 +total 174 0.01 +total 173 0.01 +total 172 0.01 +total 171 0.01 +total 170 0.01 +total 169 0.01 +total 168 0.01 +total 167 0.01 +total 166 0.01 +total 165 0.01 +total 164 0.01 +total 163 0.01 +total 162 0.01 +total 161 0.01 +total 160 0.01 +total 159 0.01 +total 158 0.01 +total 157 0.01 +total 156 0.01 +total 155 0.01 +total 154 0.01 +total 153 0.01 +total 152 0.01 +total 151 0.02 +total 150 0.02 +total 149 0.02 +total 148 0.02 +total 147 0.02 +total 146 0.02 +total 145 0.02 +total 144 0.02 +total 143 0.02 +total 142 0.02 +total 141 0.02 +total 140 0.02 +total 139 0.02 +total 138 0.02 +total 137 0.02 +total 136 0.02 +total 135 0.02 +total 134 0.02 +total 133 0.02 +total 132 0.02 +total 131 0.02 +total 130 0.02 +total 129 0.02 +total 128 0.02 +total 127 0.02 +total 126 0.02 +total 125 0.02 +total 124 0.02 +total 123 0.02 +total 122 0.02 +total 121 0.02 +total 120 0.02 +total 119 0.02 +total 118 0.02 +total 117 0.02 +total 116 0.02 +total 115 0.02 +total 114 0.02 +total 113 0.02 +total 112 0.02 +total 111 0.02 +total 110 0.02 +total 109 0.02 +total 108 0.02 +total 107 0.02 +total 106 0.02 +total 105 0.02 +total 104 0.02 +total 103 0.02 +total 102 0.02 +total 101 0.02 +total 100 0.02 +total 99 0.02 +total 98 0.02 +total 97 0.02 +total 96 0.02 +total 95 0.02 +total 94 0.02 +total 93 0.02 +total 92 0.02 +total 91 0.02 +total 90 0.02 +total 89 0.02 +total 88 0.02 +total 87 0.02 +total 86 0.02 +total 85 0.02 +total 84 0.02 +total 83 0.02 +total 82 0.02 +total 81 0.02 +total 80 0.04 +total 79 0.04 +total 78 0.04 +total 77 0.04 +total 76 0.04 +total 75 0.04 +total 74 0.04 +total 73 0.04 +total 72 0.04 +total 71 0.04 +total 70 0.04 +total 69 0.04 +total 68 0.04 +total 67 0.04 +total 66 0.04 +total 65 0.04 +total 64 0.04 +total 63 0.04 +total 62 0.04 +total 61 0.04 +total 60 0.04 +total 59 0.04 +total 58 0.04 +total 57 0.04 +total 56 0.04 +total 55 0.04 +total 54 0.04 +total 53 0.04 +total 52 0.04 +total 51 0.04 +total 50 0.04 +total 49 0.05 +total 48 0.05 +total 47 0.05 +total 46 0.05 +total 45 0.05 +total 44 0.05 +total 43 0.05 +total 42 0.05 +total 41 0.05 +total 40 0.05 +total 39 0.05 +total 38 0.05 +total 37 0.05 +total 36 0.05 +total 35 0.05 +total 34 0.05 +total 33 0.05 +total 32 0.05 +total 31 0.05 +total 30 0.05 +total 29 0.05 +total 28 0.05 +total 27 0.05 +total 26 0.05 +total 25 0.05 +total 24 0.05 +total 23 0.05 +total 22 0.05 +total 21 0.05 +total 20 0.05 +total 19 0.05 +total 18 0.05 +total 17 0.05 +total 16 0.05 +total 15 0.05 +total 14 0.05 +total 13 0.05 +total 12 0.05 +total 11 0.05 +total 10 0.05 +total 9 0.05 +total 8 0.05 +total 7 0.06 +total 6 0.06 +total 5 0.07 +total 4 0.07 +total 3 0.07 +total 2 0.07 +total 1 0.07 +total 0 1.00 diff --git a/tests/expected/dna/test.mosdepth.summary.txt b/tests/expected/dna/test.mosdepth.summary.txt new file mode 100644 index 00000000..ec15caf6 --- /dev/null +++ b/tests/expected/dna/test.mosdepth.summary.txt @@ -0,0 +1,5 @@ +chrom length bases mean min max +chr22 40001 247878 6.20 0 867 +chr22_region 40001 247878 6.20 0 867 +total 40001 247878 6.20 0 867 +total_region 40001 247878 6.20 0 867 diff --git a/tests/expected/dna/test.per-base.bed.gz b/tests/expected/dna/test.per-base.bed.gz new file mode 100644 index 00000000..2bcaa495 Binary files /dev/null and b/tests/expected/dna/test.per-base.bed.gz differ diff --git a/tests/expected/dna/test.per-base.bed.gz.csi b/tests/expected/dna/test.per-base.bed.gz.csi new file mode 100644 index 00000000..360bf772 Binary files /dev/null and b/tests/expected/dna/test.per-base.bed.gz.csi differ diff --git a/tests/expected/dna/test.regions.bed.gz b/tests/expected/dna/test.regions.bed.gz new file mode 100644 index 00000000..06a1c5e1 Binary files /dev/null and b/tests/expected/dna/test.regions.bed.gz differ diff --git a/tests/expected/dna/test.regions.bed.gz.csi b/tests/expected/dna/test.regions.bed.gz.csi new file mode 100644 index 00000000..7fe77157 Binary files /dev/null and b/tests/expected/dna/test.regions.bed.gz.csi differ diff --git a/tests/expected/dna/test.stats.txt b/tests/expected/dna/test.stats.txt new file mode 100644 index 00000000..9779c96d --- /dev/null +++ b/tests/expected/dna/test.stats.txt @@ -0,0 +1,1916 @@ +# This file was produced by samtools stats (1.24+htslib-1.24) and can be plotted using plot-bamstats +# This file contains statistics for all reads. +# The command line was: stats /Users/benjamin/RustQC-dna/tests/data/dna/test.dna.bam +# CHK, Checksum [2]Read Names [3]Sequences [4]Qualities +# CHK, CRC32 of reads which passed filtering followed by addition (32bit overflow) +CHK 82cbdacd 541c12e0 25a61aa9 +# Summary Numbers. Use `grep ^SN | cut -f 2-` to extract this part. +SN raw total sequences: 5642 # excluding supplementary and secondary reads +SN filtered sequences: 0 +SN sequences: 5642 +SN is sorted: 1 # sorted by coordinate +SN 1st fragments: 2821 +SN last fragments: 2821 +SN reads mapped: 5640 +SN reads mapped and paired: 5640 # paired-end technology bit set + both mates mapped +SN reads unmapped: 2 +SN reads properly paired: 5638 # proper-pair bit set +SN reads paired: 5642 # paired-end technology bit set +SN reads duplicated: 1656 # PCR or optical duplicate bit set +SN reads MQ0: 0 # mapped and MQ=0 +SN reads QC failed: 0 +SN non-primary alignments: 2 +SN supplementary alignments: 0 +SN total length: 672131 # ignores clipping +SN total first fragment length: 335944 # ignores clipping +SN total last fragment length: 336187 # ignores clipping +SN bases mapped: 671854 # ignores clipping +SN bases mapped (cigar): 670991 # more accurate +SN bases trimmed: 0 +SN bases duplicated: 201314 +SN mismatches: 1352 # from NM fields +SN error rate: 2.014930e-03 # mismatches / bases mapped (cigar) +SN average length: 119 +SN average first fragment length: 119 +SN average last fragment length: 119 +SN maximum length: 143 +SN maximum first fragment length: 143 +SN maximum last fragment length: 143 +SN average quality: 40.9 +SN insert size average: 124.8 +SN insert size standard deviation: 31.2 +SN inward oriented pairs: 2814 +SN outward oriented pairs: 6 +SN pairs with other orientation: 0 +SN pairs on different chromosomes: 0 +SN percentage of properly paired reads (%): 99.9 +# First Fragment Qualities. Use `grep ^FFQ | cut -f 2-` to extract this part. +# Columns correspond to qualities and rows to cycles. First column is the cycle number. +FFQ 1 0 0 2 0 0 0 0 0 0 0 0 0 0 17 0 0 0 0 0 0 15 0 0 0 0 0 0 0 0 0 0 34 0 0 881 0 0 0 0 0 1 0 0 1 54 1816 0 +FFQ 2 0 0 1 0 0 0 0 0 0 0 0 0 0 25 0 0 0 0 0 0 15 0 0 0 0 0 0 0 0 0 0 24 0 0 882 0 0 0 3 0 0 0 1 1 53 1816 0 +FFQ 3 0 0 0 0 1 0 0 0 0 0 0 0 0 24 0 0 0 0 0 0 15 0 0 0 0 0 0 0 0 0 0 35 0 0 872 0 0 0 2 0 0 0 0 0 62 1810 0 +FFQ 4 0 0 1 0 0 0 0 0 0 0 0 0 0 13 0 0 0 0 0 0 15 0 0 0 0 0 0 0 0 0 0 36 0 0 883 1 0 0 4 0 0 0 0 0 50 1818 0 +FFQ 5 0 0 0 0 0 0 0 0 0 0 0 0 0 11 0 0 0 0 0 0 18 0 0 0 0 0 0 0 0 0 0 34 0 0 885 2 0 0 0 0 0 0 0 0 42 1829 0 +FFQ 6 0 0 1 0 0 0 0 0 0 0 0 0 0 25 0 0 0 0 0 0 14 0 0 0 0 0 0 0 0 0 0 32 0 0 877 0 0 0 1 0 0 0 0 0 66 1805 0 +FFQ 7 0 0 0 0 0 0 0 0 0 0 0 0 0 25 0 0 0 0 0 0 14 0 0 0 0 0 1 0 0 0 0 30 0 0 876 1 0 0 1 0 0 0 0 0 46 1827 0 +FFQ 8 0 0 2 0 0 0 0 0 0 0 0 0 0 23 0 0 0 0 0 0 16 0 0 0 0 0 1 0 0 0 0 33 0 0 873 0 0 0 2 0 0 0 0 0 61 1810 0 +FFQ 9 0 0 1 0 0 0 0 0 0 0 0 0 0 24 0 0 0 0 1 0 8 0 0 0 0 0 1 0 0 0 0 37 0 0 876 0 0 0 4 0 0 0 0 1 53 1815 0 +FFQ 10 0 0 1 0 0 0 0 0 0 0 0 0 0 20 0 0 0 0 0 0 9 0 0 0 0 0 0 0 0 0 0 23 0 0 895 1 0 0 3 0 2 0 0 0 58 1809 0 +FFQ 11 0 0 0 0 0 0 0 0 0 0 0 0 0 8 0 0 0 0 0 0 22 0 0 0 0 0 0 0 0 0 0 23 0 0 895 0 0 0 0 0 0 0 0 1 63 1809 0 +FFQ 12 0 0 4 0 0 0 0 0 0 0 0 0 0 13 0 0 0 0 0 0 17 0 0 0 0 0 0 0 0 0 0 40 0 0 879 0 0 0 6 0 0 0 0 0 53 1809 0 +FFQ 13 0 0 2 0 0 0 0 0 0 0 0 0 0 21 0 0 0 1 0 0 13 0 0 0 0 0 3 0 0 0 0 38 0 0 872 0 0 0 3 0 0 0 0 1 49 1818 0 +FFQ 14 0 0 1 0 0 0 0 0 0 0 0 0 0 18 0 0 0 0 0 0 15 0 0 0 0 0 3 0 0 0 0 41 2 0 870 0 0 0 2 0 1 0 0 1 64 1803 0 +FFQ 15 0 0 4 0 1 0 0 0 0 0 0 0 0 21 0 1 0 0 0 0 14 0 0 0 0 0 0 0 0 0 0 34 0 0 878 0 0 0 1 0 0 0 0 0 63 1804 0 +FFQ 16 0 0 4 0 0 0 0 0 0 0 0 0 0 20 0 0 0 0 0 0 12 0 0 0 0 0 1 0 0 0 0 30 1 0 886 0 0 0 0 0 0 0 0 1 44 1822 0 +FFQ 17 0 0 3 0 0 0 0 0 0 0 0 0 0 18 0 0 0 0 0 0 11 0 0 0 0 0 1 0 0 0 0 35 0 0 883 0 0 0 2 0 1 0 0 0 61 1806 0 +FFQ 18 0 0 3 0 0 0 0 0 0 0 0 0 0 22 0 0 0 0 0 0 11 0 0 0 0 0 1 0 0 0 0 39 0 0 876 0 0 0 0 0 0 0 0 1 55 1813 0 +FFQ 19 0 0 2 0 1 0 0 0 0 0 0 0 0 27 0 0 0 0 0 0 14 0 0 0 0 0 0 0 0 0 0 35 0 0 873 0 0 0 2 0 0 0 0 0 69 1798 0 +FFQ 20 0 0 2 0 1 0 0 0 1 0 0 0 0 26 0 0 0 0 0 0 9 0 0 0 0 0 3 0 0 0 0 36 3 0 873 0 0 0 1 0 3 0 1 2 70 1790 0 +FFQ 21 0 0 0 0 0 0 0 0 0 0 0 0 0 28 0 0 0 0 0 0 5 0 0 0 0 0 2 0 0 0 0 46 0 0 866 0 0 0 1 0 0 0 0 0 64 1809 0 +FFQ 22 0 0 3 0 0 0 0 0 0 0 0 0 0 26 0 0 0 0 1 0 13 0 0 0 0 0 5 0 0 0 0 31 0 0 873 0 0 0 2 0 0 0 0 0 55 1812 0 +FFQ 23 0 0 3 0 0 0 0 0 0 0 0 0 0 30 0 0 0 0 0 0 10 0 0 0 0 0 4 0 0 0 0 41 0 0 863 0 0 0 4 0 2 0 1 1 52 1810 0 +FFQ 24 0 0 2 0 0 0 0 0 0 0 0 0 0 34 0 0 0 0 1 0 12 0 0 0 0 0 4 0 0 0 0 29 0 0 870 1 0 0 6 0 0 0 0 1 73 1788 0 +FFQ 25 0 0 5 0 0 0 0 0 0 0 0 0 0 28 0 0 0 0 0 0 11 0 0 0 0 0 0 0 0 0 0 34 0 0 877 0 0 0 1 0 0 0 2 0 53 1810 0 +FFQ 26 0 0 2 0 2 0 0 0 0 0 0 0 0 32 0 0 0 0 0 0 6 0 0 0 0 0 0 0 0 0 0 37 0 0 873 0 0 0 3 0 0 0 0 0 57 1809 0 +FFQ 27 0 0 3 0 0 0 0 0 0 0 0 0 0 40 0 0 0 0 0 0 7 0 0 0 0 0 4 0 0 0 0 29 0 0 870 0 0 0 6 0 0 0 0 0 40 1822 0 +FFQ 28 0 0 5 0 1 0 0 0 0 0 0 0 0 36 0 0 0 0 0 0 9 0 0 0 0 0 1 0 0 0 0 24 2 0 880 0 0 0 2 0 0 0 0 0 50 1811 0 +FFQ 29 0 0 1 0 1 0 0 0 0 0 0 0 0 32 0 0 0 0 0 0 3 0 0 0 0 1 4 0 0 0 0 52 0 0 857 3 0 0 7 0 0 0 0 1 68 1791 0 +FFQ 30 0 0 1 0 0 0 0 0 0 0 0 0 0 35 0 0 0 0 0 0 4 0 0 0 0 0 3 0 0 0 0 32 0 0 874 0 0 0 6 0 0 0 0 4 58 1804 0 +FFQ 31 0 0 5 0 2 0 0 0 0 0 0 0 0 24 0 0 0 0 0 0 11 0 0 0 0 0 1 0 0 0 0 38 0 0 875 0 0 0 3 0 0 0 0 0 54 1807 0 +FFQ 32 0 0 3 0 1 0 0 0 0 0 0 0 0 35 0 0 0 0 0 0 8 0 0 0 0 0 1 0 0 0 0 32 1 0 873 0 0 0 2 0 0 0 1 0 59 1804 0 +FFQ 33 0 0 7 0 2 0 0 0 0 0 0 0 0 35 0 0 0 0 1 0 6 0 0 0 0 0 5 0 0 0 0 26 0 0 877 0 0 0 3 0 0 0 0 0 53 1805 0 +FFQ 34 0 0 5 0 0 0 0 0 0 0 0 0 0 32 0 0 0 0 0 0 6 0 0 0 0 0 5 0 0 0 0 29 1 0 877 0 0 0 2 0 0 0 0 0 62 1801 0 +FFQ 35 0 0 4 0 0 0 0 0 0 0 0 0 0 31 0 0 0 0 0 0 8 0 0 0 0 0 5 0 0 0 0 29 0 0 877 0 0 0 1 0 0 0 0 0 55 1810 0 +FFQ 36 0 0 2 0 0 0 0 0 0 0 0 0 0 32 0 0 0 0 0 0 7 0 0 0 0 0 7 0 0 0 0 31 0 0 872 0 0 0 2 0 0 0 0 1 64 1802 0 +FFQ 37 0 0 3 0 0 0 0 0 0 0 0 0 0 36 0 0 0 0 0 0 8 0 0 0 0 0 4 0 0 0 0 34 0 0 867 1 0 0 0 0 0 0 2 0 65 1800 0 +FFQ 38 0 0 2 0 0 0 0 0 0 0 0 0 0 36 0 0 0 0 0 0 6 0 0 0 0 0 7 0 0 0 0 33 1 0 867 1 0 0 5 0 1 0 0 0 69 1792 0 +FFQ 39 0 0 3 0 2 0 0 0 0 0 0 0 0 32 0 0 0 0 0 0 2 0 0 0 0 0 5 0 0 0 0 40 1 0 870 0 0 0 2 0 0 0 0 0 63 1800 0 +FFQ 40 1 0 2 0 0 0 0 0 0 0 0 0 0 33 0 0 0 0 1 0 9 0 0 0 0 0 7 0 0 0 0 35 2 0 865 0 0 0 3 0 0 0 0 0 62 1800 0 +FFQ 41 0 0 3 0 0 0 0 0 0 0 0 0 0 24 0 0 0 0 0 0 8 0 0 0 0 0 9 0 0 0 0 33 0 0 875 0 0 0 2 0 0 1 1 1 58 1805 0 +FFQ 42 0 0 3 0 0 0 0 0 0 0 0 0 0 32 0 0 0 0 0 0 14 0 0 0 0 0 9 0 0 0 0 26 0 0 870 0 0 0 4 0 0 0 0 0 51 1810 0 +FFQ 43 0 0 2 0 0 0 0 0 0 0 0 0 0 42 0 0 0 0 1 0 6 0 0 0 0 0 4 0 0 0 0 30 0 0 864 0 0 0 2 0 0 0 2 0 60 1806 0 +FFQ 44 1 0 5 0 0 0 0 0 0 0 0 0 0 39 0 0 0 0 0 0 7 0 0 0 0 0 5 0 0 0 0 33 0 0 863 0 0 0 5 0 0 0 0 1 72 1788 0 +FFQ 45 1 0 3 0 0 0 0 0 0 0 0 0 0 30 0 0 0 0 0 0 7 0 0 0 0 0 5 0 0 0 0 39 0 0 867 1 0 0 3 0 0 0 1 1 70 1791 0 +FFQ 46 1 0 1 0 0 0 0 0 0 0 0 0 0 37 0 0 0 0 0 0 7 0 0 0 0 0 5 0 0 0 0 32 1 0 863 0 0 0 2 0 0 0 0 1 71 1797 0 +FFQ 47 2 0 4 0 0 0 0 0 0 0 0 0 0 39 0 0 0 0 0 0 4 0 0 0 0 0 1 0 0 0 0 34 0 0 866 0 0 0 7 0 1 0 0 2 76 1782 0 +FFQ 48 1 0 4 0 1 0 0 0 0 0 1 0 0 37 0 0 0 1 0 0 6 0 0 0 0 0 11 0 0 0 0 40 0 0 851 0 0 0 4 0 0 0 0 2 99 1760 0 +FFQ 49 1 0 1 0 1 0 0 0 0 0 0 0 0 43 0 0 0 0 0 0 6 0 0 0 0 0 7 0 0 0 0 45 0 0 844 0 0 0 2 0 0 0 1 2 94 1771 0 +FFQ 50 0 0 4 0 0 0 0 0 0 0 0 0 0 30 0 0 0 0 0 0 9 0 0 0 0 0 11 0 0 0 0 59 0 0 837 0 0 0 2 0 0 0 0 1 91 1771 0 +FFQ 51 1 0 3 0 1 0 0 0 0 0 0 0 0 32 0 0 0 0 0 0 12 0 0 0 0 0 8 0 0 0 0 59 0 0 835 0 0 0 4 0 0 0 1 2 99 1758 0 +FFQ 52 2 0 0 0 1 0 0 0 0 0 0 0 0 35 0 0 0 0 0 0 8 0 0 0 0 1 14 0 0 0 0 59 0 0 829 0 0 0 0 0 2 0 1 3 117 1742 0 +FFQ 53 0 0 1 0 0 0 0 0 0 0 0 0 0 33 0 0 0 0 0 0 7 0 0 0 0 0 10 0 0 0 0 57 0 0 838 0 0 0 1 0 1 0 0 1 100 1763 0 +FFQ 54 1 0 3 0 2 0 0 0 0 0 0 0 0 33 0 0 0 0 0 0 5 0 0 0 0 0 9 0 0 0 0 49 0 0 847 0 0 0 1 0 0 0 1 0 95 1766 0 +FFQ 55 0 0 6 0 0 0 0 0 0 0 0 0 0 34 0 0 0 0 0 0 13 0 0 0 0 0 16 0 0 0 0 51 1 0 830 1 0 0 4 0 1 0 1 1 107 1745 0 +FFQ 56 1 0 3 0 0 0 0 0 0 0 0 0 0 38 0 0 0 0 0 0 12 0 0 0 0 0 12 0 0 0 0 48 0 0 834 0 0 0 4 0 2 0 0 0 94 1763 0 +FFQ 57 0 0 4 0 0 0 0 0 0 0 0 0 0 37 0 0 0 0 1 0 9 0 0 0 0 0 9 0 0 0 0 57 0 0 833 1 0 0 2 0 3 0 1 0 108 1746 0 +FFQ 58 2 0 5 0 0 0 0 0 0 0 0 0 0 38 0 0 0 0 0 0 5 0 0 0 0 0 14 0 0 0 0 44 0 0 842 0 0 0 2 0 0 0 2 2 76 1779 0 +FFQ 59 1 0 3 0 0 1 0 0 0 0 0 0 0 42 0 0 0 0 0 0 9 0 0 0 0 0 8 0 0 0 0 54 1 0 831 0 0 0 6 0 0 0 2 1 105 1746 0 +FFQ 60 2 0 2 0 1 0 0 0 0 0 0 0 0 35 0 0 0 1 0 0 8 0 0 0 0 0 12 0 0 0 0 52 0 0 832 1 0 0 2 0 2 0 1 2 109 1746 0 +FFQ 61 1 0 3 0 0 0 0 0 0 0 0 0 0 36 0 0 0 0 0 0 11 0 0 0 0 0 18 0 0 0 0 69 1 0 811 0 1 0 3 0 0 0 0 6 99 1748 0 +FFQ 62 1 0 2 0 2 0 0 0 0 0 0 0 0 47 0 0 0 0 0 0 5 0 0 1 0 1 16 0 0 0 0 60 0 0 812 1 0 0 2 0 0 0 2 3 89 1759 0 +FFQ 63 1 0 6 0 0 0 0 0 0 0 0 0 0 34 0 0 0 0 0 0 8 0 0 0 0 0 19 0 0 0 0 60 1 0 821 0 0 0 3 0 0 0 2 3 93 1751 0 +FFQ 64 2 0 2 0 0 0 0 0 0 0 0 1 0 34 0 0 0 0 1 0 7 0 0 0 0 0 20 0 0 0 0 56 1 0 820 0 0 0 1 0 1 0 0 2 96 1753 0 +FFQ 65 1 0 5 0 0 0 0 0 0 0 0 0 0 42 0 0 0 1 0 0 8 0 0 0 0 0 14 0 0 0 0 51 1 0 821 0 0 0 4 0 0 0 0 4 93 1752 0 +FFQ 66 1 0 1 0 0 0 0 0 1 0 0 0 0 38 0 0 0 0 0 0 6 0 0 0 0 0 12 0 0 0 0 47 0 0 829 0 0 0 0 0 0 1 1 1 118 1736 0 +FFQ 67 0 0 3 0 0 0 0 0 0 0 0 0 0 28 0 0 0 0 0 0 5 0 0 0 0 0 16 0 0 0 0 51 0 0 831 0 0 0 1 0 0 0 0 0 109 1746 0 +FFQ 68 0 0 1 0 3 1 0 0 0 0 0 0 0 36 0 0 0 1 0 0 4 0 0 0 0 0 17 0 0 0 0 59 0 0 811 0 0 0 4 0 1 0 0 2 110 1734 0 +FFQ 69 0 0 2 0 1 0 0 0 0 0 0 0 0 38 0 0 0 0 1 0 7 0 0 0 0 0 16 0 0 0 0 62 0 0 805 0 0 0 1 0 4 0 0 3 111 1730 0 +FFQ 70 0 0 1 0 0 0 0 0 2 0 0 0 0 42 0 0 0 0 0 0 5 0 0 0 0 0 16 0 0 0 0 56 0 0 805 0 0 0 1 0 0 0 4 0 105 1739 0 +FFQ 71 0 0 1 0 0 0 0 0 2 0 0 0 0 38 0 0 0 0 1 0 8 0 0 0 0 0 18 0 0 0 0 53 0 0 805 0 0 0 5 0 0 0 1 6 99 1729 0 +FFQ 72 0 0 1 0 1 0 0 0 2 0 0 0 0 31 0 0 0 0 0 0 8 0 0 0 0 0 23 0 0 0 0 79 1 0 778 1 0 0 4 0 0 0 1 2 102 1721 0 +FFQ 73 0 0 1 0 1 0 0 0 1 0 0 0 0 43 0 0 0 0 0 0 5 0 0 0 0 0 10 0 0 0 0 58 0 0 797 0 0 0 2 0 1 0 0 5 111 1713 0 +FFQ 74 0 0 0 0 1 0 0 0 0 0 0 0 0 33 0 0 0 0 0 0 10 0 0 0 0 0 19 0 0 0 0 53 1 0 793 0 0 0 4 0 0 0 0 1 108 1717 0 +FFQ 75 0 0 3 0 0 0 0 0 0 0 0 0 0 41 0 0 0 1 0 0 8 0 0 0 0 0 22 0 0 0 0 63 1 0 776 0 0 0 1 0 1 0 0 1 115 1703 0 +FFQ 76 0 0 3 0 0 0 0 0 0 0 0 0 0 39 0 0 0 0 0 0 8 0 0 0 0 0 13 0 0 0 0 51 1 0 793 1 0 0 0 0 0 0 1 2 114 1698 0 +FFQ 77 0 0 0 0 0 0 0 0 0 0 0 0 0 39 0 0 0 0 0 0 2 0 0 0 0 0 16 0 0 0 0 55 1 0 786 1 0 0 0 0 0 0 0 1 120 1692 0 +FFQ 78 0 0 4 0 0 0 0 0 0 0 0 0 0 40 0 0 0 0 0 0 2 0 0 0 0 0 18 0 0 0 0 49 0 0 777 0 0 0 2 0 1 0 1 0 112 1688 0 +FFQ 79 0 0 2 0 0 0 0 0 0 0 0 0 0 48 0 0 0 1 0 0 7 0 0 0 0 0 12 0 0 0 0 57 0 0 753 0 0 0 0 0 0 0 1 1 117 1680 0 +FFQ 80 0 0 3 0 0 0 0 0 1 0 0 0 0 43 0 0 0 0 0 0 10 0 0 0 0 0 12 0 0 0 0 69 0 0 738 0 0 0 2 0 0 0 1 0 118 1669 0 +FFQ 81 0 0 2 0 0 0 0 0 1 0 0 0 0 37 0 0 0 0 1 0 6 0 0 0 0 0 19 0 0 0 0 55 0 0 745 0 0 0 0 0 1 0 3 3 127 1649 0 +FFQ 82 0 0 0 0 0 0 0 0 0 0 0 0 0 41 0 0 0 0 0 0 4 0 0 0 0 0 13 0 0 0 0 76 3 0 715 1 0 0 1 0 0 0 0 0 109 1662 0 +FFQ 83 0 0 2 0 0 0 0 0 0 0 0 0 0 46 0 0 0 0 0 0 5 0 0 0 0 0 15 0 0 0 0 57 0 0 717 0 0 0 0 0 0 0 0 1 92 1672 0 +FFQ 84 0 0 1 0 0 0 0 0 1 0 0 0 0 31 0 0 0 0 0 0 6 0 0 0 0 0 17 0 0 0 0 60 2 0 718 0 0 0 0 0 0 0 0 0 122 1630 0 +FFQ 85 0 0 4 0 0 0 0 0 0 0 0 0 0 27 0 0 0 0 0 0 9 0 0 0 0 0 16 0 0 0 0 58 2 0 710 0 0 0 0 0 0 0 0 4 130 1603 0 +FFQ 86 0 0 1 0 0 0 0 0 0 0 0 0 0 43 0 0 0 0 0 0 10 0 0 0 0 0 25 0 0 0 0 46 1 0 685 0 0 0 1 0 0 0 1 1 109 1625 0 +FFQ 87 0 0 1 0 1 0 0 0 0 0 0 0 0 35 0 0 0 0 0 0 3 0 0 0 0 0 19 0 0 0 0 53 2 0 692 0 0 0 0 0 0 0 0 2 117 1599 0 +FFQ 88 0 0 0 0 1 0 0 0 1 0 0 0 0 37 0 0 0 0 0 0 2 0 0 0 0 0 23 0 0 0 0 63 1 0 664 0 0 0 0 0 0 0 2 3 118 1579 0 +FFQ 89 0 0 1 0 0 0 0 0 0 0 0 0 0 27 0 0 0 0 0 0 7 0 0 0 0 0 17 0 0 0 0 59 1 0 666 0 0 0 2 0 0 0 0 3 131 1551 0 +FFQ 90 0 0 0 0 1 0 0 0 0 0 0 0 0 29 0 0 0 0 0 0 2 0 0 0 0 0 15 0 0 0 0 47 0 0 678 1 0 0 0 0 2 1 1 4 115 1548 0 +FFQ 91 0 0 0 0 0 0 0 0 0 0 0 0 0 40 0 0 0 0 0 0 2 0 0 0 0 0 17 0 0 0 0 66 1 0 635 1 0 0 1 0 0 0 2 4 119 1540 0 +FFQ 92 0 0 1 0 0 0 0 0 0 0 0 0 0 34 0 0 0 1 0 0 7 0 0 0 0 0 18 0 0 0 0 47 0 0 643 0 0 0 0 0 1 0 1 10 120 1522 0 +FFQ 93 0 0 0 0 0 0 0 0 0 0 0 0 0 35 0 0 0 0 0 0 5 1 0 0 0 0 26 0 0 0 0 63 1 0 610 1 0 0 2 0 0 0 1 8 103 1519 0 +FFQ 94 0 0 0 0 0 0 0 0 0 0 0 0 0 34 0 0 0 0 0 0 5 0 0 0 0 0 25 0 0 0 0 68 0 0 599 0 0 0 1 0 0 0 2 3 121 1494 0 +FFQ 95 0 0 1 0 0 0 0 0 0 0 0 0 0 29 0 0 0 1 0 0 3 0 0 0 0 0 17 0 0 0 0 78 0 0 593 0 0 0 0 0 0 0 1 1 124 1484 0 +FFQ 96 0 0 0 0 0 0 0 0 0 0 0 0 0 36 0 0 0 0 0 0 2 0 0 0 0 0 14 0 0 0 0 74 1 0 575 0 0 0 0 0 0 0 0 3 126 1459 0 +FFQ 97 0 0 0 0 0 0 0 0 1 0 0 0 0 30 0 0 0 1 0 0 6 0 0 0 0 0 21 0 0 0 0 76 0 0 545 0 0 0 0 0 0 0 1 5 137 1413 0 +FFQ 98 0 0 0 0 0 0 0 0 0 0 0 0 0 30 0 0 0 0 1 0 6 0 0 0 0 0 27 0 0 0 0 71 1 0 532 0 0 0 0 0 0 0 0 5 132 1397 0 +FFQ 99 0 0 0 0 0 0 0 0 0 0 0 0 0 25 0 0 0 0 0 0 5 0 0 0 0 0 32 0 0 0 0 85 1 0 506 1 0 0 1 0 0 0 0 4 133 1381 0 +FFQ 100 0 0 0 0 0 0 0 0 0 0 0 0 0 31 0 0 0 0 0 0 4 0 0 0 0 0 20 0 0 0 0 66 0 0 521 0 0 0 0 0 0 0 3 3 135 1367 0 +FFQ 101 0 0 0 0 0 0 0 0 0 0 0 0 0 26 0 0 0 0 0 0 5 0 0 0 0 0 21 0 0 0 0 83 0 0 489 0 0 0 0 0 0 0 0 2 135 1345 0 +FFQ 102 0 0 0 0 0 0 0 0 0 0 0 0 0 28 0 0 0 0 0 0 3 0 0 0 0 0 25 0 0 0 0 78 0 0 483 0 0 0 0 0 0 0 3 1 136 1325 0 +FFQ 103 0 0 0 0 0 0 0 0 0 0 0 0 0 32 0 0 0 0 0 0 3 0 0 0 0 0 34 0 0 0 0 80 1 0 460 0 0 0 0 0 0 0 1 4 138 1302 0 +FFQ 104 0 0 0 0 0 0 0 0 0 0 0 0 0 20 0 0 0 0 0 0 5 0 0 0 0 0 28 0 0 0 0 75 0 0 473 0 0 0 0 0 0 0 2 4 141 1285 0 +FFQ 105 0 0 0 0 0 0 0 0 0 0 0 0 0 35 0 0 0 0 0 0 8 0 0 0 0 0 22 0 0 0 0 80 0 0 441 0 0 0 0 0 0 0 3 6 125 1280 0 +FFQ 106 0 0 0 0 0 0 0 0 0 0 0 0 0 18 0 0 0 0 0 0 2 0 0 0 0 0 27 0 0 0 0 89 4 0 440 0 0 0 1 0 0 0 2 3 132 1256 0 +FFQ 107 0 0 0 0 0 0 0 0 0 0 0 0 0 30 0 0 0 0 0 0 7 0 0 0 0 0 24 0 0 0 0 81 0 0 423 0 0 0 1 0 0 0 4 4 147 1225 0 +FFQ 108 0 0 0 0 0 0 0 0 0 0 0 0 0 23 0 0 0 0 0 0 6 0 0 0 0 0 28 0 0 0 0 84 1 0 412 0 0 0 1 0 0 0 0 4 158 1193 0 +FFQ 109 0 0 0 0 0 0 0 0 0 0 0 0 0 33 0 0 0 1 0 0 6 0 0 0 0 0 22 0 0 0 0 99 1 0 381 0 0 0 1 0 0 0 3 7 134 1196 0 +FFQ 110 0 0 0 0 0 0 0 0 0 0 0 0 0 31 0 0 0 1 0 0 4 0 0 0 0 0 30 0 0 0 0 94 2 0 371 0 0 0 0 0 0 1 3 3 149 1171 0 +FFQ 111 0 0 0 0 0 0 0 0 0 0 0 0 0 33 0 0 0 0 0 0 3 0 0 0 0 0 44 0 0 0 0 86 2 0 353 0 0 0 1 0 0 0 2 2 132 1168 0 +FFQ 112 0 0 0 0 1 0 0 0 0 0 0 0 0 27 0 0 0 0 0 0 5 0 0 0 0 0 30 0 0 0 0 107 1 0 343 0 0 0 0 0 0 0 4 3 167 1109 0 +FFQ 113 0 0 0 0 0 0 0 0 0 0 0 0 0 27 0 0 0 0 0 0 4 0 0 0 0 0 34 0 1 0 0 103 1 0 332 0 0 0 0 0 0 0 3 3 164 1103 0 +FFQ 114 0 0 0 0 0 0 0 0 0 0 0 0 0 30 0 0 0 0 0 0 6 0 0 0 0 0 33 0 0 0 0 105 1 0 317 1 0 0 0 0 0 0 5 6 163 1072 0 +FFQ 115 0 0 0 0 1 0 0 0 0 0 0 0 0 26 0 0 0 0 0 0 5 0 0 0 0 0 41 0 0 0 0 117 0 0 293 0 0 0 1 0 0 0 3 5 151 1066 0 +FFQ 116 0 0 0 0 0 0 0 0 0 0 0 0 0 17 0 0 0 0 0 0 8 0 0 0 0 0 39 0 0 0 0 113 2 0 291 0 0 0 1 0 0 0 1 0 162 1026 0 +FFQ 117 0 0 0 0 0 0 0 0 0 0 0 0 0 25 0 0 0 0 0 0 11 0 0 0 0 0 25 0 0 0 0 116 0 0 279 0 0 0 0 0 0 0 0 4 156 1008 0 +FFQ 118 0 0 0 0 0 0 0 0 0 0 0 0 0 29 0 0 0 0 0 0 8 0 0 0 0 0 29 0 0 0 0 96 0 0 285 0 0 0 2 0 0 0 2 3 151 986 0 +FFQ 119 0 0 1 0 0 0 0 0 0 0 0 0 0 18 0 0 0 0 0 0 8 0 0 0 0 0 36 0 0 0 0 105 0 0 269 0 0 0 1 0 0 0 2 2 154 961 0 +FFQ 120 0 0 0 0 0 0 1 0 0 0 0 0 0 23 0 0 0 0 0 0 10 0 0 0 0 0 33 0 0 0 0 110 2 0 251 0 0 0 0 0 0 0 3 5 157 924 0 +FFQ 121 0 0 0 0 0 0 0 0 0 0 0 0 0 23 0 0 0 0 0 0 5 0 0 0 0 0 32 0 0 0 0 92 0 0 264 0 0 0 0 0 0 0 3 7 142 937 0 +FFQ 122 0 0 0 0 0 0 0 0 0 0 0 0 0 19 0 0 0 1 0 0 8 0 0 0 0 0 47 0 0 0 0 91 0 0 237 0 0 0 0 0 0 0 1 8 143 911 0 +FFQ 123 0 0 0 0 0 0 0 0 0 0 0 0 0 25 0 0 0 0 0 0 10 0 0 0 0 0 24 0 0 0 0 116 0 0 221 0 0 0 0 0 0 0 2 7 147 884 0 +FFQ 124 0 0 0 0 0 0 0 0 0 0 0 0 0 20 0 0 0 1 0 0 8 0 0 0 0 0 45 0 0 0 0 97 0 0 216 0 0 0 0 0 0 0 2 4 140 875 0 +FFQ 125 0 0 0 0 0 0 0 0 0 0 0 0 0 20 0 0 0 0 0 0 8 0 0 0 0 0 35 0 0 0 0 102 1 0 204 0 0 0 0 0 0 0 0 5 119 878 0 +FFQ 126 0 0 0 0 0 0 0 0 0 0 0 0 0 15 0 0 0 1 0 0 5 0 0 0 0 0 23 0 0 0 0 100 0 0 214 0 0 0 0 0 0 0 1 9 139 829 0 +FFQ 127 0 0 0 0 0 0 0 0 0 0 0 0 0 20 0 0 0 0 0 0 7 0 0 0 0 0 30 0 0 0 0 100 1 0 193 0 0 0 1 0 0 0 1 2 131 824 0 +FFQ 128 0 0 0 0 0 0 0 0 0 0 0 0 0 20 0 0 0 0 0 0 3 0 0 0 0 0 21 0 0 0 0 85 0 0 208 0 0 0 0 0 0 0 2 6 136 798 0 +FFQ 129 0 0 0 0 0 0 0 0 0 0 0 0 0 23 0 0 0 0 0 0 4 0 0 0 0 0 16 0 0 0 0 72 2 0 212 0 0 0 1 0 0 0 1 1 127 789 0 +FFQ 130 0 0 0 0 0 0 0 0 0 0 0 0 0 19 0 0 0 1 0 0 6 0 0 0 0 0 24 0 0 0 0 69 0 0 198 0 0 0 0 0 1 0 2 4 115 781 0 +FFQ 131 0 0 0 0 0 0 0 0 0 0 0 0 0 21 0 0 0 0 0 0 4 0 0 0 0 0 27 0 0 0 0 71 0 0 183 0 0 0 1 0 0 0 1 7 114 751 0 +FFQ 132 0 0 0 0 0 0 0 0 0 0 0 0 0 16 0 0 0 0 0 0 4 0 0 0 0 0 27 0 0 0 0 73 0 0 174 0 0 0 2 0 0 0 1 6 107 725 0 +FFQ 133 0 0 0 0 0 0 0 0 0 0 0 0 0 19 0 0 0 0 0 0 2 0 0 0 0 0 23 0 0 0 0 86 0 0 158 0 0 0 0 0 0 0 0 2 121 699 0 +FFQ 134 0 0 0 0 0 0 0 0 0 0 0 0 0 20 0 0 0 0 0 0 4 0 0 0 0 0 15 0 0 0 0 79 1 0 163 0 0 0 0 0 0 0 1 5 108 696 0 +FFQ 135 0 0 0 0 0 0 0 0 0 0 0 0 0 15 0 0 0 0 0 0 4 0 0 0 0 0 25 0 0 0 0 75 0 0 149 0 0 0 0 0 0 0 2 8 100 688 0 +FFQ 136 0 0 0 0 1 0 0 0 0 0 0 0 0 16 0 0 0 0 0 0 2 0 0 0 0 0 21 0 0 0 0 81 1 0 136 0 0 0 1 0 0 0 5 4 94 673 0 +FFQ 137 0 0 0 0 0 0 0 0 0 0 0 0 0 10 0 0 0 0 0 0 2 0 0 0 0 0 20 0 0 0 0 69 0 0 144 0 0 0 0 0 0 0 0 1 113 646 0 +FFQ 138 0 0 0 0 0 0 0 0 0 0 0 0 0 14 0 0 0 0 0 0 5 0 0 0 0 0 16 0 0 0 0 73 1 0 134 0 0 0 1 0 0 0 2 3 98 629 0 +FFQ 139 0 0 0 0 0 0 0 0 0 0 0 0 0 9 0 0 0 0 0 0 5 0 0 0 0 0 19 0 0 0 0 77 0 0 124 0 0 0 0 0 0 0 0 6 87 615 0 +FFQ 140 0 0 0 0 0 0 0 0 0 0 0 0 0 8 0 0 0 0 0 0 3 0 0 0 0 0 18 0 0 0 0 70 0 0 122 0 0 0 0 0 0 0 2 5 87 595 0 +FFQ 141 0 0 0 0 0 0 0 0 0 0 0 0 0 15 0 0 0 0 0 0 4 0 0 0 0 0 12 0 0 0 0 67 0 0 116 0 0 0 0 0 0 0 2 5 71 590 0 +FFQ 142 0 0 0 0 0 0 0 0 0 0 0 0 0 10 0 0 0 0 0 0 3 0 0 0 0 0 19 0 0 0 0 52 0 0 116 0 0 0 0 0 0 0 0 4 78 559 0 +FFQ 143 0 0 1 1 0 0 0 0 0 0 0 0 0 61 0 0 0 1 3 0 10 0 0 0 0 0 29 0 0 0 0 64 21 0 33 1 0 0 1 0 0 1 13 15 117 442 0 +# Last Fragment Qualities. Use `grep ^LFQ | cut -f 2-` to extract this part. +# Columns correspond to qualities and rows to cycles. First column is the cycle number. +LFQ 1 1 0 0 0 0 0 0 0 0 0 0 0 0 34 0 0 0 0 0 0 15 0 0 0 0 0 1 0 0 0 0 48 0 0 849 0 0 0 0 0 0 0 0 1 80 1792 0 +LFQ 2 2 0 1 0 0 0 0 0 0 0 0 0 0 28 0 0 0 0 0 0 10 0 0 0 0 0 0 0 0 0 0 38 0 0 869 0 0 0 0 0 0 0 0 3 86 1784 0 +LFQ 3 2 0 0 0 0 0 0 0 0 0 0 0 0 31 0 0 0 0 0 0 8 0 0 0 0 0 0 0 0 0 0 50 0 0 855 0 0 0 3 0 1 0 0 0 69 1802 0 +LFQ 4 3 0 0 0 0 0 0 0 0 0 0 0 0 32 0 0 0 0 0 0 12 0 0 0 0 0 2 0 0 0 0 38 0 0 861 0 0 0 0 0 0 0 0 2 60 1811 0 +LFQ 5 2 0 0 0 0 0 0 0 0 0 0 0 0 33 0 0 0 0 0 0 8 0 0 0 0 0 1 0 0 0 0 39 0 0 866 0 0 0 0 0 0 0 0 1 72 1799 0 +LFQ 6 3 0 0 0 0 0 0 0 0 0 0 0 0 29 0 0 0 0 0 0 15 0 0 0 0 0 1 0 0 0 0 42 0 0 858 0 0 0 1 0 0 0 0 0 68 1804 0 +LFQ 7 2 0 0 0 0 0 0 0 0 0 0 0 0 39 0 0 0 0 0 0 12 0 0 0 0 0 0 0 0 0 0 44 0 0 852 0 0 0 1 0 0 0 0 1 71 1799 0 +LFQ 8 3 0 1 0 0 0 0 0 0 0 0 0 0 31 0 0 0 0 0 0 16 0 0 0 0 0 2 0 0 0 0 39 0 0 857 0 0 0 0 0 0 0 0 1 72 1799 0 +LFQ 9 3 0 0 0 0 0 0 0 0 0 1 0 0 32 0 0 0 0 0 0 7 0 0 0 0 0 0 0 0 0 0 41 0 0 863 0 0 0 1 0 0 0 0 1 69 1803 0 +LFQ 10 3 0 0 0 0 0 0 0 0 0 0 0 0 34 0 0 0 0 0 0 13 0 0 0 0 0 0 0 0 0 0 57 0 0 844 0 0 0 0 0 0 0 0 3 70 1797 0 +LFQ 11 3 0 0 0 0 0 0 0 0 0 0 0 0 33 0 0 0 0 0 0 12 0 0 0 0 0 1 0 0 0 0 33 1 0 866 0 0 0 1 0 0 0 1 0 66 1804 0 +LFQ 12 3 0 0 0 0 0 0 0 0 0 0 0 0 34 0 0 0 0 0 0 12 0 0 0 0 0 3 0 0 0 0 31 0 0 865 0 0 0 0 0 0 0 0 0 62 1811 0 +LFQ 13 2 0 0 0 0 0 0 0 0 0 0 0 0 42 0 0 0 0 0 0 10 0 0 0 0 0 4 0 0 0 0 44 1 0 848 0 0 0 1 0 0 0 0 1 75 1793 0 +LFQ 14 3 0 0 0 0 0 0 0 0 0 0 0 0 38 0 0 0 0 0 0 5 0 0 0 0 0 4 0 0 0 0 47 0 0 850 0 0 0 0 0 0 0 0 3 75 1796 0 +LFQ 15 3 0 0 0 0 0 0 0 0 0 0 0 0 41 0 0 0 0 0 0 7 0 0 0 0 0 0 0 0 0 0 41 1 0 858 0 0 0 1 0 0 0 0 1 84 1784 0 +LFQ 16 3 0 1 0 0 0 0 0 0 0 0 0 0 39 0 0 0 0 0 0 4 0 0 0 0 0 13 0 0 0 0 39 1 0 850 0 0 0 0 0 0 0 0 0 70 1801 0 +LFQ 17 3 0 0 0 0 0 0 0 0 0 0 0 0 42 0 0 0 0 0 0 8 0 0 0 0 0 2 0 0 0 0 31 0 0 863 0 0 0 0 0 0 0 0 1 77 1794 0 +LFQ 18 2 0 0 0 0 0 0 0 0 0 0 0 0 46 0 0 0 0 0 0 9 0 0 0 0 0 3 0 0 0 0 35 0 0 854 0 0 0 0 0 0 0 1 2 67 1802 0 +LFQ 19 3 0 0 0 0 0 0 0 0 0 0 0 0 44 0 0 0 0 0 0 8 0 0 0 0 0 3 0 0 0 0 37 0 0 854 0 0 0 1 0 0 0 0 1 70 1800 0 +LFQ 20 3 0 0 0 0 0 0 0 0 0 0 0 0 50 0 0 0 0 0 0 6 0 0 0 0 0 3 0 0 0 0 36 0 0 851 0 0 0 2 0 0 0 0 3 69 1798 0 +LFQ 21 3 0 1 0 0 0 0 0 0 0 0 0 0 34 0 0 0 0 0 0 6 0 0 0 0 0 2 0 0 0 0 36 1 0 867 0 0 0 1 0 0 0 0 1 73 1796 0 +LFQ 22 3 0 0 0 0 0 0 0 0 0 0 0 0 30 0 0 0 0 0 0 6 0 0 0 0 0 1 0 0 0 0 43 2 0 866 0 0 0 0 0 0 0 0 3 70 1797 0 +LFQ 23 3 0 0 0 0 0 0 0 0 0 0 0 0 46 0 0 0 0 0 0 2 0 0 0 0 0 2 0 0 0 0 38 0 0 859 0 0 0 0 0 0 0 0 0 71 1800 0 +LFQ 24 3 0 0 0 0 0 0 0 0 0 0 0 0 45 0 0 0 0 0 0 4 0 0 0 0 0 2 0 0 0 0 35 0 0 859 0 0 0 0 0 0 0 0 1 72 1800 0 +LFQ 25 3 0 0 0 0 0 0 0 0 0 0 0 0 40 0 0 0 0 0 0 6 0 0 0 0 0 2 0 0 0 0 36 0 0 861 0 0 0 0 0 0 0 0 3 63 1807 0 +LFQ 26 3 0 0 0 0 0 0 0 0 0 0 0 0 43 0 0 0 0 0 0 4 0 0 0 0 0 4 0 0 0 0 41 1 0 853 0 0 0 0 0 0 0 0 2 73 1797 0 +LFQ 27 3 0 0 0 0 0 0 0 0 0 0 0 0 46 0 0 0 0 0 0 10 0 0 0 0 0 9 0 0 0 0 32 0 0 850 0 0 0 0 0 0 0 0 2 73 1796 0 +LFQ 28 3 0 0 0 0 0 0 0 0 0 0 0 0 45 0 0 0 0 1 0 6 0 0 0 0 0 7 0 0 0 0 39 2 0 848 0 0 0 0 0 0 0 0 0 73 1797 0 +LFQ 29 3 0 0 0 0 0 0 0 0 0 0 0 0 45 0 0 0 0 0 0 1 0 0 0 0 0 4 0 0 0 0 35 1 0 860 0 0 0 1 0 0 0 0 3 73 1795 0 +LFQ 30 3 0 0 0 0 0 0 0 0 0 0 0 0 39 0 0 0 0 0 0 2 0 0 0 0 0 8 0 0 0 0 34 0 0 863 0 0 0 0 0 0 0 0 2 69 1801 0 +LFQ 31 3 0 0 0 0 0 0 0 0 0 0 0 0 45 0 0 0 1 0 0 5 0 0 0 0 0 2 0 0 0 0 38 1 0 857 0 0 0 0 0 0 0 0 3 77 1789 0 +LFQ 32 3 0 0 0 0 0 0 0 0 0 0 0 0 46 0 0 0 0 0 0 5 0 0 0 0 0 3 0 0 0 0 37 2 0 855 0 0 0 0 0 0 0 0 2 82 1786 0 +LFQ 33 3 0 0 0 0 0 0 0 0 0 0 0 0 45 0 0 0 0 0 0 7 0 0 0 0 0 6 0 0 0 0 35 0 0 853 0 0 0 0 0 0 0 0 1 76 1795 0 +LFQ 34 3 0 0 0 0 0 0 0 0 0 0 0 0 43 0 0 0 0 0 0 11 0 0 0 0 0 10 0 0 0 0 38 2 0 844 0 0 0 1 0 0 0 0 0 85 1783 0 +LFQ 35 3 0 0 0 0 0 0 0 0 0 0 0 0 39 0 0 0 1 0 0 9 0 0 0 0 0 5 0 0 0 0 37 0 0 856 0 0 0 1 0 0 0 1 1 80 1787 0 +LFQ 36 3 0 1 0 0 0 0 0 0 0 0 0 0 41 0 0 0 0 0 0 8 0 0 0 0 0 6 0 0 0 0 33 2 0 856 0 0 0 1 0 0 0 0 1 72 1796 0 +LFQ 37 3 0 0 0 0 0 0 0 0 0 0 0 0 47 0 0 0 0 0 0 4 0 0 0 0 0 3 0 0 0 0 25 0 0 867 0 0 0 0 0 0 0 0 3 84 1784 0 +LFQ 38 3 0 0 0 0 0 0 0 0 0 0 0 0 54 0 0 0 0 0 0 8 0 0 0 0 0 8 0 0 0 0 27 0 0 849 0 0 0 0 0 0 0 0 1 85 1785 0 +LFQ 39 3 0 2 0 0 0 0 0 0 0 0 0 0 41 0 0 0 0 0 0 11 0 0 0 0 0 8 0 0 0 0 44 1 0 840 0 0 0 1 0 0 0 0 3 67 1799 0 +LFQ 40 3 0 0 0 0 0 0 0 0 0 0 0 0 51 0 0 0 0 0 0 5 0 0 0 0 0 10 0 0 0 0 41 2 0 838 0 0 0 1 0 0 0 1 1 93 1774 0 +LFQ 41 3 0 0 0 0 0 0 0 0 0 0 0 0 52 0 0 0 1 0 0 7 0 0 0 0 0 11 0 0 0 0 56 0 0 818 0 0 0 0 0 0 0 0 2 96 1774 0 +LFQ 42 3 0 0 0 0 0 0 0 0 0 0 0 0 46 0 0 0 0 0 0 5 0 0 0 0 0 10 0 0 0 0 35 1 0 848 0 0 0 1 0 0 0 2 3 102 1764 0 +LFQ 43 3 0 0 0 0 0 0 0 0 0 0 0 0 46 0 0 0 0 0 0 6 0 0 0 0 0 5 0 0 0 0 42 0 0 845 2 0 0 0 0 0 0 0 2 103 1766 0 +LFQ 44 3 0 0 0 1 0 0 0 0 0 0 0 0 40 0 0 0 0 0 0 10 0 0 0 0 0 11 0 0 0 0 51 0 0 834 0 0 0 0 0 0 0 0 3 97 1770 0 +LFQ 45 3 0 1 0 0 0 0 0 0 0 0 0 0 50 0 0 0 1 0 0 4 0 0 0 0 0 14 0 0 0 0 46 1 0 831 0 0 0 1 0 0 0 0 3 94 1771 0 +LFQ 46 3 0 0 0 0 0 0 0 0 0 0 0 0 59 0 0 0 0 0 0 10 0 0 0 0 0 9 0 0 0 0 33 0 0 835 0 0 0 1 0 0 0 0 4 104 1762 0 +LFQ 47 3 0 0 0 0 0 0 0 0 0 0 0 0 48 0 0 0 0 0 0 8 0 0 0 0 0 15 0 0 0 0 64 1 0 810 0 0 0 1 0 0 0 1 3 117 1749 0 +LFQ 48 3 0 0 0 0 0 0 0 0 0 0 0 0 43 0 0 0 0 0 0 7 0 0 0 0 0 14 0 0 0 0 62 3 0 820 0 0 0 0 0 0 0 0 3 103 1762 0 +LFQ 49 3 0 0 0 0 0 0 0 0 0 0 0 0 51 0 0 0 0 0 0 7 0 0 0 0 0 17 0 0 0 0 61 2 0 809 0 0 0 0 0 0 0 1 3 106 1760 0 +LFQ 50 3 0 0 0 1 0 0 0 0 0 0 0 0 48 0 0 0 0 0 0 4 0 0 0 0 0 18 0 0 0 0 56 3 0 818 0 0 0 1 0 0 0 2 3 132 1728 0 +LFQ 51 3 0 1 0 0 0 0 0 0 0 0 0 0 55 0 0 0 0 0 0 6 0 0 0 0 0 20 0 0 0 0 66 3 0 796 0 0 0 1 0 0 0 1 4 111 1750 0 +LFQ 52 3 0 0 0 0 0 0 0 0 0 0 0 0 47 0 0 0 0 0 0 5 0 0 0 0 0 36 0 0 0 0 56 0 0 799 0 0 0 0 0 0 0 0 3 139 1728 0 +LFQ 53 3 0 0 0 0 0 0 0 0 0 0 0 0 47 0 0 0 0 0 0 7 0 0 0 0 0 22 0 0 0 0 60 1 0 808 0 0 0 1 0 0 0 0 2 114 1749 0 +LFQ 54 3 0 0 0 0 0 0 0 0 0 0 0 0 51 0 0 0 0 0 0 5 0 0 0 0 0 21 0 0 0 0 62 0 0 803 0 0 0 1 0 0 0 2 1 109 1756 0 +LFQ 55 3 0 0 0 0 0 0 0 0 0 0 0 0 40 0 0 0 0 0 0 12 0 0 0 0 0 17 0 0 0 0 74 1 0 799 0 0 0 1 0 0 0 0 2 121 1743 0 +LFQ 56 3 0 0 0 0 0 0 0 0 0 0 0 0 49 0 0 0 0 0 0 9 0 0 0 0 0 16 0 0 0 0 60 1 0 808 0 0 0 0 0 0 0 0 2 134 1731 0 +LFQ 57 3 0 1 0 0 0 0 0 0 0 0 0 0 45 0 0 0 0 0 0 3 0 0 0 0 0 18 0 0 0 0 57 0 0 818 0 0 0 1 0 0 0 1 5 132 1729 0 +LFQ 58 3 0 0 0 0 0 0 0 0 0 0 0 0 51 0 0 0 0 0 0 2 0 0 0 0 0 11 0 0 0 0 52 1 0 826 0 0 0 2 0 0 0 0 6 107 1752 0 +LFQ 59 3 0 0 0 0 0 0 0 0 0 0 0 0 30 0 0 0 0 0 0 9 0 0 0 0 0 14 0 0 0 0 55 1 0 832 1 0 0 0 0 0 0 1 0 129 1737 0 +LFQ 60 3 0 0 0 0 0 0 0 0 0 0 0 0 35 0 0 0 0 0 0 8 0 0 0 0 0 14 0 0 0 0 70 0 0 813 0 0 0 0 0 0 0 0 6 118 1743 0 +LFQ 61 3 0 0 0 0 0 0 0 0 0 0 0 0 50 0 0 0 0 0 0 3 0 0 0 0 0 15 0 0 0 0 49 0 0 820 0 0 0 1 0 0 0 0 2 107 1759 0 +LFQ 62 3 0 0 0 0 0 0 0 0 0 0 0 0 35 0 0 0 1 0 0 9 0 0 0 0 0 21 0 0 0 0 67 1 0 808 0 0 0 0 0 0 0 1 2 121 1736 0 +LFQ 63 3 0 0 0 0 0 0 0 0 0 0 0 0 46 0 0 0 0 0 0 4 0 0 0 0 0 23 0 0 0 0 72 1 0 792 0 0 0 1 0 0 0 0 2 124 1736 0 +LFQ 64 3 0 0 0 0 0 0 0 0 0 0 0 0 58 0 0 0 0 0 0 6 0 0 0 0 0 19 0 0 0 0 78 2 0 772 0 0 0 1 0 0 0 0 2 111 1747 0 +LFQ 65 3 0 0 0 0 0 0 0 0 0 0 0 0 47 0 0 0 0 0 0 11 0 0 0 0 0 22 0 0 0 0 61 1 0 794 0 0 0 0 0 0 0 0 6 134 1720 0 +LFQ 66 2 0 0 0 0 0 0 0 0 0 0 0 0 46 0 0 0 0 0 0 8 0 0 0 0 0 25 0 0 0 0 53 1 0 801 0 0 0 1 0 0 0 1 1 135 1720 0 +LFQ 67 2 0 0 0 0 0 0 0 0 0 0 0 0 39 0 0 0 0 0 0 3 0 0 0 0 0 22 0 0 0 0 69 1 0 796 0 0 0 1 0 0 0 2 0 114 1743 0 +LFQ 68 2 0 0 0 0 0 0 0 0 0 0 0 0 40 0 0 0 0 0 0 6 0 0 0 0 0 28 0 0 0 0 60 0 0 792 0 0 0 1 0 0 0 0 3 128 1726 0 +LFQ 69 2 0 0 0 0 0 0 0 0 0 0 0 0 42 0 0 0 1 0 0 11 0 0 0 0 0 22 0 0 0 0 68 0 0 781 0 0 0 1 0 0 0 1 5 143 1705 0 +LFQ 70 3 0 0 0 0 0 0 0 0 0 0 0 0 47 0 0 0 0 0 0 7 0 0 0 0 0 31 0 0 0 0 57 2 0 778 0 0 0 0 0 0 0 1 4 127 1720 0 +LFQ 71 2 0 0 0 0 0 0 0 0 0 0 0 0 54 0 0 0 0 0 0 3 0 0 0 0 0 16 0 0 0 0 67 3 0 778 0 0 0 0 0 0 0 1 4 138 1701 0 +LFQ 72 2 0 1 0 0 0 0 0 0 0 0 0 0 49 0 0 0 0 0 0 6 0 0 0 0 0 18 0 0 0 0 64 2 0 777 0 0 0 1 0 0 0 0 5 139 1692 0 +LFQ 73 1 0 0 0 0 0 0 0 0 0 0 0 0 38 0 0 0 0 0 0 8 0 0 0 0 0 29 0 0 0 0 75 0 0 761 0 0 0 0 0 0 0 1 5 146 1685 0 +LFQ 74 2 0 1 0 1 0 0 0 0 0 0 0 0 54 0 0 0 0 0 0 9 0 0 0 0 0 13 0 0 0 0 84 0 0 745 0 0 0 0 0 0 0 2 0 145 1685 0 +LFQ 75 2 0 0 0 0 0 0 0 0 0 0 0 0 56 0 0 0 0 0 0 8 0 0 0 0 0 24 0 0 0 0 55 0 0 762 0 0 0 0 0 1 0 0 3 153 1673 0 +LFQ 76 2 0 0 0 0 0 0 0 0 0 0 0 0 40 0 0 0 0 0 0 15 0 0 0 0 0 18 0 0 0 0 80 2 0 749 0 0 0 0 0 0 0 1 6 146 1666 0 +LFQ 77 1 0 1 0 0 0 0 0 0 0 0 0 0 61 0 0 0 0 0 0 8 0 0 0 0 0 20 0 0 0 0 73 1 0 736 0 0 0 1 0 0 0 1 4 136 1671 0 +LFQ 78 1 0 0 0 0 0 0 0 0 0 0 0 0 56 0 0 0 0 0 0 16 0 0 0 0 0 21 0 0 0 0 68 1 0 724 0 0 0 1 0 0 0 0 4 148 1655 0 +LFQ 79 1 0 0 0 0 0 0 0 0 0 0 0 0 45 0 0 0 0 0 0 14 0 0 0 0 0 24 0 0 0 0 78 2 0 717 0 0 0 1 0 0 0 2 4 151 1641 0 +LFQ 80 2 0 0 0 0 0 0 0 0 0 0 0 0 51 0 0 0 0 0 0 7 0 0 0 0 0 37 0 0 0 0 76 1 0 700 0 0 0 0 0 0 0 1 6 146 1640 0 +LFQ 81 1 0 0 0 0 0 0 0 0 0 0 0 0 45 0 0 0 0 0 0 10 0 0 0 0 0 13 0 0 0 0 90 2 0 703 0 0 0 0 0 0 0 1 5 142 1638 0 +LFQ 82 2 0 0 0 1 0 0 0 0 0 0 0 0 54 0 0 0 0 0 0 6 0 0 0 0 1 25 0 0 0 0 74 0 0 688 0 0 0 4 0 0 0 0 5 154 1612 0 +LFQ 83 1 0 0 0 0 0 0 0 0 0 0 0 0 39 0 0 0 0 0 0 4 0 0 0 0 0 25 0 0 0 0 71 0 0 700 0 0 0 1 0 0 0 1 5 160 1601 0 +LFQ 84 1 0 0 0 0 0 0 0 0 0 0 0 0 54 0 0 0 0 0 0 10 0 0 0 0 0 20 0 0 0 0 70 1 0 675 0 0 0 0 0 0 0 2 9 135 1612 0 +LFQ 85 1 0 0 0 0 0 0 0 0 0 0 0 0 45 0 0 0 0 0 0 5 0 0 0 0 0 24 0 0 0 0 76 2 0 668 0 0 0 0 0 0 0 0 6 145 1592 0 +LFQ 86 0 0 0 0 0 0 0 0 0 0 0 0 0 45 0 0 0 0 0 0 9 0 0 0 0 0 29 0 0 0 0 84 3 0 642 0 0 0 0 0 0 0 3 6 139 1589 0 +LFQ 87 1 0 0 0 0 0 0 0 0 0 0 0 0 53 0 0 0 0 0 0 7 0 0 0 0 0 27 0 0 0 0 88 1 0 629 0 0 0 0 0 0 0 0 10 147 1562 0 +LFQ 88 1 0 0 0 0 0 0 0 0 0 0 0 0 38 0 0 0 1 0 0 7 0 0 0 0 0 25 0 0 0 0 83 2 0 637 0 0 0 0 0 0 0 3 7 122 1569 0 +LFQ 89 1 0 1 0 0 0 0 0 0 0 0 0 0 44 0 0 0 0 0 0 5 0 0 0 0 0 32 0 0 0 0 75 3 0 621 0 0 0 0 0 0 0 1 7 124 1552 0 +LFQ 90 1 0 0 0 0 0 0 0 0 0 0 0 0 55 0 0 0 0 0 0 2 0 0 0 0 0 20 0 0 0 0 57 0 0 637 0 0 0 0 0 0 0 2 3 137 1531 0 +LFQ 91 1 0 0 0 0 0 0 0 0 0 0 0 0 42 0 0 0 0 0 0 5 0 0 0 0 0 31 0 0 0 0 67 1 0 614 0 0 0 1 0 0 0 1 3 135 1528 0 +LFQ 92 1 0 1 0 0 0 0 0 0 0 0 0 0 40 0 0 0 0 0 0 3 0 0 0 0 0 24 0 0 0 0 67 3 0 616 0 0 0 1 0 0 0 2 2 149 1498 0 +LFQ 93 1 0 0 0 0 0 0 0 0 0 0 0 0 46 0 0 0 0 0 0 5 0 0 0 0 0 30 0 0 0 0 49 2 0 610 0 0 0 0 0 0 0 1 5 142 1486 0 +LFQ 94 0 0 1 0 0 0 0 0 0 0 0 0 0 48 0 0 0 0 0 0 4 0 0 0 0 0 20 0 0 0 0 73 2 0 588 0 0 0 1 0 0 0 1 8 149 1459 0 +LFQ 95 0 0 0 0 0 0 0 0 0 0 0 0 0 52 0 0 0 0 0 0 2 0 0 0 0 0 30 0 0 0 0 68 2 0 569 0 0 0 0 0 0 0 2 6 143 1460 0 +LFQ 96 1 0 0 0 0 0 0 0 0 0 0 0 0 45 0 0 0 0 0 0 9 0 0 0 0 0 32 0 0 0 0 74 0 0 544 0 0 0 0 0 0 0 1 5 149 1431 0 +LFQ 97 1 0 1 0 0 0 0 0 0 0 0 0 0 52 0 0 0 0 0 0 6 0 0 0 0 0 29 0 0 0 0 62 0 0 530 0 0 0 0 0 0 0 2 5 147 1402 0 +LFQ 98 1 0 0 0 0 0 0 0 0 0 0 0 0 50 0 0 0 0 0 0 10 0 0 0 0 0 27 0 0 0 0 61 2 0 520 0 0 0 2 0 0 0 4 4 148 1374 0 +LFQ 99 1 0 0 0 0 0 0 0 0 0 0 0 0 46 0 0 0 0 0 0 5 0 0 0 0 0 28 0 0 0 0 58 0 0 518 0 0 0 0 0 0 0 1 6 152 1360 0 +LFQ 100 0 0 0 0 0 0 0 0 0 0 0 0 0 39 0 0 0 0 0 0 5 0 0 0 0 0 32 0 0 0 0 66 1 0 502 0 0 0 0 0 0 0 4 7 138 1357 0 +LFQ 101 1 0 1 0 0 0 0 0 0 0 0 0 0 35 0 0 0 0 0 0 6 0 0 0 0 1 38 0 0 0 0 80 0 0 466 0 0 0 0 0 0 0 0 11 143 1325 0 +LFQ 102 1 0 0 0 0 0 0 0 0 0 0 0 0 39 0 0 0 0 0 0 3 0 0 0 0 0 36 0 0 0 0 73 0 0 466 0 0 0 0 0 0 0 1 6 150 1308 0 +LFQ 103 1 0 0 0 0 0 0 0 0 0 0 0 0 40 0 0 0 0 0 0 6 0 0 0 0 0 42 0 0 0 0 77 2 0 445 0 0 0 1 0 0 0 2 4 144 1292 0 +LFQ 104 0 0 0 0 0 0 0 0 0 0 0 0 0 38 0 0 0 1 0 0 6 0 0 0 0 0 46 0 0 0 0 70 1 0 444 0 0 0 0 0 0 0 1 9 149 1269 0 +LFQ 105 0 0 0 0 0 0 0 0 0 0 0 0 0 38 0 0 0 0 0 0 4 0 0 0 0 0 30 0 0 0 0 83 3 0 432 0 0 0 0 0 0 0 6 5 175 1225 0 +LFQ 106 0 0 0 0 1 0 0 0 0 0 0 0 0 39 0 0 0 0 0 0 4 0 0 0 0 0 37 0 0 0 0 81 3 0 417 0 0 0 0 0 0 0 2 4 186 1201 0 +LFQ 107 0 0 1 0 0 0 0 0 0 0 0 0 0 38 0 0 0 0 0 0 6 0 0 0 0 0 43 0 0 0 0 81 0 0 400 0 0 0 1 0 0 0 1 8 161 1207 0 +LFQ 108 0 0 2 0 0 0 0 0 0 0 0 0 0 33 0 0 0 1 0 0 3 0 0 0 0 0 47 0 0 0 0 108 2 0 365 0 0 0 0 0 0 0 2 3 161 1185 0 +LFQ 109 1 0 0 0 0 0 0 0 0 0 0 0 0 35 0 0 0 1 0 0 2 0 0 0 0 0 45 0 0 0 0 107 0 0 353 0 0 0 0 0 0 0 2 7 178 1155 0 +LFQ 110 1 0 0 0 1 0 0 0 0 0 0 0 0 36 0 0 0 0 0 0 10 0 0 0 0 0 44 0 0 0 0 90 2 0 352 0 0 0 0 0 0 0 4 10 166 1146 0 +LFQ 111 0 0 0 0 0 0 0 0 1 0 0 0 0 30 0 0 0 0 0 0 9 0 0 0 0 0 36 0 0 0 0 124 0 0 322 0 0 0 0 0 0 0 3 5 182 1116 0 +LFQ 112 1 0 0 0 0 0 0 0 0 0 0 0 0 31 0 0 0 0 0 0 10 0 0 0 0 0 33 0 0 0 0 118 2 0 322 0 0 0 0 0 0 0 4 7 182 1089 0 +LFQ 113 0 0 0 0 0 0 0 0 0 0 0 0 0 35 0 0 0 0 0 0 11 0 0 0 0 0 42 0 0 0 0 110 2 0 305 0 0 0 1 0 0 0 2 11 194 1064 0 +LFQ 114 0 0 0 0 0 0 0 0 0 0 0 0 0 38 0 0 0 1 0 0 7 0 0 0 0 0 44 0 0 0 0 120 1 0 285 0 0 0 1 0 0 0 6 9 190 1039 0 +LFQ 115 1 0 0 0 0 0 0 0 0 0 0 0 0 31 0 0 0 2 0 0 12 0 0 0 0 0 39 0 0 0 0 114 2 0 287 0 0 0 1 0 0 0 1 10 187 1024 0 +LFQ 116 1 0 0 0 0 0 0 0 0 0 0 0 0 38 0 0 0 0 0 0 5 0 0 0 0 0 44 0 0 0 0 117 1 0 266 0 0 0 0 0 0 0 4 7 183 997 0 +LFQ 117 0 0 0 0 0 0 0 1 0 0 0 0 0 33 0 0 0 2 0 0 9 0 0 0 0 0 43 0 0 0 0 102 1 0 273 0 0 0 0 0 0 0 3 4 164 992 0 +LFQ 118 0 0 0 0 0 0 0 0 0 0 0 0 0 34 0 0 0 1 0 0 6 0 0 0 0 0 46 0 0 0 0 98 0 0 266 0 0 0 0 0 0 0 2 5 180 956 0 +LFQ 119 0 0 1 0 0 0 0 0 0 0 0 0 0 35 0 0 0 0 0 0 8 0 0 0 0 0 37 0 0 0 0 110 1 0 249 0 0 0 1 0 0 0 2 9 161 946 0 +LFQ 120 0 0 0 0 0 0 0 0 0 0 0 0 0 30 0 0 0 1 0 0 11 0 0 0 0 0 36 0 0 0 0 97 2 0 256 0 0 0 1 0 0 0 4 8 163 914 0 +LFQ 121 0 0 0 0 0 0 0 0 0 0 0 0 0 33 0 0 0 1 0 0 13 0 0 0 0 0 33 0 0 0 0 103 0 0 237 0 0 0 0 0 0 0 3 6 162 917 0 +LFQ 122 0 0 0 0 0 0 0 0 0 0 0 0 0 38 0 0 0 0 0 0 6 0 0 0 0 0 31 0 0 0 0 105 0 0 225 0 0 0 0 0 0 0 0 6 147 911 0 +LFQ 123 0 0 0 0 0 0 0 0 0 0 0 0 0 30 0 0 0 0 0 0 13 0 0 0 0 0 31 0 0 0 0 107 0 0 218 0 0 0 0 0 0 0 2 6 153 879 0 +LFQ 124 0 0 0 0 0 0 0 0 0 0 0 0 0 28 0 0 0 0 0 0 9 0 0 0 0 0 33 0 0 0 0 91 1 0 228 0 0 0 2 0 0 0 2 8 158 851 0 +LFQ 125 0 0 0 0 0 0 0 0 0 0 0 0 0 29 0 0 0 1 0 0 8 0 0 0 0 0 29 0 0 0 0 113 0 0 193 0 0 0 0 0 0 0 3 8 155 836 0 +LFQ 126 0 0 0 0 0 0 0 0 0 0 0 0 0 36 0 0 0 0 0 0 11 0 0 0 0 0 27 0 0 0 0 84 0 0 202 0 0 0 1 0 0 1 3 8 146 820 0 +LFQ 127 1 0 0 0 1 0 0 0 0 0 0 0 0 27 0 0 0 0 0 0 12 0 0 0 0 0 35 0 0 0 0 96 1 0 186 0 0 0 0 0 0 0 2 4 138 812 0 +LFQ 128 1 0 0 0 0 0 0 0 0 0 0 0 0 28 0 0 0 1 0 0 7 0 0 0 0 0 26 0 0 0 0 80 0 0 200 0 0 0 0 0 0 0 2 13 141 785 0 +LFQ 129 0 0 0 0 0 0 0 1 0 0 1 0 0 34 0 0 0 1 0 0 6 0 0 0 0 0 27 0 0 0 0 75 3 0 192 0 0 0 0 0 0 0 1 7 138 767 0 +LFQ 130 0 0 1 0 0 0 0 0 0 0 0 0 0 27 0 0 0 0 0 0 4 0 0 0 0 0 28 0 0 0 0 79 2 0 184 0 0 0 0 0 0 0 4 11 133 752 0 +LFQ 131 0 0 0 0 0 0 0 0 0 0 0 0 0 23 0 0 0 1 0 0 6 0 0 0 0 0 21 0 0 0 0 82 1 0 182 0 0 0 1 0 0 0 3 7 113 746 0 +LFQ 132 0 0 0 0 0 0 0 0 0 0 0 0 0 19 0 0 0 0 0 0 9 0 0 0 0 0 18 0 0 0 0 71 0 0 186 0 0 0 0 0 0 0 0 5 134 699 0 +LFQ 133 0 0 0 0 0 0 0 0 0 0 0 0 0 25 0 0 0 0 0 0 3 0 0 0 0 0 25 0 0 0 0 83 0 0 160 0 0 0 1 0 0 0 3 5 112 699 0 +LFQ 134 0 0 0 0 0 0 0 0 0 0 0 0 0 26 0 0 0 1 0 0 5 0 0 0 0 0 26 0 0 0 0 70 1 0 160 0 0 0 0 0 0 0 0 9 115 684 0 +LFQ 135 0 0 0 0 0 0 0 0 0 0 0 0 0 21 0 0 0 0 0 0 6 0 0 0 0 0 26 0 0 0 0 70 0 0 152 2 0 0 0 0 0 0 0 5 115 675 0 +LFQ 136 0 0 0 0 0 0 0 0 0 0 0 0 0 26 0 0 0 1 0 0 7 0 0 0 0 0 15 0 0 0 0 71 0 0 144 0 0 0 1 0 0 0 0 3 111 662 0 +LFQ 137 0 0 0 0 0 0 0 0 0 0 0 0 0 25 0 0 0 1 0 0 6 0 0 0 0 0 21 0 0 0 0 62 0 0 138 0 0 0 0 0 0 0 4 5 103 646 0 +LFQ 138 0 0 0 0 0 0 0 0 0 0 0 0 0 17 0 0 0 0 0 0 4 0 0 0 0 0 21 0 0 0 0 65 1 0 142 1 0 0 0 0 0 0 2 5 111 613 0 +LFQ 139 0 0 0 0 0 0 0 0 0 0 0 0 0 15 0 0 0 0 0 0 10 0 0 0 0 0 15 0 0 0 0 66 0 0 134 0 0 0 1 0 0 0 3 4 109 591 0 +LFQ 140 0 0 0 0 0 0 0 0 0 0 0 0 0 25 0 0 0 0 0 0 5 0 0 0 0 0 12 0 0 0 0 59 0 0 127 0 0 0 1 0 0 0 2 5 97 583 0 +LFQ 141 0 0 0 0 0 0 0 0 0 0 0 0 0 18 0 0 0 0 0 0 5 0 0 0 0 0 16 0 0 0 0 64 0 0 119 0 0 0 0 0 0 0 2 6 84 574 0 +LFQ 142 0 0 0 0 0 0 0 0 0 0 0 0 0 20 0 0 0 1 0 0 3 0 0 0 0 0 20 0 0 0 0 47 0 0 117 0 0 0 1 0 0 0 1 2 88 547 0 +LFQ 143 0 0 5 1 0 0 0 0 0 0 0 1 0 71 0 1 0 5 4 0 12 0 0 0 0 3 34 0 0 0 0 52 18 1 36 2 1 0 5 0 0 1 13 14 124 416 0 +# GC Content of first fragments. Use `grep ^GCF | cut -f 2-` to extract this part. +GCF 7.29 0 +GCF 15.08 1 +GCF 17.09 0 +GCF 18.84 1 +GCF 19.35 2 +GCF 19.85 1 +GCF 20.35 0 +GCF 20.85 5 +GCF 21.36 4 +GCF 21.86 6 +GCF 22.36 20 +GCF 22.86 14 +GCF 23.37 23 +GCF 23.87 57 +GCF 24.37 86 +GCF 24.87 83 +GCF 25.38 107 +GCF 25.88 104 +GCF 26.38 78 +GCF 26.88 77 +GCF 27.39 125 +GCF 27.89 201 +GCF 28.39 258 +GCF 28.89 336 +GCF 29.40 353 +GCF 29.90 254 +GCF 30.40 213 +GCF 30.90 217 +GCF 31.41 195 +GCF 31.91 203 +GCF 32.41 177 +GCF 32.91 153 +GCF 33.42 160 +GCF 33.92 130 +GCF 34.42 104 +GCF 34.92 82 +GCF 35.43 96 +GCF 35.93 100 +GCF 36.43 106 +GCF 36.93 136 +GCF 37.44 138 +GCF 37.94 113 +GCF 38.44 73 +GCF 38.94 35 +GCF 39.45 16 +GCF 39.95 12 +GCF 40.45 8 +GCF 40.95 11 +GCF 41.46 13 +GCF 42.21 11 +GCF 42.96 13 +GCF 43.47 15 +GCF 43.97 12 +GCF 44.47 13 +GCF 44.97 12 +GCF 45.48 10 +GCF 45.98 12 +GCF 46.48 10 +GCF 46.98 5 +GCF 47.49 13 +GCF 47.99 14 +GCF 48.49 13 +GCF 49.25 4 +GCF 50.00 3 +GCF 50.50 2 +GCF 51.01 1 +# GC Content of last fragments. Use `grep ^GCL | cut -f 2-` to extract this part. +GCL 5.53 0 +GCL 11.31 1 +GCL 13.07 0 +GCL 14.82 1 +GCL 15.83 0 +GCL 17.09 2 +GCL 18.09 1 +GCL 18.84 0 +GCL 19.35 1 +GCL 19.85 0 +GCL 20.35 2 +GCL 20.85 6 +GCL 21.36 5 +GCL 21.86 6 +GCL 22.61 16 +GCL 23.37 24 +GCL 23.87 58 +GCL 24.37 94 +GCL 24.87 90 +GCL 25.38 110 +GCL 25.88 106 +GCL 26.38 85 +GCL 26.88 79 +GCL 27.39 120 +GCL 27.89 210 +GCL 28.39 262 +GCL 29.15 342 +GCL 29.90 254 +GCL 30.40 211 +GCL 30.90 227 +GCL 31.41 196 +GCL 31.91 203 +GCL 32.41 176 +GCL 32.91 155 +GCL 33.42 153 +GCL 33.92 122 +GCL 34.42 94 +GCL 34.92 75 +GCL 35.43 96 +GCL 35.93 100 +GCL 36.43 103 +GCL 37.19 141 +GCL 37.94 111 +GCL 38.44 65 +GCL 38.94 34 +GCL 39.45 19 +GCL 39.95 10 +GCL 40.45 5 +GCL 40.95 10 +GCL 41.46 16 +GCL 41.96 12 +GCL 42.46 10 +GCL 42.96 13 +GCL 43.47 15 +GCL 44.22 12 +GCL 45.23 10 +GCL 45.98 13 +GCL 46.48 10 +GCL 46.98 5 +GCL 47.49 12 +GCL 47.99 13 +GCL 48.49 12 +GCL 49.50 3 +GCL 50.50 2 +GCL 51.01 1 +# ACGT content per cycle. Use `grep ^GCC | cut -f 2-` to extract this part. The columns are: cycle; A,C,G,T base counts as a percentage of all A/C/G/T bases [%]; and N and O counts as a percentage of all A/C/G/T bases [%] +GCC 1 33.73 15.23 16.08 34.95 0.05 0.00 +GCC 2 33.26 15.50 17.56 33.68 0.07 0.00 +GCC 3 34.59 15.25 15.74 34.41 0.04 0.00 +GCC 4 34.25 15.59 14.81 35.35 0.07 0.00 +GCC 5 34.04 15.51 14.65 35.80 0.04 0.00 +GCC 6 34.55 15.18 14.90 35.37 0.07 0.00 +GCC 7 33.79 15.99 15.21 35.00 0.04 0.00 +GCC 8 33.09 15.24 16.00 35.66 0.11 0.00 +GCC 9 34.44 14.22 15.75 35.58 0.07 0.00 +GCC 10 34.60 13.75 16.53 35.12 0.07 0.00 +GCC 11 34.33 14.12 15.87 35.68 0.05 0.00 +GCC 12 34.09 13.63 16.18 36.10 0.12 0.00 +GCC 13 33.74 13.78 18.14 34.34 0.07 0.00 +GCC 14 33.91 13.41 17.33 35.35 0.07 0.00 +GCC 15 33.43 15.10 16.95 34.52 0.12 0.00 +GCC 16 35.00 14.64 16.58 33.78 0.14 0.00 +GCC 17 33.22 13.89 16.64 36.25 0.11 0.00 +GCC 18 33.53 14.44 17.40 34.63 0.09 0.00 +GCC 19 32.82 14.78 16.44 35.96 0.09 0.00 +GCC 20 32.91 14.01 15.13 37.95 0.09 0.00 +GCC 21 32.48 14.01 15.89 37.62 0.07 0.00 +GCC 22 33.20 13.15 17.02 36.64 0.11 0.00 +GCC 23 33.37 13.15 16.96 36.52 0.11 0.00 +GCC 24 33.42 14.32 16.55 35.71 0.09 0.00 +GCC 25 34.75 14.16 15.39 35.69 0.14 0.00 +GCC 26 34.77 14.74 15.86 34.63 0.09 0.00 +GCC 27 34.44 14.62 16.41 34.53 0.11 0.00 +GCC 28 33.81 14.80 16.60 34.79 0.14 0.00 +GCC 29 32.78 15.11 17.35 34.76 0.07 0.00 +GCC 30 33.59 14.01 16.28 36.11 0.07 0.00 +GCC 31 31.51 15.00 16.46 37.03 0.14 0.00 +GCC 32 32.85 15.33 15.53 36.29 0.11 0.00 +GCC 33 33.03 14.83 14.74 37.40 0.18 0.00 +GCC 34 31.55 14.77 15.43 38.25 0.14 0.00 +GCC 35 31.62 13.70 17.11 37.56 0.12 0.00 +GCC 36 33.35 13.99 16.95 35.71 0.11 0.00 +GCC 37 34.93 13.37 15.30 36.40 0.11 0.00 +GCC 38 32.95 15.35 16.29 35.40 0.09 0.00 +GCC 39 32.83 14.42 16.44 36.31 0.14 0.00 +GCC 40 32.53 14.47 17.47 35.53 0.11 0.00 +GCC 41 32.82 14.00 16.47 36.71 0.11 0.00 +GCC 42 32.08 15.16 14.97 37.80 0.11 0.00 +GCC 43 32.53 14.39 15.71 37.36 0.09 0.00 +GCC 44 33.32 13.78 16.18 36.71 0.16 0.00 +GCC 45 34.93 14.79 15.20 35.07 0.14 0.00 +GCC 46 33.30 14.49 15.91 36.30 0.09 0.00 +GCC 47 33.27 14.41 15.58 36.74 0.16 0.00 +GCC 48 33.61 13.94 16.09 36.36 0.14 0.00 +GCC 49 33.68 14.10 16.10 36.13 0.09 0.00 +GCC 50 34.40 14.52 16.02 35.06 0.12 0.00 +GCC 51 32.24 16.31 15.61 35.85 0.14 0.00 +GCC 52 30.68 14.79 17.46 37.07 0.09 0.00 +GCC 53 32.12 15.12 17.20 35.56 0.07 0.00 +GCC 54 32.50 15.04 16.66 35.81 0.12 0.00 +GCC 55 31.01 16.58 17.31 35.10 0.16 0.00 +GCC 56 31.83 15.29 16.65 36.23 0.12 0.00 +GCC 57 31.48 15.42 15.83 37.27 0.14 0.00 +GCC 58 33.02 14.52 15.02 37.44 0.18 0.00 +GCC 59 31.54 14.75 16.56 37.15 0.12 0.00 +GCC 60 30.03 14.95 16.88 38.14 0.12 0.00 +GCC 61 30.74 15.64 16.51 37.12 0.12 0.00 +GCC 62 31.90 16.15 15.32 36.63 0.11 0.00 +GCC 63 32.40 14.53 15.56 37.51 0.18 0.00 +GCC 64 30.36 15.07 16.43 38.15 0.13 0.00 +GCC 65 30.70 14.89 16.59 37.82 0.16 0.00 +GCC 66 30.54 16.30 15.44 37.71 0.07 0.00 +GCC 67 30.73 16.55 14.74 37.98 0.09 0.00 +GCC 68 30.99 14.80 16.96 37.26 0.05 0.00 +GCC 69 29.97 15.79 16.10 38.14 0.07 0.00 +GCC 70 29.88 15.26 16.44 38.42 0.07 0.00 +GCC 71 29.89 15.82 16.20 38.08 0.05 0.00 +GCC 72 31.27 16.51 15.56 36.66 0.07 0.00 +GCC 73 30.37 15.27 15.12 39.24 0.04 0.00 +GCC 74 30.08 14.71 15.41 39.80 0.05 0.00 +GCC 75 30.67 14.94 14.61 39.78 0.09 0.00 +GCC 76 29.79 14.84 15.43 39.93 0.09 0.00 +GCC 77 29.44 14.93 16.17 39.47 0.04 0.00 +GCC 78 29.81 15.62 16.34 38.22 0.09 0.00 +GCC 79 31.83 14.51 14.84 38.82 0.06 0.00 +GCC 80 29.94 15.88 14.58 39.60 0.09 0.00 +GCC 81 31.19 15.92 15.18 37.71 0.06 0.00 +GCC 82 31.66 16.19 15.34 36.81 0.04 0.00 +GCC 83 31.18 16.06 13.76 39.01 0.06 0.00 +GCC 84 30.96 16.06 12.89 40.10 0.04 0.00 +GCC 85 30.01 15.23 13.24 41.53 0.10 0.00 +GCC 86 30.81 15.86 14.66 38.68 0.02 0.00 +GCC 87 30.47 16.15 12.72 40.66 0.04 0.00 +GCC 88 31.11 16.50 12.35 40.04 0.02 0.00 +GCC 89 30.24 15.85 11.73 42.19 0.06 0.00 +GCC 90 31.22 14.87 12.36 41.55 0.02 0.00 +GCC 91 30.23 17.01 12.89 39.87 0.02 0.00 +GCC 92 31.36 14.43 13.43 40.78 0.06 0.00 +GCC 93 32.06 15.47 13.66 38.81 0.02 0.00 +GCC 94 32.56 14.86 14.96 37.62 0.02 0.00 +GCC 95 31.75 15.88 14.36 38.01 0.02 0.00 +GCC 96 32.25 16.62 12.71 38.43 0.02 0.00 +GCC 97 31.78 16.01 15.10 37.11 0.04 0.00 +GCC 98 31.47 15.10 15.78 37.65 0.02 0.00 +GCC 99 32.77 16.08 14.79 36.36 0.02 0.00 +GCC 100 31.18 16.81 13.53 38.48 0.00 0.00 +GCC 101 32.53 15.79 12.82 38.85 0.05 0.00 +GCC 102 34.85 14.89 13.38 36.89 0.02 0.00 +GCC 103 33.09 15.40 14.57 36.93 0.02 0.00 +GCC 104 34.25 13.60 14.78 37.37 0.00 0.00 +GCC 105 34.82 15.70 13.12 36.37 0.00 0.00 +GCC 106 36.36 14.74 13.60 35.30 0.00 0.00 +GCC 107 34.51 15.11 13.95 36.43 0.03 0.00 +GCC 108 32.09 16.88 14.37 36.65 0.05 0.00 +GCC 109 33.64 14.78 14.09 37.49 0.03 0.00 +GCC 110 35.15 14.14 15.88 34.83 0.03 0.00 +GCC 111 32.46 15.41 15.41 36.73 0.00 0.00 +GCC 112 30.38 16.47 14.49 38.66 0.03 0.00 +GCC 113 30.38 16.75 13.57 39.30 0.00 0.00 +GCC 114 31.95 16.24 15.52 36.29 0.00 0.00 +GCC 115 31.65 16.61 16.00 35.74 0.03 0.00 +GCC 116 32.63 15.92 13.73 37.72 0.03 0.00 +GCC 117 30.30 15.50 13.72 40.48 0.00 0.00 +GCC 118 31.59 15.86 13.78 38.78 0.00 0.00 +GCC 119 29.82 15.70 15.86 38.62 0.06 0.00 +GCC 120 32.51 15.42 14.86 37.21 0.00 0.00 +GCC 121 30.50 16.06 15.80 37.64 0.00 0.00 +GCC 122 31.72 16.32 15.67 36.29 0.00 0.00 +GCC 123 31.69 15.51 15.51 37.29 0.00 0.00 +GCC 124 31.50 15.71 16.74 36.04 0.00 0.00 +GCC 125 31.78 15.14 16.82 36.26 0.00 0.00 +GCC 126 33.98 16.26 14.92 34.84 0.00 0.00 +GCC 127 36.59 13.61 14.98 34.83 0.04 0.00 +GCC 128 31.58 16.32 16.04 36.07 0.04 0.00 +GCC 129 32.15 14.51 15.23 38.10 0.00 0.00 +GCC 130 32.32 14.77 14.65 38.26 0.04 0.00 +GCC 131 33.14 14.71 15.77 36.39 0.00 0.00 +GCC 132 31.94 14.32 16.61 37.13 0.00 0.00 +GCC 133 34.86 13.79 15.59 35.76 0.00 0.00 +GCC 134 32.53 15.71 18.55 33.21 0.00 0.00 +GCC 135 30.59 14.64 17.77 37.00 0.00 0.00 +GCC 136 31.60 13.15 18.79 36.46 0.00 0.00 +GCC 137 31.35 13.59 18.50 36.56 0.00 0.00 +GCC 138 33.20 12.92 20.58 33.30 0.00 0.00 +GCC 139 33.17 13.39 18.15 35.29 0.00 0.00 +GCC 140 33.84 12.65 17.74 35.76 0.00 0.00 +GCC 141 32.37 12.77 17.18 37.68 0.00 0.00 +GCC 142 33.29 12.32 17.83 36.55 0.00 0.00 +GCC 143 32.56 11.24 16.09 40.11 0.37 0.00 +# ACGT content per cycle, read oriented. Use `grep ^GCT | cut -f 2-` to extract this part. The columns are: cycle; A,C,G,T base counts as a percentage of all A/C/G/T bases [%] +GCT 1 36.58 12.84 18.48 32.10 +GCT 2 36.22 13.80 19.26 30.72 +GCT 3 37.11 13.23 17.77 31.90 +GCT 4 36.98 13.14 17.26 32.62 +GCT 5 38.07 12.38 17.78 31.77 +GCT 6 37.81 12.27 17.81 32.10 +GCT 7 36.21 12.91 18.30 32.59 +GCT 8 35.72 13.64 17.60 33.04 +GCT 9 36.34 12.08 17.90 33.68 +GCT 10 37.67 12.10 18.18 32.05 +GCT 11 36.48 12.75 17.24 33.53 +GCT 12 36.45 12.69 17.13 33.74 +GCT 13 35.19 14.35 17.58 32.88 +GCT 14 35.86 12.95 17.79 33.40 +GCT 15 36.82 14.00 18.05 31.13 +GCT 16 37.26 13.01 18.21 31.52 +GCT 17 37.51 13.08 17.46 31.96 +GCT 18 35.16 14.12 17.72 33.00 +GCT 19 35.16 14.78 16.44 33.62 +GCT 20 34.03 13.30 15.84 36.83 +GCT 21 35.12 12.75 17.15 34.98 +GCT 22 34.87 13.08 17.09 34.97 +GCT 23 34.23 13.27 16.84 35.66 +GCT 24 34.10 13.18 17.69 35.04 +GCT 25 34.81 13.03 16.52 35.64 +GCT 26 34.86 13.54 17.07 34.54 +GCT 27 34.40 13.54 17.49 34.56 +GCT 28 33.32 13.45 17.94 35.29 +GCT 29 34.23 14.47 17.99 33.31 +GCT 30 34.50 13.53 16.76 35.21 +GCT 31 35.20 13.78 17.68 33.34 +GCT 32 32.78 14.18 16.68 36.36 +GCT 33 34.98 13.09 16.48 35.45 +GCT 34 33.88 13.58 16.62 35.92 +GCT 35 33.39 14.11 16.71 35.79 +GCT 36 32.46 13.61 17.32 36.60 +GCT 37 34.27 12.07 16.60 37.06 +GCT 38 34.06 13.54 18.10 34.30 +GCT 39 33.98 13.46 17.40 35.16 +GCT 40 33.55 15.02 16.92 34.52 +GCT 41 35.20 14.57 15.90 34.33 +GCT 42 34.00 13.72 16.40 35.88 +GCT 43 34.03 14.00 16.10 35.87 +GCT 44 34.12 14.23 15.74 35.91 +GCT 45 35.14 14.83 15.17 34.86 +GCT 46 34.12 15.13 15.27 35.49 +GCT 47 34.39 14.60 15.38 35.62 +GCT 48 34.99 14.97 15.06 34.97 +GCT 49 35.63 14.56 15.64 34.17 +GCT 50 36.43 14.61 15.93 33.03 +GCT 51 34.32 15.65 16.27 33.77 +GCT 52 35.36 16.62 15.63 32.39 +GCT 53 35.01 15.21 17.11 32.68 +GCT 54 35.63 15.93 15.77 32.67 +GCT 55 33.93 16.85 17.04 32.18 +GCT 56 36.03 15.86 16.08 32.03 +GCT 57 35.72 15.46 15.79 33.03 +GCT 58 36.44 14.57 14.96 34.02 +GCT 59 34.96 16.46 14.85 33.73 +GCT 60 34.75 16.61 15.22 33.42 +GCT 61 34.73 16.81 15.33 33.13 +GCT 62 36.49 15.41 16.07 32.04 +GCT 63 35.69 14.65 15.44 34.22 +GCT 64 33.57 15.32 16.17 34.94 +GCT 65 35.42 15.12 16.36 33.09 +GCT 66 34.43 15.55 16.19 33.82 +GCT 67 36.56 16.42 14.86 32.15 +GCT 68 35.24 16.51 15.25 33.00 +GCT 69 35.89 16.19 15.70 32.22 +GCT 70 35.74 15.39 16.31 32.56 +GCT 71 35.12 15.48 16.55 32.86 +GCT 72 36.28 16.36 15.71 31.65 +GCT 73 36.49 15.27 15.12 33.12 +GCT 74 36.00 15.06 15.06 33.88 +GCT 75 35.02 14.25 15.31 35.42 +GCT 76 33.67 15.61 14.66 36.06 +GCT 77 33.99 15.78 15.32 34.91 +GCT 78 34.47 16.31 15.66 33.56 +GCT 79 35.53 15.03 14.32 35.12 +GCT 80 34.22 16.55 13.91 35.32 +GCT 81 34.06 16.94 14.16 34.84 +GCT 82 33.49 16.12 15.41 34.98 +GCT 83 33.37 16.39 13.43 36.82 +GCT 84 34.51 16.41 12.54 36.54 +GCT 85 35.36 15.54 12.92 36.18 +GCT 86 35.87 16.56 13.95 33.61 +GCT 87 36.14 16.15 12.72 34.99 +GCT 88 34.86 15.78 13.07 36.29 +GCT 89 36.49 15.48 12.09 35.94 +GCT 90 35.66 14.38 12.85 37.11 +GCT 91 34.51 16.17 13.74 35.58 +GCT 92 36.35 15.74 12.12 35.79 +GCT 93 37.04 15.83 13.30 33.82 +GCT 94 36.03 16.49 13.33 34.16 +GCT 95 35.13 15.86 14.38 34.62 +GCT 96 36.29 15.90 13.43 34.39 +GCT 97 34.38 16.04 15.07 34.51 +GCT 98 34.97 16.60 14.28 34.15 +GCT 99 35.63 17.13 13.73 33.51 +GCT 100 35.04 17.97 12.37 34.62 +GCT 101 36.07 16.41 12.21 35.31 +GCT 102 35.95 15.95 12.32 35.78 +GCT 103 35.69 16.42 13.55 34.33 +GCT 104 36.71 15.83 12.54 34.92 +GCT 105 35.89 16.97 11.85 35.29 +GCT 106 37.78 16.00 12.33 33.88 +GCT 107 34.71 16.75 12.31 36.23 +GCT 108 33.61 16.91 14.35 35.13 +GCT 109 33.72 15.23 13.64 37.41 +GCT 110 33.73 16.96 13.06 36.25 +GCT 111 33.42 18.23 12.59 35.77 +GCT 112 33.35 17.52 13.44 35.69 +GCT 113 33.19 16.78 13.54 36.49 +GCT 114 31.64 17.33 14.43 36.61 +GCT 115 31.76 18.46 14.16 35.62 +GCT 116 33.38 16.35 13.31 36.97 +GCT 117 31.41 16.15 13.07 39.37 +GCT 118 34.41 16.99 12.65 35.95 +GCT 119 32.94 19.20 12.36 35.51 +GCT 120 33.20 17.23 13.05 36.52 +GCT 121 32.36 18.62 13.24 35.78 +GCT 122 31.69 18.88 13.12 36.32 +GCT 123 30.82 18.61 12.42 38.16 +GCT 124 30.51 19.76 12.70 37.03 +GCT 125 32.33 18.89 13.07 35.71 +GCT 126 31.78 18.09 13.08 37.05 +GCT 127 35.79 16.65 11.93 35.63 +GCT 128 31.73 17.56 14.79 35.91 +GCT 129 32.59 17.07 12.67 37.66 +GCT 130 33.80 17.72 11.70 36.78 +GCT 131 34.32 17.75 12.72 35.21 +GCT 132 33.96 18.59 12.35 35.11 +GCT 133 34.23 15.86 13.52 36.39 +GCT 134 30.65 19.64 14.62 35.08 +GCT 135 32.13 18.62 13.80 35.45 +GCT 136 30.97 18.93 13.01 37.09 +GCT 137 30.41 17.96 14.14 37.50 +GCT 138 30.75 20.17 13.33 35.75 +GCT 139 32.65 18.89 12.65 35.82 +GCT 140 33.73 18.57 11.83 35.87 +GCT 141 32.09 16.33 13.62 37.97 +GCT 142 32.41 16.47 13.68 37.44 +GCT 143 32.43 16.71 10.63 40.23 +# ACGT content per cycle for first fragments. Use `grep ^FBC | cut -f 2-` to extract this part. The columns are: cycle; A,C,G,T base counts as a percentage of all A/C/G/T bases [%]; and N and O counts as a percentage of all A/C/G/T bases [%] +FBC 1 34.09 15.25 16.11 34.55 0.07 0.00 +FBC 2 33.51 15.96 16.74 33.79 0.04 0.00 +FBC 3 34.70 14.46 15.92 34.92 0.00 0.00 +FBC 4 34.08 15.92 14.79 35.21 0.04 0.00 +FBC 5 34.42 15.38 15.03 35.16 0.00 0.00 +FBC 6 34.72 14.72 15.14 35.43 0.04 0.00 +FBC 7 34.46 15.88 15.31 34.35 0.00 0.00 +FBC 8 33.88 15.04 15.75 35.33 0.07 0.00 +FBC 9 34.04 14.61 15.78 35.57 0.04 0.00 +FBC 10 34.82 13.16 16.95 35.07 0.04 0.00 +FBC 11 34.10 14.14 15.60 36.16 0.00 0.00 +FBC 12 34.68 13.35 16.19 35.78 0.14 0.00 +FBC 13 33.81 14.40 17.81 33.98 0.07 0.00 +FBC 14 34.11 13.40 17.48 35.00 0.04 0.00 +FBC 15 33.33 15.55 16.68 34.43 0.14 0.00 +FBC 16 34.22 14.52 17.32 33.94 0.14 0.00 +FBC 17 32.82 13.27 16.64 37.26 0.11 0.00 +FBC 18 33.71 15.08 17.28 33.92 0.11 0.00 +FBC 19 32.67 14.83 16.42 36.08 0.07 0.00 +FBC 20 32.74 14.44 15.04 37.78 0.07 0.00 +FBC 21 32.29 13.97 16.06 37.68 0.00 0.00 +FBC 22 32.82 13.38 17.28 36.52 0.11 0.00 +FBC 23 33.43 12.53 16.93 37.12 0.11 0.00 +FBC 24 33.63 14.51 16.28 35.58 0.07 0.00 +FBC 25 34.69 13.99 15.06 36.26 0.18 0.00 +FBC 26 35.15 15.40 15.29 34.16 0.07 0.00 +FBC 27 34.00 14.58 16.11 35.31 0.11 0.00 +FBC 28 33.45 14.60 17.37 34.59 0.18 0.00 +FBC 29 32.27 15.21 18.12 34.40 0.04 0.00 +FBC 30 34.18 14.08 15.89 35.85 0.04 0.00 +FBC 31 31.26 15.10 16.91 36.73 0.18 0.00 +FBC 32 33.51 15.26 15.16 36.07 0.11 0.00 +FBC 33 33.03 15.32 14.54 37.11 0.25 0.00 +FBC 34 30.80 14.42 15.63 39.15 0.18 0.00 +FBC 35 31.00 14.10 17.86 37.04 0.14 0.00 +FBC 36 33.46 13.80 16.75 35.98 0.07 0.00 +FBC 37 35.46 13.38 15.19 35.96 0.11 0.00 +FBC 38 33.43 14.98 16.11 35.49 0.07 0.00 +FBC 39 32.84 14.48 16.90 35.78 0.11 0.00 +FBC 40 33.19 14.16 17.96 34.68 0.11 0.00 +FBC 41 32.13 13.99 16.79 37.10 0.11 0.00 +FBC 42 32.03 15.48 14.42 38.07 0.11 0.00 +FBC 43 33.26 14.41 15.65 36.67 0.07 0.00 +FBC 44 33.84 13.58 15.85 36.72 0.21 0.00 +FBC 45 34.60 14.71 15.52 35.17 0.14 0.00 +FBC 46 32.88 14.74 15.98 36.40 0.07 0.00 +FBC 47 32.79 14.83 15.40 36.98 0.21 0.00 +FBC 48 33.99 14.54 15.71 35.76 0.18 0.00 +FBC 49 33.45 13.96 16.26 36.33 0.07 0.00 +FBC 50 33.69 14.62 16.08 35.61 0.14 0.00 +FBC 51 32.16 16.15 15.44 36.25 0.14 0.00 +FBC 52 30.94 14.69 16.75 37.62 0.07 0.00 +FBC 53 32.23 15.12 17.22 35.43 0.04 0.00 +FBC 54 32.55 15.38 16.52 35.54 0.14 0.00 +FBC 55 30.94 17.04 17.65 34.37 0.21 0.00 +FBC 56 31.92 15.18 17.10 35.80 0.14 0.00 +FBC 57 31.60 14.82 15.96 37.62 0.14 0.00 +FBC 58 33.31 14.59 14.87 37.23 0.25 0.00 +FBC 59 31.90 15.29 16.25 36.56 0.14 0.00 +FBC 60 29.60 15.34 17.19 37.87 0.14 0.00 +FBC 61 30.86 15.13 16.77 37.25 0.14 0.00 +FBC 62 31.18 16.18 15.79 36.86 0.11 0.00 +FBC 63 33.06 14.31 15.67 36.96 0.25 0.00 +FBC 64 30.43 15.11 16.65 37.81 0.14 0.00 +FBC 65 30.88 14.73 16.34 38.05 0.21 0.00 +FBC 66 30.22 16.45 15.38 37.96 0.07 0.00 +FBC 67 31.04 16.65 14.68 37.64 0.11 0.00 +FBC 68 31.15 14.73 17.21 36.90 0.04 0.00 +FBC 69 29.94 16.05 16.23 37.78 0.07 0.00 +FBC 70 29.91 15.10 16.43 38.56 0.04 0.00 +FBC 71 30.05 15.37 16.24 38.34 0.04 0.00 +FBC 72 30.68 16.45 16.19 36.67 0.04 0.00 +FBC 73 29.85 15.33 15.36 39.46 0.04 0.00 +FBC 74 30.33 15.04 15.36 39.27 0.00 0.00 +FBC 75 31.03 15.04 14.16 39.77 0.11 0.00 +FBC 76 30.10 14.81 15.21 39.88 0.11 0.00 +FBC 77 28.60 14.89 15.85 40.66 0.00 0.00 +FBC 78 29.78 15.54 16.28 38.40 0.15 0.00 +FBC 79 31.34 14.87 15.09 38.70 0.07 0.00 +FBC 80 29.67 15.62 14.91 39.80 0.11 0.00 +FBC 81 30.87 16.55 15.45 37.14 0.08 0.00 +FBC 82 31.50 16.42 15.39 36.69 0.00 0.00 +FBC 83 30.02 16.62 14.13 39.23 0.08 0.00 +FBC 84 30.85 15.31 13.18 40.66 0.04 0.00 +FBC 85 30.17 15.47 12.93 41.42 0.16 0.00 +FBC 86 31.37 15.78 14.76 38.08 0.04 0.00 +FBC 87 30.56 16.41 12.64 40.39 0.04 0.00 +FBC 88 30.67 16.76 13.03 39.53 0.00 0.00 +FBC 89 29.71 16.27 11.77 42.25 0.04 0.00 +FBC 90 31.34 14.16 12.48 42.02 0.00 0.00 +FBC 91 30.60 16.14 12.93 40.32 0.00 0.00 +FBC 92 30.78 14.52 13.14 41.56 0.04 0.00 +FBC 93 32.80 15.03 13.81 38.36 0.00 0.00 +FBC 94 32.91 14.97 14.75 37.37 0.00 0.00 +FBC 95 32.13 15.79 14.71 37.37 0.04 0.00 +FBC 96 31.62 16.77 13.01 38.60 0.00 0.00 +FBC 97 32.07 15.65 15.47 36.81 0.00 0.00 +FBC 98 32.11 14.85 15.80 37.24 0.00 0.00 +FBC 99 33.12 15.64 15.32 35.92 0.00 0.00 +FBC 100 30.84 16.23 13.95 38.98 0.00 0.00 +FBC 101 33.24 15.62 12.49 38.65 0.00 0.00 +FBC 102 34.92 14.41 13.59 37.08 0.00 0.00 +FBC 103 32.65 15.33 14.94 37.08 0.00 0.00 +FBC 104 34.58 13.08 14.46 37.88 0.00 0.00 +FBC 105 34.40 15.80 13.45 36.35 0.00 0.00 +FBC 106 35.66 15.10 13.22 36.02 0.00 0.00 +FBC 107 34.69 15.42 13.87 36.02 0.00 0.00 +FBC 108 31.83 17.43 14.35 36.39 0.00 0.00 +FBC 109 32.91 15.23 13.91 37.95 0.00 0.00 +FBC 110 35.22 13.71 15.48 35.59 0.00 0.00 +FBC 111 32.48 15.28 14.95 37.29 0.00 0.00 +FBC 112 30.55 16.69 14.75 38.01 0.00 0.00 +FBC 113 30.42 17.35 14.25 37.97 0.00 0.00 +FBC 114 32.26 16.85 15.53 35.37 0.00 0.00 +FBC 115 31.07 17.14 15.68 36.10 0.00 0.00 +FBC 116 32.41 16.39 13.31 37.89 0.00 0.00 +FBC 117 30.23 15.83 13.61 40.33 0.00 0.00 +FBC 118 30.92 15.96 13.64 39.47 0.00 0.00 +FBC 119 29.50 16.07 15.55 38.88 0.06 0.00 +FBC 120 33.18 14.88 15.08 36.87 0.00 0.00 +FBC 121 30.76 16.15 16.15 36.94 0.00 0.00 +FBC 122 31.86 16.71 15.69 35.74 0.00 0.00 +FBC 123 32.17 15.53 14.97 37.33 0.00 0.00 +FBC 124 31.68 15.84 16.48 36.01 0.00 0.00 +FBC 125 32.43 14.87 16.69 36.01 0.00 0.00 +FBC 126 35.10 15.72 14.45 34.73 0.00 0.00 +FBC 127 35.80 13.82 15.11 35.27 0.00 0.00 +FBC 128 32.06 16.34 15.79 35.81 0.00 0.00 +FBC 129 32.05 14.34 15.54 38.06 0.00 0.00 +FBC 130 31.15 14.75 14.75 39.34 0.00 0.00 +FBC 131 32.63 14.15 16.36 36.86 0.00 0.00 +FBC 132 31.81 14.54 16.48 37.18 0.00 0.00 +FBC 133 35.14 13.69 15.23 35.95 0.00 0.00 +FBC 134 31.78 16.30 18.50 33.42 0.00 0.00 +FBC 135 30.21 14.35 17.54 37.90 0.00 0.00 +FBC 136 31.59 12.75 18.94 36.71 0.00 0.00 +FBC 137 30.85 14.23 19.60 35.32 0.00 0.00 +FBC 138 33.71 13.11 20.80 32.38 0.00 0.00 +FBC 139 33.12 13.06 18.37 35.46 0.00 0.00 +FBC 140 33.74 12.75 17.47 36.04 0.00 0.00 +FBC 141 32.65 13.61 17.35 36.39 0.00 0.00 +FBC 142 31.63 12.84 19.02 36.50 0.00 0.00 +FBC 143 31.49 11.32 16.11 41.08 0.12 0.00 +# ACGT raw counters for first fragments. Use `grep ^FTC | cut -f 2-` to extract this part. The columns are: A,C,G,T,N base counters +FTC 108689 50470 52527 124018 240 +# ACGT content per cycle for last fragments. Use `grep ^LBC | cut -f 2-` to extract this part. The columns are: cycle; A,C,G,T base counts as a percentage of all A/C/G/T bases [%]; and N and O counts as a percentage of all A/C/G/T bases [%] +LBC 1 33.37 15.21 16.06 35.35 0.04 0.00 +LBC 2 33.00 15.05 18.38 33.57 0.11 0.00 +LBC 3 34.48 16.03 15.57 33.91 0.07 0.00 +LBC 4 34.42 15.26 14.83 35.49 0.11 0.00 +LBC 5 33.66 15.64 14.26 36.43 0.07 0.00 +LBC 6 34.39 15.65 14.66 35.31 0.11 0.00 +LBC 7 33.13 16.11 15.11 35.65 0.07 0.00 +LBC 8 32.30 15.44 16.26 36.00 0.14 0.00 +LBC 9 34.85 13.84 15.72 35.59 0.11 0.00 +LBC 10 34.39 14.34 16.11 35.17 0.11 0.00 +LBC 11 34.56 14.09 16.15 35.20 0.11 0.00 +LBC 12 33.50 13.91 16.18 36.41 0.11 0.00 +LBC 13 33.66 13.16 18.48 34.69 0.07 0.00 +LBC 14 33.71 13.41 17.18 35.70 0.11 0.00 +LBC 15 33.53 14.66 17.21 34.60 0.11 0.00 +LBC 16 35.78 14.77 15.83 33.62 0.14 0.00 +LBC 17 33.61 14.51 16.64 35.24 0.11 0.00 +LBC 18 33.35 13.80 17.52 35.33 0.07 0.00 +LBC 19 32.97 14.73 16.47 35.84 0.11 0.00 +LBC 20 33.07 13.59 15.22 38.11 0.11 0.00 +LBC 21 32.66 14.06 15.73 37.56 0.14 0.00 +LBC 22 33.57 12.92 16.75 36.76 0.11 0.00 +LBC 23 33.32 13.77 17.00 35.91 0.11 0.00 +LBC 24 33.22 14.12 16.82 35.84 0.11 0.00 +LBC 25 34.81 14.34 15.72 35.13 0.11 0.00 +LBC 26 34.39 14.09 16.43 35.10 0.11 0.00 +LBC 27 34.88 14.66 16.71 33.75 0.11 0.00 +LBC 28 34.17 15.01 15.83 34.99 0.11 0.00 +LBC 29 33.29 15.01 16.57 35.13 0.11 0.00 +LBC 30 33.00 13.95 16.68 36.37 0.11 0.00 +LBC 31 31.76 14.90 16.00 37.33 0.11 0.00 +LBC 32 32.19 15.40 15.90 36.52 0.11 0.00 +LBC 33 33.04 14.34 14.94 37.69 0.11 0.00 +LBC 34 32.30 15.12 15.23 37.34 0.11 0.00 +LBC 35 32.23 13.31 16.36 38.09 0.11 0.00 +LBC 36 33.24 14.17 17.15 35.44 0.14 0.00 +LBC 37 34.40 13.35 15.41 36.85 0.11 0.00 +LBC 38 32.48 15.73 16.47 35.32 0.11 0.00 +LBC 39 32.82 14.35 15.99 36.84 0.18 0.00 +LBC 40 31.88 14.77 16.97 36.39 0.11 0.00 +LBC 41 33.51 14.02 16.15 36.32 0.11 0.00 +LBC 42 32.13 14.84 15.51 37.52 0.11 0.00 +LBC 43 31.81 14.38 15.76 38.05 0.11 0.00 +LBC 44 32.80 13.99 16.51 36.71 0.11 0.00 +LBC 45 35.26 14.88 14.88 34.98 0.14 0.00 +LBC 46 33.72 14.24 15.83 36.21 0.11 0.00 +LBC 47 33.76 13.99 15.76 36.49 0.11 0.00 +LBC 48 33.23 13.35 16.47 36.95 0.11 0.00 +LBC 49 33.90 14.24 15.94 35.92 0.11 0.00 +LBC 50 35.11 14.43 15.96 34.51 0.11 0.00 +LBC 51 32.31 16.46 15.78 35.44 0.14 0.00 +LBC 52 30.43 14.90 18.17 36.51 0.11 0.00 +LBC 53 32.02 15.12 17.18 35.68 0.11 0.00 +LBC 54 32.44 14.69 16.79 36.07 0.11 0.00 +LBC 55 31.07 16.12 16.98 35.84 0.11 0.00 +LBC 56 31.74 15.41 16.19 36.65 0.11 0.00 +LBC 57 31.36 16.02 15.70 36.92 0.14 0.00 +LBC 58 32.74 14.45 15.16 37.65 0.11 0.00 +LBC 59 31.19 14.20 16.87 37.74 0.11 0.00 +LBC 60 30.46 14.57 16.57 38.40 0.11 0.00 +LBC 61 30.61 16.14 16.25 36.99 0.11 0.00 +LBC 62 32.62 16.13 14.85 36.40 0.11 0.00 +LBC 63 31.74 14.74 15.46 38.06 0.11 0.00 +LBC 64 30.29 15.02 16.20 38.48 0.11 0.00 +LBC 65 30.51 15.06 16.85 37.59 0.11 0.00 +LBC 66 30.87 16.15 15.51 37.46 0.07 0.00 +LBC 67 30.43 16.45 14.80 38.32 0.07 0.00 +LBC 68 30.82 14.87 16.70 37.61 0.07 0.00 +LBC 69 30.00 15.54 15.97 38.49 0.07 0.00 +LBC 70 29.85 15.43 16.44 38.28 0.11 0.00 +LBC 71 29.73 16.27 16.17 37.83 0.07 0.00 +LBC 72 31.86 16.56 14.93 36.65 0.11 0.00 +LBC 73 30.90 15.21 14.88 39.01 0.04 0.00 +LBC 74 29.84 14.39 15.45 40.32 0.11 0.00 +LBC 75 30.31 14.84 15.06 39.78 0.07 0.00 +LBC 76 29.49 14.87 15.64 39.99 0.07 0.00 +LBC 77 30.27 14.97 16.48 38.27 0.07 0.00 +LBC 78 29.84 15.70 16.41 38.05 0.04 0.00 +LBC 79 32.33 14.15 14.59 38.93 0.04 0.00 +LBC 80 30.21 16.14 14.26 39.40 0.08 0.00 +LBC 81 31.52 15.29 14.91 38.28 0.04 0.00 +LBC 82 31.82 15.97 15.28 36.93 0.08 0.00 +LBC 83 32.34 15.50 13.39 38.78 0.04 0.00 +LBC 84 31.07 16.81 12.60 39.53 0.04 0.00 +LBC 85 29.85 14.98 13.54 41.63 0.04 0.00 +LBC 86 30.25 15.93 14.55 39.27 0.00 0.00 +LBC 87 30.39 15.89 12.80 40.93 0.04 0.00 +LBC 88 31.56 16.24 11.67 40.54 0.04 0.00 +LBC 89 30.76 15.42 11.69 42.13 0.08 0.00 +LBC 90 31.10 15.59 12.23 41.08 0.04 0.00 +LBC 91 29.86 17.87 12.85 39.42 0.04 0.00 +LBC 92 31.93 14.35 13.72 40.00 0.08 0.00 +LBC 93 31.31 15.91 13.51 39.27 0.04 0.00 +LBC 94 32.21 14.75 15.17 37.87 0.04 0.00 +LBC 95 31.36 15.98 14.01 38.65 0.00 0.00 +LBC 96 32.88 16.46 12.40 38.25 0.04 0.00 +LBC 97 31.50 16.38 14.72 37.40 0.09 0.00 +LBC 98 30.84 15.35 15.76 38.06 0.05 0.00 +LBC 99 32.43 16.51 14.26 36.80 0.05 0.00 +LBC 100 31.52 17.39 13.11 37.98 0.00 0.00 +LBC 101 31.83 15.96 13.16 39.05 0.10 0.00 +LBC 102 34.77 15.37 13.16 36.70 0.05 0.00 +LBC 103 33.53 15.47 14.21 36.79 0.05 0.00 +LBC 104 33.92 14.11 15.09 36.87 0.00 0.00 +LBC 105 35.23 15.59 12.79 36.38 0.00 0.00 +LBC 106 37.06 14.38 13.97 34.58 0.00 0.00 +LBC 107 34.33 14.80 14.03 36.84 0.05 0.00 +LBC 108 32.36 16.34 14.40 36.91 0.10 0.00 +LBC 109 34.38 14.32 14.27 37.03 0.05 0.00 +LBC 110 35.09 14.56 16.28 34.07 0.05 0.00 +LBC 111 32.44 15.54 15.86 36.16 0.00 0.00 +LBC 112 30.20 16.24 14.24 39.32 0.06 0.00 +LBC 113 30.33 16.15 12.89 40.63 0.00 0.00 +LBC 114 31.65 15.62 15.51 37.22 0.00 0.00 +LBC 115 32.22 16.08 16.32 35.38 0.06 0.00 +LBC 116 32.85 15.46 14.14 37.55 0.06 0.00 +LBC 117 30.36 15.18 13.83 40.63 0.00 0.00 +LBC 118 32.25 15.75 13.93 38.08 0.00 0.00 +LBC 119 30.15 15.33 16.16 38.36 0.06 0.00 +LBC 120 31.85 15.96 14.64 37.56 0.00 0.00 +LBC 121 30.24 15.98 15.45 38.33 0.00 0.00 +LBC 122 31.59 15.93 15.66 36.83 0.00 0.00 +LBC 123 31.20 15.50 16.05 37.25 0.00 0.00 +LBC 124 31.33 15.59 17.01 36.07 0.00 0.00 +LBC 125 31.13 15.42 16.95 36.51 0.00 0.00 +LBC 126 32.86 16.80 15.38 34.95 0.00 0.00 +LBC 127 37.37 13.39 14.84 34.40 0.08 0.00 +LBC 128 31.10 16.29 16.29 36.32 0.08 0.00 +LBC 129 32.24 14.68 14.92 38.15 0.00 0.00 +LBC 130 33.50 14.79 14.54 37.17 0.08 0.00 +LBC 131 33.64 15.26 15.18 35.92 0.00 0.00 +LBC 132 32.08 14.11 16.74 37.07 0.00 0.00 +LBC 133 34.59 13.89 15.95 35.57 0.00 0.00 +LBC 134 33.27 15.13 18.60 33.00 0.00 0.00 +LBC 135 30.97 14.93 18.00 36.10 0.00 0.00 +LBC 136 31.60 13.54 18.64 36.22 0.00 0.00 +LBC 137 31.85 12.96 17.41 37.78 0.00 0.00 +LBC 138 32.69 12.73 20.37 34.22 0.00 0.00 +LBC 139 33.23 13.71 17.93 35.13 0.00 0.00 +LBC 140 33.95 12.55 18.01 35.48 0.00 0.00 +LBC 141 32.09 11.94 17.00 38.96 0.00 0.00 +LBC 142 34.95 11.81 16.65 36.60 0.00 0.00 +LBC 143 33.62 11.17 16.07 39.14 0.61 0.00 +# ACGT raw counters for last fragments. Use `grep ^LTC | cut -f 2-` to extract this part. The columns are: A,C,G,T,N base counters +LTC 108882 50371 52310 124355 269 +# Insert sizes. Use `grep ^IS | cut -f 2-` to extract this part. The columns are: insert size, pairs total, inward oriented pairs, outward oriented pairs, other pairs +IS 0 0 0 0 0 +IS 1 0 0 0 0 +IS 2 0 0 0 0 +IS 3 0 0 0 0 +IS 4 0 0 0 0 +IS 5 0 0 0 0 +IS 6 0 0 0 0 +IS 7 0 0 0 0 +IS 8 0 0 0 0 +IS 9 0 0 0 0 +IS 10 0 0 0 0 +IS 11 0 0 0 0 +IS 12 0 0 0 0 +IS 13 0 0 0 0 +IS 14 0 0 0 0 +IS 15 0 0 0 0 +IS 16 0 0 0 0 +IS 17 0 0 0 0 +IS 18 0 0 0 0 +IS 19 0 0 0 0 +IS 20 0 0 0 0 +IS 21 0 0 0 0 +IS 22 0 0 0 0 +IS 23 0 0 0 0 +IS 24 0 0 0 0 +IS 25 0 0 0 0 +IS 26 0 0 0 0 +IS 27 0 0 0 0 +IS 28 0 0 0 0 +IS 29 0 0 0 0 +IS 30 0 0 0 0 +IS 31 0 0 0 0 +IS 32 1 0 1 0 +IS 33 0 0 0 0 +IS 34 0 0 0 0 +IS 35 0 0 0 0 +IS 36 0 0 0 0 +IS 37 0 0 0 0 +IS 38 0 0 0 0 +IS 39 0 0 0 0 +IS 40 0 0 0 0 +IS 41 1 1 0 0 +IS 42 0 0 0 0 +IS 43 0 0 0 0 +IS 44 0 0 0 0 +IS 45 0 0 0 0 +IS 46 0 0 0 0 +IS 47 0 0 0 0 +IS 48 0 0 0 0 +IS 49 3 3 0 0 +IS 50 0 0 0 0 +IS 51 1 1 0 0 +IS 52 2 2 0 0 +IS 53 0 0 0 0 +IS 54 1 1 0 0 +IS 55 0 0 0 0 +IS 56 0 0 0 0 +IS 57 0 0 0 0 +IS 58 1 1 0 0 +IS 59 2 2 0 0 +IS 60 1 1 0 0 +IS 61 4 4 0 0 +IS 62 1 1 0 0 +IS 63 5 5 0 0 +IS 64 0 0 0 0 +IS 65 5 5 0 0 +IS 66 2 2 0 0 +IS 67 6 6 0 0 +IS 68 3 3 0 0 +IS 69 5 5 0 0 +IS 70 10 10 0 0 +IS 71 11 11 0 0 +IS 72 7 7 0 0 +IS 73 8 8 0 0 +IS 74 4 4 0 0 +IS 75 12 12 0 0 +IS 76 11 11 0 0 +IS 77 19 19 0 0 +IS 78 15 15 0 0 +IS 79 13 13 0 0 +IS 80 17 17 0 0 +IS 81 24 24 0 0 +IS 82 18 18 0 0 +IS 83 19 19 0 0 +IS 84 25 25 0 0 +IS 85 15 15 0 0 +IS 86 24 24 0 0 +IS 87 30 30 0 0 +IS 88 29 29 0 0 +IS 89 21 21 0 0 +IS 90 16 16 0 0 +IS 91 24 24 0 0 +IS 92 30 30 0 0 +IS 93 23 23 0 0 +IS 94 21 20 1 0 +IS 95 43 43 0 0 +IS 96 54 54 0 0 +IS 97 34 34 0 0 +IS 98 28 28 0 0 +IS 99 24 24 0 0 +IS 100 44 44 0 0 +IS 101 24 24 0 0 +IS 102 27 27 0 0 +IS 103 22 22 0 0 +IS 104 33 33 0 0 +IS 105 26 26 0 0 +IS 106 28 28 0 0 +IS 107 35 35 0 0 +IS 108 26 26 0 0 +IS 109 24 24 0 0 +IS 110 34 34 0 0 +IS 111 29 29 0 0 +IS 112 22 22 0 0 +IS 113 36 36 0 0 +IS 114 30 30 0 0 +IS 115 49 49 0 0 +IS 116 36 35 1 0 +IS 117 33 33 0 0 +IS 118 34 34 0 0 +IS 119 38 38 0 0 +IS 120 14 14 0 0 +IS 121 39 39 0 0 +IS 122 30 30 0 0 +IS 123 28 28 0 0 +IS 124 36 35 1 0 +IS 125 36 36 0 0 +IS 126 25 25 0 0 +IS 127 32 32 0 0 +IS 128 31 31 0 0 +IS 129 28 28 0 0 +IS 130 39 39 0 0 +IS 131 45 44 1 0 +IS 132 25 25 0 0 +IS 133 18 18 0 0 +IS 134 25 25 0 0 +IS 135 31 31 0 0 +IS 136 30 29 1 0 +IS 137 29 29 0 0 +IS 138 34 34 0 0 +IS 139 32 32 0 0 +IS 140 28 28 0 0 +IS 141 41 41 0 0 +IS 142 27 27 0 0 +IS 143 23 23 0 0 +IS 144 26 26 0 0 +IS 145 31 31 0 0 +IS 146 21 21 0 0 +IS 147 29 29 0 0 +IS 148 18 18 0 0 +IS 149 17 17 0 0 +IS 150 19 19 0 0 +IS 151 20 20 0 0 +IS 152 28 28 0 0 +IS 153 28 28 0 0 +IS 154 18 18 0 0 +IS 155 23 23 0 0 +IS 156 20 20 0 0 +IS 157 29 29 0 0 +IS 158 16 16 0 0 +IS 159 15 15 0 0 +IS 160 14 14 0 0 +IS 161 18 18 0 0 +IS 162 19 19 0 0 +IS 163 15 15 0 0 +IS 164 9 9 0 0 +IS 165 11 11 0 0 +IS 166 21 21 0 0 +IS 167 9 9 0 0 +IS 168 17 17 0 0 +IS 169 16 16 0 0 +IS 170 17 17 0 0 +IS 171 13 13 0 0 +IS 172 14 14 0 0 +IS 173 21 21 0 0 +IS 174 9 9 0 0 +IS 175 9 9 0 0 +IS 176 7 7 0 0 +IS 177 9 9 0 0 +IS 178 9 9 0 0 +IS 179 9 9 0 0 +IS 180 2 2 0 0 +IS 181 8 8 0 0 +IS 182 8 8 0 0 +IS 183 3 3 0 0 +IS 184 12 12 0 0 +IS 185 10 10 0 0 +IS 186 5 5 0 0 +IS 187 7 7 0 0 +IS 188 1 1 0 0 +IS 189 5 5 0 0 +IS 190 8 8 0 0 +IS 191 10 10 0 0 +IS 192 8 8 0 0 +IS 193 2 2 0 0 +IS 194 6 6 0 0 +IS 195 1 1 0 0 +IS 196 2 2 0 0 +IS 197 3 3 0 0 +IS 198 2 2 0 0 +IS 199 4 4 0 0 +IS 200 7 7 0 0 +IS 201 2 2 0 0 +IS 202 6 6 0 0 +IS 203 4 4 0 0 +IS 204 4 4 0 0 +IS 205 2 2 0 0 +IS 206 4 4 0 0 +IS 207 4 4 0 0 +# Read lengths. Use `grep ^RL | cut -f 2-` to extract this part. The columns are: read length, count +RL 30 1 +RL 33 1 +RL 41 1 +RL 45 1 +RL 49 6 +RL 51 2 +RL 52 4 +RL 54 2 +RL 58 2 +RL 59 4 +RL 60 2 +RL 61 8 +RL 62 2 +RL 63 10 +RL 65 10 +RL 66 4 +RL 67 12 +RL 68 7 +RL 69 10 +RL 70 20 +RL 71 22 +RL 72 14 +RL 73 16 +RL 74 8 +RL 75 24 +RL 76 22 +RL 77 38 +RL 78 30 +RL 79 26 +RL 80 34 +RL 81 48 +RL 82 36 +RL 83 38 +RL 84 50 +RL 85 30 +RL 86 48 +RL 87 60 +RL 88 58 +RL 89 42 +RL 90 32 +RL 91 45 +RL 92 60 +RL 93 46 +RL 94 40 +RL 95 85 +RL 96 108 +RL 97 68 +RL 98 56 +RL 99 48 +RL 100 88 +RL 101 48 +RL 102 54 +RL 103 44 +RL 104 66 +RL 105 52 +RL 106 56 +RL 107 71 +RL 108 52 +RL 109 48 +RL 110 68 +RL 111 58 +RL 112 44 +RL 113 72 +RL 114 60 +RL 115 97 +RL 116 72 +RL 117 66 +RL 118 68 +RL 119 75 +RL 120 29 +RL 121 78 +RL 122 60 +RL 123 56 +RL 124 72 +RL 125 72 +RL 126 50 +RL 127 62 +RL 128 62 +RL 129 56 +RL 130 79 +RL 131 90 +RL 132 50 +RL 133 37 +RL 134 51 +RL 135 62 +RL 136 60 +RL 137 58 +RL 138 68 +RL 139 64 +RL 140 56 +RL 141 82 +RL 142 54 +RL 143 1634 +# Read lengths - first fragments. Use `grep ^FRL | cut -f 2-` to extract this part. The columns are: read length, count +FRL 30 1 +FRL 41 1 +FRL 45 1 +FRL 49 3 +FRL 51 1 +FRL 52 2 +FRL 54 1 +FRL 58 1 +FRL 59 2 +FRL 60 1 +FRL 61 4 +FRL 62 1 +FRL 63 5 +FRL 65 5 +FRL 66 2 +FRL 67 6 +FRL 68 3 +FRL 69 5 +FRL 70 10 +FRL 71 11 +FRL 72 7 +FRL 73 8 +FRL 74 4 +FRL 75 12 +FRL 76 11 +FRL 77 19 +FRL 78 15 +FRL 79 13 +FRL 80 17 +FRL 81 24 +FRL 82 18 +FRL 83 19 +FRL 84 25 +FRL 85 15 +FRL 86 24 +FRL 87 30 +FRL 88 29 +FRL 89 21 +FRL 90 16 +FRL 91 23 +FRL 92 30 +FRL 93 23 +FRL 94 20 +FRL 95 42 +FRL 96 54 +FRL 97 34 +FRL 98 28 +FRL 99 24 +FRL 100 44 +FRL 101 24 +FRL 102 27 +FRL 103 22 +FRL 104 33 +FRL 105 26 +FRL 106 28 +FRL 107 36 +FRL 108 26 +FRL 109 24 +FRL 110 34 +FRL 111 29 +FRL 112 22 +FRL 113 36 +FRL 114 30 +FRL 115 49 +FRL 116 36 +FRL 117 33 +FRL 118 34 +FRL 119 38 +FRL 120 14 +FRL 121 39 +FRL 122 30 +FRL 123 28 +FRL 124 36 +FRL 125 36 +FRL 126 26 +FRL 127 31 +FRL 128 31 +FRL 129 28 +FRL 130 40 +FRL 131 45 +FRL 132 25 +FRL 133 18 +FRL 134 26 +FRL 135 31 +FRL 136 30 +FRL 137 29 +FRL 138 34 +FRL 139 32 +FRL 140 28 +FRL 141 41 +FRL 142 27 +FRL 143 814 +# Read lengths - last fragments. Use `grep ^LRL | cut -f 2-` to extract this part. The columns are: read length, count +LRL 33 1 +LRL 49 3 +LRL 51 1 +LRL 52 2 +LRL 54 1 +LRL 58 1 +LRL 59 2 +LRL 60 1 +LRL 61 4 +LRL 62 1 +LRL 63 5 +LRL 65 5 +LRL 66 2 +LRL 67 6 +LRL 68 4 +LRL 69 5 +LRL 70 10 +LRL 71 11 +LRL 72 7 +LRL 73 8 +LRL 74 4 +LRL 75 12 +LRL 76 11 +LRL 77 19 +LRL 78 15 +LRL 79 13 +LRL 80 17 +LRL 81 24 +LRL 82 18 +LRL 83 19 +LRL 84 25 +LRL 85 15 +LRL 86 24 +LRL 87 30 +LRL 88 29 +LRL 89 21 +LRL 90 16 +LRL 91 22 +LRL 92 30 +LRL 93 23 +LRL 94 20 +LRL 95 43 +LRL 96 54 +LRL 97 34 +LRL 98 28 +LRL 99 24 +LRL 100 44 +LRL 101 24 +LRL 102 27 +LRL 103 22 +LRL 104 33 +LRL 105 26 +LRL 106 28 +LRL 107 35 +LRL 108 26 +LRL 109 24 +LRL 110 34 +LRL 111 29 +LRL 112 22 +LRL 113 36 +LRL 114 30 +LRL 115 48 +LRL 116 36 +LRL 117 33 +LRL 118 34 +LRL 119 37 +LRL 120 15 +LRL 121 39 +LRL 122 30 +LRL 123 28 +LRL 124 36 +LRL 125 36 +LRL 126 24 +LRL 127 31 +LRL 128 31 +LRL 129 28 +LRL 130 39 +LRL 131 45 +LRL 132 25 +LRL 133 19 +LRL 134 25 +LRL 135 31 +LRL 136 30 +LRL 137 29 +LRL 138 34 +LRL 139 32 +LRL 140 28 +LRL 141 41 +LRL 142 27 +LRL 143 820 +# Mapping qualities for reads !(UNMAP|SECOND|SUPPL|QCFAIL|DUP). Use `grep ^MAPQ | cut -f 2-` to extract this part. The columns are: mapq, count +MAPQ 40 1 +MAPQ 42 1 +MAPQ 44 1 +MAPQ 54 1 +MAPQ 60 3980 +# Indel distribution. Use `grep ^ID | cut -f 2-` to extract this part. The columns are: length, number of insertions, number of deletions +ID 1 2 10 +# Indels per cycle. Use `grep ^IC | cut -f 2-` to extract this part. The columns are: cycle, number of insertions (fwd), .. (rev) , number of deletions (fwd), .. (rev) +IC 3 0 0 1 0 +IC 10 0 1 0 0 +IC 35 0 0 1 0 +IC 39 0 0 1 0 +IC 53 0 0 0 1 +IC 54 0 0 0 1 +IC 61 0 0 1 0 +IC 62 0 0 0 1 +IC 77 0 0 1 0 +IC 80 1 0 0 1 +IC 132 0 0 0 1 +# Coverage distribution. Use `grep ^COV | cut -f 2-` to extract this part. +COV [1-1] 1 40 +COV [2-2] 2 83 +COV [3-3] 3 32 +COV [4-4] 4 14 +COV [5-5] 5 10 +COV [6-6] 6 1 +COV [7-7] 7 12 +COV [8-8] 8 8 +COV [9-9] 9 9 +COV [10-10] 10 10 +COV [11-11] 11 1 +COV [12-12] 12 5 +COV [13-13] 13 1 +COV [14-14] 14 4 +COV [15-15] 15 1 +COV [16-16] 16 9 +COV [17-17] 17 1 +COV [18-18] 18 1 +COV [19-19] 19 2 +COV [20-20] 20 5 +COV [21-21] 21 13 +COV [22-22] 22 9 +COV [23-23] 23 2 +COV [24-24] 24 6 +COV [25-25] 25 1 +COV [26-26] 26 98 +COV [30-30] 30 2 +COV [32-32] 32 1 +COV [36-36] 36 1 +COV [37-37] 37 1 +COV [40-40] 40 2 +COV [41-41] 41 1 +COV [43-43] 43 2 +COV [45-45] 45 2 +COV [46-46] 46 1 +COV [48-48] 48 1 +COV [50-50] 50 5 +COV [52-52] 52 5 +COV [54-54] 54 3 +COV [55-55] 55 1 +COV [56-56] 56 2 +COV [57-57] 57 1 +COV [58-58] 58 2 +COV [59-59] 59 1 +COV [60-60] 60 1 +COV [63-63] 63 1 +COV [64-64] 64 1 +COV [66-66] 66 5 +COV [68-68] 68 1 +COV [70-70] 70 1 +COV [71-71] 71 1 +COV [72-72] 72 3 +COV [73-73] 73 1 +COV [74-74] 74 7 +COV [78-78] 78 6 +COV [80-80] 80 6 +COV [81-81] 81 1 +COV [82-82] 82 7 +COV [83-83] 83 1 +COV [84-84] 84 2 +COV [85-85] 85 1 +COV [86-86] 86 4 +COV [87-87] 87 1 +COV [88-88] 88 23 +COV [90-90] 90 7 +COV [92-92] 92 7 +COV [93-93] 93 1 +COV [94-94] 94 1 +COV [95-95] 95 1 +COV [98-98] 98 1 +COV [100-100] 100 1 +COV [101-101] 101 2 +COV [103-103] 103 1 +COV [104-104] 104 1 +COV [111-111] 111 1 +COV [114-114] 114 1 +COV [115-115] 115 2 +COV [116-116] 116 1 +COV [120-120] 120 1 +COV [121-121] 121 1 +COV [125-125] 125 1 +COV [126-126] 126 1 +COV [129-129] 129 2 +COV [130-130] 130 1 +COV [135-135] 135 1 +COV [136-136] 136 1 +COV [140-140] 140 1 +COV [143-143] 143 1 +COV [144-144] 144 1 +COV [145-145] 145 1 +COV [147-147] 147 1 +COV [148-148] 148 1 +COV [151-151] 151 1 +COV [154-154] 154 1 +COV [158-158] 158 2 +COV [159-159] 159 1 +COV [160-160] 160 1 +COV [164-164] 164 1 +COV [166-166] 166 1 +COV [170-170] 170 2 +COV [172-172] 172 1 +COV [173-173] 173 1 +COV [176-176] 176 1 +COV [178-178] 178 1 +COV [180-180] 180 2 +COV [184-184] 184 1 +COV [188-188] 188 1 +COV [190-190] 190 2 +COV [191-191] 191 1 +COV [192-192] 192 2 +COV [197-197] 197 1 +COV [200-200] 200 2 +COV [205-205] 205 1 +COV [208-208] 208 2 +COV [210-210] 210 1 +COV [212-212] 212 1 +COV [214-214] 214 1 +COV [215-215] 215 1 +COV [218-218] 218 1 +COV [224-224] 224 1 +COV [226-226] 226 1 +COV [229-229] 229 1 +COV [231-231] 231 2 +COV [232-232] 232 1 +COV [236-236] 236 1 +COV [240-240] 240 2 +COV [241-241] 241 1 +COV [242-242] 242 1 +COV [244-244] 244 3 +COV [245-245] 245 1 +COV [247-247] 247 1 +COV [250-250] 250 1 +COV [252-252] 252 1 +COV [254-254] 254 1 +COV [258-258] 258 2 +COV [259-259] 259 1 +COV [262-262] 262 1 +COV [263-263] 263 1 +COV [264-264] 264 1 +COV [265-265] 265 1 +COV [271-271] 271 1 +COV [274-274] 274 1 +COV [275-275] 275 1 +COV [278-278] 278 1 +COV [280-280] 280 1 +COV [281-281] 281 2 +COV [284-284] 284 1 +COV [286-286] 286 2 +COV [288-288] 288 2 +COV [289-289] 289 1 +COV [292-292] 292 1 +COV [293-293] 293 1 +COV [294-294] 294 1 +COV [296-296] 296 1 +COV [300-300] 300 1 +COV [302-302] 302 1 +COV [304-304] 304 2 +COV [306-306] 306 1 +COV [308-308] 308 1 +COV [310-310] 310 1 +COV [311-311] 311 1 +COV [314-314] 314 1 +COV [315-315] 315 1 +COV [317-317] 317 1 +COV [318-318] 318 2 +COV [320-320] 320 2 +COV [324-324] 324 1 +COV [325-325] 325 1 +COV [326-326] 326 3 +COV [329-329] 329 1 +COV [330-330] 330 1 +COV [331-331] 331 1 +COV [332-332] 332 1 +COV [333-333] 333 1 +COV [334-334] 334 2 +COV [338-338] 338 1 +COV [339-339] 339 1 +COV [340-340] 340 1 +COV [342-342] 342 1 +COV [343-343] 343 1 +COV [344-344] 344 3 +COV [345-345] 345 1 +COV [348-348] 348 3 +COV [349-349] 349 1 +COV [350-350] 350 1 +COV [352-352] 352 1 +COV [356-356] 356 3 +COV [357-357] 357 1 +COV [358-358] 358 4 +COV [360-360] 360 1 +COV [362-362] 362 9 +COV [364-364] 364 7 +COV [366-366] 366 3 +COV [367-367] 367 2 +COV [368-368] 368 28 +COV [374-374] 374 2 +COV [375-375] 375 2 +COV [387-387] 387 2 +COV [388-388] 388 1 +COV [389-389] 389 1 +COV [399-399] 399 1 +COV [401-401] 401 1 +COV [403-403] 403 1 +COV [406-406] 406 1 +COV [415-415] 415 1 +COV [419-419] 419 1 +COV [425-425] 425 1 +COV [426-426] 426 1 +COV [430-430] 430 1 +COV [432-432] 432 1 +COV [436-436] 436 1 +COV [445-445] 445 1 +COV [447-447] 447 1 +COV [454-454] 454 1 +COV [458-458] 458 2 +COV [459-459] 459 1 +COV [460-460] 460 1 +COV [463-463] 463 1 +COV [476-476] 476 1 +COV [477-477] 477 1 +COV [480-480] 480 1 +COV [481-481] 481 1 +COV [483-483] 483 2 +COV [489-489] 489 1 +COV [492-492] 492 1 +COV [500-500] 500 1 +COV [501-501] 501 1 +COV [504-504] 504 1 +COV [508-508] 508 1 +COV [511-511] 511 1 +COV [512-512] 512 1 +COV [515-515] 515 1 +COV [525-525] 525 3 +COV [529-529] 529 1 +COV [533-533] 533 1 +COV [539-539] 539 1 +COV [540-540] 540 1 +COV [541-541] 541 1 +COV [546-546] 546 1 +COV [549-549] 549 1 +COV [550-550] 550 1 +COV [553-553] 553 1 +COV [559-559] 559 1 +COV [563-563] 563 2 +COV [565-565] 565 1 +COV [569-569] 569 1 +COV [575-575] 575 1 +COV [577-577] 577 1 +COV [578-578] 578 1 +COV [579-579] 579 1 +COV [591-591] 591 2 +COV [592-592] 592 2 +COV [593-593] 593 2 +COV [601-601] 601 1 +COV [603-603] 603 1 +COV [605-605] 605 1 +COV [610-610] 610 1 +COV [611-611] 611 1 +COV [613-613] 613 2 +COV [617-617] 617 1 +COV [622-622] 622 1 +COV [625-625] 625 1 +COV [628-628] 628 1 +COV [637-637] 637 2 +COV [639-639] 639 1 +COV [640-640] 640 1 +COV [643-643] 643 1 +COV [652-652] 652 2 +COV [657-657] 657 1 +COV [661-661] 661 1 +COV [663-663] 663 2 +COV [665-665] 665 1 +COV [669-669] 669 1 +COV [671-671] 671 1 +COV [674-674] 674 1 +COV [675-675] 675 1 +COV [679-679] 679 1 +COV [685-685] 685 1 +COV [687-687] 687 1 +COV [689-689] 689 1 +COV [692-692] 692 1 +COV [694-694] 694 1 +COV [697-697] 697 2 +COV [698-698] 698 1 +COV [699-699] 699 1 +COV [705-705] 705 1 +COV [711-711] 711 1 +COV [714-714] 714 1 +COV [719-719] 719 2 +COV [724-724] 724 1 +COV [727-727] 727 1 +COV [728-728] 728 1 +COV [732-732] 732 1 +COV [733-733] 733 1 +COV [735-735] 735 1 +COV [738-738] 738 1 +COV [741-741] 741 1 +COV [746-746] 746 1 +COV [752-752] 752 1 +COV [755-755] 755 3 +COV [756-756] 756 1 +COV [757-757] 757 1 +COV [763-763] 763 1 +COV [765-765] 765 1 +COV [767-767] 767 1 +COV [769-769] 769 1 +COV [770-770] 770 1 +COV [771-771] 771 2 +COV [773-773] 773 2 +COV [774-774] 774 1 +COV [775-775] 775 1 +COV [779-779] 779 3 +COV [781-781] 781 1 +COV [782-782] 782 1 +COV [785-785] 785 2 +COV [788-788] 788 1 +COV [789-789] 789 2 +COV [792-792] 792 1 +COV [793-793] 793 5 +COV [794-794] 794 4 +COV [795-795] 795 7 +COV [796-796] 796 9 +COV [797-797] 797 8 +COV [799-799] 799 1 +COV [801-801] 801 1 +COV [806-806] 806 1 +COV [807-807] 807 1 +COV [817-817] 817 1 +COV [820-820] 820 1 +COV [824-824] 824 1 +COV [825-825] 825 1 +COV [847-847] 847 1 +COV [850-850] 850 1 +COV [851-851] 851 1 +COV [853-853] 853 1 +COV [868-868] 868 1 +COV [873-873] 873 1 +COV [874-874] 874 1 +COV [875-875] 875 1 +COV [892-892] 892 1 +COV [893-893] 893 1 +COV [902-902] 902 1 +COV [906-906] 906 1 +COV [908-908] 908 1 +COV [916-916] 916 1 +COV [925-925] 925 1 +COV [927-927] 927 1 +COV [935-935] 935 1 +COV [937-937] 937 1 +COV [944-944] 944 1 +COV [955-955] 955 1 +COV [965-965] 965 2 +COV [967-967] 967 1 +COV [986-986] 986 1 +COV [988-988] 988 1 +COV [999-999] 999 2 +COV [1000<] 1000 259 +# GC-depth. Use `grep ^GCD | cut -f 2-` to extract this part. The columns are: GC%, unique sequence percentiles, 10th, 25th, 50th, 75th and 90th depth percentile +GCD 0.0 100.000 0.000 0.000 0.000 0.000 0.000 diff --git a/tests/expected/dna/test.thresholds.bed.gz b/tests/expected/dna/test.thresholds.bed.gz new file mode 100644 index 00000000..10d328e2 Binary files /dev/null and b/tests/expected/dna/test.thresholds.bed.gz differ diff --git a/tests/expected/dna/test.thresholds.bed.gz.csi b/tests/expected/dna/test.thresholds.bed.gz.csi new file mode 100644 index 00000000..efa85327 Binary files /dev/null and b/tests/expected/dna/test.thresholds.bed.gz.csi differ diff --git a/tests/expected/dna/test.wgs_metrics.txt b/tests/expected/dna/test.wgs_metrics.txt new file mode 100644 index 00000000..68e27b72 --- /dev/null +++ b/tests/expected/dna/test.wgs_metrics.txt @@ -0,0 +1,258 @@ +## METRICS CLASS picard.analysis.WgsMetrics +GENOME_TERRITORY MEAN_COVERAGE SD_COVERAGE MEDIAN_COVERAGE MAD_COVERAGE PCT_EXC_ADAPTER PCT_EXC_MAPQ PCT_EXC_DUPE PCT_EXC_UNPAIRED PCT_EXC_BASEQ PCT_EXC_OVERLAP PCT_EXC_CAPPED PCT_EXC_TOTAL PCT_1X PCT_5X PCT_10X PCT_15X PCT_20X PCT_25X PCT_30X PCT_40X PCT_50X PCT_60X PCT_70X PCT_80X PCT_90X PCT_100X FOLD_80_BASE_PENALTY FOLD_90_BASE_PENALTY FOLD_95_BASE_PENALTY HET_SNP_SENSITIVITY HET_SNP_Q +40001 3.531312 27.339314 0 0 0 0 0.299737 0 0.007352 0.324694 0.157699 0.789481 0.029124 0.024374 0.022949 0.020174 0.019425 0.019075 0.018375 0.01705 0.016725 0.016375 0.01615 0.0158 0.01545 0.01505 ? ? ? 0.027852 0 + +## HISTOGRAM java.lang.Integer +coverage high_quality_coverage_count +0 38836 +1 105 +2 42 +3 23 +4 20 +5 9 +6 19 +7 9 +8 13 +9 7 +10 95 +11 2 +12 5 +13 4 +14 5 +15 6 +16 10 +17 6 +18 5 +19 3 +20 1 +21 4 +22 4 +23 2 +24 3 +25 5 +26 5 +27 8 +28 5 +29 5 +30 17 +31 20 +32 9 +33 0 +34 2 +35 0 +36 2 +37 1 +38 1 +39 1 +40 0 +41 4 +42 0 +43 2 +44 0 +45 1 +46 2 +47 1 +48 1 +49 2 +50 0 +51 3 +52 2 +53 0 +54 1 +55 2 +56 0 +57 0 +58 5 +59 1 +60 0 +61 1 +62 3 +63 0 +64 1 +65 3 +66 0 +67 0 +68 0 +69 1 +70 2 +71 2 +72 3 +73 0 +74 1 +75 0 +76 2 +77 2 +78 2 +79 0 +80 1 +81 3 +82 1 +83 3 +84 0 +85 1 +86 1 +87 1 +88 1 +89 2 +90 0 +91 4 +92 1 +93 2 +94 1 +95 1 +96 3 +97 0 +98 1 +99 3 +100 1 +101 0 +102 1 +103 1 +104 1 +105 4 +106 2 +107 1 +108 3 +109 2 +110 2 +111 2 +112 2 +113 3 +114 0 +115 2 +116 0 +117 3 +118 6 +119 0 +120 3 +121 1 +122 2 +123 3 +124 2 +125 4 +126 0 +127 4 +128 5 +129 1 +130 12 +131 8 +132 6 +133 25 +134 2 +135 0 +136 2 +137 0 +138 0 +139 0 +140 1 +141 2 +142 2 +143 0 +144 1 +145 0 +146 0 +147 0 +148 2 +149 0 +150 1 +151 0 +152 1 +153 1 +154 1 +155 0 +156 1 +157 1 +158 1 +159 2 +160 3 +161 1 +162 0 +163 0 +164 0 +165 2 +166 0 +167 1 +168 0 +169 0 +170 3 +171 1 +172 0 +173 1 +174 1 +175 1 +176 2 +177 0 +178 1 +179 2 +180 1 +181 0 +182 0 +183 2 +184 0 +185 2 +186 0 +187 1 +188 0 +189 1 +190 0 +191 3 +192 0 +193 0 +194 0 +195 1 +196 1 +197 2 +198 2 +199 0 +200 3 +201 0 +202 3 +203 0 +204 0 +205 1 +206 0 +207 0 +208 1 +209 0 +210 1 +211 0 +212 1 +213 1 +214 0 +215 2 +216 3 +217 2 +218 0 +219 1 +220 1 +221 0 +222 0 +223 2 +224 0 +225 1 +226 2 +227 0 +228 0 +229 3 +230 1 +231 1 +232 2 +233 2 +234 0 +235 1 +236 0 +237 1 +238 1 +239 1 +240 2 +241 0 +242 1 +243 0 +244 2 +245 1 +246 1 +247 2 +248 0 +249 0 +250 387 + diff --git a/tests/expected/rseqc/geneBodyCoverage.txt b/tests/expected/rseqc/geneBodyCoverage.txt new file mode 100644 index 00000000..e3f16f61 --- /dev/null +++ b/tests/expected/rseqc/geneBodyCoverage.txt @@ -0,0 +1,2 @@ +Percentile 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 +test 4.0 10.0 15.0 15.0 14.0 17.0 14.0 13.0 17.0 16.0 15.0 19.0 20.0 19.0 24.0 24.0 32.0 35.0 35.0 29.0 29.0 30.0 34.0 35.0 40.0 35.0 30.0 29.0 29.0 30.0 32.0 34.0 33.0 31.0 29.0 29.0 31.0 37.0 38.0 35.0 33.0 37.0 44.0 49.0 50.0 52.0 52.0 48.0 47.0 53.0 53.0 52.0 49.0 44.0 43.0 43.0 45.0 50.0 48.0 50.0 52.0 56.0 52.0 48.0 45.0 43.0 40.0 38.0 38.0 37.0 38.0 38.0 34.0 28.0 35.0 34.0 35.0 35.0 32.0 32.0 33.0 28.0 29.0 27.0 25.0 19.0 22.0 20.0 22.0 21.0 25.0 27.0 26.0 21.0 21.0 17.0 16.0 13.0 10.0 1.0 diff --git a/tests/expected/rseqc/read_gc.GC.xls b/tests/expected/rseqc/read_gc.GC.xls new file mode 100644 index 00000000..e7aff082 --- /dev/null +++ b/tests/expected/rseqc/read_gc.GC.xls @@ -0,0 +1,725 @@ +GC% read_count +31.47 29 +32.17 35 +31.11 5 +32.87 41 +30.37 1 +31.97 4 +31.58 28 +33.57 37 +30.48 11 +29.73 9 +31.40 13 +31.71 21 +34.15 5 +31.78 5 +31.30 12 +31.85 8 +30.63 7 +32.52 1 +31.62 11 +31.50 12 +33.10 4 +30.39 8 +29.25 14 +30.89 3 +33.33 118 +30.65 12 +31.25 30 +30.97 8 +31.54 4 +28.85 6 +29.09 5 +31.93 12 +29.60 16 +30.00 25 +29.52 11 +31.82 22 +31.03 20 +32.85 2 +31.09 8 +33.08 6 +32.48 7 +31.75 6 +32.06 2 +33.81 2 +29.81 13 +31.73 12 +34.06 2 +29.57 13 +30.43 18 +32.09 6 +29.82 13 +29.31 20 +31.90 5 +27.78 22 +30.09 17 +31.15 4 +31.86 3 +28.95 18 +27.84 10 +28.71 8 +30.40 4 +28.80 4 +28.57 56 +28.83 6 +32.33 8 +26.37 2 +25.81 6 +28.00 28 +27.83 16 +28.21 7 +30.16 10 +30.77 97 +25.56 2 +27.52 7 +29.36 7 +29.91 25 +29.27 16 +28.45 17 +26.04 2 +27.08 4 +27.55 2 +28.04 5 +27.27 106 +27.93 14 +29.41 35 +29.51 14 +30.71 7 +30.47 4 +28.97 6 +27.45 6 +28.32 10 +28.07 6 +27.37 2 +28.30 6 +28.18 6 +32.03 6 +32.81 1 +26.74 2 +29.17 17 +31.36 8 +27.38 6 +30.30 19 +27.50 8 +28.09 2 +29.70 5 +30.69 13 +32.82 2 +30.70 6 +34.53 4 +28.41 8 +30.56 17 +25.93 14 +28.72 8 +29.47 14 +28.87 17 +29.21 6 +28.89 14 +29.00 10 +30.19 8 +27.16 4 +30.11 8 +28.12 14 +30.91 6 +30.84 16 +28.92 12 +33.04 4 +27.85 2 +29.67 3 +32.08 10 +32.74 6 +28.42 19 +28.74 6 +27.96 4 +29.59 5 +29.07 6 +23.68 1 +32.35 10 +31.52 12 +30.59 8 +30.34 6 +32.29 25 +33.65 5 +30.14 1 +32.65 5 +33.66 3 +32.88 7 +30.12 2 +32.99 10 +30.86 6 +26.67 4 +34.69 6 +30.07 42 +33.68 3 +32.14 10 +33.70 2 +34.74 6 +34.38 6 +32.53 6 +34.78 9 +35.05 6 +36.08 2 +32.84 3 +32.50 13 +33.73 4 +34.18 1 +34.57 9 +36.26 8 +35.87 5 +32.91 3 +42.86 7 +33.80 6 +33.77 4 +36.05 5 +37.63 4 +17.48 1 +36.36 54 +37.78 7 +37.36 2 +37.65 4 +37.23 6 +37.35 2 +38.37 6 +38.64 4 +38.20 2 +37.97 2 +37.70 5 +35.71 9 +38.55 6 +37.10 12 +37.80 5 +37.84 2 +38.16 4 +36.51 9 +37.14 8 +38.36 4 +37.68 5 +34.43 6 +40.00 3 +38.46 27 +11.19 1 +39.02 1 +39.39 2 +32.20 4 +31.20 3 +31.34 2 +29.46 8 +30.53 7 +31.00 8 +31.51 2 +32.18 9 +31.46 8 +30.85 3 +32.73 4 +32.46 5 +30.58 3 +31.87 5 +32.71 6 +33.64 5 +31.91 10 +33.00 4 +32.61 10 +32.67 2 +34.62 8 +33.03 2 +30.83 2 +34.58 1 +32.11 15 +33.94 5 +31.53 2 +33.90 2 +32.98 4 +34.00 11 +33.02 4 +33.67 4 +33.98 8 +32.05 7 +35.64 3 +32.86 6 +32.56 4 +33.72 12 +32.95 18 +32.63 4 +34.29 1 +32.22 5 +31.07 11 +31.43 6 +32.10 5 +31.17 2 +31.37 10 +32.26 4 +31.08 4 +34.48 2 +32.97 4 +31.63 8 +29.87 5 +27.40 2 +28.17 8 +19.28 1 +27.71 1 +30.38 4 +29.11 7 +26.58 1 +26.28 4 +29.37 157 +25.87 109 +27.21 7 +26.47 1 +26.87 2 +27.86 16 +27.97 140 +27.07 7 +27.41 6 +29.32 7 +26.92 8 +26.72 6 +27.34 3 +28.36 6 +28.67 185 +29.08 20 +28.68 20 +28.79 8 +28.37 7 +29.79 5 +31.65 6 +28.78 8 +28.10 6 +27.64 7 +27.12 6 +27.91 2 +28.15 6 +29.93 3 +28.47 9 +28.23 2 +28.24 10 +27.66 3 +28.70 15 +28.81 12 +29.50 5 +30.22 4 +27.10 4 +30.50 4 +31.21 4 +27.13 6 +30.15 5 +30.94 6 +27.72 2 +27.62 2 +29.06 9 +30.99 6 +29.75 17 +31.39 2 +29.20 22 +29.84 12 +30.23 8 +29.29 19 +32.39 2 +30.25 4 +29.35 8 +30.21 10 +34.27 41 +32.31 4 +33.09 4 +30.17 8 +34.97 20 +33.05 2 +33.82 2 +32.00 8 +34.35 6 +35.21 6 +35.66 44 +32.80 1 +35.77 8 +33.60 1 +34.65 1 +35.51 2 +39.16 1 +34.59 1 +32.38 2 +35.11 2 +35.88 6 +36.17 4 +37.06 93 +32.93 5 +35.00 9 +35.38 1 +36.03 2 +36.96 6 +36.88 4 +35.25 4 +35.43 4 +36.76 8 +34.26 6 +35.54 2 +36.29 4 +35.34 4 +36.92 14 +37.31 10 +35.48 1 +36.04 10 +37.76 92 +32.69 4 +34.91 6 +35.78 1 +34.82 4 +36.84 12 +34.86 3 +34.04 4 +35.40 6 +33.91 1 +35.83 2 +36.07 6 +35.59 6 +36.50 4 +35.65 4 +37.41 4 +37.01 1 +34.34 4 +36.52 4 +36.13 4 +37.19 8 +37.40 10 +37.59 4 +38.24 6 +36.00 17 +37.50 19 +38.06 2 +38.69 4 +38.41 9 +34.09 3 +35.90 6 +37.86 3 +38.57 1 +37.12 4 +36.62 8 +37.30 1 +34.02 2 +36.59 4 +35.45 4 +36.21 6 +36.44 4 +38.18 1 +33.93 1 +34.55 2 +36.72 7 +37.21 2 +35.94 1 +31.88 2 +35.19 2 +37.39 10 +38.10 6 +38.28 2 +37.98 2 +37.69 2 +35.35 2 +36.61 2 +38.40 6 +38.58 2 +37.04 4 +36.70 2 +37.96 5 +36.45 4 +37.27 10 +40.16 1 +37.60 2 +36.54 5 +36.43 2 +30.67 5 +34.41 2 +36.11 2 +36.28 6 +36.89 2 +36.97 8 +36.15 1 +37.88 2 +35.42 6 +36.19 2 +38.17 4 +34.44 2 +35.92 1 +34.95 1 +35.79 3 +37.89 1 +36.79 1 +37.38 4 +38.68 1 +30.51 2 +36.46 4 +36.94 4 +35.29 8 +35.37 2 +38.00 3 +38.14 2 +36.90 2 +36.78 1 +35.63 1 +37.00 2 +35.16 2 +36.67 2 +34.72 2 +37.74 6 +38.53 2 +38.89 2 +39.78 1 +37.11 2 +38.61 4 +38.71 1 +39.33 2 +39.29 6 +39.77 1 +39.00 1 +39.80 2 +39.60 2 +40.62 1 +39.58 1 +40.23 2 +40.82 2 +41.46 3 +41.05 4 +41.67 6 +40.96 2 +40.86 2 +42.11 2 +41.11 2 +42.67 6 +41.03 2 +41.25 2 +41.86 2 +42.53 4 +41.56 2 +43.94 2 +43.48 4 +42.31 2 +43.08 3 +44.90 2 +24.19 6 +22.58 3 +16.94 1 +23.97 6 +24.22 7 +23.77 4 +24.58 4 +23.62 9 +22.50 6 +23.08 4 +20.91 2 +22.11 2 +30.68 1 +32.54 1 +30.95 10 +29.55 10 +28.99 13 +31.76 2 +31.96 4 +30.28 2 +23.19 3 +28.26 11 +30.10 5 +29.13 21 +28.46 13 +20.77 1 +19.23 1 +31.33 2 +29.03 9 +28.03 4 +29.89 10 +29.90 8 +28.06 10 +28.93 10 +28.69 4 +27.48 3 +29.66 12 +31.31 1 +31.18 4 +28.35 2 +24.48 103 +30.61 13 +32.58 4 +31.68 4 +26.05 5 +29.77 14 +18.32 1 +29.58 10 +19.08 1 +31.01 1 +30.36 3 +29.69 3 +34.33 4 +29.01 7 +29.23 2 +29.85 4 +32.04 2 +30.08 2 +30.60 1 +34.67 4 +29.92 3 +29.10 6 +26.57 58 +30.93 4 +33.06 1 +34.07 1 +29.63 6 +32.47 6 +26.62 4 +26.06 4 +28.91 3 +27.69 2 +26.24 4 +28.40 8 +28.28 6 +25.17 154 +27.73 9 +24.82 10 +27.20 1 +27.87 2 +27.42 4 +26.15 16 +25.19 3 +25.95 5 +25.53 13 +28.16 1 +31.67 2 +25.35 9 +27.17 3 +25.76 6 +25.38 1 +30.49 2 +28.33 2 +26.77 3 +30.26 1 +25.74 2 +22.78 1 +32.89 1 +26.36 6 +25.00 12 +20.98 1 +27.59 2 +26.45 4 +26.96 4 +25.78 6 +23.78 52 +26.02 8 +26.61 4 +24.64 6 +26.50 4 +26.27 2 +25.60 4 +24.80 6 +29.33 3 +27.19 2 +25.58 4 +25.18 6 +25.41 2 +24.43 6 +24.46 2 +28.75 7 +27.88 2 +24.24 6 +23.88 6 +44.83 1 +25.69 2 +23.13 3 +23.40 4 +22.38 1 +22.39 1 +27.18 2 +25.22 2 +24.41 3 +24.81 2 +23.70 2 +23.36 2 +27.63 4 +26.83 4 +26.80 3 +24.37 6 +23.53 4 +26.73 2 +25.96 4 +25.45 2 +23.81 5 +23.44 2 +24.17 4 +24.79 2 +20.69 1 +24.09 2 +21.84 1 +23.39 4 +23.20 2 +14.69 1 +27.47 4 +24.35 2 +24.65 1 +26.26 2 +25.90 4 +23.93 4 +23.66 4 +25.97 1 +26.97 4 +24.63 2 +26.95 1 +24.49 2 +23.64 2 +23.21 3 +24.11 1 +25.71 2 +24.74 2 +24.04 2 +23.48 4 +22.13 5 +23.02 2 +22.83 5 +25.20 1 +26.09 2 +22.22 3 +23.23 2 +22.32 2 +22.12 3 +24.29 4 +26.85 1 +20.35 1 +22.00 2 +21.50 2 +23.26 2 +22.48 2 +24.44 2 +21.74 2 +20.66 2 +14.95 1 +20.49 1 +16.78 1 +21.00 2 +21.65 2 +24.26 2 +21.28 1 +22.34 1 +49.23 1 +46.94 2 +48.98 2 +45.36 1 +44.33 3 +48.65 2 +45.16 4 +47.62 6 +46.07 8 +46.55 2 +50.00 2 +44.68 4 +44.21 4 +47.69 6 +49.25 2 +47.67 4 +48.15 2 +46.05 2 +45.45 2 +43.21 4 +45.71 4 +47.06 2 +43.42 2 +47.54 2 +41.00 1 +42.00 1 +44.78 2 +45.90 2 +43.66 4 +47.56 1 diff --git a/tests/integration_test.rs b/tests/integration_test.rs index c7ea3acd..a5e6d820 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -1026,3 +1026,135 @@ fn test_dup_check_parallel_uses_global_duplicate_state() { let _ = fs::remove_dir_all(root); } + +// =================================================================== +// RSeQC geneBody_coverage +// =================================================================== + +/// Read the BED12 gene model the fixture was generated from. +fn read_bed12(path: &std::path::Path) -> Vec<(String, bool, Vec<(i64, i64)>)> { + std::fs::read_to_string(path) + .unwrap() + .lines() + .filter(|l| !l.is_empty() && !l.starts_with('#')) + .map(|line| { + let f: Vec<&str> = line.split('\t').collect(); + let chrom = f[0].to_string(); + let start: i64 = f[1].parse().unwrap(); + let reverse = f[5] == "-"; + let sizes: Vec = f[10] + .trim_end_matches(',') + .split(',') + .map(|v| v.parse().unwrap()) + .collect(); + let starts: Vec = f[11] + .trim_end_matches(',') + .split(',') + .map(|v| v.parse().unwrap()) + .collect(); + let exons = starts + .iter() + .zip(&sizes) + .map(|(offset, size)| (start + offset, start + offset + size)) + .collect(); + (chrom, reverse, exons) + }) + .collect() +} + +/// The aggregated gene body coverage curve, against RSeQC's own output. +/// +/// RSeQC reads coverage at 100 interpolated positions per transcript and sums +/// them 5' to 3'. A read counts unless it is deleted at that base, QC-failed, +/// secondary, unmapped or duplicate-flagged. +#[test] +fn genebody_coverage_matches_rseqc() { + use rust_htslib::bam::{IndexedReader, Read as BamRead}; + use rustqc::rna::rseqc::genebody_coverage::{ + transcript_points, GeneBodyCoverage, DEFAULT_MIN_MRNA_LENGTH, + }; + use std::collections::BTreeMap; + + let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")); + let genes = read_bed12(&root.join("tests/data/test_genes.bed")); + let mut reader = IndexedReader::from_path(root.join("tests/data/test.bam")).unwrap(); + + let mut aggregate = GeneBodyCoverage::default(); + for (chrom, reverse, exons) in &genes { + let Some(points) = transcript_points(chrom, *reverse, exons, DEFAULT_MIN_MRNA_LENGTH) + else { + continue; + }; + + // Count, at each percentile position, the reads covering it. + let mut coverage: BTreeMap = points.positions.iter().map(|p| (*p, 0)).collect(); + let first = points.positions.first().copied().unwrap_or(1) - 1; + let last = points.positions.last().copied().unwrap_or(1); + if reader.fetch((chrom.as_str(), first.max(0), last)).is_err() { + continue; + } + let mut record = rust_htslib::bam::Record::new(); + while let Some(result) = reader.read(&mut record) { + result.unwrap(); + let flags = record.flags(); + // The exclusions RSeQC applies per read. + if flags & 0x4 != 0 || flags & 0x100 != 0 || flags & 0x200 != 0 || flags & 0x400 != 0 { + continue; + } + // Reference-covering blocks only, so a deletion does not count. + let mut reference = record.pos(); + for op in record.cigar().iter() { + use rust_htslib::bam::record::Cigar; + match op { + Cigar::Match(n) | Cigar::Equal(n) | Cigar::Diff(n) => { + for k in 0..i64::from(*n) { + // Positions are one-based. + if let Some(count) = coverage.get_mut(&(reference + k + 1)) { + *count += 1; + } + } + reference += i64::from(*n); + } + // A deletion is skipped, but a reference skip is not: + // pysam's pileup excludes `is_del` alone, so a read + // spanning an intron still counts at the bases it skips. + Cigar::RefSkip(n) => { + for k in 0..i64::from(*n) { + if let Some(count) = coverage.get_mut(&(reference + k + 1)) { + *count += 1; + } + } + reference += i64::from(*n); + } + Cigar::Del(n) => reference += i64::from(*n), + _ => {} + } + } + } + aggregate.add_transcript(&points, &coverage); + } + + let reference_line = + std::fs::read_to_string(root.join("tests/expected/rseqc/geneBodyCoverage.txt")) + .unwrap() + .lines() + .nth(1) + .unwrap() + .to_string(); + let want: Vec = reference_line + .split('\t') + .skip(1) + .map(|v| v.parse::().unwrap() as u64) + .collect(); + + assert_eq!(want.len(), 100, "RSeQC reports 100 percentile points"); + assert_eq!(aggregate.totals.len(), 100); + for (index, (got, expected)) in aggregate.totals.iter().zip(&want).enumerate() { + assert_eq!( + got, + expected, + "percentile {} differs: got {got}, want {expected}", + index + 1 + ); + } +}