From ed5e6ed4f802b51b76698e3cb24bca53d5fec577 Mon Sep 17 00:00:00 2001 From: Tom Mokveld Date: Wed, 1 Nov 2023 15:58:46 -0400 Subject: [PATCH] Init --- Cargo.lock | 1352 +++++++++++++++++++++++++++++++ Cargo.toml | 35 + LICENSE-THIRDPARTY.json | 1343 ++++++++++++++++++++++++++++++ LICENSE.md | 15 + README.md | 36 + build.rs | 21 + docs/cli.md | 19 + docs/example.md | 104 +++ docs/figures/example_site_1.png | Bin 0 -> 83143 bytes docs/figures/example_site_2.png | Bin 0 -> 101816 bytes docs/output.md | 27 + src/aligner.rs | 1038 ++++++++++++++++++++++++ src/allele.rs | 409 ++++++++++ src/cli.rs | 206 +++++ src/commands.rs | 3 + src/commands/trio.rs | 146 ++++ src/denovo.rs | 506 ++++++++++++ src/handles.rs | 90 ++ src/lib.rs | 12 + src/locus.rs | 203 +++++ src/main.rs | 15 + src/read.rs | 198 +++++ src/snp.rs | 644 +++++++++++++++ src/stats.rs | 28 + src/util.rs | 17 + src/wfa2.rs | 2 + 26 files changed, 6469 insertions(+) create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE-THIRDPARTY.json create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 build.rs create mode 100644 docs/cli.md create mode 100644 docs/example.md create mode 100644 docs/figures/example_site_1.png create mode 100644 docs/figures/example_site_2.png create mode 100644 docs/output.md create mode 100644 src/aligner.rs create mode 100644 src/allele.rs create mode 100644 src/cli.rs create mode 100644 src/commands.rs create mode 100644 src/commands/trio.rs create mode 100644 src/denovo.rs create mode 100644 src/handles.rs create mode 100644 src/lib.rs create mode 100644 src/locus.rs create mode 100644 src/main.rs create mode 100644 src/read.rs create mode 100644 src/snp.rs create mode 100644 src/stats.rs create mode 100644 src/util.rs create mode 100644 src/wfa2.rs diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..3adbe05 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1352 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" + +[[package]] +name = "anstyle-parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bindgen" +version = "0.69.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "042e2e131c066e496ea7880ef6cfeec415a9adc79fc882a65979394f8840bf7c" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", + "which", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" + +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets", +] + +[[package]] +name = "clang-sys" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac495e00dcec98c83465d5ad66c5c4fabd652fd6686e7c6269b117e729a6f17b" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c77ed9a32a62e6ca27175d00d29d05ca32e396ea1eb5fb01d8256b669cec7663" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" + +[[package]] +name = "cmake" +version = "0.1.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" +dependencies = [ + "cc", +] + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "csv" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" +dependencies = [ + "memchr", +] + +[[package]] +name = "deranged" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "env_logger" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "flate2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "hashbrown" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" + +[[package]] +name = "home" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "iana-time-zone" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi", + "rustix", + "windows-sys", +] + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "js-sys" +version = "0.3.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "lexical-core" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cde5de06e8d4c2faabc400238f9ae1c74d5412d03a7bd067645ccbc47070e46" +dependencies = [ + "lexical-parse-float", + "lexical-parse-integer", + "lexical-util", + "lexical-write-float", + "lexical-write-integer", +] + +[[package]] +name = "lexical-parse-float" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683b3a5ebd0130b8fb52ba0bdc718cc56815b6a097e28ae5a6997d0ad17dc05f" +dependencies = [ + "lexical-parse-integer", + "lexical-util", + "static_assertions", +] + +[[package]] +name = "lexical-parse-integer" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d0994485ed0c312f6d965766754ea177d07f9c00c9b82a5ee62ed5b47945ee9" +dependencies = [ + "lexical-util", + "static_assertions", +] + +[[package]] +name = "lexical-util" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5255b9ff16ff898710eb9eb63cb39248ea8a5bb036bea8085b1a767ff6c4e3fc" +dependencies = [ + "static_assertions", +] + +[[package]] +name = "lexical-write-float" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accabaa1c4581f05a3923d1b4cfd124c329352288b7b9da09e766b0668116862" +dependencies = [ + "lexical-util", + "lexical-write-integer", + "static_assertions", +] + +[[package]] +name = "lexical-write-integer" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b6f3d1f4422866b68192d62f77bc5c700bee84f3069f2469d7bc8c77852446" +dependencies = [ + "lexical-util", + "static_assertions", +] + +[[package]] +name = "libc" +version = "0.2.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "matrixmultiply" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7574c1cf36da4798ab73da5b215bbf444f50718207754cb522201d78d1cd0ff2" +dependencies = [ + "autocfg", + "rawpointer", +] + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "ndarray" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb12d4e967ec485a5f71c6311fe28158e9d6f4bc4a447b474184d0f91a8fa32" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "rawpointer", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "noodles" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ba860dd272b95bc6758e9cf1ec9d13d159c928726b020432a88bd897ca75bbb" +dependencies = [ + "noodles-bam", + "noodles-bed", + "noodles-bgzf", + "noodles-core", + "noodles-fasta", + "noodles-sam", + "noodles-vcf", +] + +[[package]] +name = "noodles-bam" +version = "0.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27efd2e9691c3cf6c894559089562f2dceebfd1f09fe1bc59e85235e16b77e1e" +dependencies = [ + "bit-vec", + "byteorder", + "bytes", + "indexmap", + "noodles-bgzf", + "noodles-core", + "noodles-csi", + "noodles-sam", +] + +[[package]] +name = "noodles-bed" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deba180b7b94c524307da91d5bc983e0ccb9a08c4e24384cc8f93e0210af254a" +dependencies = [ + "noodles-core", +] + +[[package]] +name = "noodles-bgzf" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d578e5a173cbfac77295db4188c959966ce24a3364e009d363170d1ed44066a" +dependencies = [ + "byteorder", + "bytes", + "crossbeam-channel", + "flate2", +] + +[[package]] +name = "noodles-core" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fbe3192fe33acacabaedd387657f39b0fc606f1996d546db0dfe14703b843a" + +[[package]] +name = "noodles-csi" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0531175d5473e6057c1724c1242b19bfc42dba644fe275b4df89c5b8d31a782" +dependencies = [ + "bit-vec", + "byteorder", + "indexmap", + "noodles-bgzf", + "noodles-core", +] + +[[package]] +name = "noodles-fasta" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "310dcfb61e8e2cafb65d9da4b329a98a390f2b570c17599a7f4639328cfb3e2c" +dependencies = [ + "bytes", + "memchr", + "noodles-bgzf", + "noodles-core", +] + +[[package]] +name = "noodles-sam" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "045d64736a3772d6edf5e05c167398808a45ce2c802627c7ba542820f3a48b64" +dependencies = [ + "bitflags", + "indexmap", + "lexical-core", + "memchr", + "noodles-bgzf", + "noodles-core", + "noodles-csi", +] + +[[package]] +name = "noodles-tabix" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "415e319f97784c110a85756a8747bf26e9a18bf321b113d00984ca1af7a6fef9" +dependencies = [ + "bit-vec", + "byteorder", + "indexmap", + "noodles-bgzf", + "noodles-core", + "noodles-csi", +] + +[[package]] +name = "noodles-vcf" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d34757c2d465c34dab54582b475ad53fd3ffb5b12943b7166876235db8a297" +dependencies = [ + "indexmap", + "memchr", + "noodles-bgzf", + "noodles-core", + "noodles-csi", + "noodles-tabix", + "percent-encoding", +] + +[[package]] +name = "num-complex" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "prettyplease" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[package]] +name = "rayon" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustix" +version = "0.38.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "shlex" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "2.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "time" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" +dependencies = [ + "deranged", + "itoa", + "libc", + "num_threads", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" +dependencies = [ + "time-core", +] + +[[package]] +name = "trgt-denovo" +version = "0.1.0" +dependencies = [ + "anyhow", + "chrono", + "clap", + "csv", + "env_logger", + "itertools", + "log", + "ndarray", + "noodles", + "once_cell", + "rand", + "rayon", + "serde", + "threadpool", + "vergen", + "wfa2-sys", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "vergen" +version = "8.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85e7dc29b3c54a2ea67ef4f953d5ec0c4085035c0ae2d325be1c0d2144bd9f16" +dependencies = [ + "anyhow", + "rustversion", + "time", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" + +[[package]] +name = "wfa2-sys" +version = "0.1.0" +source = "git+https://github.com/ctsa/rust-wfa2.git?rev=6e1d1dd#6e1d1dd70d1f76ab35206ce72d32f08075ae02e6" +dependencies = [ + "bindgen", + "cmake", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..8353c58 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "trgt-denovo" +version = "0.1.0" +authors = ["Tom Mokveld", "Egor Dolzhenko"] +description = """ +trgt-denovo is a CLI tool for targeted de novo tandem repeat calling from long-read HiFI sequencing data. +""" +edition = "2021" +build = "build.rs" + +[build-dependencies] +vergen = { version = "8.0.0", features = ["git", "gitcl"] } + +[[bin]] +bench = false +path = "src/main.rs" +name = "trgt-denovo" + +[dependencies] +serde = { version = "1.0.150", features = ["derive"] } +wfa2-sys = { version = "*", git = "https://github.com/ctsa/rust-wfa2.git", rev = "6e1d1dd" } +clap = { version = "4.0.18", features = ["suggestions", "derive"] } +itertools = "*" +log = "*" +env_logger = "*" +rand = "0.8.5" +threadpool = "*" +chrono = "*" +csv = "*" +ndarray = "*" +# flate2 = { version = "1.0.20", default-features = false, features = ["zlib-ng"] } # Options: zlib-ng, zlib, zlib-ng-compat +noodles = { version = "*", features = ["vcf", "fasta", "core", "bam", "sam", "bgzf", "bed"] } +once_cell = "*" +rayon = "*" +anyhow = "*" \ No newline at end of file diff --git a/LICENSE-THIRDPARTY.json b/LICENSE-THIRDPARTY.json new file mode 100644 index 0000000..a134b50 --- /dev/null +++ b/LICENSE-THIRDPARTY.json @@ -0,0 +1,1343 @@ +[ + { + "name": "adler", + "version": "1.0.2", + "authors": "Jonas Schievink ", + "repository": "https://github.com/jonas-schievink/adler.git", + "license": "0BSD OR Apache-2.0 OR MIT", + "license_file": null, + "description": "A simple clean-room implementation of the Adler-32 checksum" + }, + { + "name": "aho-corasick", + "version": "1.1.2", + "authors": "Andrew Gallant ", + "repository": "https://github.com/BurntSushi/aho-corasick", + "license": "MIT OR Unlicense", + "license_file": null, + "description": "Fast multiple substring searching." + }, + { + "name": "android-tzdata", + "version": "0.1.1", + "authors": "RumovZ", + "repository": "https://github.com/RumovZ/android-tzdata", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Parser for the Android-specific tzdata file" + }, + { + "name": "android_system_properties", + "version": "0.1.5", + "authors": "Nicolas Silva ", + "repository": "https://github.com/nical/android_system_properties", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Minimal Android system properties wrapper" + }, + { + "name": "anstream", + "version": "0.6.4", + "authors": null, + "repository": "https://github.com/rust-cli/anstyle.git", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "A simple cross platform library for writing colored text to a terminal." + }, + { + "name": "anstyle", + "version": "1.0.4", + "authors": null, + "repository": "https://github.com/rust-cli/anstyle.git", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "ANSI text styling" + }, + { + "name": "anstyle-parse", + "version": "0.2.2", + "authors": null, + "repository": "https://github.com/rust-cli/anstyle.git", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Parse ANSI Style Escapes" + }, + { + "name": "anstyle-query", + "version": "1.0.0", + "authors": null, + "repository": "https://github.com/rust-cli/anstyle", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Look up colored console capabilities" + }, + { + "name": "anstyle-wincon", + "version": "3.0.1", + "authors": null, + "repository": "https://github.com/rust-cli/anstyle.git", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Styling legacy Windows terminals" + }, + { + "name": "anyhow", + "version": "1.0.75", + "authors": "David Tolnay ", + "repository": "https://github.com/dtolnay/anyhow", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Flexible concrete Error type built on std::error::Error" + }, + { + "name": "autocfg", + "version": "1.1.0", + "authors": "Josh Stone ", + "repository": "https://github.com/cuviper/autocfg", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Automatic cfg for Rust compiler features" + }, + { + "name": "bindgen", + "version": "0.69.0", + "authors": "Jyun-Yan You |Emilio Cobos Álvarez |Nick Fitzgerald |The Servo project developers", + "repository": "https://github.com/rust-lang/rust-bindgen", + "license": "BSD-3-Clause", + "license_file": null, + "description": "Automatically generates Rust FFI bindings to C and C++ libraries." + }, + { + "name": "bit-vec", + "version": "0.6.3", + "authors": "Alexis Beingessner ", + "repository": "https://github.com/contain-rs/bit-vec", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "A vector of bits" + }, + { + "name": "bitflags", + "version": "2.4.1", + "authors": "The Rust Project Developers", + "repository": "https://github.com/bitflags/bitflags", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "A macro to generate structures which behave like bitflags." + }, + { + "name": "bumpalo", + "version": "3.14.0", + "authors": "Nick Fitzgerald ", + "repository": "https://github.com/fitzgen/bumpalo", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "A fast bump allocation arena for Rust." + }, + { + "name": "byteorder", + "version": "1.5.0", + "authors": "Andrew Gallant ", + "repository": "https://github.com/BurntSushi/byteorder", + "license": "MIT OR Unlicense", + "license_file": null, + "description": "Library for reading/writing numbers in big-endian and little-endian." + }, + { + "name": "bytes", + "version": "1.5.0", + "authors": "Carl Lerche |Sean McArthur ", + "repository": "https://github.com/tokio-rs/bytes", + "license": "MIT", + "license_file": null, + "description": "Types and traits for working with bytes" + }, + { + "name": "cc", + "version": "1.0.83", + "authors": "Alex Crichton ", + "repository": "https://github.com/rust-lang/cc-rs", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "A build-time dependency for Cargo build scripts to assist in invoking the native C compiler to compile native C code into a static archive to be linked into Rust code." + }, + { + "name": "cexpr", + "version": "0.6.0", + "authors": "Jethro Beekman ", + "repository": "https://github.com/jethrogb/rust-cexpr", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "A C expression parser and evaluator" + }, + { + "name": "cfg-if", + "version": "1.0.0", + "authors": "Alex Crichton ", + "repository": "https://github.com/alexcrichton/cfg-if", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "A macro to ergonomically define an item depending on a large number of #[cfg] parameters. Structured like an if-else chain, the first matching branch is the item that gets emitted." + }, + { + "name": "chrono", + "version": "0.4.31", + "authors": null, + "repository": "https://github.com/chronotope/chrono", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Date and time library for Rust" + }, + { + "name": "clang-sys", + "version": "1.6.1", + "authors": "Kyle Mayes ", + "repository": "https://github.com/KyleMayes/clang-sys", + "license": "Apache-2.0", + "license_file": null, + "description": "Rust bindings for libclang." + }, + { + "name": "clap", + "version": "4.4.7", + "authors": null, + "repository": "https://github.com/clap-rs/clap", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "A simple to use, efficient, and full-featured Command Line Argument Parser" + }, + { + "name": "clap_builder", + "version": "4.4.7", + "authors": null, + "repository": "https://github.com/clap-rs/clap", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "A simple to use, efficient, and full-featured Command Line Argument Parser" + }, + { + "name": "clap_derive", + "version": "4.4.7", + "authors": null, + "repository": "https://github.com/clap-rs/clap/tree/master/clap_derive", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Parse command line argument by defining a struct, derive crate." + }, + { + "name": "clap_lex", + "version": "0.6.0", + "authors": null, + "repository": "https://github.com/clap-rs/clap/tree/master/clap_lex", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Minimal, flexible command line parser" + }, + { + "name": "cmake", + "version": "0.1.50", + "authors": "Alex Crichton ", + "repository": "https://github.com/rust-lang/cmake-rs", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "A build dependency for running `cmake` to build a native library" + }, + { + "name": "colorchoice", + "version": "1.0.0", + "authors": null, + "repository": "https://github.com/rust-cli/anstyle", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Global override of color control" + }, + { + "name": "core-foundation-sys", + "version": "0.8.4", + "authors": "The Servo Project Developers", + "repository": "https://github.com/servo/core-foundation-rs", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Bindings to Core Foundation for macOS" + }, + { + "name": "crc32fast", + "version": "1.3.2", + "authors": "Sam Rijs |Alex Crichton ", + "repository": "https://github.com/srijs/rust-crc32fast", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Fast, SIMD-accelerated CRC32 (IEEE) checksum computation" + }, + { + "name": "crossbeam-channel", + "version": "0.5.8", + "authors": null, + "repository": "https://github.com/crossbeam-rs/crossbeam", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Multi-producer multi-consumer channels for message passing" + }, + { + "name": "crossbeam-deque", + "version": "0.8.3", + "authors": null, + "repository": "https://github.com/crossbeam-rs/crossbeam", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Concurrent work-stealing deque" + }, + { + "name": "crossbeam-epoch", + "version": "0.9.15", + "authors": null, + "repository": "https://github.com/crossbeam-rs/crossbeam", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Epoch-based garbage collection" + }, + { + "name": "crossbeam-utils", + "version": "0.8.16", + "authors": null, + "repository": "https://github.com/crossbeam-rs/crossbeam", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Utilities for concurrent programming" + }, + { + "name": "csv", + "version": "1.3.0", + "authors": "Andrew Gallant ", + "repository": "https://github.com/BurntSushi/rust-csv", + "license": "MIT OR Unlicense", + "license_file": null, + "description": "Fast CSV parsing with support for serde." + }, + { + "name": "csv-core", + "version": "0.1.11", + "authors": "Andrew Gallant ", + "repository": "https://github.com/BurntSushi/rust-csv", + "license": "MIT OR Unlicense", + "license_file": null, + "description": "Bare bones CSV parsing with no_std support." + }, + { + "name": "deranged", + "version": "0.3.9", + "authors": "Jacob Pratt ", + "repository": "https://github.com/jhpratt/deranged", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Ranged integers" + }, + { + "name": "either", + "version": "1.9.0", + "authors": "bluss", + "repository": "https://github.com/bluss/either", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "The enum `Either` with variants `Left` and `Right` is a general purpose sum type with two cases." + }, + { + "name": "env_logger", + "version": "0.10.0", + "authors": null, + "repository": "https://github.com/rust-cli/env_logger/", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "A logging implementation for `log` which is configured via an environment variable." + }, + { + "name": "equivalent", + "version": "1.0.1", + "authors": null, + "repository": "https://github.com/cuviper/equivalent", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Traits for key comparison in maps." + }, + { + "name": "errno", + "version": "0.3.5", + "authors": "Chris Wong ", + "repository": "https://github.com/lambda-fairy/rust-errno", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Cross-platform interface to the `errno` variable." + }, + { + "name": "flate2", + "version": "1.0.28", + "authors": "Alex Crichton |Josh Triplett ", + "repository": "https://github.com/rust-lang/flate2-rs", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "DEFLATE compression and decompression exposed as Read/BufRead/Write streams. Supports miniz_oxide and multiple zlib implementations. Supports zlib, gzip, and raw deflate streams." + }, + { + "name": "getrandom", + "version": "0.2.10", + "authors": "The Rand Project Developers", + "repository": "https://github.com/rust-random/getrandom", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "A small cross-platform library for retrieving random data from system source" + }, + { + "name": "glob", + "version": "0.3.1", + "authors": "The Rust Project Developers", + "repository": "https://github.com/rust-lang/glob", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Support for matching file paths against Unix shell style patterns." + }, + { + "name": "hashbrown", + "version": "0.14.2", + "authors": "Amanieu d'Antras ", + "repository": "https://github.com/rust-lang/hashbrown", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "A Rust port of Google's SwissTable hash map" + }, + { + "name": "heck", + "version": "0.4.1", + "authors": "Without Boats ", + "repository": "https://github.com/withoutboats/heck", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "heck is a case conversion library." + }, + { + "name": "hermit-abi", + "version": "0.3.3", + "authors": "Stefan Lankes", + "repository": "https://github.com/hermitcore/hermit-rs", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Hermit system calls definitions." + }, + { + "name": "home", + "version": "0.5.5", + "authors": "Brian Anderson ", + "repository": "https://github.com/rust-lang/cargo", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Shared definitions of home directories." + }, + { + "name": "humantime", + "version": "2.1.0", + "authors": "Paul Colomiets ", + "repository": "https://github.com/tailhook/humantime", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "A parser and formatter for std::time::{Duration, SystemTime}" + }, + { + "name": "iana-time-zone", + "version": "0.1.58", + "authors": "Andrew Straw |René Kijewski |Ryan Lopopolo ", + "repository": "https://github.com/strawlab/iana-time-zone", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "get the IANA time zone for the current system" + }, + { + "name": "iana-time-zone-haiku", + "version": "0.1.2", + "authors": "René Kijewski ", + "repository": "https://github.com/strawlab/iana-time-zone", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "iana-time-zone support crate for Haiku OS" + }, + { + "name": "indexmap", + "version": "2.1.0", + "authors": null, + "repository": "https://github.com/bluss/indexmap", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "A hash table with consistent order and fast iteration." + }, + { + "name": "is-terminal", + "version": "0.4.9", + "authors": "softprops |Dan Gohman ", + "repository": "https://github.com/sunfishcode/is-terminal", + "license": "MIT", + "license_file": null, + "description": "Test whether a given stream is a terminal" + }, + { + "name": "itertools", + "version": "0.11.0", + "authors": "bluss", + "repository": "https://github.com/rust-itertools/itertools", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Extra iterator adaptors, iterator methods, free functions, and macros." + }, + { + "name": "itoa", + "version": "1.0.9", + "authors": "David Tolnay ", + "repository": "https://github.com/dtolnay/itoa", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Fast integer primitive to string conversion" + }, + { + "name": "js-sys", + "version": "0.3.65", + "authors": "The wasm-bindgen Developers", + "repository": "https://github.com/rustwasm/wasm-bindgen/tree/master/crates/js-sys", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Bindings for all JS global objects and functions in all JS environments like Node.js and browsers, built on `#[wasm_bindgen]` using the `wasm-bindgen` crate." + }, + { + "name": "lazy_static", + "version": "1.4.0", + "authors": "Marvin Löbel ", + "repository": "https://github.com/rust-lang-nursery/lazy-static.rs", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "A macro for declaring lazily evaluated statics in Rust." + }, + { + "name": "lazycell", + "version": "1.3.0", + "authors": "Alex Crichton |Nikita Pekin ", + "repository": "https://github.com/indiv0/lazycell", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "A library providing a lazily filled Cell struct" + }, + { + "name": "lexical-core", + "version": "0.8.5", + "authors": "Alex Huszagh ", + "repository": "https://github.com/Alexhuszagh/rust-lexical", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Lexical, to- and from-string conversion routines." + }, + { + "name": "lexical-parse-float", + "version": "0.8.5", + "authors": "Alex Huszagh ", + "repository": "https://github.com/Alexhuszagh/rust-lexical", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Efficient parsing of floats from strings." + }, + { + "name": "lexical-parse-integer", + "version": "0.8.6", + "authors": "Alex Huszagh ", + "repository": "https://github.com/Alexhuszagh/rust-lexical", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Efficient parsing of integers from strings." + }, + { + "name": "lexical-util", + "version": "0.8.5", + "authors": "Alex Huszagh ", + "repository": "https://github.com/Alexhuszagh/rust-lexical", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Shared utilities for lexical creates." + }, + { + "name": "lexical-write-float", + "version": "0.8.5", + "authors": "Alex Huszagh ", + "repository": "https://github.com/Alexhuszagh/rust-lexical", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Efficient formatting of floats to strings." + }, + { + "name": "lexical-write-integer", + "version": "0.8.5", + "authors": "Alex Huszagh ", + "repository": "https://github.com/Alexhuszagh/rust-lexical", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Efficient formatting of integers to strings." + }, + { + "name": "libc", + "version": "0.2.149", + "authors": "The Rust Project Developers", + "repository": "https://github.com/rust-lang/libc", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Raw FFI bindings to platform libraries like libc." + }, + { + "name": "libloading", + "version": "0.7.4", + "authors": "Simonas Kazlauskas ", + "repository": "https://github.com/nagisa/rust_libloading/", + "license": "ISC", + "license_file": null, + "description": "Bindings around the platform's dynamic library loading primitives with greatly improved memory safety." + }, + { + "name": "linux-raw-sys", + "version": "0.4.10", + "authors": "Dan Gohman ", + "repository": "https://github.com/sunfishcode/linux-raw-sys", + "license": "Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT", + "license_file": null, + "description": "Generated bindings for Linux's userspace API" + }, + { + "name": "log", + "version": "0.4.20", + "authors": "The Rust Project Developers", + "repository": "https://github.com/rust-lang/log", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "A lightweight logging facade for Rust" + }, + { + "name": "matrixmultiply", + "version": "0.3.8", + "authors": "bluss|R. Janis Goldschmidt", + "repository": "https://github.com/bluss/matrixmultiply/", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "General matrix multiplication for f32 and f64 matrices. Operates on matrices with general layout (they can use arbitrary row and column stride). Detects and uses AVX or SSE2 on x86 platforms transparently for higher performance. Uses a microkernel strategy, so that the implementation is easy to parallelize and optimize. Supports multithreading." + }, + { + "name": "memchr", + "version": "2.6.4", + "authors": "Andrew Gallant |bluss", + "repository": "https://github.com/BurntSushi/memchr", + "license": "MIT OR Unlicense", + "license_file": null, + "description": "Provides extremely fast (uses SIMD on x86_64, aarch64 and wasm32) routines for 1, 2 or 3 byte search and single substring search." + }, + { + "name": "memoffset", + "version": "0.9.0", + "authors": "Gilad Naaman ", + "repository": "https://github.com/Gilnaa/memoffset", + "license": "MIT", + "license_file": null, + "description": "offset_of functionality for Rust structs." + }, + { + "name": "minimal-lexical", + "version": "0.2.1", + "authors": "Alex Huszagh ", + "repository": "https://github.com/Alexhuszagh/minimal-lexical", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Fast float parsing conversion routines." + }, + { + "name": "miniz_oxide", + "version": "0.7.1", + "authors": "Frommi |oyvindln ", + "repository": "https://github.com/Frommi/miniz_oxide/tree/master/miniz_oxide", + "license": "Apache-2.0 OR MIT OR Zlib", + "license_file": null, + "description": "DEFLATE compression and decompression library rewritten in Rust based on miniz" + }, + { + "name": "ndarray", + "version": "0.15.6", + "authors": "Ulrik Sverdrup \"bluss\"|Jim Turner", + "repository": "https://github.com/rust-ndarray/ndarray", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "An n-dimensional array for general elements and for numerics. Lightweight array views and slicing; views support chunking and splitting." + }, + { + "name": "nom", + "version": "7.1.3", + "authors": "contact@geoffroycouprie.com", + "repository": "https://github.com/Geal/nom", + "license": "MIT", + "license_file": null, + "description": "A byte-oriented, zero-copy, parser combinators library" + }, + { + "name": "noodles", + "version": "0.56.0", + "authors": "Michael Macias ", + "repository": "https://github.com/zaeleus/noodles", + "license": "MIT", + "license_file": null, + "description": "Bioinformatics I/O libraries" + }, + { + "name": "noodles-bam", + "version": "0.49.0", + "authors": "Michael Macias ", + "repository": "https://github.com/zaeleus/noodles", + "license": "MIT", + "license_file": null, + "description": "Binary Alignment/Map (BAM) format reader and writer" + }, + { + "name": "noodles-bed", + "version": "0.10.0", + "authors": "Michael Macias ", + "repository": "https://github.com/zaeleus/noodles", + "license": "MIT", + "license_file": null, + "description": "BED (Browser Extensible Data) reader" + }, + { + "name": "noodles-bgzf", + "version": "0.25.0", + "authors": "Michael Macias ", + "repository": "https://github.com/zaeleus/noodles", + "license": "MIT", + "license_file": null, + "description": "Blocked gzip format (BGZF) reader and writer" + }, + { + "name": "noodles-core", + "version": "0.12.0", + "authors": "Michael Macias ", + "repository": "https://github.com/zaeleus/noodles", + "license": "MIT", + "license_file": null, + "description": "Shared utilities when working with noodles" + }, + { + "name": "noodles-csi", + "version": "0.26.0", + "authors": "Michael Macias ", + "repository": "https://github.com/zaeleus/noodles", + "license": "MIT", + "license_file": null, + "description": "Coordinate-sorted index (CSI) format reader and writer" + }, + { + "name": "noodles-fasta", + "version": "0.30.0", + "authors": "Michael Macias ", + "repository": "https://github.com/zaeleus/noodles", + "license": "MIT", + "license_file": null, + "description": "FASTA format reader and writer" + }, + { + "name": "noodles-sam", + "version": "0.46.0", + "authors": "Michael Macias ", + "repository": "https://github.com/zaeleus/noodles", + "license": "MIT", + "license_file": null, + "description": "Sequence Alignment/Map (SAM) format reader and writer" + }, + { + "name": "noodles-tabix", + "version": "0.32.0", + "authors": "Michael Macias ", + "repository": "https://github.com/zaeleus/noodles", + "license": "MIT", + "license_file": null, + "description": "Tabix (TBI) format reader and writer" + }, + { + "name": "noodles-vcf", + "version": "0.44.0", + "authors": "Michael Macias ", + "repository": "https://github.com/zaeleus/noodles", + "license": "MIT", + "license_file": null, + "description": "Variant Call Format (VCF) reader and writer" + }, + { + "name": "num-complex", + "version": "0.4.4", + "authors": "The Rust Project Developers", + "repository": "https://github.com/rust-num/num-complex", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Complex numbers implementation for Rust" + }, + { + "name": "num-integer", + "version": "0.1.45", + "authors": "The Rust Project Developers", + "repository": "https://github.com/rust-num/num-integer", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Integer traits and functions" + }, + { + "name": "num-traits", + "version": "0.2.17", + "authors": "The Rust Project Developers", + "repository": "https://github.com/rust-num/num-traits", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Numeric traits for generic mathematics" + }, + { + "name": "num_cpus", + "version": "1.16.0", + "authors": "Sean McArthur ", + "repository": "https://github.com/seanmonstar/num_cpus", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Get the number of CPUs on a machine." + }, + { + "name": "num_threads", + "version": "0.1.6", + "authors": "Jacob Pratt ", + "repository": "https://github.com/jhpratt/num_threads", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "A minimal library that determines the number of running threads for the current process." + }, + { + "name": "once_cell", + "version": "1.18.0", + "authors": "Aleksey Kladov ", + "repository": "https://github.com/matklad/once_cell", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Single assignment cells and lazy values." + }, + { + "name": "peeking_take_while", + "version": "0.1.2", + "authors": "Nick Fitzgerald ", + "repository": "https://github.com/fitzgen/peeking_take_while", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Like `Iterator::take_while`, but calls the predicate on a peeked value. This allows you to use `Iterator::by_ref` and `Iterator::take_while` together, and still get the first value for which the `take_while` predicate returned false after dropping the `by_ref`." + }, + { + "name": "percent-encoding", + "version": "2.3.0", + "authors": "The rust-url developers", + "repository": "https://github.com/servo/rust-url/", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Percent encoding and decoding" + }, + { + "name": "powerfmt", + "version": "0.2.0", + "authors": "Jacob Pratt ", + "repository": "https://github.com/jhpratt/powerfmt", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "`powerfmt` is a library that provides utilities for formatting values. This crate makes it significantly easier to support filling to a minimum width with alignment, avoid heap allocation, and avoid repetitive calculations." + }, + { + "name": "ppv-lite86", + "version": "0.2.17", + "authors": "The CryptoCorrosion Contributors", + "repository": "https://github.com/cryptocorrosion/cryptocorrosion", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Implementation of the crypto-simd API for x86" + }, + { + "name": "prettyplease", + "version": "0.2.15", + "authors": "David Tolnay ", + "repository": "https://github.com/dtolnay/prettyplease", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "A minimal `syn` syntax tree pretty-printer" + }, + { + "name": "proc-macro2", + "version": "1.0.69", + "authors": "David Tolnay |Alex Crichton ", + "repository": "https://github.com/dtolnay/proc-macro2", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "A substitute implementation of the compiler's `proc_macro` API to decouple token-based libraries from the procedural macro use case." + }, + { + "name": "quote", + "version": "1.0.33", + "authors": "David Tolnay ", + "repository": "https://github.com/dtolnay/quote", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Quasi-quoting macro quote!(...)" + }, + { + "name": "rand", + "version": "0.8.5", + "authors": "The Rand Project Developers|The Rust Project Developers", + "repository": "https://github.com/rust-random/rand", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Random number generators and other randomness functionality." + }, + { + "name": "rand_chacha", + "version": "0.3.1", + "authors": "The Rand Project Developers|The Rust Project Developers|The CryptoCorrosion Contributors", + "repository": "https://github.com/rust-random/rand", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "ChaCha random number generator" + }, + { + "name": "rand_core", + "version": "0.6.4", + "authors": "The Rand Project Developers|The Rust Project Developers", + "repository": "https://github.com/rust-random/rand", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Core random number generator traits and tools for implementation." + }, + { + "name": "rawpointer", + "version": "0.2.1", + "authors": "bluss", + "repository": "https://github.com/bluss/rawpointer/", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Extra methods for raw pointers and `NonNull`. For example `.post_inc()` and `.pre_dec()` (c.f. `ptr++` and `--ptr`), `offset` and `add` for `NonNull`, and the function `ptrdistance`." + }, + { + "name": "rayon", + "version": "1.8.0", + "authors": "Niko Matsakis |Josh Stone ", + "repository": "https://github.com/rayon-rs/rayon", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Simple work-stealing parallelism for Rust" + }, + { + "name": "rayon-core", + "version": "1.12.0", + "authors": "Niko Matsakis |Josh Stone ", + "repository": "https://github.com/rayon-rs/rayon", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Core APIs for Rayon" + }, + { + "name": "regex", + "version": "1.10.2", + "authors": "The Rust Project Developers|Andrew Gallant ", + "repository": "https://github.com/rust-lang/regex", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "An implementation of regular expressions for Rust. This implementation uses finite automata and guarantees linear time matching on all inputs." + }, + { + "name": "regex-automata", + "version": "0.4.3", + "authors": "The Rust Project Developers|Andrew Gallant ", + "repository": "https://github.com/rust-lang/regex/tree/master/regex-automata", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Automata construction and matching using regular expressions." + }, + { + "name": "regex-syntax", + "version": "0.8.2", + "authors": "The Rust Project Developers|Andrew Gallant ", + "repository": "https://github.com/rust-lang/regex/tree/master/regex-syntax", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "A regular expression parser." + }, + { + "name": "rustc-hash", + "version": "1.1.0", + "authors": "The Rust Project Developers", + "repository": "https://github.com/rust-lang-nursery/rustc-hash", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "speed, non-cryptographic hash used in rustc" + }, + { + "name": "rustix", + "version": "0.38.21", + "authors": "Dan Gohman |Jakub Konka ", + "repository": "https://github.com/bytecodealliance/rustix", + "license": "Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT", + "license_file": null, + "description": "Safe Rust bindings to POSIX/Unix/Linux/Winsock2-like syscalls" + }, + { + "name": "rustversion", + "version": "1.0.14", + "authors": "David Tolnay ", + "repository": "https://github.com/dtolnay/rustversion", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Conditional compilation according to rustc compiler version" + }, + { + "name": "ryu", + "version": "1.0.15", + "authors": "David Tolnay ", + "repository": "https://github.com/dtolnay/ryu", + "license": "Apache-2.0 OR BSL-1.0", + "license_file": null, + "description": "Fast floating point to string conversion" + }, + { + "name": "scopeguard", + "version": "1.2.0", + "authors": "bluss", + "repository": "https://github.com/bluss/scopeguard", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "A RAII scope guard that will run a given closure when it goes out of scope, even if the code between panics (assuming unwinding panic). Defines the macros `defer!`, `defer_on_unwind!`, `defer_on_success!` as shorthands for guards with one of the implemented strategies." + }, + { + "name": "serde", + "version": "1.0.190", + "authors": "Erick Tryzelaar |David Tolnay ", + "repository": "https://github.com/serde-rs/serde", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "A generic serialization/deserialization framework" + }, + { + "name": "serde_derive", + "version": "1.0.190", + "authors": "Erick Tryzelaar |David Tolnay ", + "repository": "https://github.com/serde-rs/serde", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Macros 1.1 implementation of #[derive(Serialize, Deserialize)]" + }, + { + "name": "shlex", + "version": "1.2.0", + "authors": "comex |Fenhl ", + "repository": "https://github.com/comex/rust-shlex", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Split a string into shell words, like Python's shlex." + }, + { + "name": "static_assertions", + "version": "1.1.0", + "authors": "Nikolai Vazquez", + "repository": "https://github.com/nvzqz/static-assertions-rs", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Compile-time assertions to ensure that invariants are met." + }, + { + "name": "strsim", + "version": "0.10.0", + "authors": "Danny Guo ", + "repository": "https://github.com/dguo/strsim-rs", + "license": "MIT", + "license_file": null, + "description": "Implementations of string similarity metrics. Includes Hamming, Levenshtein, OSA, Damerau-Levenshtein, Jaro, Jaro-Winkler, and Sørensen-Dice." + }, + { + "name": "syn", + "version": "2.0.38", + "authors": "David Tolnay ", + "repository": "https://github.com/dtolnay/syn", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Parser for Rust source code" + }, + { + "name": "termcolor", + "version": "1.3.0", + "authors": "Andrew Gallant ", + "repository": "https://github.com/BurntSushi/termcolor", + "license": "MIT OR Unlicense", + "license_file": null, + "description": "A simple cross platform library for writing colored text to a terminal." + }, + { + "name": "threadpool", + "version": "1.8.1", + "authors": "The Rust Project Developers|Corey Farwell |Stefan Schindler ", + "repository": "https://github.com/rust-threadpool/rust-threadpool", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "A thread pool for running a number of jobs on a fixed set of worker threads." + }, + { + "name": "time", + "version": "0.3.30", + "authors": "Jacob Pratt |Time contributors", + "repository": "https://github.com/time-rs/time", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Date and time library. Fully interoperable with the standard library. Mostly compatible with #![no_std]." + }, + { + "name": "time-core", + "version": "0.1.2", + "authors": "Jacob Pratt |Time contributors", + "repository": "https://github.com/time-rs/time", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "This crate is an implementation detail and should not be relied upon directly." + }, + { + "name": "time-macros", + "version": "0.2.15", + "authors": "Jacob Pratt |Time contributors", + "repository": "https://github.com/time-rs/time", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Procedural macros for the time crate. This crate is an implementation detail and should not be relied upon directly." + }, + { + "name": "trgt-denovo", + "version": "0.1.0", + "authors": "Tom Mokveld|Egor Dolzhenko", + "repository": null, + "license": null, + "license_file": null, + "description": "trgt-denovo is a CLI tool for targeted de novo tandem repeat calling from long-read HiFI sequencing data." + }, + { + "name": "unicode-ident", + "version": "1.0.12", + "authors": "David Tolnay ", + "repository": "https://github.com/dtolnay/unicode-ident", + "license": "(MIT OR Apache-2.0) AND Unicode-DFS-2016", + "license_file": null, + "description": "Determine whether characters have the XID_Start or XID_Continue properties according to Unicode Standard Annex #31" + }, + { + "name": "utf8parse", + "version": "0.2.1", + "authors": "Joe Wilm |Christian Duerr ", + "repository": "https://github.com/alacritty/vte", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Table-driven UTF-8 parser" + }, + { + "name": "vergen", + "version": "8.2.5", + "authors": "Jason Ozias ", + "repository": "https://github.com/rustyhorde/vergen", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Generate 'cargo:rustc-env' instructions via 'build.rs' for use in your code via the 'env!' macro" + }, + { + "name": "wasi", + "version": "0.11.0+wasi-snapshot-preview1", + "authors": "The Cranelift Project Developers", + "repository": "https://github.com/bytecodealliance/wasi", + "license": "Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT", + "license_file": null, + "description": "Experimental WASI API bindings for Rust" + }, + { + "name": "wasm-bindgen", + "version": "0.2.88", + "authors": "The wasm-bindgen Developers", + "repository": "https://github.com/rustwasm/wasm-bindgen", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Easy support for interacting between JS and Rust." + }, + { + "name": "wasm-bindgen-backend", + "version": "0.2.88", + "authors": "The wasm-bindgen Developers", + "repository": "https://github.com/rustwasm/wasm-bindgen/tree/master/crates/backend", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Backend code generation of the wasm-bindgen tool" + }, + { + "name": "wasm-bindgen-macro", + "version": "0.2.88", + "authors": "The wasm-bindgen Developers", + "repository": "https://github.com/rustwasm/wasm-bindgen/tree/master/crates/macro", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Definition of the `#[wasm_bindgen]` attribute, an internal dependency" + }, + { + "name": "wasm-bindgen-macro-support", + "version": "0.2.88", + "authors": "The wasm-bindgen Developers", + "repository": "https://github.com/rustwasm/wasm-bindgen/tree/master/crates/macro-support", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "The part of the implementation of the `#[wasm_bindgen]` attribute that is not in the shared backend crate" + }, + { + "name": "wasm-bindgen-shared", + "version": "0.2.88", + "authors": "The wasm-bindgen Developers", + "repository": "https://github.com/rustwasm/wasm-bindgen/tree/master/crates/shared", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Shared support between wasm-bindgen and wasm-bindgen cli, an internal dependency." + }, + { + "name": "wfa2-sys", + "version": "0.1.0", + "authors": null, + "repository": null, + "license": null, + "license_file": null, + "description": null + }, + { + "name": "which", + "version": "4.4.2", + "authors": "Harry Fei ", + "repository": "https://github.com/harryfei/which-rs.git", + "license": "MIT", + "license_file": null, + "description": "A Rust equivalent of Unix command \"which\". Locate installed executable in cross platforms." + }, + { + "name": "winapi", + "version": "0.3.9", + "authors": "Peter Atashian ", + "repository": "https://github.com/retep998/winapi-rs", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Raw FFI bindings for all of Windows API." + }, + { + "name": "winapi-i686-pc-windows-gnu", + "version": "0.4.0", + "authors": "Peter Atashian ", + "repository": "https://github.com/retep998/winapi-rs", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Import libraries for the i686-pc-windows-gnu target. Please don't use this crate directly, depend on winapi instead." + }, + { + "name": "winapi-util", + "version": "0.1.6", + "authors": "Andrew Gallant ", + "repository": "https://github.com/BurntSushi/winapi-util", + "license": "MIT OR Unlicense", + "license_file": null, + "description": "A dumping ground for high level safe wrappers over winapi." + }, + { + "name": "winapi-x86_64-pc-windows-gnu", + "version": "0.4.0", + "authors": "Peter Atashian ", + "repository": "https://github.com/retep998/winapi-rs", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Import libraries for the x86_64-pc-windows-gnu target. Please don't use this crate directly, depend on winapi instead." + }, + { + "name": "windows-core", + "version": "0.51.1", + "authors": "Microsoft", + "repository": "https://github.com/microsoft/windows-rs", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Rust for Windows" + }, + { + "name": "windows-sys", + "version": "0.48.0", + "authors": "Microsoft", + "repository": "https://github.com/microsoft/windows-rs", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Rust for Windows" + }, + { + "name": "windows-targets", + "version": "0.48.5", + "authors": "Microsoft", + "repository": "https://github.com/microsoft/windows-rs", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Import libs for Windows" + }, + { + "name": "windows_aarch64_gnullvm", + "version": "0.48.5", + "authors": "Microsoft", + "repository": "https://github.com/microsoft/windows-rs", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Import lib for Windows" + }, + { + "name": "windows_aarch64_msvc", + "version": "0.48.5", + "authors": "Microsoft", + "repository": "https://github.com/microsoft/windows-rs", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Import lib for Windows" + }, + { + "name": "windows_i686_gnu", + "version": "0.48.5", + "authors": "Microsoft", + "repository": "https://github.com/microsoft/windows-rs", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Import lib for Windows" + }, + { + "name": "windows_i686_msvc", + "version": "0.48.5", + "authors": "Microsoft", + "repository": "https://github.com/microsoft/windows-rs", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Import lib for Windows" + }, + { + "name": "windows_x86_64_gnu", + "version": "0.48.5", + "authors": "Microsoft", + "repository": "https://github.com/microsoft/windows-rs", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Import lib for Windows" + }, + { + "name": "windows_x86_64_gnullvm", + "version": "0.48.5", + "authors": "Microsoft", + "repository": "https://github.com/microsoft/windows-rs", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Import lib for Windows" + }, + { + "name": "windows_x86_64_msvc", + "version": "0.48.5", + "authors": "Microsoft", + "repository": "https://github.com/microsoft/windows-rs", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Import lib for Windows" + } +] diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..5b0993a --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,15 @@ +# Pacific Biosciences Software License Agreement +1. **Introduction and Acceptance.** This Software License Agreement (this “**Agreement**”) is a legal agreement between you (either an individual or an entity) and Pacific Biosciences of California, Inc. (“**PacBio**”) regarding the use of the PacBio software accompanying this Agreement, which includes documentation provided in “online” or electronic form (together, the “**Software**”). PACBIO PROVIDES THE SOFTWARE SOLELY ON THE TERMS AND CONDITIONS SET FORTH IN THIS AGREEMENT AND ON THE CONDITION THAT YOU ACCEPT AND COMPLY WITH THEM. BY DOWNLOADING, DISTRIBUTING, MODIFYING OR OTHERWISE USING THE SOFTWARE, YOU (A) ACCEPT THIS AGREEMENT AND AGREE THAT YOU ARE LEGALLY BOUND BY ITS TERMS; AND (B) REPRESENT AND WARRANT THAT: (I) YOU ARE OF LEGAL AGE TO ENTER INTO A BINDING AGREEMENT; AND (II) IF YOU REPRESENT A CORPORATION, GOVERNMENTAL ORGANIZATION OR OTHER LEGAL ENTITY, YOU HAVE THE RIGHT, POWER AND AUTHORITY TO ENTER INTO THIS AGREEMENT ON BEHALF OF SUCH ENTITY AND BIND SUCH ENTITY TO THESE TERMS. IF YOU DO NOT AGREE TO THE TERMS OF THIS AGREEMENT, PACBIO WILL NOT AND DOES NOT LICENSE THE SOFTWARE TO YOU AND YOU MUST NOT DOWNLOAD, INSTALL OR OTHERWISE USE THE SOFTWARE OR DOCUMENTATION. +2. **Grant of License.** Subject to your compliance with the restrictions set forth in this Agreement, PacBio hereby grants to you a non-exclusive, non-transferable license during the Term to install, copy, use, distribute in binary form only, and host the Software. If you received the Software from PacBio in source code format, you may also modify and/or compile the Software. +3. **License Restrictions.** You may not remove or destroy any copyright notices or other proprietary markings. You may only use the Software to process or analyze data generated on a PacBio instrument or otherwise provided to you by PacBio. Any use, modification, translation, or compilation of the Software not expressly authorized in Section 2 is prohibited. You may not use, modify, host, or distribute the Software so that any part of the Software becomes subject to any license that requires, as a condition of use, modification, hosting, or distribution, that (a) the Software, in whole or in part, be disclosed or distributed in source code form or (b) any third party have the right to modify the Software, in whole or in part. +4. **Ownership.** The license granted to you in Section 2 is not a transfer or sale of PacBio’s ownership rights in or to the Software. Except for the license granted in Section 2, PacBio retains all right, title and interest (including all intellectual property rights) in and to the Software. The Software is protected by applicable intellectual property laws, including United States copyright laws and international treaties. +5. **Third Party Materials.** The Software may include software, content, data or other materials, including related documentation and open source software, that are owned by one or more third parties and that are subject to separate licensee terms (“**Third-Party Licenses**”). A list of all materials, if any, can be found the documentation for the Software. You acknowledge and agree that such third party materials subject to Third-Party Licenses are not licensed to you pursuant to the provisions of this Agreement and that this Agreement shall not be construed to grant any such right and/or license. You shall have only such rights and/or licenses, if any, to use such third party materials as set forth in the applicable Third-Party Licenses. +6. **Feedback.** If you provide any feedback to PacBio concerning the functionality and performance of the Software, including identifying potential errors and improvements (“**Feedback**”), such Feedback shall be owned by PacBio. You hereby assign to PacBio all right, title, and interest in and to the Feedback, and PacBio is free to use the Feedback without any payment or restriction. +7. **Confidentiality.** You must hold in the strictest confidence the Software and any related materials or information including, but not limited to, any Feedback, technical data, research, product plans, or know-how provided by PacBio to you, directly or indirectly in writing, orally or by inspection of tangible objects (“**Confidential Information**”). You will not disclose any Confidential Information to third parties, including any of your employees who do not have a need to know such information, and you will take reasonable measures to protect the secrecy of, and to avoid disclosure and unauthorized use of, the Confidential Information. You will immediately notify the PacBio in the event of any unauthorized or suspected use or disclosure of the Confidential Information. To protect the Confidential Information contained in the Software, you may not reverse engineer, decompile, or disassemble the Software, except to the extent the foregoing restriction is expressly prohibited by applicable law. +8. **Termination.** This Agreement will terminate upon the earlier of: (a) your failure to comply with any term of this Agreement; or (b) return, destruction, or deletion of all copies of the Software in your possession. PacBio’s rights and your obligations will survive the termination of this Agreement. The “**Term**” means the period beginning on when this Agreement becomes effective until the termination of this Agreement. Upon termination of this Agreement for any reason, you will delete from all of your computer libraries or storage devices or otherwise destroy all copies of the Software and derivatives thereof. +9. **NO OTHER WARRANTIES.** THE SOFTWARE IS PROVIDED ON AN “AS IS” BASIS. YOU ASSUME ALL RESPONSIBILITIES FOR SELECTION OF THE SOFTWARE TO ACHIEVE YOUR INTENDED RESULTS, AND FOR THE INSTALLATION OF, USE OF, AND RESULTS OBTAINED FROM THE SOFTWARE. TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, PACBIO DISCLAIMS ALL WARRANTIES, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO IMPLIED WARRANTIES OF MERCHANTABILITY, QUALITY, ACCURACY, TITLE, NONINFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE WITH RESPECT TO THE SOFTWARE AND THE ACCOMPANYING WRITTEN MATERIALS. THERE IS NO WARRANTY AGAINST INTERFERENCE WITH THE ENJOYMENT OF THE SOFTWARE OR AGAINST INFRINGEMENT. THERE IS NO WARRANTY THAT THE SOFTWARE OR PACBIO’S EFFORTS WILL FULFILL ANY OF YOUR PARTICULAR PURPOSES OR NEEDS. +10. **LIMITATION OF LIABILITY.** UNDER NO CIRCUMSTANCES WILL PACBIO BE LIABLE FOR ANY CONSEQUENTIAL, SPECIAL, INDIRECT, INCIDENTAL OR PUNITIVE DAMAGES WHATSOEVER (INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF BUSINESS PROFITS, BUSINESS INTERRUPTION, LOSS OF BUSINESS INFORMATION, LOSS OF DATA OR OTHER SUCH PECUNIARY LOSS) ARISING OUT OF THE USE OR INABILITY TO USE THE SOFTWARE, EVEN IF PACBIO HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. IN NO EVENT WILL PACBIO’S AGGREGATE LIABILITY FOR DAMAGES ARISING OUT OF THIS AGREEMENT EXCEED $5. THE FOREGOING EXCLUSIONS AND LIMITATIONS OF LIABILITY AND DAMAGES WILL NOT APPLY TO CONSEQUENTIAL DAMAGES FOR PERSONAL INJURY. +11. **Indemnification.** You will indemnify, hold harmless, and defend PacBio (including all of its officers, employees, directors, subsidiaries, representatives, affiliates, and agents) and PacBio’s suppliers from and against any damages (including attorney’s fees and expenses), claims, and lawsuits that arise or result from your use of the Software. +12. **Trademarks.** Certain of the product and PacBio names used in this Agreement, the Software may constitute trademarks of PacBio or third parties. You are not authorized to use any such trademarks. +13. **Export Restrictions.** YOU UNDERSTAND AND AGREE THAT THE SOFTWARE IS SUBJECT TO UNITED STATES AND OTHER APPLICABLE EXPORT-RELATED LAWS AND REGULATIONS AND THAT YOU MAY NOT EXPORT, RE-EXPORT OR TRANSFER THE SOFTWARE OR ANY DIRECT PRODUCT OF THE SOFTWARE EXCEPT AS PERMITTED UNDER THOSE LAWS. WITHOUT LIMITING THE FOREGOING, EXPORT, RE-EXPORT, OR TRANSFER OF THE SOFTWARE TO CUBA, IRAN, NORTH KOREA, SYRIA, RUSSIA, BELARUS, AND THE REGIONS OF CRIMEA, LNR, AND DNR OF UKRAINE IS PROHIBITED. +14. **General.** This Agreement is governed by the laws of the State of California, without reference to its conflict of laws principles. This Agreement is the entire agreement between you and PacBio and supersedes any other communications with respect to the Software. If any provision of this Agreement is held invalid or unenforceable, the remainder of this Agreement will continue in full force and effect. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..9b4a7a2 --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +# TRGT-denovo: *de novo* tandem repeat caller + +TRGT-denovo is a companion tool of [TRGT (Tandem Repeat Genotyper)](https://github.com/PacificBiosciences/trgt) that does targeted *de novo* calling of tandem repeat mutations from PacBio Hifi Data in trios. It uses the output generated by TRGT to do so. + +Authors: [Tom Mokveld](https://github.com/tmokveld), [Egor Dolzhenko](https://github.com/egor-dolzhenko) + +## Early version warning + +Please note that TRGT-denovo is still in early development and is subject to signficant changes that can affect anything from input / output file formats to program behavior. + +## Availability + +* [Latest release with binary](https://github.com/PacificBiosciences/trgt-denovo/releases/latest) + +To build TRGT-denovo you need a working C compiler. It was tested on Linux with Clang 13.0.0 & GCC 11.3.0 and on Mac OSX (M1) with Clang 15.0.7 & GCC 14.0.0. + +## Documentation + +* [Example run](docs/example.md) +* [Output](docs/output.md) +* [Command-line interface](docs/cli.md) + +## Need help? +If you notice any missing features, bugs, or need assistance with analyzing the output of TRGT-denovo, +please don't hesitate to open a GitHub issue. + +## Support information +TRGT-denovo is a pre-release software intended for research use only and not for use in diagnostic procedures. +While efforts have been made to ensure that TRGT-denovo lives up to the quality that PacBio strives for, we make no warranty regarding this software. + +As TRGT-denovo is not covered by any service level agreement or the like, please do not contact a PacBio Field Applications Scientists or PacBio Customer Service for assistance with any TRGT-denovo release. +Please report all issues through GitHub instead. +We make no warranty that any such issue will be addressed, to any extent or within any time frame. + +### DISCLAIMER +THIS WEBSITE AND CONTENT AND ALL SITE-RELATED SERVICES, INCLUDING ANY DATA, ARE PROVIDED "AS IS," WITH ALL FAULTS, WITH NO REPRESENTATIONS OR WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTIES OF MERCHANTABILITY, SATISFACTORY QUALITY, NON-INFRINGEMENT OR FITNESS FOR A PARTICULAR PURPOSE. YOU ASSUME TOTAL RESPONSIBILITY AND RISK FOR YOUR USE OF THIS SITE, ALL SITE-RELATED SERVICES, AND ANY THIRD PARTY WEBSITES OR APPLICATIONS. NO ORAL OR WRITTEN INFORMATION OR ADVICE SHALL CREATE A WARRANTY OF ANY KIND. ANY REFERENCES TO SPECIFIC PRODUCTS OR SERVICES ON THE WEBSITES DO NOT CONSTITUTE OR IMPLY A RECOMMENDATION OR ENDORSEMENT BY PACIFIC BIOSCIENCES. \ No newline at end of file diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..2e62fcd --- /dev/null +++ b/build.rs @@ -0,0 +1,21 @@ +use std::error::Error; +use vergen::EmitBuilder; + +fn main() -> Result<(), Box> { + match EmitBuilder::builder() + .fail_on_error() + .custom_build_rs(".") // override such that it will re-run whenever we have a file change in this folder + .all_git() + .git_describe(true, false, Some("ThisPatternShouldNotMatchAnythingEver")) + .emit() + { + Ok(_) => { + println!("cargo:rerun-if-changed=Cargo.toml"); + println!("cargo:rerun-if-changed=src"); + } + Err(_e) => { + println!("cargo:rustc-env=VERGEN_GIT_DESCRIBE=unknown"); + } + } + Ok(()) +} diff --git a/docs/cli.md b/docs/cli.md new file mode 100644 index 0000000..62c0582 --- /dev/null +++ b/docs/cli.md @@ -0,0 +1,19 @@ +## TRGT-denovo command-line options + +### Command: trio +Basic: +- `-r, --reference ` Path to the FASTA file containing the reference genome. The same reference genome as was used for read alignment. +- `-b, --bed ` Path to the BED file with reference coordinates of tandem repeats +- `-m, --mother ` Common (path) prefix of spanning reads BAM file and variant call VCF file of mother +- `-f, --father ` Common (path) prefix of spanning reads BAM file and variant call VCF file of father +- `-c, --child ` Common (path) prefix of spanning reads BAM file and variant call VCF file of child +- `-o, --out ` Output csv path +- `--trid ` Optionally a tandem repeat ID may be supplied, if so, only this site will be tested, default = None +- `-@ ` Number of threads, the number of sites that are processed in parallel, default = 1 +- `-h, --help` Print help +- `-V, --version` Print version + +Advanced: +- `--flank-len ` Number of flanking nucleotides added to a target region during realignment, default = 50 +- `--no-clip-aln` Score alignments without stripping the flanks +- `--parental-quantile ` Quantile of alignment scores to determine the parental threshold, default is strict and takes only the top scoring alignment, default = 1.0 \ No newline at end of file diff --git a/docs/example.md b/docs/example.md new file mode 100644 index 0000000..dc5a77a --- /dev/null +++ b/docs/example.md @@ -0,0 +1,104 @@ +# Introduction + +A brief overview of the steps necessary to perform *de novo* tandem repeat mutation calling from TRGT output using TRGT-denovo + +## Prerequisites + +- [TRGT binary](https://github.com/PacificBiosciences/trgt/releases/latest) +- [TRGT-denovo binary](https://github.com/PacificBiosciences/trgt-denovo/releases/latest) +- Aligned HiFi data of a family trio (father, mother, child) +- The reference genome used for the alignments +- A BED file with repeat expansion definitions (always same or a subset of those with TRGT) + +## Calling *de novo* tandem repeats + +Given the following data: + +- reference genome `reference.fasta` +- aligned sequencing data of the family (father, mother, and son respectively) `sample_F.bam`, `sample_M.bam`, `sample_S.bam`, +- repeat definition file `repeat.bed`. + +### Data pre-processing + +Prior to *de novo* calling all data must first be genotyped by TRGT: + +``` +./trgt --genome reference.fasta \ + --repeats repeat.bed \ + --reads sample_F.bam \ + --output-prefix sample_F \ + --karyotype XY +``` + +``` +./trgt --genome reference.fasta \ + --repeats repeat.bed \ + --reads sample_M.bam \ + --output-prefix sample_M \ + --karyotype XX +``` + +``` +./trgt --genome reference.fasta \ + --repeats repeat.bed \ + --reads sample_S.bam \ + --output-prefix sample_S \ + --karyotype XY +``` + +TRGT outputs the genotyped repeat sites in a VCF file stored in `prefix.vcf.gz` and the spanning reads that were used to genotype each site (that fully span the repeat sequences) stored in `prefix.spanning.bam`. TRGT-denovo requires sorted BAM and VCF data, hence you will need to sort and index the output VCF and BAM files. For each family member this involves: + +#### VCF sorting +``` +bcftools sort -Ob -o sample_F.sorted.vcf.gz sample_F.vcf.gz +bcftools index sample_F.sorted.vcf.gz +``` + +#### BAM sorting +``` +samtools sort -o sample_F.spanning.sorted.bam sample_F.spanning.bam +samtools index sample_F.spanning.sorted.bam +``` + +Such that you end up with `sample_F.sorted.vcf.gz`, `sample_F.spanning.sorted.bam`, `sample_M.sorted.vcf.gz`, `sample_M.spanning.sorted.bam`, `sample_S.sorted.vcf.gz`, `sample_S.spanning.sorted.bam` (and their associated `.bam.bai` and `.vcf.gz.csi` indices). + +### Running TRGT-denovo + +With all preprocessing completed it we can call *de novo* repeat expansion mutations using TRGT-denovo from the sample data. Note that family members are supplied by their common prefix of `spanning.sorted.bam` and `sorted.vcf.gz`, i.e., `sample_F`, `sample_M`, and `sample_S` and path if not running TRGT-denovo in the same directory as the data: + +``` +./TRGT-denovo trio --reference reference.fasta \ + --bed repeat.bed \ + --father sample_F \ + --mother sample_M \ + --child sample_S \ + --out out.csv +``` + +## Example + +Below output of HG002 is shown for two candidate *de novo* tandem repeat mutation sites: +``` +trid genotype denovo_coverage allele_coverage allele_ratio child_coverage child_ratio mean_diff_father mean_diff_mother father_dropout_prob mother_dropout_prob allele_origin denovo_status per_allele_reads_father per_allele_reads_mother per_allele_reads_child index father_motif_counts mother_motif_counts child_motif_counts maxlh +chr1_47268728_47268830_ATAA 1 19 37 0.5135 37 0.5135 6.7368 6.7368 0.0000 0.0000 M:1 Y:= 43 26 37 0 25 25 25 0.7152 +chr1_7862944_7863157_TATTG 1 0 21 0.0000 37 0.0000 0.0000 19.2000 0.0000 0.0000 F:2 X 18,17 16,19 21,16 0 27,29 27,63 29,60 0.9820 +chr1_7862944_7863157_TATTG 2 16 16 1.0000 37 0.4324 171.8750 22.8750 0.0000 0.0000 M:2 Y:- 18,17 16,19 21,16 1 27,29 27,63 29,60 1.0000 +``` + +### Site 1 + +The first site is homozygous in the child, hence only one call is made. It has a *de novo* coverage (DNC) of 19, i.e., there are 19 reads that support a candidate *de novo* allele relative to the parental read alignments. The DNC should always be put into context of the total coverage, to ascertain that: + +1. There is sufficient coverage +2. The ratio between the two is close to 0.5 + +The DNC at this site is high and the ratio makes it likely that this is a confident call. However, the score difference with respect to either parents is low, such that the expected event size is small. Additionally, the score difference is equivalent in both parents such that parental origin may not be assessed. Generally the parent with the smallest score difference is the one from which is inherited: + +![Example site 1](figures/example_site_1.png) + +### Site 2 + +The second site is heterozygous in the child, hence both child alleles are tested. The second allele is a potential *de novo* call. The score difference with respect to the maternal alleles is the smallest. + +![Example site 2](figures/example_site_2.png) + diff --git a/docs/figures/example_site_1.png b/docs/figures/example_site_1.png new file mode 100644 index 0000000000000000000000000000000000000000..8eb4532ddb294b5a17a92cfb886c8087a90e7182 GIT binary patch literal 83143 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 T#vlI;{*yhfa4hkN-rxTR6+p0z literal 0 HcmV?d00001 diff --git a/docs/figures/example_site_2.png b/docs/figures/example_site_2.png new file mode 100644 index 0000000000000000000000000000000000000000..325bd794498db9244a1698b412d073f715dc364e GIT binary patch literal 101816 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 J&m}M1`adkmHT?hp literal 0 HcmV?d00001 diff --git a/docs/output.md b/docs/output.md new file mode 100644 index 0000000..5f19cbf --- /dev/null +++ b/docs/output.md @@ -0,0 +1,27 @@ +# Interpreting TRGT-denovo output + +TRGT-denovo scores and then outputs target sites in a tab-separated format, with 11 columns: + +- `trid` ID of the tandem repeat, encoded as in the BED file +- `genotype` Genotype ID of the child for a specific allele, corresponds to the TRGT genotype ID +- `denovo_coverage` Number of child reads supporting a de novo allele compared to parental data. +- `allele_coverage` Number of child reads mapped to this specific allele +- `allele_ratio` Ratio of de novo coverage to allele coverage. +- `child_coverage` Total number of child reads at this site +- `child_ratio` Ratio of de novo coverage to total coverage at this site +- `mean_diff_father` Score difference between de novo and paternal reads; lower values indicate greater similarity +- `mean_diff_mother` Score difference between de novo and maternal reads; lower values indicate greater similarity +- `father_dropout_prob` Dropout rate for reads coming from the mother +- `mother_dropout_prob` Dropout rate for reads coming from the father. +- `allele_origin` Inferred origin of the allele based on alignment; possible values: `{F:{1,2,?}, M:{1,2,?}, ?}` +- `denovo_status` Indicates if the allele is de novo, only if `allele_origin` is defined; possible values: `{?, Y:{+, -, =}}` +- `per_allele_reads_father` Number of reads partitioned per allele in the father (allele1, allele2) +- `per_allele_reads_mother` Number of reads partitioned per allele in the mother (allele1, allele2) +- `per_allele_reads_child` Number of reads partitioned per allele in the child (allele1, allele2) +- `index` Index of this allele in the TRGT VCF, used for linking to `child_motif_counts` +- `father_motif_counts` TRGT VCF motif counts for this locus in the father +- `mother_motif_counts` TRGT VCF motif counts for this locus in the mother +- `child_motif_counts` TRGT VCF motif counts for this locus in the child +- `maxlh` Maximum likelihood score of child allele data relative to parental alleles (unstable) + +TRGT-denovo does calling based on the genotyping and spanning reads generated by TRGT and will test and output at most two alleles in the event of a heterozygous site. \ No newline at end of file diff --git a/src/aligner.rs b/src/aligner.rs new file mode 100644 index 0000000..878d7e9 --- /dev/null +++ b/src/aligner.rs @@ -0,0 +1,1038 @@ +use crate::wfa2; +use serde::{Deserialize, Serialize}; +use std::ptr; + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] +pub enum MemoryModel { + MemoryHigh, + MemoryMed, + MemoryLow, + MemoryUltraLow, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] +pub enum AlignmentScope { + Score, + Alignment, +} + +impl From for AlignmentScope { + fn from(value: u32) -> Self { + match value { + wfa2::alignment_scope_t_compute_score => AlignmentScope::Score, + wfa2::alignment_scope_t_compute_alignment => AlignmentScope::Alignment, + _ => panic!("Unknown alignment scope: {}", value), + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] +pub enum Heuristic { + None, + WFadaptive(i32, i32, i32), + WFmash(i32, i32, i32), + XDrop(i32, i32), + ZDrop(i32, i32), + BandedStatic(i32, i32), + BandedAdaptive(i32, i32, i32), +} + +pub enum DistanceMetric { + Indel, + Edit, + GapLinear, + GapAffine, + GapAffine2p, +} + +impl From for DistanceMetric { + fn from(value: u32) -> Self { + match value { + wfa2::distance_metric_t_indel => DistanceMetric::Indel, + wfa2::distance_metric_t_edit => DistanceMetric::Edit, + wfa2::distance_metric_t_gap_linear => DistanceMetric::GapLinear, + wfa2::distance_metric_t_gap_affine => DistanceMetric::GapAffine, + wfa2::distance_metric_t_gap_affine_2p => DistanceMetric::GapAffine2p, + _ => panic!("Unknown distance metric: {}", value), + } + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum AlignmentStatus { + // OK Status (>=0) + StatusAlgCompleted = wfa2::WF_STATUS_ALG_COMPLETED as isize, + StatusAlgPartial = wfa2::WF_STATUS_ALG_PARTIAL as isize, + // FAILED Status (<0) + StatusMaxStepsReached = wfa2::WF_STATUS_MAX_STEPS_REACHED as isize, + StatusOOM = wfa2::WF_STATUS_OOM as isize, + StatusUnattainable = wfa2::WF_STATUS_UNATTAINABLE as isize, +} + +impl From for AlignmentStatus { + fn from(value: i32) -> Self { + match value { + x if x == wfa2::WF_STATUS_ALG_COMPLETED as i32 => AlignmentStatus::StatusAlgCompleted, + x if x == wfa2::WF_STATUS_ALG_PARTIAL as i32 => AlignmentStatus::StatusAlgPartial, + wfa2::WF_STATUS_MAX_STEPS_REACHED => AlignmentStatus::StatusMaxStepsReached, + wfa2::WF_STATUS_OOM => AlignmentStatus::StatusOOM, + wfa2::WF_STATUS_UNATTAINABLE => AlignmentStatus::StatusUnattainable, + _ => panic!("Unknown alignment status: {}", value), + } + } +} + +#[derive(Debug, Copy, Clone)] +struct WFAttributes { + inner: wfa2::wavefront_aligner_attr_t, +} + +impl WFAttributes { + fn default() -> Self { + Self { + inner: unsafe { wfa2::wavefront_aligner_attr_default }, + } + } + + fn memory_model(mut self, memory_model: MemoryModel) -> Self { + let memory_mode = match memory_model { + MemoryModel::MemoryHigh => wfa2::wavefront_memory_t_wavefront_memory_high, + MemoryModel::MemoryMed => wfa2::wavefront_memory_t_wavefront_memory_med, + MemoryModel::MemoryLow => wfa2::wavefront_memory_t_wavefront_memory_low, + MemoryModel::MemoryUltraLow => wfa2::wavefront_memory_t_wavefront_memory_ultralow, + }; + self.inner.memory_mode = memory_mode; + self + } + + fn alignment_scope(mut self, alignment_scope: AlignmentScope) -> Self { + let alignment_scope = match alignment_scope { + AlignmentScope::Score => wfa2::alignment_scope_t_compute_score, + AlignmentScope::Alignment => wfa2::alignment_scope_t_compute_alignment, + }; + self.inner.alignment_scope = alignment_scope; + self + } + + fn indel_penalties(mut self) -> Self { + self.inner.distance_metric = wfa2::distance_metric_t_indel; + self + } + + fn edit_penalties(mut self) -> Self { + self.inner.distance_metric = wfa2::distance_metric_t_edit; + self + } + + fn linear_penalties(mut self, match_: i32, mismatch: i32, indel: i32) -> Self { + self.inner.distance_metric = wfa2::distance_metric_t_gap_linear; + self.inner.linear_penalties.match_ = match_; // (Penalty representation usually M <= 0) + self.inner.linear_penalties.mismatch = mismatch; // (Penalty representation usually X > 0) + self.inner.linear_penalties.indel = indel; // (Penalty representation usually I > 0) + self + } + + fn affine_penalties( + mut self, + match_: i32, + mismatch: i32, + gap_opening: i32, + gap_extension: i32, + ) -> Self { + self.inner.distance_metric = wfa2::distance_metric_t_gap_affine; + self.inner.affine_penalties.match_ = match_; // (Penalty representation usually M <= 0) + self.inner.affine_penalties.mismatch = mismatch; // (Penalty representation usually X > 0) + self.inner.affine_penalties.gap_opening = gap_opening; // (Penalty representation usually O > 0) + self.inner.affine_penalties.gap_extension = gap_extension; // (Penalty representation usually E > 0) + self + } + + fn affine2p_penalties( + mut self, + match_: i32, + mismatch: i32, + gap_opening1: i32, + gap_extension1: i32, + gap_opening2: i32, + gap_extension2: i32, + ) -> Self { + self.inner.distance_metric = wfa2::distance_metric_t_gap_affine_2p; + self.inner.affine2p_penalties.match_ = match_; // (Penalty representation usually M <= 0) + self.inner.affine2p_penalties.mismatch = mismatch; // (Penalty representation usually X > 0) + // Usually concave Q1 + E1 < Q2 + E2 and E1 > E2. + self.inner.affine2p_penalties.gap_opening1 = gap_opening1; // (Penalty representation usually O1 > 0) + self.inner.affine2p_penalties.gap_extension1 = gap_extension1; // (Penalty representation usually E1 > 0) + self.inner.affine2p_penalties.gap_opening2 = gap_opening2; // (Penalty representation usually O2 > 0) + self.inner.affine2p_penalties.gap_extension2 = gap_extension2; // (Penalty representation usually E2 > 0) + self + } +} + +pub struct WFAligner { + attributes: WFAttributes, + inner: *mut wfa2::wavefront_aligner_t, +} + +impl WFAligner { + pub fn new(alignment_scope: AlignmentScope, memory_model: MemoryModel) -> Self { + let attributes = WFAttributes::default() + .memory_model(memory_model) + .alignment_scope(alignment_scope); + Self { + attributes, + inner: ptr::null_mut(), + } + } +} + +impl Drop for WFAligner { + fn drop(&mut self) { + unsafe { + if !self.inner.is_null() { + wfa2::wavefront_aligner_delete(self.inner); + } + } + } +} + +impl WFAligner { + pub fn align_end_to_end(&mut self, pattern: &[u8], text: &[u8]) -> AlignmentStatus { + let status = unsafe { + wfa2::wavefront_aligner_set_alignment_end_to_end(self.inner); + wfa2::wavefront_align( + self.inner, + pattern.as_ptr() as *const i8, + pattern.len() as i32, + text.as_ptr() as *const i8, + text.len() as i32, + ) + }; + AlignmentStatus::from(status) + } + + pub fn align_ends_free( + &mut self, + pattern: &[u8], + pattern_begin_free: i32, + pattern_end_free: i32, + text: &[u8], + text_begin_free: i32, + text_end_free: i32, + ) -> AlignmentStatus { + let status = unsafe { + wfa2::wavefront_aligner_set_alignment_free_ends( + self.inner, + pattern_begin_free, + pattern_end_free, + text_begin_free, + text_end_free, + ); + wfa2::wavefront_align( + self.inner, + pattern.as_ptr() as *const i8, + pattern.len() as i32, + text.as_ptr() as *const i8, + text.len() as i32, + ) + }; + AlignmentStatus::from(status) + } + + pub fn score(&self) -> i32 { + unsafe { *(*self.inner).cigar }.score + } + + pub fn cigar_score_gap_affine2p_direct(&mut self) -> i32 { + unsafe { + wfa2::cigar_score_gap_affine2p( + (*self.inner).cigar, + &mut self.attributes.inner.affine2p_penalties, + ) + } + } + + fn cigar_score_gap_affine2p_get_operations_score( + operation: char, + op_length: i32, + penalties: &wfa2::affine2p_penalties_t, + ) -> i32 { + match operation { + 'M' => op_length * penalties.match_, + 'X' => op_length * penalties.mismatch, + 'D' | 'I' => { + let score1 = penalties.gap_opening1 + penalties.gap_extension1 * op_length; + let score2 = penalties.gap_opening2 + penalties.gap_extension2 * op_length; + std::cmp::min(score1, score2) + } + _ => panic!("Invalid operation: {}", operation), + } + } + + fn cigar_score_gap_affine2_clipped(&self, flank_len: usize) -> i32 { + let cigar = unsafe { (*self.inner).cigar.as_ref() }.unwrap(); + let begin_offset = cigar.begin_offset as isize + flank_len as isize; + let end_offset = cigar.end_offset as isize - flank_len as isize; + + let operations: Vec = + unsafe { std::slice::from_raw_parts(cigar.operations, cigar.max_operations as usize) } + .iter() + .map(|&op| op as u8 as char) + .collect(); + + let mut score = 0; + let mut op_length = 0; + let mut last_op: Option = None; + for i in begin_offset..end_offset { + let cur_op = operations[i as usize]; + if last_op.is_some() && cur_op != last_op.unwrap() { + score -= Self::cigar_score_gap_affine2p_get_operations_score( + last_op.unwrap(), + op_length, + &self.attributes.inner.affine2p_penalties, + ); + op_length = 0; + } + last_op = Some(cur_op); + op_length += 1; + } + if let Some(op) = last_op { + score -= Self::cigar_score_gap_affine2p_get_operations_score( + op, + op_length, + &self.attributes.inner.affine2p_penalties, + ); + } + score + } + + pub fn cigar_score_clipped(&self, flank_len: usize) -> i32 { + if AlignmentScope::from(self.attributes.inner.alignment_scope) == AlignmentScope::Score { + panic!("Cannot clip when AlignmentScope is Score"); + } + + match DistanceMetric::from(self.attributes.inner.distance_metric) { + DistanceMetric::Indel | DistanceMetric::Edit => { + unimplemented!("Indel/Linear distance metric not implemented") + } + DistanceMetric::GapLinear => { + unimplemented!("GapLinear distance metric not implemented") + } + DistanceMetric::GapAffine => { + unimplemented!("GapAffine distance metric not implemented") + } + DistanceMetric::GapAffine2p => self.cigar_score_gap_affine2_clipped(flank_len), + } + } + + pub fn cigar_score(&mut self) -> i32 { + if AlignmentScope::from(self.attributes.inner.alignment_scope) == AlignmentScope::Score { + panic!("Cannot clip when AlignmentScope is Score"); + } + + unsafe { + match DistanceMetric::from(self.attributes.inner.distance_metric) { + DistanceMetric::Indel | DistanceMetric::Edit => { + wfa2::cigar_score_edit((*self.inner).cigar) + } + DistanceMetric::GapLinear => wfa2::cigar_score_gap_linear( + (*self.inner).cigar, + &mut self.attributes.inner.linear_penalties, + ), + DistanceMetric::GapAffine => wfa2::cigar_score_gap_affine( + (*self.inner).cigar, + &mut self.attributes.inner.affine_penalties, + ), + DistanceMetric::GapAffine2p => wfa2::cigar_score_gap_affine2p( + (*self.inner).cigar, + &mut self.attributes.inner.affine2p_penalties, + ), + } + } + } + + pub fn cigar(&self, flank_len: Option) -> String { + let offset = flank_len.unwrap_or(0); + let mut cstr = String::new(); + + let cigar = unsafe { (*self.inner).cigar.as_ref() }.unwrap(); + let begin_offset = cigar.begin_offset as usize + offset; + let end_offset = cigar.end_offset as usize - offset; + + if begin_offset >= end_offset { + return cstr; + } + + let operations = + unsafe { std::slice::from_raw_parts(cigar.operations, cigar.max_operations as usize) }; + let mut last_op = operations[begin_offset]; + let mut last_op_length = 1; + + for i in 1..(end_offset - begin_offset) { + let cur_op = operations[begin_offset + i]; + if cur_op == last_op { + last_op_length += 1; + } else { + cstr.push_str(&format!("{}", last_op_length)); + cstr.push(last_op as u8 as char); + last_op = cur_op; + last_op_length = 1; + } + } + cstr.push_str(&format!("{}", last_op_length)); + cstr.push(last_op as u8 as char); + cstr + } + + pub fn matching( + &self, + pattern: &[u8], + text: &[u8], + flank_len: Option, + ) -> (String, String, String) { + let offset = flank_len.unwrap_or(0); + + let mut pattern_iter = pattern.iter().peekable(); + let mut text_iter = text.iter().peekable(); + + if offset > 0 { + text_iter.nth(offset - 1); + pattern_iter.nth(offset - 1); + } + + let mut pattern_alg = String::new(); + let mut ops_alg = String::new(); + let mut text_alg = String::new(); + + let cigar = unsafe { (*self.inner).cigar.as_ref() }.unwrap(); + let operations = + unsafe { std::slice::from_raw_parts(cigar.operations, cigar.max_operations as usize) }; + + let begin_offset = cigar.begin_offset as isize + offset as isize; + let end_offset = cigar.end_offset as isize - offset as isize; + + for i in begin_offset..end_offset { + match operations[i as usize] as u8 as char { + 'M' => { + if pattern_iter.peek() != text_iter.peek() { + ops_alg.push('X'); + } else { + ops_alg.push('|'); + } + pattern_alg.push(*pattern_iter.next().unwrap() as char); + text_alg.push(*text_iter.next().unwrap() as char); + } + 'X' => { + if pattern_iter.peek() != text_iter.peek() { + ops_alg.push(' '); + } else { + ops_alg.push('X'); + } + pattern_alg.push(*pattern_iter.next().unwrap() as char); + text_alg.push(*text_iter.next().unwrap() as char); + } + 'I' => { + pattern_alg.push('-'); + ops_alg.push(' '); + text_alg.push(*text_iter.next().unwrap() as char); + } + 'D' => { + pattern_alg.push(*pattern_iter.next().unwrap() as char); + ops_alg.push(' '); + text_alg.push('-'); + } + _ => panic!("Unknown cigar operation"), + } + } + (pattern_alg, ops_alg, text_alg) + } + + // TODO: Update this and other functions using cigar.operations to use cigar_buffer instead. + pub fn find_alignment_span(&self) -> ((usize, usize), (usize, usize)) { + let mut pattern_start: usize = 0; + let mut pattern_end: usize = 0; + let mut text_start: usize = 0; + let mut text_end: usize = 0; + + let mut pattern_index: usize = 0; + let mut text_index: usize = 0; + let mut is_span_started: bool = false; + + let cigar = unsafe { (*self.inner).cigar.as_ref() }.unwrap(); + let operations = + unsafe { std::slice::from_raw_parts(cigar.operations, cigar.max_operations as usize) }; + + for i in cigar.begin_offset..cigar.end_offset { + match operations[i as usize] as u8 as char { + 'I' => text_index += 1, + 'D' => pattern_index += 1, + 'M' => { + if !is_span_started { + pattern_start = pattern_index; + text_start = text_index; + is_span_started = true; + } + pattern_index += 1; + text_index += 1; + pattern_end = pattern_index; + text_end = text_index; + } + 'X' => { + pattern_index += 1; + text_index += 1; + } + _ => panic!("Unexpected operation"), + } + } + ((pattern_start, pattern_end), (text_start, text_end)) + } + + pub fn set_heuristic(&mut self, heuristic: Heuristic) { + unsafe { + match heuristic { + Heuristic::None => wfa2::wavefront_aligner_set_heuristic_none(self.inner), + Heuristic::WFadaptive( + min_wavefront_length, + max_distance_threshold, + steps_between_cutoffs, + ) => wfa2::wavefront_aligner_set_heuristic_wfadaptive( + self.inner, + min_wavefront_length, + max_distance_threshold, + steps_between_cutoffs, + ), + Heuristic::WFmash( + min_wavefront_length, + max_distance_threshold, + steps_between_cutoffs, + ) => wfa2::wavefront_aligner_set_heuristic_wfmash( + self.inner, + min_wavefront_length, + max_distance_threshold, + steps_between_cutoffs, + ), + Heuristic::XDrop(xdrop, steps_between_cutoffs) => { + wfa2::wavefront_aligner_set_heuristic_xdrop( + self.inner, + xdrop, + steps_between_cutoffs, + ) + } + Heuristic::ZDrop(zdrop, steps_between_cutoffs) => { + wfa2::wavefront_aligner_set_heuristic_zdrop( + self.inner, + zdrop, + steps_between_cutoffs, + ) + } + Heuristic::BandedStatic(band_min_k, band_max_k) => { + wfa2::wavefront_aligner_set_heuristic_banded_static( + self.inner, band_min_k, band_max_k, + ) + } + Heuristic::BandedAdaptive(band_min_k, band_max_k, steps_between_cutoffs) => { + wfa2::wavefront_aligner_set_heuristic_banded_adaptive( + self.inner, + band_min_k, + band_max_k, + steps_between_cutoffs, + ) + } + } + } + } +} + +/// Indel Aligner (a.k.a Longest Common Subsequence - LCS) +pub struct WFAlignerIndel; + +impl WFAlignerIndel { + pub fn new(alignment_scope: AlignmentScope, memory_model: MemoryModel) -> WFAligner { + let mut aligner = WFAligner::new(alignment_scope, memory_model); + aligner.attributes = aligner.attributes.indel_penalties(); + unsafe { + aligner.inner = wfa2::wavefront_aligner_new(&mut aligner.attributes.inner); + } + aligner + } +} + +/// Edit Aligner (a.k.a Levenshtein) +pub struct WFAlignerEdit; + +impl WFAlignerEdit { + pub fn new(alignment_scope: AlignmentScope, memory_model: MemoryModel) -> WFAligner { + let mut aligner = WFAligner::new(alignment_scope, memory_model); + aligner.attributes = aligner.attributes.edit_penalties(); + unsafe { + aligner.inner = wfa2::wavefront_aligner_new(&mut aligner.attributes.inner); + } + aligner + } +} + +/// Gap-Linear Aligner (a.k.a Needleman-Wunsch) +pub struct WFAlignerGapLinear; + +impl WFAlignerGapLinear { + pub fn new_with_match( + match_: i32, + mismatch: i32, + indel: i32, + alignment_scope: AlignmentScope, + memory_model: MemoryModel, + ) -> WFAligner { + let mut aligner = WFAligner::new(alignment_scope, memory_model); + aligner.attributes = WFAttributes::default() + .linear_penalties(match_, mismatch, indel) + .alignment_scope(alignment_scope) + .memory_model(memory_model); + unsafe { + aligner.inner = wfa2::wavefront_aligner_new(&mut aligner.attributes.inner); + } + aligner + } + + pub fn new( + mismatch: i32, + indel: i32, + alignment_scope: AlignmentScope, + memory_model: MemoryModel, + ) -> WFAligner { + Self::new_with_match(0, mismatch, indel, alignment_scope, memory_model) + } +} + +/// Gap-Affine Aligner (a.k.a Smith-Waterman-Gotoh) +pub struct WFAlignerGapAffine; + +impl WFAlignerGapAffine { + pub fn new_with_match( + match_: i32, + mismatch: i32, + gap_opening: i32, + gap_extension: i32, + alignment_scope: AlignmentScope, + memory_model: MemoryModel, + ) -> WFAligner { + let mut aligner = WFAligner::new(alignment_scope, memory_model); + aligner.attributes = WFAttributes::default() + .affine_penalties(match_, mismatch, gap_opening, gap_extension) + .alignment_scope(alignment_scope) + .memory_model(memory_model); + unsafe { + aligner.inner = wfa2::wavefront_aligner_new(&mut aligner.attributes.inner); + } + aligner + } + + pub fn new( + mismatch: i32, + gap_opening: i32, + gap_extension: i32, + alignment_scope: AlignmentScope, + memory_model: MemoryModel, + ) -> WFAligner { + Self::new_with_match( + 0, + mismatch, + gap_opening, + gap_extension, + alignment_scope, + memory_model, + ) + } +} + +/// Gap-Affine Dual-Cost Aligner (a.k.a. concave 2-pieces) +pub struct WFAlignerGapAffine2Pieces; + +impl WFAlignerGapAffine2Pieces { + pub fn new_with_match( + match_: i32, + mismatch: i32, + gap_opening1: i32, + gap_extension1: i32, + gap_opening2: i32, + gap_extension2: i32, + alignment_scope: AlignmentScope, + memory_model: MemoryModel, + ) -> WFAligner { + let mut aligner = WFAligner::new(alignment_scope, memory_model); + aligner.attributes = WFAttributes::default() + .affine2p_penalties( + match_, + mismatch, + gap_opening1, + gap_extension1, + gap_opening2, + gap_extension2, + ) + .alignment_scope(alignment_scope) + .memory_model(memory_model); + unsafe { + aligner.inner = wfa2::wavefront_aligner_new(&mut aligner.attributes.inner); + } + aligner + } + + pub fn new( + mismatch: i32, + gap_opening1: i32, + gap_extension1: i32, + gap_opening2: i32, + gap_extension2: i32, + alignment_scope: AlignmentScope, + memory_model: MemoryModel, + ) -> WFAligner { + Self::new_with_match( + 0, + mismatch, + gap_opening1, + gap_extension1, + gap_opening2, + gap_extension2, + alignment_scope, + memory_model, + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const PATTERN: &[u8] = b"AGCTAGTGTCAATGGCTACTTTTCAGGTCCT"; + const TEXT: &[u8] = b"AACTAAGTGTCGGTGGCTACTATATATCAGGTCCT"; + + #[test] + fn test_aligner_indel() { + let mut aligner = WFAlignerIndel::new(AlignmentScope::Alignment, MemoryModel::MemoryHigh); + let status = aligner.align_end_to_end(PATTERN, TEXT); + assert_eq!(status, AlignmentStatus::StatusAlgCompleted); + assert_eq!(aligner.score(), 10); + assert_eq!(aligner.cigar(None), "1M1I1D3M1I5M2I2D8M1I1M1I1M1I9M"); + let (a, b, c) = aligner.matching(PATTERN, TEXT, None); + assert_eq!( + format!("{}\n{}\n{}", a, b, c), + "A-GCTA-GTGTC--AATGGCTACT-T-T-TCAGGTCCT\n| ||| ||||| |||||||| | | |||||||||\nAA-CTAAGTGTCGG--TGGCTACTATATATCAGGTCCT" + ); + } + + #[test] + fn test_aligner_edit() { + let mut aligner = WFAlignerEdit::new(AlignmentScope::Alignment, MemoryModel::MemoryHigh); + let status = aligner.align_end_to_end(PATTERN, TEXT); + assert_eq!(status, AlignmentStatus::StatusAlgCompleted); + assert_eq!(aligner.score(), 7); + assert_eq!(aligner.cigar(None), "1M1X3M1I5M2X8M1I1M1I1M1I9M"); + let (a, b, c) = aligner.matching(PATTERN, TEXT, None); + assert_eq!( + format!("{}\n{}\n{}", a, b, c), + "AGCTA-GTGTCAATGGCTACT-T-T-TCAGGTCCT\n| ||| ||||| |||||||| | | |||||||||\nAACTAAGTGTCGGTGGCTACTATATATCAGGTCCT" + ); + } + + #[test] + fn test_aligner_gap_linear() { + let mut aligner = + WFAlignerGapLinear::new(6, 2, AlignmentScope::Alignment, MemoryModel::MemoryHigh); + let status = aligner.align_end_to_end(PATTERN, TEXT); + assert_eq!(status, AlignmentStatus::StatusAlgCompleted); + assert_eq!(aligner.score(), -20); + assert_eq!(aligner.cigar(None), "1M1I1D3M1I5M2I2D8M1I1M1I1M1I9M"); + let (a, b, c) = aligner.matching(PATTERN, TEXT, None); + assert_eq!( + format!("{}\n{}\n{}", a, b, c), + "A-GCTA-GTGTC--AATGGCTACT-T-T-TCAGGTCCT\n| ||| ||||| |||||||| | | |||||||||\nAA-CTAAGTGTCGG--TGGCTACTATATATCAGGTCCT" + ); + } + + #[test] + fn test_aligner_gap_affine() { + let mut aligner = + WFAlignerGapAffine::new(6, 4, 2, AlignmentScope::Alignment, MemoryModel::MemoryLow); + let status = aligner.align_end_to_end(PATTERN, TEXT); + assert_eq!(status, AlignmentStatus::StatusAlgCompleted); + assert_eq!(aligner.score(), -40); + assert_eq!(aligner.cigar(None), "1M1X3M1I5M2X8M3I1M1X9M"); + let (a, b, c) = aligner.matching(PATTERN, TEXT, None); + assert_eq!( + format!("{}\n{}\n{}", a, b, c), + "AGCTA-GTGTCAATGGCTACT---TTTCAGGTCCT\n| ||| ||||| |||||||| | |||||||||\nAACTAAGTGTCGGTGGCTACTATATATCAGGTCCT" + ); + } + + #[test] + fn test_aligner_score_only() { + let mut aligner = + WFAlignerGapAffine::new(6, 4, 2, AlignmentScope::Score, MemoryModel::MemoryLow); + let status = aligner.align_end_to_end(PATTERN, TEXT); + assert_eq!(status, AlignmentStatus::StatusAlgCompleted); + assert_eq!(aligner.score(), -40); + assert_eq!(aligner.cigar(None), ""); + let (a, b, c) = aligner.matching(PATTERN, TEXT, None); + assert_eq!(format!("{}\n{}\n{}", a, b, c), "\n\n"); + } + + #[test] + fn test_aligner_gap_affine_2pieces() { + let mut aligner = WFAlignerGapAffine2Pieces::new( + 6, + 2, + 2, + 4, + 1, + AlignmentScope::Alignment, + MemoryModel::MemoryHigh, + ); + let status = aligner.align_end_to_end(PATTERN, TEXT); + assert_eq!(status, AlignmentStatus::StatusAlgCompleted); + assert_eq!(aligner.score(), -34); + assert_eq!(aligner.cigar(None), "1M1X3M1I5M2X8M1I1M1I1M1I9M"); + let (a, b, c) = aligner.matching(PATTERN, TEXT, None); + assert_eq!( + format!("{}\n{}\n{}", a, b, c), + "AGCTA-GTGTCAATGGCTACT-T-T-TCAGGTCCT\n| ||| ||||| |||||||| | | |||||||||\nAACTAAGTGTCGGTGGCTACTATATATCAGGTCCT" + ); + } + + #[test] + fn test_aligner_span_1() { + let pattern = b"AATTTAAGTCTAGGCTACTTTC"; + let text = b"CCGACTACTACGAAATTTAAGTATAGGCTACTTTCCGTACGTACGTACGT"; + let mut aligner = WFAlignerGapAffine2Pieces::new( + 8, + 4, + 2, + 24, + 1, + AlignmentScope::Alignment, + MemoryModel::MemoryHigh, + ); + let status = aligner.align_ends_free(pattern, 0, 0, text, 0, text.len() as i32); + assert_eq!(status, AlignmentStatus::StatusAlgCompleted); + let ((xstart, xend), (ystart, yend)) = aligner.find_alignment_span(); + assert_eq!(ystart, 13); + assert_eq!(yend, 35); + assert_eq!(xstart, 0); + assert_eq!(xend, 22); + } + + #[test] + fn test_aligner_span_2() { + let pattern = b"GGGATCCCCGAAAAAGCGGGTTTGGCAAAAGCAAATTTCCCGAGTAAGCAGGCAGAGATCGCGCCAGACGCTCCCCAGAGCAGGGCGTCATGCACAAGAAAGCTTTGCACTTTGCGAACCAACGATAGGTGGGGGTGCGTGGAGGATGGAACACGGACGGCCCGGCTTGCTGCCTTCCCAGGCCTGCAGTTTGCCCATCCACGTCAGGGCCTCAGCCTGGCCGAAAGAAAGAAATGGTCTGTGATCCCCC"; + let text = b"AGCAGGGCGTCATGCACAAGAAAGCTTTGCACTTTGCGAACCAACGATAGGTGGGGGTGCGTGGAGGATGGAACACGGACGGCCCGGCTTGCTGCCTTCCCAGGCCTGCAGTTTGCCCATCCACGTCAGGGCCTCAGCCTGGCCGAAAGAAAGAAATGGTCTGTGATCCCCCCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCATTCCCGGCTACAAGGACCCTTCGAGCCCCGTTCGCCGGCCGCGGACCCGGCCCCTCCCTCCCCGGCCGCTAGGGGGCGGGCCCGGATCACAGGACTGGAGCTGGGCGGAGACCCACGCTCGGAGCGGTTGTGAACTGGCAGGCGGTGGGCGCGGCTTCTGTGCCGTGCCCCGGGCACTCAGTCTTCCAACGGGGCCCCGGAGTCGAAGACAGTTCTAGGGTTCAGGGAGCGCGGGCGGCTCCTGGGCGGCGCCAGACTGCGGTGAGTTGGCCGGCGTGGGCCACCAACCCAATGCAGCCCAGGGCGGCGGCACGAGACAGAACAACGGCGAACAGGAGCAGGGAAAGCGCCTCCGATAGGCCAGGCCTAGGGACCTGCGGGGAGAGGGCGAGGTCAACACCCGGCATGGGCCTCTGATTGGCTCCTGGGACTCGCCCCGCCTACGCCCATAGGTGGGCCCGCACTCTTCCCTGCGCCCCGCCCCCGCCCCAACAGCCT"; + let mut aligner = WFAlignerGapAffine2Pieces::new( + 8, + 4, + 2, + 24, + 1, + AlignmentScope::Alignment, + MemoryModel::MemoryHigh, + ); + aligner.set_heuristic(Heuristic::None); + let status = aligner.align_ends_free(pattern, 0, 0, text, 0, text.len() as i32); + assert_eq!(status, AlignmentStatus::StatusAlgCompleted); + let ((xstart, xend), (ystart, yend)) = aligner.find_alignment_span(); + + assert_eq!(ystart, 0); + assert_eq!(yend, 172); + assert_eq!(xstart, 78); + assert_eq!(xend, 250); + } + + #[test] + fn test_aligner_ends_free_global() { + let pattern = b"AATTTAAGTCTAGGCTACTTTC"; + let text = b"CCGACTACTACGAAATTTAAGTATAGGCTACTTTCCGTACGTACGTACGT"; + let mut aligner = + WFAlignerGapAffine::new(6, 4, 2, AlignmentScope::Alignment, MemoryModel::MemoryHigh); + let status = aligner.align_ends_free(pattern, 0, 0, text, 0, text.len() as i32); + assert_eq!(status, AlignmentStatus::StatusAlgCompleted); + assert_eq!(aligner.score(), -36); + assert_eq!(aligner.cigar(None), "13I9M1X12M15I"); + let (a, b, c) = aligner.matching(pattern, text, None); + assert_eq!( + format!("{}\n{}\n{}", a, b, c), + "-------------AATTTAAGTCTAGGCTACTTTC---------------\n ||||||||| |||||||||||| \nCCGACTACTACGAAATTTAAGTATAGGCTACTTTCCGTACGTACGTACGT" + ); + } + + #[test] + fn test_aligner_ends_free_right_extent() { + let pattern = b"AATTTAAGTCTGCTACTTTCACGCAGCT"; + let text = b"AATTTCAGTCTGGCTACTTTCACGTACGATGACAGACTCT"; + let mut aligner = + WFAlignerGapAffine::new(6, 4, 2, AlignmentScope::Alignment, MemoryModel::MemoryHigh); + let status = + aligner.align_ends_free(pattern, 0, pattern.len() as i32, text, 0, text.len() as i32); + assert_eq!(status, AlignmentStatus::StatusAlgCompleted); + assert_eq!(aligner.score(), -24); + assert_eq!(aligner.cigar(None), "5M1X6M1I11M4D1M15I"); + let (a, b, c) = aligner.matching(pattern, text, None); + assert_eq!( + format!("{}\n{}\n{}", a, b, c), + "AATTTAAGTCTG-CTACTTTCACGCAGCT---------------\n||||| |||||| ||||||||||| | \nAATTTCAGTCTGGCTACTTTCACG----TACGATGACAGACTCT" + ); + } + + #[test] + fn test_aligner_ends_free_left_extent() { + let pattern = b"CTTTCACGTACGTGACAGTCTCT"; + let text = b"AATTTCAGTCTGGCTACTTTCACGTACGATGACAGACTCT"; + let mut aligner = + WFAlignerGapAffine::new(6, 4, 2, AlignmentScope::Alignment, MemoryModel::MemoryHigh); + let status = aligner.align_ends_free(pattern, 0, 0, text, 0, 0); + assert_eq!(status, AlignmentStatus::StatusAlgCompleted); + assert_eq!(aligner.score(), -48); + assert_eq!(aligner.cigar(None), "16I12M1I6M1X4M"); + let (a, b, c) = aligner.matching(pattern, text, None); + assert_eq!( + format!("{}\n{}\n{}", a, b, c), + "----------------CTTTCACGTACG-TGACAGTCTCT\n |||||||||||| |||||| ||||\nAATTTCAGTCTGGCTACTTTCACGTACGATGACAGACTCT" + ); + } + + #[test] + fn test_aligner_ends_free_right_overlap() { + let pattern = b"CGCGTCTGACTGACTGACTAAACTTTCATGTACCTGACA"; + let text = b"AAACTTTCACGTACGTGACATATAGCGATCGATGACT"; + let mut aligner = + WFAlignerGapAffine::new(6, 4, 2, AlignmentScope::Alignment, MemoryModel::MemoryHigh); + let status = aligner.align_ends_free(pattern, 0, 0, text, 0, 0); + assert_eq!(status, AlignmentStatus::StatusAlgCompleted); + assert_eq!(aligner.score(), -92); + assert_eq!(aligner.cigar(None), "19D9M1X4M1X5M17I"); + let (a, b, c) = aligner.matching(pattern, text, None); + assert_eq!( + format!("{}\n{}\n{}", a, b, c), + "CGCGTCTGACTGACTGACTAAACTTTCATGTACCTGACA-----------------\n ||||||||| |||| ||||| \n-------------------AAACTTTCACGTACGTGACATATAGCGATCGATGACT" + ); + } + + #[test] + fn test_clipping_score() { + let text_lf = b"AAGGAGCTGAGAATTGTTCTTCCAGATACCTTTCCGACCTCTTCTTGGTT"; + let text_rf = b"GGAGTGCAGTGGTGCAATCTTGGCTCACTACAACCTCCGCATCCTGGGTT"; + + let pattern_lf = b"AAGGAGCTGAGAATTGTTCGTCCAGATACCTTTCCGACCTCTTCTTGGTT"; + let pattern_rf = b"GGAGTGCAGTGGTGCAATCTTGGCTCACTACAACCTCTGCATCCTGGGTT"; + + let motif = b"ATTT"; + + let text = [text_lf, &motif.repeat(10)[..], text_rf].concat(); + let pattern = [pattern_lf, &motif.repeat(8)[..], pattern_rf].concat(); + + let mut aligner = WFAlignerGapAffine2Pieces::new( + 8, + 4, + 2, + 24, + 1, + AlignmentScope::Alignment, + MemoryModel::MemoryHigh, + ); + let status = aligner.align_end_to_end(&pattern, &text); + + assert_eq!(status, AlignmentStatus::StatusAlgCompleted); + assert_eq!(aligner.score(), -36); + assert_eq!(aligner.cigar(None), "19M1X62M8I37M1X12M"); + let (a, b, c) = aligner.matching(&pattern, &text, None); + assert_eq!( + format!("{}\n{}\n{}", a, b, c), + "AAGGAGCTGAGAATTGTTCGTCCAGATACCTTTCCGACCTCTTCTTGGTTATTTATTTATTTATTTATTTATTTATTTATTT--------GGAGTGCAGTGGTGCAATCTTGGCTCACTACAACCTCTGCATCCTGGGTT\n||||||||||||||||||| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ||||||||||||||||||||||||||||||||||||| ||||||||||||\nAAGGAGCTGAGAATTGTTCTTCCAGATACCTTTCCGACCTCTTCTTGGTTATTTATTTATTTATTTATTTATTTATTTATTTATTTATTTGGAGTGCAGTGGTGCAATCTTGGCTCACTACAACCTCCGCATCCTGGGTT" + ); + assert_eq!(aligner.cigar_score(), -36); + assert_eq!(aligner.cigar_score_clipped(50), -20); + assert_eq!(aligner.cigar(Some(50)), "32M8I"); + let (a, b, c) = aligner.matching(&pattern, &text, Some(50)); + assert_eq!( + format!("{}\n{}\n{}", a, b, c), + "ATTTATTTATTTATTTATTTATTTATTTATTT--------\n|||||||||||||||||||||||||||||||| \nATTTATTTATTTATTTATTTATTTATTTATTTATTTATTT" + ); + } + + #[test] + fn test_memory_modes() { + let expected_cigar = "1M1X3M1I5M2X8M3I1M1X9M"; + let expected_matching = "AGCTA-GTGTCAATGGCTACT---TTTCAGGTCCT\n| ||| ||||| |||||||| | |||||||||\nAACTAAGTGTCGGTGGCTACTATATATCAGGTCCT"; + let expected_score = -48; + + struct Test { + memory_mode: MemoryModel, + } + + let tests = vec![ + Test { + memory_mode: MemoryModel::MemoryHigh, + }, + Test { + memory_mode: MemoryModel::MemoryMed, + }, + Test { + memory_mode: MemoryModel::MemoryLow, + }, + // Test { + // memory_mode: MemoryModel::MemoryUltraLow, + // }, + ]; + + for test in tests { + let mut aligner = WFAlignerGapAffine2Pieces::new( + 8, + 4, + 2, + 24, + 1, + AlignmentScope::Alignment, + test.memory_mode, + ); + let status = aligner.align_end_to_end(PATTERN, TEXT); + assert_eq!(status, AlignmentStatus::StatusAlgCompleted); + assert_eq!(aligner.score(), expected_score); + assert_eq!(aligner.cigar_score(), expected_score); + assert_eq!(aligner.cigar_score_clipped(0), expected_score); + assert_eq!(aligner.cigar(None), expected_cigar); + let (a, b, c) = aligner.matching(PATTERN, TEXT, None); + assert_eq!(format!("{}\n{}\n{}", a, b, c), expected_matching); + } + } + + #[test] + fn test_set_heuristic() { + let mut aligner = + WFAlignerGapAffine::new(6, 4, 2, AlignmentScope::Alignment, MemoryModel::MemoryHigh); + aligner.set_heuristic(Heuristic::WFmash(1, 2, 3)); + aligner.set_heuristic(Heuristic::BandedStatic(1, 2)); + aligner.set_heuristic(Heuristic::BandedAdaptive(1, 2, 3)); + aligner.set_heuristic(Heuristic::WFadaptive(1, 2, 3)); + aligner.set_heuristic(Heuristic::XDrop(1, 2)); + aligner.set_heuristic(Heuristic::ZDrop(1, 2)); + aligner.set_heuristic(Heuristic::None); + } + + #[test] + fn test_invalid_sequence() { + let read = b"GCTGCTACTGGGGTGTCCCCTCTCAAAGGACAAACCCAGGATCTACAGATGTGTGTGCTAAGCCATGTATGCACATGCACGTGTGTGTGTATATATTTAACCTATCTGTATATATGTATTATGTAAACATGAGTTCCTGCTGGCATATCTGACTATAACTGACCACCTCAGGGTCCATTCTGATCTGTATATATGTATCATGTAAACATGAGTTCCCTGCTGGCATATCTGACTATAACTGACCACCTCACAGTCCATTCTGATCTCTATATATGTATTATGTAAACATGAGTTCCTACTGGCATATCTGACTATAACTGACCACCTCAGGGTCCATTCTGATCTGTATATATGTATTATGTAAACATGAGTTCCCTGCTGGCATATCTGATTATAACTGACCACCTCAGGGTCCATTCTGATCTGTATATATGTATTATGTAAACATGAGTTCCTACTGGCATATCTGACTATAACCGACCACCTCAGGGTCCATTCTGATCTGTATATATGTATCATGTAAACACGAGTTCCTACTGGCATATCTGACTATAACTGACCACCTCAGGGTCCATTCTGATCTGTATATATGTATCATGTAAACACGAGTTCCTGCTGGCATATCTGACTATAACTGACCACCTCAGGGTCCATTCTGATCTGTATATATGTATAATATATATTATATATGGACCTCAGGGTCCATTCTGATCTGTATATATGTATCATGTAAACATGAGTTCCTGCTGGCATATCTGACTATAACTGACCACCTCAGGGTCTATTCTGATCTGTATATATGTATAATATATATTATATATGGACCTCAGGGTCCATTCTGATCTGTATATATGTATCATGTAAACATGAGTTCCTGCTGGCATATCTGATTATAACCGACCACCTCAGGGTCCATTCTGATCTGTATATATGTATTATGTAAACATGAGTTCCTACTGGCATATCTGACTATAACCGACCACCTCAGGGTCCATTCTGATCTGTATATATGTATCATGTAAACATGAGTTCCTACTGGCATATCTGACTATAACTGACCACCTCAGGGTCCATTCTGATCTGTATATATGTATCATGTAAACATGAGTTCCTACTGGCATATCTGACTATAACTGACCACCTCAGGATCCATTCTGATCTGTATATATGTATAATATATATTATATATGGACCTCAGGGTCCATTCTGATCTGTATATATGTATAATATATATTATATATGGACCTCAGGGTCCATTCTGATCTGTATATATGTATCATGTAAACATGAGTTCCTGGCTGGCATATCTGATTATAACCGACCACCTCAGGGTCCATTCTGATCTGTATATATGTATCATGTAAACACGAGTTCCTACTGGCATATCTGACTATAACTGACCACCTCAGGGTCCATTCTGATCTGTATATATGTATCATGTAAACACGAGTTCCTGCTGGCATATCTGATTATAACCGACCACCTCAGGGTCCATTCTGATCTGTATATATGTATAATATATATTATATATGGACCTCAGGGTCCCCGCTGGCTTTTCCATGACTTCCTTATCCAGCTGTGAGAACCCTGACTCTTACTACCCATACTGTATTGACTTATTT"; + let allele = b"GCTGCTACTGGGGTGTCCCCTCTCAAAGGACAAACCCAGGATCTACAGATGTGTGTGCTAAGCCATGTATGCACACGCACGTGTGTGTGTATATATTTAACCTATCTGTATATATGTATTATGTAAACATGAGTTCCTGCTGGCATATCTGACTATAACTGACCACCTCAGGGTCCATTCTGATCTGTATATATGTATCATGTAAACACGACTTCCTACTGGCATATCTGACTGTAACCGACCACCTCAGGGTCCATTCTGATCTGTATATATGTATCATGTAAACATGATTTCCTACTGGCATATCTGACTATAACTGACCACCTCAGGGTTCATTCCGATCTGTATATAAGTATCATGTAAACACGAGTTCCTGCTGGCATATCTGACTGTAACCGACCACCTCAGGGTCCATTCTGATCTGTATATATGTATCATGTAAACACGAGTTCCTGCTGGCATATCTGACTATAACCGACCACCTCAGGGTCCATTCTGATCTGTATATATGTATCATGTAAACATGAGTTCCTACTGGCATATCTGACTATAACTGACCACCTCAGGGTCCATTCTGATCTGTATGTATGTATCATGTAAACACGAGTTCCTACTGGCATATCTGACTATAACTGACCACCTCAGGGTCCATTCCGATCTGTATATAAGTATCATGTAAACACGAGTTCCTGCTGGCATATCTGACTGTAACCGACCACCTCAGGGTCCATTCTGATCTGTATATATGTATCATGTAAACACGAGTTCCTGCTGGCATATCTGACTATAACTGACCACCTCAGGGTCCATTCTGATCTGTATATATGTATAATATATATTATATATGGACCTCAGGGTCCATTCTGATCTGCATATATGTATAATATATATTATATATGGACCTCAGGGTCCATTCTGATCTGTATATATGTATCATGTAAACATGAGTTCCTGCTGGCATATCTGTCTATAACCGACCACCTTAGGGTCCATTCTGATCTGTATATATGTATAATATATATTATATATGGTCCTCAGGGTCCATTCTGATCTGTATATATGTATCATGTAAACATGAGTTCCTGCTGGCATATCTGTCTATAACCGACCACCTTAGGGTCCATTCTGATCTGTATATATGTATAATATATATTATATATGGACCTCAGGGTCCATTCTGATCTGCATATATGTATAATATATATTATATATGGTCCTCAGGGTCCATTCTGATCTGTATATATGTATCATGTAAACATGAGTTCCTGCTGGCATATCTGTCTATAACCGACCACCTTAGGGTCCATTCTGATCTGTATATATGTATAATATATATTATATATGGACCTCAGGGTCCCCGCTGGCTTTTCCATGACTTCCTTATCCAGCTGTGAGAACCCTGACTCTTACTACTGTATTGACTTATTTGTGAAACCT"; + + let mut aligner = WFAlignerGapAffine2Pieces::new( + 8, + 4, + 2, + 24, + 1, + AlignmentScope::Alignment, + MemoryModel::MemoryUltraLow, + ); + let _status = aligner.align_end_to_end(read, allele); + assert_eq!(_status, AlignmentStatus::StatusUnattainable); + assert_eq!(aligner.score(), -2147483648); + + aligner.set_heuristic(Heuristic::None); + let _status = aligner.align_end_to_end(read, allele); + assert_eq!(_status, AlignmentStatus::StatusAlgCompleted); + assert_eq!(aligner.score(), -881); + } +} diff --git a/src/allele.rs b/src/allele.rs new file mode 100644 index 0000000..5e38e76 --- /dev/null +++ b/src/allele.rs @@ -0,0 +1,409 @@ +use crate::aligner::{AlignmentStatus, WFAligner}; +use crate::denovo::{self, AlleleOrigin, DenovoStatus}; +use crate::handles; +use crate::locus::Locus; +use crate::read::ReadInfo; +use crate::snp::{self, TrinaryMatrix}; +use crate::stats; +use crate::util::Result; +use anyhow::anyhow; +use itertools::Itertools; +use log; +use noodles::{ + bam, + bgzf::Reader, + sam, + vcf::{ + self, + record::genotypes::{self, sample::value::Genotype}, + record::info::field, + Record, + }, +}; +use once_cell::sync::Lazy; +use serde::Serialize; +use std::{ + fs::File, + sync::{Arc, Mutex}, +}; + +#[derive(Debug, PartialEq)] +pub struct Allele { + pub seq: Vec, + pub genotype: usize, + pub read_aligns: Vec<(ReadInfo, i32)>, + pub motif_count: String, + pub index: usize, +} + +impl Allele { + pub fn dummy(reads: Vec) -> Allele { + Allele { + seq: vec![], + genotype: 0, + read_aligns: reads.into_iter().map(|info| (info, 0i32)).collect_vec(), + motif_count: String::from("0"), + index: 0, + } + } +} + +pub static FILTERS: &[&'static (dyn snp::ReadFilter + Sync)] = + &[&snp::FilterByDist, &snp::FilterByFreq]; + +pub fn load_alleles( + locus: &Locus, + subhandle: &handles::SubHandle, + clip_len: usize, + aligner: &mut WFAligner, +) -> Result> { + let (allele_seqs, motif_counts, genotype_indices) = + get_allele_seqs(locus, &subhandle.vcf, &subhandle.vcf_header)?; + let mut reads = get_reads(&subhandle.bam, &subhandle.bam_header, locus)?; + + snp::apply_read_filters(&mut reads, FILTERS); + + let reads_by_allele = assign_reads(&allele_seqs, reads, clip_len, aligner); + let alleles = allele_seqs + .into_iter() + .enumerate() + .map(|(index, allele_seq)| Allele { + seq: allele_seq, + genotype: genotype_indices[index], + read_aligns: reads_by_allele[index].to_owned(), + motif_count: motif_counts[index].to_owned(), + index, + }) + .collect(); + Ok(alleles) +} + +fn assign_reads( + alleles: &[Vec], + reads: Vec, + clip_len: usize, + aligner: &mut WFAligner, +) -> Vec> { + let mut reads_by_allele = vec![Vec::new(); alleles.len()]; + let mut index_flip: usize = 0; + for read in reads { + let mut max_score = None; + let mut max_aligns = Vec::new(); + for (i, a) in alleles.iter().enumerate() { + if let AlignmentStatus::StatusAlgCompleted = aligner.align_end_to_end(&read.bases, a) { + let score = aligner.cigar_score_clipped(clip_len); + match max_score { + None => { + max_score = Some(score); + max_aligns = vec![(i, score)]; + } + Some(max) if score > max => { + max_score = Some(score); + max_aligns = vec![(i, score)]; + } + Some(max) if score == max => { + max_aligns.push((i, score)); + } + _ => (), + } + } + } + if !max_aligns.is_empty() { + if max_aligns.len() > 1 { + index_flip = (index_flip + 1) % max_aligns.len(); + } + let (allele_index, align) = max_aligns[index_flip % max_aligns.len()]; + reads_by_allele[allele_index].push((read, align)); + } + } + reads_by_allele +} + +pub fn get_reads( + bam: &Arc>>>, + bam_header: &Arc, + locus: &Locus, +) -> Result> { + let mut bam = bam + .lock() + .map_err(|e| anyhow!("Failed to acquire lock: {}", e))?; + let query = bam.query(bam_header, &locus.region)?; + + let reads: Vec<_> = query + .map(|record| record.map_err(|e| e.into()).map(ReadInfo::new)) + .collect::>>()?; + + Ok(reads) +} + +// TODO: improve, for now it just pulls out the genotype index and checks duplicates +fn is_homozygous(genotypes: &Genotype) -> bool { + let alleles: Vec<_> = genotypes + .iter() + .filter_map(|allele| allele.position()) + .collect(); + alleles.into_iter().unique().count() == 1 +} + +static TRID_KEY: Lazy = Lazy::new(|| "TRID".parse().unwrap()); +static MC_KEY: Lazy = Lazy::new(|| "MC".parse().unwrap()); + +fn get_allele_seqs( + locus: &Locus, + vcf: &Arc>>, + vcf_header: &Arc, +) -> Result<(Vec>, Vec, Vec)> { + let mut vcf = vcf + .lock() + .map_err(|_| anyhow!("Error locking Mutex for vcf::IndexedReader"))?; + + let query = vcf.query(vcf_header, &locus.region)?; + let locus_id = &locus.id; + + if let Some(result) = query.into_iter().next() { + let record = result?; + let info = record.info(); + + let trid = info.get(&*TRID_KEY).unwrap().unwrap().to_string(); + if &trid != locus_id { + return Err(anyhow!("TRID={} missing", locus_id)); + } + + let format = record.genotypes().get_index(0).unwrap(); + let genotype = format.genotype().unwrap(); + if genotype.is_err() { + return Err(anyhow!("TRID={} misses genotyping", locus_id)); + } + let genotype = genotype.unwrap(); + + let mc_field = format.get(&*MC_KEY).unwrap().unwrap().to_string(); + let motif_counts: Vec = mc_field.split(',').map(ToString::to_string).collect(); + + let (alleles, genotype_indices) = process_genotypes(&genotype, &record, locus)?; + return Ok((alleles, motif_counts, genotype_indices)); + } + Err(anyhow!("TRID={} missing", &locus.id)) +} + +fn process_genotypes( + genotype: &Genotype, + record: &Record, + locus: &Locus, +) -> Result<(Vec>, Vec)> { + let reference_bases = record.reference_bases().to_string().into_bytes(); + let alternate_bases = record + .alternate_bases() + .iter() + .map(|base| base.to_string().into_bytes()) + .collect::>(); + let mut seq = locus.left_flank.clone(); + let mut alleles = Vec::new(); + let mut genotype_indices = Vec::new(); + + for allele in genotype.iter() { + let allele_index: usize = allele + .position() + .ok_or_else(|| anyhow!("Allele position missing for TRID={}", locus.id))?; + match allele_index { + 0 => seq.extend_from_slice(&reference_bases), + _ => seq.extend_from_slice( + alternate_bases + .get(allele_index - 1) + .ok_or_else(|| anyhow!("Invalid allele index for TRID={}", locus.id))?, + ), + } + seq.extend_from_slice(&locus.right_flank); + alleles.push(seq.clone()); + seq.truncate(locus.left_flank.len()); // reset the sequence for the next allele + + genotype_indices.push(allele_index); + + if genotype.len() > 1 && is_homozygous(genotype) { + break; + } + } + Ok((alleles, genotype_indices)) +} + +#[derive(Debug, PartialEq, Serialize)] +pub struct AlleleResult { + pub trid: String, + pub genotype: usize, + pub denovo_coverage: usize, + pub allele_coverage: usize, + #[serde(serialize_with = "serialize_with_precision")] + pub allele_ratio: f64, + pub child_coverage: usize, + #[serde(serialize_with = "serialize_with_precision")] + pub child_ratio: f64, + #[serde(serialize_with = "serialize_with_precision")] + pub mean_diff_father: f32, + #[serde(serialize_with = "serialize_with_precision")] + pub mean_diff_mother: f32, + #[serde(serialize_with = "serialize_with_precision")] + pub father_dropout_prob: f64, + #[serde(serialize_with = "serialize_with_precision")] + pub mother_dropout_prob: f64, + #[serde(serialize_with = "serialize_as_display")] + pub allele_origin: AlleleOrigin, + #[serde(serialize_with = "serialize_as_display")] + pub denovo_status: DenovoStatus, + pub per_allele_reads_father: String, + pub per_allele_reads_mother: String, + pub per_allele_reads_child: String, + pub index: usize, + pub father_motif_counts: String, + pub mother_motif_counts: String, + pub child_motif_counts: String, + #[serde(serialize_with = "serialize_with_precision")] + pub maxlh: f64, +} + +fn serialize_as_display( + value: &T, + serializer: S, +) -> std::result::Result { + serializer.collect_str(value) +} + +fn serialize_with_precision( + value: &T, + serializer: S, +) -> std::result::Result { + let formatted_value = format!("{:.4}", value); + serializer.serialize_str(&formatted_value) +} + +fn load_alleles_handle( + role: char, + locus: &Locus, + subhandle: &handles::SubHandle, + clip_len: usize, + aligner: &mut WFAligner, +) -> Result> { + load_alleles(locus, subhandle, clip_len, aligner).map_err(|err| { + log::warn!("Skipping {} in {}", err, role); + err + }) +} + +pub fn process_alleles( + locus: &Locus, + handle: Arc, + clip_len: usize, + parent_quantile: f64, + aligner: &mut WFAligner, +) -> Result> { + let father_alleles = load_alleles_handle('F', locus, &handle.father, clip_len, aligner)?; + let mother_alleles = load_alleles_handle('M', locus, &handle.mother, clip_len, aligner)?; + let child_alleles = load_alleles_handle('C', locus, &handle.child, clip_len, aligner)?; + + // TODO: ongoing work, the maximum likelihood is obtained naively + let max_lhs = TrinaryMatrix::new(&child_alleles, &father_alleles, &mother_alleles) + .and_then(|trinary_mat| snp::inheritance_prob(&trinary_mat)) + .map(|(_inherit_p, max_lh)| max_lh) + .unwrap_or((-1.0, -1.0)); + let max_lhs = vec![max_lhs.0, max_lhs.1]; + + let mother_dropout_prob = stats::get_dropout_prob(&mother_alleles); + let father_dropout_prob = stats::get_dropout_prob(&father_alleles); + + let father_reads = stats::get_per_allele_reads(&father_alleles); + let mother_reads = stats::get_per_allele_reads(&mother_alleles); + let child_reads = stats::get_per_allele_reads(&child_alleles); + + let father_motifs = father_alleles + .iter() + .map(|a| a.motif_count.to_string()) + .collect::>() + .join(","); + let mother_motifs = mother_alleles + .iter() + .map(|a| a.motif_count.to_string()) + .collect::>() + .join(","); + let child_motifs = child_alleles + .iter() + .map(|a| a.motif_count.to_string()) + .collect::>() + .join(","); + + let mut out_vec = Vec::::new(); + for (i, dna) in denovo::assess_denovo( + &mother_alleles, + &father_alleles, + &child_alleles, + clip_len, + parent_quantile, + aligner, + ) + .enumerate() + { + let output = AlleleResult { + trid: locus.id.clone(), + genotype: dna.genotype, + denovo_coverage: dna.denovo_score, + allele_coverage: dna.allele_coverage, + allele_ratio: dna.denovo_score as f64 / dna.allele_coverage as f64, + child_coverage: dna.child_coverage, + child_ratio: dna.denovo_score as f64 / dna.child_coverage as f64, + mean_diff_father: dna.mean_diff_father, + mean_diff_mother: dna.mean_diff_mother, + father_dropout_prob, + mother_dropout_prob, + allele_origin: dna.allele_origin, + denovo_status: dna.denovo_status, + per_allele_reads_father: father_reads + .iter() + .map(|a| a.to_string()) + .collect::>() + .join(","), + per_allele_reads_mother: mother_reads + .iter() + .map(|a| a.to_string()) + .collect::>() + .join(","), + per_allele_reads_child: child_reads + .iter() + .map(|a| a.to_string()) + .collect::>() + .join(","), + index: dna.index, + father_motif_counts: father_motifs.clone(), + mother_motif_counts: mother_motifs.clone(), + child_motif_counts: child_motifs.clone(), + maxlh: max_lhs[i], + }; + out_vec.push(output); + } + Ok(out_vec) +} + +// #[cfg(test)] +// mod tests { +// use super::*; + +// #[test] +// fn test_is_homozygous() { +// let mut header = Header::new(); +// let header_contig_line = r#"##contig="#; +// header.push_record(header_contig_line.as_bytes()); +// let header_gt_line = r#"##FORMAT="#; +// header.push_record(header_gt_line.as_bytes()); +// header.push_sample("test_sample".as_bytes()); +// let vcf = Writer::from_stdout(&header, true, Format::Vcf).unwrap(); +// let mut record = vcf.empty_record(); + +// let alleles = &[GenotypeAllele::Unphased(0), GenotypeAllele::Phased(0)]; +// record.push_genotypes(alleles).unwrap(); +// let genotypes = record.genotypes().unwrap().get(0); +// // assert!(is_homozygous(&genotypes)); + +// record.clear(); + +// let alleles = &[GenotypeAllele::Unphased(2), GenotypeAllele::Phased(1)]; +// record.push_genotypes(alleles).unwrap(); +// let genotypes = record.genotypes().unwrap().get(0); +// // assert!(!is_homozygous(&genotypes)); +// } +// } diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..cdcb922 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,206 @@ +use crate::util::Result; +use anyhow::anyhow; +use chrono::Datelike; +use clap::{ArgAction, ArgGroup, Parser, Subcommand}; +use env_logger::fmt::Color; +use log::{Level, LevelFilter}; +use once_cell::sync::Lazy; +use std::{ + io::Write, + path::{Path, PathBuf}, +}; + +pub static FULL_VERSION: Lazy = Lazy::new(|| { + format!( + "{}-{}", + env!("CARGO_PKG_VERSION"), + env!("VERGEN_GIT_DESCRIBE") + ) +}); + +#[derive(Parser)] +#[command(name="trgt-denovo", + author="Tom Mokveld \nEgor Dolzhenko ", + version=&**FULL_VERSION, + about="Tandem repeat de novo caller", + long_about = None, + after_help = format!("Copyright (C) 2004-{} Pacific Biosciences of California, Inc. + This program comes with ABSOLUTELY NO WARRANTY; it is intended for + Research Use Only and not for use in diagnostic procedures.", chrono::Utc::now().year()), + help_template = "{name} {version}\n{author}{about-section}\n{usage-heading}\n {usage}\n\n{all-args}{after-help}", + )] +pub struct Cli { + #[command(subcommand)] + pub command: Command, + + #[clap(short = 'v')] + #[clap(long = "verbose")] + #[clap(action = ArgAction::Count)] + pub verbosity: u8, +} + +#[derive(Subcommand)] +pub enum Command { + Trio(TrioArgs), +} + +#[derive(Parser, Debug)] +#[command(group(ArgGroup::new("trio")))] +#[command(arg_required_else_help(true))] +pub struct TrioArgs { + #[clap(required = true)] + #[clap(short = 'r')] + #[clap(long = "reference")] + #[clap(help = "Path to reference genome FASTA")] + #[clap(value_name = "FASTA")] + #[arg(value_parser = check_file_exists)] + pub reference_filename: PathBuf, + + #[clap(required = true)] + #[clap(short = 'b')] + #[clap(long = "bed")] + #[clap(help = "BED file with repeat coordinates")] + #[clap(value_name = "BED")] + #[arg(value_parser = check_file_exists)] + pub bed_filename: PathBuf, + + #[clap(required = true)] + #[clap(short = 'm')] + #[clap(long = "mother")] + #[clap(help = "Prefix of mother VCF and spanning reads BAM files")] + #[clap(value_name = "PREFIX")] + #[arg(value_parser = check_prefix_path)] + pub mother_prefix: String, + + #[clap(required = true)] + #[clap(short = 'f')] + #[clap(long = "father")] + #[clap(help = "Prefix of father VCF and spanning reads BAM files")] + #[arg(value_parser = check_prefix_path)] + #[clap(value_name = "PREFIX")] + #[arg(value_parser = check_prefix_path)] + pub father_prefix: String, + + #[clap(required = true)] + #[clap(short = 'c')] + #[clap(long = "child")] + #[clap(help = "Prefix of child VCF and spanning reads BAM files")] + #[clap(value_name = "PREFIX")] + #[arg(value_parser = check_prefix_path)] + pub child_prefix: String, + + #[clap(required = true)] + #[clap(short = 'o')] + #[clap(long = "out")] + #[clap(help = "Output csv path")] + #[clap(value_name = "CSV")] + #[arg(value_parser = check_prefix_path)] + pub output_path: String, + + #[clap(long = "trid")] + #[clap(help = "TRID of a specific repeat to analyze, should be in the BED file")] + #[clap(value_name = "TRID")] + pub trid: Option, + + #[clap(short = '@')] + #[clap(value_name = "THREADS")] + #[clap(default_value = "1")] + #[arg(value_parser = threads_in_range)] + pub num_threads: usize, + + #[clap(help_heading("Advanced"))] + #[clap(long = "flank-len")] + #[clap(help = "Amount of additional flanking sequence that should be used during alignment")] + #[clap(value_name = "FLANK_LEN")] + #[clap(default_value = "50")] + pub flank_len: usize, + + #[clap(help_heading("Advanced"))] + #[clap(long = "no-clip-aln")] + #[clap(help = "Score alignments without stripping the flanks")] + #[clap(value_name = "CLIP")] + pub no_clip_aln: bool, + + #[clap(help_heading("Advanced"))] + #[clap(long = "parental-quantile")] + #[clap( + help = "Quantile of alignments scores to determine the parental threshold (default is strict and takes only the top score)" + )] + #[clap(value_name = "QUANTILE")] + #[clap(default_value = "1.0")] + #[arg(value_parser = parse_quantile)] + pub parent_quantile: f64, +} + +pub fn init_verbose(args: &Cli) { + let filter_level: LevelFilter = match args.verbosity { + 0 => LevelFilter::Info, + 1 => LevelFilter::Debug, + _ => LevelFilter::Trace, + }; + + env_logger::Builder::from_default_env() + .format(|buf, record| { + let level = record.level(); + let mut style = buf.style(); + match record.level() { + Level::Error => style.set_color(Color::Red), + Level::Warn => style.set_color(Color::Yellow), + Level::Info => style.set_color(Color::Green), + Level::Debug => style.set_color(Color::Blue), + Level::Trace => style.set_color(Color::Cyan), + }; + + writeln!( + buf, + "{} [{}] - {}", + chrono::Local::now().format("%Y-%m-%d %H:%M:%S"), + style.value(level), + record.args() + ) + }) + .filter_level(filter_level) + .init(); +} + +fn check_prefix_path(s: &str) -> Result { + let path = Path::new(s); + if let Some(parent_dir) = path.parent() { + if !parent_dir.as_os_str().is_empty() && !parent_dir.exists() { + return Err(anyhow!("Path does not exist: {}", parent_dir.display())); + } + } + Ok(s.to_string()) +} + +fn threads_in_range(s: &str) -> Result { + let thread: usize = s + .parse::() + .map_err(|_| anyhow!("`{}` is not a valid thread number", s))?; + if thread <= 0 { + return Err(anyhow!("Number of threads must be >= 1")); + } + Ok(thread) +} + +fn parse_quantile(s: &str) -> Result { + let value = s + .parse::() + .map_err(|e| anyhow!("Could not parse float: {}", e))?; + if value < 0.0 || value > 1.0 { + Err(anyhow!( + "The value must be between 0.0 and 1.0, got: {}", + value + )) + } else { + Ok(value) + } +} + +fn check_file_exists(s: &str) -> Result { + let path = Path::new(s); + if !path.exists() { + return Err(anyhow!("File does not exist: {}", path.display())); + } + Ok(path.to_path_buf()) +} diff --git a/src/commands.rs b/src/commands.rs new file mode 100644 index 0000000..8aacde7 --- /dev/null +++ b/src/commands.rs @@ -0,0 +1,3 @@ +pub mod trio; + +pub use self::trio::trio; diff --git a/src/commands/trio.rs b/src/commands/trio.rs new file mode 100644 index 0000000..a0d45b1 --- /dev/null +++ b/src/commands/trio.rs @@ -0,0 +1,146 @@ +use crate::aligner::{AlignmentScope, MemoryModel, WFAligner, WFAlignerGapAffine2Pieces}; +use crate::{ + allele, + cli::TrioArgs, + handles, locus, + util::{self}, +}; +use anyhow::Result; +use csv::WriterBuilder; +use log; +use noodles::{ + bed, + fasta::{self, io::BufReadSeek}, +}; +use rayon::prelude::*; +use rayon::ThreadPoolBuilder; +use std::{ + cell::RefCell, + fs, + io::BufReader, + sync::{mpsc::channel, Arc}, + thread, + time::Instant, +}; + +thread_local! { + static ALIGNER: RefCell = RefCell::new(WFAlignerGapAffine2Pieces::new( + 8, + 4, + 2, + 24, + 1, + AlignmentScope::Alignment, + MemoryModel::MemoryLow + )); +} + +pub fn trio(args: TrioArgs) -> Result<()> { + log::info!( + "{}-{} trio start", + env!("CARGO_PKG_NAME"), + *crate::cli::FULL_VERSION + ); + let start_timer = Instant::now(); + let clip_len = if args.no_clip_aln { 0 } else { args.flank_len }; + + let handles = + handles::Handles::new(&args.mother_prefix, &args.father_prefix, &args.child_prefix) + .unwrap_or_else(|err| util::handle_error_and_exit(err)); + let handles_arc = Arc::new(handles); + + let mut catalog_reader = fs::File::open(args.bed_filename) + .map(BufReader::new) + .map(bed::Reader::new)?; + + let mut genome_reader: fasta::IndexedReader> = + fasta::indexed_reader::Builder::default() + .build_from_path(&args.reference_filename) + .unwrap_or_else(|err| util::handle_error_and_exit(err.into())); + + match args.trid { + Some(trid) => { + let locus = locus::get_locus( + &mut genome_reader, + &mut catalog_reader, + &trid, + args.flank_len, + ) + .unwrap_or_else(|err| util::handle_error_and_exit(err)); + + let mut csv_wtr = WriterBuilder::new() + .delimiter(b'\t') + .from_writer(std::io::stdout()); + + ALIGNER.with(|aligner| { + let mut aligner = aligner.borrow_mut(); + if let Ok(result) = allele::process_alleles( + &locus, + handles_arc, + clip_len, + args.parent_quantile, + &mut aligner, + ) { + for row in result { + if let Err(err) = csv_wtr.serialize(row) { + log::error!("Failed to write record: {}", err); + } + csv_wtr.flush().unwrap(); + } + } + }); + } + None => { + let all_loci = locus::get_loci(&mut genome_reader, &mut catalog_reader, args.flank_len) + .collect::>>() + .unwrap_or_else(|err| util::handle_error_and_exit(err)); + + let mut csv_wtr = WriterBuilder::new() + .delimiter(b'\t') + .from_path(&args.output_path)?; + + let (sender, receiver) = channel(); + let writer_thread = thread::spawn(move || { + for results in &receiver { + for row in results { + if let Err(err) = csv_wtr.serialize(row) { + log::error!("Failed to write record: {}", err); + } + csv_wtr.flush().unwrap(); + } + } + }); + + log::info!("Starting job pool with {} threads...", args.num_threads); + let pool = ThreadPoolBuilder::new() + .num_threads(args.num_threads) + .build() + .unwrap(); + + pool.install(|| { + all_loci + .into_iter() + .par_bridge() + .for_each_with(sender, |s, locus| { + ALIGNER.with(|aligner| { + let mut aligner = aligner.borrow_mut(); + if let Ok(result) = allele::process_alleles( + &locus, + handles_arc.clone(), + clip_len, + args.parent_quantile, + &mut aligner, + ) { + s.send(result).unwrap(); + } + }); + }); + }); + writer_thread.join().unwrap(); + } + } + log::info!("Total execution time: {:?}", start_timer.elapsed()); + log::info!("{} trio end", env!("CARGO_PKG_NAME")); + + Ok(()) +} diff --git a/src/denovo.rs b/src/denovo.rs new file mode 100644 index 0000000..6c9dc52 --- /dev/null +++ b/src/denovo.rs @@ -0,0 +1,506 @@ +use crate::aligner::WFAligner; +use crate::allele::Allele; +use ndarray::{Array2, ArrayBase, Dim, OwnedRepr}; +use serde::Serialize; +use std::{cmp::Ordering, fmt}; + +#[derive(Debug)] +pub struct DenovoAllele { + pub genotype: usize, + pub denovo_score: usize, + pub child_coverage: usize, + pub allele_coverage: usize, + pub mean_diff_father: f32, + pub mean_diff_mother: f32, + pub allele_origin: AlleleOrigin, + pub denovo_status: DenovoStatus, + pub motif_count: String, + pub index: usize, +} + +#[derive(Debug, PartialEq, Clone, Serialize)] +pub enum DenovoType { + Expansion, + Contraction, + Substitution, + Unclear, +} + +#[derive(Debug, PartialEq, Clone, Serialize)] +pub enum DenovoStatus { + Denovo(DenovoType), + NotDenovo, +} + +impl std::fmt::Display for DenovoType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DenovoType::Expansion => write!(f, "+"), + DenovoType::Contraction => write!(f, "-"), + DenovoType::Substitution => write!(f, "="), + DenovoType::Unclear => write!(f, "?"), + } + } +} + +impl std::fmt::Display for DenovoStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DenovoStatus::Denovo(denovo_type) => write!(f, "Y:{}", denovo_type), + DenovoStatus::NotDenovo => write!(f, "X"), + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize)] +pub enum AlleleNum { + One, + Two, + Unclear, +} + +#[derive(Debug, PartialEq, Clone, Serialize)] +pub enum AlleleOrigin { + Father { allele: AlleleNum }, + Mother { allele: AlleleNum }, + Unclear, +} + +impl AlleleOrigin { + pub fn new(value: usize) -> Option { + match value { + 0 => Some(AlleleOrigin::Mother { + allele: AlleleNum::One, + }), + 1 => Some(AlleleOrigin::Mother { + allele: AlleleNum::Two, + }), + 2 => Some(AlleleOrigin::Father { + allele: AlleleNum::One, + }), + 3 => Some(AlleleOrigin::Father { + allele: AlleleNum::Two, + }), + _ => None, + } + } +} + +impl fmt::Display for AlleleNum { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + AlleleNum::One => write!(f, "1"), + AlleleNum::Two => write!(f, "2"), + AlleleNum::Unclear => write!(f, "?"), + } + } +} + +impl fmt::Display for AlleleOrigin { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + AlleleOrigin::Father { allele } => write!(f, "F:{}", allele), + AlleleOrigin::Mother { allele } => write!(f, "M:{}", allele), + AlleleOrigin::Unclear => write!(f, "?"), + } + } +} + +// Valid allele combinations +static COMBS_1D: [[(usize, usize); 2]; 4] = [ + [(0, 0), (0, 0)], + [(1, 0), (1, 0)], + [(2, 0), (2, 0)], + [(3, 0), (3, 0)], +]; +static COMBS_2D: [[(usize, usize); 2]; 8] = [ + [(0, 0), (2, 1)], + [(0, 0), (3, 1)], + [(1, 0), (2, 1)], + [(1, 0), (3, 1)], + [(2, 0), (0, 1)], + [(3, 0), (0, 1)], + [(2, 0), (1, 1)], + [(3, 0), (1, 1)], +]; + +pub fn assess_denovo<'a>( + mother_gts: &'a [Allele], + father_gts: &'a [Allele], + child_gts: &'a [Allele], + clip_len: usize, + parent_quantile: f64, + aligner: &mut WFAligner, +) -> impl Iterator + 'a { + let mut matrix = Array2::from_elem((4, 2), f64::MIN); + let mut dnrs = Vec::with_capacity(child_gts.len()); + + for (index, denovo_allele) in child_gts.iter().enumerate() { + let mother_align_scores = align(mother_gts, &denovo_allele.seq, clip_len, aligner); + let father_align_scores = align(father_gts, &denovo_allele.seq, clip_len, aligner); + let child_align_scores = align(child_gts, &denovo_allele.seq, clip_len, aligner); + + let (denovo_coverage, mean_diff_father, mean_diff_mother) = get_denovo_coverage( + &mother_align_scores, + &father_align_scores, + &child_align_scores, + parent_quantile, + ); + + update_mean_matrix( + &mut matrix, + index, + &mother_align_scores, + &father_align_scores, + ); + + dnrs.push(DenovoAllele { + genotype: denovo_allele.genotype, + denovo_score: denovo_coverage, + child_coverage: child_align_scores.iter().map(|vec| vec.len()).sum(), + allele_coverage: denovo_allele.read_aligns.len(), + mean_diff_mother, + mean_diff_father, + allele_origin: AlleleOrigin::Unclear, + denovo_status: DenovoStatus::NotDenovo, + motif_count: denovo_allele.motif_count.clone(), + index: denovo_allele.index, + }); + } + + // Get best allele combinations + let mut combs_score: Vec<(f64, [(usize, usize); 2])> = Vec::new(); + if child_gts.len() > 1 { + for c in COMBS_2D.iter() { + combs_score.push((c.iter().map(|&(i, j)| matrix[[i, j]]).sum::(), *c)); + } + } else { + for c in COMBS_1D.iter() { + combs_score.push((matrix[[c[0].0, c[0].1]], *c)); + } + } + combs_score.sort_unstable_by(|a, b| b.0.partial_cmp(&a.0).unwrap()); + + // Update allele origin and de novo type + let comb_diff = (combs_score[0].0 - combs_score[1].0).abs(); + let comb_0 = combs_score[0].1; + for (index, denovo_allele) in child_gts.iter().enumerate() { + let dna = &mut dnrs[index]; + // TODO: Alignment based: while we might not know the allele origin, may still figure out parental origin + if comb_diff < 1.0 { + dna.allele_origin = AlleleOrigin::Unclear; + } else { + dna.allele_origin = AlleleOrigin::new(comb_0[index].0).unwrap(); + } + + dna.denovo_status = match dna.denovo_score { + 0 => DenovoStatus::NotDenovo, + _ => DenovoStatus::Denovo(get_denovo_type( + mother_gts, + father_gts, + &dna.allele_origin, + &denovo_allele.seq, + )), + }; + } + dnrs.into_iter().filter_map(Some) +} + +pub fn update_mean_matrix( + matrix: &mut ArrayBase, Dim<[usize; 2]>>, + index: usize, + mother_align_scores: &[Vec], + father_align_scores: &[Vec], +) { + let get_mean = |aligns: &[Vec]| -> Vec<(f64, usize)> { + aligns + .iter() + .enumerate() + .map(|(i, v)| { + if v.is_empty() { + (f64::NEG_INFINITY, i) + } else { + (v.iter().sum::() as f64 / v.len() as f64, i) + } + }) + .collect() + }; + + let mother_mean: Vec<(f64, usize)> = get_mean(mother_align_scores); + let father_mean: Vec<(f64, usize)> = get_mean(father_align_scores); + + matrix[[0, index]] = mother_mean[0].0; + matrix[[1, index]] = mother_mean.get(1).map_or(f64::MIN, |v| v.0); + matrix[[2, index]] = father_mean[0].0; + matrix[[3, index]] = father_mean.get(1).map_or(f64::MIN, |v| v.0); +} + +fn compare_seq_lengths(gts: &[Allele], allele: &AlleleNum, child_seq: &[u8]) -> Option { + match allele { + AlleleNum::One => compare_sequences(>s[0].seq, child_seq), + AlleleNum::Two => compare_sequences(>s[1].seq, child_seq), + _ => None, + } +} + +fn compare_sequences(seq: &[u8], child_seq: &[u8]) -> Option { + match seq.len().cmp(&child_seq.len()) { + Ordering::Less => Some(DenovoType::Expansion), + Ordering::Greater => Some(DenovoType::Contraction), + Ordering::Equal => Some(DenovoType::Substitution), + } +} + +fn get_denovo_type( + mother_gts: &[Allele], + father_gts: &[Allele], + allele_origin: &AlleleOrigin, + child_seq: &[u8], +) -> DenovoType { + match allele_origin { + AlleleOrigin::Father { allele } => { + compare_seq_lengths(father_gts, allele, child_seq).unwrap_or(DenovoType::Unclear) + } + AlleleOrigin::Mother { allele } => { + compare_seq_lengths(mother_gts, allele, child_seq).unwrap_or(DenovoType::Unclear) + } + _ => DenovoType::Unclear, + } +} + +fn align(gts: &[Allele], target: &[u8], clip_len: usize, aligner: &mut WFAligner) -> Vec> { + let mut align_scores = vec![vec![]; gts.len()]; + for (i, allele) in gts.iter().enumerate() { + for (read, _align) in &allele.read_aligns { + let _status = aligner.align_end_to_end(&read.bases, target); + align_scores[i].push(aligner.cigar_score_clipped(clip_len)); + } + } + align_scores +} + +fn get_parental_count_diff(top_parent_score: f64, child_aligns: &[Vec]) -> (usize, f32) { + let (count, sum) = child_aligns + .iter() + .flatten() + .map(|&a| a as f64) + .filter(|a| a > &top_parent_score) + .fold((0, 0.0), |(count, sum), a| (count + 1, sum + a)); + let mean_diff = if count > 0 { + (sum / count as f64 - top_parent_score).abs() as f32 + } else { + 0.0 + }; + (count, mean_diff) +} + +fn get_score_quantile(xs: &mut [f64], q: f64) -> Option { + if xs.is_empty() { + return None; + } + xs.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + + let index = q * (xs.len() as f64 - 1.0); + let lower_index = index.floor() as usize; + let upper_index = lower_index + 1; + let fraction = index - lower_index as f64; + + if upper_index >= xs.len() { + return Some(xs[xs.len() - 1]); + } + + Some(xs[lower_index] + (xs[upper_index] - xs[lower_index]) * fraction) +} + +fn get_top_parent_score(align_scores: &[Vec], quantile: f64) -> Option { + align_scores + .iter() + .filter_map(|scores| { + let mut scores_f64: Vec = scores.iter().map(|&x| x as f64).collect(); + get_score_quantile(&mut scores_f64, quantile) + }) + .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)) +} + +fn get_denovo_coverage( + mother_align_scores: &[Vec], + father_align_scores: &[Vec], + child_align_scores: &[Vec], + parent_quantile: f64, +) -> (usize, f32, f32) { + let top_mother_score = get_top_parent_score(mother_align_scores, parent_quantile).unwrap(); + let top_father_score = get_top_parent_score(father_align_scores, parent_quantile).unwrap(); + + let (mother_count, mean_diff_mother) = + get_parental_count_diff(top_mother_score, child_align_scores); + let (father_count, mean_diff_father) = + get_parental_count_diff(top_father_score, child_align_scores); + + let top_score = if top_mother_score >= top_father_score { + mother_count + } else { + father_count + }; + + (top_score, mean_diff_father, mean_diff_mother) +} + +#[cfg(test)] +mod tests { + use crate::read::ReadInfoBuilder; + + use super::*; + + #[test] + fn test_parent_origin_matrix_1() { + let mut matrix = Array2::from_elem((4, 2), f64::MIN); + matrix[[0, 0]] = -25.0; + matrix[[0, 1]] = -50.0; + matrix[[1, 0]] = -40.0; + matrix[[1, 1]] = -30.0; + matrix[[2, 0]] = 0.0; + matrix[[2, 1]] = -50.0; + matrix[[3, 0]] = -10.0; + matrix[[3, 1]] = -20.0; + + let mut combs_score: Vec<(f64, [(usize, usize); 2])> = Vec::new(); + for c in COMBS_2D.iter() { + combs_score.push((c.iter().map(|&(i, j)| matrix[[i, j]]).sum::(), *c)); + } + combs_score.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap()); + + let expected_result = (-30.0, [(2, 0), (1, 1)]); + assert_eq!(combs_score[0], expected_result); + + assert_eq!( + AlleleOrigin::new(combs_score[0].1[0].0).unwrap(), + AlleleOrigin::Father { + allele: AlleleNum::One, + }, + ); + + assert_eq!( + AlleleOrigin::new(combs_score[0].1[1].0).unwrap(), + AlleleOrigin::Mother { + allele: AlleleNum::Two, + }, + ); + } + + #[test] + fn test_get_parental_count_diff() { + let top_parent_score = -8.0; + let child_aligns = vec![vec![-30, -20, -30], vec![-6, 3, 0]]; + assert_eq!( + get_parental_count_diff(top_parent_score, &child_aligns), + (3, 7.0) + ); + } + + #[test] + fn test_get_denovo_coverage() { + let mother_aligns = vec![vec![-24, -24, -24], vec![-24, -12, -24]]; + let father_aligns = vec![vec![-8, -8, -12], vec![-8, -8, -20]]; + let child_aligns = vec![vec![-30, -20, -30], vec![-6, 0, 0]]; + assert_eq!( + get_denovo_coverage(&mother_aligns, &father_aligns, &child_aligns, 1.0), + (3, 6.0, 10.0) + ); + } + + #[test] + fn test_compare_seq_lengths_substitution() { + let gts = vec![ + Allele { + seq: b"ATATATAT".to_vec(), + genotype: 0, + read_aligns: vec![ + (ReadInfoBuilder::default().build().unwrap(), 0), + (ReadInfoBuilder::default().build().unwrap(), 1), + ], + motif_count: "".to_string(), + index: 0, + }, + Allele { + seq: b"ATATATAT".to_vec(), + genotype: 1, + read_aligns: vec![ + (ReadInfoBuilder::default().build().unwrap(), 0), + (ReadInfoBuilder::default().build().unwrap(), 1), + ], + motif_count: "".to_string(), + index: 1, + }, + ]; + let allele = AlleleNum::One; + let child_seq = b"ATCGATAT".to_vec(); + assert_eq!( + compare_seq_lengths(>s, &allele, &child_seq).unwrap(), + DenovoType::Substitution + ); + } + + #[test] + fn test_compare_seq_lengths_contraction() { + let gts = vec![ + Allele { + seq: b"ATATATAT".to_vec(), + genotype: 0, + read_aligns: vec![ + (ReadInfoBuilder::default().build().unwrap(), 0), + (ReadInfoBuilder::default().build().unwrap(), 1), + ], + motif_count: "".to_string(), + index: 0, + }, + Allele { + seq: b"ATA".to_vec(), + genotype: 1, + read_aligns: vec![ + (ReadInfoBuilder::default().build().unwrap(), 0), + (ReadInfoBuilder::default().build().unwrap(), 1), + ], + motif_count: "".to_string(), + index: 1, + }, + ]; + let allele = AlleleNum::One; + let child_seq = b"ATATAT".to_vec(); + assert_eq!( + compare_seq_lengths(>s, &allele, &child_seq).unwrap(), + DenovoType::Contraction + ); + } + + #[test] + fn test_compare_seq_lengths_expansion() { + let gts = vec![ + Allele { + seq: b"ATATA".to_vec(), + genotype: 0, + read_aligns: vec![ + (ReadInfoBuilder::default().build().unwrap(), 0), + (ReadInfoBuilder::default().build().unwrap(), 1), + ], + motif_count: "".to_string(), + index: 0, + }, + Allele { + seq: b"ATATATAT".to_vec(), + genotype: 1, + read_aligns: vec![ + (ReadInfoBuilder::default().build().unwrap(), 0), + (ReadInfoBuilder::default().build().unwrap(), 1), + ], + motif_count: "".to_string(), + index: 1, + }, + ]; + let allele = AlleleNum::Two; + let child_seq = b"ATATATATAT".to_vec(); + assert_eq!( + compare_seq_lengths(>s, &allele, &child_seq).unwrap(), + DenovoType::Expansion + ); + } +} diff --git a/src/handles.rs b/src/handles.rs new file mode 100644 index 0000000..eb221bd --- /dev/null +++ b/src/handles.rs @@ -0,0 +1,90 @@ +use crate::util::{self, Result}; +use anyhow::anyhow; +use noodles::{bam, bgzf::Reader, sam, vcf}; +use std::{ + fs::File, + path::PathBuf, + sync::{Arc, Mutex}, +}; + +pub fn build_paths(prefix: &str) -> Result> { + let mut paths = Vec::new(); + for ext in &["spanning.sorted.bam", "sorted.vcf.gz"] { + let mut path = PathBuf::from(prefix); + if let Some(file_name) = path.file_name() { + let file_name = file_name.to_string_lossy().to_string(); + path.set_file_name(format!("{}.{}", file_name, ext)); + } + util::try_exists(&path)?; + paths.push(path); + } + Ok(paths) +} + +#[derive(Clone)] +pub struct Handles { + pub mother: SubHandle, + pub father: SubHandle, + pub child: SubHandle, +} + +impl Handles { + pub fn new(mother_prefix: &str, father_prefix: &str, child_prefix: &str) -> Result { + Ok(Handles { + mother: SubHandle::new(mother_prefix)?, + father: SubHandle::new(father_prefix)?, + child: SubHandle::new(child_prefix)?, + }) + } +} + +#[derive(Clone)] +pub struct SubHandle { + pub vcf: Arc>>, + pub vcf_header: Arc, + pub bam: Arc>>>, + pub bam_header: Arc, +} + +impl SubHandle { + pub fn new(prefix: &str) -> Result { + let paths = build_paths(prefix)?; + let paths_slice = paths.as_slice(); + + if paths_slice.len() < 2 { + return Err(anyhow!("Failed to parse paths")); + } + + let bam_path = &paths_slice[0]; + let vcf_path = &paths_slice[1]; + + let mut bam = bam::indexed_reader::Builder::default() + .build_from_path(bam_path) + .map_err(|e| anyhow!("Failed to create bam reader: {}", e))?; + + let bam_header = bam + .read_header() + .map_err(|e| anyhow!("Failed to read bam header: {}", e))?; + + let bam = Arc::new(Mutex::new(bam)); + let bam_header = Arc::new(bam_header); + + let mut vcf = vcf::indexed_reader::Builder::default() + .build_from_path(vcf_path) + .map_err(|e| anyhow!("Failed to create vcf reader: {}", e))?; + + let vcf_header = vcf + .read_header() + .map_err(|e| anyhow!("Failed to read vcf header: {}", e))?; + + let vcf = Arc::new(Mutex::new(vcf)); + let vcf_header = Arc::new(vcf_header); + + Ok(SubHandle { + vcf, + vcf_header, + bam, + bam_header, + }) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..a02f43c --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,12 @@ +pub mod aligner; +pub mod allele; +pub mod cli; +pub mod commands; +pub mod denovo; +pub mod handles; +pub mod locus; +pub mod read; +pub mod snp; +pub mod stats; +pub mod util; +pub mod wfa2; diff --git a/src/locus.rs b/src/locus.rs new file mode 100644 index 0000000..ec69cac --- /dev/null +++ b/src/locus.rs @@ -0,0 +1,203 @@ +use crate::util::Result; +use anyhow::{anyhow, Context}; +use noodles::{ + bed, + core::{Position, Region}, + fasta::{self, io::BufReadSeek, record::Sequence}, +}; +use std::{ + collections::{HashMap, HashSet}, + fs::File, + io::BufReader, +}; + +#[derive(Debug, PartialEq)] +pub struct Locus { + pub id: String, + pub struc: String, + pub motifs: Vec, + pub left_flank: Vec, + pub right_flank: Vec, + pub region: Region, +} + +fn decode_info_field(encoding: &str) -> Result<(&str, &str)> { + let mut name_and_value = encoding.splitn(2, '='); + let name = name_and_value + .next() + .ok_or_else(|| anyhow!("Invalid entry: {}", encoding))?; + let value = name_and_value + .next() + .ok_or_else(|| anyhow!("Invalid entry: {}", encoding))?; + Ok((name, value)) +} + +fn decode_fields(info_fields: &str) -> Result> { + let mut fields = HashMap::new(); + for field_encoding in info_fields.split(';') { + let (name, value) = decode_info_field(field_encoding)?; + if fields.insert(name, value.to_string()).is_some() { + return Err(anyhow!("Duplicate field: {}", name)); + } + } + Ok(fields) +} + +impl Locus { + pub fn new( + genome: &mut fasta::IndexedReader>, + chrom_lookup: &HashSet, + line: &bed::Record<3>, + flank_len: usize, + ) -> Result { + if !chrom_lookup.contains(line.reference_sequence_name()) { + return Err(anyhow!( + "Chromosome {} not found in reference", + line.reference_sequence_name() + )); + } + + // -1 because bed is 0-based + let region = Region::new( + line.reference_sequence_name(), + Position::new(usize::from(line.start_position()) - 1).unwrap()..=line.end_position(), + ); + + let fields = decode_fields(&line.optional_fields()[0])?; + + let id = fields + .get("ID") + .ok_or_else(|| anyhow!("ID field missing"))? + .to_string(); + let struc = fields + .get("STRUC") + .ok_or_else(|| anyhow!("STRUC field missing"))? + .to_string(); + let motifs = fields + .get("MOTIFS") + .ok_or_else(|| anyhow!("MOTIFS field missing"))? + .split(',') + .map(|s| s.to_string()) + .collect(); + + let (left_flank, right_flank) = get_flanks(genome, ®ion, flank_len)?; + + Ok(Locus { + id, + struc, + motifs, + left_flank, + right_flank, + region, + }) + } +} + +pub fn get_locus( + genome_reader: &mut fasta::IndexedReader>, + catalog_reader: &mut bed::Reader>, + tr_id: &str, + flank_len: usize, +) -> Result { + let chrom_lookup = HashSet::from_iter( + genome_reader + .index() + .iter() + .map(|entry| entry.name().to_string()), + ); + + let query = format!("ID={tr_id};"); + for (line_number, result) in catalog_reader.records::<3>().enumerate() { + let line = result.with_context(|| format!("Error reading BED line {}", line_number + 1))?; + if line.optional_fields().get(0).unwrap().contains(&query) { + return Locus::new(genome_reader, &chrom_lookup, &line, flank_len) + .with_context(|| format!("Error processing BED line {}", line_number + 1)); + } + } + Err(anyhow!("Unable to find locus {tr_id}")) +} + +pub fn get_loci<'a>( + genome_reader: &'a mut fasta::IndexedReader>, + catalog_reader: &'a mut bed::Reader>, + flank_len: usize, +) -> impl Iterator> + 'a { + let chrom_lookup = HashSet::from_iter( + genome_reader + .index() + .iter() + .map(|entry| entry.name().to_string()), + ); + + catalog_reader + .records::<3>() + .enumerate() + .filter_map(move |(line_number, result_line)| match result_line { + Ok(line) => Some(Ok((line, line_number))), + Err(err) => Some(Err( + anyhow!(err).context(format!("Error at BED line {}", line_number + 1)) + )), + }) + .map(move |result| { + result.and_then(|(line, line_number)| { + Locus::new(genome_reader, &chrom_lookup, &line, flank_len) + .with_context(|| format!("Error processing BED line {}", line_number + 1)) + }) + }) +} + +fn get_flank( + genome: &mut fasta::IndexedReader>, + region: &Region, + start: usize, + end: usize, +) -> Result { + let start_pos = Position::try_from(start + 1).unwrap(); + let end_pos = Position::try_from(end).unwrap(); + let query_region = Region::new(region.name(), start_pos..=end_pos); + match genome.query(&query_region) { + Ok(seq) => Ok(seq.sequence().to_owned()), + Err(_) => Err(anyhow!("Unable to extract: {:?}", region)), + } +} + +fn get_flanks( + genome: &mut fasta::IndexedReader>, + region: &Region, + flank_len: usize, +) -> Result<(Vec, Vec)> { + let (lf_start, lf_end) = ( + region.interval().start().unwrap().get() - flank_len, + region.interval().start().unwrap().get(), + ); + let (rf_start, rf_end) = ( + region.interval().end().unwrap().get(), + region.interval().end().unwrap().get() + flank_len, + ); + + let left_flank = get_flank(genome, region, lf_start, lf_end)?; + let right_flank = get_flank(genome, region, rf_start, rf_end)?; + + Ok(( + left_flank.as_ref().to_ascii_uppercase(), + right_flank.as_ref().to_ascii_uppercase(), + )) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn can_decode_info_field() { + let decoded = decode_info_field("ID=AFF2").unwrap(); + assert_eq!("ID", decoded.0); + assert_eq!("AFF2", decoded.1); + } + + #[test] + #[should_panic] + fn panic_invalid_decode_info_field() { + decode_info_field("ID:AFF2").unwrap(); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..9fa805b --- /dev/null +++ b/src/main.rs @@ -0,0 +1,15 @@ +use clap::Parser; +use trgt_denovo::{ + cli::{init_verbose, Cli, Command}, + commands::trio, + util::Result, +}; + +fn main() -> Result<()> { + let cli = Cli::parse(); + init_verbose(&cli); + match cli.command { + Command::Trio(args) => trio(args)?, + } + Ok(()) +} diff --git a/src/read.rs b/src/read.rs new file mode 100644 index 0000000..a14e103 --- /dev/null +++ b/src/read.rs @@ -0,0 +1,198 @@ +use noodles::sam::alignment::Record; +use noodles::sam::record::data::field::value::Array; +use noodles::sam::record::data::field::{tag, Value}; +use once_cell::sync::Lazy; + +#[derive(Debug, PartialEq, Clone)] +pub struct FlankingReadInfo { + is_left_flank: bool, +} + +#[derive(Debug, PartialEq, Clone)] +pub struct ReadInfo { + pub bases: Box<[u8]>, + pub classification: Option, + pub start_offset: Option, + pub end_offset: Option, + pub mismatch_offsets: Option>, + pub flank_info: Option, +} + +static AL_KEY: Lazy = Lazy::new(|| "AL".parse().unwrap()); +static MO_KEY: Lazy = Lazy::new(|| "MO".parse().unwrap()); +static SO_KEY: Lazy = Lazy::new(|| "SO".parse().unwrap()); +static EO_KEY: Lazy = Lazy::new(|| "EO".parse().unwrap()); + +impl ReadInfo { + pub fn new(record: Record) -> Self { + let bases = record + .sequence() + .as_ref() + .iter() + .map(|base| u8::from(*base)) + .collect::>() + .into_boxed_slice(); + + let data = record.data(); + + let classification = data.get(&*AL_KEY).and_then(|value| match value { + Value::UInt8(v) => Some(*v), + _ => None, + }); + + let start_offset = data.get(&*SO_KEY).and_then(|value| match value { + Value::Int32(v) => Some(*v), + _ => None, + }); + + let end_offset = data.get(&*EO_KEY).and_then(|value| match value { + Value::Int32(v) => Some(*v), + _ => None, + }); + + let mismatch_offsets = data + .get(&*MO_KEY) + .and_then(|value| value.as_array()) + .and_then(|array| match array { + Array::Int32(vec) => Some(vec.clone()), + _ => None, + }); + + Self { + bases, + classification, + start_offset, + end_offset, + mismatch_offsets, + flank_info: None, + } + } +} + +#[derive(Debug, PartialEq, Clone)] + +pub struct ReadInfoBuilder { + bases: Box<[u8]>, + classification: Option, + start_offset: Option, + end_offset: Option, + mismatch_offsets: Option>, + flank_info: Option, +} + +impl ReadInfoBuilder { + pub fn new() -> Self { + Self { + bases: Box::new([0u8; 10]), + classification: Some(0), + start_offset: Some(0), + end_offset: Some(0), + mismatch_offsets: Some(vec![0, 0, 0, 0, 0]), + flank_info: None, + } + } + + pub fn with_bases>>(mut self, bases: T) -> Self { + self.bases = bases.into(); + self + } + + pub fn with_classification(mut self, classification: Option) -> Self { + self.classification = classification; + self + } + + pub fn with_start_offset(mut self, start_offset: Option) -> Self { + self.start_offset = start_offset; + self + } + + pub fn with_end_offset(mut self, end_offset: Option) -> Self { + self.end_offset = end_offset; + self + } + + pub fn with_mismatch_offsets(mut self, mismatch_offsets: Option>) -> Self { + self.mismatch_offsets = mismatch_offsets; + self + } + + pub fn with_flank_info(mut self, flank_info: Option) -> Self { + self.flank_info = flank_info; + self + } + + pub fn build(self) -> Result { + Ok(ReadInfo { + bases: self.bases, + classification: self.classification, + start_offset: self.start_offset, + end_offset: self.end_offset, + mismatch_offsets: self.mismatch_offsets, + flank_info: self.flank_info, + }) + } +} + +impl Default for ReadInfoBuilder { + fn default() -> Self { + Self { + bases: Box::new([0u8; 10]), + classification: None, + start_offset: None, + end_offset: None, + mismatch_offsets: None, + flank_info: None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_read_info_builder() { + let dummy_read_info = ReadInfoBuilder::default() + .with_bases(vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + .with_classification(Some(1)) + .with_start_offset(Some(100)) + .with_end_offset(Some(200)) + .with_mismatch_offsets(Some(vec![1, 2, 3, 4, 5])) + .with_flank_info(Some(FlankingReadInfo { + is_left_flank: true, + })) + .build() + .unwrap(); + + assert_eq!( + dummy_read_info.bases, + vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10].into() + ); + assert_eq!(dummy_read_info.classification, Some(1)); + assert_eq!(dummy_read_info.start_offset, Some(100)); + assert_eq!(dummy_read_info.end_offset, Some(200)); + assert_eq!(dummy_read_info.mismatch_offsets, Some(vec![1, 2, 3, 4, 5])); + assert_eq!( + dummy_read_info.flank_info, + Some(FlankingReadInfo { + is_left_flank: true + }) + ); + } + + #[test] + fn test_read_info_builder_defaults() { + let default_read_info = ReadInfoBuilder::default().build().unwrap(); + + assert_eq!( + default_read_info.bases, + vec![0, 0, 0, 0, 0, 0, 0, 0, 0, 0].into() + ); + assert_eq!(default_read_info.classification, None); + assert_eq!(default_read_info.start_offset, None); + assert_eq!(default_read_info.end_offset, None); + assert_eq!(default_read_info.mismatch_offsets, None); + assert_eq!(default_read_info.flank_info, None); + } +} diff --git a/src/snp.rs b/src/snp.rs new file mode 100644 index 0000000..26f0f5b --- /dev/null +++ b/src/snp.rs @@ -0,0 +1,644 @@ +use crate::{allele::Allele, read::ReadInfo}; +use ndarray::{s, Array2, ArrayView1, ArrayView2}; +use std::collections::{HashMap, HashSet}; +use std::iter; + +#[cfg(not(test))] +mod constants { + pub const MAX_SNP_DIFF: i32 = 6000; + pub const MIN_SNP_FREQ: f64 = 0.2; +} + +#[cfg(test)] +mod constants { + pub const MAX_SNP_DIFF: i32 = 4000; + pub const MIN_SNP_FREQ: f64 = 0.2; +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum FamilyMember { + Father, + Mother, + Child, +} + +impl FamilyMember { + fn index(&self) -> usize { + match self { + FamilyMember::Father => 0, + FamilyMember::Mother => 1, + FamilyMember::Child => 2, + } + } +} + +#[derive(Debug, Clone, PartialEq, Default)] +pub struct FamilyOffsets { + pub start_offset: usize, + pub end_offset: usize, + pub allele_offsets: Vec, +} + +#[derive(Debug, PartialEq)] +pub struct TrinaryMatrix { + pub matrix: Array2, + pub offsets: [FamilyOffsets; 3], + pub mismatch_offsets: Vec, +} + +impl TrinaryMatrix { + pub fn new( + child: &Vec, + father: &Vec, + mother: &Vec, + ) -> Option { + let mut n_reads = 0; + let mut all_pois: HashSet = HashSet::new(); + let family_members = [father, mother, child]; + + for family_member in &family_members { + for allele in *family_member { + n_reads += allele.read_aligns.len(); + for (read, _) in &allele.read_aligns { + all_pois.extend(read.mismatch_offsets.as_ref().unwrap()); + } + } + } + + if all_pois.is_empty() { + return None; + } + + let mut all_pois: Vec = all_pois.into_iter().collect(); + all_pois.sort_unstable(); + + let mut matrix = Array2::from_elem((n_reads, all_pois.len()), 2); + + // TODO: ideally want: offsets = [FamilyOffsets::default(); 3], can't because of vec, maybe box Vec? + let mut offsets = [ + (FamilyOffsets { + start_offset: 0, + end_offset: 0, + allele_offsets: Vec::new(), + }), + (FamilyOffsets { + start_offset: 0, + end_offset: 0, + allele_offsets: Vec::new(), + }), + (FamilyOffsets { + start_offset: 0, + end_offset: 0, + allele_offsets: Vec::new(), + }), + ]; + + let mut row_idx = 0; + let mut allele_offsets = Vec::new(); + for (member_idx, family_member) in family_members.iter().enumerate() { + let start_offset = row_idx; + for (allele_idx, allele) in family_member.iter().enumerate() { + if allele_idx > 0 { + allele_offsets.push(row_idx); + } + for (read, _) in &allele.read_aligns { + let mismatch_offsets = read.mismatch_offsets.as_ref().unwrap(); + let start_col_idx = all_pois + .binary_search(&read.start_offset.unwrap()) + .unwrap_or_else(|x| x); + let end_col_idx = all_pois + .binary_search(&read.end_offset.unwrap()) + .unwrap_or_else(|x| x); + for col_idx in start_col_idx..end_col_idx { + let offset = all_pois[col_idx]; + matrix[[row_idx, col_idx]] = + mismatch_offsets.binary_search(&offset).is_ok() as u8; + } + row_idx += 1; + } + } + offsets[member_idx] = FamilyOffsets { + start_offset, + end_offset: row_idx - 1, + allele_offsets: allele_offsets.clone(), + }; + allele_offsets.clear(); + } + + Some(TrinaryMatrix { + matrix, + offsets, + mismatch_offsets: all_pois, + }) + } + + pub fn family_submatrix(&self, member: FamilyMember) -> ArrayView2 { + let offset = &self.offsets[member.index()]; + self.matrix + .slice(s![offset.start_offset..=offset.end_offset, ..]) + } + + pub fn allele_submatrix( + &self, + member: FamilyMember, + allele_idx: usize, + ) -> Option> { + let offset = &self.offsets[member.index()]; + + if allele_idx > offset.allele_offsets.len() { + return None; + } + + let start_offset = if allele_idx == 0 { + offset.start_offset + } else { + offset.allele_offsets[allele_idx - 1] + }; + + let end_offset = if allele_idx < offset.allele_offsets.len() { + offset.allele_offsets[allele_idx] - 1 + } else { + offset.end_offset + }; + + Some(self.matrix.slice(s![start_offset..=end_offset, ..])) + } + + pub fn iter_alleles<'a>( + &'a self, + member: FamilyMember, + ) -> impl Iterator> + 'a { + let offset = &self.offsets[member.index()]; + let start_offsets = + iter::once(offset.start_offset).chain(offset.allele_offsets.iter().cloned()); + let end_offsets = offset + .allele_offsets + .iter() + .cloned() + .chain(iter::once(offset.end_offset + 1)); + + start_offsets + .zip(end_offsets) + .map(move |(start, end)| self.matrix.slice(s![start..end, ..])) + } + + pub fn count_alleles(&self, member: FamilyMember) -> usize { + let offset = &self.offsets[member.index()]; + offset.allele_offsets.len() + 1 + } +} + +pub trait ReadFilter { + fn filter(&self, reads: &mut Vec); +} + +pub struct FilterByDist; +impl ReadFilter for FilterByDist { + fn filter(&self, reads: &mut Vec) { + for read in reads.iter_mut() { + if let Some(offsets) = &mut read.mismatch_offsets { + offsets.drain( + ..offsets + .binary_search(&(-constants::MAX_SNP_DIFF - 1)) + .unwrap_or_else(|x| x), + ); + offsets.drain( + offsets + .binary_search(&(constants::MAX_SNP_DIFF + 1)) + .unwrap_or_else(|x| x).., + ); + } + } + } +} + +fn calc_similarity(child_row: &ArrayView1, parent_row: &ArrayView1) -> f64 { + let mut matching_positions = 0; + let mut total_covered_positions = 0; + + for (&child_value, &parent_value) in child_row.iter().zip(parent_row.iter()) { + if child_value == 2 || parent_value == 2 { + continue; + } + + total_covered_positions += 1; + + if child_value == parent_value { + matching_positions += 1; + } + } + + let total_covered_positions = total_covered_positions as f64; + let total_covered_positions = if total_covered_positions == 0.0 { + 0.00000001 + } else { + total_covered_positions + }; + + matching_positions as f64 / total_covered_positions +} + +fn get_likelihood(child_allele: ArrayView2, parent_allele: ArrayView2) -> f64 { + let similarity_scores: Vec = child_allele + .outer_iter() + .flat_map(|child_row| { + parent_allele + .outer_iter() + .map(move |parent_row| calc_similarity(&child_row, &parent_row)) + }) + .collect(); + + let likelihood: f64 = similarity_scores.iter().sum::() / similarity_scores.len() as f64; + likelihood +} + +fn get_individual_prob( + trinary_matrix: &TrinaryMatrix, + child_allele_idx: usize, +) -> Option<(Vec, f64)> { + let child_allele = trinary_matrix.allele_submatrix(FamilyMember::Child, child_allele_idx)?; + if child_allele.is_empty() { + return None; + } + + let father_count = trinary_matrix.count_alleles(FamilyMember::Father) as f64; + let mother_count = trinary_matrix.count_alleles(FamilyMember::Mother) as f64; + + let mut hypotheses = Vec::new(); + let mut priors = Vec::new(); + + for father_allele in trinary_matrix.iter_alleles(FamilyMember::Father) { + if father_allele.is_empty() { + return None; + } + hypotheses.push((child_allele, father_allele)); + priors.push(0.5 / father_count); + } + + for mother_allele in trinary_matrix.iter_alleles(FamilyMember::Mother) { + if mother_allele.is_empty() { + return None; + } + hypotheses.push((child_allele, mother_allele)); + priors.push(0.5 / mother_count); + } + + let likelihoods: Vec = hypotheses + .iter() + .map(|(c, p)| get_likelihood(*c, *p)) + .collect(); + + let max_likelihood = likelihoods + .iter() + .cloned() + .max_by(|a, b| a.partial_cmp(b).unwrap()) + .unwrap(); + + let all_probabilities: f64 = likelihoods.iter().zip(&priors).map(|(l, p)| l * p).sum(); + let posteriors: Vec = likelihoods + .iter() + .zip(&priors) + .map(|(l, p)| l * p / all_probabilities) + .collect(); + + Some((posteriors, max_likelihood)) +} + +fn combine_probabilities( + child_allele1_prob: &[f64], + child_allele2_prob: &[f64], + father_count: usize, + mother_count: usize, +) -> HashMap { + let mut combined_probabilities = HashMap::new(); + let mut total_prob = 0.0; + + for i in 0..(father_count + mother_count) { + for j in 0..(father_count + mother_count) { + if (i < father_count && j < father_count) || (i >= father_count && j >= father_count) { + continue; + } + + let probability = child_allele1_prob[i] * child_allele2_prob[j]; + combined_probabilities.insert(format!("H{}_{}", i, j), probability); + total_prob += probability; + } + } + + // Normalize probabilities + for value in combined_probabilities.values_mut() { + *value /= total_prob; + } + + combined_probabilities +} + +pub fn inheritance_prob( + trinary_matrix: &TrinaryMatrix, +) -> Option<(HashMap, (f64, f64))> { + match trinary_matrix.count_alleles(FamilyMember::Child) { + 1 => { + let (child_allele1_prob, max_likelihood) = get_individual_prob(trinary_matrix, 0)?; + let mut result: HashMap = HashMap::new(); + for (i, prob) in child_allele1_prob.iter().enumerate() { + result.insert(i.to_string(), *prob); + } + Some((result, ((max_likelihood, -1.0)))) + } + 2 => { + let (child_allele1_prob, max_likelihood_1) = get_individual_prob(trinary_matrix, 0)?; + let (child_allele2_prob, max_likelihood_2) = get_individual_prob(trinary_matrix, 1)?; + Some(( + combine_probabilities( + &child_allele1_prob, + &child_allele2_prob, + trinary_matrix.count_alleles(FamilyMember::Father), + trinary_matrix.count_alleles(FamilyMember::Mother), + ), + (max_likelihood_1, max_likelihood_2), + )) + } + _ => None, + } +} + +pub struct FilterByFreq; +impl ReadFilter for FilterByFreq { + fn filter(&self, reads: &mut Vec) { + let mut offset_counts: HashMap = HashMap::new(); + let total_reads = reads.len(); + + for read in reads.iter() { + if let Some(offsets) = &read.mismatch_offsets { + for &offset in offsets { + *offset_counts.entry(offset).or_insert(0) += 1; + } + } + } + for read in reads.iter_mut() { + if let Some(offsets) = &mut read.mismatch_offsets { + offsets.retain(|&offset| { + let count = offset_counts.get(&offset).unwrap_or(&0); + (*count as f64) / (total_reads as f64) >= constants::MIN_SNP_FREQ + }); + } + } + } +} + +pub fn apply_read_filters(reads: &mut Vec, filters: &[&(dyn ReadFilter + Sync)]) { + for filter in filters { + filter.filter(reads); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::read::ReadInfoBuilder; + use itertools::Itertools; + + fn create_read_info( + mismatch_offsets: Vec, + start_offset: i32, + end_offset: i32, + ) -> ReadInfo { + ReadInfoBuilder::new() + .with_mismatch_offsets(Some(mismatch_offsets)) + .with_start_offset(Some(start_offset)) + .with_end_offset(Some(end_offset)) + .build() + .unwrap() + } + + #[test] + fn test_empty_trinary_matrix() { + let child_alleles: Vec = vec![]; + let father_alleles: Vec = vec![]; + let mother_alleles: Vec = vec![]; + let trinaray_mat: Option = + TrinaryMatrix::new(&child_alleles, &father_alleles, &mother_alleles); + assert_eq!(trinaray_mat, None); + } + + // TODO: split into multiple tests + #[test] + fn test_trinary_matrix() { + let father_alleles = vec![ + Allele::dummy(vec![ + create_read_info(vec![-8, -2, 3, 5, 8, 42], -12, 70), + create_read_info(vec![-2, 3, 5, 8], -6, 30), + create_read_info(vec![3, 5, 8, 42], 0, 50), + ]), + Allele::dummy(vec![ + create_read_info(vec![-8, -4, 2], -22, 30), + create_read_info(vec![-8, -4, 2], -40, 60), + create_read_info(vec![-8, -4, 2], -30, 90), + ]), + ]; + + let mother_alleles = vec![ + Allele::dummy(vec![ + create_read_info(vec![-4, -1, 2, 6], -5, 7), + create_read_info(vec![-4, -1, 2, 6, 23], -20, 70), + create_read_info(vec![2, 6, 23], 1, 120), + ]), + // Allele::dummy(vec![ + // create_read_info(vec![6, 23], -30, 40), + // create_read_info(vec![6, 23], -10, 90), + // create_read_info(vec![6, 23], -2, 120), + // ]), + ]; + + let child_alleles = vec![ + Allele::dummy(vec![ + create_read_info(vec![-8, -2, 3, 5, 8, 42], -12, 70), + create_read_info(vec![-8, -2, 3, 5, 8], -10, 30), + create_read_info(vec![3, 5, 8, 42], 0, 50), + ]), + Allele::dummy(vec![ + create_read_info(vec![-4, -1, 2, 6, 23], -40, 90), + create_read_info(vec![-4, -1, 2, 6], -32, 20), + create_read_info(vec![2, 6, 23], 1, 120), + ]), + Allele::dummy(vec![create_read_info(vec![], -40, 90)]), + Allele::dummy(vec![ + create_read_info(vec![-1, 2, 6], -32, 20), + create_read_info(vec![2, 6], 1, 10), + ]), + ]; + + let trinaray_mat = + TrinaryMatrix::new(&child_alleles, &father_alleles, &mother_alleles).unwrap(); + + // Test dimensionality + assert_eq!(trinaray_mat.matrix.shape(), &[18, 11]); + + assert_eq!( + get_likelihood( + trinaray_mat + .allele_submatrix(FamilyMember::Child, 0) + .unwrap(), + trinaray_mat + .allele_submatrix(FamilyMember::Child, 0) + .unwrap() + ), + 1.0 + ); + + // Counts alleles + assert_eq!(trinaray_mat.count_alleles(FamilyMember::Child), 4); + assert_eq!(trinaray_mat.count_alleles(FamilyMember::Mother), 1); + assert_eq!(trinaray_mat.count_alleles(FamilyMember::Father), 2); + + // Check for correctness of mismatch offset labels + assert_eq!( + trinaray_mat.mismatch_offsets, + vec![-8, -4, -2, -1, 2, 3, 5, 6, 8, 23, 42,] + ); + + // Row 0 should correspond to the first read of the father + assert_eq!( + trinaray_mat.matrix.row(0), + trinaray_mat + .matrix + .row(trinaray_mat.offsets[FamilyMember::Father.index()].start_offset), + ); + + // Check for correctness of matrix row 0 + assert_eq!( + trinaray_mat.matrix.row(0).to_vec(), + vec![1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1] + ); + + // Check for correctness for the first read of the mother + assert_eq!( + trinaray_mat + .matrix + .row(trinaray_mat.offsets[FamilyMember::Mother.index()].start_offset) + .to_vec(), + vec![2, 1, 0, 1, 1, 0, 0, 1, 2, 2, 2] + ); + + // Mother has only one allele so the mother submatrix should be equivalent to allele submatrix 0 + assert_eq!( + trinaray_mat.family_submatrix(FamilyMember::Mother), + trinaray_mat + .allele_submatrix(FamilyMember::Mother, 0) + .unwrap() + ); + + // Allele submatrix 1 (and beyond) of the mother should be None + assert_eq!(None, trinaray_mat.allele_submatrix(FamilyMember::Mother, 1)); + + // Child has 4 alleles, check that allele 3 is correct + assert_eq!( + trinaray_mat + .allele_submatrix(FamilyMember::Child, 2) + .unwrap() + .rows() + .into_iter() + .map(|row| row.to_vec()) + .collect_vec(), + vec![[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], + ); + } + + #[test] + fn test_filter_by_dist() { + let mut reads = vec![ + create_read_info( + vec![ + -5000, -4000, -3000, -2000, -1000, 1000, 2000, 3000, 4000, 5000, + ], + 0, + 0, + ), + create_read_info(vec![4001, 6000, 7000, 8000, 9000, 10000], 0, 0), + create_read_info(vec![0, 1000, 2000, 3000, 3999, 4000, 4001], 0, 0), + ]; + + FilterByDist.filter(&mut reads); + + assert_eq!( + reads[0].mismatch_offsets.as_ref().unwrap(), + &vec![-4000, -3000, -2000, -1000, 1000, 2000, 3000, 4000] + ); + assert_eq!(reads[1].mismatch_offsets.as_ref().unwrap(), &vec![]); + assert_eq!( + reads[2].mismatch_offsets.as_ref().unwrap(), + &vec![0, 1000, 2000, 3000, 3999, 4000] + ); + } + + #[test] + fn test_filter_by_freq() { + let mut reads = vec![ + create_read_info(vec![1000, 2000, 3000, 3002, 4000, 5000], 0, 0), + create_read_info(vec![1000, 2000, 3000, 4000, 4090, 5000], 0, 0), + create_read_info(vec![-3213, 1000, 2000, 3000, 4000, 5000], 0, 0), + create_read_info(vec![1, 2, 3, 4, 5], 0, 0), + create_read_info(vec![-3, -2, -1, 0, 2000], 0, 0), + create_read_info(vec![], 0, 0), + ]; + + FilterByFreq.filter(&mut reads); + + assert_eq!( + reads[0].mismatch_offsets.as_ref().unwrap(), + &vec![1000, 2000, 3000, 4000, 5000] + ); + assert_eq!( + reads[1].mismatch_offsets.as_ref().unwrap(), + &vec![1000, 2000, 3000, 4000, 5000] + ); + assert_eq!( + reads[2].mismatch_offsets.as_ref().unwrap(), + &vec![1000, 2000, 3000, 4000, 5000] + ); + assert_eq!(reads[3].mismatch_offsets.as_ref().unwrap(), &vec![]); + assert_eq!(reads[4].mismatch_offsets.as_ref().unwrap(), &vec![2000]); + } + + #[test] + fn test_filter_by_dist_and_freq() { + let mut reads = vec![ + create_read_info( + vec![ + -5000, -4000, -3000, -2000, -1000, 1000, 2000, 3000, 3002, 4000, 5000, + ], + 0, + 0, + ), + create_read_info( + vec![ + -5000, -4000, -3000, -2000, -1000, 1000, 2000, 3000, 4000, 4090, 5000, + ], + 0, + 0, + ), + create_read_info(vec![-3213, 1000, 2000, 3000, 4000, 5000], 0, 0), + create_read_info(vec![1, 2, 3, 4, 5], 0, 0), + create_read_info(vec![-3, -2, -1, 0, 2000], 0, 0), + create_read_info(vec![], 0, 0), + ]; + + FilterByDist.filter(&mut reads); + FilterByFreq.filter(&mut reads); + + assert_eq!( + reads[0].mismatch_offsets.as_ref().unwrap(), + &vec![-4000, -3000, -2000, -1000, 1000, 2000, 3000, 4000] + ); + assert_eq!( + reads[1].mismatch_offsets.as_ref().unwrap(), + &vec![-4000, -3000, -2000, -1000, 1000, 2000, 3000, 4000] + ); + assert_eq!( + reads[2].mismatch_offsets.as_ref().unwrap(), + &vec![1000, 2000, 3000, 4000] + ); + assert_eq!(reads[3].mismatch_offsets.as_ref().unwrap(), &vec![]); + assert_eq!(reads[4].mismatch_offsets.as_ref().unwrap(), &vec![2000]); + } +} diff --git a/src/stats.rs b/src/stats.rs new file mode 100644 index 0000000..001abf2 --- /dev/null +++ b/src/stats.rs @@ -0,0 +1,28 @@ +use crate::allele::Allele; + +pub fn get_per_allele_reads(alleles: &[Allele]) -> Vec { + alleles.iter().map(|a| a.read_aligns.len()).collect() +} + +pub fn get_total_reads(alleles: &[Allele]) -> usize { + get_per_allele_reads(alleles).iter().sum() +} + +pub fn get_dropout_prob(alleles: &[Allele]) -> f64 { + assert!(!alleles.is_empty()); + let allele_frac: f64 = 0.5; + let num_reads = get_total_reads(alleles) as f64; + allele_frac.powf(num_reads) +} + +pub fn get_allele_freqs(alleles: &[Allele]) -> Vec { + let num_reads = get_total_reads(alleles) as f64; + let allele_freqs: Vec = alleles + .iter() + .map(|a| a.read_aligns.len() as f64 / num_reads) + .collect(); + allele_freqs +} + +#[cfg(test)] +mod tests {} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..65d6467 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,17 @@ +use anyhow::anyhow; +use log; +use std::path::Path; + +pub type Result = anyhow::Result; + +pub fn handle_error_and_exit(err: anyhow::Error) -> ! { + log::error!("{:#}", err); + std::process::exit(1); +} + +pub fn try_exists(path: &Path) -> Result<()> { + if !path.exists() { + return Err(anyhow!("Path does not exist: {}", path.display())); + } + Ok(()) +} diff --git a/src/wfa2.rs b/src/wfa2.rs new file mode 100644 index 0000000..d9c0de6 --- /dev/null +++ b/src/wfa2.rs @@ -0,0 +1,2 @@ +//! Re-export wfa2-sys bindings +pub use wfa2_sys::*;