diff --git a/AGENTS.md b/AGENTS.md index 108414ac..2d487396 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -62,10 +62,39 @@ 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 + coverage/ + mod.rs — Coverage tracks + bedgraph.rs — bedtools genomecov semantics, per-contig accumulators + bigwig.rs — bigWig writing via bigtools (behind the `bigwig` feature) + 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 +104,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,9 +116,6 @@ 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 @@ -98,7 +123,6 @@ src/ plots.rs — RSeQC plot generation (duplication, junctions, etc.) read_distribution.rs — read_distribution.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 +131,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..5c77dc24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,31 @@ # 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. + +- New bigWig coverage tracks for `rustqc rna`, computed in the existing single + pass and replacing the `bedtools genomecov` into `bedGraphToBigWig` + round-trip (#112). Off by default; enable with `coverage_tracks.enabled` and + optionally `stranded`. Behind the `bigwig` cargo feature. + +### 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/Cargo.lock b/Cargo.lock index da20215e..f18e3d33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -88,6 +88,38 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "bigtools" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1b9bbf6596d602e472a23ed5aa5d611fb04f14e7772226fb61c720e806202e" +dependencies = [ + "bincode", + "byteorder", + "byteordered", + "bytes", + "crossbeam-channel", + "crossbeam-utils", + "futures", + "index_list", + "itertools 0.10.5", + "libdeflater", + "serde", + "smallvec", + "tempfile", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bindgen" version = "0.69.5" @@ -97,7 +129,7 @@ dependencies = [ "bitflags 2.11.0", "cexpr", "clang-sys", - "itertools", + "itertools 0.12.1", "lazy_static", "lazycell", "proc-macro2", @@ -105,7 +137,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn", + "syn 2.0.117", ] [[package]] @@ -151,6 +183,21 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteordered" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbf2cd9424f5ff404aba1959c835cbc448ee8b689b870a9981c76c0fd46280e6" +dependencies = [ + "byteorder", +] + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + [[package]] name = "bzip2-sys" version = "0.1.13+1.0.8" @@ -254,7 +301,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -373,6 +420,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -427,7 +483,7 @@ checksum = "d150dea618e920167e5973d70ae6ece4385b7164e0d799fe7c122dd0a5d912ad" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -438,7 +494,7 @@ checksum = "2cdc8d50f426189eef89dac62fabfa0abb27d5cc008f25bf4156a0203325becc" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -470,7 +526,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -535,6 +591,22 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + [[package]] name = "fdeflate" version = "0.3.7" @@ -615,7 +687,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -653,6 +725,94 @@ dependencies = [ "quick-error", ] +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + [[package]] name = "getrandom" version = "0.2.17" @@ -896,6 +1056,12 @@ dependencies = [ "png", ] +[[package]] +name = "index_list" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30141a73bc8a129ac1ce472e33f45af3e2091d86b3479061b9c2f92fdbe9a28c" + [[package]] name = "indexmap" version = "2.13.0" @@ -927,6 +1093,15 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.12.1" @@ -963,7 +1138,7 @@ checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1016,6 +1191,24 @@ version = "0.2.184" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" +[[package]] +name = "libdeflate-sys" +version = "1.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6949d73714ba8c32d2757405b89c94e427f64f078a4041debe07e1b8e9e850e9" +dependencies = [ + "cc", +] + +[[package]] +name = "libdeflater" +version = "1.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b72b274104f747cb65358c918e38c6cd4a69937937a36c8ad3cfd516c9c471e" +dependencies = [ + "libdeflate-sys", +] + [[package]] name = "libloading" version = "0.8.9" @@ -1054,6 +1247,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfae20f6b19ad527b550c223fddc3077a547fc70cda94b9b566575423fd303ee" +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "litemap" version = "0.8.1" @@ -1198,6 +1397,12 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + [[package]] name = "pkg-config" version = "0.3.32" @@ -1303,7 +1508,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.117", ] [[package]] @@ -1475,11 +1680,25 @@ dependencies = [ "semver 1.0.27", ] +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + [[package]] name = "rustqc" version = "0.2.1" dependencies = [ "anyhow", + "bigtools", "cc", "clap", "coitrees", @@ -1502,6 +1721,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml_ng", + "tokio", ] [[package]] @@ -1564,7 +1784,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1605,6 +1825,12 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "smallvec" version = "1.15.1" @@ -1633,7 +1859,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn", + "syn 2.0.117", ] [[package]] @@ -1647,6 +1873,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "synstructure" version = "0.13.2" @@ -1655,7 +1892,20 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", ] [[package]] @@ -1684,7 +1934,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1695,7 +1945,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1708,6 +1958,15 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "pin-project-lite", +] + [[package]] name = "ttf-parser" version = "0.20.0" @@ -1834,7 +2093,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wasm-bindgen-shared", ] @@ -1959,7 +2218,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1970,7 +2229,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2118,7 +2377,7 @@ dependencies = [ "heck", "indexmap", "prettyplease", - "syn", + "syn 2.0.117", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -2134,7 +2393,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -2212,7 +2471,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -2233,7 +2492,7 @@ checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2253,7 +2512,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -2287,7 +2546,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index ebf02f0e..4423d2fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,18 @@ path = "src/lib.rs" name = "rustqc" path = "src/main.rs" +[features] +default = ["bigwig"] +# bigWig coverage tracks. Enabled by default; disable with +# `--no-default-features` to drop bigtools and its transitive packages when +# only the text outputs are wanted. +bigwig = ["dep:bigtools", "dep:tokio"] + [dependencies] +# bigWig writing for coverage tracks (optional, see the `bigwig` feature) +bigtools = { version = "0.5", default-features = false, features = ["write", "read"], optional = true } +tokio = { version = "1", default-features = false, features = ["rt-multi-thread"], optional = true } + # CLI argument parsing clap = { version = "4", features = ["derive", "env"] } 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/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/common/coverage/bedgraph.rs b/src/common/coverage/bedgraph.rs new file mode 100644 index 00000000..8bc79392 --- /dev/null +++ b/src/common/coverage/bedgraph.rs @@ -0,0 +1,451 @@ +//! Per-base coverage in `bedtools genomecov` semantics. +//! +//! # Upstream semantics +//! +//! Matches `bedtools genomecov -ibam -bg -split`. Two things about it are +//! worth stating, because neither matches the depth engines already in this +//! crate: +//! +//! `-split` counts only the reference blocks a read actually aligns to, so an +//! `N` in the CIGAR breaks the read into separate intervals rather than +//! covering the intron. Deletions break it too. +//! +//! There is no filtering. Duplicates, secondary alignments and low mapping +//! quality all contribute, and overlapping mates of a pair each count. That is +//! the opposite of what mosdepth does by default and of what +//! `CollectWgsMetrics` does, and it is why this has its own accumulator rather +//! than reusing either. + +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::*; + +/// One bedGraph interval: a half-open span at a constant depth. +#[derive(Debug, Clone, PartialEq)] +pub struct Interval { + /// Contig name. + pub chrom: String, + /// Zero-based start. + pub start: u32, + /// Half-open end. + pub end: u32, + /// Depth over the span, after any scaling. + pub value: f32, +} + +/// Accumulates per-base coverage for one contig. +#[derive(Debug)] +pub struct CoverageAccum { + /// Delta array of length `contig_len + 1`. + deltas: Vec, + len: usize, + /// Only reads on this strand contribute; `None` counts every read. + strand: Option, + /// Whether the array grows to fit the reads rather than being clamped to a + /// known contig length. This is a field rather than something inferred + /// from the array, because after its first read a growable accumulator is + /// indistinguishable from a fixed one of that size and would silently stop + /// growing. + growable: bool, +} + +impl CoverageAccum { + /// Allocate for one contig of known length. + /// + /// `strand` restricts the reads counted, which is how the forward and + /// reverse tracks are produced. It follows `bedtools genomecov -strand`: + /// the read's own strand, not the fragment's. + pub fn new(length: u64, strand: Option) -> Self { + let len = length as usize; + Self { + deltas: vec![0i32; len + 1], + len, + strand, + growable: false, + } + } + + /// Allocate without knowing the contig length, growing as reads arrive. + /// + /// This is what lets the accumulator sit in the single alignment pass + /// alongside the others, which are constructed before any contig is known. + /// The length is only needed when the track is written, and it comes from + /// the alignment header there. + pub fn growable(strand: Option) -> Self { + Self { + deltas: Vec::new(), + len: 0, + strand, + growable: true, + } + } + + /// Offer one record. + pub fn process_read(&mut self, record: &bam::Record) { + if record.flags() & BAM_FUNMAP != 0 { + return; + } + if let Some(wanted) = self.strand { + let reverse = record.flags() & BAM_FREVERSE != 0; + let actual = if reverse { '-' } else { '+' }; + if actual != wanted { + return; + } + } + + let mut position = record.pos(); + for op in record.cigar().iter() { + match op { + Cigar::Match(n) | Cigar::Equal(n) | Cigar::Diff(n) => { + let n = i64::from(*n); + self.add(position, position + n); + position += n; + } + // Both break the read into separate intervals under `-split`. + Cigar::Del(n) | Cigar::RefSkip(n) => position += i64::from(*n), + Cigar::Ins(_) | Cigar::SoftClip(_) | Cigar::HardClip(_) | Cigar::Pad(_) => {} + } + } + } + + /// Record a half-open block, clamped to the contig when its length is + /// known and growing the array when it is not. + fn add(&mut self, start: i64, end: i64) { + let start = start.max(0) as usize; + let mut end = end.max(0) as usize; + if self.growable { + if end + 1 > self.deltas.len() { + self.deltas.resize(end + 1, 0); + self.len = end; + } + } else { + end = end.min(self.len); + } + if start >= end { + return; + } + self.deltas[start] += 1; + self.deltas[end] -= 1; + } + + /// Fold another accumulator for the same contig in. + pub fn merge(&mut self, other: CoverageAccum) { + if other.deltas.len() > self.deltas.len() { + self.deltas.resize(other.deltas.len(), 0); + self.len = other.len.max(self.len); + } + for (index, delta) in other.deltas.iter().enumerate() { + self.deltas[index] += delta; + } + } + + /// Collapse into bedGraph intervals, dropping the zero-depth spans that + /// `bedtools genomecov -bg` omits. + /// + /// `scale` multiplies every depth, which is how normalised tracks are + /// produced; a scale of 1.0 leaves the raw counts. + pub fn into_intervals(self, chrom: &str, scale: f32) -> Vec { + let mut intervals = Vec::new(); + let mut running = 0i32; + let mut run_start = 0usize; + let mut run_depth = 0i32; + + for position in 0..self.len { + running += self.deltas[position]; + if position == 0 { + run_depth = running; + run_start = 0; + continue; + } + if running != run_depth { + if run_depth > 0 { + intervals.push(Interval { + chrom: chrom.to_string(), + start: run_start as u32, + end: position as u32, + value: run_depth as f32 * scale, + }); + } + run_depth = running; + run_start = position; + } + } + if run_depth > 0 && run_start < self.len { + intervals.push(Interval { + chrom: chrom.to_string(), + start: run_start as u32, + end: self.len as u32, + value: run_depth as f32 * scale, + }); + } + intervals + } +} + +/// Write intervals as a bedGraph. +pub fn write_bedgraph(intervals: &[Interval], path: &Path) -> Result<()> { + let mut out = std::fs::File::create(path) + .map(std::io::BufWriter::new) + .with_context(|| format!("Failed to create bedGraph: {}", path.display()))?; + for interval in intervals { + // Whole numbers print without a fractional part, as bedtools does for + // unscaled counts; a scaled track keeps its decimals. + if interval.value.fract() == 0.0 { + writeln!( + out, + "{}\t{}\t{}\t{}", + interval.chrom, interval.start, interval.end, interval.value as i64 + )?; + } else { + writeln!( + out, + "{}\t{}\t{}\t{}", + interval.chrom, interval.start, interval.end, interval.value + )?; + } + } + out.flush()?; + Ok(()) +} + +/// Coverage tracks across every contig, one accumulator per strand. +/// +/// Positions are contig-local, so accumulators from different contigs must +/// never be summed. This keeps them keyed by contig, which is what makes the +/// per-chromosome workers safe to merge. +#[derive(Debug, Default)] +pub struct CoverageTracks { + /// Strands to track, in output order. A single `None` means one combined + /// track counting every read. + strands: Vec>, + /// Per contig, one accumulator per entry in `strands`. + per_chrom: std::collections::HashMap>, +} + +impl CoverageTracks { + /// Track the given strands. An empty list disables the tracks entirely. + pub fn new(strands: Vec>) -> Self { + Self { + strands, + per_chrom: std::collections::HashMap::new(), + } + } + + /// Whether anything is being tracked. + pub fn is_enabled(&self) -> bool { + !self.strands.is_empty() + } + + /// Offer one record, which must belong to `chrom`. + pub fn process_read(&mut self, record: &bam::Record, chrom: &str) { + if self.strands.is_empty() { + return; + } + let strands = &self.strands; + let accums = self.per_chrom.entry(chrom.to_string()).or_insert_with(|| { + strands + .iter() + .map(|strand| CoverageAccum::growable(*strand)) + .collect() + }); + for accum in accums.iter_mut() { + accum.process_read(record); + } + } + + /// Fold another set in, contig by contig. + pub fn merge(&mut self, other: CoverageTracks) { + for (chrom, theirs) in other.per_chrom { + match self.per_chrom.get_mut(&chrom) { + Some(mine) => { + for (mine, theirs) in mine.iter_mut().zip(theirs) { + mine.merge(theirs); + } + } + None => { + self.per_chrom.insert(chrom, theirs); + } + } + } + } + + /// Collapse into one interval list per tracked strand, contigs in the + /// order given, which is the alignment header's order. + pub fn into_intervals( + mut self, + chrom_order: &[String], + scale: f32, + ) -> Vec<(Option, Vec)> { + let mut out: Vec<(Option, Vec)> = + self.strands.iter().map(|s| (*s, Vec::new())).collect(); + for chrom in chrom_order { + if let Some(accums) = self.per_chrom.remove(chrom) { + for (index, accum) in accums.into_iter().enumerate() { + out[index].1.extend(accum.into_intervals(chrom, scale)); + } + } + } + out + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rust_htslib::bam::record::{CigarString, Record}; + + fn record(pos: i64, cigar: Vec, flags: u16) -> Record { + let query: 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 mut r = Record::new(); + r.set( + b"q", + Some(&CigarString(cigar)), + &vec![b'A'; query], + &vec![30u8; query], + ); + r.set_pos(pos); + r.set_flags(flags); + r + } + + #[test] + fn a_spliced_read_does_not_cover_its_intron() { + let mut accum = CoverageAccum::new(20, None); + accum.process_read(&record( + 0, + vec![Cigar::Match(3), Cigar::RefSkip(5), Cigar::Match(3)], + 0, + )); + let intervals = accum.into_intervals("chr1", 1.0); + assert_eq!( + intervals, + vec![ + Interval { + chrom: "chr1".into(), + start: 0, + end: 3, + value: 1.0 + }, + Interval { + chrom: "chr1".into(), + start: 8, + end: 11, + value: 1.0 + }, + ], + "the intron is absent rather than at depth zero" + ); + } + + #[test] + fn a_deletion_also_splits_the_interval() { + let mut accum = CoverageAccum::new(20, None); + accum.process_read(&record( + 0, + vec![Cigar::Match(2), Cigar::Del(2), Cigar::Match(2)], + 0, + )); + let intervals = accum.into_intervals("chr1", 1.0); + assert_eq!(intervals.len(), 2); + assert_eq!((intervals[0].start, intervals[0].end), (0, 2)); + assert_eq!((intervals[1].start, intervals[1].end), (4, 6)); + } + + #[test] + fn runs_of_equal_depth_collapse_into_one_interval() { + let mut accum = CoverageAccum::new(20, None); + accum.process_read(&record(0, vec![Cigar::Match(10)], 0)); + accum.process_read(&record(0, vec![Cigar::Match(10)], 0)); + let intervals = accum.into_intervals("chr1", 1.0); + assert_eq!(intervals.len(), 1, "one span at depth 2"); + assert_eq!(intervals[0].value, 2.0); + } + + #[test] + fn zero_depth_spans_are_omitted() { + let mut accum = CoverageAccum::new(20, None); + accum.process_read(&record(5, vec![Cigar::Match(3)], 0)); + let intervals = accum.into_intervals("chr1", 1.0); + assert_eq!(intervals.len(), 1); + assert_eq!((intervals[0].start, intervals[0].end), (5, 8)); + } + + #[test] + fn nothing_is_filtered_out() { + // Duplicates and secondary alignments contribute, unlike every other + // depth engine in this crate. + let mut accum = CoverageAccum::new(20, None); + accum.process_read(&record(0, vec![Cigar::Match(4)], BAM_FDUP)); + accum.process_read(&record(0, vec![Cigar::Match(4)], BAM_FSECONDARY)); + assert_eq!(accum.into_intervals("chr1", 1.0)[0].value, 2.0); + } + + #[test] + fn an_unmapped_read_contributes_nothing() { + let mut accum = CoverageAccum::new(20, None); + accum.process_read(&record(0, vec![Cigar::Match(4)], BAM_FUNMAP)); + assert!(accum.into_intervals("chr1", 1.0).is_empty()); + } + + #[test] + fn the_strand_filter_follows_the_reads_own_strand() { + let mut forward = CoverageAccum::new(20, Some('+')); + forward.process_read(&record(0, vec![Cigar::Match(4)], 0)); + forward.process_read(&record(0, vec![Cigar::Match(4)], BAM_FREVERSE)); + assert_eq!(forward.into_intervals("chr1", 1.0)[0].value, 1.0); + + let mut reverse = CoverageAccum::new(20, Some('-')); + reverse.process_read(&record(0, vec![Cigar::Match(4)], 0)); + reverse.process_read(&record(0, vec![Cigar::Match(4)], BAM_FREVERSE)); + assert_eq!(reverse.into_intervals("chr1", 1.0)[0].value, 1.0); + } + + #[test] + fn scaling_multiplies_every_depth() { + let mut accum = CoverageAccum::new(20, None); + accum.process_read(&record(0, vec![Cigar::Match(4)], 0)); + assert_eq!(accum.into_intervals("chr1", 2.5)[0].value, 2.5); + } + + #[test] + fn a_growable_accumulator_needs_no_length_up_front() { + let mut accum = CoverageAccum::growable(None); + accum.process_read(&record(100, vec![Cigar::Match(5)], 0)); + let intervals = accum.into_intervals("chr1", 1.0); + assert_eq!(intervals.len(), 1); + assert_eq!((intervals[0].start, intervals[0].end), (100, 105)); + } + + #[test] + fn merging_adds_two_workers_coverage() { + let mut a = CoverageAccum::growable(None); + a.process_read(&record(0, vec![Cigar::Match(4)], 0)); + let mut b = CoverageAccum::growable(None); + b.process_read(&record(0, vec![Cigar::Match(4)], 0)); + b.process_read(&record(10, vec![Cigar::Match(2)], 0)); + a.merge(b); + let intervals = a.into_intervals("chr1", 1.0); + assert_eq!(intervals[0].value, 2.0, "the shared span doubles"); + assert_eq!((intervals[1].start, intervals[1].end), (10, 12)); + } + + #[test] + fn a_read_running_past_the_contig_end_is_clipped() { + let mut accum = CoverageAccum::new(6, None); + accum.process_read(&record(4, vec![Cigar::Match(10)], 0)); + let intervals = accum.into_intervals("chr1", 1.0); + assert_eq!((intervals[0].start, intervals[0].end), (4, 6)); + } +} diff --git a/src/common/coverage/bigwig.rs b/src/common/coverage/bigwig.rs new file mode 100644 index 00000000..c2014bc3 --- /dev/null +++ b/src/common/coverage/bigwig.rs @@ -0,0 +1,154 @@ +//! bigWig writing for coverage tracks. +//! +//! Replaces the `bedtools genomecov` into `bedGraphToBigWig` round-trip with a +//! single write from the coverage already computed in the alignment pass. +//! +//! Behind the `bigwig` cargo feature, so a build that only wants the text +//! outputs need not carry `bigtools`. + +use std::collections::HashMap; +use std::path::Path; + +use anyhow::{anyhow, Context, Result}; +use bigtools::beddata::BedParserStreamingIterator; +use bigtools::{BigWigWrite, Value}; + +use super::bedgraph::Interval; + +/// Write intervals as a bigWig, returning whether a file was produced. +/// +/// `chrom_sizes` must name every contig the intervals refer to, and comes from +/// the alignment header. Intervals are expected in the order the contigs +/// appear there. +/// +/// An empty interval list writes nothing and returns `false`. The format has +/// no representation for a track with no data, and `bigtools` rejects it +/// outright, so the alternative would be failing a run because an alignment +/// covered nothing. +/// +/// Note that a gap between intervals is not zero coverage in a bigWig: it is +/// *undefined*, and reads back as `NaN`. That is the format's own semantics +/// and matches what `bedGraphToBigWig` produces from a bedGraph that omits its +/// zero-depth spans, which is what `bedtools genomecov -bg` emits. +pub fn write_bigwig( + intervals: &[Interval], + chrom_sizes: &[(String, u64)], + path: &Path, +) -> Result { + if intervals.is_empty() { + return Ok(false); + } + let sizes: HashMap = chrom_sizes + .iter() + .map(|(name, length)| (name.clone(), *length as u32)) + .collect(); + + let values: Vec<(String, Value)> = intervals + .iter() + .map(|interval| { + ( + interval.chrom.clone(), + Value { + start: interval.start, + end: interval.end, + value: interval.value, + }, + ) + }) + .collect(); + + // `false` because the intervals are already grouped by contig in header + // order; letting the writer accept out-of-order chromosomes would hide a + // bug in the caller rather than catching it. + let data = BedParserStreamingIterator::wrap_infallible_iter(values.into_iter(), false); + + let writer = BigWigWrite::create_file(path, sizes) + .with_context(|| format!("Failed to create bigWig: {}", path.display()))?; + + // One worker: the encoding is not the bottleneck next to reading the + // alignment, and a fixed thread count keeps the output deterministic. + let runtime = tokio::runtime::Builder::new_multi_thread() + .worker_threads(1) + .build() + .context("Failed to start the bigWig writer runtime")?; + + writer + .write(data, runtime) + .map_err(|e| anyhow!("Failed to write bigWig {}: {e}", path.display()))?; + Ok(true) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn scratch(name: &str) -> std::path::PathBuf { + let dir = std::env::temp_dir().join("rustqc-bigwig-tests"); + std::fs::create_dir_all(&dir).unwrap(); + dir.join(name) + } + + #[test] + fn a_written_track_reads_back_with_the_same_values() { + use bigtools::BigWigRead; + + let intervals = vec![ + Interval { + chrom: "chr1".into(), + start: 10, + end: 20, + value: 3.0, + }, + Interval { + chrom: "chr1".into(), + start: 30, + end: 35, + value: 7.5, + }, + ]; + let path = scratch("roundtrip.bw"); + assert!(write_bigwig(&intervals, &[("chr1".to_string(), 100)], &path).unwrap()); + + let mut reader = BigWigRead::open_file(&path).unwrap(); + let values = reader.values("chr1", 0, 100).unwrap(); + assert_eq!(values[15], 3.0, "inside the first interval"); + assert_eq!(values[32], 7.5, "inside the second"); + } + + #[test] + fn a_gap_is_undefined_rather_than_zero() { + use bigtools::BigWigRead; + + let intervals = vec![Interval { + chrom: "chr1".into(), + start: 10, + end: 20, + value: 3.0, + }]; + let path = scratch("gap.bw"); + write_bigwig(&intervals, &[("chr1".to_string(), 100)], &path).unwrap(); + + let mut reader = BigWigRead::open_file(&path).unwrap(); + let values = reader.values("chr1", 0, 100).unwrap(); + assert!( + values[50].is_nan(), + "a bigWig has no representation for zero coverage; the gap is \ + undefined and reads back as NaN, got {}", + values[50] + ); + } + + #[test] + fn nothing_to_write_produces_no_file_rather_than_failing() { + let path = scratch("empty.bw"); + let _ = std::fs::remove_file(&path); + assert!( + !write_bigwig(&[], &[("chr1".to_string(), 100)], &path).unwrap(), + "an empty track reports that nothing was written" + ); + assert!( + !path.exists(), + "the format cannot express an empty track, so none is created" + ); + } +} diff --git a/src/common/coverage/mod.rs b/src/common/coverage/mod.rs new file mode 100644 index 00000000..131a905a --- /dev/null +++ b/src/common/coverage/mod.rs @@ -0,0 +1,11 @@ +//! Coverage tracks. +//! +//! Produces the per-base coverage that pipelines currently get by running +//! `bedtools genomecov` and then converting its bedGraph to bigWig. Both come +//! out of the alignment pass that is already happening. + +pub mod bedgraph; + +/// bigWig writing, available when built with the `bigwig` feature. +#[cfg(feature = "bigwig")] +pub mod bigwig; 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..77f5a0b0 --- /dev/null +++ b/src/common/mod.rs @@ -0,0 +1,14 @@ +//! 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 coverage; +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..25928bfe 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. @@ -158,6 +162,10 @@ pub struct RnaConfig { #[serde(default)] pub samtools_stats: SamtoolsStatsConfig, + /// bigWig coverage track configuration. + #[serde(default)] + pub coverage_tracks: CoverageTracksConfig, + /// preseq lc_extrap library complexity extrapolation configuration. #[serde(default)] pub preseq: PreseqConfig, @@ -908,6 +916,348 @@ impl RnaConfig { } } +/// Configuration for the bigWig coverage tracks. +/// +/// Replaces the `bedtools genomecov` into `bedGraphToBigWig` round-trip that +/// pipelines otherwise run per strand. +/// +/// Example: +/// ```yaml +/// coverage_tracks: +/// enabled: true +/// stranded: true +/// scale: 1.0 +/// ``` +#[derive(Debug, Deserialize)] +#[serde(default)] +pub struct CoverageTracksConfig { + /// Whether to write coverage tracks. Off by default, since a bigWig is + /// large next to the other outputs and not every run wants one. + pub enabled: bool, + /// Write separate forward and reverse tracks rather than one combined. + pub stranded: bool, + /// Multiplies every depth, for normalised tracks. 1.0 leaves raw counts. + pub scale: f32, +} + +impl Default for CoverageTracksConfig { + fn default() -> Self { + Self { + enabled: false, + stranded: false, + scale: 1.0, + } + } +} + +// =================================================================== +// 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 +1644,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..68212308 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,830 @@ 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) +} + +/// Write one coverage track, returning whether a file was produced. +/// +/// Split out so the `bigwig` feature gate lives in one place: without it the +/// tracks are simply not written, and the run says so rather than failing. +#[cfg(feature = "bigwig")] +fn write_coverage_track( + intervals: &[rustqc::common::coverage::bedgraph::Interval], + chrom_sizes: &[(String, u64)], + path: &Path, +) -> Result { + rustqc::common::coverage::bigwig::write_bigwig(intervals, chrom_sizes, path) +} + +/// Stub for builds without the `bigwig` feature. +#[cfg(not(feature = "bigwig"))] +fn write_coverage_track( + _intervals: &[rustqc::common::coverage::bedgraph::Interval], + _chrom_sizes: &[(String, u64)], + _path: &Path, +) -> Result { + Ok(false) } /// Reconstruct the command line for the featureCounts-compatible header comment. @@ -525,6 +1353,7 @@ fn run_rna(args: cli::RnaArgs, ui: &Ui) -> Result<()> { // Build the shared parameters struct for process_single_bam let shared = SharedParams { + coverage_scale: config.coverage_tracks.scale, ui, stranded: effective_stranded, paired: effective_paired, @@ -618,6 +1447,7 @@ fn run_rna(args: cli::RnaArgs, ui: &Ui) -> Result<()> { runtime_seconds: 0.0, counting: None, dupradar: None, + dna: None, outputs: vec![], }); } @@ -809,6 +1639,8 @@ struct SharedParams<'a> { inner_distance_step: i64, /// Pre-built TIN index for transcript integrity analysis (from GTF). tin_index: Option<&'a rna::rseqc::tin::TinIndex>, + /// Multiplies every coverage depth, for normalised tracks. + coverage_scale: f32, /// Number of equally-spaced positions to sample per transcript for TIN. tin_sample_size: usize, /// Minimum read-start count per transcript to compute TIN. @@ -902,6 +1734,7 @@ impl BamResult { runtime_seconds: self.duration.as_secs_f64(), counting, dupradar, + dna: None, outputs: self .outputs .iter() @@ -956,6 +1789,15 @@ fn process_single_bam( // === Build RSeQC config and annotations === let rseqc_config = RseqcConfig { + coverage_strands: if config.coverage_tracks.enabled { + if config.coverage_tracks.stranded { + vec![Some('+'), Some('-')] + } else { + vec![None] + } + } else { + Vec::new() + }, mapq_cut: params.mapq_cut, infer_experiment_sample_size: params.infer_experiment_sample_size, min_intron: params.min_intron, @@ -1562,7 +2404,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 +2414,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 +2424,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)); @@ -1669,6 +2511,49 @@ fn write_rseqc_outputs( written.push(("read_distribution".into(), p)); } + // --- coverage tracks --- + if accums.coverage.is_enabled() { + let dir_path = if params.flat_output { + outdir.to_path_buf() + } else { + outdir.join("coverage") + }; + std::fs::create_dir_all(&dir_path)?; + let chrom_order: Vec = bam_header_refs + .iter() + .map(|(name, _)| name.clone()) + .collect(); + for (strand, intervals) in accums + .coverage + .into_intervals(&chrom_order, params.coverage_scale) + { + let suffix = match strand { + Some('+') => ".forward", + Some('-') => ".reverse", + _ => "", + }; + let output_path = dir_path.join(format!("{sample_name}{suffix}.bigWig")); + let wrote = write_coverage_track(&intervals, bam_header_refs, &output_path)?; + if wrote { + let p = output_path.display().to_string(); + ui.output_item("coverage", &p); + ui.output_detail(&format!( + "{} intervals", + format_count(intervals.len() as u64) + )); + written.push(("coverage".into(), p)); + } else { + ui.warn(&format!( + "no coverage on the {} strand, so no track was written", + match strand { + Some(s) => s.to_string(), + None => "combined".to_string(), + } + )); + } + } + } + // --- junction_annotation --- if let Some(accum) = accums.junc_annot { std::fs::create_dir_all(&rseqc_junc_annot_dir)?; 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..22ff90a4 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::*; @@ -59,6 +58,9 @@ pub struct RseqcAnnotations<'a> { #[derive(Debug, Clone)] #[allow(dead_code)] pub struct RseqcConfig { + /// Strands to build coverage track accumulators for. Empty disables them; + /// a single `None` entry counts every read into one combined track. + pub coverage_strands: Vec>, /// MAPQ cutoff for read quality filtering. pub mapq_cut: u8, /// Maximum reads to sample for infer_experiment. @@ -116,1200 +118,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 // ------------------------------------------------------------------- @@ -2123,6 +931,8 @@ pub struct RseqcAccumulators { pub tin: Option, /// preseq library complexity accumulator (`None` when disabled). pub preseq: Option, + /// Coverage tracks, kept per contig so workers merge safely. + pub coverage: crate::common::coverage::bedgraph::CoverageTracks, } impl RseqcAccumulators { @@ -2138,6 +948,7 @@ impl RseqcAccumulators { inner_dist: None, tin: None, preseq: None, + coverage: crate::common::coverage::bedgraph::CoverageTracks::default(), } } @@ -2196,6 +1007,9 @@ impl RseqcAccumulators { } else { None }, + coverage: crate::common::coverage::bedgraph::CoverageTracks::new( + config.coverage_strands.clone(), + ), } } @@ -2227,6 +1041,9 @@ impl RseqcAccumulators { accum.process_read(record, chrom, model, config.mapq_cut); } + // Coverage tracks: no filtering at all, matching bedtools genomecov. + self.coverage.process_read(record, chrom); + // read_distribution: needs region sets, uses uppercased chrom if let (Some(ref mut accum), Some(regions)) = (&mut self.read_dist, annotations.rd_regions) { @@ -2311,6 +1128,7 @@ impl RseqcAccumulators { if let (Some(ref mut a), Some(b)) = (&mut self.tin, other.tin) { a.merge(b); } + self.coverage.merge(other.coverage); if let (Some(ref mut a), Some(b)) = (&mut self.preseq, other.preseq) { a.merge(b); } @@ -2327,112 +1145,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/mod.rs b/src/rna/rseqc/mod.rs index a4730ccb..6f38ecca 100644 --- a/src/rna/rseqc/mod.rs +++ b/src/rna/rseqc/mod.rs @@ -7,14 +7,34 @@ pub mod accumulators; pub mod common; pub mod plots; -pub mod bam_stat; -pub mod flagstat; -pub mod idxstats; 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 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/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/dna_integration_test.rs b/tests/dna_integration_test.rs new file mode 100644 index 00000000..406e7397 --- /dev/null +++ b/tests/dna_integration_test.rs @@ -0,0 +1,1373 @@ +//! 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"); +} diff --git a/tests/expected/coverage/test.bedgraph b/tests/expected/coverage/test.bedgraph new file mode 100644 index 00000000..8d68f36e --- /dev/null +++ b/tests/expected/coverage/test.bedgraph @@ -0,0 +1,738 @@ +chr1 1014 1022 1 +chr1 1022 1023 3 +chr1 1023 1031 4 +chr1 1031 1038 5 +chr1 1038 1039 6 +chr1 1039 1040 7 +chr1 1040 1047 8 +chr1 1047 1058 9 +chr1 1058 1064 10 +chr1 1064 1072 9 +chr1 1072 1073 7 +chr1 1073 1074 6 +chr1 1074 1076 7 +chr1 1076 1080 8 +chr1 1080 1081 9 +chr1 1081 1088 8 +chr1 1088 1090 7 +chr1 1090 1097 6 +chr1 1097 1098 5 +chr1 1098 1101 6 +chr1 1101 1105 7 +chr1 1105 1107 8 +chr1 1107 1108 9 +chr1 1108 1124 8 +chr1 1124 1126 7 +chr1 1126 1128 6 +chr1 1128 1130 7 +chr1 1130 1139 6 +chr1 1139 1144 5 +chr1 1144 1147 6 +chr1 1147 1148 7 +chr1 1148 1151 6 +chr1 1151 1155 5 +chr1 1155 1157 4 +chr1 1157 1160 3 +chr1 1160 1163 4 +chr1 1163 1178 5 +chr1 1178 1180 4 +chr1 1180 1186 5 +chr1 1186 1194 6 +chr1 1194 1195 5 +chr1 1195 1197 6 +chr1 1197 1210 5 +chr1 1210 1213 4 +chr1 1213 1224 3 +chr1 1224 1230 4 +chr1 1230 1236 3 +chr1 1236 1245 2 +chr1 1245 1252 1 +chr1 1252 1257 2 +chr1 1257 1260 3 +chr1 1260 1266 5 +chr1 1266 1274 6 +chr1 1274 1286 5 +chr1 1286 1292 6 +chr1 1292 1294 7 +chr1 1294 1296 8 +chr1 1296 1297 9 +chr1 1297 1302 11 +chr1 1302 1307 10 +chr1 1307 1309 9 +chr1 1309 1310 10 +chr1 1310 1312 8 +chr1 1312 1315 9 +chr1 1315 1316 10 +chr1 1316 1319 9 +chr1 1319 1323 10 +chr1 1323 1336 11 +chr1 1336 1342 10 +chr1 1342 1344 9 +chr1 1344 1347 8 +chr1 1347 1354 6 +chr1 1354 1359 7 +chr1 1359 1362 6 +chr1 1362 1365 5 +chr1 1365 1367 4 +chr1 1367 1369 5 +chr1 1369 1372 4 +chr1 1372 1373 5 +chr1 1373 1375 4 +chr1 1375 1388 5 +chr1 1388 1396 6 +chr1 1396 1400 5 +chr1 1400 1408 6 +chr1 1408 1417 7 +chr1 1417 1422 6 +chr1 1422 1424 5 +chr1 1424 1438 6 +chr1 1438 1440 5 +chr1 1440 1450 6 +chr1 1450 1454 5 +chr1 1454 1458 4 +chr1 1458 1474 3 +chr1 1474 1475 2 +chr1 1475 1490 1 +chr1 2018 2021 1 +chr1 2021 2028 2 +chr1 2028 2030 3 +chr1 2030 2051 4 +chr1 2051 2059 5 +chr1 2059 2064 6 +chr1 2064 2068 7 +chr1 2068 2071 6 +chr1 2071 2073 5 +chr1 2073 2078 7 +chr1 2078 2080 6 +chr1 2080 2084 5 +chr1 2084 2093 6 +chr1 2093 2101 7 +chr1 2101 2108 6 +chr1 2108 2109 7 +chr1 2109 2114 6 +chr1 2114 2123 5 +chr1 2123 2131 3 +chr1 2131 2134 4 +chr1 2134 2143 3 +chr1 2143 2148 2 +chr1 2148 2154 3 +chr1 2154 2158 4 +chr1 2158 2168 3 +chr1 2168 2169 4 +chr1 2169 2170 5 +chr1 2170 2173 6 +chr1 2173 2177 7 +chr1 2177 2183 8 +chr1 2183 2185 9 +chr1 2185 2187 10 +chr1 2187 2197 11 +chr1 2197 2198 12 +chr1 2198 2204 11 +chr1 2204 2211 10 +chr1 2211 2218 11 +chr1 2218 2219 10 +chr1 2219 2220 9 +chr1 2220 2223 8 +chr1 2223 2227 7 +chr1 2227 2231 6 +chr1 2231 2233 5 +chr1 2233 2235 4 +chr1 2235 2237 3 +chr1 2237 2245 2 +chr1 2245 2247 3 +chr1 2247 2250 2 +chr1 2250 2253 3 +chr1 2253 2261 4 +chr1 2261 2267 3 +chr1 2267 2274 5 +chr1 2274 2280 6 +chr1 2280 2295 7 +chr1 2295 2300 6 +chr1 2300 2301 5 +chr1 2301 2303 6 +chr1 2303 2305 5 +chr1 2305 2307 6 +chr1 2307 2311 7 +chr1 2311 2317 8 +chr1 2317 2320 7 +chr1 2320 2321 8 +chr1 2321 2324 9 +chr1 2324 2328 8 +chr1 2328 2343 9 +chr1 2343 2346 10 +chr1 2346 2351 11 +chr1 2351 2355 10 +chr1 2355 2357 9 +chr1 2357 2358 8 +chr1 2358 2361 9 +chr1 2361 2367 8 +chr1 2367 2370 7 +chr1 2370 2378 6 +chr1 2378 2380 5 +chr1 2380 2383 4 +chr1 2383 2393 5 +chr1 2393 2398 4 +chr1 2398 2407 5 +chr1 2407 2408 6 +chr1 2408 2409 5 +chr1 2409 2421 6 +chr1 2421 2433 5 +chr1 2433 2448 4 +chr1 2448 2457 3 +chr1 2457 2459 2 +chr1 2459 2496 1 +chr1 3002 3008 1 +chr1 3008 3010 2 +chr1 3010 3018 3 +chr1 3018 3028 4 +chr1 3028 3039 5 +chr1 3039 3041 6 +chr1 3041 3047 8 +chr1 3047 3052 10 +chr1 3052 3058 9 +chr1 3058 3060 8 +chr1 3060 3065 7 +chr1 3065 3068 9 +chr1 3068 3076 8 +chr1 3076 3080 9 +chr1 3080 3084 10 +chr1 3084 3089 11 +chr1 3089 3091 10 +chr1 3091 3092 8 +chr1 3092 3093 9 +chr1 3093 3095 10 +chr1 3095 3097 11 +chr1 3097 3098 9 +chr1 3098 3102 10 +chr1 3102 3105 11 +chr1 3105 3111 12 +chr1 3111 3115 13 +chr1 3115 3123 11 +chr1 3123 3126 12 +chr1 3126 3128 11 +chr1 3128 3130 10 +chr1 3130 3142 9 +chr1 3142 3143 8 +chr1 3143 3145 7 +chr1 3145 3148 6 +chr1 3148 3152 5 +chr1 3152 3155 4 +chr1 3155 3157 3 +chr1 3157 3161 4 +chr1 3161 3173 3 +chr1 3173 3174 2 +chr1 3174 3184 3 +chr1 3184 3194 2 +chr1 3194 3207 3 +chr1 3207 3217 2 +chr1 3217 3218 3 +chr1 3218 3224 4 +chr1 3224 3232 3 +chr1 3232 3244 4 +chr1 3244 3247 3 +chr1 3247 3248 4 +chr1 3248 3258 5 +chr1 3258 3261 6 +chr1 3261 3266 7 +chr1 3266 3267 8 +chr1 3267 3268 7 +chr1 3268 3273 6 +chr1 3273 3278 7 +chr1 3278 3282 8 +chr1 3282 3297 7 +chr1 3297 3298 6 +chr1 3298 3301 5 +chr1 3301 3308 6 +chr1 3308 3311 5 +chr1 3311 3313 4 +chr1 3313 3316 5 +chr1 3316 3323 4 +chr1 3323 3328 3 +chr1 3328 3332 2 +chr1 3332 3334 3 +chr1 3334 3344 4 +chr1 3344 3348 5 +chr1 3348 3351 7 +chr1 3351 3358 6 +chr1 3358 3363 7 +chr1 3363 3365 6 +chr1 3365 3368 7 +chr1 3368 3371 8 +chr1 3371 3382 9 +chr1 3382 3384 8 +chr1 3384 3394 7 +chr1 3394 3398 6 +chr1 3398 3403 4 +chr1 3403 3405 5 +chr1 3405 3408 7 +chr1 3408 3412 6 +chr1 3412 3415 7 +chr1 3415 3417 6 +chr1 3417 3421 7 +chr1 3421 3423 6 +chr1 3423 3434 7 +chr1 3434 3445 8 +chr1 3445 3449 9 +chr1 3449 3453 10 +chr1 3453 3455 9 +chr1 3455 3462 7 +chr1 3462 3467 6 +chr1 3467 3473 5 +chr1 3473 3475 4 +chr1 3475 3477 5 +chr1 3477 3480 6 +chr1 3480 3484 7 +chr1 3484 3489 6 +chr1 3489 3492 8 +chr1 3492 3495 9 +chr1 3495 3499 8 +chr1 3499 3518 7 +chr1 3518 3527 6 +chr1 3527 3539 5 +chr1 3539 3542 3 +chr1 3542 3561 2 +chr1 3561 3566 3 +chr1 3566 3575 4 +chr1 3575 3580 3 +chr1 3580 3594 2 +chr1 3594 3613 3 +chr1 3613 3616 4 +chr1 3616 3624 3 +chr1 3624 3626 4 +chr1 3626 3633 5 +chr1 3633 3636 6 +chr1 3636 3637 7 +chr1 3637 3638 8 +chr1 3638 3644 9 +chr1 3644 3652 8 +chr1 3652 3661 9 +chr1 3661 3663 8 +chr1 3663 3664 7 +chr1 3664 3667 9 +chr1 3667 3675 11 +chr1 3675 3676 12 +chr1 3676 3683 11 +chr1 3683 3686 10 +chr1 3686 3687 9 +chr1 3687 3688 8 +chr1 3688 3689 7 +chr1 3689 3698 8 +chr1 3698 3702 9 +chr1 3702 3714 8 +chr1 3714 3717 6 +chr1 3717 3721 4 +chr1 3721 3724 5 +chr1 3724 3725 4 +chr1 3725 3727 3 +chr1 3727 3739 4 +chr1 3739 3740 3 +chr1 3740 3741 4 +chr1 3741 3742 5 +chr1 3742 3748 6 +chr1 3748 3771 5 +chr1 3771 3777 4 +chr1 3777 3790 3 +chr1 3790 3791 2 +chr1 3791 3792 1 +chr1 5000 5008 1 +chr1 5008 5010 2 +chr1 5010 5050 3 +chr1 5050 5058 2 +chr1 5058 5061 1 +chr1 5061 5110 2 +chr1 5110 5111 1 +chr1 5122 5125 1 +chr1 5125 5172 2 +chr1 5172 5175 1 +chr1 5178 5196 1 +chr1 5196 5199 2 +chr1 5199 5206 4 +chr1 5206 5210 5 +chr1 5210 5228 6 +chr1 5228 5246 5 +chr1 5246 5249 4 +chr1 5249 5253 2 +chr1 5253 5256 3 +chr1 5256 5260 2 +chr1 5260 5267 1 +chr1 5267 5303 3 +chr1 5303 5317 2 +chr1 5335 5380 1 +chr1 5380 5385 2 +chr1 5385 5399 1 +chr1 5399 5430 2 +chr1 5430 5442 1 +chr1 5442 5449 2 +chr1 5449 5492 1 +chr1 5537 5538 1 +chr1 5538 5540 2 +chr1 5540 5587 3 +chr1 5587 5588 2 +chr1 5588 5590 1 +chr1 6006 6008 1 +chr1 6008 6010 2 +chr1 6010 6020 3 +chr1 6020 6034 4 +chr1 6034 6056 5 +chr1 6056 6058 4 +chr1 6058 6060 3 +chr1 6060 6070 2 +chr1 6070 6075 1 +chr1 6075 6084 2 +chr1 6084 6095 1 +chr1 6095 6117 2 +chr1 6117 6118 3 +chr1 6118 6124 4 +chr1 6124 6125 5 +chr1 6125 6145 4 +chr1 6145 6152 3 +chr1 6152 6167 4 +chr1 6167 6168 3 +chr1 6168 6174 2 +chr1 6174 6188 1 +chr1 6188 6192 2 +chr1 6192 6202 3 +chr1 6202 6216 2 +chr1 6216 6218 3 +chr1 6218 6238 4 +chr1 6238 6242 3 +chr1 6242 6266 2 +chr1 6266 6268 1 +chr1 6271 6310 1 +chr1 6310 6315 2 +chr1 6315 6321 3 +chr1 6321 6359 2 +chr1 6359 6365 3 +chr1 6365 6368 2 +chr1 6368 6376 3 +chr1 6376 6402 4 +chr1 6402 6409 5 +chr1 6409 6410 4 +chr1 6410 6414 3 +chr1 6414 6418 4 +chr1 6418 6424 3 +chr1 6424 6426 4 +chr1 6426 6438 3 +chr1 6438 6439 4 +chr1 6439 6452 5 +chr1 6452 6464 4 +chr1 6464 6474 3 +chr1 6474 6488 2 +chr1 6488 6489 1 +chr1 8001 8051 1 +chr1 8082 8091 1 +chr1 8091 8132 2 +chr1 8132 8141 1 +chr1 8179 8229 1 +chr1 8237 8250 1 +chr1 8250 8282 2 +chr1 8282 8287 3 +chr1 8287 8300 2 +chr1 8300 8311 1 +chr1 8311 8332 2 +chr1 8332 8336 1 +chr1 8336 8361 2 +chr1 8361 8372 1 +chr1 8372 8386 2 +chr1 8386 8422 1 +chr1 10059 10109 1 +chr1 10214 10223 1 +chr1 10223 10264 2 +chr1 10264 10273 1 +chr2 1025 1035 1 +chr2 1035 1044 2 +chr2 1044 1047 3 +chr2 1047 1075 4 +chr2 1075 1083 3 +chr2 1083 1084 4 +chr2 1084 1085 5 +chr2 1085 1086 4 +chr2 1086 1094 5 +chr2 1094 1097 4 +chr2 1097 1100 3 +chr2 1100 1103 4 +chr2 1103 1111 5 +chr2 1111 1113 6 +chr2 1113 1119 7 +chr2 1119 1133 8 +chr2 1133 1134 7 +chr2 1134 1136 6 +chr2 1136 1137 5 +chr2 1137 1146 6 +chr2 1146 1150 7 +chr2 1150 1151 6 +chr2 1151 1153 7 +chr2 1153 1154 6 +chr2 1154 1163 7 +chr2 1163 1167 6 +chr2 1167 1168 7 +chr2 1168 1196 8 +chr2 1196 1200 7 +chr2 1200 1201 8 +chr2 1201 1204 7 +chr2 1204 1205 6 +chr2 1205 1212 7 +chr2 1212 1217 8 +chr2 1217 1218 7 +chr2 1218 1219 6 +chr2 1219 1223 5 +chr2 1223 1224 6 +chr2 1224 1237 7 +chr2 1237 1238 6 +chr2 1238 1248 7 +chr2 1248 1250 8 +chr2 1250 1255 7 +chr2 1255 1261 6 +chr2 1261 1262 5 +chr2 1262 1266 4 +chr2 1266 1272 5 +chr2 1272 1273 6 +chr2 1273 1274 5 +chr2 1274 1278 4 +chr2 1278 1285 5 +chr2 1285 1288 6 +chr2 1288 1290 5 +chr2 1290 1298 7 +chr2 1298 1316 6 +chr2 1316 1322 5 +chr2 1322 1328 4 +chr2 1328 1335 3 +chr2 1335 1340 2 +chr2 2009 2017 1 +chr2 2017 2023 2 +chr2 2023 2026 3 +chr2 2026 2038 4 +chr2 2038 2045 5 +chr2 2045 2051 6 +chr2 2051 2059 7 +chr2 2059 2065 6 +chr2 2065 2067 7 +chr2 2067 2068 6 +chr2 2068 2076 7 +chr2 2076 2085 6 +chr2 2085 2088 7 +chr2 2088 2095 6 +chr2 2095 2101 5 +chr2 2101 2104 4 +chr2 2104 2111 5 +chr2 2111 2115 6 +chr2 2115 2116 5 +chr2 2116 2118 6 +chr2 2118 2123 5 +chr2 2123 2135 4 +chr2 2135 2136 3 +chr2 2136 2154 4 +chr2 2154 2161 3 +chr2 2161 2166 2 +chr2 2166 2169 1 +chr2 2169 2186 2 +chr2 2186 2189 1 +chr2 2189 2197 2 +chr2 2197 2202 3 +chr2 2202 2206 4 +chr2 2206 2213 5 +chr2 2213 2214 6 +chr2 2214 2219 8 +chr2 2219 2239 7 +chr2 2239 2247 6 +chr2 2247 2248 5 +chr2 2248 2252 6 +chr2 2252 2253 5 +chr2 2253 2256 6 +chr2 2256 2263 5 +chr2 2263 2264 4 +chr2 2264 2272 2 +chr2 2272 2273 3 +chr2 2273 2292 4 +chr2 2292 2298 5 +chr2 2298 2303 4 +chr2 2303 2309 3 +chr2 2309 2322 4 +chr2 2322 2323 3 +chr2 2323 2332 2 +chr2 2332 2340 3 +chr2 2340 2341 4 +chr2 2341 2342 5 +chr2 2342 2346 4 +chr2 2346 2353 5 +chr2 2353 2359 6 +chr2 2359 2362 5 +chr2 2362 2366 6 +chr2 2366 2379 7 +chr2 2379 2382 9 +chr2 2382 2390 8 +chr2 2390 2391 7 +chr2 2391 2396 6 +chr2 2396 2409 5 +chr2 2409 2412 6 +chr2 2412 2416 5 +chr2 2416 2426 4 +chr2 2426 2429 5 +chr2 2429 2442 3 +chr2 2442 2453 4 +chr2 2453 2459 3 +chr2 2459 2466 2 +chr2 2466 2468 3 +chr2 2468 2469 4 +chr2 2469 2473 5 +chr2 2473 2474 6 +chr2 2474 2476 7 +chr2 2476 2486 6 +chr2 2486 2492 7 +chr2 2492 2505 6 +chr2 2505 2506 7 +chr2 2506 2515 8 +chr2 2515 2516 9 +chr2 2516 2517 8 +chr2 2517 2518 9 +chr2 2518 2519 8 +chr2 2519 2520 7 +chr2 2520 2524 8 +chr2 2524 2536 7 +chr2 2536 2542 6 +chr2 2542 2555 7 +chr2 2555 2556 6 +chr2 2556 2565 5 +chr2 2565 2567 4 +chr2 2567 2570 3 +chr2 2570 2573 2 +chr2 2573 2592 1 +chr2 4044 4052 1 +chr2 4052 4071 2 +chr2 4071 4094 3 +chr2 4094 4102 2 +chr2 4102 4121 1 +chr2 4136 4157 1 +chr2 4157 4186 2 +chr2 4186 4200 1 +chr2 4200 4204 2 +chr2 4204 4207 3 +chr2 4207 4210 2 +chr2 4210 4250 3 +chr2 4250 4254 2 +chr2 4254 4260 1 +chr2 4263 4299 1 +chr2 4299 4313 2 +chr2 4313 4333 1 +chr2 4333 4346 2 +chr2 4346 4349 3 +chr2 4349 4356 2 +chr2 4356 4376 3 +chr2 4376 4383 4 +chr2 4383 4385 3 +chr2 4385 4396 4 +chr2 4396 4406 3 +chr2 4406 4407 2 +chr2 4407 4426 3 +chr2 4426 4435 2 +chr2 4435 4457 1 +chr2 4512 4538 1 +chr2 4538 4540 2 +chr2 4540 4562 3 +chr2 4562 4588 2 +chr2 4588 4590 1 +chr2 4592 4603 1 +chr2 4603 4618 2 +chr2 4618 4621 3 +chr2 4621 4625 4 +chr2 4625 4642 5 +chr2 4642 4645 4 +chr2 4645 4651 5 +chr2 4651 4653 6 +chr2 4653 4657 5 +chr2 4657 4668 6 +chr2 4668 4671 5 +chr2 4671 4675 4 +chr2 4675 4682 3 +chr2 4682 4695 4 +chr2 4695 4701 3 +chr2 4701 4707 2 +chr2 4707 4709 1 +chr2 4709 4732 2 +chr2 4732 4745 1 +chr2 4745 4759 2 +chr2 4759 4795 1 +chr2 5999 6000 2 +chr2 6000 6001 3 +chr2 6001 6002 6 +chr2 6002 6003 9 +chr2 6003 6004 11 +chr2 6004 6007 14 +chr2 6007 6008 17 +chr2 6008 6009 22 +chr2 6009 6010 23 +chr2 6010 6012 25 +chr2 6012 6014 26 +chr2 6014 6015 27 +chr2 6015 6016 29 +chr2 6016 6017 31 +chr2 6017 6018 33 +chr2 6018 6020 37 +chr2 6020 6021 40 +chr2 6021 6023 44 +chr2 6023 6024 48 +chr2 6024 6025 50 +chr2 6025 6026 53 +chr2 6026 6027 54 +chr2 6027 6028 55 +chr2 6028 6029 57 +chr2 6029 6031 59 +chr2 6031 6032 61 +chr2 6032 6034 65 +chr2 6034 6035 66 +chr2 6035 6036 68 +chr2 6036 6037 70 +chr2 6037 6038 72 +chr2 6038 6039 74 +chr2 6039 6040 75 +chr2 6040 6041 79 +chr2 6041 6042 81 +chr2 6042 6043 83 +chr2 6043 6044 84 +chr2 6044 6045 91 +chr2 6045 6046 92 +chr2 6046 6047 94 +chr2 6047 6048 95 +chr2 6048 6049 96 +chr2 6049 6050 98 +chr2 6050 6051 97 +chr2 6051 6052 94 +chr2 6052 6053 91 +chr2 6053 6054 89 +chr2 6054 6057 86 +chr2 6057 6058 83 +chr2 6058 6059 78 +chr2 6059 6060 77 +chr2 6060 6062 75 +chr2 6062 6064 74 +chr2 6064 6065 73 +chr2 6065 6066 71 +chr2 6066 6067 69 +chr2 6067 6068 67 +chr2 6068 6070 63 +chr2 6070 6071 60 +chr2 6071 6073 56 +chr2 6073 6074 52 +chr2 6074 6075 50 +chr2 6075 6076 47 +chr2 6076 6077 46 +chr2 6077 6078 45 +chr2 6078 6079 43 +chr2 6079 6081 41 +chr2 6081 6082 39 +chr2 6082 6084 35 +chr2 6084 6085 34 +chr2 6085 6086 32 +chr2 6086 6087 30 +chr2 6087 6088 28 +chr2 6088 6089 26 +chr2 6089 6090 25 +chr2 6090 6091 21 +chr2 6091 6092 19 +chr2 6092 6093 17 +chr2 6093 6094 16 +chr2 6094 6095 9 +chr2 6095 6096 8 +chr2 6096 6097 6 +chr2 6097 6098 5 +chr2 6098 6099 4 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/integration_test.rs b/tests/integration_test.rs index c7ea3acd..de915818 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -1026,3 +1026,158 @@ fn test_dup_check_parallel_uses_global_duplicate_state() { let _ = fs::remove_dir_all(root); } + +// =================================================================== +// Coverage tracks +// =================================================================== + +/// Per-base coverage against `bedtools genomecov -bg -split`. +/// +/// This is the output nf-core/rnaseq currently gets by running bedtools and +/// then converting its bedGraph to bigWig. Note the semantics differ from +/// every other depth engine in the crate: nothing is filtered, so duplicates, +/// secondary alignments and overlapping mates all contribute. +#[test] +fn coverage_intervals_match_bedtools() { + use rust_htslib::bam::{Read as BamRead, Reader}; + use rustqc::common::coverage::bedgraph::CoverageAccum; + + let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")); + let mut reader = Reader::from_path(root.join("tests/data/test.bam")).unwrap(); + let header = reader.header().to_owned(); + + // One accumulator per contig, as the pipeline drives them. + let mut per_chrom: std::collections::BTreeMap = header + .target_names() + .iter() + .enumerate() + .map(|(tid, name)| { + let chrom = String::from_utf8_lossy(name).to_string(); + let len = header.target_len(tid as u32).unwrap(); + (chrom, CoverageAccum::new(len, None)) + }) + .collect(); + + let mut record = rust_htslib::bam::Record::new(); + while let Some(result) = reader.read(&mut record) { + result.unwrap(); + if record.tid() < 0 { + continue; + } + let chrom = String::from_utf8_lossy(header.tid2name(record.tid() as u32)).to_string(); + if let Some(accum) = per_chrom.get_mut(&chrom) { + accum.process_read(&record); + } + } + + // bedtools emits contigs in header order, so follow that rather than the + // alphabetical order the map would give. + let mut got = Vec::new(); + for (tid, name) in header.target_names().iter().enumerate() { + let _ = tid; + let chrom = String::from_utf8_lossy(name).to_string(); + if let Some(accum) = per_chrom.remove(&chrom) { + got.extend(accum.into_intervals(&chrom, 1.0)); + } + } + + let want: Vec<(String, u32, u32, f32)> = + std::fs::read_to_string(root.join("tests/expected/coverage/test.bedgraph")) + .unwrap() + .lines() + .map(|line| { + let f: Vec<&str> = line.split('\t').collect(); + ( + f[0].to_string(), + f[1].parse().unwrap(), + f[2].parse().unwrap(), + f[3].parse().unwrap(), + ) + }) + .collect(); + + assert_eq!(got.len(), want.len(), "interval count"); + for (index, (ours, theirs)) in got.iter().zip(&want).enumerate() { + assert_eq!( + (ours.chrom.as_str(), ours.start, ours.end, ours.value), + (theirs.0.as_str(), theirs.1, theirs.2, theirs.3), + "interval {} differs", + index + 1 + ); + } +} + +/// The written bigWig read back against the bedtools reference. +/// +/// A gap between intervals is *undefined* in a bigWig rather than zero, and +/// reads back as `NaN`. That is the format's own semantics and matches what +/// `bedGraphToBigWig` produces from a bedGraph that omits its zero-depth +/// spans, so the check is that every covered base agrees and every uncovered +/// one is undefined. +#[cfg(feature = "bigwig")] +#[test] +fn bigwig_track_matches_the_bedtools_reference() { + use bigtools::BigWigRead; + use rust_htslib::bam::{Read as BamRead, Reader}; + use rustqc::common::coverage::bedgraph::CoverageTracks; + + let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")); + let mut reader = Reader::from_path(root.join("tests/data/test.bam")).unwrap(); + let header = reader.header().to_owned(); + let chrom_sizes: Vec<(String, u64)> = header + .target_names() + .iter() + .enumerate() + .map(|(tid, name)| { + ( + String::from_utf8_lossy(name).to_string(), + header.target_len(tid as u32).unwrap(), + ) + }) + .collect(); + + let mut tracks = CoverageTracks::new(vec![None]); + let mut record = rust_htslib::bam::Record::new(); + while let Some(result) = reader.read(&mut record) { + result.unwrap(); + if record.tid() < 0 { + continue; + } + let chrom = String::from_utf8_lossy(header.tid2name(record.tid() as u32)).to_string(); + tracks.process_read(&record, &chrom); + } + + let order: Vec = chrom_sizes.iter().map(|(n, _)| n.clone()).collect(); + let intervals = tracks.into_intervals(&order, 1.0).remove(0).1; + + let path = std::env::temp_dir().join("rustqc-coverage-parity.bigWig"); + assert!( + rustqc::common::coverage::bigwig::write_bigwig(&intervals, &chrom_sizes, &path).unwrap() + ); + + // Every interval bedtools reported must read back at the same depth. + let mut bw = BigWigRead::open_file(&path).unwrap(); + let reference = + std::fs::read_to_string(root.join("tests/expected/coverage/test.bedgraph")).unwrap(); + let mut checked = 0usize; + for line in reference.lines() { + let f: Vec<&str> = line.split('\t').collect(); + let (chrom, start, end, depth) = ( + f[0], + f[1].parse::().unwrap(), + f[2].parse::().unwrap(), + f[3].parse::().unwrap(), + ); + let values = bw.values(chrom, start, end).unwrap(); + for (offset, value) in values.iter().enumerate() { + assert_eq!( + *value, + depth, + "{chrom}:{} should be at depth {depth}", + start as usize + offset + ); + checked += 1; + } + } + assert!(checked > 0, "the reference must cover something"); +}