From 8fd37eb00c51fe290dbf98bc473513ef96bab56d Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Mon, 23 Dec 2024 15:26:32 +0100 Subject: [PATCH] Breaking changes for v1 (#35) In this mega-commit, Kmers.jl is thoroughly overhauled, with a new API, new docs, and new types. --- .JuliaFormatter.toml | 8 + .github/workflows/Documentation.yml | 16 +- .github/workflows/UnitTests.yml | 19 +- .gitignore | 4 +- Project.toml | 26 +- README.md | 58 +- docs/Project.toml | 6 +- docs/make.jl | 50 +- docs/src/composition.md | 52 ++ docs/src/construction.md | 12 - docs/src/faq.md | 40 + docs/src/hashing.md | 60 ++ docs/src/index.md | 60 +- docs/src/iteration.md | 106 ++- docs/src/kmer_types.md | 92 --- docs/src/kmers.md | 201 +++++ docs/src/minhash.md | 42 + docs/src/predicates.md | 18 - docs/src/random.md | 14 - docs/src/replacements.md | 59 ++ docs/src/skipmers.png | Bin 214672 -> 0 bytes docs/src/transforms.md | 52 -- docs/src/translate.md | 62 -- docs/src/translation.md | 67 ++ ext/StringViewsExt.jl | 11 + src/Kmers.jl | 221 +++-- src/construction.jl | 359 ++++++++ src/construction_utils.jl | 236 ++++++ src/counting.jl | 47 -- src/indexing.jl | 105 ++- src/iterators/CanonicalKmers.jl | 225 +++++ src/iterators/FwKmers.jl | 129 +++ src/iterators/SpacedKmers.jl | 139 ++++ src/iterators/UnambiguousKmers.jl | 148 ++++ src/iterators/common.jl | 32 + src/kmer.jl | 840 +++++++++---------- src/kmer_iteration/AbstractKmerIterator.jl | 36 - src/kmer_iteration/EveryCanonicalKmer.jl | 122 --- src/kmer_iteration/EveryKmer.jl | 124 --- src/kmer_iteration/SpacedCanonicalKmers.jl | 133 --- src/kmer_iteration/SpacedKmers.jl | 127 --- src/predicates.jl | 11 - src/revtrans.jl | 55 +- src/transformations.jl | 276 ++----- src/tuple_bitflipping.jl | 98 +-- sticker.svg | 191 ++++- test/access.jl | 96 --- test/benchmark.jl | 125 +++ test/biosequences_interface.jl | 12 - test/comparisons.jl | 64 -- test/construction_and_conversion.jl | 170 ---- test/debruijn_neighbors.jl | 6 - test/find.jl | 46 -- test/iteration.jl | 214 ----- test/length.jl | 7 - test/mismatches.jl | 51 -- test/order.jl | 7 - test/print.jl | 37 - test/random.jl | 26 - test/runtests.jl | 904 ++++++++++++++++++++- test/shuffle.jl | 25 - test/transformations.jl | 60 -- test/translation.jl | 272 ++++--- test/utils.jl | 76 +- 64 files changed, 4097 insertions(+), 2890 deletions(-) create mode 100644 .JuliaFormatter.toml create mode 100644 docs/src/composition.md delete mode 100644 docs/src/construction.md create mode 100644 docs/src/faq.md create mode 100644 docs/src/hashing.md delete mode 100644 docs/src/kmer_types.md create mode 100644 docs/src/kmers.md create mode 100644 docs/src/minhash.md delete mode 100644 docs/src/predicates.md delete mode 100644 docs/src/random.md create mode 100644 docs/src/replacements.md delete mode 100644 docs/src/skipmers.png delete mode 100644 docs/src/transforms.md delete mode 100644 docs/src/translate.md create mode 100644 docs/src/translation.md create mode 100644 ext/StringViewsExt.jl create mode 100644 src/construction.jl create mode 100644 src/construction_utils.jl delete mode 100644 src/counting.jl create mode 100644 src/iterators/CanonicalKmers.jl create mode 100644 src/iterators/FwKmers.jl create mode 100644 src/iterators/SpacedKmers.jl create mode 100644 src/iterators/UnambiguousKmers.jl create mode 100644 src/iterators/common.jl delete mode 100644 src/kmer_iteration/AbstractKmerIterator.jl delete mode 100644 src/kmer_iteration/EveryCanonicalKmer.jl delete mode 100644 src/kmer_iteration/EveryKmer.jl delete mode 100644 src/kmer_iteration/SpacedCanonicalKmers.jl delete mode 100644 src/kmer_iteration/SpacedKmers.jl delete mode 100644 src/predicates.jl delete mode 100644 test/access.jl create mode 100644 test/benchmark.jl delete mode 100644 test/biosequences_interface.jl delete mode 100644 test/comparisons.jl delete mode 100644 test/construction_and_conversion.jl delete mode 100644 test/debruijn_neighbors.jl delete mode 100644 test/find.jl delete mode 100644 test/iteration.jl delete mode 100644 test/length.jl delete mode 100644 test/mismatches.jl delete mode 100644 test/order.jl delete mode 100644 test/print.jl delete mode 100644 test/random.jl delete mode 100644 test/shuffle.jl delete mode 100644 test/transformations.jl diff --git a/.JuliaFormatter.toml b/.JuliaFormatter.toml new file mode 100644 index 0000000..903da37 --- /dev/null +++ b/.JuliaFormatter.toml @@ -0,0 +1,8 @@ +always_for_in = true +whitespace_typedefs = true +whitespace_ops_in_indices = true +remove_extra_newlines = true +import_to_using = true +normalize_line_endings = "unix" +separate_kwargs_with_semicolon = true +whitespace_in_kwargs = false diff --git a/.github/workflows/Documentation.yml b/.github/workflows/Documentation.yml index 35d4097..9bf03bd 100644 --- a/.github/workflows/Documentation.yml +++ b/.github/workflows/Documentation.yml @@ -10,17 +10,13 @@ on: pull_request: jobs: - build: + Documenter: + name: Documentation runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: julia-actions/setup-julia@v1 - with: - version: '1' - - name: Install dependencies - run: julia --color=yes --project=docs/ -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()' - - name: Build and deploy + - uses: julia-actions/julia-buildpkg@latest + - uses: julia-actions/julia-docdeploy@latest env: - # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # For authentication with GitHub Actions token - DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} # For authentication with SSH deploy key - run: julia --color=yes --project=docs/ docs/make.jl + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} diff --git a/.github/workflows/UnitTests.yml b/.github/workflows/UnitTests.yml index cff1346..d8ee908 100644 --- a/.github/workflows/UnitTests.yml +++ b/.github/workflows/UnitTests.yml @@ -11,31 +11,30 @@ jobs: strategy: fail-fast: false matrix: - julia-version: - - '1.6' # LTS - - '1' - julia-arch: [x86] - os: [ubuntu-latest, windows-latest, macOS-latest] + julia-version: ['1', '1.10'] + os: [ubuntu-latest, macOS-latest, windows-latest] experimental: [false] include: + # Include nightly, but experimental, so it's allowed to fail without + # failing CI. - julia-version: nightly - julia-arch: x86 os: ubuntu-latest experimental: true + fail_ci_if_error: false steps: - name: Checkout Repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup Julia - uses: julia-actions/setup-julia@v1 + uses: julia-actions/setup-julia@latest with: version: ${{ matrix.julia-version }} - name: Run Tests uses: julia-actions/julia-runtest@latest - name: Create CodeCov - uses: julia-actions/julia-processcoverage@v1 + uses: julia-actions/julia-processcoverage@latest - name: Upload CodeCov - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v4 with: file: ./lcov.info flags: unittests diff --git a/.gitignore b/.gitignore index 02e1207..37956a9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ *.jl.*.cov *.jl.mem .DS_Store -Manifest.toml \ No newline at end of file +Manifest.toml +TODO.md +docs/build diff --git a/Project.toml b/Project.toml index 28dacf5..5b81b21 100644 --- a/Project.toml +++ b/Project.toml @@ -1,17 +1,33 @@ name = "Kmers" uuid = "445028e4-d31f-4f27-89ad-17affd83fc22" -authors = ["Sabrina Jaye Ward "] -version = "0.1.0" +authors = [ + "Jakob Nybo Nissen ", + "Sabrina Jaye Ward " +] +version = "1.0.0" + +[weakdeps] +StringViews = "354b36f9-a18e-4713-926e-db85100087ba" [deps] BioSequences = "7e6ae17a-c86d-528c-b3b9-7f778a29fe59" +BioSymbols = "3c28c6f8-a34d-59c4-9654-267d177fcfa9" + +[extensions] +StringViewsExt = "StringViews" +# Note: We intentionally have strict compat on BioSequences because Kmers +# reaches into the internals of BioSequences. [compat] -BioSequences = "3.1.3" -julia = "1.5" +BioSequences = "~3.4.1" +Random = "1.10" +julia = "1.10" +StringViews = "1" [extras] Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +StringViews = "354b36f9-a18e-4713-926e-db85100087ba" [targets] -test = ["Test"] +test = ["Test", "Random", "StringViews"] diff --git a/README.md b/README.md index ed51eff..2dd7595 100644 --- a/README.md +++ b/README.md @@ -3,57 +3,57 @@ [![Latest Release](https://img.shields.io/github/release/BioJulia/Kmers.jl.svg)](https://github.com/BioJulia/Kmers.jl/releases/latest) [![MIT license](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/BioJulia/Kmers.jl/blob/master/LICENSE) [![Documentation](https://img.shields.io/badge/docs-stable-blue.svg)](https://biojulia.github.io/Kmers.jl/stable) -[![Pkg Status](http://www.repostatus.org/badges/latest/active.svg)](http://www.repostatus.org/#active) - ## Description +Kmers.jl provide the `Kmer <: BioSequence` type which implement the concept of a +[k-mer](https://en.wikipedia.org/wiki/K-mer), a biological sequence of exactly length `k`. -Kmers provides a specialised concrete `BioSequence` subtype, optimised for -representing short immutable sequences called kmers: contiguous sub-strings of k -nucleotides of some reference sequence. -They are used extensively in bioinformatic analyses as an informational unit. -This concept was popularised by short read assemblers. -Analyses within the kmer space benefit from a simple formulation of the sampling -problem and direct in-hash comparisons. +K-mers are used frequently in bioinformatics because, when k is small and known at +compile time, these sequences can be efficiently represented as integers and stored +directly in CPU registers, allowing for much more efficient computation than arbitrary-length sequences. -Kmers provides the type representing kmers as well as the implementations of -the APIs specified by the -[`BioSequences.jl`](https://github.com/BioJulia/BioSequences.jl) package. +In Kmers.jl, the `Kmer` type is psrameterized by its length, and its data is stored in an `NTuple`. This makes `Kmers` bitstypes and highly efficient. -## Installation +Conceptually, one may use the following analogy: +* `BioSequence` is like `AbstractVector` +* `LongSequence` is like `Vector` +* `Kmer` is like [`SVector`](https://github.com/JuliaArrays/StaticArrays.jl) from `StaticArrays` + +Kmers.jl is tightly coupled to the +[`BioSequences.jl`](https://github.com/BioJulia/BioSequences.jl) package, +and relies on its internals. +Hence, you should expect strict compat bounds on BioSequences.jl. + +## Usage +### ⚠️ WARNING ⚠️ +`Kmer`s are parameterized by their length. That means any operation on `Kmer`s that change their length, such as `push`, `pop`, slicing, or masking (logical indexing) will be **type unstable** and hence slow and memory inefficient, unless you write your code in such as way that the compiler can use constant folding. +Further, as `Kmer`s are immutable and their operations are aggressively inlined and unrolled, +they become inefficent as they get longer. +For example, reverse-complementing a 32-mer takes 26 ns, compared to 102 ns for the equivalent `LongSequence`. However, for 512-mers, the `LongSequence` takes 126 ns, and the `Kmer` 16 μs! + +Kmers.jl is intended for high-performance computing. If you do not need the extra performance that register-stored sequences provide, you might consider using `LongSequence` from BioSequences.jl instead + +## Installation You can install BioSequences from the julia REPL. Press `]` to enter pkg mode, and enter the following: ```julia -add Kmers +pkg> add Kmers ``` -If you are interested in the cutting edge of the development, please check out +If you are interested in the cutting edge of development, please check out the master branch to try new features before release. - -## Testing - -Kmers is tested against Julia `1.X` on Linux, OS X, and Windows. - -[![Unit tests](https://github.com/BioJulia/Kmers.jl/workflows/Unit%20tests/badge.svg?branch=master)](https://github.com/BioJulia/Kmers.jl/actions?query=workflow%3A%22Unit+tests%22+branch%3Amaster) -[![Documentation](https://github.com/BioJulia/Kmers.jl/workflows/Documentation/badge.svg?branch=master)](https://github.com/BioJulia/BioKmers.jl/actions?query=workflow%3ADocumentation+branch%3Amaster) -[![](https://codecov.io/gh/BioJulia/Kmers.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/BioJulia/Kmers.jl) - - ## Contributing - We appreciate contributions from users including reporting bugs, fixing issues, improving performance and adding new features. Take a look at the [contributing files](https://github.com/BioJulia/Contributing) detailed contributor and maintainer guidelines, and code of conduct. - ## Questions? - If you have a question about contributing or using BioJulia software, come -on over and chat to us on [Gitter](https://gitter.im/BioJulia/General), or you can try the +on over and chat to us on [the Julia Slack workspace](https://julialang.org/slack/), or you can try the [Bio category of the Julia discourse site](https://discourse.julialang.org/c/domain/bio). diff --git a/docs/Project.toml b/docs/Project.toml index e064fd1..2d21498 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,5 +1,9 @@ [deps] +BioSequences = "7e6ae17a-c86d-528c-b3b9-7f778a29fe59" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +FASTX = "c2308a5c-f048-11e8-3e8a-31650f418d12" +Kmers = "445028e4-d31f-4f27-89ad-17affd83fc22" +MinHash = "4b3c9753-2685-44e9-8a29-365b96c023ed" [compat] -Documenter = "0.24" \ No newline at end of file +Documenter = "1" diff --git a/docs/make.jl b/docs/make.jl index 61b4844..32919be 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,29 +1,33 @@ using Documenter, Kmers -makedocs( - format = Documenter.HTML(), - sitename = "Kmers.jl", - pages = [ - "Home" => "index.md", - "Kmer types" => "kmer_types.md", - "Constructing kmers" => "construction.md", - "Indexing & modifying kmers" => "transforms.md", - "Predicates" => "predicates.md", - "Random kmers" => "random.md", - "Iterating over Kmers" => "iteration.md", - "Translation" => "translate.md", - #"Pattern matching and searching" => "sequence_search.md", - #"Iteration" => "iteration.md", - #"Counting" => "counting.md", - #"I/O" => "io.md", - #"Interfaces" => "interfaces.md" +DocMeta.setdocmeta!( + Kmers, + :DocTestSetup, + :(using BioSequences, Kmers, Test); + recursive=true, +) + +makedocs(; + modules=[Kmers], + format=Documenter.HTML(; prettyurls=get(ENV, "CI", nothing) == "true"), + sitename="Kmers.jl", + pages=[ + "Home" => "index.md", + "The Kmer type" => "kmers.md", + "Iteration" => "iteration.md", + "Translation" => "translation.md", + "Hashing" => "hashing.md", + "K-mer replacements" => "replacements.md", + "FAQ" => "faq.md", + "Cookbook" => ["MinHash" => "minhash.md", "Kmer composition" => "composition.md"], ], - authors = "Ben J. Ward, The BioJulia Organisation and other contributors." + authors="Jakob Nybo Nissen, Sabrina J. Ward, The BioJulia Organisation and other contributors.", + checkdocs=:exports, ) -deploydocs( - repo = "github.com/BioJulia/Kmers.jl.git", - push_preview = true, - deps = nothing, - make = nothing +deploydocs(; + repo="github.com/BioJulia/Kmers.jl.git", + push_preview=true, + deps=nothing, + make=nothing, ) diff --git a/docs/src/composition.md b/docs/src/composition.md new file mode 100644 index 0000000..ac9cc4f --- /dev/null +++ b/docs/src/composition.md @@ -0,0 +1,52 @@ +```@meta +CurrentModule = Kmers +DocTestSetup = quote + using BioSequences + using Test + using Kmers +end +``` +## Kmer composition +In metagenomics, sequences are often summarized by counting the occurrence of +all k-mers of a given length in a sequence. +For example, for K=4, there are 4^4 = 256 possible DNA 4-mers. +If these counts are ordered, the composition can be represented by a length 256 +vector. + +Vector similarity operations (e.g. cosine distance) can then be used as an +approximate proxy for phylogenetic distance. + +In the example below, we exploit that: +* A `DNAKmer{4}`'s data is a single-element tuple, which + stores the sequence in the 8 lower bits. +* The `encoded_data` function will return this tuple. + +```jldoctest; output=false +using BioSequences, FASTX, Kmers +using BioSequences: encoded_data + +function composition(record::FASTARecord) + counts = zeros(UInt32, 256) + frequencies = zeros(Float32, 256) + for kmer in FwDNAMers{4}(sequence(record)) + @inbounds counts[only(encoded_data(kmer)) + 1] += 1 + end + factor = 1 / sum(counts; init=zero(eltype(counts))) + for i in eachindex(counts, frequencies) + frequencies[i] = counts[i] * factor + end + frequencies +end + +# Make two FASTA records - could be from an assembly +recs = [FASTARecord(string(i), randdnaseq(10000)) for i in "AB"] + +# Compute the 2-norm difference and verify it's in [0, 2]. +(comp_a, comp_b) = map(composition, recs) +comp_distance = sum((comp_a .- comp_b).^2) +println(0.0 ≤ comp_distance ≤ 2.0) + +# output +true + +``` \ No newline at end of file diff --git a/docs/src/construction.md b/docs/src/construction.md deleted file mode 100644 index 6b300a9..0000000 --- a/docs/src/construction.md +++ /dev/null @@ -1,12 +0,0 @@ -```@meta -CurrentModule = Kmers -DocTestSetup = quote - using Kmers -end -``` - -# Construction & conversion - -```@docs -Kmer{A,K,N}(itr) -``` diff --git a/docs/src/faq.md b/docs/src/faq.md new file mode 100644 index 0000000..970e095 --- /dev/null +++ b/docs/src/faq.md @@ -0,0 +1,40 @@ +```@meta +CurrentModule = Kmers +DocTestSetup = quote + using BioSequences + using Test + using Kmers +end +``` +## FAQ +### Why can kmers not be compared to biosequences? +It may be surprising that kmers cannot be compared to other biosequences: + +```jldoctest +julia> dna"TAG" == mer"TAG"d +ERROR: MethodError +[...] +``` + +In fact, this is implemented by a manually thrown `MethodError`; the generic case `Base.:==(::BioSequence, ::BioSequence)` is defined. + +The reason for this is the consequence of the following limitations: +* `isequal(x, y)` implies `hash(x) == hash(y)` +* `isequal(x, y)` and `x == y` ought to be identical for well-defined elements (i.e. in the absence of `missing`s and `NaN`s etc.) +* `hash(::Kmer)` must be absolutely maximally efficient + +If kmers were to be comparable to `BioSequence`, then the hashing of `BioSequence` should follow `Kmer`, which practically speaking would mean that all biosequences would need to be recoded to `Kmer`s before hashing. + +### Why isn't there an iterator of unambiguous, canonical kmers or spaced, canonical kmers? +Any iterator of nucleotide kmers can be made into a canonical kmer iterator by simply calling `canonical` on its output kers. + +The `CanonicalKmers` iterator is special cased, because with a step size of 1, it is generally faster to build the next kmer by storing both the reverse and forward kmer, then creating the next kmer by prepending/append the next symbol. + +However, with a larger step size, it becomes more efficient to build the forward kmer, then reverse-complement the whole kmer. + +### Why isn't there an iterator of skipmers/minimizers/k-min-mers, etc? +The concept of kmers have turned out to be remarkably flexible and useful in bioinformatics, and have spawned a neverending stream of variations. +We simply can't implement them all. + +However, see the section [Building kmer replacements](@ref replacements) on how to implement them +as a user of Kmers.jl yourself. diff --git a/docs/src/hashing.md b/docs/src/hashing.md new file mode 100644 index 0000000..1ac8125 --- /dev/null +++ b/docs/src/hashing.md @@ -0,0 +1,60 @@ +```@meta +CurrentModule = Kmers +DocTestSetup = quote + using BioSequences + using Test + using Kmers +end +``` + +!!! warning + The value of hashes are guaranteed to be reproducible for a given version + of Kmers.jl and Julia, but may __change__ in new minor versions of Julia + or Kmers.jl + +## Hashing +Kmers.jl implements `Base.hash`, yielding a `UInt` value: + +```jldoctest; filter = r"^0x[0-9a-fA-F]+$" +julia> hash(mer"UGCUGUAC"r) +0xe5057d38c8907b22 +``` + +The implementation of `Base.hash` for kmers strikes a compromise between providing a high-quality (non-cryptographic) hash, while being reasonably fast. +While hash collisions can easily be found, they are unlikely to occur at random. +When kmers are of the same (or compatible) alphabets, different kmers hash to different values +(not counting the occational hash collision), even when they have the same underlying bitpattern: + +```jldoctest +julia> using BioSequences: encoded_data + +julia> a = mer"TAG"d; b = mer"AAAAAAATAG"d; + +julia> encoded_data(a) === encoded_data(b) +true + +julia> hash(a) == hash(b) +false +``` + +When they are of compatible alphabets, and have the same content, they hash to the same value. +Currently, only DNA and RNA of the alphabets `DNAAlphabet` and `RNAAlphabet` are compatible: + +```jldoctest +julia> a = mer"UUGU"r; b = mer"TTGT"d; + +julia> a == b # equal +true + +julia> a === b # not egal +false + +julia> hash(a) === hash(b) +true +``` + +For some applications, fast hashing is absolutely crucial. For these cases, Kmers.jl provides [`fx_hash`](@ref), which trades off hash quality for speed: + +```@docs +fx_hash +``` \ No newline at end of file diff --git a/docs/src/index.md b/docs/src/index.md index 11c4fe9..b3ca804 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -1,59 +1,53 @@ -# Kmers +# Kmers.jl +Kmers.jl provides the `Kmer <: BioSequence` type which implement the concept of a +[k-mer](https://en.wikipedia.org/wiki/K-mer), a biological sequence of exactly length `k`. -[![Latest Release](https://img.shields.io/github/release/BioJulia/Kmers.jl.svg)](https://github.com/BioJulia/Kmers.jl/releases/latest) -[![MIT license](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/BioJulia/Kmers.jl/blob/master/LICENSE) -[![Documentation](https://img.shields.io/badge/docs-stable-blue.svg)](https://biojulia.github.io/Kmers.jl/stable) -[![Pkg Status](http://www.repostatus.org/badges/latest/active.svg)](http://www.repostatus.org/#active) +Compared to other `BioSequence`s, `Kmer`s are characterized by: +* Being immutable bitstypes +* Being parameterized by its length, such that the length can be known at compile time. +When kmers are short, and their length is known at compile time, +these characteristics allow k-mers to be stack allocated, or stored in registers, +allowing for much more efficient computation than arbitrary-length sequences. -## Description +Conceptually, one may use the following analogy: +* `BioSequence` is like `AbstractVector` +* `LongSequence` is like `Vector` +* `Kmer` is like [`SVector`](https://github.com/JuliaArrays/StaticArrays.jl) from `StaticArrays` -Kmers provides a specialised concrete `BioSequence` subtype, optimised for -representing short immutable sequences called kmers: contiguous sub-strings of k -nucleotides of some reference sequence. +Kmers.jl is tightly coupled to the +[`BioSequences.jl`](https://github.com/BioJulia/BioSequences.jl) package, +and relies on its internals. +Hence, you should expect strict compat bounds on BioSequences.jl. -They are used extensively in bioinformatic analyses as an informational unit. -This concept was popularised by short read assemblers. -Analyses within the kmer space benefit from a simple formulation of the sampling -problem and direct in-hash comparisons. +!!! warning + `Kmer`s are parameterized by their length. + That means any operation on `Kmer`s that change their length, such as `push`, + `pop`, slicing, or masking (logical indexing) will be **type unstable** + and hence slow and memory inefficient, + unless you write your code in such as way that the compiler can use constant folding. -Kmers provides the type representing kmers as well as the implementations of -the APIs specified by the -[`BioSequences.jl`](https://github.com/BioJulia/BioSequences.jl) package. +Kmers.jl is intended for high-performance computing. If you do not need the extra performance that register-stored sequences provide, you might consider using `LongSequence` from BioSequences.jl instead ## Installation - You can install BioSequences from the julia REPL. Press `]` to enter pkg mode, and enter the following: ```julia -add Kmers +pkg> add Kmers ``` -If you are interested in the cutting edge of the development, please check out +If you are interested in the cutting edge of development, please check out the master branch to try new features before release. - -## Testing - -Kmers is tested against Julia `1.X` on Linux, OS X, and Windows. - -[![Unit tests](https://github.com/BioJulia/Kmers.jl/workflows/Unit%20tests/badge.svg?branch=master)](https://github.com/BioJulia/Kmers.jl/actions?query=workflow%3A%22Unit+tests%22+branch%3Amaster) -[![Documentation](https://github.com/BioJulia/Kmers.jl/workflows/Documentation/badge.svg?branch=master)](https://github.com/BioJulia/BioKmers.jl/actions?query=workflow%3ADocumentation+branch%3Amaster) -[![](https://codecov.io/gh/BioJulia/Kmers.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/BioJulia/Kmers.jl) - - ## Contributing - We appreciate contributions from users including reporting bugs, fixing issues, improving performance and adding new features. Take a look at the [contributing files](https://github.com/BioJulia/Contributing) detailed contributor and maintainer guidelines, and code of conduct. - ## Questions? - If you have a question about contributing or using BioJulia software, come -on over and chat to us on [Gitter](https://gitter.im/BioJulia/General), or you can try the +on over and chat to us on [the Julia Slack workspace](https://julialang.org/slack/), or you can try the [Bio category of the Julia discourse site](https://discourse.julialang.org/c/domain/bio). diff --git a/docs/src/iteration.md b/docs/src/iteration.md index 3b36181..bf37d76 100644 --- a/docs/src/iteration.md +++ b/docs/src/iteration.md @@ -1,31 +1,105 @@ ```@meta CurrentModule = Kmers DocTestSetup = quote + using BioSequences + using Test using Kmers end ``` +## Iteration +Most applications of kmers extract multiple kmers from an underlying sequence. +To facilitate this, Kmers.jl implements a few basic kmer iterators, most of which are subtypes of `AbstractKmerIterator`. -# Iterating over kmers +The underlying sequence can be a `BioSequence`, `AbstractString`, or `AbstractVector{UInt8}`. +In the latter case, if the alphabet of the element type implements `BioSequences.AsciiAlphabet`, the vector will be treated as a vector of ASCII characters. -When introducing the `Kmer` type we described kmers as contiguous sub-strings of -k nucleotides of some reference sequence. +Similarly to the rules when constructing kmers directly, DNA and RNA is treated interchangeably when the underlying sequence is a `BioSequence`, but when the underlying sequence is a string or bytevector, `U` and `T` are considered different, and e.g. uracil cannot be constructed from a sequence containing `T`: -This package therefore contains functionality for iterating over all the valid -`Kmers{A,K,N}` in a longer `BioSequence`. +```jldoctest +julia> only(FwDNAMers{3}(rna"UGU")) +DNA 3-mer: +TGT + +julia> only(FwDNAMers{3}("UGU")) +ERROR: +[...] +``` + +The following kmer iterators are implemented: + +### `FwKmers` +The most basic kmer iterator is `FwKmers`, which simply iterates every kmer, in order: + +```@docs +FwKmers +FwDNAMers +FwRNAMers +FwAAMers +``` + +### `FwRvIterator` +This iterates over a nucleic acid sequence. For every kmer it encounters, it outputs the kmer and its reverse complement. + +```@docs +FwRvIterator +``` + +### `CanonicalKmers` +This iterator is similar to [`FwKmers`](@ref), however, for each `Kmer` encountered, it returns the _canonical_ kmer. + +The canonical kmer is defined as the lexographically smaller of a kmer and its reverse complement. +That is, if [`FwKmers`](@ref) would iterate `TCAC`, then [`CanonicalKmers`](@ref) would return `GTGA`, as this is the reverse complement of `TCAC`, and is before `TCAC` in the alphabet. + +[`CanonicalKmers`](@ref) is useful for summarizing the kmer composition of sequences whose strandedness is unknown. + +```@docs +CanonicalKmers +CanonicalDNAMers +CanonicalRNAMers +``` + +### `UnambiguousKmers` +[`UnambiguousKmers`](@ref) iterates unambiguous nucleotides (that is, kmers of the alphabets `DNAAlphabet{2}` or `RNAAlphabet{2}`). +Any kmers containing [ambiguous nucleotides](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC341218/) such as `W` or `N` are skipped. + +```@docs +UnambiguousKmers +UnambiguousDNAMers +UnambiguousRNAMers +``` + +### `SpacedKmers` +The [`SpacedKmers`](@ref) iterator iterates kmers with a fixed step size between k-mers. +For example, for a K of 4, and a step size of 3, the output kmers would overlap with a single nucleotide, like so: + +``` +seq: TGATGCGTAGTG + TGCT + TGCG + GTAG +``` + +Hence, if `FwKmers` are analogous to `UnitRange`, `SpacedKmers` is analogous to `StepRange`. ```@docs -EveryKmer -EveryKmer{T,S}(seq::S, start::Int = firstindex(seq), stop::Int = lastindex(seq)) where {T<:Kmer,S<:BioSequence} -EveryKmer(seq::BioSequence{A}, ::Val{K}, start = firstindex(seq), stop = lastindex(seq)) where {A,K} SpacedKmers -SpacedKmers{T,S}(seq::S, step::Int, start::Int, stop::Int) where {T<:Kmer,S<:BioSequence} -SpacedKmers(seq::BioSequence{A}, ::Val{K}, step::Int, start = firstindex(seq), stop = lastindex(seq)) where {A,K} -EveryCanonicalKmer -EveryCanonicalKmer{T}(seq::S, start = firstindex(seq), stop = lastindex(seq)) where {T<:Kmer,S<:BioSequence} -EveryCanonicalKmer(seq::BioSequence{A}, ::Val{K}, start = firstindex(seq), stop = lastindex(seq)) where {A,K} -SpacedCanonicalKmers -SpacedCanonicalKmers{T}(seq::S, step::Int, start = firstindex(seq), stop = lastindex(seq)) where {T<:Kmer,S<:BioSequence} -SpacedCanonicalKmers(seq::BioSequence{A}, ::Val{K}, step::Int, start = firstindex(seq), stop = lastindex(seq)) where {A,K} +SpacedDNAMers +SpacedRNAMers +SpacedAAMers +``` + +The convenience functions [`each_codon`](@ref) return `SpacedKmers` with a K value of 3 and step size of 3: + +```@docs +each_codon ``` +## The `AbstractKmerIterator` interface +It's very likely that users of Kmers.jl need to implement their own custom kmer iterators, in which case they should subtype [`AbstractKmerIterator`](@ref). + +```@docs +AbstractKmerIterator +``` +At the moment, there is no real interface implemented for this abstract type, +other than that `AbstractKmerIterator{A, K}` needs to iterate `Kmer{A, K}`. diff --git a/docs/src/kmer_types.md b/docs/src/kmer_types.md deleted file mode 100644 index aab9330..0000000 --- a/docs/src/kmer_types.md +++ /dev/null @@ -1,92 +0,0 @@ -```@meta -CurrentModule = Kmers -DocTestSetup = quote - using Kmers -end -``` - -# Kmer types - -Bioinformatic analyses make extensive use of kmers. -Kmers are contiguous sub-strings of k nucleotides of some reference sequence. - -They are used extensively in bioinformatic analyses as an informational unit. -This concept popularised by short read assemblers. -Analyses within the kmer space benefit from a simple formulation of the sampling -problem and direct in-hash comparisons. - -BioSequences provides the following types to represent Kmers. - -```@docs -Kmer -``` - -The following aliases are also defined: - -```@docs -DNAKmer -DNA27mer -DNA31mer -DNA63mer -RNAKmer -RNA27mer -RNA31mer -RNA63mer -AAKmer -DNACodon -RNACodon -``` - - -### Skipmers - -For some analyses, the contiguous nature of kmers imposes limitations. -A single base difference, due to real biological variation or a sequencing error, -affects all k-mers crossing that position thus impeding direct analyses by identity. -Also, given the strong interdependence of local sequence, contiguous sections -capture less information about genome structure, and so they are more affected by -sequence repetition. - -Skipmers are a generalisation of the concept of a kmer. -They are created using a cyclic pattern of used-and-skipped positions which -achieves increased entropy and tolerance to nucleotide substitution differences -by following some simple rules. - -Skipmers preserve many of the elegant properties of kmers such as reverse -complementability and existence of a canonical representation. -Also, using cycles of three greatly increases the power of direct intersection -between the genomes of different organisms by grouping together the more conserved -nucleotides of protein-coding regions. - -BioSequences currently does not provide a separate type for skipmers, they are -represented using `Mer` and `BigMer` as their representation as a short immutable -sequence encoded in an unsigned integer is the same. -The distinction lies in how they are generated. - -#### Skipmer generation - -A skipmer is a simple cyclic q-gram that includes _m_ out of every _n_ bases -until a total of _k_ bases is reached. - -This is illustrated in the figure below (from -[this paper](https://www.biorxiv.org/content/biorxiv/early/2017/08/23/179960.full.pdf).): - -![skipmer-fig](skipmers.png) - -To maintain cyclic properties and the existence of the reverse-complement as a -skipmer defined by the same function, _k_ should be a multiple of _m_. - -This also enables the existence of a canonical representation for each skipmer, -defined as the lexicographically smaller of the forward and reverse-complement -representations. - -Defining _m_, _n_ and _k_ fixes a value for _S_, the total span of the skipmer, -given by: - -```math -S = n * (\frac{k}{m} - 1) + m -``` - -To see how to iterate over skipmers cf. kmers, see the Iteration section -of the manual. - diff --git a/docs/src/kmers.md b/docs/src/kmers.md new file mode 100644 index 0000000..d2d9dab --- /dev/null +++ b/docs/src/kmers.md @@ -0,0 +1,201 @@ +```@meta +CurrentModule = Kmers +DocTestSetup = quote + using BioSequences + using Test + using Kmers +end +``` + +## The `Kmer` type +The central type of Kmers.jl is the `Kmer`. +A `Kmer` is an immutable, bitstype `BioSequence`, with a length known at compile +time. Compared to `LongSequence` in BioSequences.jl, +this gives to one advantage, and comes with two disadvantages: +* Kmers are much faster than `LongSequence`, as they can be stored in registers. +* As kmers gets longer, the code gets increasingly inefficient, as the unrolling + and inlining of the immutable operations breaks down. +* Since their length is part of their type, any operation that results in a kmer + whose length cannot be determined at compile time will be type unstable. + This includes slicing a kmer, pushing and popping it, and other operations. + +The `Kmer` type is (roughly) defined as +```julia +struct Kmer{A <: Alphabet, K, N} <: BioSequence{A} + x::NTuple{N, UInt} +end +``` +Where: +* `A` is the `Alphabet` as defined in BioSequences.jl. +* `K` is the length. +* `N` is an extra type parameter derived from the first two used for the + length of the tuple, + which exists only because Julia does not allow computed type parameters. + +Given a `Kmer{A, K}`, the `N` parameter may be computed using the function +[`derive_type`](@ref): + +```jldoctest +julia> derive_type(Kmer{AminoAcidAlphabet, 6}) +Kmer{AminoAcidAlphabet, 6, 1} +``` + +### Construction +Kmers can be constructed from a `BioSequence` or `AbstractString` by explicitly +specifying the length of the sequence: + +```jldoctest +julia> Kmer{DNAAlphabet{2}, 5, 1}("TAGCT") +DNA 5-mer: +TAGCT +``` + +The final type parameter `N` can be elided, in which case it will be inferred: + +```jldoctest +julia> Kmer{DNAAlphabet{2}, 5}("TAGCT") +DNA 5-mer: +TAGCT +``` + +Kmers with alphabets `DNAAlphabet{2}`, `RNAAlphabet{2}` and `AminoAcidAlphabet` +can be created with the type aliases `DNAKmer`, `RNAKmer` and `AAKmer`: + +```jldoctest +julia> DNAKmer{3}("tag") +DNA 3-mer: +TAG + +julia> AAKmer{5}("PWYSK") +AminoAcid 5-mer: +PWYSK +``` + +For kmers with an `Alphabet` that implement the trait `BioSequences.AsciiAlphabet`, they can also be constructed from `AbstractVector{UInt8}`, in which case the vector is interpreted as being bytes of ASCII text: + +```jldoctest +julia> AAKmer{3}([0x65, 0x67, 0x7a]) +AminoAcid 3-mer: +EGZ +``` + +When constructing from an `AbstractString` (or byte vector), uracil (`U`) and thymine `T` are treated differently - a `U` cannot be read as thymine: + +```jldoctest +julia> DNAKmer{3}("UAG") +ERROR: cannot encode 0x55 (Char 'U') in DNAAlphabet{2} +[...] +``` + +However, when constructing from a `BioSequence`, these nucleotides are considered +interchangeable: + +```jldoctest +julia> RNAKmer{4}(dna"TATC") +RNA 4-mer: +UAUC +``` + +Finally, kmers can be constructed with a string literal `@mer_str`, where the string must be appended with `d` for DNA, `r` for RNA, or `a` for amino acid: + +```jldoctest +julia> mer"UGCUGA"r +RNA 6-mer: +UGCUGA + +julia> mer"EDEHL"a +AminoAcid 5-mer: +EDEHL +``` + +Since the literals produce the kmer at parse time and inserts the kmer directly into the abstract syntax tree, this will always be type stable, +and the overhead related to parsing the string will not be paid. + +In the following example, each iteration takes less than 1 nanosecond, +which implies that parsing the string literal `mer"AAA"d` is not done +in the loop at runtime: + +```jldoctest; filter = [r"^\s*0\.\d+ seconds.+"s, r"^\d+"s] +julia> function count_aaas(dna) + x = 0 + for kmer in FwDNAMers{3}(dna) + # The parsing happens once here, when the + # code is parsed, and is fine to have in the loop + x += kmer == mer"AAA"d + end + x + end; + +julia> seq = randseq(DNAAlphabet{2}(), 100_000_000); + +julia> count_aaas(seq); # compile + +julia> @time count_aaas(seq) # about 1ns per iteration + 0.088556 seconds (1 allocation: 16 bytes) +1561361 +``` + +### Indexing +Kmers support most normal indexing, such as scalar indexing: + +```jldoctest +julia> mer"CAGCU"r[3] +RNA_G +``` + +Slicing + +```jldoctest +julia> mer"AGGCTA"d[2:5] +DNA 4-mer: +GGCT +``` + +And indexing with boolean vectors, and vectors of indices: + +```jldoctest +julia> m = mer"MDGKRY"a; + +julia> m[[true, false, true, true, false, true]] +AminoAcid 4-mer: +MGKY + +julia> m[[4,2]] +AminoAcid 2-mer: +KD +``` + +### A note on type stability +Except scalar indexing which always returns a single symbol, all the operations +above are _type unstable_, since the length (and thus type) of the resulting +kmer depends on the input value, not its type. + +However, type unstable functions may be type-stable, if the indexing value is +known at compile time, and the Julia compiler uses constant folding: + +```jldoctest +julia> f(x) = x[2:5]; # 2:5 is a compile time constant + +julia> Test.@inferred f(mer"UCGUAGC"r) +RNA 4-mer: +CGUA +``` + +### Reference +```@docs +Kmer +derive_type +Mer +@mer_str +DNAKmer +RNAKmer +AAKmer +DNACodon +RNACodon +pop +pop_first +push +push_first +shift +shift_first +``` \ No newline at end of file diff --git a/docs/src/minhash.md b/docs/src/minhash.md new file mode 100644 index 0000000..0ee41d5 --- /dev/null +++ b/docs/src/minhash.md @@ -0,0 +1,42 @@ +```@meta +CurrentModule = Kmers +DocTestSetup = quote + using BioSequences + using Test + using Kmers + using FASTX + using MinHash +end +``` +## MinHash +The MinHash algorithm is used in tools such as +[Mash](https://genomebiology.biomedcentral.com/articles/10.1186/s13059-016-0997-x) +and [sourmash](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC6720031/) +to quickly compute approximate similarities of genomes, collections of genomes, or collections of reads. + +```jldoctest; filter = r"^\d+ MB/s$" => s"***" +using BioSequences, MinHash, FASTX, Kmers + +# Write 25 sequences of length 20 to a buffer. +# Try changing this to length 4 million! +buffer = IOBuffer() +writer = FASTAWriter(buffer) +n_bytes = sum(1:25) do genome + rec = FASTARecord("seq_$(genome)", randdnaseq(20)) + write(writer, rec) +end +flush(writer) + +# Time minhashing the 50 genomes +timing = @timed FASTAReader(seekstart(buffer); copy=false) do reader + map(reader) do record + seq = codeunits(sequence(record)) + sketch(fx_hash, CanonicalDNAMers{16}(sequence(record)), 1000) + end +end +println(round(Int, n_bytes / (timing.time * 1e6)), " MB/s") + +# output + +200 MB/s +``` diff --git a/docs/src/predicates.md b/docs/src/predicates.md deleted file mode 100644 index 2aebc49..0000000 --- a/docs/src/predicates.md +++ /dev/null @@ -1,18 +0,0 @@ -```@meta -CurrentModule = Kmers -DocTestSetup = quote - using Kmers -end -``` - -# Predicates - -The following predicate functions from BioSequences.jl are compatible with `Kmer`s. -Some have an optimised method defined in Kmers.jl. - -```@docs -isrepetitive -ispalindromic -hasambiguity -iscanonical -``` \ No newline at end of file diff --git a/docs/src/random.md b/docs/src/random.md deleted file mode 100644 index 8a1990b..0000000 --- a/docs/src/random.md +++ /dev/null @@ -1,14 +0,0 @@ -```@meta -CurrentModule = Kmers -DocTestSetup = quote - using Kmers -end -``` - -# Generating random sequences - -You can generate random kmers using `Base.rand` function. - -```@docs -Base.rand(::Type{<:Kmer}) -``` \ No newline at end of file diff --git a/docs/src/replacements.md b/docs/src/replacements.md new file mode 100644 index 0000000..bdefb8c --- /dev/null +++ b/docs/src/replacements.md @@ -0,0 +1,59 @@ +```@meta +CurrentModule = Kmers +DocTestSetup = quote + using BioSequences + using Test + using Kmers + using Random +end +``` + +# [Building kmer replacements](@id replacements) +_Kmer replacements_ is the general term for sequences that can be represented +computationally like kmers, but are sampled differently. Examples include minimizers, strobemers, syncmers and k-min-mers. + +Since there is no end to the variations of kmer replacements, Kmers.jl does not try to implement all of them. +Instead, Kmers.jl implements the base kmer type, and exposes some efficient primitives to allow downstream users to build kmer replacements. + +These functions are: + +```@docs +unsafe_extract +shift_encoding +unsafe_shift_from +``` + +# Example: Minimizers +Minimizers are currently the most common kmer replacement. +They are defined as the minimum of W consecutive kmers, as ordered by some ordering O. + +If we use [`fx_hash`](@ref) as the ordering function, and assume K and W are known at compile time, we can implement it reasonably efficiently like so: + +```jldoctest +function unsafe_extract_minimizer( + seq::LongDNA{2}, + i::Int, + ::Val{K}, + ::Val{W}, +) where {K, W} + T = derive_type(Kmer{DNAAlphabet{2}, K}) + kmer = Kmers.unsafe_extract(Kmers.Copyable(), T, seq, i) + hash = fx_hash(kmer) + for offset in 0:W-2 + new_kmer = Kmers.unsafe_shift_from(Kmers.Copyable(), kmer, seq, i+K+offset, Val(1)) + new_hash = fx_hash(new_kmer) + if new_hash < hash + hash = new_hash + kmer = new_kmer + end + end + kmer +end + +rng = Random.Xoshiro(1) +unsafe_extract_minimizer(randseq(rng, DNAAlphabet{2}(), 100), 1, Val(5), Val(9)) + +# output +DNA 5-mer: +TATCA +``` diff --git a/docs/src/skipmers.png b/docs/src/skipmers.png deleted file mode 100644 index 31edbc6d786ab6f08687858b8622636e24a8adde..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 214672 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ǛFl)&~;@ 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 kmer = Kmer(DNA_T, DNA_T, DNA_A, DNA_G, DNA_C) -DNA 5-mer: -TTAGC - -julia> kmer[3] -DNA_A -``` - -You can also slice Kmers using UnitRanges: - -```jldoctest -julia> kmer = Kmer(DNA_T, DNA_T, DNA_A, DNA_G, DNA_C) -DNA 5-mer: -TTAGC - -julia> kmer[1:3] -DNA 3-mer: -TTA -``` - -!!! warning - Using slicing will introduce performance penalties in your code if - you pass values of `i` that are not constants that can be propagated. - -## Modifying sequences - -Many modifying operations that are possible for some `BioSequences` such as -`LongSequence` are not possible for `Kmer`s, this is primarily due to the fact -`Kmer`s are an immutable struct. - -However some non-mutating transformations are available: - -```@docs -BioSequences.complement(::Kmer) -Base.reverse(::Kmer) -BioSequences.reverse_complement(::Kmer) -canonical -``` \ No newline at end of file diff --git a/docs/src/translate.md b/docs/src/translate.md deleted file mode 100644 index d518556..0000000 --- a/docs/src/translate.md +++ /dev/null @@ -1,62 +0,0 @@ -```@meta -CurrentModule = Kmers -DocTestSetup = quote - using Kmers -end -``` - -# Translating and reverse translating - -## Translating -Just like other `BioSequence`s, `Kmer`s of RNA or DNA alphabets can be efficiently translated to amino acids: - -``` -julia> kmer = RNAKmer("AUGGGCCACUGA"); - -julia> translate(kmer) -AminoAcid 4-mer: -MGH* -``` - -For more information on translation and different genetic codes, see the documentation of BioSequences.jl. - -## Reverse translation -Reverse translation (or "revtrans", for short) refers to the mapping from amino acids back to the set of RNA codons that code for the given amino acid, under a given genetic code. -There is no known natural process of revtrans, but it can be useful to do _in silico_. - -In Kmers.jl, revtrans is done through the `reverse_translate` function. -This takes an amino acid sequence and produces a `Vector{CodonSet}`, where `CodonSet <: AbstractSet{RNACodon}`. -Alternatively, it takes an amino acid and produces a `CodonSet`. - -A reverse genetic code can optionally be specified as the second argument. -If not provided, it default to the reverse standard genetic code. - -### Example of reverse translation -```julia -julia> reverse_translate(AA_W) # default to standard genetic code -Kmers.CodonSet with 1 element: - UGG - -julia> code = ReverseGeneticCode(BioSequences.trematode_mitochondrial_genetic_code); - -julia> reverse_translate(AA_W, code) -Kmers.CodonSet with 2 elements: - UGA - UGG -``` - -### Important notes on reverse translation -* `AA_Gap` cannot be reverse translated. Attempting so throws an error -* In cells, `AA_O` and `AA_U` are encoded by dynamic overloading of the codons `UAG` and `UGA`, respectively. - Because these codons normally code for `AA_Term`, the forward genetic code returns `AA_Term` for these codons. - However, we can unambiguously reverse translate them, so these amino acids translate to codonsets with these - precise codons. -* Ambiguous amino acids translate to the union of the possible amino acids. For example, if `AA_L` translate to set `S1`, - and `AA_I` translate to `S2`, then `AA_J` translate to `union(S1, S2)`. - -```@docs -Kmers.CodonSet -Kmers.ReverseGeneticCode -reverse_translate -reverse_translate! -``` diff --git a/docs/src/translation.md b/docs/src/translation.md new file mode 100644 index 0000000..d4397d1 --- /dev/null +++ b/docs/src/translation.md @@ -0,0 +1,67 @@ +```@meta +CurrentModule = Kmers +DocTestSetup = quote + using BioSequences + using Test + using Kmers +end +``` + +## Translation +`Kmer`s can be translated using the `translate` function exported by `BioSequences`: + +```jldoctest +julia> translate(mer"UGCUUGAUC"r) +AminoAcid 3-mer: +CLI +``` + +Since `Kmer`s are immutable, the in-place `translate!` function is not implemented for `Kmers`. +Also, remember that `Kmer`s are only efficient when short (at most a few hundred symbols). Hence, entire exons or genes should probably not ever be represented by a `Kmer`, but rather as a `LongSequence` or `LongSubSeq` from BioSequences.jl. + +### Reverse translation +Kmers.jl implements reverse translation, which maps an amino acid sequence to one or more RNA sequences. +While this process doesn't occur naturally (as far as we know), it is still useful for some analyses. + +Since genetic codes are degenerate, i.e. multiple codons code for the same amino acid, reverse translating a sequence does not return a nucleic acid sequence, but a vector of `CodonSet`: + +```@docs +reverse_translate +CodonSet +``` + +`CodonSet` is an efficiently implemented `AbstractSet{RNACodon}` (and remember, `RNACodon` is an alias for `RNAKmer{3, 1}`). + +To avoid allocating a new `Vector`, you can use `reverse_translate!`: + +```@docs +reverse_translate! +``` + +Both functions take a genetic code as a keyword argument of the type `ReverseGeneticCode`. This object determines the mapping from amino acid to `CodonSet` - by default the [standard genetic code](https://en.wikipedia.org/wiki/DNA_and_RNA_codon_tables#Standard_RNA_codon_table) is used - this mapping is used by nearly all organisms. + +Only the reverse standard genetic code is defined in Kmers.jl. +To use another genetic code, build a `ReverseGeneticCode` object from an existing +`BioSequences.GeneticCode`: + +```jldoctest +julia> code = BioSequences.pterobrachia_mitochondrial_genetic_code; + +julia> rv_code = ReverseGeneticCode(code); + +julia> seq = aa"KWLP"; + +julia> codonsets = reverse_translate(seq, rv_code) +4-element Vector{CodonSet}: + CodonSet(0x0000000000000405) + CodonSet(0x0500000000000000) + CodonSet(0x50000000f0000000) + CodonSet(0x0000000000f00000) + +julia> codonsets == reverse_translate(seq) # default standard code +false +``` + +```@docs +ReverseGeneticCode +``` diff --git a/ext/StringViewsExt.jl b/ext/StringViewsExt.jl new file mode 100644 index 0000000..f674427 --- /dev/null +++ b/ext/StringViewsExt.jl @@ -0,0 +1,11 @@ +module StringViewsExt + +using StringViews: StringView +using Kmers: Kmers + +# This extension is important because FASTX uses string views. +# The documentation of StringViews promises that the underlying +# string is UTF-8 encoded. +Kmers.is_ascii(::Type{<:StringView}) = true + +end # module diff --git a/src/Kmers.jl b/src/Kmers.jl index be1a2c5..3e46ab9 100644 --- a/src/Kmers.jl +++ b/src/Kmers.jl @@ -5,82 +5,61 @@ # # This file is a part of the Kmers.jl, a package in the BioJulia ecosystem. # License is MIT: https://github.com/BioJulia/Kmers.jl/blob/master/LICENSE +module Kmers + +export Kmer, + Mer, + DNAKmer, + RNAKmer, + AAKmer, + DNACodon, + RNACodon, + ReverseGeneticCode, + reverse_translate, + reverse_translate!, + @mer_str, + fx_hash, + derive_type, + + # Immutable operations + push, + push_first, + shift, + shift_first, + pop, + pop_first, -__precompile__() + # Iterators + FwKmers, + FwDNAMers, + FwRNAMers, + FwAAMers, + FwRvIterator, + CanonicalKmers, + CanonicalDNAMers, + CanonicalRNAMers, + UnambiguousKmers, + UnambiguousDNAMers, + UnambiguousRNAMers, + SpacedKmers, + SpacedDNAMers, + SpacedRNAMers, + SpacedAAMers, + each_codon, -module Kmers + # Reverse translation + CodonSet, + delete, # push already exported -export - # BioSymbols re-exports. + ################## + # Re-exports + ################## + # BioSymbols re-exports NucleicAcid, DNA, RNA, - DNA_A, - DNA_C, - DNA_G, - DNA_T, - DNA_M, - DNA_R, - DNA_W, - DNA_S, - DNA_Y, - DNA_K, - DNA_V, - DNA_H, - DNA_D, - DNA_B, - DNA_N, - DNA_Gap, - ACGT, - ACGTN, - RNA_A, - RNA_C, - RNA_G, - RNA_U, - RNA_M, - RNA_R, - RNA_W, - RNA_S, - RNA_Y, - RNA_K, - RNA_V, - RNA_H, - RNA_D, - RNA_B, - RNA_N, - RNA_Gap, - ACGU, - ACGUN, AminoAcid, - AA_A, - AA_R, - AA_N, - AA_D, - AA_C, - AA_Q, - AA_E, - AA_G, - AA_H, - AA_I, - AA_L, - AA_K, - AA_M, - AA_F, - AA_P, - AA_S, - AA_T, - AA_W, - AA_Y, - AA_V, - AA_O, - AA_U, - AA_B, - AA_J, - AA_Z, - AA_X, - AA_Term, - AA_Gap, - + # BioSequences re-exports Alphabet, BioSequence, @@ -89,65 +68,65 @@ export DNAAlphabet, RNAAlphabet, translate, - - - ### - ### Mers - ### - - # Type & aliases - Kmer, - DNAKmer, - DNA27mer, - DNA31mer, - DNA63mer, - RNAKmer, - RNA27mer, - RNA31mer, - RNA63mer, - AAKmer, - DNACodon, - RNACodon, + complement, + reverse_complement, + canonical, + iscanonical - # Iteration - EveryKmer, - SpacedKmers, - EveryCanonicalKmer, - SpacedCanonicalKmers, - fw_neighbors, - bw_neighbors, +# TODO: Remove this ugly hack when 1.11 becomes LTS +if VERSION >= v"1.11.0-DEV.469" + let str = """ + public unsafe_shift_from, + shift_encoding, + unsafe_extract, + RecodingScheme, + Copyable, + TwoToFour, + FourToTwo, + AsciiEncode, + GenericRecoding + """ + eval(Meta.parse(str)) + end +end - # Immutable operators - push, - delete, +# Kmers.jl is tightly coupled to BioSequences and relies on much of its internals. +# Hence, we do not care about carefully importing specific symbols +using BioSequences +using BioSymbols: BioSymbol - # Translation - reverse_translate, - reverse_translate!, - ReverseGeneticCode, - - ### - ### Sequence literals - ### - - @mer_str, - @bigmer_str +# This is a documented method, not internals +using Base: tail -using BioSequences +""" + Kmers.Unsafe + +Internal trait object used to access unsafe methods of functions. +`unsafe` is the singleton of `Unsafe`. +""" +struct Unsafe end +const unsafe = Unsafe() -ispermitted(::DNAAlphabet{2}, nt::DNA) = count_ones(nt) == 1 && isvalid(nt) -ispermitted(::DNAAlphabet{2}, data::UInt) = data < UInt(4) -ispermitted(::DNAAlphabet{4}, nt::DNA) = isvalid(nt) -ispermitted(::DNAAlphabet{4}, data::UInt) = isvalid(DNA, data) -ispermitted(::AminoAcidAlphabet, aa::AminoAcid) = reinterpret(UInt8, aa) <= reinterpret(UInt8, AA_Gap) -ispermitted(::AminoAcidAlphabet, data::UInt) = data <= 0x1b +const FourBit = Union{DNAAlphabet{4}, RNAAlphabet{4}} +const TwoBit = Union{DNAAlphabet{2}, RNAAlphabet{2}} +const BitInteger = + Union{Int8, UInt8, Int16, UInt16, Int32, UInt32, Int64, UInt64, Int128, UInt128} +include("tuple_bitflipping.jl") include("kmer.jl") +include("construction.jl") +include("indexing.jl") +include("transformations.jl") +include("revtrans.jl") + +include("iterators/common.jl") +include("iterators/FwKmers.jl") +include("iterators/CanonicalKmers.jl") +include("iterators/UnambiguousKmers.jl") +include("iterators/SpacedKmers.jl") -include("kmer_iteration/AbstractKmerIterator.jl") -include("kmer_iteration/EveryKmer.jl") -include("kmer_iteration/SpacedKmers.jl") -include("kmer_iteration/EveryCanonicalKmer.jl") -include("kmer_iteration/SpacedCanonicalKmers.jl") +if !isdefined(Base, :get_extension) + include("../ext/StringViewsExt.jl") +end end # module diff --git a/src/construction.jl b/src/construction.jl new file mode 100644 index 0000000..032c3aa --- /dev/null +++ b/src/construction.jl @@ -0,0 +1,359 @@ +################################################ +# Trait dispatch +################################################ + +""" + RecodingScheme + +Trait object which determines the methods used to recode +data from one sequence into a `BioSequence`. +Any given construction of a kmer from a source sequence will +dispatch to one of the concrete subtypes of `RecodingScheme` +to determine how the data is copied most effectively. +""" +abstract type RecodingScheme end + +""" + Copyable <: RecodingScheme + +Trait object that signifies that two sequences share identical encodings, +such that the encoded sequence can be copied directly. +This is the case for e.g. sequences of the same alphabet, or two +sequences of the alphabets `DNAAlphabet{2}` and `RNAAlphabet{2}` +""" +struct Copyable <: RecodingScheme end + +""" + TwoToFour <: RecodingScheme + +Trait object that signifies a recoding from a 4-bit nucleotide sequence +to a 2-bit nucleotide sequence. +This can be more efficient than using `GenericRecoding` +""" +struct TwoToFour <: RecodingScheme end + +""" + FourToTwo <: RecodingScheme + +Trait object that signifies a recoding from a 2-bit nucleotide sequence +to a 4-bit nucleotide sequence. +This can be more efficient than using `GenericRecoding` +""" +struct FourToTwo <: RecodingScheme end + +""" + AsciiEncode <: RecodingScheme + +Trait object that signifies a recoding from an ASCII-encoded +`AbstractVector{UInt8}` to an `Alphabet` that implements the +`BioSequences.AsciiAlphabet` trait. +The validity of the bytes are checked during the recoding, including +checking that the sequence is actually ASCII encoded. +""" +struct AsciiEncode <: RecodingScheme end + +""" + GenericRecoding <: RecodingScheme + +Trait object that signifies that none of the other recoding schemes +apply, that the fallback implementations must be used instead. +""" +struct GenericRecoding <: RecodingScheme end + +""" + is_ascii(::Type{T})::Bool + +Trait function. Should return `true` for `AbstractVector{UInt8}`, or for +string types for which `codeunits(s)` returns an `AbstractVector{UInt8}`, where +every ASCII byte in the string is perserved in the vector. +This is true for all UTF8, latin1 and ASCII encoded string types. +""" +is_ascii(::Type) = false +is_ascii(::Type{<:Union{String, SubString{String}}}) = true +is_ascii(::Type{<:AbstractVector{UInt8}}) = true + +function RecodingScheme(A::Alphabet, source_type::Type)::RecodingScheme + return if source_type <: BioSequence + if BioSequences.encoded_data_eltype(source_type) <: BitInteger + As = Alphabet(source_type) + if As == A + Copyable() + elseif As isa TwoBit && A isa TwoBit + Copyable() + elseif As isa FourBit && A isa FourBit + Copyable() + elseif As isa FourBit && A isa TwoBit + FourToTwo() + elseif As isa TwoBit && A isa FourBit + TwoToFour() + else + GenericRecoding() + end + else + GenericRecoding() + end + elseif is_ascii(source_type) && BioSequences.codetype(A) isa BioSequences.AsciiAlphabet + AsciiEncode() + else + GenericRecoding() + end +end + +include("construction_utils.jl") + +################################################ +# Unsafe extract +################################################ + +@noinline function throw_uncertain(A::Alphabet, T::Type{<:BioSymbol}, enc::Unsigned) + throw(BioSequences.EncodeError(A, reinterpret(T, enc % UInt8))) +end + +#= +"Extract a full kmer at a given index of a sequence. +Note: These methods don't do any bounds checking" +function unsafe_extract end + +@inline function unsafe_extract( + ::TwoToFour, + ::Type{T}, + seq::BioSequence, + from_index, +) where {T <: Kmer} + data = zero_tuple(T) + for i in from_index:(from_index + ksize(T) - 1) + encoding = left_shift(UInt(1), UInt(BioSequences.extract_encoded_element(seq, i))) + (_, data) = leftshift_carry(data, 4, encoding) + end + T(unsafe, data) +end + +@inline function unsafe_extract( + ::FourToTwo, + ::Type{T}, + seq::BioSequence, + from_index, +) where {T <: Kmer} + data = zero_tuple(T) + for i in from_index:(from_index + ksize(T) - 1) + encoding = UInt(BioSequences.extract_encoded_element(seq, i))::UInt + isone(count_ones(encoding)) || throw_uncertain(Alphabet(T), eltype(seq), encoding) + (_, data) = leftshift_carry(data, 2, trailing_zeros(encoding) % UInt) + end + T(unsafe, data) +end + +@inline function unsafe_extract( + ::Copyable, + ::Type{T}, + seq::BioSequence, + from_index, +) where {T <: Kmer} + data = zero_tuple(T) + bps = BioSequences.bits_per_symbol(Alphabet(seq)) + for i in from_index:(from_index + ksize(T) - 1) + encoding = UInt(BioSequences.extract_encoded_element(seq, i))::UInt + (_, data) = leftshift_carry(data, bps, encoding) + end + T(unsafe, data) +end + +@inline function unsafe_extract( + ::AsciiEncode, + ::Type{T}, + seq::AbstractVector{UInt8}, + from_index, +) where {T <: Kmer} + data = zero_tuple(T) + bps = BioSequences.bits_per_symbol(Alphabet(T)) + @inbounds for i in from_index:(from_index + ksize(T) - 1) + byte = seq[i] + encoding = BioSequences.ascii_encode(Alphabet(T), byte) + if encoding > 0x7f + throw(BioSequences.EncodeError(Alphabet(T), byte)) + end + (_, data) = leftshift_carry(data, bps, encoding % UInt) + end + T(unsafe, data) +end + +@inline function unsafe_extract( + ::GenericRecoding, + ::Type{T}, + seq, + from_index, +) where {T <: Kmer} + data = zero_tuple(T) + bps = BioSequences.bits_per_symbol(Alphabet(T)) + @inbounds for i in 1:ksize(T) + symbol = convert(eltype(T), seq[i]) + encoding = UInt(BioSequences.encode(Alphabet(T), symbol)) + (_, data) = leftshift_carry(data, bps, encoding) + end + T(unsafe, data) +end +=# + +################################################ +# Constructors with full parameterisation +################################################ + +function Kmer{A, K, N}(x) where {A, K, N} + check_kmer(Kmer{A, K, N}) + build_kmer(RecodingScheme(A(), typeof(x)), Kmer{A, K, N}, x) +end + +# BioSequences support indexing and fast length checks +@inline function build_kmer(R::RecodingScheme, ::Type{T}, s::BioSequence) where {T} + length(s) == ksize(T) || error("Length of sequence must be K elements to build Kmer") + unsafe_extract(R, T, s, 1) +end + +# LongSequence with compatible alphabet: Extract whole coding elements +@inline function build_kmer(::Copyable, ::Type{T}, s::LongSequence) where {T} + length(s) == ksize(T) || error("Length of sequence must be K elements to build Kmer") + bps = BioSequences.BitsPerSymbol(Alphabet(T)) + data = ntuple(i -> BioSequences.reversebits(@inbounds(s.data[i]), bps), Val{nsize(T)}()) + (_, data) = rightshift_carry(data, bits_unused(T), zero(UInt)) + T(unsafe, data) +end + +# TODO: LongSubSeq with compatible alphabet +# Note: LongSequence may be UInt64 whereas kmers use UInt32 + +# For UTF8-strings combined with an ASCII kmer alphabet, we convert to byte vector +@inline function build_kmer( + R::AsciiEncode, + ::Type{T}, + s::Union{String, SubString{String}}, +) where {T} + build_kmer(R, T, codeunits(s)) +end + +# For byte vectors, we can build a kmer iff the kmer alphabet is AsciiAlphabet +@inline function build_kmer(R::AsciiEncode, ::Type{T}, s::AbstractVector{UInt8}) where {T} + length(s) == ksize(T) || error("Length of sequence must be K elements to build Kmer") + unsafe_extract(R, T, s, 1) +end + +# The generic fallback - dispatch on whether we can check length once +@inline function build_kmer(R::RecodingScheme, T::Type, s) + build_kmer(Base.IteratorSize(typeof(s)), R, T, s) +end + +@inline function build_kmer(::Base.SizeUnknown, ::RecodingScheme, T::Type, s) + data = zero_tuple(T) + A = Alphabet(T) + bps = BioSequences.bits_per_symbol(A) + i = 0 + for element in s + i += 1 + i > ksize(T) && error("Length of sequence must be K elements to build Kmer") + symbol = convert(eltype(A), element) + carry = UInt(BioSequences.encode(A, symbol)) + (_, data) = leftshift_carry(data, bps, carry) + end + i == ksize(T) || error("Length of sequence must be K elements to build Kmer") + T(unsafe, data) +end + +@inline function build_kmer( + ::Union{Base.HasLength, Base.HasShape}, + ::RecodingScheme, + T::Type, + s, +) + length(s) == ksize(T) || error("Length of sequence must be K elements to build Kmer") + data = zero_tuple(T) + A = Alphabet(T) + bps = BioSequences.bits_per_symbol(A) + for element in s + symbol = convert(eltype(A), element) + carry = UInt(BioSequences.encode(A, symbol)) + (_, data) = leftshift_carry(data, bps, carry) + end + T(unsafe, data) +end + +################################################ +# Derived constructors +################################################ + +Kmer{A, K}(x) where {A, K} = derive_type(Kmer{A, K})(x) +Kmer{A1}(x::Kmer{A2, K, N}) where {A1, A2, K, N} = Kmer{A1, K, N}(x) + +################################################ +# Construct other types from Kmers +################################################ + +function BioSequences.LongSequence{A}(kmer::Kmer{A}) where {A <: Alphabet} + ratio = div(sizeof(UInt64), sizeof(UInt)) + data = zeros(UInt64, cld(n_coding_elements(typeof(kmer)), ratio)) + @inbounds for i in 1:n_coding_elements(typeof(kmer)) + target = cld(i, ratio) + src = kmer.data[i] % UInt64 + if ratio == 2 && iseven(i) + src <<= 32 + end + data[target] |= src + end + bps = BioSequences.BitsPerSymbol(A()) + @inbounds for i in eachindex(data) + data[i] = BioSequences.reversebits(data[i], bps) + end + bu = bits_unused(typeof(kmer)) + @inbounds if !iszero(bu) + data[end] = data[end] >> bu + end + LongSequence{A}(data, length(kmer) % UInt) +end + +# TODO: Do we want specialized constructors to contruct cross-alphabet longseqs +# from kmers? + +################################################ +# String literals +################################################ + +""" + @mer_str -> Kmer + +Construct a `Kmer` from the given string. The macro must be used with a flag +after the input string, e.g. `d` in `mer"TAG"d` or `a` in `mer"PCW"a`, signifying +the alphabet of the kmer. +The flags `d = DNAAlphabet{2}`, `r = RNAAlphabet{2}` and `a = AminoAcidAlphabet` +are recognized. + +Because the macro is resolved and the kmer is created at parse time, +the macro is type stable, and may be used in high performance code. + +# Examples +```jldoctest +julia> mer"UGCUA"r +RNA 5-mer: +UGCUA + +julia> mer"YKVSTEDLLKKR"a +AminoAcid 12-mer: +YKVSTEDLLKKR + +julia> mer"TATTAGCA"d +DNA 8-mer: +TATTAGCA +``` +""" +macro mer_str(seq, flag) + trimmed = BioSequences.remove_newlines(seq) + ncu = ncodeunits(trimmed) + # Unlike @dna_str, we default to 2-bit alphabets, because kmers + # by convention are usually 2-bit only + if flag == "dna" || flag == "d" + Kmer{DNAAlphabet{2}, ncu}(trimmed) + elseif flag == "rna" || flag == "r" + Kmer{RNAAlphabet{2}, ncu}(trimmed) + elseif flag == "aa" || flag == "a" + Kmer{AminoAcidAlphabet, ncu}(trimmed) + else + error("Invalid type flag: '$(flag)'") + end +end diff --git a/src/construction_utils.jl b/src/construction_utils.jl new file mode 100644 index 0000000..3b6463d --- /dev/null +++ b/src/construction_utils.jl @@ -0,0 +1,236 @@ +# This file contains construction utilities that are public, allowing users +# to create custom K-mer types such as syncmers, strobemers, minimizers etc. + +# unsafe_extract +# shift_encoding +# shift_seq + +""" + unsafe_extract(::RecodingScheme, T::Type{<:Kmer}, seq, from::Int) -> T + +Extract a `Kmer` of type `T` from `seq` beginning at index `from`. +This function is useful to create kmer or kmer-like types. + +This function does not do any bounds checking, so the user must know +that `from:from+K-1` is inbounds in `seq`. +The validity of the data in the `seq` is validated by this function. + +# Examples +```jldoctest +julia> seq = b"TAGCTAGA"; + +julia> Kmers.unsafe_extract(Kmers.AsciiEncode(), DNAKmer{4, 1}, seq, 2) +DNA 4-mer: +AGCT +``` +""" +@inline function unsafe_extract( + ::TwoToFour, + ::Type{T}, + seq::BioSequence, + from_index, +) where {T <: Kmer} + data = zero_tuple(T) + for i in from_index:(from_index + ksize(T) - 1) + encoding = left_shift(UInt(1), UInt(BioSequences.extract_encoded_element(seq, i))) + (_, data) = leftshift_carry(data, 4, encoding) + end + T(unsafe, data) +end + +@inline function unsafe_extract( + ::FourToTwo, + ::Type{T}, + seq::BioSequence, + from_index, +) where {T <: Kmer} + data = zero_tuple(T) + for i in from_index:(from_index + ksize(T) - 1) + encoding = UInt(BioSequences.extract_encoded_element(seq, i))::UInt + isone(count_ones(encoding)) || throw_uncertain(Alphabet(T), eltype(seq), encoding) + (_, data) = leftshift_carry(data, 2, trailing_zeros(encoding) % UInt) + end + T(unsafe, data) +end + +@inline function unsafe_extract( + ::Copyable, + ::Type{T}, + seq::BioSequence, + from_index, +) where {T <: Kmer} + data = zero_tuple(T) + bps = BioSequences.bits_per_symbol(Alphabet(seq)) + for i in from_index:(from_index + ksize(T) - 1) + encoding = UInt(BioSequences.extract_encoded_element(seq, i))::UInt + (_, data) = leftshift_carry(data, bps, encoding) + end + T(unsafe, data) +end + +@inline function unsafe_extract( + ::AsciiEncode, + ::Type{T}, + seq::AbstractVector{UInt8}, + from_index, +) where {T <: Kmer} + data = zero_tuple(T) + bps = BioSequences.bits_per_symbol(Alphabet(T)) + @inbounds for i in from_index:(from_index + ksize(T) - 1) + byte = seq[i] + encoding = BioSequences.ascii_encode(Alphabet(T), byte) + if encoding > 0x7f + throw(BioSequences.EncodeError(Alphabet(T), byte)) + end + (_, data) = leftshift_carry(data, bps, encoding % UInt) + end + T(unsafe, data) +end + +@inline function unsafe_extract( + ::GenericRecoding, + ::Type{T}, + seq, + from_index, +) where {T <: Kmer} + data = zero_tuple(T) + bps = BioSequences.bits_per_symbol(Alphabet(T)) + @inbounds for i in 0:(ksize(T) - 1) + symbol = convert(eltype(T), seq[from_index + i]) + encoding = UInt(BioSequences.encode(Alphabet(T), symbol)) + (_, data) = leftshift_carry(data, bps, encoding) + end + T(unsafe, data) +end + +########################## +# Shift encoding +########################## + +""" + shift_encoding(kmer::T, encoding::UInt) where {T <: Kmer} -> T + +Add `encoding`, a valid encoding in the alphabet of the `kmer`, +to the end of `kmer` and discarding the first symbol in `kmer`. + +It is the user's responsibility to ensure that `encoding` is valid. + +# Examples +```jldoctest +julia> enc = UInt(0x0a); # encoding of DNA_Y in 4-bit alphabets + +julia> kmer = Kmer{DNAAlphabet{4}, 4}("TAGA"); + +julia> Kmers.shift_encoding(kmer, enc) +DNA 4-mer: +AGAY +``` +""" +@inline function shift_encoding(kmer::Kmer, encoding::UInt) + isempty(kmer) && return kmer + bps = BioSequences.bits_per_symbol(kmer) + (_, new_data) = leftshift_carry(kmer.data, bps, encoding) + typeof(kmer)(unsafe, (first(new_data) & get_mask(typeof(kmer)), Base.tail(new_data)...)) +end + +########################### + +""" + unsafe_shift_from(::RecodingScheme, kmer::T, seq, from::Int, ::Val{S}) -> T + +Extract `S::Int` symbols from sequence `seq` at positions `from:from+S-1`, +and shift them into `kmer`. + +This function does not do any bounds checking, so it is the user's +responsibility to ensure that `from` is inbounds, and the recoding scheme +valid. +It is assumed that `S < K`, where `K == length(kmer)`. If `S ≥ K`, use +[`unsafe_extract`](@ref) instead. + +# Examples +```jldoctest +julia> seq = dna"TAGCGGA"; + +julia> kmer = mer"GGTG"d; + +julia> Kmers.unsafe_shift_from(Kmers.FourToTwo(), kmer, seq, 3, Val(2)) +DNA 4-mer: +TGGC +``` +""" +@inline function unsafe_shift_from( + ::GenericRecoding, + kmer::Kmer, + seq, + from::Int, + ::Val{S}, +) where {S} + for i in 0:(S - 1) + symbol = @inbounds seq[from + i] + kmer = shift(kmer, convert(eltype(kmer), symbol)) + end + kmer +end + +@inline function unsafe_shift_from( + ::Copyable, + kmer::Kmer, + seq::BioSequence, + from::Int, + ::Val{S}, +) where {S} + for i in 0:(S - 1) + encoding = UInt(BioSequences.extract_encoded_element(seq, from + i)) + kmer = shift_encoding(kmer, encoding) + end + kmer +end + +@inline function unsafe_shift_from( + ::TwoToFour, + kmer::Kmer{<:NucleicAcidAlphabet{4}}, + seq::BioSequence{<:NucleicAcidAlphabet{2}}, + from::Int, + ::Val{S}, +) where {S} + for i in 0:(S - 1) + encoding = + left_shift(UInt(1), UInt(BioSequences.extract_encoded_element(seq, from + i))) + kmer = shift_encoding(kmer, encoding) + end + kmer +end + +@inline function unsafe_shift_from( + ::FourToTwo, + kmer::Kmer{<:NucleicAcidAlphabet{2}}, + seq::BioSequence{<:NucleicAcidAlphabet{4}}, + from::Int, + ::Val{S}, +) where {S} + for i in 0:(S - 1) + encoding = UInt(BioSequences.extract_encoded_element(seq, from + i))::UInt + isone(count_ones(encoding)) || + throw_uncertain(Alphabet(kmer), eltype(seq), encoding) + kmer = shift_encoding(kmer, trailing_zeros(encoding) % UInt) + end + kmer +end + +@inline function unsafe_shift_from( + ::AsciiEncode, + kmer::Kmer, + seq::AbstractVector{UInt8}, + from::Int, + ::Val{S}, +) where {S} + for i in 0:(S - 1) + byte = @inbounds seq[from + i] + encoding = BioSequences.ascii_encode(Alphabet(typeof(kmer)), byte) + if encoding > 0x7f + throw(BioSequences.EncodeError(Alphabet(typeof(kmer)), byte)) + end + kmer = shift_encoding(kmer, encoding % UInt) + end + kmer +end diff --git a/src/counting.jl b/src/counting.jl deleted file mode 100644 index cbd9340..0000000 --- a/src/counting.jl +++ /dev/null @@ -1,47 +0,0 @@ -### -### Mer specific specializations of src/biosequence/counting.jl -### - -for i in [(:_count_a, :a_bitcount), (:_count_c, :c_bitcount), (:_count_g, :g_bitcount), (:_count_t, :t_bitcount)] - @eval begin - @inline function $(i[1])(alph::A, head::UInt64, tail...) where {A<:NucleicAcidAlphabet} - return $BioSequences.$(i[2])(head, alph) + $(i[1])(alph, tail...) - end - @inline $(i[1])(alph::A) where {A<:NucleicAcidAlphabet} = 0 - end -end - -@inline function _count_gc(alph::A, head::UInt64, tail...) where {A<:NucleicAcidAlphabet} - return BioSequences.gc_bitcount(head, alph) + _count_gc(alph, tail...) -end -@inline _count_gc(::A) where {A<:NucleicAcidAlphabet} = 0 - -count_a(x::Kmer{A,K,N}) where {A<:NucleicAcidAlphabet,K,N} = _count_a(A(), x.data...) - n_unused(x) -count_c(x::Kmer{A,K,N}) where {A<:NucleicAcidAlphabet,K,N} = _count_c(A(), x.data...) -count_g(x::Kmer{A,K,N}) where {A<:NucleicAcidAlphabet,K,N} = _count_g(A(), x.data...) -count_t(x::Kmer{A,K,N}) where {A<:NucleicAcidAlphabet,K,N} = _count_t(A(), x.data...) - -count_gc(x::Kmer{A,K,N}) where {A<:NucleicAcidAlphabet,K,N} = _count_gc(A(), x.data...) -Base.count(::typeof(isGC), x::Kmer{A,K,N}) where {A<:NucleicAcidAlphabet,K,N} = count_gc(x) - -# TODO: Expand to Amino Acid Kmers as well... -@inline function Base.count(::typeof(!=), a::Kmer{A,K,N}, b::Kmer{A,K,N}) where {A<:NucleicAcidAlphabet,K,N} - ad = a.data - bd = b.data - sum = 0 - @inbounds for i in 1:N - sum += BioSequences.mismatch_bitcount(ad[i], bd[i], A()) - end - return sum -end - -# TODO: Expand to Amino Acid Kmers as well... -@inline function Base.count(::typeof(==), a::Kmer{A,K,N}, b::Kmer{A,K,N}) where {A<:NucleicAcidAlphabet,K,N} - ad = a.data - bd = b.data - sum = 0 - @inbounds for i in 1:N - sum += BioSequences.match_bitcount(ad[i], bd[i], A()) - end - return sum - n_unused(a) -end \ No newline at end of file diff --git a/src/indexing.jl b/src/indexing.jl index 021926a..daa28dc 100644 --- a/src/indexing.jl +++ b/src/indexing.jl @@ -1,35 +1,86 @@ -@inline BioSequences.encoded_data_eltype(::Type{<:Kmer}) = UInt64 - @inline function BioSequences.extract_encoded_element(seq::Kmer, i::Integer) - bi = BioSequences.bitindex(seq, i % UInt) - return BioSequences.extract_encoded_element(bi, seq.data) + T = typeof(seq) + bps = BioSequences.bits_per_symbol(Alphabet(seq)) % UInt + index = div((i + n_unused(T) - 1) % UInt, per_word_capacity(T) % UInt) + 1 + offset = mod(((elements_in_head(T) - i) * bps) % UInt, 8 * sizeof(UInt)) + mask = UInt(1) << bps - 1 + right_shift(@inbounds(seq.data[index]), offset) & mask end -@inline Base.copy(seq::Kmer) = typeof(seq)(seq.data) - -@inline encoded_data(x::Kmer) = x.data +# This is usually type unstable, but in user code, users may use constant-folded ranges, +# e.g. f(x) = x[2:4]. In this case, we need it to compile to very efficient code. +# Hence, it MUST use @inline +@inline function Base.getindex(kmer::Kmer{A}, range::AbstractUnitRange{<:Integer}) where {A} + @boundscheck checkbounds(kmer, range) + K = length(range) + iszero(K) && return Kmer{A, 0, 0}(unsafe, ()) + (i1, _) = BioSequences.bitindex(kmer, first(range)) + (i2, o2) = BioSequences.bitindex(kmer, last(range)) + data = kmer.data[i1:i2] + (_, data) = rightshift_carry(data, o2, zero(UInt)) + T = derive_type(Kmer{A, K}) + N = nsize(T) + # After the shift, the first coding element may be unused + new_data = if N < length(data) + tail(data) + else + data + end + T(unsafe, (first(new_data) & get_mask(T), tail(new_data)...)) +end -@inline BioSequences.bitindex(seq::Kmer, i::Integer) = BioSequences.bitindex(BioSequences.BitsPerSymbol(seq), BioSequences.encoded_data_eltype(typeof(seq)), i + n_unused(seq)) +# Same as above: This needs to be able to inline if the indices are known statically +@inline function Base.getindex(kmer::Kmer{A}, indices::AbstractVector{Bool}) where {A} + @boundscheck checkbounds(eachindex(kmer), indices) + K = sum(indices) + N = n_coding_elements(Kmer{A, K}) + T = Kmer{A, K, N} + data = zero_tuple(T) + nbits = BioSequences.bits_per_symbol(A()) + for (i, bool) in enumerate(indices) + bool || continue + (_, data) = + leftshift_carry(data, nbits, BioSequences.extract_encoded_element(kmer, i)) + end + T(unsafe, data) +end +function Base.getindex(kmer::Kmer{A}, indices::AbstractVector{<:Integer}) where {A} + K = length(indices) + N = n_coding_elements(Kmer{A, K}) + T = Kmer{A, K, N} + data = zero_tuple(T) + nbits = BioSequences.bits_per_symbol(A()) + for i in indices + checkbounds(kmer, i) + (_, data) = + leftshift_carry(data, nbits, BioSequences.extract_encoded_element(kmer, i)) + end + T(unsafe, data) +end -""" -Base.getindex(seq::Kmer, i::UnitRange) +@inline function BioSequences.bitindex(kmer::Kmer, i::Integer) + BioSequences.bitindex(kmer, UInt(i)::UInt) +end -Slice a Kmer by a UnitRange. +@inline function BioSequences.bitindex(kmer::Kmer, i::UInt)::Tuple{UInt, UInt} + bps = BioSequences.bits_per_symbol(kmer) % UInt + bpe = (8 * sizeof(UInt)) % UInt + num = (UInt(i) - UInt(1) + n_unused(typeof(kmer)) % UInt) * bps + (i, o) = divrem(num, bpe) + o = bpe - o - bps + i + 1, o +end -!!! warning - Using this function will introduce performance penalties in your code if - you pass values of `i` that are not constants that can be propagated. -""" -@inline function Base.getindex(seq::Kmer{A}, i::UnitRange) where A - @boundscheck Base.checkbounds(seq, i) - ind(s, i) = BioSequences.index(BioSequences.bitindex(s, i)) - off(s, i) = BioSequences.offset(BioSequences.bitindex(s, i)) - isempty(i) && return Kmer{A, 0, 0}(()) - rshift = (64 - off(seq, last(i) + 1)) & 63 - stop = ind(seq, last(i)) - start = BioSequences.index(BioSequences.bitindex(seq, first(i)) + rshift) - data = Kmers.rightshift_carry(seq.data, rshift) - T = Kmers.kmertype(Kmer{A, length(i)}) - return T(data[start:stop]) -end \ No newline at end of file +@inline function Base.setindex(kmer::Kmer, i::Integer, s) + @boundscheck checkbounds(kmer, i) + bps = BioSequences.bits_per_symbol(kmer) + symbol = convert(eltype(kmer), s) + encoding = UInt(BioSequences.encode(Alphabet(kmer), symbol)) + (i, o) = BioSequences.bitindex(kmer, i % UInt) + element = @inbounds kmer.data[i] + mask = left_shift(UInt(1) << bps - 1, o) + element &= ~mask + element |= left_shift(encoding, o) + typeof(kmer)(unsafe, @inbounds Base.setindex(kmer.data, element, i)) +end diff --git a/src/iterators/CanonicalKmers.jl b/src/iterators/CanonicalKmers.jl new file mode 100644 index 0000000..d972ced --- /dev/null +++ b/src/iterators/CanonicalKmers.jl @@ -0,0 +1,225 @@ +""" + FwRvIterator{A <: NucleicAcidAlphabet, K, S} + +Iterates 2-tuples of `(forward, reverse_complement)` of every kmer of type +`Kmer{A, K}` from the underlying sequence, in order. +`S` signifies the type of the underlying sequence. +This is typically more efficient than iterating over a `FwKmers` and +computing `reverse_complement` on every element. + +See also: [`FwKmers`](@ref), [`CanonicalKmers`](@ref) + +# Examples: +```jldoctest +julia> collect(FwRvIterator{DNAAlphabet{4}, 3}("AGCGT")) +3-element Vector{Tuple{Mer{3, DNAAlphabet{4}, 1}, Mer{3, DNAAlphabet{4}, 1}}}: + (AGC, GCT) + (GCG, CGC) + (CGT, ACG) + +julia> collect(FwRvIterator{DNAAlphabet{2}, 3}("AGNGT")) +ERROR: cannot encode 0x4e (Char 'N') in DNAAlphabet{2} +[...] +``` +""" +struct FwRvIterator{A <: NucleicAcidAlphabet, K, S} + seq::S + + function FwRvIterator{A, K, S}(seq::S) where {A, K, S} + K isa Int || error("K must be an Int") + K > 0 || error("K must be at least 1") + new{A, K, S}(seq) + end +end + +"`FwRvDNAIterator{K, S}`: Alias for `FwRvIterator{DNAAlphabet{2}, K, S}`" +const FwRvDNAIterator{K, S} = FwRvIterator{DNAAlphabet{2}, K, S} + +"`FwRvRNAIterator{K, S}`: Alias for `FwRvIterator{RNAAlphabet{2}, K, S}`" +const FwRvRNAIterator{K, S} = FwRvIterator{RNAAlphabet{2}, K, S} + +source_type(::Type{FwRvIterator{A, K, S}}) where {A, K, S} = S +kmertype(::Type{<:FwRvIterator{A, K}}) where {A, K} = derive_type(Kmer{A, K}) +kmertype(it::FwRvIterator) = kmertype(typeof(it)) +Base.eltype(T::Type{<:FwRvIterator{A, K}}) where {A, K} = + Tuple{K, K} where {K <: kmertype(T)} + +@inline function Base.length(it::FwRvIterator{A, K, S}) where {A, K, S} + src = used_source(RecodingScheme(A(), S), it.seq) + max(0, length(src) - K + 1) +end + +FwRvIterator{A, K}(s) where {A <: Alphabet, K} = FwRvIterator{A, K, typeof(s)}(s) + +@inline function Base.iterate(it::FwRvIterator{A, K, S}, state...) where {A, K, S} + iterate_kmer(RecodingScheme(A(), S), it, state...) +end + +# For the first kmer, we extract it, then reverse complement. +# When it's not done incrementally, it's faster to RC the whole +# kmer at once. +@inline function iterate_kmer(R::RecodingScheme, it::FwRvIterator{A, K}) where {A, K} + length(it.seq) < K && return nothing + fw = unsafe_extract(R, kmertype(it), it.seq, 1) + rv = reverse_complement(fw) + ((fw, rv), (fw, rv, K + 1)) +end + +# Here, we need to convert to an abstractvector +@inline function iterate_kmer( + R::AsciiEncode, + it::FwRvIterator{A, K, S}, +) where {A <: NucleicAcidAlphabet, K, S} + src = used_source(RecodingScheme(A(), S), it.seq) + Base.require_one_based_indexing(src) + length(src) < K && return nothing + fw = unsafe_extract(R, kmertype(it), src, 1) + rv = reverse_complement(fw) + ((fw, rv), (fw, rv, K + 1)) +end + +@inline function iterate_kmer( + ::GenericRecoding, + it::FwRvIterator, + state::Tuple{Kmer, Kmer, Int}, +) + (fw, rv, i) = state + i > length(it.seq) && return nothing + symbol = convert(eltype(fw), @inbounds it.seq[i]) + fw = shift(fw, symbol) + rv = shift_first(rv, complement(symbol)) + ((fw, rv), (fw, rv, i + 1)) +end + +@inline function iterate_kmer( + ::Copyable, + it::FwRvIterator{<:TwoBit, K, <:BioSequence{<:TwoBit}}, + state::Tuple{Kmer, Kmer, Int}, +) where {K} + (fw, rv, i) = state + i > length(it.seq) && return nothing + encoding = UInt(BioSequences.extract_encoded_element(it.seq, i)) + fw = shift_encoding(fw, encoding) + rv = shift_first_encoding(rv, encoding ⊻ 0x03) + ((fw, rv), (fw, rv, i + 1)) +end + +@inline function iterate_kmer( + ::Copyable, + it::FwRvIterator{<:FourBit, K, <:BioSequence{<:FourBit}}, + state::Tuple{Kmer, Kmer, Int}, +) where {K} + (fw, rv, i) = state + i > length(it.seq) && return nothing + encoding = UInt(BioSequences.extract_encoded_element(it.seq, i)) + fw = shift_encoding(fw, encoding) + rc_encoding = + reinterpret(UInt8, complement(reinterpret(eltype(rv), encoding % UInt8))) % UInt + rv = shift_first_encoding(rv, rc_encoding) + ((fw, rv), (fw, rv, i + 1)) +end + +@inline function iterate_kmer(::TwoToFour, it::FwRvIterator, state::Tuple{Kmer, Kmer, Int}) + (fw, rv, i) = state + i > length(it.seq) && return nothing + encoding = UInt(BioSequences.extract_encoded_element(it.seq, i)) + fw = shift_encoding(fw, left_shift(UInt(1), encoding)) + rv = shift_first_encoding(rv, left_shift(UInt(1), encoding ⊻ 0x03)) + ((fw, rv), (fw, rv, i + 1)) +end + +@inline function iterate_kmer( + ::FourToTwo, + it::FwRvIterator{A, K, <:BioSequence}, + state::Tuple{Kmer, Kmer, Int}, +) where {A, K} + (fw, rv, i) = state + i > length(it.seq) && return nothing + encoding = UInt(BioSequences.extract_encoded_element(it.seq, i))::UInt + isone(count_ones(encoding)) || throw_uncertain(Alphabet(fw), eltype(it.seq), encoding) + enc = trailing_zeros(encoding) % UInt + fw = shift_encoding(fw, enc) + rv = shift_first_encoding(rv, enc ⊻ 0x03) + ((fw, rv), (fw, rv, i + 1)) +end + +@inline function iterate_kmer( + ::AsciiEncode, + it::FwRvIterator{A}, + state::Tuple{Kmer, Kmer, Int}, +) where {A} + src = used_source(RecodingScheme(A(), source_type(typeof(it))), it.seq) + Base.require_one_based_indexing(src) + (fw, rv, i) = state + i > length(src) && return nothing + byte = @inbounds src[i] + encoding = BioSequences.ascii_encode(A(), byte) + if encoding > 0x7f + throw(BioSequences.EncodeError(A(), repr(byte))) + end + # Hopefully this branch is eliminated at compile time... + rc_encoding = if Alphabet(fw) isa FourBit + reinterpret(UInt8, complement(reinterpret(DNA, encoding))) + elseif Alphabet(fw) isa TwoBit + encoding ⊻ 0x03 + else + error( + "Complementing encoding of a Nucleotide AsciiAlphabet which is neither 2 or 4 " * + "bits have not been implemented yet.", + ) + end + fw = shift_encoding(fw, encoding % UInt) + rv = shift_first_encoding(rv, rc_encoding % UInt) + ((fw, rv), (fw, rv, i + 1)) +end + +""" + CanonicalKmers{A <: NucleicAcidAlphabet, K, S} <: AbstractKmerIterator{A, K} + +Iterator of canonical nucleic acid kmers. The result of this iterator is equivalent +to calling `canonical` on each value of a `FwKmers` iterator, but may be more +efficient. + +!!! note + When counting small kmers, it may be more efficient to count `FwKmers`, + then call `canonical` only once per unique kmer. + +Can be constructed more conventiently with the constructors `CanonicalDNAMers{K}(s)` +`CanonicalRNAMers{K}(s)` + +# Examples: +```jldoctest +julia> collect(CanonicalRNAMers{3}("AGCGA")) +3-element Vector{Kmer{RNAAlphabet{2}, 3, 1}}: + AGC + CGC + CGA +``` +""" +struct CanonicalKmers{A <: NucleicAcidAlphabet, K, S} <: AbstractKmerIterator{A, K} + it::FwRvIterator{A, K, S} +end + +source_type(::Type{CanonicalKmers{A, K, S}}) where {A, K, S} = S +@inline Base.length(it::CanonicalKmers) = length(it.it) + +# Constructors +function CanonicalKmers{A, K}(s::S) where {S, A <: NucleicAcidAlphabet, K} + CanonicalKmers{A, K, S}(FwRvIterator{A, K}(s)) +end +function CanonicalKmers{A, K, S}(s::S) where {S, A <: NucleicAcidAlphabet, K} + CanonicalKmers{A, K, S}(FwRvIterator{A, K}(s)) +end + +"`CanonicalDNAMers{K, S}`: Alias for `CanonicalKmers{DNAAlphabet{2}, K, S}`" +const CanonicalDNAMers{K, S} = CanonicalKmers{DNAAlphabet{2}, K, S} + +"`CanonicalRNAMers{K, S}`: Alias for `CanonicalKmers{RNAAlphabet{2}, K, S}`" +const CanonicalRNAMers{K, S} = CanonicalKmers{RNAAlphabet{2}, K, S} + +@inline function Base.iterate(it::CanonicalKmers{A, K, S}, state...) where {A, K, S} + it = iterate(it.it, state...) + isnothing(it) && return nothing + ((fw, rv), state) = it + (fw < rv ? fw : rv, state) +end diff --git a/src/iterators/FwKmers.jl b/src/iterators/FwKmers.jl new file mode 100644 index 0000000..eb6af9e --- /dev/null +++ b/src/iterators/FwKmers.jl @@ -0,0 +1,129 @@ +""" + FwKmers{A <: Alphabet, K, S} <: AbstractKmerIterator{A, K} + +Iterator of forward kmers. `S` signifies the type of the underlying sequence, +and the eltype of the iterator is `Kmer{A, K, N}` with the appropriate `N`. +The elements in a `FwKmers{A, K, S}(s::S)` correspond to all the `Kmer{A, K}` +in `s`, in order. + +Can be constructed more conventiently with the constructors `FwDNAMers{K}(s)` +and similar also for `FwRNAMers` and `FwAAMers`. + +# Examples: +```jldoctest +julia> s = "AGCGTATA"; + +julia> v = collect(FwDNAMers{3}(s)); + +julia> v == [DNAKmer{3}(s[i:i+2]) for i in 1:length(s)-2] +true + +julia> eltype(v), length(v) +(Kmer{DNAAlphabet{2}, 3, 1}, 6) + +julia> collect(FwRNAMers{3}(rna"UGCDUGAVC")) +ERROR: cannot encode D in RNAAlphabet{2} +``` +""" +struct FwKmers{A <: Alphabet, K, S} <: AbstractKmerIterator{A, K} + seq::S + + function FwKmers{A, K, S}(seq::S) where {A, K, S} + K isa Int || error("K must be an Int") + K > 0 || error("K must be at least 1") + new{A, K, S}(seq) + end +end + +source_type(::Type{FwKmers{A, K, S}}) where {A, K, S} = S + +@inline function Base.length(it::FwKmers{A, K, S}) where {A, K, S} + src = used_source(RecodingScheme(A(), S), it.seq) + max(0, length(src) - K + 1) +end + +# Constructors +FwKmers{A, K}(s) where {A <: Alphabet, K} = FwKmers{A, K, typeof(s)}(s) + +"`FwDNAMers{K, S}`: Alias for `FwKmers{DNAAlphabet{2}, K, S}`" +const FwDNAMers{K, S} = FwKmers{DNAAlphabet{2}, K, S} + +"`FwRNAMers{K, S}`: Alias for `FwKmers{RNAAlphabet{2}, K, S}`" +const FwRNAMers{K, S} = FwKmers{RNAAlphabet{2}, K, S} + +"`FwAAMers{K, S}`: Alias for `FwKmers{AminoAcidAlphabet, K, S}`" +const FwAAMers{K, S} = FwKmers{AminoAcidAlphabet, K, S} + +@inline function Base.iterate(it::FwKmers{A, K, S}, state...) where {A, K, S} + iterate_kmer(RecodingScheme(A(), S), it, state...) +end + +# For the first kmer, we just forward to `unsafe_extract` +@inline function iterate_kmer(R::RecodingScheme, it::FwKmers) + length(it.seq) < ksize(eltype(it)) && return nothing + kmer = unsafe_extract(R, eltype(it), it.seq, 1) + (kmer, (kmer, ksize(eltype(it)) + 1)) +end + +# Here, we need to convert to an abstractvector +@inline function iterate_kmer( + R::AsciiEncode, + it::FwKmers{A, K, S}, +) where {A <: Alphabet, K, S} + src = used_source(RecodingScheme(A(), S), it.seq) + Base.require_one_based_indexing(src) + length(src) < K && return nothing + kmer = unsafe_extract(R, eltype(it), src, 1) + (kmer, (kmer, K + 1)) +end + +@inline function iterate_kmer(::GenericRecoding, it::FwKmers, state::Tuple{Kmer, Int}) + (kmer, i) = state + i > length(it.seq) && return nothing + symbol = @inbounds it.seq[i] + new_kmer = shift(kmer, convert(eltype(kmer), symbol)) + (new_kmer, (new_kmer, nextind(it.seq, i))) +end + +@inline function iterate_kmer(::Copyable, it::FwKmers, state::Tuple{Kmer, Int}) + (kmer, i) = state + i > length(it.seq) && return nothing + encoding = UInt(BioSequences.extract_encoded_element(it.seq, i)) + new_kmer = shift_encoding(kmer, encoding) + (new_kmer, (new_kmer, nextind(it.seq, i))) +end + +@inline function iterate_kmer(::TwoToFour, it::FwKmers, state::Tuple{Kmer, Int}) + (kmer, i) = state + i > length(it.seq) && return nothing + encoding = left_shift(UInt(1), UInt(BioSequences.extract_encoded_element(it.seq, i))) + new_kmer = shift_encoding(kmer, encoding) + (new_kmer, (new_kmer, nextind(it.seq, i))) +end + +@inline function iterate_kmer( + ::FourToTwo, + it::FwKmers{A, K, <:BioSequence}, + state::Tuple{Kmer, Int}, +) where {A, K} + (kmer, i) = state + i > length(it.seq) && return nothing + encoding = UInt(BioSequences.extract_encoded_element(it.seq, i))::UInt + isone(count_ones(encoding)) || throw_uncertain(Alphabet(kmer), eltype(it.seq), encoding) + kmer = shift_encoding(kmer, trailing_zeros(encoding) % UInt) + return (kmer, (kmer, nextind(it.seq, i))) +end + +@inline function iterate_kmer(::AsciiEncode, it::FwKmers, state::Tuple{Kmer, Int}) + src = used_source(RecodingScheme(Alphabet(eltype(it)), source_type(typeof(it))), it.seq) + Base.require_one_based_indexing(src) + (kmer, i) = state + i > length(src) && return nothing + byte = @inbounds src[i] + encoding = BioSequences.ascii_encode(Alphabet(eltype(it)), byte) + if encoding > 0x7f + throw(BioSequences.EncodeError(Alphabet(eltype(it)), repr(byte))) + end + kmer = shift_encoding(kmer, encoding % UInt) + return (kmer, (kmer, nextind(src, i))) +end diff --git a/src/iterators/SpacedKmers.jl b/src/iterators/SpacedKmers.jl new file mode 100644 index 0000000..26fbfae --- /dev/null +++ b/src/iterators/SpacedKmers.jl @@ -0,0 +1,139 @@ +""" + SpacedKmers{A <: Alphabet, K, J, S} <: AbstractKmerIterator{A, K} + +Iterator of kmers with step size. `J` signifies the step size, `S` +the type of the underlying sequence, and the eltype of the iterator +is `Kmer{A, K, N}` with the appropriate `N`. + +For example, a `SpacedKmers{AminoAcidAlphabet, 3, 5, Vector{UInt8}}` sampling +over `seq::Vector{UInt8}` will sample all kmers corresponding to +`seq[1:3], seq[6:8], seq[11:13]` etc. + +See also: [`each_codon`](@ref), [`FwKmers`](@ref) + +# Examples: +```jldoctest +julia> collect(SpacedDNAMers{3, 2}("AGCGTATA")) +3-element Vector{Kmer{DNAAlphabet{2}, 3, 1}}: + AGC + CGT + TAT +``` +""" +struct SpacedKmers{A <: Alphabet, K, J, S} <: AbstractKmerIterator{A, K} + seq::S + + function SpacedKmers{A, K, J, S}(seq::S) where {A, K, J, S} + K isa Int || error("K must be an Int") + K > 0 || error("K must be at least 1") + J isa Int || error("J must be an Int") + J > 0 || error("J must be at least 1") + new{A, K, J, S}(seq) + end +end + +source_type(::Type{SpacedKmers{A, K, J, S}}) where {A, K, J, S} = S +stepsize(::SpacedKmers{A, K, J}) where {A, K, J} = J + +@inline function Base.length(it::SpacedKmers{A, K, J}) where {A, K, J} + src = used_source(RecodingScheme(A(), source_type(typeof(it))), it.seq) + L = length(src) + L < K ? 0 : div((L - K), J) + 1 +end + +SpacedKmers{A, K, J}(s) where {A <: Alphabet, K, J} = SpacedKmers{A, K, J, typeof(s)}(s) + +"`SpacedDNAMers{K, J, S}`: Alias for `SpacedKmers{DNAAlphabet{2}, K, J, S}`" +const SpacedDNAMers{K, J, S} = SpacedKmers{DNAAlphabet{2}, K, J, S} + +"`SpacedRNAMers{K, J, S}`: Alias for `SpacedKmers{RNAAlphabet{2}, K, J, S}`" +const SpacedRNAMers{K, J, S} = SpacedKmers{RNAAlphabet{2}, K, J, S} + +"`SpacedAAMers{K, J, S}`: Alias for `SpacedKmers{AminoAcidAlphabet, K, J, S}`" +const SpacedAAMers{K, J, S} = SpacedKmers{AminoAcidAlphabet, K, J, S} + +""" + each_codon(s::BioSequence{<:Union{DNAAlphabet, RNAAlphabet}}) + each_codon(::Type{<:Union{DNA, RNA}}, s) + +Construct an iterator of nucleotide 3-mers with step size 3 from `s`. +The sequence `s` may be an RNA or DNA biosequence, in which case the element +type is inferred, or the element type may be specified explicitly, in which +case `s` may be a byte-like sequence such as a `String` or `Vector{UInt8}`. + +This function returns [`SpacedKmers`](@ref) iterator. + +See also: [`SpacedKmers`](@ref) + +Examples: +```jldoctest +julia> collect(each_codon(DNA, "TGACGATCGAC")) +3-element Vector{Kmer{DNAAlphabet{2}, 3, 1}}: + TGA + CGA + TCG +``` +""" +each_codon(::Type{DNA}, s) = SpacedDNAMers{3, 3}(s) +each_codon(::Type{RNA}, s) = SpacedRNAMers{3, 3}(s) + +each_codon(s::BioSequence{<:DNAAlphabet}) = SpacedDNAMers{3, 3}(s) +each_codon(s::BioSequence{<:RNAAlphabet}) = SpacedRNAMers{3, 3}(s) + +@inline function Base.iterate(it::SpacedKmers{A}, state...) where {A} + iterate_kmer(RecodingScheme(A(), source_type(typeof(it))), it, state...) +end + +# TODO: Maybe in all kmer iterators, instantiate it with the source type, +# so we don't have to get the source type in functions (and thus +# it is allwoed to be a costly operation). +# However, this means we instantiate e.g. a FwKmers{A, K, S} and change S +# in the source type in the constructor +@inline function iterate_kmer( + R::RecodingScheme, + it::SpacedKmers{A, K}, +) where {A <: Alphabet, K} + length(it.seq) < K && return nothing + kmer = unsafe_extract( + R, + eltype(it), + used_source(RecodingScheme(A(), source_type(typeof(it))), it.seq), + 1, + ) + next_index = 1 + max(stepsize(it), K) + (kmer, (kmer, next_index)) +end + +# Here, we need to convert to an abstractvector +# TODO: This function and the one above can be merged with the FwKmers one? +@inline function iterate_kmer( + R::AsciiEncode, + it::SpacedKmers{A, K, J, S}, +) where {A <: Alphabet, K, J, S} + src = used_source(RecodingScheme(A(), S), it.seq) + Base.require_one_based_indexing(src) + length(src) < K && return nothing + kmer = unsafe_extract(R, eltype(it), src, 1) + next_index = 1 + max(stepsize(it), K) + (kmer, (kmer, next_index)) +end + +@inline function iterate_kmer( + ::RecodingScheme, + it::SpacedKmers{A, K, J, S}, + state, +) where {A, K, S, J} + src = used_source(RecodingScheme(A(), S), it.seq) + R = RecodingScheme(A(), S) + Base.require_one_based_indexing(src) + (kmer, i) = state + i > lastindex(src) - min(K, J) + 1 && return nothing + next_i = i + J + # This branch should be resolved statically + if J ≥ K + kmer = unsafe_extract(R, eltype(it), src, i) + else + kmer = unsafe_shift_from(R, kmer, src, i, Val{J}()) + end + (kmer, (kmer, next_i)) +end diff --git a/src/iterators/UnambiguousKmers.jl b/src/iterators/UnambiguousKmers.jl new file mode 100644 index 0000000..b386881 --- /dev/null +++ b/src/iterators/UnambiguousKmers.jl @@ -0,0 +1,148 @@ +""" + UnambiguousKmers{A <: TwoBit, K, S} + +Iterator of `(kmer, index)`, where `kmer` are 2-bit nucleic acid kmers in the +underlying sequence, and `index::Int` the starting position of the kmer in the +sequence. +The extracted kmers differ from those of `FwKmers` in that any kmers +containing ambiguous nucleotides are skipped, whereas using `FwKmers`, encountering +unambiguous nucleotides result in an error. + +This iterator can be constructed more conventiently with the constructors +`UnambiguousDNAMers{K}(s)` and `UnambiguousRNAMers{K}(s)`. + +!!! note + To obtain canonical unambiguous kmers, simply call `canonical` on each kmer output + by `UnambiguousKmers`. + +# Examples: +```jldoctest +julia> it = UnambiguousRNAMers{4}(dna"TGAGCWKCATC"); + +julia> collect(it) +3-element Vector{Tuple{Kmer{RNAAlphabet{2}, 4, 1}, Int64}}: + (UGAG, 1) + (GAGC, 2) + (CAUC, 8) +``` +""" +struct UnambiguousKmers{A <: TwoBit, K, S} + it::FwKmers{A, K, S} +end + +Base.IteratorSize(::Type{<:UnambiguousKmers}) = Base.SizeUnknown() +Base.IteratorSize(::Type{<:UnambiguousKmers{A, K, <:NucSeq{2}}}) where {A <: TwoBit, K} = + Base.HasLength() + +Base.length(it::UnambiguousKmers{A, K, <:NucSeq{2}}) where {A, K} = length(it.it) + +function Base.eltype(::Type{<:UnambiguousKmers{A, K}}) where {A, K} + Tuple{derive_type(Kmer{A, K}), Int} +end + +source_type(::Type{UnambiguousKmers{A, K, S}}) where {A, K, S} = S + +# Constructors +function UnambiguousKmers{A, K}(s::S) where {S, A <: TwoBit, K} + UnambiguousKmers{A, K, S}(FwKmers{A, K}(s)) +end +function UnambiguousKmers{A, K, S}(s::S) where {S, A <: TwoBit, K} + UnambiguousKmers{A, K, S}(FwKmers{A, K}(s)) +end + +"`UnambiguousDNAMers{K, S}`: Alias for `UnambiguousKmers{DNAAlphabet{2}, K, S}`" +const UnambiguousDNAMers{K, S} = UnambiguousKmers{DNAAlphabet{2}, K, S} + +"`UnambiguousRNAMers{K, S}`: Alias for `UnambiguousKmers{RNAAlphabet{2}, K, S}`" +const UnambiguousRNAMers{K, S} = UnambiguousKmers{RNAAlphabet{2}, K, S} + +@inline function Base.iterate(it::UnambiguousKmers{A, K, S}, state...) where {A, K, S} + R = RecodingScheme(A(), S) + iterate_kmer(R, it, state...) +end + +@inline function iterate_kmer(::Copyable, it::UnambiguousKmers) + itval = iterate(it.it) + isnothing(itval) && return nothing + (kmer, state) = itval + ((kmer, 1), state) +end + +@inline function iterate_kmer(::Copyable, it::UnambiguousKmers, state::Tuple{Kmer, Integer}) + itval = iterate(it.it, state) + isnothing(itval) && return nothing + (_, i) = state + (kmer, state) = itval + ((kmer, i - ksize(typeof(kmer)) + 1), state) +end + +@inline function iterate_kmer( + ::RecodingScheme, + it::UnambiguousKmers{A, K, S}, +) where {A, K, S} + T = derive_type(Kmer{A, K}) + state = (T(unsafe, zero_tuple(T)), K, 1) + iterate_kmer(RecodingScheme(A(), S), it, state) +end + +@inline function iterate_kmer( + ::RecodingScheme, + it::UnambiguousKmers, + state::Tuple{Kmer, Int, Int}, +) + (kmer, remaining, index) = state + K = ksize(typeof(kmer)) + while !iszero(remaining) + index > lastindex(it.it.seq) && return nothing + symbol = convert(eltype(kmer), it.it.seq[index]) + index += 1 + if isambiguous(symbol) + remaining = K + else + remaining -= 1 + kmer = shift(kmer, symbol) + end + end + ((kmer, index - K), (kmer, 1, index)) +end + +@inline function iterate_kmer( + ::AsciiEncode, + it::UnambiguousKmers{A, K, S}, + state::Tuple{Kmer, Int, Int}, +) where {A <: TwoBit, K, S} + src = used_source(RecodingScheme(A(), S), it.it.seq) + Base.require_one_based_indexing(src) + (kmer, remaining, index) = state + while !iszero(remaining) + index > lastindex(src) && return nothing + byte = @inbounds src[index] + index += 1 + encoding = @inbounds ASCII_SKIPPING_LUT[(byte + 0x01) % Int] + if encoding == 0xff + throw(BioSequences.EncodeError(Alphabet(kmer), repr(byte))) + elseif encoding == 0xf0 + remaining = K + else + remaining -= 1 + kmer = shift_encoding(kmer, encoding % UInt) + end + end + ((kmer, index - K), (kmer, 1, index)) +end + +@inline function iterate_kmer( + ::FourToTwo, + it::UnambiguousKmers{A, K, S}, + state::Tuple{Kmer, Int, Int}, +) where {A <: TwoBit, K, S} + (kmer, remaining, index) = state + while !iszero(remaining) + index > lastindex(it.it.seq) && return nothing + encoding = UInt(BioSequences.extract_encoded_element(it.it.seq, index))::UInt + kmer = shift_encoding(kmer, (trailing_zeros(encoding)) % UInt) + index += 1 + remaining = isone(count_ones(encoding)) ? remaining - 1 : K + end + ((kmer, index - K), (kmer, 1, index)) +end diff --git a/src/iterators/common.jl b/src/iterators/common.jl new file mode 100644 index 0000000..dfbbbfc --- /dev/null +++ b/src/iterators/common.jl @@ -0,0 +1,32 @@ +""" + AbstractKmerIterator{A <: Alphabet, K} + +Abstract type for kmer iterators. The element type is `Kmer{A, K, N}`, +with the appropriately derived N. + +Functions to implement: +* `Base.iterate` +* `Base.length` or `Base.IteratorSize` if not `HasLength` +""" +abstract type AbstractKmerIterator{A <: Alphabet, K} end + +function Base.eltype(::Type{<:AbstractKmerIterator{A, K}}) where {A, K} + Kmer{A, K, n_coding_elements(Kmer{A, K})} +end + +function used_source(::AsciiEncode, s::AbstractString) + is_ascii(typeof(s)) ? codeunits(s) : s +end +used_source(::RecodingScheme, s) = s + +const ASCII_SKIPPING_LUT = let + v = fill(0xff, 256) + for (i, s) in [(0, "Aa"), (1, "cC"), (2, "gG"), (3, "TtUu")], c in s + v[UInt8(c) + 1] = i + end + for c in "-MRSVWYHKDBN" + v[UInt8(c) + 1] = 0xf0 + v[UInt8(lowercase(c)) + 1] = 0xf0 + end + Tuple(v) +end diff --git a/src/kmer.jl b/src/kmer.jl index b71a57a..246b6ef 100644 --- a/src/kmer.jl +++ b/src/kmer.jl @@ -1,590 +1,478 @@ -### -### Kmer Type definition -### - -# Include some basic tuple bitflipping ops - the secret sauce to efficiently -# manipping Kmer's static data. -include("tuple_bitflipping.jl") - -""" - Kmers.Unsafe - -Trait object used to access unsafe methods of functions. -`unsafe` is the singleton of `Unsafe`. -""" -struct Unsafe end -const unsafe = Unsafe() - """ Kmer{A<:Alphabet,K,N} <: BioSequence{A} -A parametric, immutable, bitstype for representing Kmers - short sequences. -Given the number of Kmers generated from raw sequencing reads, avoiding -repetetive memory allocation and triggering of garbage collection is important, -as is the ability to effectively pack Kmers into arrays and similar collections. - -In practice that means we an immutable bitstype as the internal representation -of these sequences. Thankfully, this is not much of a limitation - kmers are -rarely manipulated and so by and large don't have to be mutable. - -Excepting their immutability, they fulfill the rest of the API and behaviours -expected from a concrete `BioSequence` type, and non-mutating transformations -of the type are still defined. - -!!! warning - Given their immutability, `setindex` and mutating sequence transformations - are not implemented for Kmers e.g. `reverse_complement!`. -!!! tip - Note that some sequence transformations that are not mutating are - available, since they can return a new kmer value as a result e.g. - `reverse_complement`. -""" -struct Kmer{A<:Alphabet,K,N} <: BioSequence{A} - data::NTuple{N,UInt64} - - # This unsafe method do not clip the head - Kmer{A,K,N}(::Unsafe, data::NTuple{N,UInt64}) where {A<:Alphabet,K,N} = new{A,K,N}(data) +An immutable bitstype for representing k-mers - short `BioSequences` +of a fixed length `K`. +Since they can be stored directly in registers, `Kmer`s are generally the most +efficient type of `BioSequence`, when `K` is small and known at compile time. - function Kmer{A,K,N}(data::NTuple{N,UInt64}) where {A<:Alphabet,K,N} - checkmer(Kmer{A,K,N}) - x = n_unused(Kmer{A,K,N}) * BioSequences.bits_per_symbol(A()) - return new(_cliphead(x, data...)) - end -end +The `N` parameter is derived from `A` and `K` and is not a free parameter. -BioSequences.encoded_data(seq::Kmer{A,K,N}) where {A,K,N} = seq.data +See also: [`DNAKmer`](@ref), [`RNAKmer`](@ref), [`AAKmer`](@ref), [`AbstractKmerIterator`](@ref) -# Create a blank ntuple of appropriate length for a given Kmer with N. -@inline blank_ntuple(::Type{Kmer{A,K,N}}) where {A,K,N} = ntuple(x -> zero(UInt64), Val{N}()) +# Examples +```jldoctest +julia> RNAKmer{5}("ACGUC") +RNA 5-mer: +ACGUC -### -### _build_kmer_data -### +julia> Kmer{DNAAlphabet{4}, 6}(dna"TGCTTA") +DNA 6-mer: +TGCTTA -#= -These are (hopefully!) very optimised kernel functions for building kmer internal -data from individual elements or from sequences. Kmers themselves are static, -tuple-based structs, and so I really didn't want these functions to create memory -allocations or GC activity through use of vectors an such, for what should be -the creation of a single, rather simple value. -=# +julia> AAKmer{5}((lowercase(i) for i in "KLWYR")) +AminoAcid 5-mer: +KLWYR +julia> RNAKmer{3}("UAUC") # wrong length +ERROR: +[...] +``` """ - _build_kmer_data(::Type{Kmer{A,K,N}}, seq::LongSequence{A}, from::Int = 1) where {A,K,N} - -Construct a ntuple of the bits data for an instance of a Kmer{A,K,N}. +struct Kmer{A <: Alphabet, K, N} <: BioSequence{A} + # The number of UInt is always exactly the number needed, no less, no more. + # The first symbols pack into the first UInts + # An UInt with N elements pack into the lowest bits of the UInt, with the + # first symbols in the higher parts of the UInt. + # Hence, a sequence A-G of 16-bit elements would pack like: + # ( ABC, DEFG) + # ^ 16 unused bits, the unused bits are always top bits of first UInt + # Unused bits are always zero + + # This layout complicates some Kmer construction code, but simplifies comparison + # operators, and we really want Kmers to be efficient. + data::NTuple{N, UInt} -This particular method is specialised for LongSequences, and for when the Kmer -and LongSequence types used, share the same alphabet, since a lot of encoding / -decoding can be skipped, and the problem is mostly one of shunting bits around. -""" -@inline function _build_kmer_data(::Type{Kmer{A,K,N}}, seq::LongSequence{A}, from::Int = 1) where {A,K,N} - checkmer(Kmer{A,K,N}) - - bits_per_sym = BioSequences.bits_per_symbol(A()) # Based on alphabet type, should constant fold. - n_head = elements_in_head(Kmer{A,K,N}) # Based on kmer type, should constant fold. - n_per_chunk = per_word_capacity(Kmer{A,K,N}) # Based on kmer type, should constant fold. - - if from + K - 1 > length(seq) - return nothing - end - - # Construct the head. - head = zero(UInt64) - @inbounds for i in from:(from + n_head - 1) - bits = UInt64(BioSequences.extract_encoded_element(seq, i)) - head = (head << bits_per_sym) | bits - end - - # And the rest of the sequence - idx = Ref(from + n_head) - tail = ntuple(Val{N - 1}()) do i - Base.@_inline_meta - body = zero(UInt64) - @inbounds for _ in 1:n_per_chunk - bits = UInt64(BioSequences.extract_encoded_element(seq, idx[])) - body = (body << bits_per_sym) | bits - idx[] += 1 - end - return body + # This unsafe method do not clip the head + function Kmer{A, K, N}(::Unsafe, data::NTuple{N, UInt}) where {A <: Alphabet, K, N} + check_kmer(Kmer{A, K, N}) + new{A, K, N}(data) end - - # Put head and tail together - return (head, tail...) end +# Useful to do e.g. `mer"TAG"d isa Mer{3}` +""" + Mer{K} +Alias for `Kmer{<:Alphabet, K}`. Useful to dispatch on `K-mers` without regard +for the alphabat -### -### Constructors -### +# Example +```jldoctest +julia> mer"DEKR"a isa Mer{4} +true +julia> DNAKmer{6}("TGATCA") isa Mer{6} +true + +julia> RNACodon <: Mer{3} +true +``` """ - Kmer{A,K,N}(itr) where {A,K,N} +const Mer{K} = Kmer{<:Alphabet, K} -Construct a `Kmer{A,K,N}` from an iterable. +# Aliases +"Alias for `Kmer{DNAAlphabet{2},K,N}`" +const DNAKmer{K, N} = Kmer{DNAAlphabet{2}, K, N} -The most generic constructor. +"Alias for `Kmer{RNAAlphabet{2},K,N}`" +const RNAKmer{K, N} = Kmer{RNAAlphabet{2}, K, N} -Currently the iterable must have `length` & support `getindex` with integers. +"Alias for `Kmer{AminoAcidAlphabet,K,N}`" +const AAKmer{K, N} = Kmer{AminoAcidAlphabet, K, N} -# Examples +"Alias for `DNAKmer{3,1}`" +const DNACodon = DNAKmer{3, 1} -```jldoctest -julia> ntseq = LongSequence("TTAGC") # 4-bit DNA alphabet -5nt DNA Sequence: -TTAGC +"Alias for `RNAKmer{3,1}`" +const RNACodon = RNAKmer{3, 1} -julia> DNAKmer{5}(ntseq) # 2-Bit DNA alphabet -DNA 5-mer: -TTAGC -``` """ -function Kmer{A,K,N}(itr) where {A,K,N} - checkmer(Kmer{A,K,N}) - - seqlen = length(itr) - if seqlen != K - throw(ArgumentError("itr does not contain enough elements ($seqlen ≠ $K)")) - end - - ## All based on alphabet type of Kmer, so should constant fold. - bits_per_sym = BioSequences.bits_per_symbol(A()) - n_head = elements_in_head(Kmer{A,K,N}) - n_per_chunk = per_word_capacity(Kmer{A,K,N}) - - # Construct the head. - head = zero(UInt64) - @inbounds for i in 1:n_head - (x, next_i) = iterate(itr, i) - sym = convert(eltype(Kmer{A,K,N}), x) - # Encode will throw if it cant encode an element. - head = (head << bits_per_sym) | UInt64(BioSequences.encode(A(), sym)) + check_kmer(::Type{Kmer{A,K,N}}) where {A,K,N} + +Internal methods that checks that the type parameters are good. + +This function should compile to a noop in case the parameterization is good. +""" +@inline function check_kmer(::Type{Kmer{A, K, N}}) where {A, K, N} + if !(K isa Int) + throw(ArgumentError("K must be an Int")) + elseif K < 0 + throw(ArgumentError("Bad kmer parameterisation. K must be greater than 0.")) end - - # And the rest of the sequence - idx = Ref(n_head + 1) - tail = ntuple(Val{N - 1}()) do i - Base.@_inline_meta - body = zero(UInt64) - @inbounds for i in 1:n_per_chunk - (x, next_idx) = iterate(itr, idx[]) - sym = convert(eltype(Kmer{A,K,N}), x) - # Encode will throw if it cant encode an element. - body = (body << bits_per_sym) | UInt64(BioSequences.encode(A(), sym)) - idx[] += 1 - end - return body + n = cld((K * BioSequences.bits_per_symbol(A())) % UInt, (sizeof(UInt) * 8) % UInt) % Int + if !(N isa Int) + throw(ArgumentError("N must be an Int")) + elseif n !== N + # This has been significantly changed conceptually from before. Now we + # don't just check K, but *enforce* the most appropriate N for K. + throw(ArgumentError("Bad kmer parameterisation. For K = $K, N should be $n")) end - - data = (head, tail...) - - return Kmer{A,K,N}(data) end -""" - Kmer{A,K,N}(seq::BioSequence{A}) +################################################ +# Compile-time functions computed on Kmer types +################################################ -Construct a `Kmer{A,K,N}` from a `BioSequence{A}`. +@inline ksize(::Type{<:Kmer{A, K}}) where {A, K} = K +@inline nsize(::Type{<:Kmer{A, K, N}}) where {A, K, N} = N +@inline n_unused(::Type{<:Kmer{A, K, N}}) where {A, K, N} = capacity(Kmer{A, K, N}) - K +@inline bits_unused(T::Type{<:Kmer}) = + n_unused(T) * BioSequences.bits_per_symbol(Alphabet(T)) -This particular method is specialised for BioSequences, and for when the Kmer -and BioSequence types used, share the same alphabet, since a lot of encoding / -decoding can be skipped, and the problem is mostly one of shunting bits around. -In the case where the alphabet of the Kmer and the alphabet of the BioSequence -differ, dispatch to the more generic constructor occurs instead. +@inline function n_coding_elements(::Type{<:Kmer{A, K}}) where {A, K} + cld(BioSequences.bits_per_symbol(A()) * K, 8 * sizeof(UInt)) +end -# Examples +@inline function per_word_capacity(::Type{<:Kmer{A}}) where {A} + div(8 * sizeof(UInt), BioSequences.bits_per_symbol(A())) +end -```jldoctest -julia> ntseq = LongSequence{DNAAlphabet{2}}("TTAGC") # 2-bit DNA alphabet -5nt DNA Sequence: -TTAGC +@inline function capacity(::Type{<:Kmer{A, K, N}}) where {A, K, N} + per_word_capacity(Kmer{A, K, N}) * N +end -julia> DNAKmer{5}(ntseq) # 2-Bit DNA alphabet -DNA 5-mer: -TTAGC -``` -""" -@inline function Kmer{A,K,N}(seq::BioSequence{A}) where {A,K,N} - checkmer(Kmer{A,K,N}) - - seqlen = length(seq) - if seqlen != K - throw(ArgumentError("seq is not the correct length ($seqlen ≠ $K)")) - end - - ## All based on alphabet type of Kmer, so should constant fold. - bits_per_sym = BioSequences.bits_per_symbol(A()) - n_head = elements_in_head(Kmer{A,K,N}) - n_per_chunk = per_word_capacity(Kmer{A,K,N}) - - # Construct the head. - head = zero(UInt64) - @inbounds for i in 1:n_head - bits = UInt64(BioSequences.extract_encoded_element(seq, i)) - head = (head << bits_per_sym) | bits - end - - # And the rest of the sequence - idx = Ref(n_head + 1) - tail = ntuple(Val{N - 1}()) do i - Base.@_inline_meta - body = zero(UInt64) - @inbounds for _ in 1:n_per_chunk - bits = UInt64(BioSequences.extract_encoded_element(seq, idx[])) - body = (body << bits_per_sym) | bits - idx[] += 1 - end - return body - end - - data = (head, tail...) - - return Kmer{A,K,N}(data) +@inline function elements_in_head(::Type{<:Kmer{A, K, N}}) where {A, K, N} + per_word_capacity(Kmer{A, K, N}) - n_unused(Kmer{A, K, N}) end +""" + derive_type(::Type{Kmer{A, K}}) -> Type{Kmer{A, K, N}} -# Convenience version of function above so you don't have to work out correct N. +Compute the fully parameterized kmer type from only the parameters `A` and `K`. """ - Kmer{A,K}(itr) where {A,K} +@inline derive_type(::Type{Kmer{A, K}}) where {A, K} = + Kmer{A, K, n_coding_elements(Kmer{A, K})} -Construct a `Kmer{A,K,N}` from an iterable. +@inline zero_tuple(T::Type{<:Kmer}) = ntuple(i -> zero(UInt), Val{nsize(T)}()) -This is a convenience method which will work out the correct `N` parameter, for -your given choice of `A` & `K`. -""" -@inline function Kmer{A,K}(itr) where {A,K} - T = kmertype(Kmer{A,K}) - return T(itr) +@inline function zero_kmer(::Type{<:Kmer{A, K}}) where {A, K} + T2 = derive_type(Kmer{A, K}) + T2(unsafe, zero_tuple(T2)) end -""" - Kmer{A}(itr) where {A} +################## +# Various methods +################## -Construct a `Kmer{A,K,N}` from an iterable. +# BioSequences interface +Base.length(x::Kmer) = ksize(typeof(x)) +Base.copy(x::Kmer) = x # immutable +BioSequences.encoded_data_eltype(::Type{<:Kmer}) = UInt -This is a convenience method which will work out K from the length of `itr`, and -the correct `N` parameter, for your given choice of `A` & `K`. +# BioSequences helper methods +BioSequences.encoded_data(seq::Kmer) = seq.data -!!! warning - Since this gets K from runtime values, this is gonna be slow! -""" -@inline Kmer{A}(itr) where {A} = Kmer{A,length(itr)}(itr) -@inline Kmer(seq::BioSequence{A}) where A = Kmer{A}(seq) +# Misc methods +Base.summary(x::Kmer{A, K, N}) where {A, K, N} = string(eltype(x), ' ', K, "-mer") -function Kmer{A1}(seq::BioSequence{A2}) where {A1 <: NucleicAcidAlphabet, A2 <: NucleicAcidAlphabet} - kmertype(Kmer{A1, length(seq)})(seq) +function Base.show(io::IO, ::MIME"text/plain", s::Kmer) + println(io, summary(s), ':') + print(io, s) end -@inline function Kmer{A}(nts::Vararg{Union{DNA, RNA}, K}) where {A <: NucleicAcidAlphabet, K} - return kmertype(Kmer{A, K})(nts) +# TODO: This is only efficient because the compiler, through Herculean effort, +# is able to completely unroll and inline the indexing operation. +@inline function _cmp(x::Kmer{A1, K1}, y::Kmer{A2, K2}) where {A1, A2, K1, K2} + if K1 == K2 + cmp(x.data, y.data) + else + m = min(K1, K2) + a = @inline x[1:m] + b = @inline y[1:m] + c = cmp(a.data, b.data) + if iszero(c) + K1 < K2 ? -1 : K2 < K1 ? 1 : 0 + else + c + end + end +end + +# Here, we don't allow comparing twobit to fourbit sequences. We could do this semantically, +# but this would open a whole can of worms, be impossible to optimise and defeat the purpose +# of using Kmers. +Base.cmp(x::Kmer{A}, y::Kmer{A}) where {A} = _cmp(x, y) +Base.cmp(x::Kmer{<:FourBit}, y::Kmer{<:FourBit}) = _cmp(x, y) +Base.cmp(x::Kmer{<:TwoBit}, y::Kmer{<:TwoBit}) = _cmp(x, y) +Base.cmp(x::Kmer{A}, y::Kmer{B}) where {A, B} = throw(MethodError(cmp, (x, y))) + +Base.isless(x::Kmer, y::Kmer) = @inline(cmp(x, y)) == -1 +Base.:(==)(x::Kmer, y::Kmer) = iszero(@inline cmp(x, y)) + +Base.:(==)(x::Kmer, y::BioSequence) = throw(MethodError(==, (x, y))) +Base.:(==)(x::BioSequence, y::Kmer) = throw(MethodError(==, (x, y))) + +Base.hash(x::Kmer, h::UInt) = hash(x.data, h ⊻ ksize(typeof(x))) + +# These constants are from the original implementation +@static if Sys.WORD_SIZE == 32 + # typemax(UInt32) / golden ratio + const FX_CONSTANT = 0x9e3779b9 +elseif Sys.WORD_SIZE == 64 + # typemax(UInt64) / pi + const FX_CONSTANT = 0x517cc1b727220a95 +else + error("Invalid word size") end +# This implementation is translated from the Rust compiler source code, +# licenced under MIT. The original source is the Firefox source code, +# also freely licensed. """ - Kmer(nts::Vararg{DNA,K}) where {K} + fx_hash(x, [h::UInt])::UInt -Construct a Kmer from a variable number `K` of DNA nucleotides. +An implementation of `FxHash`. This hash function is extremely fast, but the hashes +are of poor quality compared to Julia's default MurmurHash3. In particular: +* The hash function does not have a good avalanche effect, e.g. the lower bits + of the result depend only on the top few bits of the input +* The bitpattern zero hashes to zero -# Examples +However, for many applications, `FxHash` is good enough, if the cost of the +higher rate of hash collisions are offset by the faster speed. -```jldoctest -julia> Kmer(DNA_T, DNA_T, DNA_A, DNA_G, DNA_C) -DNA 5-mer: -TTAGC -``` -""" -@inline Kmer(nt::DNA, nts::Vararg{DNA}) = DNAKmer((nt, nts...)) +The precise hash value of a given kmer is not guaranteed to be stable across minor +releases of Kmers.jl, but _is_ guaranteed to be stable across minor versions of +Julia. -""" - Kmer(nts::Vararg{RNA,K}) where {K} +# Examples +```jldoctest +julia> x = fx_hash(mer"KWQLDE"a); -Construct a Kmer from a variable number `K` of RNA nucleotides. +julia> y = fx_hash(mer"KWQLDE"a, UInt(1)); -# Examples +julia> x isa UInt +true -```jldoctest -julia> Kmer(RNA_U, RNA_U, RNA_A, RNA_G, RNA_C) -DNA 5-mer: -UUAGC +julia> x == y +false ``` """ -@inline Kmer(nt::RNA, nts::Vararg{RNA}) = RNAKmer((nt, nts...)) - +function fx_hash(x::Kmer, h::UInt) + for i in x.data + h = (bitrotate(h, 5) ⊻ i) * FX_CONSTANT + end + h +end +fx_hash(x) = fx_hash(x, zero(UInt)) """ - Kmer(seq::String) + push(kmer::Kmer{A, K}, s)::Kmer{A, K+1} -Construct a DNA or RNA kmer from a string. +Create a new kmer which is the concatenation of `kmer` and `s`. +Returns a `K+1`-mer. -!!! warning - As a convenience method, this derives the `K`, `Alphabet`, and `N` parameters - for the `Kmer{A,K,N}` type from the input string. +!!! warn + Since the output of this function is a `K+1`-mer, use of this function + in a loop may result in type-instability. -# Examples +See also: [`push_first`](@ref), [`pop`](@ref), [`shift`](@ref) +# Examples ```jldoctest -julia> Kmer("TTAGC") -DNA 5-mer: -TTAGC +julia> push(mer"UGCUGA"r, RNA_G) +RNA 7-mer: +UGCUGAG + +julia> push(mer"W"a, 'E') +AminoAcid 2-mer: +WE ``` """ -@inline function Kmer(seq::String) - seq′ = BioSequences.remove_newlines(seq) - hast = false - hasu = false - for c in seq′ - hast |= ((c == 'T') | (c == 't')) - hasu |= ((c == 'U') | (c == 'u')) - end - if (hast & hasu) | (!hast & !hasu) - throw(ArgumentError("Can't detect alphabet type from string")) +function push(kmer::Kmer, s) + bps = BioSequences.bits_per_symbol(kmer) + A = Alphabet(kmer) + newT = derive_type(Kmer{typeof(A), length(kmer) + 1}) + # If no free space in data, add new tuple + new_data = if bits_unused(typeof(kmer)) < bps + (zero(UInt), kmer.data...) + else + kmer.data end - A = ifelse(hast & !hasu, DNAAlphabet{2}, RNAAlphabet{2}) - return Kmer{A,length(seq′)}(seq′) + # leftshift_carry the new encoding in. + encoding = UInt(BioSequences.encode(A, convert(eltype(kmer), s))) + (_, new_data) = leftshift_carry(new_data, bps, encoding) + newT(unsafe, new_data) end - """ - kmertype(::Type{Kmer{A,K}}) where {A,K} -Resolve and incomplete kmer typing, computing the N parameter of -`Kmer{A,K,N}`, given only `Kmer{A,K}`. -## Example -```julia -julia> DNAKmer{63} -Kmer{DNAAlphabet{2},63,N} where N -julia> kmertype(DNAKmer{63}) -Kmer{DNAAlphabet{2},63,2} -``` -""" -@inline function kmertype(::Type{Kmer{A,K}}) where {A,K} - return Kmer{A,K,BioSequences.seq_data_len(A, K)} -end -@inline kmertype(::Type{Kmer{A,K,N}}) where {A,K,N} = Kmer{A,K,N} - -# Aliases -"Shortcut for the type `Kmer{DNAAlphabet{2},K,N}`" -const DNAKmer{K,N} = Kmer{DNAAlphabet{2},K,N} - -"Shortcut for the type `DNAKmer{27,1}`" -const DNA27mer = DNAKmer{27,1} - -"Shortcut for the type `DNAKmer{31,1}`" -const DNA31mer = DNAKmer{31,1} - -"Shortcut for the type `DNAKmer{63,2}`" -const DNA63mer = DNAKmer{63,2} - -"Shortcut for the type `Kmer{RNAAlphabet{2},K,N}`" -const RNAKmer{K,N} = Kmer{RNAAlphabet{2},K,N} + shift(kmer::Kmer{A, K}, s)::Kmer{A, K} -"Shortcut for the type `RNAKmer{27,1}`" -const RNA27mer = RNAKmer{27,1} +Push `symbol` onto the end of `kmer`, and pop the first symbol in `kmer`. +Unlike `push`, this preserves the input type, and is less likely to result in +type instability. -"Shortcut for the type `RNAKmer{31,1}`" -const RNA31mer = RNAKmer{31,1} +See also: [`shift_first`](@ref), [`push`](@ref) -"Shortcut for the type `RNAKmer{63,2}`" -const RNA63mer = RNAKmer{63,2} - -"Shortcut for the type `Kmer{AminoAcidAlphabet,K,N}`" -const AAKmer{K,N} = Kmer{AminoAcidAlphabet,K,N} - -"Shorthand for `DNAKmer{3,1}`" -const DNACodon = DNAKmer{3,1} +# Examples +```jldoctest +julia> shift(mer"TACC"d, DNA_A) +DNA 4-mer: +ACCA -"Shorthand for `RNAKmer{3,1}`" -const RNACodon = RNAKmer{3,1} +julia> shift(mer"WKYMLPIIRS"aa, 'F') +AminoAcid 10-mer: +KYMLPIIRSF +``` +""" +function shift(kmer::Kmer{A}, s) where {A} + encoding = UInt(BioSequences.encode(A(), convert(eltype(kmer), s))) + shift_encoding(kmer, encoding) +end +""" + push_first(kmer::Kmer{A, K}, s)::Kmer{A, K+1} -### -### Base Functions -### +Create a new kmer which is the concatenation of `s` and `kmer`. +Returns a `K+1`-mer. Similar to [`push`](@ref), but places the new symbol `s` +at the front. -@inline ksize(::Type{Kmer{A,K,N}}) where {A,K,N} = K -@inline nsize(::Type{Kmer{A,K,N}}) where {A,K,N} = N -@inline per_word_capacity(::Type{Kmer{A,K,N}}) where {A,K,N} = div(64, BioSequences.bits_per_symbol(A())) -@inline per_word_capacity(seq::Kmer) = per_word_capacity(typeof(seq)) -@inline capacity(::Type{Kmer{A,K,N}}) where {A,K,N} = per_word_capacity(Kmer{A,K,N}) * N -@inline capacity(seq::Kmer) = capacity(typeof(seq)) -@inline n_unused(::Type{Kmer{A,K,N}}) where {A,K,N} = capacity(Kmer{A,K,N}) - K -@inline n_unused(seq::Kmer) = n_unused(typeof(seq)) -@inline elements_in_head(::Type{Kmer{A,K,N}}) where {A,K,N} = per_word_capacity(Kmer{A,K,N}) - n_unused(Kmer{A,K,N}) -@inline elements_in_head(seq::Kmer) = elements_in_head(typeof(seq)) +!!! warn + Since the output of this function is a `K+1`-mer, use of this function + in a loop may result in type-instability. -""" - checkmer(::Type{Kmer{A,K,N}}) where {A,K,N} +See also: [`push`](@ref), [`pop`](@ref), [`shift`](@ref) -Internal method - enforces good kmer type parameterisation. - -For a given Kmer{A,K,N} of length K, the number of words used to -represent it (N) should be the minimum needed to contain all K symbols, -no larger (wasteful) no smaller (just... wrong). +# Examples +```jldoctest +julia> push_first(mer"GCU"r, RNA_G) +RNA 4-mer: +GGCU -Because it is used on type parameters / variables, these conditions should be -checked at compile time, and the branches / error throws eliminated when the -parameterisation of the Kmer type is good. +julia> push_first(mer"W"a, 'E') +AminoAcid 2-mer: +EW +``` """ -@inline function checkmer(::Type{Kmer{A,K,N}}) where {A,K,N} - if K < 1 - throw(ArgumentError("Bad kmer parameterisation. K must be greater than 0.")) - end - n = BioSequences.seq_data_len(A, K) - if n !== N - # This has been significantly changed conceptually from before. Now we - # don't just check K, but *enforce* the most appropriate N for K. - throw(ArgumentError("Bad kmer parameterisation. For K = $K, N should be $n")) +function push_first(kmer::Kmer{A}, s) where {A} + bps = BioSequences.bits_per_symbol(A()) + newT = derive_type(Kmer{A, length(kmer) + 1}) + # If no free space in data, add new tuple + new_data = if bits_unused(typeof(kmer)) < bps + (zero(UInt), kmer.data...) + else + kmer.data end + encoding = UInt(BioSequences.encode(A(), convert(eltype(kmer), s))) + head = first(new_data) | left_shift(encoding, (elements_in_head(newT) - 1) * bps) + newT(unsafe, (head, tail(new_data)...)) end -@inline Base.length(x::Kmer{A,K,N}) where {A,K,N} = K -@inline Base.summary(x::Kmer{A,K,N}) where {A,K,N} = string(eltype(x), ' ', K, "-mer") +""" + shift_first(kmer::kmer, symbol)::typeof(kmer) -function Base.typemin(::Type{Kmer{A,K,N}}) where {A,K,N} - return Kmer{A,K,N}(unsafe, ntuple(i -> zero(UInt64), N)) -end +Push `symbol` onto the start of `kmer`, and pop the last symbol in `kmer`. -function Base.typemax(::Type{Kmer{A,K,N}}) where {A,K,N} - return Kmer{A,K,N}((typemax(UInt64), ntuple(i -> typemax(UInt64), N - 1)...)) -end +See also: [`shift`](@ref), [`push`](@ref) -@inline function rand_kmer_data(::Type{Kmer{A,K,N}}, ::Val{true}) where {A,K,N} - return Kmer{A,K,N}(ntuple(i -> rand(UInt64), Val{N}())) -end +# Examples +```jldoctest +julia> shift_first(mer"TACC"d, DNA_A) +DNA 4-mer: +ATAC -@inline function rand_kmer_data(::Type{Kmer{A,K,N}}, ::Val{false}) where {A,K,N} - ## All based on alphabet type of Kmer, so should constant fold. - bits_per_sym = BioSequences.bits_per_symbol(A()) - n_head = elements_in_head(Kmer{A,K,N}) - n_per_chunk = per_word_capacity(Kmer{A,K,N}) - # Construct the head. - head = zero(UInt64) - @inbounds for i in 1:n_head - bits = UInt64(BioSequences.encode(A(), rand(symbols(A())))) - head = (head << bits_per_sym) | bits - end - # And the rest of the sequence - tail = ntuple(Val{N - 1}()) do i - Base.@_inline_meta - body = zero(UInt64) - @inbounds for _ in 1:n_per_chunk - bits = UInt64(BioSequences.encode(A(), rand(symbols(A())))) - body = (body << bits_per_sym) | bits - end - return body - end - return (head, tail...) +julia> shift_first(mer"WKYMLPIIRS"aa, 'F') +AminoAcid 10-mer: +FWKYMLPIIR +``` +""" +function shift_first(kmer::Kmer{A}, s) where {A} + encoding = UInt(BioSequences.encode(A(), convert(eltype(kmer), s))) + shift_first_encoding(kmer, encoding) end +function shift_first_encoding(kmer::Kmer{A}, encoding::UInt) where {A} + isempty(kmer) && return kmer + bps = BioSequences.bits_per_symbol(A()) + (_, new_data) = rightshift_carry(kmer.data, bps, zero(UInt)) + head = + first(new_data) | left_shift(encoding, (elements_in_head(typeof(kmer)) - 1) * bps) + typeof(kmer)(unsafe, (head, tail(new_data)...)) +end """ - Base.rand(::Type{Kmer{A,K,N}}) where {A,K,N} - Base.rand(::Type{Kmer{A,K}}) where {A,K} + pop(kmer::Kmer{A, K})::Kmer{A, K-1} -Create a random kmer of a specified alphabet and length +Returns a new kmer with the last symbol of the input `kmer` removed. +Throws an `ArgumentError` if `kmer` is empty. + +!!! warn + Since the output of this function is a `K-1`-mer, use of this function + in a loop may result in type-instability. + +See also: [`pop_first`](@ref), [`push`](@ref), [`shift`](@ref) # Examples -```julia -julia> rand(Kmer{DNAAlphabet{2}, 3}) -BioSymbols.DNA 3-mer: -ACT +```jldoctest +julia> pop(mer"TCTGTA"d) +DNA 5-mer: +TCTGT +julia> pop(mer"QPSY"a) +AminoAcid 3-mer: +QPS + +julia> pop(mer""a) +ERROR: ArgumentError: +[...] ``` """ -@inline function Base.rand(::Type{Kmer{A,K,N}}) where {A,K,N} - checkmer(Kmer{A,K,N}) - return Kmer{A,K,N}(rand_kmer_data(Kmer{A,K,N}, BioSequences.iscomplete(A()))) +function pop(kmer::Kmer{A}) where {A} + isempty(kmer) && throw(ArgumentError("Cannot pop 0-mer")) + bps = BioSequences.bits_per_symbol(A()) + newT = derive_type(Kmer{A, length(kmer) - 1}) + (_, new_data) = rightshift_carry(kmer.data, bps, zero(UInt)) + new_data = if elements_in_head(typeof(kmer)) == 1 + tail(new_data) + else + new_data + end + newT(unsafe, new_data) end -Base.rand(::Type{Kmer{A,K}}) where {A,K} = rand(kmertype(Kmer{A,K})) +""" + pop_first(kmer::Kmer{A, K})::Kmer{A, K-1} -function Base.rand(::Type{T}, size::Integer) where {T<:Kmer} - return [rand(T) for _ in 1:size] -end +Returns a new kmer with the first symbol of the input `kmer` removed. +Throws an `ArgumentError` if `kmer` is empty. -### -### Old Mer Base Functions - not transferred to new type. -### -#@inline encoded_data_type(::Type{Mer{A,K}}) where {A,K} = UInt64 -#@inline encoded_data_type(::Type{BigMer{A,K}}) where {A,K} = UInt128 -#@inline encoded_data_type(x::AbstractMer) = encoded_data_type(typeof(x)) -#@inline encoded_data(x::AbstractMer) = reinterpret(encoded_data_type(typeof(x)), x) -#@inline ksize(::Type{T}) where {A,K,T<:AbstractMer{A,K}} = K -#@inline Base.unsigned(x::AbstractMer) = encoded_data(x) -#Base.:-(x::AbstractMer, y::Integer) = typeof(x)(encoded_data(x) - y % encoded_data_type(x)) -#Base.:+(x::AbstractMer, y::Integer) = typeof(x)(encoded_data(x) + y % encoded_data_type(x)) -#Base.:+(x::AbstractMer, y::AbstractMer) = y + x -#Alphabet(::Type{Mer{A,K} where A<:NucleicAcidAlphabet{2}}) where {K} = Any - -include("indexing.jl") - -#LongSequence{A}(x::Kmer{A,K,N}) where {A,K,N} = LongSequence{A}([nt for nt in x]) -# Convenience method so as don't need to specify A in LongSequence{A}. -BioSequences.LongSequence(x::Kmer{A,K,N}) where {A,K,N} = LongSequence{A}(x) - -include("predicates.jl") -include("counting.jl") -include("transformations.jl") - -### -### Kmer de-bruijn neighbors -### - -# TODO: Decide on this vs. old iterator pattern. I like the terseness of the code vs defining an iterator. Neither should allocate. -fw_neighbors(kmer::Kmer{A,K,N}) where {A<:DNAAlphabet,K,N} = ntuple(i -> pushlast(kmer, ACGT[i]), Val{4}()) -fw_neighbors(kmer::Kmer{A,K,N}) where {A<:RNAAlphabet,K,N} = ntuple(i -> pushlast(kmer, ACGU[i]), Val{4}()) -bw_neighbors(kmer::Kmer{A,K,N}) where {A<:DNAAlphabet,K,N} = ntuple(i -> pushfirst(kmer, ACGT[i]), Val{4}()) -bw_neighbors(kmer::Kmer{A,K,N}) where {A<:RNAAlphabet,K,N} = ntuple(i -> pushfirst(kmer, ACGU[i]), Val{4}()) - -#= -# Neighbors on a de Bruijn graph -struct KmerNeighborIterator{S<:Kmer} - x::S -end +!!! warn + Since the output of this function is a `K-1`-mer, use of this function + in a loop may result in type-instability. -""" - neighbors(kmer::S) where {S<:Kmer} +See also: [`pop`](@ref), [`push`](@ref), [`shift`](@ref) -Return an iterator through skip-mers neighboring `skipmer` on a de Bruijn graph. -""" -neighbors(kmer::Kmer) = KmerNeighborIterator{typeof(kmer)}(kmer) +# Examples +```jldoctest +julia> pop_first(mer"TCTGTA"d) +DNA 5-mer: +CTGTA -Base.length(::KmerNeighborIterator) = 4 -Base.eltype(::Type{KmerNeighborIterator{S}}) where {S<:Kmer} = S +julia> pop_first(mer"QPSY"a) +AminoAcid 3-mer: +PSY -function Base.iterate(it::KmerNeighborIterator{S}, i::UInt64 = 0) where {S<:Kmer} - if i == 4 - return nothing - else - #return S((encoded_data(it.x) << 2) | i), i + 1 - return it.x << 1, i + one(UInt64) - end -end -=# - -### -### String literals -### - -macro mer_str(seq, flag) - seq′ = BioSequences.remove_newlines(seq) - if flag == "dna" || flag == "d" - T = kmertype(DNAKmer{length(seq′)}) - return T(seq′) - elseif flag == "rna" || flag == "r" - T = kmertype(RNAKmer{length(seq′)}) - return T(seq′) - elseif flag == "aa" || flag == "a" || flag == "prot" || flag == "p" - T = kmertype(AAKmer{length(seq′)}) - return T(seq′) +julia> pop_first(mer""a) +ERROR: ArgumentError: +[...] +``` +""" +function pop_first(kmer::Kmer{A}) where {A} + isempty(kmer) && throw(ArgumentError("Cannot pop 0-mer")) + data = if elements_in_head(typeof(kmer)) == 1 + tail(kmer.data) else - error("Invalid type flag: '$(flag)'") + bps = BioSequences.bits_per_symbol(A()) + bits_used = 8 * sizeof(UInt) - (bits_unused(typeof(kmer)) + bps) + mask = left_shift(UInt(1), bits_used) - UInt(1) + (first(kmer.data) & mask, tail(kmer.data)...) end + newT = derive_type(Kmer{A, length(kmer) - 1}) + newT(unsafe, data) end -macro mer_str(seq) - seq′ = BioSequences.remove_newlines(seq) - T = kmertype(DNAKmer{length(seq′)}) - return T(seq′) +# Get a mask 0x0001111 ... masking away the unused bits of the head element +# in the UInt tuple +@inline function get_mask(T::Type{<:Kmer}) + UInt(1) << (8 * sizeof(UInt) - bits_unused(T)) - 1 end - -include("revtrans.jl") \ No newline at end of file diff --git a/src/kmer_iteration/AbstractKmerIterator.jl b/src/kmer_iteration/AbstractKmerIterator.jl deleted file mode 100644 index f320476..0000000 --- a/src/kmer_iteration/AbstractKmerIterator.jl +++ /dev/null @@ -1,36 +0,0 @@ -### -### Kmer Iteration -### -### Abstract Kmer Iterator type. -### -### This file is a part of BioJulia. -### License is MIT: https://github.com/BioJulia/BioSequences.jl/blob/master/LICENSE.md - -### Type for storing the result of Kmer iteration. - -abstract type AbstractKmerIterator{T<:Kmer,S<:BioSequence} end - -@inline Base.eltype(::Type{<:AbstractKmerIterator{T,S}}) where {T,S} = Tuple{UInt64,T} - -@inline Base.IteratorSize(::Type{<:AbstractKmerIterator{Kmer{A,K,N},S}}) where {A,S<:BioSequence{A},K,N} = Base.HasLength() -@inline Base.IteratorSize(::Type{<:AbstractKmerIterator{Kmer{A,K,N},S}}) where {A,B,S<:BioSequence{B},K,N} = Base.SizeUnknown() - -@inline function Base.length(it::AbstractKmerIterator{Kmer{A,K,N},S}) where {A,K,N,S<:BioSequence{A}} - return max(0, fld(it.stop - it.start + 1 - K, step(it)) + 1) -end - -# Iteration where the Kmer and Seq alphabets match: - -## Initial iteration without state. -@inline function Base.iterate(it::AbstractKmerIterator{Kmer{A,K,N},LongSequence{A}}) where {A,K,N} - fwkmer = _build_kmer_data(Kmer{A,K,N}, it.seq, 1) - if isnothing(fwkmer) - return nothing - else - # Get the reverse. - alph = Alphabet(Kmer{A,K,N}) - rshift = n_unused(Kmer{A,K,N}) * BioSequences.bits_per_symbol(alph) # Based on alphabet type, should constant fold. - rvkmer = rightshift_carry(_reverse(BioSequences.BitsPerSymbol(alph), _complement_bitpar(alph, fwkmer...)...), rshift) - return KmerAt{Kmer{A,K,N}}(1, Kmer{A,K,N}(fwkmer), Kmer{A,K,N}(rvkmer)), (K, fwkmer, rvkmer) - end -end \ No newline at end of file diff --git a/src/kmer_iteration/EveryCanonicalKmer.jl b/src/kmer_iteration/EveryCanonicalKmer.jl deleted file mode 100644 index 21e883b..0000000 --- a/src/kmer_iteration/EveryCanonicalKmer.jl +++ /dev/null @@ -1,122 +0,0 @@ -""" - EveryCanonicalKmer{T,S}(seq::S, start::Int = firstindex(seq), stop::Int = lastindex(seq)) where {T<:Kmer,S<:BioSequence} - -An iterator over every canonical valid overlapping `T<:Kmer` in a given longer -`BioSequence`, between a `start` and `stop` position. - -!!! note - Typically, the alphabet of the Kmer type matches the alphabet of the input - BioSequence. In these cases, the iterator will have `Base.IteratorSize` of - `Base.HasLength`, and successive kmers produced by the iterator will overlap - by K - 1 bases. - - However, in the specific case of iterating over kmers in a DNA or RNA sequence, you - may iterate over a Kmers where the alphabet is a NucleicAcidAlphabet{2}, but - the input BioSequence has a NucleicAcidAlphabet{4}. - - In this case then the iterator will skip over positions in the BioSequence - with characters that are not supported by the Kmer type's NucleicAcidAlphabet{2}. - - As a result, the overlap between successive kmers may not reliably be K - 1, - and the iterator will have `Base.IteratorSize` of `Base.SizeUnknown`. -""" -struct EveryCanonicalKmer{T<:Kmer,S<:BioSequence{<:NucleicAcidAlphabet}} <: AbstractKmerIterator{T,S} - seq::S - start::Int - stop::Int - - function EveryCanonicalKmer{T,S}(seq::S, start::Int = firstindex(seq), stop::Int = lastindex(seq)) where {T<:Kmer,S<:BioSequence} - T′ = kmertype(T) - checkmer(T′) # Should inline and constant fold. - return new{T′,S}(seq, start, stop) - end -end - -""" - EveryCanonicalKmer{T}(seq::S, start = firstindex(seq), stop = lastindex(seq)) where {T<:Kmer,S<:BioSequence} - -Convenience outer constructor so you don't have to specify `S` along with `T`. - -E.g. Instead of `EveryCanonicalKmer{DNACodon,typeof(s)}(s)`, you can just use `EveryCanonicalKmer{DNACodon}(s)` -""" -function EveryCanonicalKmer{T}(seq::S, start = firstindex(seq), stop = lastindex(seq)) where {T<:Kmer,S<:BioSequence} - return EveryCanonicalKmer{T,S}(seq, start, stop) -end - -""" - EveryCanonicalKmer(seq::BioSequence{A}, ::Val{K}, start = firstindex(seq), stop = lastindex(seq)) where {A,K} - -Convenience outer constructor so yyou don't have to specify full `Kmer` typing. - -In order to deduce `Kmer{A,K,N}`, `A` is taken from the input `seq` type, `K` is -taken from `::Val{K}`, and `N` is deduced using `A` and `K`. - -E.g. Instead of `EveryCanonicalKmer{DNAKmer{3,1}}(s)`, or `EveryCanonicalKmer{DNACodon}(s)`, -you can use `EveryCanonicalKmer(s, Val(3))` -""" -function EveryCanonicalKmer(seq::BioSequence{A}, ::Val{K}, start = firstindex(seq), stop = lastindex(seq)) where {A,K} - return EveryCanonicalKmer{Kmer{A,K}}(seq, start, stop) -end - -Base.step(x::EveryCanonicalKmer) = 1 - - - -## Initial iteration without state. -@inline function Base.iterate(it::EveryCanonicalKmer{Kmer{A,K,N},LongSequence{A}}) where {A,K,N} - fwkmer = _build_kmer_data(Kmer{A,K,N}, it.seq, it.start) - if isnothing(fwkmer) - return nothing - else - rshift = n_unused(Kmer{A,K,N}) * BioSequences.bits_per_symbol(A()) # Based on alphabet type, should constant fold. - rvkmer = rightshift_carry(_reverse(BioSequences.BitsPerSymbol(A()), _complement_bitpar(A(), fwkmer...)...), rshift) - return (it.start, Kmer{A,K,N}(min(fwkmer, rvkmer))), (it.start + K - 1, fwkmer, rvkmer) - end -end - -@inline function Base.iterate(it::EveryCanonicalKmer{Kmer{A,K,N},LongSequence{A}}, state) where {A,K,N} - i, fwkmer, rvkmer = state - i += 1 - if i > it.stop - return nothing - else - bps = BioSequences.bits_per_symbol(A()) # Based on type info, should constant fold. - rshift = (64 - (n_unused(Kmer{A,K,N}) + 1) * bps) # Based on type info, should constant fold. - mask = (one(UInt64) << bps) - one(UInt64) # Based on type info, should constant fold. - - fbits = UInt64(BioSequences.extract_encoded_element(it.seq, i)) - rbits = (BioSequences.complement_bitpar(fbits, A()) & mask) << rshift - fwkmer = leftshift_carry(fwkmer, bps, fbits) - rvkmer = rightshift_carry(rvkmer, bps, rbits) - pos = i - K + 1 - return (pos, min(Kmer{A,K,N}(fwkmer), Kmer{A,K,N}(rvkmer))), (i, fwkmer, rvkmer) - end -end - -@inline Base.IteratorSize(::Type{<:EveryCanonicalKmer{Kmer{A,N,K},LongSequence{B}}}) where {A<:NucleicAcidAlphabet{2},N,K,B<:NucleicAcidAlphabet{4}} = Base.SizeUnknown() - -@inline function Base.iterate(it::EveryCanonicalKmer{Kmer{A,K,N},LongSequence{B}}, - state = (it.start - 1, 1, blank_ntuple(Kmer{A,K,N}), blank_ntuple(Kmer{A,K,N})) - ) where {A<:NucleicAcidAlphabet{2},B<:NucleicAcidAlphabet{4},K,N} - - i, filled, fwkmer, rvkmer = state - i += 1 - filled -= 1 - - rshift = (64 - (n_unused(Kmer{A,K,N}) + 1) * 2) # Based on type info, should constant fold. - mask = (one(UInt64) << 2) - one(UInt64) # Based on type info, should constant fold. - - while i ≤ it.stop - @inbounds nt = reinterpret(UInt8, it.seq[i]) - @inbounds fbits = kmerbits[nt + 1] - rbits = (BioSequences.complement_bitpar(fbits, A()) & mask) << rshift - fwkmer = leftshift_carry(fwkmer, 2, fbits) - rvkmer = rightshift_carry(rvkmer, 2, rbits) - filled = ifelse(fbits == UInt64(0xff), 0, filled + 1) - if filled == K - return (i - K + 1, min(Kmer{A,K,N}(fwkmer), Kmer{A,K,N}(rvkmer))), (i, filled, fwkmer, rvkmer) - end - i += 1 - end - return nothing -end \ No newline at end of file diff --git a/src/kmer_iteration/EveryKmer.jl b/src/kmer_iteration/EveryKmer.jl deleted file mode 100644 index 7885afb..0000000 --- a/src/kmer_iteration/EveryKmer.jl +++ /dev/null @@ -1,124 +0,0 @@ -### -### Kmer Iteration -### -### Iterator type over every kmer in a sequence - overlapping. -### -### This file is a part of BioJulia. -### License is MIT: https://github.com/BioJulia/BioSequences.jl/blob/master/LICENSE.md - -""" - EveryKmer{T,S}(seq::S, start::Int = firstindex(seq), stop::Int = lastindex(seq)) where {T<:Kmer,S<:BioSequence} - -An iterator over every valid overlapping `T<:Kmer` in a given longer -`BioSequence` between a `start` and `stop` position. - -!!! note - Typically, the alphabet of the Kmer type matches the alphabet of the input - BioSequence. In these cases, the iterator will have `Base.IteratorSize` of - `Base.HasLength`, and successive kmers produced by the iterator will overlap - by K - 1 bases. - - However, in the specific case of iterating over kmers in a DNA or RNA sequence, you - may iterate over a Kmers where the alphabet is a NucleicAcidAlphabet{2}, but - the input BioSequence has a NucleicAcidAlphabet{4}. - - In this case then the iterator will skip over positions in the BioSequence - with characters that are not supported by the Kmer type's NucleicAcidAlphabet{2}. - - As a result, the overlap between successive kmers may not reliably be K - 1, - and the iterator will have `Base.IteratorSize` of `Base.SizeUnknown`. -""" -struct EveryKmer{T<:Kmer,S<:BioSequence} <: AbstractKmerIterator{T,S} - seq::S - start::Int - stop::Int - - function EveryKmer{T,S}(seq::S, start::Int = firstindex(seq), stop::Int = lastindex(seq)) where {T<:Kmer,S<:BioSequence} - T′ = kmertype(T) - checkmer(T′) # Should inline and constant fold. - return new{T′,S}(seq, start, stop) - end -end - -""" - EveryKmer{T}(seq::S, start = firstindex(seq), stop = lastindex(seq)) where {T<:Kmer,S<:BioSequence} - -Convenience outer constructor so you don't have to specify `S` along with `T`. - -E.g. Instead of `EveryKmer{DNACodon,typeof(s)}(s)`, you can just use `EveryKmer{DNACodon}(s)` -""" -function EveryKmer{T}(seq::S, start = firstindex(seq), stop = lastindex(seq)) where {T<:Kmer,S<:BioSequence} - return EveryKmer{T,S}(seq, start, stop) -end - -""" - EveryKmer(seq::BioSequence{A}, ::Val{K}, start = firstindex(seq), stop = lastindex(seq)) where {A,K} - -Convenience outer constructor so yyou don't have to specify full `Kmer` typing. - -In order to deduce `Kmer{A,K,N}`, `A` is taken from the input `seq` type, `K` is -taken from `::Val{K}`, and `N` is deduced using `A` and `K`. - -E.g. Instead of `EveryKmer{DNAKmer{3,1}}(s)`, or `EveryKmer{DNACodon}(s)`, -you can use `EveryKmer(s, Val(3))` -""" -function EveryKmer(seq::BioSequence{A}, ::Val{K}, start = firstindex(seq), stop = lastindex(seq)) where {A,K} - return EveryKmer{Kmer{A,K}}(seq, start, stop) -end - -Base.step(x::EveryKmer) = 1 - -## Initial iteration without state. -@inline function Base.iterate(it::EveryKmer{Kmer{A,K,N},LongSequence{A}}) where {A,K,N} - kmer = _build_kmer_data(Kmer{A,K,N}, it.seq, 1) - if isnothing(kmer) - return nothing - else - return (1, Kmer{A,K,N}(kmer)), (K, kmer) - end -end - -@inline function Base.iterate(it::EveryKmer{Kmer{A,K,N},LongSequence{A}}, state) where {A,K,N} - i, fwkmer = state - i += 1 - if i > it.stop - return nothing - else - bps = BioSequences.bits_per_symbol(A()) # Based on type info, should constant fold. - bits = UInt64(BioSequences.extract_encoded_element(it.seq, i)) - kmer = leftshift_carry(fwkmer, bps, bits) - pos = i - K + 1 - return (pos, Kmer{A,K,N}(kmer)), (i, kmer) - end -end - -## Special case where iterating over 2-Bit encoded kmers in a 4-Bit encoded sequence, -## behaviour is to produce kmers by skipping over the ambiguous sites. - -const kmerbits = (UInt64(0xff), UInt64(0x00), UInt64(0x01), UInt64(0xff), - UInt64(0x02), UInt64(0xff), UInt64(0xff), UInt64(0xff), - UInt64(0x03), UInt64(0xff), UInt64(0xff), UInt64(0xff), - UInt64(0xff), UInt64(0xff), UInt64(0xff), UInt64(0xff)) - -@inline Base.IteratorSize(::Type{<:EveryKmer{Kmer{A,N,K},S}}) where {A<:NucleicAcidAlphabet{2},N,K,B<:NucleicAcidAlphabet{4},S<:BioSequence{B}} = Base.SizeUnknown() - -@inline function Base.iterate(it::EveryKmer{Kmer{A,K,N},S}, - state = (it.start - 1, 1, blank_ntuple(Kmer{A,K,N})) - ) where {A<:NucleicAcidAlphabet{2},B<:NucleicAcidAlphabet{4},S<:BioSequence{B},K,N} - - i, filled, fwkmer = state - i += 1 - filled -= 1 - - while i ≤ it.stop - @inbounds nt = reinterpret(UInt8, it.seq[i]) - @inbounds fbits = kmerbits[nt + 1] - fwkmer = leftshift_carry(fwkmer, 2, fbits) - filled = ifelse(fbits == UInt64(0xff), 0, filled + 1) - if filled == K - return (i - K + 1, Kmer{A,K,N}(fwkmer)), (i, filled, fwkmer) - end - i += 1 - end - return nothing -end \ No newline at end of file diff --git a/src/kmer_iteration/SpacedCanonicalKmers.jl b/src/kmer_iteration/SpacedCanonicalKmers.jl deleted file mode 100644 index c650cf9..0000000 --- a/src/kmer_iteration/SpacedCanonicalKmers.jl +++ /dev/null @@ -1,133 +0,0 @@ - -""" - SpacedCanonicalKmers{T,S}(seq::S, step::Int, start::Int, stop::Int) where {T<:Kmer,S<:BioSequence} - -An iterator over every valid `T<:Kmer` separated by a `step` parameter, in a given -longer `BioSequence`, between a `start` and `stop` position. - -!!! note - Typically, the alphabet of the Kmer type matches the alphabet of the input - BioSequence. In these cases, the iterator will have `Base.IteratorSize` of - `Base.HasLength`, and successive kmers produced by the iterator will overlap - by `max(0, K - step)` bases. - - However, in the specific case of iterating over kmers in a DNA or RNA sequence, you - may iterate over a Kmers where the alphabet is a NucleicAcidAlphabet{2}, but - the input BioSequence has a NucleicAcidAlphabet{4}. - - In this case then the iterator will skip over positions in the BioSequence - with characters that are not supported by the Kmer type's NucleicAcidAlphabet{2}. - - As a result, the overlap between successive kmers may not consistent, but the - reading frame will be preserved. - In addition, the iterator will have `Base.IteratorSize` of `Base.SizeUnknown`. -""" -struct SpacedCanonicalKmers{T<:Kmer,S<:BioSequence} <: AbstractKmerIterator{T,S} - seq::S - start::Int - step::Int - stop::Int - filled::Int # This is cached for speed - increment::Int # This is cached for speed - - function SpacedCanonicalKmers{T,S}(seq::S, step::Int, start::Int, stop::Int) where {T<:Kmer,S<:BioSequence} - T′ = kmertype(T) - checkmer(T′) # Should inline and constant fold. - if step <= 1 - throw(ArgumentError("step size must be greater than 1")) - end - filled = max(0, ksize(T′) - step) - increment = max(1, step - ksize(T′) + 1) - return new{T′,S}(seq, start, step, stop, filled, increment) - end -end - -""" - SpacedCanonicalKmers{T}(seq::S, start = firstindex(seq), stop = lastindex(seq)) where {T<:Kmer,S<:BioSequence} - -Convenience outer constructor so you don't have to specify `S` along with `T`. - -E.g. Instead of `SpacedCanonicalKmers{DNACodon,typeof(s)}(s, 3)`, you can just use `SpacedCanonicalKmers{DNACodon}(s, 3)` -""" -function SpacedCanonicalKmers{T}(seq::S, step::Int, start = firstindex(seq), stop = lastindex(seq)) where {T<:Kmer,S<:BioSequence} - return SpacedCanonicalKmers{T,S}(seq, step, start, stop) -end - -""" - SpacedCanonicalKmers(seq::BioSequence{A}, ::Val{K}, step::Int, start = firstindex(seq), stop = lastindex(seq)) where {A,K} - -Convenience outer constructor so yyou don't have to specify full `Kmer` typing. - -In order to deduce `Kmer{A,K,N}`, `A` is taken from the input `seq` type, `K` is -taken from `::Val{K}`, and `N` is deduced using `A` and `K`. - -E.g. Instead of `SpacedCanonicalKmers{DNAKmer{3,1}}(s, 3)`, or `SpacedCanonicalKmers{DNACodon}(s, 3)`, -you can use `SpacedCanonicalKmers(s, Val(3), 3)` -""" -function SpacedCanonicalKmers(seq::BioSequence{A}, ::Val{K}, step::Int, start = firstindex(seq), stop = lastindex(seq)) where {A,K} - return SpacedCanonicalKmers{Kmer{A,K}}(seq, step, start, stop) -end - -Base.step(x::SpacedCanonicalKmers) = x.step - -@inline function Base.iterate(it::SpacedCanonicalKmers{Kmer{A,K,N},LongSequence{A}}) where {A,K,N} - fwkmer = _build_kmer_data(Kmer{A,K,N}, it.seq, 1) - if isnothing(fwkmer) - return nothing - else - rshift = n_unused(Kmer{A,K,N}) * BioSequences.bits_per_symbol(A()) # Based on alphabet type, should constant fold. - rvkmer = rightshift_carry(_reverse(BioSequences.BitsPerSymbol(A()), _complement_bitpar(A(), fwkmer...)...), rshift) - return (1, min(Kmer{A,K,N}(fwkmer), Kmer{A,K,N}(rvkmer))), (K, fwkmer, rvkmer) - end -end - -@inline function Base.iterate(it::SpacedCanonicalKmers{Kmer{A,K,N},LongSequence{A}}, state) where {A,K,N} - i, fwkmer, rvkmer = state - filled = it.filled - i += it.increment - - for _ in filled:K-1 - if i > it.stop - return nothing - else - bps = BioSequences.bits_per_symbol(A()) # Based on type info, should constant fold. - rshift = (64 - (n_unused(Kmer{A,K,N}) + 1) * bps) # Based on type info, should constant fold. - mask = (one(UInt64) << bps) - one(UInt64) # Based on type info, should constant fold. - fbits = UInt64(BioSequences.extract_encoded_element(it.seq, i)) - rbits = (BioSequences.complement_bitpar(fbits, A()) & mask) << rshift - fwkmer = leftshift_carry(fwkmer, bps, fbits) - rvkmer = rightshift_carry(rvkmer, bps, rbits) - i += 1 - end - end - pos = i - K + 1 - return (pos, min(Kmer{A,K,N}(fwkmer), Kmer{A,K,N}(rvkmer))), (i, fwkmer, rvkmer) -end - -@inline function Base.iterate(it::SpacedCanonicalKmers{Kmer{A,K,N},LongSequence{B}}, state = (it.start - it.increment, 1, 0, blank_ntuple(Kmer{A,K,N}), blank_ntuple(Kmer{A,K,N})) - ) where {A<:NucleicAcidAlphabet{2},B<:NucleicAcidAlphabet{4},K,N} - i, pos, filled, fwkmer, rvkmer = state - i += it.increment - - while i ≤ it.stop - nt = reinterpret(UInt8, @inbounds getindex(it.seq, i)) - @inbounds fbits = UInt64(kmerbits[nt + 1]) - rbits = ~fbits & typeof(fbits)(0x03) - if fbits == 0xff # ambiguous - filled = 0 - # Find the beginning of next possible kmer after i - pos = i + it.step - Core.Intrinsics.urem_int(i - pos, it.step) - i = pos - 1 - else - filled += 1 - fwkmer = leftshift_carry(fwkmer, 2, fbits) - rvkmer = rightshift_carry(rvkmer, 2, UInt64(rbits) << (62 - (64N - 2K))) - end - if filled == K - state = (i, i - K + 1 + it.step, it.filled, fwkmer, rvkmer) - return (pos, min(Kmer{A,K,N}(fwkmer), Kmer{A,K,N}(rvkmer))), state - end - i += 1 - end - return nothing -end \ No newline at end of file diff --git a/src/kmer_iteration/SpacedKmers.jl b/src/kmer_iteration/SpacedKmers.jl deleted file mode 100644 index 18461ee..0000000 --- a/src/kmer_iteration/SpacedKmers.jl +++ /dev/null @@ -1,127 +0,0 @@ - -""" - SpacedKmers{T,S}(seq::S, step::Int, start::Int, stop::Int) where {T<:Kmer,S<:BioSequence} - -An iterator over every valid `T<:Kmer` separated by a `step` parameter, in a given -longer `BioSequence`, between a `start` and `stop` position. - -!!! note - Typically, the alphabet of the Kmer type matches the alphabet of the input - BioSequence. In these cases, the iterator will have `Base.IteratorSize` of - `Base.HasLength`, and successive kmers produced by the iterator will overlap - by `max(0, K - step)` bases. - - However, in the specific case of iterating over kmers in a DNA or RNA sequence, you - may iterate over a Kmers where the alphabet is a NucleicAcidAlphabet{2}, but - the input BioSequence has a NucleicAcidAlphabet{4}. - - In this case then the iterator will skip over positions in the BioSequence - with characters that are not supported by the Kmer type's NucleicAcidAlphabet{2}. - - As a result, the overlap between successive kmers may not consistent, but the - reading frame will be preserved. - In addition, the iterator will have `Base.IteratorSize` of `Base.SizeUnknown`. -""" -struct SpacedKmers{T<:Kmer,S<:BioSequence} <: AbstractKmerIterator{T,S} - seq::S - start::Int - step::Int - stop::Int - filled::Int # This is cached for speed - increment::Int # This is cached for speed - - function SpacedKmers{T,S}(seq::S, step::Int, start::Int, stop::Int) where {T<:Kmer,S<:BioSequence} - T′ = kmertype(T) - checkmer(T′) # Should inline and constant fold. - if step <= 1 - throw(ArgumentError("step size must be greater than 1")) - end - filled = max(0, ksize(T′) - step) - increment = max(1, step - ksize(T′) + 1) - return new{T′,S}(seq, start, step, stop, filled, increment) - end -end - -""" - SpacedKmers{T}(seq::S, start = firstindex(seq), stop = lastindex(seq)) where {T<:Kmer,S<:BioSequence} - -Convenience outer constructor so you don't have to specify `S` along with `T`. - -E.g. Instead of `SpacedKmers{DNACodon,typeof(s)}(s, 3)`, you can just use `SpacedKmers{DNACodon}(s, 3)` -""" -function SpacedKmers{T}(seq::S, step::Int, start = firstindex(seq), stop = lastindex(seq)) where {T<:Kmer,S<:BioSequence} - return SpacedKmers{T,S}(seq, step, start, stop) -end - -""" - SpacedKmers(seq::BioSequence{A}, ::Val{K}, step::Int, start = firstindex(seq), stop = lastindex(seq)) where {A,K} - -Convenience outer constructor so yyou don't have to specify full `Kmer` typing. - -In order to deduce `Kmer{A,K,N}`, `A` is taken from the input `seq` type, `K` is -taken from `::Val{K}`, and `N` is deduced using `A` and `K`. - -E.g. Instead of `SpacedKmers{DNAKmer{3,1}}(s, 3)`, or `SpacedKmers{DNACodon}(s, 3)`, -you can use `SpacedKmers(s, Val(3), 3)` -""" -function SpacedKmers(seq::BioSequence{A}, ::Val{K}, step::Int, start = firstindex(seq), stop = lastindex(seq)) where {A,K} - return SpacedKmers{Kmer{A,K}}(seq, step, start, stop) -end - -Base.step(x::SpacedKmers) = x.step - -@inline function Base.iterate(it::SpacedKmers{Kmer{A,K,N},LongSequence{A}}) where {A,K,N} - kmer = _build_kmer_data(Kmer{A,K,N}, it.seq, 1) - if isnothing(kmer) - return nothing - else - # Get the reverse. - alph = Alphabet(Kmer{A,K,N}) - return (1, Kmer{A,K,N}(kmer)), (K, kmer) - end -end - -@inline function Base.iterate(it::SpacedKmers{Kmer{A,K,N},LongSequence{A}}, state) where {A,K,N} - i, kmer = state - filled = it.filled - i += it.increment - - for _ in filled:K-1 - if i > it.stop - return nothing - else - bps = BioSequences.bits_per_symbol(A()) # Based on type info, should constant fold. - bits = UInt64(BioSequences.extract_encoded_element(it.seq, i)) - kmer = leftshift_carry(kmer, bps, bits) - i += 1 - end - end - pos = i - K + 1 - return (pos, Kmer{A,K,N}(kmer)), (i, kmer) -end - -@inline function Base.iterate(it::SpacedKmers{Kmer{A,K,N},LongSequence{B}}, state = (it.start - it.increment, 1, 0, blank_ntuple(Kmer{A,K,N})) - ) where {A<:NucleicAcidAlphabet{2},B<:NucleicAcidAlphabet{4},K,N} - i, pos, filled, kmer = state - i += it.increment - - while i ≤ it.stop - nt = reinterpret(UInt8, @inbounds getindex(it.seq, i)) - @inbounds bits = UInt64(kmerbits[nt + 1]) - if bits == 0xff # ambiguous - filled = 0 - # Find the beginning of next possible kmer after i - pos = i + it.step - Core.Intrinsics.urem_int(i - pos, it.step) - i = pos - 1 - else - filled += 1 - kmer = leftshift_carry(kmer, 2, bits) - end - if filled == K - state = (i, i - K + 1 + it.step, it.filled, kmer) - return (pos, Kmer{A,K,N}(kmer)), state - end - i += 1 - end - return nothing -end diff --git a/src/predicates.jl b/src/predicates.jl deleted file mode 100644 index 86258e6..0000000 --- a/src/predicates.jl +++ /dev/null @@ -1,11 +0,0 @@ -### -### Mer specific specializations of src/biosequence/predicates.jl -### - -Base.cmp(x::T, y::T) where {T<:Kmer} = cmp(x.data, y.data) -Base.:(==)(x::T, y::T) where {T<:Kmer} = x.data == y.data -Base.isless(x::T, y::T) where {T<:Kmer} = isless(x.data, y.data) - -# TODO: Ensure this is the right way to go. -# See https://github.com/BioJulia/BioSequences.jl/pull/121#discussion_r475234270 -Base.hash(x::Kmer{A,K,N}, h::UInt) where {A,K,N} = hash(x.data, h ⊻ K) \ No newline at end of file diff --git a/src/revtrans.jl b/src/revtrans.jl index e4ebedf..dc8fb1d 100644 --- a/src/revtrans.jl +++ b/src/revtrans.jl @@ -18,14 +18,12 @@ julia> Set(CodonSet(v)) == Set(v) true julia> union(CodonSet(v), CodonSet([mer"GAG"r])) -Kmers.CodonSet with 4 elements: +CodonSet with 4 elements: GAG GGA UAG UUU ``` - -See also: `push` """ struct CodonSet <: AbstractSet{RNACodon} x::UInt64 @@ -33,11 +31,11 @@ struct CodonSet <: AbstractSet{RNACodon} CodonSet(x::UInt64, ::Unsafe) = new(x) end CodonSet() = CodonSet(UInt64(0), Unsafe()) -CodonSet(itr) = foldl(push, itr, init=CodonSet()) +CodonSet(itr) = foldl(push, itr; init=CodonSet()) function Base.iterate(x::CodonSet, s::UInt64=x.x) - codon = RNACodon((trailing_zeros(s) % UInt64,)) - iszero(s) ? nothing : (codon, s & (s-1)) + codon = RNACodon(unsafe, (trailing_zeros(s) % UInt64,)) + iszero(s) ? nothing : (codon, s & (s - 1)) end function push(s::CodonSet, x::RNACodon) @@ -52,8 +50,8 @@ Base.filter(f, s::CodonSet) = CodonSet(Iterators.filter(f, s)) Base.setdiff(a::CodonSet, b::Vararg{CodonSet}) = CodonSet(a.x & ~(union(b...).x), Unsafe()) for (name, f) in [(:union, |), (:intersect, &), (:symdiff, ⊻)] - @eval function Base.$(name)(a::CodonSet, b::Vararg{CodonSet}) - CodonSet(mapreduce(i -> i.x, $f, b, init=a.x), Unsafe()) + @eval function Base.$(name)(a::CodonSet, b::Vararg{CodonSet}) + CodonSet(mapreduce(i -> i.x, $f, b; init=a.x), Unsafe()) end end @@ -76,7 +74,7 @@ inverse of the mapping through `GeneticCode` julia> code = ReverseGeneticCode(BioSequences.candidate_division_sr1_genetic_code); julia> code[AA_E] -Kmers.CodonSet with 2 elements: +CodonSet with 2 elements: GAA GAG @@ -89,17 +87,17 @@ See also: [`reverse_translate`](@ref) """ struct ReverseGeneticCode <: AbstractDict{AminoAcid, CodonSet} name::String - sets::NTuple{N_AA-1, CodonSet} + sets::NTuple{N_AA - 1, CodonSet} end function ReverseGeneticCode(x::BioSequences.GeneticCode) ind(aa::AminoAcid) = reinterpret(UInt8, aa) + 1 - sets = fill(CodonSet(), N_AA-1) + sets = fill(CodonSet(), N_AA - 1) x_set = CodonSet() for i in Int64(0):Int64(63) aa = x.tbl[i + 1] - codon = RNACodon((i % UInt64,)) + codon = RNACodon(unsafe, (i % UInt64,)) sets[ind(aa)] = push(sets[ind(aa)], codon) if aa !== AA_Term x_set = push(x_set, codon) @@ -122,9 +120,7 @@ function ReverseGeneticCode(x::BioSequences.GeneticCode) ReverseGeneticCode(x.name, Tuple(sets)) end -const rev_standard_genetic_code = ReverseGeneticCode( - BioSequences.standard_genetic_code -) +const rev_standard_genetic_code = ReverseGeneticCode(BioSequences.standard_genetic_code) function Base.getindex(s::ReverseGeneticCode, a::AminoAcid) if reinterpret(UInt8, a) > (N_AA - 2) # cannot translate gap @@ -136,21 +132,29 @@ end Base.length(c::ReverseGeneticCode) = length(c.sets) function Base.iterate(c::ReverseGeneticCode, s=1) s > length(c.sets) && return nothing - return (reinterpret(AminoAcid, (s-1)%UInt8) => c.sets[s], s+1) + return (reinterpret(AminoAcid, (s - 1) % UInt8) => c.sets[s], s + 1) end """ - reverse_translate!(v::Vector{CodonSet}, s::AASeq code=rev_standard_genetic_code) + reverse_translate!(v::Vector{CodonSet}, s::AASeq, code=rev_standard_genetic_code) -> v Reverse-translates `s` under the reverse genetic code `code`, putting the result in `v`. See also: [`reverse_translate`](@ref) + +# Examples: +```jldoctest +julia> v = CodonSet[]; + +julia> reverse_translate!(v, aa"KWCL") +4-element Vector{CodonSet}: + CodonSet(0x0000000000000005) + CodonSet(0x0400000000000000) + CodonSet(0x0a00000000000000) + CodonSet(0x50000000f0000000) +``` """ -function reverse_translate!( - v::Vector{CodonSet}, - seq::AASeq, - code=rev_standard_genetic_code -) +function reverse_translate!(v::Vector{CodonSet}, seq::AASeq, code=rev_standard_genetic_code) resize!(v, length(seq)) @inbounds for i in eachindex(v) v[i] = code[seq[i]] @@ -168,21 +172,20 @@ If `s` is an `AASeq`, return `Vector{CodonSet}`. # Examples ```jldoctest julia> reverse_translate(AA_W) -Kmers.CodonSet with 1 element: +CodonSet with 1 element: UGG julia> v = reverse_translate(aa"MMLVQ"); julia> typeof(v) -Vector{Kmers.CodonSet} +Vector{CodonSet} (alias for Array{CodonSet, 1}) julia> v[4] -Kmers.CodonSet with 4 elements: +CodonSet with 4 elements: GUA GUC GUG GUU -[...] ``` See also: [`reverse_translate!`](@ref), [`ReverseGeneticCode`](@ref) diff --git a/src/transformations.jl b/src/transformations.jl index 6aa3456..53a3e3c 100644 --- a/src/transformations.jl +++ b/src/transformations.jl @@ -1,221 +1,103 @@ - -# Bit-parallel element nucleotide complementation -@inline function _complement_bitpar(a::A, head::UInt64, tail...) where {A<:NucleicAcidAlphabet} - return (BioSequences.complement_bitpar(head, A()), _complement_bitpar(a, tail...)...) -end - -@inline _complement_bitpar(a::A) where {A<:NucleicAcidAlphabet} = () - -@inline function pushfirst(x::Kmer{A,K,N}, nt) where {A,K,N} - ntbits = UInt64(BioSequences.encode(A(), nt)) << (62 - (64N - 2K)) - #ntbits = UInt64(@inbounds BioSequences.twobitnucs[reinterpret(UInt8, nt) + 0x01]) << (62 - (64N - 2K)) - return Kmer{A,K,N}(_rightshift_carry(2, ntbits, x.data...)) -end - -@inline function pushlast(x::Kmer{A,K,N}, nt) where {A,K,N} - ntbits = UInt64(BioSequences.encode(A(), nt)) - #ntbits = UInt64(@inbounds BioSequences.twobitnucs[reinterpret(UInt8, nt) + 0x01]) - _, newbits = _leftshift_carry(2, ntbits, x.data...) - return Kmer{A,K,N}(newbits) -end - - -### -### Transformation methods -### - -""" - complement(seq::T) where {T<:Kmer} - -Return a kmer's complement kmer. - -# Examples - -```jldoctest -julia> complement(Kmer(DNA_T, DNA_T, DNA_A, DNA_G, DNA_C)) -DNA 5-mer: -AATCG -``` -""" -@inline function BioSequences.complement(seq::T) where {T<:Kmer} - return T(_complement_bitpar(Alphabet(seq), seq.data...)) -end - -""" - reverse(seq::Kmer{A,K,N}) where {A,K,N} - -Return a kmer that is the reverse of the input kmer. - -# Examples - -```jldoctest -julia> reverse(Kmer(DNA_T, DNA_T, DNA_A, DNA_G, DNA_C)) -DNA 5-mer: -CGATT -``` -""" -@inline function Base.reverse(seq::Kmer{A,K,N}) where {A,K,N} - rdata = _reverse(BioSequences.BitsPerSymbol(seq), seq.data...) - # rshift should constant-fold. - rshift = n_unused(Kmer{A,K,N}) * BioSequences.bits_per_symbol(A()) - return Kmer{A,K,N}(rightshift_carry(rdata, rshift)) # based on only 2 bit alphabet. -end - -""" - reverse_complement(seq::Kmer) - -Return the kmer that is the reverse complement of the input kmer. - -# Examples - -```jldoctest -julia> reverse_complement(Kmer(DNA_T, DNA_T, DNA_A, DNA_G, DNA_C)) -DNA 5-mer: -GCTAA -``` -""" -@inline function BioSequences.reverse_complement(seq::Kmer{A,K,N}) where {A,K,N} - return complement(reverse(seq)) +function Base.reverse(x::Kmer) + # ( ABC, DEFG) # reverse each element + # (CBA , GFED) # reverse elements + # (GFED, CBA ) # rightshift carry a zero + # ( GFE, DBCA) # final result + Bps = BioSequences.BitsPerSymbol(Alphabet(x)) + data = map(i -> BioSequences.reversebits(i, Bps), reverse(x.data)) + (_, data) = rightshift_carry(data, bits_unused(typeof(x)), zero(UInt)) + typeof(x)(unsafe, data) end -#= -@inline function reverse_complement2(seq::Kmer{A,K,N}) where {A,K,N} - f = x -> complement_bitpar(x, A()) - rdata = _reverse(f, BioSequences.BitsPerSymbol(seq), seq.data...) - return Kmer{A,K,N}(rightshift_carry(rdata, 64N - 2K)) +# For this method, we don't need to mask the unused bits, because the complement of +# 0x0 == DNA_Gap is still DNA_Gap +function BioSequences.complement(x::Kmer{<:Union{DNAAlphabet{4}, RNAAlphabet{4}}}) + isempty(x) && return x + data = map(i -> BioSequences.complement_bitpar(i, Alphabet(x)), x.data) + typeof(x)(unsafe, data) end -=# - -""" - BioSequences.canonical(seq::Kmer{A,K,N}) where {A,K,N} -Return the canonical sequence of `seq`. - -A canonical sequence is the numerical lesser of a kmer and its reverse complement. -This is useful in hashing/counting sequences in data that is not strand specific, -and thus observing the short sequence is equivalent to observing its reverse complement. - -# Examples - -```jldoctest -julia> canonical(Kmer(DNA_T, DNA_T, DNA_A, DNA_G, DNA_C)) -DNA 5-mer: -GCTAA -``` -""" -@inline function BioSequences.canonical(seq::Kmer{A,K,N}) where {A,K,N} - if N < 4 - return min(seq, reverse_complement(seq)) - else - return iscanonical(seq) ? seq : reverse_complement(seq) - end +# For this method we do need to mask unused bits, unlike above +function BioSequences.complement(x::Kmer{<:Union{DNAAlphabet{2}, RNAAlphabet{2}}}) + isempty(x) && return x + data = map(i -> BioSequences.complement_bitpar(i, Alphabet(x)), x.data) + typeof(x)(unsafe, ((first(data) & get_mask(typeof(x))), Base.tail(data)...)) end -### -### Old Mer specific specializations of src/biosequence/transformations.jl -### - not currently transferred to new type. - -# TODO: Sort this and decide on transferring to new NTuple based kmers or no. - -#= -function swap(x::T, i, j) where {T<:AbstractMer} - i = 2 * length(x) - 2i - j = 2 * length(x) - 2j - b = encoded_data(x) - x = ((b >> i) ⊻ (b >> j)) & encoded_data_type(x)(0x03) - return T(b ⊻ ((x << i) | (x << j))) +# Generic fallback +function BioSequences.complement(x::Kmer{<:NucleicAcidAlphabet}) + typeof(x)((complement(i) for i in x)) end - - -function Random.shuffle(x::T) where {T<:AbstractMer} - # Fisher-Yates shuffle for mers. - j = lastindex(x) - for i in firstindex(x):(j - 1) - j′ = rand(i:j) - x = swap(x, i, j′) - end - return x +function BioSequences.reverse_complement(x::Kmer) + @inline(reverse(@inline(complement(x)))) end -=# -throw_translate_err(K) = error("Cannot translate Kmer of size $K not divisible by 3") - -@inline function setup_translate(seq::Kmer{<:NucleicAcidAlphabet, K}) where K - naa, rem = divrem(K, 3) - iszero(rem) || throw_translate_err(K) - kmertype(AAKmer{naa}) +function BioSequences.canonical(x::Kmer) + rc = reverse_complement(x) + ifelse(x < rc, x, rc) end -# This sets the first amino acid to methionine, returning the data tuple -@inline function set_methionine_data(data::Tuple{Vararg{UInt64}}, ::Val{K}) where K - offset = ((K - 1) * 8) & 63 - mask = ~(UInt64(0xff) << offset) # mask off existing AA in pos 1 - addition = UInt64(0x0c) << offset # 0x0c is encoded methionine - chunk, rest... = data - chunk = (chunk & mask) | addition - return (chunk, rest...) -end +BioSequences.iscanonical(x::Kmer) = x <= reverse_complement(x) function BioSequences.translate( - seq::Union{RNAKmer, DNAKmer}; - code=BioSequences.standard_genetic_code, - allow_ambiguous_codons::Bool = true, # a noop for this method - alternative_start::Bool = false -) - T = setup_translate(seq) - data = blank_ntuple(T) - for i in 1:ksize(T) - a = seq[3*i - 2] - b = seq[3*i - 1] - c = seq[3*i - 0] + seq::Kmer{<:Union{DNAAlphabet{2}, RNAAlphabet{2}}}; + code::BioSequences.GeneticCode=BioSequences.standard_genetic_code, + allow_ambiguous_codons::Bool=true, # noop in this method + alternative_start::Bool=false, +) + iszero(ksize(typeof(seq))) && return mer""a + n_aa, remainder = divrem(length(seq), 3) + iszero(remainder) || + error("LongRNA length is not divisible by three. Cannot translate.") + N = n_coding_elements(Kmer{AminoAcidAlphabet, n_aa}) + T = Kmer{AminoAcidAlphabet, n_aa, N} + data = zero_tuple(T) + # In the next two lines: If alternative_start, we shift in the encoding of M + # to first place, then we skip the first 3 nucleotides + (_, data) = leftshift_carry(data, 8, UInt(0x0c) * alternative_start) + @inbounds for i in (1 + (3 * alternative_start)):n_aa + a = seq[3i - 2] + b = seq[3i - 1] + c = seq[3i - 0] codon = BioSequences.unambiguous_codon(a, b, c) aa = code[codon] - # Next line is equivalent to encode, but without checking. - # We assume genetic codes do not code to invalid data. - enc_data = reinterpret(UInt8, aa) % UInt64 - data = leftshift_carry(data, 8, enc_data) + carry = UInt(reinterpret(UInt8, aa)) + (_, data) = + leftshift_carry(data, BioSequences.bits_per_symbol(AminoAcidAlphabet()), carry) end - # This is probably not needed for kmers, but kept for compatibility. - # It does slightly slow down translation, even when not taken. - if alternative_start && !iszero(ksize(T)) - data = set_methionine_data(data, Val(ksize(T))) - end - return T(data) + T(unsafe, data) end -# See the function above for comments, or the equivalent function -# in BioSequences function BioSequences.translate( - seq::Kmer{<:NucleicAcidAlphabet}; - code=BioSequences.standard_genetic_code, - allow_ambiguous_codons::Bool = true, - alternative_start::Bool = false -) - T = setup_translate(seq) - data = blank_ntuple(T) - for i in 1:ksize(T) - a = reinterpret(RNA, seq[3*i - 2]) - b = reinterpret(RNA, seq[3*i - 1]) - c = reinterpret(RNA, seq[3*i - 0]) - aa = if BioSequences.isambiguous(a) | BioSequences.isambiguous(b) | BioSequences.isambiguous(c) - aa_ = BioSequences.try_translate_ambiguous_codon(code, a, b, c) - if aa_ === nothing - if allow_ambiguous_codons - aa_ = AA_X - else - error("codon ", a, b, c, " cannot be unambiguously translated") - end - end - aa_ - else + seq::Kmer{<:Union{DNAAlphabet{4}, RNAAlphabet{4}}}; + code::BioSequences.GeneticCode=BioSequences.standard_genetic_code, + allow_ambiguous_codons::Bool=true, + alternative_start::Bool=false, +) + n_aa, remainder = divrem(length(seq), 3) + iszero(remainder) || + error("LongRNA length is not divisible by three. Cannot translate.") + N = n_coding_elements(Kmer{AminoAcidAlphabet, n_aa}) + T = Kmer{AminoAcidAlphabet, n_aa, N} + data = zero_tuple(T) + # In the next two lines: If alternative_start, we shift in the encoding of M + # to first place, then we skip the first 3 nucleotides + (_, data) = leftshift_carry(data, 8, UInt(0x0c) * alternative_start) + @inbounds for i in (1 + (3 * alternative_start)):n_aa + a = reinterpret(RNA, seq[3i - 2]) + b = reinterpret(RNA, seq[3i - 1]) + c = reinterpret(RNA, seq[3i - 0]) + aa = if isgap(a) | isgap(b) | isgap(c) + error("Cannot translate nucleotide sequences with gaps.") + elseif iscertain(a) & iscertain(b) & iscertain(c) code[BioSequences.unambiguous_codon(a, b, c)] + else + BioSequences.try_translate_ambiguous_codon(code, a, b, c, allow_ambiguous_codons) end - enc_data = reinterpret(UInt8, aa) % UInt64 - data = leftshift_carry(data, 8, enc_data) - end - if alternative_start && !iszero(ksize(T)) - data = set_methionine_data(data, Val(ksize(T))) + carry = UInt(reinterpret(UInt8, aa)) + (_, data) = + leftshift_carry(data, BioSequences.bits_per_symbol(AminoAcidAlphabet()), carry) end - return T(data) + T(unsafe, data) end diff --git a/src/tuple_bitflipping.jl b/src/tuple_bitflipping.jl index 4476956..974bf55 100644 --- a/src/tuple_bitflipping.jl +++ b/src/tuple_bitflipping.jl @@ -1,72 +1,50 @@ - -# TODO: this should end up in BioSequences.jl? - -"Extract the element stored in a packed bitarray referred to by bidx." -@inline function BioSequences.extract_encoded_element(bidx::BioSequences.BitIndex{N,W}, data::NTuple{n,W}) where {N,n,W} - @inbounds chunk = data[BioSequences.index(bidx)] - offchunk = chunk >> (BioSequences.bitwidth(W) - N - BioSequences.offset(bidx)) - return offchunk & BioSequences.bitmask(bidx) -end - - - -""" - _cliphead(by::Integer, head::UInt64, tail...) - -A method used to mask the first `by` MSB's in `head`, before catting it with -tail to return a NTuple. - -This is used internally to mask the first `by` bits in the first word of a -NTuple of UInt64's. - -Notably it's used when constructing a Kmer from an existing NTuple of UInt64 -""" -@inline function _cliphead(by::Integer, head::UInt64, tail...) - return (head & (typemax(UInt64) >> by), tail...) -end - -#= -rightshift_carry & leftshift_carry - -These methods are micro-optimised (or should be!!!) for shifting the bits in -an NTuple of unsigned integers, carrying the bits "shifted off" one word -over to the next word. The carry can also be "seeded" so as other methods like -pushfirst and pushlast can be efficiently implemented without duplication of code -or less efficient implementations that first shift and then insert an element. -=# - -@inline function rightshift_carry(x::NTuple{N,UInt64}, nbits::Integer, prevcarry = zero(UInt64)) where {N} - return _rightshift_carry(nbits, prevcarry, x...) +# These compile to raw CPU instructions and are therefore more +# efficient than simply using << and >>> +@inline function left_shift(x::Unsigned, n::Integer) + x << (n & ((sizeof(x) * 8) - 1)) end -@inline function _rightshift_carry(nbits::Integer, carry::UInt64, head::UInt64, tail...) - return ((head >> nbits) | carry, _rightshift_carry(nbits, (head & ((one(UInt64) << nbits) - 1)) << (64 - nbits), tail...)...) +@inline function right_shift(x::Unsigned, n::Integer) + x >>> (n & ((sizeof(x) * 8) - 1)) end -@inline _rightshift_carry(nbits::Integer, carry::UInt64) = () - -@inline function leftshift_carry(x::NTuple{N,UInt64}, nbits::Integer, prevcarry::UInt64 = zero(UInt64)) where {N} - _, newbits = _leftshift_carry(nbits, prevcarry, x...) - return newbits +# When the UInt is shifted n bits, these are the bits +# that are shifted away (carried over) +@inline function left_carry(x::Unsigned, n::Integer) + right_shift(x, 8 * sizeof(x) - n) end -@inline function _leftshift_carry(nbits::Integer, prevcarry::UInt64, head::UInt64, tail...) - carry, newtail = _leftshift_carry(nbits, prevcarry, tail...) - return head >> (64 - nbits), ((head << nbits) | carry, newtail...) +@inline function right_carry(x::Unsigned, n::Integer) + left_shift(x, 8 * sizeof(x) - n) end -@inline _leftshift_carry(nbits::Integer, prevcarry::UInt64) = prevcarry, () - -@inline function _reverse(bpe::BioSequences.BitsPerSymbol{N}, head::UInt64, tail...) where {N} - return (_reverse(bpe, tail...)..., BioSequences.reversebits(head, bpe)) +# Shift a tuple left nbits, carry over bits between tuple elements, and OR +# the `carry` argument to the right side of the resulting tuple. +# Returns (new_carry, new_tuple) +@inline function leftshift_carry( + x::Tuple{Vararg{T}}, + nbits::Integer, + carry::T, +) where {T <: Unsigned} + isempty(x) && return x + (new_carry, new_tail) = leftshift_carry(tail(x), nbits, carry) + new_head = left_shift(first(x), nbits) | new_carry + (left_carry(first(x), nbits), (new_head, new_tail...)) end -@inline _reverse(::BioSequences.BitsPerSymbol{N}) where {N} = () - -#= -@inline function _reverse(f::F, bpe::BioSequences.BitsPerSymbol{N}, head::UInt64, tail...) where {N,F<:Function} - return (_reverse(f, bpe, tail...)..., f(reversebits(head, bpe))) +@inline function rightshift_carry( + x::Tuple{Vararg{T}}, + nbits::Integer, + carry::T, +) where {T <: Unsigned} + isempty(x) && return x + new_head = right_shift(first(x), nbits) | right_carry(carry, nbits) + mask = left_shift(UInt(1), nbits) - 1 + tail_carry = first(x) & mask + (new_carry, new_tail) = rightshift_carry(tail(x), nbits, tail_carry) + (new_carry, (new_head, new_tail...)) end -@inline _reverse(f::F, ::BioSequences.BitsPerSymbol{N}) where {N,F<:Function} = () -=# \ No newline at end of file +# Recusion terminator for above +@inline leftshift_carry(::Tuple{}, nbits::Integer, carry::Unsigned) = (carry, ()) +@inline rightshift_carry(::Tuple{}, nbits::Integer, carry::Unsigned) = (carry, ()) diff --git a/sticker.svg b/sticker.svg index 1b2ea24..c2c0b89 100644 --- a/sticker.svg +++ b/sticker.svg @@ -1,26 +1,169 @@ - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/access.jl b/test/access.jl deleted file mode 100644 index aa4a35e..0000000 --- a/test/access.jl +++ /dev/null @@ -1,96 +0,0 @@ -@testset "Access and Iterations" begin - dna_kmer = mer"ACTG"dna - rna_kmer = mer"ACUG"rna - aa_kmer = mer"MVXN"aa - - @testset "Access DNA Kmer" begin - @test dna_kmer[1] == DNA_A - @test dna_kmer[2] == DNA_C - @test dna_kmer[3] == DNA_T - @test dna_kmer[4] == DNA_G - - @test dna_kmer[1:3] == mer"ACT"dna - @test dna_kmer[2:4] == mer"CTG"dna - - # Access indexes out of bounds - @test_throws BoundsError dna_kmer[-1] - @test_throws BoundsError dna_kmer[0] - @test_throws BoundsError dna_kmer[5] - @test_throws BoundsError getindex(dna_kmer,-1) - @test_throws BoundsError getindex(dna_kmer, 0) - @test_throws BoundsError getindex(dna_kmer, 5) - @test_throws BoundsError dna_kmer[3:7] - end - - @testset "Iteration through DNA Kmer" begin - @test iterate(DNAKmer("ACTG")) == (DNA_A, 2) - - @test iterate(DNAKmer("ACTG"), 1) == (DNA_A, 2) - @test iterate(DNAKmer("ACTG"), 4) == (DNA_G, 5) - - @test iterate(DNAKmer("ACTG"), 1) !== nothing - @test iterate(DNAKmer("ACTG"), 4) !== nothing - @test iterate(DNAKmer("ACTG"), 5) === nothing - @test isnothing(iterate(DNAKmer("ACTG"), -1)) - @test iterate(DNAKmer("ACTG"), 0) === nothing - - dna_vec = [DNA_A, DNA_C, DNA_T, DNA_G] - @test all([nt === dna_vec[i] for (i, nt) in enumerate(dna_kmer)]) - end - - @testset "Access RNA Kmer" begin - @test rna_kmer[1] == RNA_A - @test rna_kmer[2] == RNA_C - @test rna_kmer[3] == RNA_U - @test rna_kmer[4] == RNA_G - - @test rna_kmer[1:3] == mer"ACU"rna - @test rna_kmer[2:4] == mer"CUG"rna - - # Access indexes out of bounds - @test_throws BoundsError rna_kmer[-1] - @test_throws BoundsError rna_kmer[0] - @test_throws BoundsError rna_kmer[5] - @test_throws BoundsError getindex(rna_kmer, -1) - @test_throws BoundsError getindex(rna_kmer, 0) - @test_throws BoundsError getindex(rna_kmer, 5) - @test_throws BoundsError rna_kmer[3:7] - end - - @testset "Iteration through RNA Kmer" begin - @test iterate(RNAKmer("ACUG")) == (RNA_A, 2) - - - @test iterate(RNAKmer("ACUG"), 1) == (RNA_A, 2) - @test iterate(RNAKmer("ACUG"), 4) == (RNA_G, 5) - - - @test iterate(RNAKmer("ACUG"), 1) !== nothing - @test iterate(RNAKmer("ACUG"), 4) !== nothing - @test iterate(RNAKmer("ACUG"), 5) === nothing - @test iterate(RNAKmer("ACUG"), -1) === nothing - @test iterate(RNAKmer("ACUG"), 0) === nothing - - rna_vec = [RNA_A, RNA_C, RNA_U, RNA_G] - @test all([nt === rna_vec[i] for (i, nt) in enumerate(rna_kmer)]) - end - - @testset "Access AA Kmer" begin - @test aa_kmer[1] == AA_M - @test aa_kmer[2] == AA_V - @test aa_kmer[3] == AA_X - @test aa_kmer[4] == AA_N - - @test aa_kmer[1:3] == mer"MVX"aa - @test aa_kmer[2:4] == mer"VXN"aa - - # Access indexes out of bounds - @test_throws BoundsError aa_kmer[-1] - @test_throws BoundsError aa_kmer[0] - @test_throws BoundsError aa_kmer[5] - @test_throws BoundsError getindex(aa_kmer,-1) - @test_throws BoundsError getindex(aa_kmer, 0) - @test_throws BoundsError getindex(aa_kmer, 5) - @test_throws BoundsError aa_kmer[3:7] - end -end diff --git a/test/benchmark.jl b/test/benchmark.jl new file mode 100644 index 0000000..b166d45 --- /dev/null +++ b/test/benchmark.jl @@ -0,0 +1,125 @@ +module TestBenchmarks + +using Kmers + +extract_kmer(::Type{<:Kmers.AbstractKmerIterator}, x) = x +extract_kmer(::Type{<:FwRvIterator}, x) = first(x) +extract_kmer(::Type{<:UnambiguousKmers}, x) = first(x) + +function reducer(it) + y = 0 + for i in it + y ⊻= extract_kmer(typeof(it), i).data[1] + end + y +end + +using Random: Xoshiro +using BioSequences +rng = Xoshiro(439824) + +const N = 10_000_000 + +const seq_2bit = randseq(DNAAlphabet{2}(), N) +const seq_4bit = randrnaseq(N) +const seq_aa = randaaseq(N) +const data = String(rand(codeunits("AaCcGgTt"), N)) + +const names = [ + ("2-bit LongSequence", seq_2bit), + ("4-bit LongSequence", seq_4bit), + ("AA LongSequence", seq_aa), + ("String", data), +] + +println("Time to iterate over $N symbols:\n") +println("FwKmers") +for (name, seq) in names[1:3] + it = FwKmers{typeof(Alphabet(seq)), 7}(seq) + y = reducer(it) + @time " " * name reducer(it) +end +it = FwDNAMers{7}(data) +y = reducer(it) +@time " String" reducer(it) + +println("FwRvIterator") +for (name, seq) in names[1:2] + it = FwRvIterator{typeof(Alphabet(seq)), 7}(seq) + y = reducer(it) + @time " " * name reducer(it) +end +it = FwRvIterator{DNAAlphabet{2}, 7}(data) +y = reducer(it) +@time " String" reducer(it) + +println("CanonicalKmers") +for (name, seq) in names[1:2] + it = CanonicalKmers{typeof(Alphabet(seq)), 7}(seq) + y = reducer(it) + @time " " * name reducer(it) +end +it = CanonicalDNAMers{7}(data) +y = reducer(it) +@time " String" reducer(it) + +println("UnambiguousKmers") +for (name, seq) in names[1:2] + it = UnambiguousRNAMers{7}(seq) + y = reducer(it) + @time " " * name reducer(it) +end +it = UnambiguousRNAMers{7}(data) +y = reducer(it) +@time " String" reducer(it) + +println("SpacedKmers, step 5") +for (name, seq) in names[1:3] + it = SpacedKmers{typeof(Alphabet(seq)), 7, 5}(seq) + y = reducer(it) + @time " " * name reducer(it) +end +it = SpacedDNAMers{7, 5}(data) +y = reducer(it) +@time " String" reducer(it) + +println("SpacedKmers, step 7") +for (name, seq) in names[1:3] + it = SpacedKmers{typeof(Alphabet(seq)), 7, 7}(seq) + y = reducer(it) + @time " " * name reducer(it) +end +it = SpacedDNAMers{7, 7}(data) +y = reducer(it) +@time " String" reducer(it) + +function unsafe_extract_minimizer(seq::LongDNA{2}, i::Int, ::Val{K}, ::Val{W}) where {K, W} + T = derive_type(Kmer{DNAAlphabet{2}, K}) + kmer = Kmers.unsafe_extract(Kmers.Copyable(), T, seq, i) + hash = fx_hash(kmer) + for offset in 0:(W - 2) + new_kmer = + Kmers.unsafe_shift_from(Kmers.Copyable(), kmer, seq, i + K + offset, Val(1)) + new_hash = fx_hash(new_kmer) + if new_hash < hash + hash = new_hash + kmer = new_kmer + end + end + kmer +end + +function benchmark_minimizer(seq) + y = 0 + for i in 1:20:(length(seq) - 19) + mer = unsafe_extract_minimizer(seq, i, Val{8}(), Val{20}()) + y ⊻= mer.data[1] + end + y +end + +println("Minimizer") +benchmark_minimizer(seq_2bit) +@time " 2-bit LongSequence" benchmark_minimizer(seq_2bit) + +end # module diff --git a/test/biosequences_interface.jl b/test/biosequences_interface.jl deleted file mode 100644 index 5db38a3..0000000 --- a/test/biosequences_interface.jl +++ /dev/null @@ -1,12 +0,0 @@ -@testset "BioSequences Interface" begin - @test BioSequences.has_interface(BioSequence, Kmers.kmertype(Kmer{DNAAlphabet{2},31}), rand(ACGT, 31), false) - @test BioSequences.has_interface(BioSequence, Kmers.kmertype(Kmer{DNAAlphabet{4},31}), rand(ACGT, 31), false) - @test BioSequences.has_interface(BioSequence, Kmers.kmertype(Kmer{RNAAlphabet{2},31}), rand(ACGU, 31), false) - @test BioSequences.has_interface(BioSequence, Kmers.kmertype(Kmer{RNAAlphabet{4},31}), rand(ACGU, 31), false) - - - @test BioSequences.has_interface(BioSequence, Kmers.kmertype(Kmer{DNAAlphabet{2},200}), rand(ACGT, 200), false) - @test BioSequences.has_interface(BioSequence, Kmers.kmertype(Kmer{DNAAlphabet{4},200}), rand(ACGT, 200), false) - @test BioSequences.has_interface(BioSequence, Kmers.kmertype(Kmer{RNAAlphabet{2},200}), rand(ACGU, 200), false) - @test BioSequences.has_interface(BioSequence, Kmers.kmertype(Kmer{RNAAlphabet{4},200}), rand(ACGU, 200), false) -end \ No newline at end of file diff --git a/test/comparisons.jl b/test/comparisons.jl deleted file mode 100644 index 78bf48a..0000000 --- a/test/comparisons.jl +++ /dev/null @@ -1,64 +0,0 @@ -@testset "Comparisons" begin - @testset "Equality" begin - function check_seq_kmer_equality(len) - a = DNAKmer(random_dna_kmer(len)) - b = LongDNA{4}(a) - c = LongDNA{2}(a) - return a == b == c && c == b == a - end - - for len in [1, 10, 32, 64, 128] - @test all(Bool[check_seq_kmer_equality(len) for _ in 1:reps]) - end - - # True negatives - @test DNAKmer("ACG") != RNAKmer("ACG") - @test DNAKmer("T") != RNAKmer("U") - @test DNAKmer("AC") != DNAKmer("AG") - @test RNAKmer("AC") != RNAKmer("AG") - @test AAKmer("MV") != AAKmer("NM") - - @test DNAKmer("ACG") != rna"ACG" - @test DNAKmer("T") != rna"U" - @test DNAKmer("AC") != dna"AG" - @test RNAKmer("AC") != rna"AG" - @test AAKmer("MV") != aa"NM" - - @test rna"ACG" != DNAKmer("ACG") - @test rna"U" != DNAKmer("T") - @test dna"AG" != DNAKmer("AC") - @test rna"AG" != RNAKmer("AC") - @test aa"MV" != AAKmer("NM") - end - - @testset "Inequality" begin - for len in [1, 10, 32, 64] - if len <= 32 - @test isless(DNAKmer{1}((UInt64(0),)), DNAKmer{1}((UInt64(1),))) - @test !isless(DNAKmer{1}((UInt64(0),)), DNAKmer{1}((UInt64(0),))) - @test !isless(DNAKmer{1}((UInt64(1),)), DNAKmer{1}((UInt64(0),))) - - @test isless(RNAKmer{1}((UInt64(0),)), RNAKmer{1}((UInt64(1),))) - @test !isless(RNAKmer{1}((UInt64(0),)), RNAKmer{1}((UInt64(0),))) - @test !isless(RNAKmer{1}((UInt64(1),)), RNAKmer{1}((UInt64(0),))) - end - end - end - - @testset "Hash" begin - kmers = map(DNAKmer, ["AAAA", "AACT", "ACGT", "TGCA"]) - for x in kmers, y in kmers - @test (x == y) == (hash(x) == hash(y)) - end - - kmers = map(RNAKmer, ["AAAA", "AACU", "ACGU", "UGCA"]) - for x in kmers, y in kmers - @test (x == y) == (hash(x) == hash(y)) - end - - kmers = map(AAKmer, ["AMVK", "FPST", "QEGH", "ARND"]) - for x in kmers, y in kmers - @test (x == y) == (hash(x) == hash(y)) - end - end -end diff --git a/test/construction_and_conversion.jl b/test/construction_and_conversion.jl deleted file mode 100644 index 567042c..0000000 --- a/test/construction_and_conversion.jl +++ /dev/null @@ -1,170 +0,0 @@ -global reps = 10 - -@testset "Construction and Conversions" begin - @test Kmer(DNA_A, DNA_G, DNA_T) === Kmer("AGT") - @test Kmer(RNA_A, RNA_G, RNA_U) === Kmer("AGU") - #@test Kmer(AA_R, AA_D, AA_C, AA_B) === Kmer("RDCB") - - @test DNAKmer(DNA_G, DNA_C, DNA_T) == Kmer("GCT") - @test RNAKmer(RNA_G, RNA_U, RNA_C, RNA_U) == Kmer("GUCU") - - # creation from iterator - @test Kmers.kmertype(Kmer{DNAAlphabet{2},31})((i for i in rand(ACGT, 31))) isa Kmers.kmertype(Kmer{DNAAlphabet{2},31}) - - # Check that kmers in strings survive round trip conversion: - # String → Kmer → String - function check_string_construction(::Type{T}, seq::AbstractString) where {T<:Kmer} - return String(T(seq)) == uppercase(seq) - end - - # Check that RNAKmers can be constructed from a LongRNASeq - # LongSequence{A} → Kmer{A,K,N} → LongSequence{A} - function check_longsequence_construction(::Type{T}, seq::S) where {T<:Kmer,S<:LongSequence} - return S(T(seq)) == seq - end - - # Check that kmers can be constructed from a BioSequence - # BioSequence → Kmer → BioSequence - function check_biosequence_construction(::Type{T}, seq::LongSequence) where {T<:Kmer} - return LongSequence(T(seq)) == seq - end - - # Check that kmers can be constructed from an array of nucleotides - # Vector{T} → Kmer → Vector{T} - function check_nucarray_kmer(::Type{M}, seq::Vector{T}) where {T,M<:Kmer} - return String([convert(Char, c) for c in seq]) == String(M(seq)) - end - - # Check that kmers in strings survive round trip conversion: - # String → BioSequence → Kmer → BioSequence → String - function check_roundabout_construction(::Type{T}, A2, seq::AbstractString) where {T<:Kmer} - return String(LongSequence{A2}(T(LongSequence{A2}(seq)))) == uppercase(seq) - end - - #= - function check_uint_conversion(::Type{T}) where {T<:Kmer} - U = BioSequences.encoded_data_type(T) - uint = rand(typemin(U):U(one(U) << 2BioSequences.ksize(T) - 1)) - return convert(U, T(uint)) === uint - end - =# - - @testset "Kmer conversion" begin - for len in [1, 16, 32, 64, 128] - # String construction - # Check that kmers in strings survive round trip conversion: - # String → Kmer → String - @test all(Bool[check_string_construction(DNAKmer{len}, random_dna_kmer(len)) for _ in 1:reps]) - @test all(Bool[check_string_construction(Kmer{DNAAlphabet{4},len}, random_dna_kmer(len)) for _ in 1:reps]) - @test all(Bool[check_string_construction(RNAKmer{len}, random_rna_kmer(len)) for _ in 1:reps]) - @test all(Bool[check_string_construction(Kmer{RNAAlphabet{4},len}, random_rna_kmer(len)) for _ in 1:reps]) - @test all(Bool[check_string_construction(AAKmer{len}, random_aa(len)) for _ in 1:reps]) - - # Long(DNA|RNA)Seq Constructions - # Check that DNAKmers can be constructed from a Long(DNA|RNA)Seq - # Long(DNA|RNA)Seq → Kmer → Long(DNA|RNA)Seq - @test all(Bool[check_longsequence_construction(Kmer{DNAAlphabet{2},len}, LongDNA{2}(random_dna_kmer(len))) for _ in 1:reps]) - @test all(Bool[check_longsequence_construction(Kmer{DNAAlphabet{4},len}, LongDNA{4}(random_dna_kmer(len))) for _ in 1:reps]) - @test all(Bool[check_longsequence_construction(Kmer{DNAAlphabet{4},len}, LongDNA{2}(random_dna_kmer(len))) for _ in 1:reps]) - @test all(Bool[check_longsequence_construction(Kmer{DNAAlphabet{2},len}, LongDNA{4}(random_dna_kmer(len))) for _ in 1:reps]) - @test all(Bool[check_longsequence_construction(Kmer{RNAAlphabet{2},len}, LongRNA{2}(random_rna_kmer(len))) for _ in 1:reps]) - @test all(Bool[check_longsequence_construction(Kmer{RNAAlphabet{4},len}, LongRNA{4}(random_rna_kmer(len))) for _ in 1:reps]) - @test all(Bool[check_longsequence_construction(Kmer{RNAAlphabet{4},len}, LongRNA{2}(random_rna_kmer(len))) for _ in 1:reps]) - @test all(Bool[check_longsequence_construction(Kmer{RNAAlphabet{2},len}, LongRNA{4}(random_rna_kmer(len))) for _ in 1:reps]) - @test all(Bool[check_longsequence_construction(AAKmer{len}, LongAA(random_aa(len))) for _ in 1:reps]) - - # Check Kmer{A1}(::BioSequence{A2}) for compatible A1 and A2 - @test all(Bool[check_longsequence_construction(Kmer{RNAAlphabet{4}}, LongRNA{2}(random_rna_kmer(len))) for _ in 1:reps]) - @test all(Bool[check_longsequence_construction(Kmer{RNAAlphabet{2}}, LongDNA{4}(random_dna_kmer(len))) for _ in 1:reps]) - @test all(Bool[check_longsequence_construction(Kmer{RNAAlphabet{4}}, LongDNA{4}(random_dna_kmer(len))) for _ in 1:reps]) - @test all(Bool[check_longsequence_construction(Kmer{DNAAlphabet{2}}, LongRNA{4}(random_rna_kmer(len))) for _ in 1:reps]) - - # BioSequence Construction - # Check that kmers can be constructed from a BioSequence - # BioSequence → Kmer → BioSequence - @test all(Bool[check_biosequence_construction(Kmer{DNAAlphabet{2},len}, LongSequence{DNAAlphabet{2}}(random_dna_kmer(len))) for _ in 1:reps]) - @test all(Bool[check_biosequence_construction(Kmer{DNAAlphabet{4},len}, LongSequence{DNAAlphabet{4}}(random_dna_kmer(len))) for _ in 1:reps]) - @test all(Bool[check_biosequence_construction(Kmer{DNAAlphabet{2},len}, LongSequence{DNAAlphabet{4}}(random_dna_kmer(len))) for _ in 1:reps]) - @test all(Bool[check_biosequence_construction(Kmer{DNAAlphabet{4},len}, LongSequence{DNAAlphabet{2}}(random_dna_kmer(len))) for _ in 1:reps]) - @test all(Bool[check_biosequence_construction(Kmer{RNAAlphabet{2},len}, LongSequence{RNAAlphabet{2}}(random_rna_kmer(len))) for _ in 1:reps]) - @test all(Bool[check_biosequence_construction(Kmer{RNAAlphabet{4},len}, LongSequence{RNAAlphabet{4}}(random_rna_kmer(len))) for _ in 1:reps]) - @test all(Bool[check_biosequence_construction(Kmer{RNAAlphabet{2},len}, LongSequence{RNAAlphabet{4}}(random_rna_kmer(len))) for _ in 1:reps]) - @test all(Bool[check_biosequence_construction(Kmer{RNAAlphabet{4},len}, LongSequence{RNAAlphabet{2}}(random_rna_kmer(len))) for _ in 1:reps]) - @test all(Bool[check_biosequence_construction(AAKmer{len}, LongSequence{AminoAcidAlphabet}(random_aa(len))) for _ in 1:reps]) - - # Check Kmer(::BioSequence) construction - @test all(Bool[check_longsequence_construction(Kmer, LongRNA{4}(random_rna_kmer(len))) for _ in 1:reps]) - @test all(Bool[check_longsequence_construction(Kmer, LongDNA{2}(random_dna_kmer(len))) for _ in 1:reps]) - @test all(Bool[check_longsequence_construction(Kmer, LongAA(random_rna_kmer(len))) for _ in 1:reps]) - - # Construction from element arrays - # Check that kmers can be constructed from an array of elements - # Vector{T} → Kmer{A,K,N} → Vector{T} - @test all(Bool[check_nucarray_kmer(Kmer{DNAAlphabet{2},len}, random_dna_symbols(len, [0.25, 0.25, 0.25, 0.25, 0.0])) for _ in 1:reps]) - @test all(Bool[check_nucarray_kmer(Kmer{DNAAlphabet{4},len}, random_dna_symbols(len)) for _ in 1:reps]) - @test all(Bool[check_nucarray_kmer(Kmer{RNAAlphabet{2},len}, random_rna_symbols(len, [0.25, 0.25, 0.25, 0.25, 0.0])) for _ in 1:reps]) - @test all(Bool[check_nucarray_kmer(Kmer{RNAAlphabet{4},len}, random_rna_symbols(len)) for _ in 1:reps]) - @test all(Bool[check_nucarray_kmer(AAKmer{len}, random_aa_symbols(len)) for _ in 1:reps]) - - # Roundabout conversions - @test all(Bool[check_roundabout_construction(Kmer{DNAAlphabet{2},len}, DNAAlphabet{2}, random_dna_kmer(len)) for _ in 1:reps]) - @test all(Bool[check_roundabout_construction(Kmer{DNAAlphabet{4},len}, DNAAlphabet{4}, random_dna_kmer(len)) for _ in 1:reps]) - @test all(Bool[check_roundabout_construction(Kmer{DNAAlphabet{2},len}, DNAAlphabet{4}, random_dna_kmer(len)) for _ in 1:reps]) - @test all(Bool[check_roundabout_construction(Kmer{DNAAlphabet{4},len}, DNAAlphabet{2}, random_dna_kmer(len)) for _ in 1:reps]) - @test all(Bool[check_roundabout_construction(Kmer{RNAAlphabet{2},len}, RNAAlphabet{2}, random_rna_kmer(len)) for _ in 1:reps]) - @test all(Bool[check_roundabout_construction(Kmer{RNAAlphabet{4},len}, RNAAlphabet{4}, random_rna_kmer(len)) for _ in 1:reps]) - @test all(Bool[check_roundabout_construction(Kmer{RNAAlphabet{2},len}, RNAAlphabet{4}, random_rna_kmer(len)) for _ in 1:reps]) - @test all(Bool[check_roundabout_construction(Kmer{RNAAlphabet{4},len}, RNAAlphabet{2}, random_rna_kmer(len)) for _ in 1:reps]) - @test all(Bool[check_roundabout_construction(AAKmer{len}, AminoAcidAlphabet, random_aa(len)) for _ in 1:reps]) - end - end - - @test_throws MethodError Kmer() # can't construct 0-mer using `Kmer()` - @test_throws ArgumentError DNAKmer(dna"") # 0-mers not allowed - @test_throws ArgumentError AAKmer(aa"") # 0-mers not allowed - @test_throws ArgumentError DNAKmer{0}(UInt64(0)) # 0-mers not allowed - @test_throws ArgumentError RNAKmer{0}(UInt64(0)) # 0-mers not allowed - @test_throws ArgumentError AAKmer{0}(UInt64(0)) # 0-mers not allowed - @test_throws BioSequences.EncodeError Kmer(RNA_A, RNA_C, RNA_G, RNA_N, RNA_U) # no Ns in kmers - @test_throws BioSequences.EncodeError Kmer(DNA_A, DNA_C, DNA_G, DNA_N, DNA_T) # no Ns in kmers - @test_throws BioSequences.EncodeError RNAKmer(rna"ACGNU") # no Ns in 2-bit nucleic acid kmers - @test_throws BioSequences.EncodeError DNAKmer(dna"ACGNT") # no Ns in 2-bit nucleic acid kmers - @test_throws MethodError Kmer(RNA_A, DNA_A) # no mixing of RNA and DNA - - @testset "From strings" begin - @test DNAKmer("ACTG") == DNAKmer(LongDNA{4}("ACTG")) - @test RNAKmer("ACUG") == RNAKmer(LongRNA{4}("ACUG")) - - # N is not allowed in Kmers - @test_throws Exception DNAMmer("ACGTNACGT") - @test_throws Exception RNAKmer("ACGUNACGU") - - # Test string literals - @test mer"ACTG"dna == DNAKmer(LongDNA{4}("ACTG")) - @test mer"AVBM"aa == AAKmer(LongAA("AVBM")) - @test isa(mer"ACGT"dna, DNAKmer{4}) - @test isa(mer"AVBM"aa, AAKmer{4}) - @test_throws LoadError eval(:(mer"ACGN"dna)) - @test_throws LoadError eval(:(mer"ACG-"dna)) - end - - @testset "Capacity" begin - @test Kmers.capacity(DNAKmer(random_dna_kmer(10))) == 32 - @test Kmers.capacity(RNAKmer(random_rna_kmer(10))) == 32 - @test Kmers.capacity(DNAKmer(random_dna_kmer(32))) == 32 - @test Kmers.capacity(RNAKmer(random_rna_kmer(32))) == 32 - @test Kmers.capacity(DNAKmer(random_dna_kmer(33))) == 64 - @test Kmers.capacity(AAKmer(random_aa(8))) == 8 - @test Kmers.capacity(AAKmer(random_aa(10))) == 16 - end - - @testset "N unused" begin - @test Kmers.n_unused(DNAKmer(random_dna_kmer(10))) == 22 - @test Kmers.n_unused(RNAKmer(random_rna_kmer(10))) == 22 - @test Kmers.n_unused(DNAKmer(random_dna_kmer(32))) == 0 - @test Kmers.n_unused(RNAKmer(random_rna_kmer(32))) == 0 - @test Kmers.n_unused(DNAKmer(random_dna_kmer(33))) == 31 - @test Kmers.n_unused(AAKmer(random_aa(8))) == 0 - @test Kmers.n_unused(AAKmer(random_aa(10))) == 6 - end -end diff --git a/test/debruijn_neighbors.jl b/test/debruijn_neighbors.jl deleted file mode 100644 index c011b84..0000000 --- a/test/debruijn_neighbors.jl +++ /dev/null @@ -1,6 +0,0 @@ -@testset "De Bruijn Neighbors" begin - @test collect(fw_neighbors(DNAKmer("ACG"))) == map(DNAKmer, ["CGA", "CGC", "CGG", "CGT"]) - @test collect(fw_neighbors(DNAKmer("GGGG"))) == map(DNAKmer, ["GGGA", "GGGC", "GGGG", "GGGT"]) - @test collect(fw_neighbors(RNAKmer("ACG"))) == map(RNAKmer, ["CGA", "CGC", "CGG", "CGU"]) - @test collect(fw_neighbors(RNAKmer("GGGG"))) == map(RNAKmer, ["GGGA", "GGGC", "GGGG", "GGGU"]) -end diff --git a/test/find.jl b/test/find.jl deleted file mode 100644 index c3d4aaa..0000000 --- a/test/find.jl +++ /dev/null @@ -1,46 +0,0 @@ -@testset "Find" begin - kmer = DNAKmer("ACGAG") - - @test findnext(DNA_A, kmer, 1) == 1 - @test findnext(DNA_C, kmer, 1) == 2 - @test findnext(DNA_G, kmer, 1) == 3 - @test findnext(DNA_T, kmer, 1) == nothing - @test findnext(DNA_A, kmer, 2) == 4 - - @test_throws BoundsError findnext(DNA_A, kmer, 0) - @test findnext(DNA_A, kmer, 6) === nothing - - @test findprev(DNA_A, kmer, 5) == 4 - @test findprev(DNA_C, kmer, 5) == 2 - @test findprev(DNA_G, kmer, 5) == 5 - @test findprev(DNA_T, kmer, 5) == nothing - @test findprev(DNA_G, kmer, 4) == 3 - - @test findprev(DNA_A, kmer, 0) === nothing - @test_throws BoundsError findprev(DNA_A, kmer, 6) - - @test findfirst(DNA_A, kmer) == 1 - @test findfirst(DNA_G, kmer) == 3 - @test findlast(DNA_A, kmer) == 4 - @test findlast(DNA_G, kmer) == 5 - - kmer = AAKmer("AMVKFPSMT") - - @test findnext(AA_A, kmer, 1) == 1 - @test findnext(AA_M, kmer, 1) == 2 - @test findnext(AA_V, kmer, 1) == 3 - @test findnext(AA_K, kmer, 1) == 4 - @test findnext(AA_F, kmer, 1) == 5 - @test findnext(AA_P, kmer, 1) == 6 - @test findnext(AA_S, kmer, 1) == 7 - @test findnext(AA_M, kmer, 1) == 2 - @test findnext(AA_T, kmer, 1) == 9 - - @test findnext(AA_F, kmer, 4) == 5 - @test findprev(AA_F, kmer, 4) == nothing - @test findnext(AA_A, kmer, 7) == nothing - @test findnext(AA_M, kmer, 5) == 8 - - @test findfirst(AA_M, kmer) == 2 - @test findlast(AA_M, kmer) == 8 -end diff --git a/test/iteration.jl b/test/iteration.jl deleted file mode 100644 index 801a4b0..0000000 --- a/test/iteration.jl +++ /dev/null @@ -1,214 +0,0 @@ -@testset "EveryKmer" begin - @testset "EveryKmer DNA" begin - s = randdnaseq(500) - s2 = LongDNA{2}(s) - # Kmer and sequence Alphabets match. - @test collect(EveryKmer(s, Val{31}())) == collect(EveryKmer(s2, Val{31}())) - @test length(EveryKmer(s, Val{31}())) == length(EveryKmer(s2, Val{31}())) == 470 - - @test collect(EveryKmer(s, Val{201}())) == collect(EveryKmer(s2, Val{201}())) - @test length(EveryKmer(s, Val{201}())) == length(EveryKmer(s2, Val{201}())) == 300 - - # Kmer and sequence Alphabets mismatch. - s3 = dna"AC-TGAG--TGC" - @test collect(EveryKmer{DNACodon}(s3)) == - [(UInt64(4), Kmer(DNA_T, DNA_G, DNA_A)), - (UInt64(5), Kmer(DNA_G, DNA_A, DNA_G)), - (UInt64(10), Kmer(DNA_T, DNA_G, DNA_C))] - end - - @testset "EveryKmer RNA" begin - s = randrnaseq(500) - s2 = LongRNA{2}(s) - # Kmer and sequence Alphabets match. - @test collect(EveryKmer(s, Val{31}())) == collect(EveryKmer(s2, Val{31}())) - @test length(EveryKmer(s, Val{31}())) == length(EveryKmer(s2, Val{31}())) == 470 - - @test collect(EveryKmer(s, Val{201}())) == collect(EveryKmer(s2, Val{201}())) - @test length(EveryKmer(s, Val{201}())) == length(EveryKmer(s2, Val{201}())) == 300 - - # Kmer and sequence Alphabets mismatch. - s3 = rna"AC-UGAG--UGC" - @test collect(EveryKmer{RNACodon}(s3)) == - [(UInt64(4), Kmer(RNA_U, RNA_G, RNA_A)), - (UInt64(5), Kmer(RNA_G, RNA_A, RNA_G)), - (UInt64(10), Kmer(RNA_U, RNA_G, RNA_C))] - end - - @testset "EveryKmer AA" begin - s = randaaseq(500) - s2 = LongAA(s) - @test collect(EveryKmer(s, Val{31}())) == collect(EveryKmer(s2, Val{31}())) - @test length(EveryKmer(s, Val{31}())) == length(EveryKmer(s2, Val{31}())) == 470 - - @test collect(EveryKmer(s, Val{201}())) == collect(EveryKmer(s2, Val{201}())) - @test length(EveryKmer(s, Val{201}())) == length(EveryKmer(s2, Val{201}())) == 300 - end -end - -@testset "SpacedKmers" begin - @testset "SpacedKmers DNA" begin - s = randdnaseq(500) - s2 = LongDNA{2}(s) - @test collect(SpacedKmers(s, Val{31}(), 50)) == collect(SpacedKmers(s2, Val{31}(), 50)) - @test length(SpacedKmers(s, Val{31}(), 50)) == length(SpacedKmers(s2, Val{31}(), 50)) == 10 - - @test collect(SpacedKmers(s, Val{201}(), 50)) == collect(SpacedKmers(s2, Val{201}(), 50)) - @test length(SpacedKmers(s, Val{201}(), 50)) == length(SpacedKmers(s2, Val{201}(), 50)) == 6 - - s3 = dna"AC-TGAG--TGC" - @test collect(SpacedKmers{DNACodon}(s3, 3)) == - [(UInt64(4), Kmer(DNA_T, DNA_G, DNA_A)), - (UInt64(10), Kmer(DNA_T, DNA_G, DNA_C))] - end - - @testset "SpacedKmers RNA" begin - s = randrnaseq(500) - s2 = LongRNA{2}(s) - @test collect(SpacedKmers(s, Val{31}(), 50)) == collect(SpacedKmers(s2, Val{31}(), 50)) - @test length(SpacedKmers(s, Val{31}(), 50)) == length(SpacedKmers(s2, Val{31}(), 50)) == 10 - - @test collect(SpacedKmers(s, Val{201}(), 50)) == collect(SpacedKmers(s2, Val{201}(), 50)) - @test length(SpacedKmers(s, Val{201}(), 50)) == length(SpacedKmers(s2, Val{201}(), 50)) == 6 - - s3 = rna"AC-UGAG--UGC" - @test collect(SpacedKmers{RNACodon}(s3, 3)) == - [(UInt64(4), Kmer(RNA_U, RNA_G, RNA_A)), - (UInt64(10), Kmer(RNA_U, RNA_G, RNA_C))] - end - - @testset "SpacedKmers AA" begin - s = randaaseq(500) - s2 = LongAA(s) - @test collect(SpacedKmers(s, Val{31}(), 50)) == collect(SpacedKmers(s2, Val{31}(), 50)) - @test length(SpacedKmers(s, Val{31}(), 50)) == length(SpacedKmers(s2, Val{31}(), 50)) == 10 - - @test collect(SpacedKmers(s, Val{201}(), 50)) == collect(SpacedKmers(s2, Val{201}(), 50)) - @test length(SpacedKmers(s, Val{201}(), 50)) == length(SpacedKmers(s2, Val{201}(), 50)) == 6 - end -end - -@testset "EveryCanonicalKmer" begin - @testset "EveryCanonicalKmer DNA" begin - s = randdnaseq(500) - s2 = LongDNA{2}(s) - - # Iterator generates expected results... - ## 2-Bit DNA - @test [(x[1], canonical(x[2])) for x in EveryKmer(s2, Val{31}())] == - collect(EveryCanonicalKmer(s2, Val{31}())) - - @test [(x[1], canonical(x[2])) for x in EveryKmer(s2, Val{201}())] == - collect(EveryCanonicalKmer(s2, Val{201}())) - - ## 4-Bit DNA - @test [(x[1], canonical(x[2])) for x in EveryKmer(s, Val{31}())] == - collect(EveryCanonicalKmer(s, Val{31}())) - - @test [(x[1], canonical(x[2])) for x in EveryKmer(s, Val{201}())] == - collect(EveryCanonicalKmer(s, Val{201}())) - - # Test equivalency between different levels of bit compression... - @test [x[2] for x in EveryCanonicalKmer(s, Val{31}())] == - [x[2] for x in EveryCanonicalKmer(s2, Val{31}())] - @test all(iscanonical.([x[2] for x in EveryCanonicalKmer(s, Val{31}())])) && - all(iscanonical.([x[2] for x in EveryCanonicalKmer(s2, Val{31}())])) - - @test [x[2] for x in EveryCanonicalKmer(s, Val{201}())] == - [x[2] for x in EveryCanonicalKmer(s2, Val{201}())] - @test all(iscanonical.([x[2] for x in EveryCanonicalKmer(s, Val{201}())])) && - all(iscanonical.([x[2] for x in EveryCanonicalKmer(s2, Val{201}())])) - - # Kmer and sequence Alphabets mismatch. - s3 = dna"AC-TGAG--TGC" - @test collect(EveryCanonicalKmer{DNACodon}(s3)) == - [(UInt64(4), canonical(Kmer(DNA_T, DNA_G, DNA_A))), - (UInt64(5), canonical(Kmer(DNA_G, DNA_A, DNA_G))), - (UInt64(10), canonical(Kmer(DNA_T, DNA_G, DNA_C)))] - end - - @testset "EveryCanonicalKmer RNA" begin - s = randrnaseq(500) - s2 = LongRNA{2}(s) - - # Iterator generates expected results... - ## 2-Bit DNA - @test [(x[1], canonical(x[2])) for x in EveryKmer(s2, Val{31}())] == - collect(EveryCanonicalKmer(s2, Val{31}())) - - @test [(x[1], canonical(x[2])) for x in EveryKmer(s2, Val{201}())] == - collect(EveryCanonicalKmer(s2, Val{201}())) - - ## 4-Bit DNA - @test [(x[1], canonical(x[2])) for x in EveryKmer(s, Val{31}())] == - collect(EveryCanonicalKmer(s, Val{31}())) - - @test [(x[1], canonical(x[2])) for x in EveryKmer(s, Val{201}())] == - collect(EveryCanonicalKmer(s, Val{201}())) - - # Test equivalency between different levels of bit compression... - @test [x[2] for x in EveryCanonicalKmer(s, Val{31}())] == - [x[2] for x in EveryCanonicalKmer(s2, Val{31}())] - @test all(iscanonical.([x[2] for x in EveryCanonicalKmer(s, Val{31}())])) && - all(iscanonical.([x[2] for x in EveryCanonicalKmer(s2, Val{31}())])) - - @test [x[2] for x in EveryCanonicalKmer(s, Val{201}())] == - [x[2] for x in EveryCanonicalKmer(s2, Val{201}())] - @test all(iscanonical.([x[2] for x in EveryCanonicalKmer(s, Val{201}())])) && - all(iscanonical.([x[2] for x in EveryCanonicalKmer(s2, Val{201}())])) - - s3 = rna"AC-UGAG--UGC" - @test collect(EveryCanonicalKmer{RNACodon}(s3)) == - [(UInt64(4), canonical(Kmer(RNA_U, RNA_G, RNA_A))), - (UInt64(5), canonical(Kmer(RNA_G, RNA_A, RNA_G))), - (UInt64(10), canonical(Kmer(RNA_U, RNA_G, RNA_C)))] - end -end - -@testset "SpacedCanonicalKmers" begin - @testset "SpacedCanonicalKmers DNA" begin - s = randdnaseq(500) - s2 = LongDNA{2}(s) - @test [(x[1], canonical(x[2])) for x in SpacedKmers(s, Val{31}(), 50)] == collect(SpacedCanonicalKmers(s, Val{31}(), 50)) - @test [(x[1], canonical(x[2])) for x in SpacedKmers(s2, Val{31}(), 50)] == collect(SpacedCanonicalKmers(s2, Val{31}(), 50)) - @test [(x[1], canonical(x[2])) for x in SpacedKmers(s, Val{31}(), 50)] == collect(SpacedCanonicalKmers(s2, Val{31}(), 50)) - @test [(x[1], canonical(x[2])) for x in SpacedKmers(s2, Val{31}(), 50)] == collect(SpacedCanonicalKmers(s, Val{31}(), 50)) - @test collect(SpacedCanonicalKmers(s, Val{31}(), 50)) == collect(SpacedCanonicalKmers(s2, Val{31}(), 50)) - @test length(SpacedCanonicalKmers(s, Val{31}(), 50)) == length(SpacedCanonicalKmers(s2, Val{31}(), 50)) == 10 - - @test [(x[1], canonical(x[2])) for x in SpacedKmers(s, Val{201}(), 50)] == collect(SpacedCanonicalKmers(s, Val{201}(), 50)) - @test [(x[1], canonical(x[2])) for x in SpacedKmers(s2, Val{201}(), 50)] == collect(SpacedCanonicalKmers(s2, Val{201}(), 50)) - @test [(x[1], canonical(x[2])) for x in SpacedKmers(s, Val{201}(), 50)] == collect(SpacedCanonicalKmers(s2, Val{201}(), 50)) - @test [(x[1], canonical(x[2])) for x in SpacedKmers(s2, Val{201}(), 50)] == collect(SpacedCanonicalKmers(s, Val{201}(), 50)) - @test collect(SpacedCanonicalKmers(s, Val{201}(), 50)) == collect(SpacedCanonicalKmers(s2, Val{201}(), 50)) - @test length(SpacedCanonicalKmers(s, Val{201}(), 50)) == length(SpacedCanonicalKmers(s2, Val{201}(), 50)) == 6 - - s3 = dna"AC-TGAG--TGC" - @test collect(SpacedCanonicalKmers{DNACodon}(s3, 3)) == - [(UInt64(4), canonical(Kmer(DNA_T, DNA_C, DNA_A))), - (UInt64(10), canonical(Kmer(DNA_T, DNA_G, DNA_C)))] - end - - @testset "SpacedCanonicalKmers RNA" begin - s = randrnaseq(500) - s2 = LongRNA{2}(s) - @test [(x[1], canonical(x[2])) for x in SpacedKmers(s, Val{31}(), 50)] == collect(SpacedCanonicalKmers(s, Val{31}(), 50)) - @test [(x[1], canonical(x[2])) for x in SpacedKmers(s2, Val{31}(), 50)] == collect(SpacedCanonicalKmers(s2, Val{31}(), 50)) - @test [(x[1], canonical(x[2])) for x in SpacedKmers(s, Val{31}(), 50)] == collect(SpacedCanonicalKmers(s2, Val{31}(), 50)) - @test [(x[1], canonical(x[2])) for x in SpacedKmers(s2, Val{31}(), 50)] == collect(SpacedCanonicalKmers(s, Val{31}(), 50)) - @test collect(SpacedCanonicalKmers(s, Val{31}(), 50)) == collect(SpacedCanonicalKmers(s2, Val{31}(), 50)) - @test length(SpacedCanonicalKmers(s, Val{31}(), 50)) == length(SpacedCanonicalKmers(s2, Val{31}(), 50)) == 10 - - @test [(x[1], canonical(x[2])) for x in SpacedKmers(s, Val{201}(), 50)] == collect(SpacedCanonicalKmers(s, Val{201}(), 50)) - @test [(x[1], canonical(x[2])) for x in SpacedKmers(s2, Val{201}(), 50)] == collect(SpacedCanonicalKmers(s2, Val{201}(), 50)) - @test [(x[1], canonical(x[2])) for x in SpacedKmers(s, Val{201}(), 50)] == collect(SpacedCanonicalKmers(s2, Val{201}(), 50)) - @test [(x[1], canonical(x[2])) for x in SpacedKmers(s2, Val{201}(), 50)] == collect(SpacedCanonicalKmers(s, Val{201}(), 50)) - @test collect(SpacedCanonicalKmers(s, Val{201}(), 50)) == collect(SpacedCanonicalKmers(s2, Val{201}(), 50)) - @test length(SpacedCanonicalKmers(s, Val{201}(), 50)) == length(SpacedCanonicalKmers(s2, Val{201}(), 50)) == 6 - - s3 = rna"AC-UGAG--UGC" - @test collect(SpacedCanonicalKmers{RNACodon}(s3, 3)) == - [(UInt64(4), canonical(Kmer(RNA_U, RNA_C, RNA_A))), - (UInt64(10), canonical(Kmer(RNA_U, RNA_G, RNA_C)))] - end -end \ No newline at end of file diff --git a/test/length.jl b/test/length.jl deleted file mode 100644 index 598980e..0000000 --- a/test/length.jl +++ /dev/null @@ -1,7 +0,0 @@ -@testset "Length" begin - for len in [1, 16, 32, 64, 128] - @test length(DNAKmer(random_dna_kmer(len))) == len - @test length(RNAKmer(random_rna_kmer(len))) == len - @test length(AAKmer(random_aa(len))) == len - end -end diff --git a/test/mismatches.jl b/test/mismatches.jl deleted file mode 100644 index b8e1bb0..0000000 --- a/test/mismatches.jl +++ /dev/null @@ -1,51 +0,0 @@ -@testset "Mismatches" begin - function test_mismatches(a, b) - count = 0 - for (x, y) in zip(a, b) - count += x != y - end - @test mismatches(a, b) === mismatches(b, a) === count - end - - for len in 1:64, _ in 1:10 - a = random_dna_kmer(len) - b = random_dna_kmer(len) - test_mismatches(DNAKmer(a), DNAKmer(b)) - test_mismatches(Kmer{DNAAlphabet{4}}(a), Kmer{DNAAlphabet{4}}(b)) - - a = random_rna_kmer(len) - b = random_rna_kmer(len) - test_mismatches(RNAKmer(a), RNAKmer(b)) - test_mismatches(Kmer{RNAAlphabet{4}}(a), Kmer{RNAAlphabet{4}}(b)) - - a = AAKmer(random_aa(len)) - b = AAKmer(random_aa(len)) - test_mismatches(a, b) - end -end - -@testset "Matches" begin - function test_matches(a, b) - count = 0 - for (x, y) in zip(a, b) - count += x == y - end - @test matches(a, b) === matches(b, a) === count - end - - for len in 1:64, _ in 1:10 - a = random_dna_kmer(len) - b = random_dna_kmer(len) - test_matches(DNAKmer(a), DNAKmer(b)) - test_matches(Kmer{DNAAlphabet{4}}(a), Kmer{DNAAlphabet{4}}(b)) - - a = random_rna_kmer(len) - b = random_rna_kmer(len) - test_matches(RNAKmer(a), RNAKmer(b)) - test_matches(Kmer{RNAAlphabet{4}}(a), Kmer{RNAAlphabet{4}}(b)) - - a = AAKmer(random_aa(len)) - b = AAKmer(random_aa(len)) - test_matches(a, b) - end -end \ No newline at end of file diff --git a/test/order.jl b/test/order.jl deleted file mode 100644 index 8b08201..0000000 --- a/test/order.jl +++ /dev/null @@ -1,7 +0,0 @@ -@testset "Order" begin - @test DNAMer("AA") < DNAMer("AC") < DNAMer("AG") < DNAMer("AT") < DNAMer("CA") - @test RNAMer("AA") < RNAMer("AC") < RNAMer("AG") < RNAMer("AU") < RNAMer("CA") - - @test BigDNAMer("AA") < BigDNAMer("AC") < BigDNAMer("AG") < BigDNAMer("AT") < BigDNAMer("CA") - @test BigRNAMer("AA") < BigRNAMer("AC") < BigRNAMer("AG") < BigRNAMer("AU") < BigRNAMer("CA") -end diff --git a/test/print.jl b/test/print.jl deleted file mode 100644 index 9e8f486..0000000 --- a/test/print.jl +++ /dev/null @@ -1,37 +0,0 @@ -@testset "Print" begin - buf = IOBuffer() - - print(buf, DNAKmer("ACGT")) - @test String(take!(buf)) == "ACGT" - - print(buf, RNAKmer("ACGU")) - @test String(take!(buf)) == "ACGU" - - print(buf, Kmer{DNAAlphabet{4}}("ACGT")) - @test String(take!(buf)) == "ACGT" - - print(buf, Kmer{RNAAlphabet{4}}("ACGU")) - @test String(take!(buf)) == "ACGU" - - print(buf, AAKmer("AMVKFPSMT")) - @test String(take!(buf)) == "AMVKFPSMT" -end - -@testset "Show" begin - buf = IOBuffer() - - show(buf, DNAKmer("AGAGT")) - @test String(take!(buf)) == "AGAGT" - - show(buf, RNAKmer("AGAGU")) - @test String(take!(buf)) == "AGAGU" - - show(buf, Kmer{DNAAlphabet{4}}("AGAGT")) - @test String(take!(buf)) == "AGAGT" - - show(buf, Kmer{RNAAlphabet{4}}("AGAGU")) - @test String(take!(buf)) == "AGAGU" - - print(buf, AAKmer("AMVKFPSMT")) - @test String(take!(buf)) == "AMVKFPSMT" -end diff --git a/test/random.jl b/test/random.jl deleted file mode 100644 index 48074e7..0000000 --- a/test/random.jl +++ /dev/null @@ -1,26 +0,0 @@ -@testset "Random" begin - @testset for k in 1:64 - for _ in 1:10 - kmer = rand(DNAKmer{k}) - @test isa(kmer, DNAKmer{k}) - kmer = rand(RNAKmer{k}) - @test isa(kmer, RNAKmer{k}) - end - for size in [0, 1, 2, 5, 10, 100] - @test length(rand(DNAKmer{k}, size)) == size - @test length(rand(RNAKmer{k}, size)) == size - end - kmers = rand(DNAKmer{k}, 10_000) - for i in 1:k - a = sum([kmer[i] for kmer in kmers] .== DNA_A) - c = sum([kmer[i] for kmer in kmers] .== DNA_C) - g = sum([kmer[i] for kmer in kmers] .== DNA_G) - t = sum([kmer[i] for kmer in kmers] .== DNA_T) - @test 2200 ≤ a ≤ 2800 - @test 2200 ≤ c ≤ 2800 - @test 2200 ≤ g ≤ 2800 - @test 2200 ≤ t ≤ 2800 - @test a + c + g + t == 10_000 - end - end -end diff --git a/test/runtests.jl b/test/runtests.jl index 4260278..8a4c0df 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,28 +1,894 @@ module TestKmers -using Kmers -using BioSequences using Test - -const GROUP = get(ENV, "GROUP", "All") +using Random +using Kmers +using BioSequences +using BioSymbols +using StringViews include("utils.jl") -if GROUP == "BioSequences" || GROUP == "All" - include("biosequences_interface.jl") - include("construction_and_conversion.jl") - include("comparisons.jl") - include("length.jl") - include("access.jl") - include("random.jl") - include("find.jl") - include("print.jl") - include("transformations.jl") - include("mismatches.jl") - include("debruijn_neighbors.jl") - include("iteration.jl") - include("translation.jl") - #include("shuffle.jl") +@testset "BioSequences Interface" begin + for A in + [DNAAlphabet{2}, DNAAlphabet{4}, RNAAlphabet{2}, RNAAlphabet{4}, AminoAcidAlphabet] + for K in (1, 9, 116) + @test BioSequences.has_interface( + BioSequence, + Kmers.derive_type(Kmer{A, K}), + rand(collect(A()), K), + false, + ) + end + end +end + +struct CharSymbol <: BioSymbol + x::Char +end +BioSymbols.prefix(::CharSymbol) = "Char" +BioSymbols.type_text(::CharSymbol) = "CharSymbol" +BioSymbols.isterm(::CharSymbol) = false + +# These two are not interface, but useful for the tests below +# (e.g. converting the sequence to a string) +Base.convert(::Type{Char}, x::CharSymbol) = x.x +Base.Char(x::CharSymbol) = convert(Char, x) +Base.convert(::Type{CharSymbol}, x::Char) = CharSymbol(x) +BioSymbols.isgap(::CharSymbol) = false + +# TODO: Should BioSymbols be updated to remove this? +BioSymbols.isvalid(::CharSymbol) = true + +struct CharAlphabet <: Alphabet end +Base.eltype(::Type{CharAlphabet}) = CharSymbol +BioSequences.symbols(::CharAlphabet) = ntuple(i -> CharSymbol(Char(i - 1)), Val{128}()) +BioSequences.encode(::CharAlphabet, c::CharSymbol) = reinterpret(UInt32, c.x) % UInt +BioSequences.decode(::CharAlphabet, c::UInt) = CharSymbol(reinterpret(Char, c % UInt32)) +BioSequences.BitsPerSymbol(::CharAlphabet) = BioSequences.BitsPerSymbol{32}() + +struct GenericNucAlphabet <: NucleicAcidAlphabet{8} end +Base.eltype(::Type{GenericNucAlphabet}) = DNA +BioSequences.symbols(::GenericNucAlphabet) = symbols(DNAAlphabet{4}()) +BioSequences.encode(::GenericNucAlphabet, c::DNA) = BioSequences.encode(DNAAlphabet{4}(), c) +BioSequences.decode(::GenericNucAlphabet, c::UInt) = + BioSequences.decode(DNAAlphabet{4}(), c) +BioSequences.BitsPerSymbol(::GenericNucAlphabet) = BioSequences.BitsPerSymbol{8}() + +const ALPHABETS = [ + DNAAlphabet{2}(), + RNAAlphabet{2}(), + DNAAlphabet{4}(), + RNAAlphabet{4}(), + AminoAcidAlphabet(), + CharAlphabet(), +] + +@testset "Construction" begin + # Fundamentals + dna = dna"TAGCTAAC" + mer = Kmer{DNAAlphabet{2}, length(dna)}(dna) + @test mer isa Kmer{DNAAlphabet{2}, length(dna)} + @test DNACodon == DNAKmer{3, 1} + @test RNACodon == RNAKmer{3, 1} + + for A in ALPHABETS + Ta = typeof(A) + for L in [0, 3, 11, 41] + for i in 1:3 + # Fundamentals and length + s = randseq(A, SamplerUniform(symbols(A)), L) + mer = Kmer{Ta, L}(collect(s)) + @test mer isa Kmer{Ta, L} + @test length(mer) == L + @test string(mer) == string(s) + + # From string + mer2 = Kmer{Ta, L}(string(s)) + @test mer == mer2 + @test mer === mer2 + @test string(mer) == string(mer2) + + # From LongSequence + mer3 = Kmer{Ta, L}(s) + @test mer === mer3 + end + end + end + + @testset "Bad parameters" begin + @test_throws Exception Kmer{DNAAlphabet, :foo, 1}("C") + @test_throws Exception Kmer{DNAAlphabet, -1, 0}("") + @test_throws Exception Kmer{DNAAlphabet, 1, 2}("A") + end + + @testset "Construct from string" begin + for s in ["TAG", "ACCGAGCC", "TGATGCTATTAGG"] + L = length(s) + for ss in (s, view(s, 1:lastindex(s))) + @test DNAKmer{L, 1}(ss) == DNAKmer{L}(ss) + @test string(DNAKmer{L}(ss)) == ss + end + end + + s = "UHVKALRIQURPFLSMOF" + @test string(AAKmer{18}(s)) == s + + for s in ["αβγδϵ", "", "中国人大网"] + L = length(s) + for ss in (s, view(s, 1:lastindex(s))) + sq = Kmer{CharAlphabet, L}(ss) + @test string(sq) == s + @test [Char(i) for i in sq] == collect(ss) + end + end + end + + @testset "Wrong length" begin + @test_throws Exception DNAKmer{4}("TAC") + @test_throws Exception DNAKmer{4}("TACCA") + @test_throws Exception Kmer{CharAlphabet, 2}(['T']) + @test_throws Exception AAMer{3}((AminoAcid(i) for i in "WPLK" if true)) + end + + @testset "Length must be given explicitly" begin + for s in ["TACA", ""] + @test_throws Exception DNAKmer("TACGA") + @test string(DNAKmer{length(s)}(s)) == s + end + @test_throws Exception AAMer(aa"WPLKM") + @test collect(AAKmer{5}(aa"WPLKM")) == collect(aa"WPLKM") + end + + @testset "Kmer literal" begin + @test collect(mer"TGAGTCA"d) == collect(dna"TGAGTCA") + @test collect(mer"WQOPMKAP"a) == collect(aa"WQOPMKAP") + @test collect(mer"UAUCGGAUC"r) == collect(rna"UAUCGGAUC") + + @test_throws Exception eval(:(mer"ATCGATAG"k)) # invalid flag + end + + @testset "Construct from Biosequences" begin + @testset "Construct from LongSequence" begin + for seq in [ + dna"TAGGCA", + rna"UUCUGUGAGUCC", + aa"TTCGGAA", + LongSequence{CharAlphabet}("HELLO"), + ] + for sq in [seq, view(seq, 2:lastindex(seq))] + A = typeof(Alphabet(sq)) + @test Kmer{A, length(sq)}(sq) == Kmer{A, length(sq)}(string(sq)) + @test string(Kmer{A, length(sq)}(sq)) == string(sq) + @test_throws Exception Kmer{A, length(sq) + 1}(sq) + end + end + end + + @testset "Construct from kmer" begin + m = mer"TAGCGTTA"d + m2 = DNAKmer{8}(m) + @test m === m2 + @test_throws Exception DNAKmer{7}(m) + m3 = RNAKmer{8}(m) + @test m3 === mer"UAGCGUUA"r + @test_throws Exception RNAKmer{9}(m) + @test_throws Exception AAKmer{8}(m) + end + + # From generic biosequence - TODO + end + + @testset "Construct from iterable" begin + m1 = DNAKmer{6}((i for i in dna"GCGATC")) + m2 = DNAKmer{6}((i for i in dna"ATCGATGCAA" if i ∈ (DNA_A, DNA_C))) + @test m1 === mer"GCGATC"d + @test m2 === mer"ACACAA"d + m3 = DNAKmer{4}((i for i in rna"GAUC" if true)) + @test m3 === mer"GATC"d + end +end + +@testset "Comparison" begin + @testset "Equality" begin + @test mer""a == mer""a + @test mer"KMNUPQCX"a == mer"KMNUPQCX"a + @test mer"PKMNEA"a != mer"PKMNE"a + @test mer"PKM"a != mer"PK"a + @test mer"IUDHLDJVIPOEJKWE"a != mer"IUDHLDJVIPOEJKW"a + end + + @testset "Ordering" begin + @test mer"UGCAG"r > mer"CGCAG"r + @test mer"TCGGAAG"d > mer"TCGGAAC"d + @test mer"OEWPM"a > mer"OEWP"a + @test mer"UGCGA"r > mer"TGAGA"d + + @test cmp(mer"TAGCTA"d, mer"TACCTA"d) == 1 + @test cmp(mer"TAC"d, mer"TAGCA"d) == -1 + end + + @testset "Hashing, isless and isequal" begin + @test hash(mer"POSMDGF"a, UInt(15)) === hash(mer"POSMDGF"a, UInt(15)) + @test isequal(mer"POSMDGF"a, mer"POSMDGF"a) + + # Same, but DNA/RNA + m1 = mer"TAGCTA"d + m2 = mer"UAGCUA"r + @test isequal(m1, m2) + @test hash(m1) === hash(m2) + m3 = Kmer{DNAAlphabet{4}}(m1) + m4 = Kmer{RNAAlphabet{4}}(m2) + @test isequal(m3, m4) + @test hash(m3) === hash(m4) + + # Other kmers + # This throws because we want kmer hashing to be maximally fast, + # which implies they must have a different hashing strategy from + # other BioSequences, which implies they can't be isequal + @test_throws Exception isequal(mer"UGCUGA"r, mer"UGCUGA"a) + @test !isequal(mer"UGCAC"r, mer"UGCGA"r) + + # Other sequences + @test_throws Exception dna"TAG" == mer"TAG"d + @test_throws Exception mer"TAG"d == dna"TAG" + end +end + +@testset "Access" begin + @testset "Scalar indexing" begin + m = mer"TGATGCTAGTAGTATTCTATAG"d + @test m isa Mer{22} + + @test m[1] == first(m) == DNA_T + @test m[3] == DNA_A + @test last(m) == m[22] == DNA_G + + @test_throws BoundsError m[0] + @test_throws BoundsError m[-1] + @test_throws BoundsError m[23] + + for s in + [dna"TAGCAAC", dna"TWKKSVVDNA-A", rna"UGUGUCA", rna"UGUCGWS", aa"PLLKMDDSH"] + m = Kmer{typeof(Alphabet(s)), length(s)}(s) + @test first(m) == first(s) + @test last(m) == last(s) + for i in [1, 3, 5] + @test s[i] == m[i] + end + end + + # Weirdly, this throws ArgumentError + @test_throws Exception first(DNAKmer{0}("")) + @test_throws Exception last(RNAKmer{0}("")) + end + + @testset "Unit ranges" begin + m = mer"POKDGTWDIKVL"a + @test m isa Mer{12} + + @test m[1:3] === mer"POK"a + @test m[2:6] === mer"OKDGT"a + @test m[6:(end - 1)] === mer"TWDIKV"a + + @test m[eachindex(m)] === m + @test m[Base.OneTo(4)] === mer"POKD"a + + @test_throws BoundsError m[0:4] + @test m[0:-1] == AAKmer{0}("") + @test_throws BoundsError m[2:13] + end + + @testset "With vector of indices" begin + m = mer"UGCUGAUCGUAU"r + @test m isa Mer{12} + + @test m[[1, 3, 5]] == mer"UCG"r + @test m[[12, 9, 7]] == mer"UGU"r + @test m[Int[]] == Kmer{RNAAlphabet{2}, 0}("") + + @test_throws BoundsError m[[2, 8, 15]] + @test_throws BoundsError m[[0, 1]] + @test_throws BoundsError m[[13]] + end + + @testset "Logical indexing" begin + m = Kmer{CharAlphabet, 4}("ØÆGD") + @test m[[true, false, true, false]] == Kmer{CharAlphabet, 2}("ØG") + @test m[trues(4)] === m + @test m[falses(4)] === Kmer{CharAlphabet, 0}("") + + @test_throws BoundsError m[[true, false, true, true, true]] + @test_throws BoundsError m[[false, false, true]] + @test_throws BoundsError m[trues(5)] + end +end + +@testset "Modification" begin + @testset "push, push_first" begin + m = mer"UHALSAP"a + @test push(m, AA_W) == mer"UHALSAPW"a + @test push(push(m, AA_W), AA_M) === mer"UHALSAPWM"a + @test push_first(m, AA_Gap) == mer"-UHALSAP"a + @test push_first(push(m, AA_K), AA_H) == mer"HUHALSAPK"a + + @test push(m, 'K') == mer"UHALSAPK"a + @test push(mer"TAG"d, RNA_A) == mer"TAGA"d + end + + @testset "shift, shiftfirst" begin + m = mer"PDOFPOLEF"a + v = collect(m) + for aa in aa"PLLMWFVB" + m = shift(m, aa) + @test m isa Mer{9} + popfirst!(push!(v, aa)) + @test collect(m) == v + end + + m = mer"AUGCGUA"r + v = collect(m) + for dna in dna"TAGTGTGCTA" + m = shift_first(m, dna) + @test m isa Mer{7} + pop!(pushfirst!(v, dna)) + @test collect(m) == v + end + end + + @testset "pop, pop_first" begin + m = mer"LNPQ"a + @test (m = pop(m)) == mer"LNP"a + @test (m = pop(m)) == mer"LN"a + @test (m = pop(m)) == mer"L"a + @test (m = pop(m)) == AAKmer{0}("") + @test_throws ArgumentError pop(m) + + @test pop(mer"MDFFIJFKL"a) === mer"MDFFIJFK"a + + m = mer"UAGC"r + @test (m = pop_first(m)) == mer"AGC"r + @test (m = pop_first(m)) == mer"GC"r + @test (m = pop_first(m)) == mer"C"r + @test (m = pop_first(m)) == mer""r + @test pop_first(mer"PKWIKMPPAVYWA"a) == mer"KWIKMPPAVYWA"a + end + + @testset "Setindex" begin + mer = mer"PLQVAK"a + setindex = Base.setindex + @test setindex(mer, 3, AA_K) == mer"PLKVAK"a + @test setindex(mer, 1, AA_R) == mer"RLQVAK"a + @test setindex(mer, 6, AA_M) == mer"PLQVAM"a + @test_throws BoundsError setindex(mer, 0, AA_K) + @test_throws BoundsError setindex(mer, 7, AA_K) + + mer = mer"ATGTCGTGA"d + @test setindex(mer, 1, DNA_T) == mer"TTGTCGTGA"d + @test setindex(mer, 5, DNA_C) == mer"ATGTCGTGA"d + @test setindex(mer, 5, DNA_A) == mer"ATGTAGTGA"d + + mer = mer"PLAKCVMARYKW"a + @test setindex(mer, 10, AA_Q) == mer"PLAKCVMARQKW"a + end +end + +@testset "Biological operations" begin + for s in [ + dna"", + aa"", + LongDNA{2}(dna"TAGTGCA"), + LongRNA{2}(rna"UGCUGUAA"), + dna"TGASWKHVAAN--A", + rna"UAGUCUYMNS", + aa"LKHWSYYVQN", + LongSequence{CharAlphabet}("LKDSJ"), + LongSequence{CharAlphabet}("κ𝚶⊸∑Γ"), + ] + m = Kmer{typeof(Alphabet(s)), length(s)}(s) + + # Reverse + @test collect(reverse(m)) == reverse(collect(m)) + @test collect(reverse(m)) == collect(reverse(s)) + + # The rest of the operations are only for nucleotides + isa(Alphabet(s), NucleicAcidAlphabet) || continue + + # Complement + @test collect(complement(s)) == collect(complement(m)) + + # Reverse complement + rv = reverse_complement(m) + @test collect(reverse_complement(s)) == collect(rv) + + # Canonical + can = canonical(m) + @test collect(can) == collect(canonical(s)) + @test can ≤ m + if can === m + @test m ≤ rv + else + @test can === rv + @test rv ≤ m + end + + @test iscanonical(mer"AGCTAG"d) + @test iscanonical(mer""d) + @test iscanonical(mer"GCGAAC"d) + @test iscanonical(mer"AATT"d) + @test !iscanonical(mer"GGATGC"d) + @test !iscanonical(mer"TCGTGA"d) + @test !iscanonical(mer"TTGAA"d) + end +end + +@testset "Translation" begin + @testset "Forward translation" begin + # Empty + @test translate(mer""r) == mer""a + @test translate(mer""d) == mer""a + @test translate(Kmer{DNAAlphabet{4}, 0}("")) == mer""a + + # Not divisible by 3 + @test_throws Exception translate(mer"U"r) + @test_throws Exception translate(mer"UGCA"r) + @test_throws Exception translate(mer"GUCGAUUGUC"r) + + # Containing gaps + @test_throws Exception translate(Kmer{DNAAlphabet{4}, 6}("CTGA-C")) + @test_throws Exception translate(Kmer{RNAAlphabet{4}, 3}("UC-")) + + # Invalid alphabet + @test_throws Exception transate(mer"CCC"a) + @test_throws Exception transate(Kmer{CharAlphabet, 3}("GGG")) + + # Compare to LongSequence + for s in [ + rna"UCGUAGUUCGAUUCUAUGCUGUAGUGGCAA", + rna"UCGUAGGCGUAUUGCGCAAAGCGC", + rna"UGCUAGUGUUCGAAA", + rna"UCGUUAGUAAAA", + ] + for A in [DNAAlphabet{4}, RNAAlphabet{2}, DNAAlphabet{2}, RNAAlphabet{4}] + ss = LongSequence{A}(s) + @test collect(translate(ss)) == collect(translate(Kmer{A, length(s)}(s))) + end + end + + for s in [ + rna"UGCUGAWKVUDUGWUGUDHUAGUGCNUBGKUGCMGGSWC", + rna"UCGUAGUCKGUCGUYCUGAGGWUGCUGANNUGCUGA", + rna"CAGGCCAGWGCUGSSSCUGSMGKYVUCUAS", + ] + for A in [DNAAlphabet{4}, RNAAlphabet{4}] + ss = LongSequence{A}(s) + @test collect(translate(ss)) == collect(translate(Kmer{A, length(s)}(s))) + end + end + + # Skip 1, the index of gap (which cannot be translated) + A = alphabet(RNA) + for i in 2:16, j in 2:16, k in 2:16 + mer = Kmer{RNAAlphabet{4}, 3}((A[i], A[j], A[k])) + @test only(translate(mer)) == only(translate(LongSequence(mer))) + end + end + + @testset "CodonSet" begin + codons = [RNACodon((i, j, k)) for i in mer"UACG"r, j in mer"UACG"r, k in mer"UACG"r] + @test length(Set(codons)) == 64 + sources = [[], codons[[1, 4, 8]], codons, codons[rand(Bool, 64)], codons[[4, 8]]] + csets = map(CodonSet, sources) + sets = map(Set, sources) + + # Basic properties + for (cset, set) in zip(csets, sets) + @test cset == set + @test sort!(collect(cset)) == sort!(collect(set)) + @test length(cset) == length(set) + @test isempty(cset) == isempty(set) + for i in set + @test i ∈ cset + end + + s = isempty(cset) ? mer"AAA"r : first(cset) + @test delete(cset, s) == delete!(copy(set), s) + @test filter(i -> first(i) == DNA_A, cset) == + filter(i -> first(i) == DNA_A, set) + end + + for (si, ci) in zip(sets, csets), (sj, cj) in zip(sets, csets) + @test issubset(si, sj) == issubset(ci, cj) + for f in [union, setdiff, intersect, symdiff] + @test Set(f(ci, cj)) == f(si, sj) + end + end + end + + @testset "Standard reverse genetic code" begin + seq = LongAA(collect((i for i in alphabet(AminoAcid) if i ∉ (AA_Gap, AA_U, AA_O)))) + codonsets = reverse_translate(seq) + seen_codons = Set{RNACodon}() + for (codonset, aa) in zip(codonsets, seq) + @test reverse_translate(aa) === codonset + if isambiguous(aa) + bits = zero(compatbits(aa)) + for codon in codonset + bits |= compatbits(only(translate(codon))) + end + # selenocysteine and Pyrrolysine have bits + # 0x00300000. However, translating normal + # codons cannot get these amino acids, + # so we ignore them by masking their bits + @test bits == (compatbits(aa) & 0x000fffff) + else + @test isdisjoint(seen_codons, codonset) + union!(seen_codons, codonset) + for codon in codonset + @test only(translate(codon)) == aa + end + end + end + @test length(seen_codons) == 64 + + code = Kmers.rev_standard_genetic_code + @test length(code) == 27 + @test collect(code) == map(0x00:UInt8(26)) do i + aa = reinterpret(AminoAcid, i) + aa => reverse_translate(aa) + end + @test_throws Exception code[AA_Gap] + end + + @testset "Custom reverse genetic code" begin + fw_code = BioSequences.pterobrachia_mitochondrial_genetic_code + code = ReverseGeneticCode(fw_code) + for (aa, set) in code + for codon in set + if aa ∈ (AA_O, AA_U, AA_B, AA_J, AA_X, AA_Z) + continue + end + @test only(translate(LongSequence(codon); code=fw_code)) === aa + end + end + end +end + +@testset "Printing" begin + function test_print(s, str) + @test string(s) == str + io = IOBuffer() + print(io, s) + @test String(take!(io)) == str + end + + for s in [ + dna"", + aa"", + LongDNA{2}(dna"TAGTGCA"), + LongRNA{2}(rna"UGCUGUAA"), + dna"TGASWKHVAAN--A", + rna"UAGUCUYMNS", + aa"LKHWSYYVQN", + ] + test_print(s, string(s)) + end +end + +@testset "Iterators" begin + @testset "Forward iteration" begin + @testset "Aliases" begin + @test FwKmers{DNAAlphabet{2}, 3}(dna"TAGA") isa + FwKmers{DNAAlphabet{2}, 3, LongDNA{4}} + @test FwDNAMers{4}(rna"UAGC") isa FwKmers{DNAAlphabet{2}, 4, LongRNA{4}} + @test FwRNAMers{4}(dna"TACA") isa FwKmers{RNAAlphabet{2}, 4, LongDNA{4}} + @test FwAAMers{4}(aa"LKCY") isa FwKmers{AminoAcidAlphabet, 4, LongAA} + end + + @testset "Smaller than K" begin + @test isempty(FwDNAMers{3}(dna"TA")) + @test isempty(FwAAMers{9}(aa"AOPJVPES")) + @test isempty(FwKmers{RNAAlphabet{4}, 6}(dna"ATGGA")) + end + + @testset "Conversible alphabets" begin + for (seqs, alphabets) in [ + ( + [LongDNA{2}("TGATGGCGTAGTA"), LongRNA{2}("UCGUGCUA"), LongDNA{2}("")], + [DNAAlphabet{2}, DNAAlphabet{4}, RNAAlphabet{2}, RNAAlphabet{4}], + ), # From two-bit + ( + [dna"TAGTCTGAC", rna"UAGUCGAUUAGGCC"], + [DNAAlphabet{2}, DNAAlphabet{4}, RNAAlphabet{2}, RNAAlphabet{4}], + ), # From four-bit + ] + for seq in seqs, alphabet in alphabets + v1 = collect(FwKmers{alphabet, 3}(seq)) + v2 = [Kmer{alphabet, 3, 1}(seq[i:(i + 2)]) for i in 1:(length(seq) - 2)] + @test v1 == v2 + end + end + for seq in [dna"TGWSNVNTGA", rna"C-GGAU-WSNUCG"] + @test_throws Exception first(FwDNAMers{3}(seq)) + @test_throws Exception first(FwRNAMers{3}(seq)) + end + end + + @testset "Four to two bit" begin + for seq in [dna"TATGCTTCGTAGTCGTCGTTGCTA"] + for seqq in [seq, LongRNA{4}(seq)] + filtered = typeof(seqq)([i for i in seqq if !isambiguous(i)]) + for A in [DNAAlphabet{2}, RNAAlphabet{2}] + v1 = collect(FwKmers{A, 4}(seqq)) + v2 = [ + Kmer{A, 4, 1}(filtered[i:(i + 3)]) for + i in 1:(length(filtered) - 3) + ] + @test v1 == v2 + end + end + end + end + + @testset "From ASCII bytes" begin + str = "TaghWS-TGnADbkWWMSTV" + T = FwKmers{DNAAlphabet{4}, 4} + mers = collect(T(str)) + for source in + [str, view(str, 1:lastindex(str)), codeunits(str), Vector(codeunits(str))] + @test collect(T(source)) == mers + end + + # Bad byte in ASCII + s = "TAGTCGTAGPATGC" + @test_throws BioSequences.EncodeError collect(FwDNAMers{3}(s)) + end + + # Unconvertible alphabet + @testset "Unconvertible alphabet" begin + @test_throws Exception iterate(FwKmers{DNAAlphabet{4}, 2}(aa"TAGTGCA")) + end + + # GenericRecoding + s = dna"TGATGTCGTAGTGAgtagtaCCA" + it = FwKmers{GenericNucAlphabet, 8}(s) + @test collect(it) == + [Kmer{GenericNucAlphabet, 8}(s[i:(i + 7)]) for i in 1:(length(s) - 7)] + end + + @testset "FwRvIterator" begin + function naive_fwrv(s::NucSeq, len::Integer) + A = typeof(Alphabet(s)) + T = Kmers.derive_type(Kmer{A, len}) + [ + (T(s[i:(i + len - 1)]), T(reverse_complement(s[i:(i + len - 1)]))) for + i in 1:(length(s) - len + 1) + ] + end + + for s in ["", "TGATGCTGTA", "TAT"] + Ts = [DNAAlphabet{2}, DNAAlphabet{4}, RNAAlphabet{2}, RNAAlphabet{4}] + srcs = Any[LongSequence{T}(LongDNA{2}(s)) for T in Ts] + dsts = copy(srcs) + for dst in dsts + srcs_ = push!(copy(srcs), String(typeof(dst)(LongDNA{2}(s)))) + for src in srcs_ + @test collect(FwRvIterator{typeof(Alphabet(dst)), 4}(src)) == + naive_fwrv(dst, 4) + end + end + end + end + + @testset "CanonicalKmers" begin + @testset "Aliases" begin + @test CanonicalKmers{DNAAlphabet{4}, 3}("TAGCTAGA") isa CanonicalKmers + @test CanonicalDNAMers{8}("TAGCTAGA") isa CanonicalKmers + @test CanonicalRNAMers{9}("TAGCTAGA") isa CanonicalKmers + end + + @testset "Only nucleic acids" begin + @test_throws Exception CanonicalKmers{AminoAcidAlphabet, 3}("UAGCTGA") + end + + @testset "Iteration" begin + for s in [ + dna"TAGCTAGGACA", + rna"UAGUCGUGAGA", + "TAGCTAGAGGA", + collect(codeunits("ATGCGAGGA")), + ] + seq = LongDNA{2}(s) + cns = [canonical(seq[i:(i + 4)]) for i in 1:(length(seq) - 4)] + for A in [DNAAlphabet{2}, DNAAlphabet{4}] + it = CanonicalKmers{A, 5}(s) + @test collect(it) == [Kmer{A, 5, 1}(i) for i in cns] + end + end + + s = dna"TAGTCGTGATGATAGTCTGAATGTC" + it = CanonicalKmers{GenericNucAlphabet, 6}(s) + @test collect(it) == [ + canonical(Kmer{GenericNucAlphabet, 6}(s[i:(i + 5)])) for + i in 1:(length(s) - 5) + ] + + s = "TAGTGTCGATGATC" + it1 = CanonicalKmers{DNAAlphabet{2}, 4, String}(s) + it2 = CanonicalDNAMers{4}(s) + @test collect(it1) == collect(it2) + end + end + + @testset "UnambiguousKmers" begin + for s in [dna"TAGCWSAGACYWNACGCNACG--", rna"UAGUCYWUAGCNUAHAGC-GAUGAGC"] + res = [(s[i:(i + 2)], i) for i in 1:(length(s) - 2)] + filter!(i -> all(iscertain, first(i)), res) + for A in [DNAAlphabet{2}, RNAAlphabet{2}] + resA = [(Kmer{A, 3, 1}(i), j) for (i, j) in res] + it = UnambiguousKmers{A, 3}(s) + @test collect(it) == resA + end + + A = s isa LongDNA ? DNAAlphabet{2} : RNAAlphabet{2} + pos = [i:(i + 3) for i in 1:(lastindex(s) - 3)] + filter!(pos) do rng + all(iscertain, s[rng]) + end + resA = [(Kmer{A, 4, 1}(s[i]), first(i)) for i in pos] + it = UnambiguousKmers{A, 4}(string(s)) + @test collect(it) == resA + end + + # Copyable + s = LongDNA{2}("TATCGGATAGGCAAA") + v = collect(UnambiguousRNAMers{4}(s)) + v2 = [(DNAKmer{4}(s[i:(i + 3)]), i) for i in 1:(length(s) - 3)] + @test v == v2 + + # GenericRecoding + s = dna"TAGCTKAGAGGAGAACWSGCGAGA" + it = UnambiguousKmers{DNAAlphabet{2}, 4}(s) + v = collect(it) + v2 = [ + (DNAKmer{4}(s[i:(i + 3)]), i) for + i in 1:(length(s) - 3) if all(iscertain, s[i:(i + 3)]) + ] + @test v == v2 + + s = LongSequence{GenericNucAlphabet}(dna"TGATCGTAGATGwATGTC") + it = UnambiguousKmers{DNAAlphabet{2}, 7, LongSequence{GenericNucAlphabet}}(s) + it2 = UnambiguousDNAMers{7}(s) + @test collect(it) == collect(it2) + + # Bad byte in ASCII + s = "TAGTCGTAGPATGC" + @test_throws BioSequences.EncodeError collect(UnambiguousDNAMers{3}(s)) + end + + @testset "SpacedKmers" begin + function test_naive_spaced(A, seq, k, space) + T = Kmers.derive_type(Kmer{A, k}) + v = [T(seq[i:(i + k - 1)]) for i in 1:space:(length(seq) - k + 1)] + @test collect(SpacedKmers{A, k, space}(seq)) == v + end + + for (s, A) in Any[ + ("TA-NGAKATCGAWTAGA", DNAAlphabet{4}), + ("AUGCUGAUGAGUCGUAG", RNAAlphabet{2}), + ("KLMYUPOKQMMNLVYRW", AminoAcidAlphabet), + ] + test_naive_spaced(A, s, 3, 2) + test_naive_spaced(A, s, 2, 4) + test_naive_spaced(A, codeunits(s), 3, 3) + end + test_naive_spaced(DNAAlphabet{2}, rna"UAGUCGUAGUAG", 4, 3) + test_naive_spaced(RNAAlphabet{4}, dna"TAGCCWKMMNAGCTV", 2, 3) + + it = SpacedDNAMers{3, 4}("TAGAWWWW") + @test_throws BioSequences.EncodeError collect(it) + end + + @testset "Each codon" begin + for s in Any[ + "TAGCGATAT", + b"UAUGCUGAA", + dna"TAGGCTATA", + LongDNA{2}(dna"TAGCTAGAGGA"), + rna"UGAUUCGUUGA", + LongRNA{2}(rna"UAGUCGUGAGUA"), + ] + @test each_codon(DNA, s) === SpacedDNAMers{3, 3}(s) + @test each_codon(RNA, s) === SpacedRNAMers{3, 3}(s) + if s isa BioSequence{<:DNAAlphabet} + @test each_codon(s) === SpacedDNAMers{3, 3}(s) + elseif s isa BioSequence{<:RNAAlphabet} + @test each_codon(s) === SpacedRNAMers{3, 3}(s) + end + end + end +end + +@testset "StringViews" begin + s = StringView(collect(codeunits("ATGCTGATGATCGTATGATGTCGAAA"))) + it = FwRvIterator{DNAAlphabet{2}, 9}(s) + @test collect(it) == map(1:(length(s) - 8)) do i + a = DNAKmer{9}(s[i:(i + 8)]) + (a, reverse_complement(a)) + end +end + +@testset "fx_hash" begin + if UInt == UInt64 + for (kmer, h) in Any[ + (mer"TAG"a, 0x55dbbe22bb3e4a13), + (mer"KPWAK"a, 0x10203d1c885b7467), + (mer"TAGCTAG"d, 0xa76409341339d05a), + (mer""a, 0x0000000000000000), + (mer""r, 0x0000000000000000), + (mer"UGAUGCA"r, 0xdd7c97ae4ca204b4), + ] + @test fx_hash(kmer) === h + end + end +end + +@testset "Construction utils" begin + @testset "Unsafe extract" begin + seq = dna"TTGCTAGGGATTCGAGGATCCTCTAGAGCGCGGCACGATCTTAGCAC" + unsafe_extract = Kmers.unsafe_extract + @test unsafe_extract(Kmers.FourToTwo(), DNAKmer{6, 1}, seq, 3) == + DNAKmer{6}(seq[3:8]) + @test unsafe_extract(Kmers.FourToTwo(), DNAKmer{36, 2}, seq, 2) == + DNAKmer{36}(seq[2:37]) + + seq = LongDNA{2}(seq) + @test unsafe_extract(Kmers.TwoToFour(), Kmer{DNAAlphabet{4}, 6, 1}, seq, 3) == + Kmer{DNAAlphabet{4}, 6}(seq[3:8]) + @test unsafe_extract(Kmers.TwoToFour(), Kmer{DNAAlphabet{4}, 36, 3}, seq, 2) == + Kmer{DNAAlphabet{4}, 36}(seq[2:37]) + + @test unsafe_extract(Kmers.Copyable(), DNAKmer{6, 1}, seq, 3) == + DNAKmer{6}(seq[3:8]) + @test unsafe_extract(Kmers.Copyable(), DNAKmer{36, 2}, seq, 2) == + DNAKmer{36}(seq[2:37]) + + seq = codeunits(String(seq)) + @test unsafe_extract(Kmers.AsciiEncode(), DNAKmer{6, 1}, seq, 3) == + DNAKmer{6}(seq[3:8]) + @test unsafe_extract(Kmers.AsciiEncode(), DNAKmer{36, 2}, seq, 2) == + DNAKmer{36}(seq[2:37]) + + seq = LongSequence{CharAlphabet}("中国¨Å!人大æ网") + @test unsafe_extract(Kmers.GenericRecoding(), Kmer{CharAlphabet, 3, 2}, seq, 4) == + Kmer{CharAlphabet, 3}("Å!人") + end + + @testset "Unsafe shift from" begin + ushift = Kmers.unsafe_shift_from + + seq = dna"TTGCTAGGGATTCGAGGATCCTCTAGAGCGCGGCACGATCTTAGCAC" + mer = Kmer{DNAAlphabet{4}, 9}("TAGwKwADH") + @test ushift(Kmers.Copyable(), mer, seq, 4, Val(3)) == + Kmer{DNAAlphabet{4}, 9}("wKwADHCTA") + + mer = mer"TAGCATCG"d + @test ushift(Kmers.FourToTwo(), mer, seq, 4, Val(3)) == mer"CATCGCTA"d + + seq = LongDNA{2}(seq) + mer = Kmer{DNAAlphabet{4}, 9}("TAGwKwADH") + @test ushift(Kmers.TwoToFour(), mer, seq, 2, Val(3)) == + Kmer{DNAAlphabet{4}, 9}("wKwADHTGC") + + seq = codeunits(String(seq)) + mer = mer"KWPLCVAKVM"a + @test ushift(Kmers.AsciiEncode(), mer, seq, 5, Val(4)) == mer"CVAKVMTAGG"a + + seq = LongSequence{CharAlphabet}("中国¨Å!人大æ网") + mer = Kmer{CharAlphabet, 5, 3}("中国¨Å!") + @test ushift(Kmers.GenericRecoding(), mer, seq, 6, Val(3)) == + Kmer{CharAlphabet, 5, 3}("Å!人大æ") + end end end # module diff --git a/test/shuffle.jl b/test/shuffle.jl deleted file mode 100644 index 793081b..0000000 --- a/test/shuffle.jl +++ /dev/null @@ -1,25 +0,0 @@ -@testset "Shuffle" begin - for s in ["A", "C", "G", "T"] - kmer = DNAKmer(s) - @test kmer === shuffle(kmer) - end - - function count(kmer) - a = c = g = t = 0 - for x in kmer - a += x == DNA_A - c += x == DNA_C - g += x == DNA_G - t += x == DNA_T - end - return a, c, g, t - end - - for k in 1:64, _ in 1:10 - kmer = rand(DNAKmer{k}) - @test count(kmer) == count(shuffle(kmer)) - if k ≥ 30 - @test kmer != shuffle(kmer) - end - end -end diff --git a/test/transformations.jl b/test/transformations.jl deleted file mode 100644 index 1691688..0000000 --- a/test/transformations.jl +++ /dev/null @@ -1,60 +0,0 @@ -@testset "Transformations" begin - function test_reverse(T, seq) - revseq = reverse(T(seq)) - @test String(revseq) == reverse(seq) - end - - function test_dna_complement(T, seq) - comp = complement(T(seq)) - @test String(comp) == dna_complement(seq) - end - - function test_rna_complement(T, seq) - comp = complement(T(seq)) - @test String(comp) == rna_complement(seq) - end - - function test_dna_revcomp(T, seq) - revcomp = reverse_complement(T(seq)) - @test String(revcomp) == reverse(dna_complement(seq)) - end - - function test_rna_revcomp(T, seq) - revcomp = reverse_complement(T(seq)) - @test String(revcomp) == reverse(rna_complement(seq)) - end - - @testset "Reverse" begin - for len in 1:64, _ in 1:10 - test_reverse(DNAKmer{len}, random_dna_kmer(len)) - test_reverse(RNAKmer{len}, random_rna_kmer(len)) - end - - seq = dna"AAAAAAAAAAAAAAAAAAAAAAAAAAAAGATAC" - @test reverse(seq[(length(seq)-9):length(seq)]) == dna"CATAGAAAAA" - end - - @testset "Complement" begin - for len in 1:64, _ in 1:10 - test_dna_complement(DNAKmer{len}, random_dna_kmer(len)) - test_rna_complement(RNAKmer{len}, random_rna_kmer(len)) - end - end - - @testset "Reverse Complement" begin - for len in 1:64, _ in 1:10 - test_dna_revcomp(DNAKmer{len}, random_dna_kmer(len)) - test_rna_revcomp(RNAKmer{len}, random_rna_kmer(len)) - end - end - - @testset "Canonical" begin - @test canonical(DNAKmer{4,1}("ACCG")) == DNAKmer{4,1}("ACCG") - @test canonical(DNAKmer{4,1}("GCAC")) == DNAKmer{4,1}("GCAC") - @test canonical(RNAKmer{4,1}("AAUU")) == RNAKmer{4,1}("AAUU") - @test canonical(RNAKmer{4,1}("UGGA")) == RNAKmer{4,1}("UCCA") - @test canonical(RNAKmer{4,1}("CGAU")) == RNAKmer{4,1}("AUCG") - @test canonical(RNAKmer{4,1}("UGGA")) == RNAKmer{4,1}("UCCA") - @test canonical(DNAKmer{4,1}("GCAC")) == DNAKmer{4,1}("GCAC") - end -end diff --git a/test/translation.jl b/test/translation.jl index ccf34dc..38d8cdb 100644 --- a/test/translation.jl +++ b/test/translation.jl @@ -1,141 +1,138 @@ @testset "Translation" begin - -sampler = BioSequences.SamplerWeighted( - dna"ACGTMRSVWYHKDBN", - vcat(fill(0.225, 4), fill(0.00909, 10)) -) - -for A in (RNAAlphabet, DNAAlphabet) - for N in (2, 4) - for len in [3, 15, 33, 66] - for alternative in (true, false) - seq = if N == 2 - randseq(A{2}(), len) - else - randseq(A{4}(), sampler, len) + sampler = BioSequences.SamplerWeighted( + dna"ACGTMRSVWYHKDBN", + vcat(fill(0.225, 4), fill(0.00909, 10)), + ) + + for A in (RNAAlphabet, DNAAlphabet) + for N in (2, 4) + for len in [3, 15, 33, 66] + for alternative in (true, false) + seq = if N == 2 + randseq(A{2}(), len) + else + randseq(A{4}(), sampler, len) + end + kmer = Kmer{A{N}}(seq) + @test ( + translate(seq; alternative_start=alternative) == + translate(kmer; alternative_start=alternative) + ) end - kmer = Kmer{A{N}}(seq) - @test ( - translate(seq, alternative_start=alternative) == - translate(kmer, alternative_start=alternative) - ) end end end -end -# Throws when ambiguous -@test_throws Exception translate( - Kmer{RNAAlphabet{4}}("AUGCCGCMA"), - allow_ambiguous_codons=false -) + # Throws when ambiguous + @test_throws Exception translate( + Kmer{RNAAlphabet{4}}("AUGCCGCMA"), + allow_ambiguous_codons=false, + ) + + # Not divisible by 3 + @test_throws Exception translate(mer"UG"r) + @test_throws Exception translate(mer"TAGCTTAA"d) + @test_throws Exception translate(mer"CUGUAGUUGUCGC"r) + @test_throws Exception translate(mer"AGCGA"d) + + # Cannot transla AA seq + @test_throws MethodError translate(mer"LLVM"aa) + @test_throws MethodError translate(mer"ATG"aa) +end # translation + +@testset "CodonSet" begin + CodonSet = Kmers.CodonSet -# Not divisible by 3 -@test_throws Exception translate(mer"UG"r) -@test_throws Exception translate(mer"TAGCTTAA"d) -@test_throws Exception translate(mer"CUGUAGUUGUCGC"r) -@test_throws Exception translate(mer"AGCGA"d) + SAMPLE_SOURCES = Any[ + [mer"UAG"r, mer"ACC"r, mer"ACC"r, mer"UGG"r], + RNACodon[], + [mer"AAA"r, mer"ACC"r, mer"AAA"r, mer"UCA"r, mer"UCC"r], + (i for i in (mer"AGC"r, mer"AGA"r, mer"UUU"r)), + (mer"AAC"r, mer"AGG"r), + (mer"UUG"r,), + ] -# Cannot transla AA seq -@test_throws MethodError translate(mer"LLVM"aa) -@test_throws MethodError translate(mer"ATG"aa) + @testset "Construction and basics" begin + @test isempty(CodonSet()) -end # translation + # Constuct the sets and basic properties + for codons in SAMPLE_SOURCES + set = Set(codons) + codonset = CodonSet(codons) + @test issetequal(set, codonset) + @test length(codonset) == length(set) + end -@testset "CodonSet" begin -CodonSet = Kmers.CodonSet - -SAMPLE_SOURCES = Any[ - [mer"UAG"r, mer"ACC"r, mer"ACC"r, mer"UGG"r], - RNACodon[], - [mer"AAA"r, mer"ACC"r, mer"AAA"r, mer"UCA"r, mer"UCC"r], - (i for i in (mer"AGC"r, mer"AGA"r, mer"UUU"r)), - (mer"AAC"r, mer"AGG"r), - (mer"UUG"r,), -] - -@testset "Construction and basics" begin - @test isempty(CodonSet()) - - # Constuct the sets and basic properties - for codons in SAMPLE_SOURCES - set = Set(codons) - codonset = CodonSet(codons) - @test issetequal(set, codonset) - @test length(codonset) == length(set) + # Fails with non-codons + @test_throws MethodError CodonSet([(RNA_A, RNA_G)]) + @test_throws MethodError CodonSet((mer"UA"r,)) + @test_throws MethodError CodonSet([rna"AGG", rna"GGG"]) + @test_throws MethodError CodonSet([1, 2, 3]) end - # Fails with non-codons - @test_throws MethodError CodonSet([(RNA_A, RNA_G)]) - @test_throws MethodError CodonSet((mer"UA"r,)) - @test_throws MethodError CodonSet([rna"AGG", rna"GGG"]) - @test_throws MethodError CodonSet([1,2,3]) -end + SAMPLE_CODONSETS = map(CodonSet, SAMPLE_SOURCES) -SAMPLE_CODONSETS = map(CodonSet, SAMPLE_SOURCES) + @testset "Iteration" begin + for things in SAMPLE_SOURCES + @test sort!(collect(CodonSet(things))) == sort!(collect(Set(things))) + end -@testset "Iteration" begin - for things in SAMPLE_SOURCES - @test sort!(collect(CodonSet(things))) == sort!(collect(Set(things))) + @test iterate(CodonSet()) === nothing + codonset = CodonSet((mer"UUU"r,)) + codon, state = iterate(codonset) + @test codon == mer"UUU"r + @test iterate(codonset, state) === nothing end - @test iterate(CodonSet()) === nothing - codonset = CodonSet((mer"UUU"r,)) - codon, state = iterate(codonset) - @test codon == mer"UUU"r - @test iterate(codonset, state) === nothing -end - -@testset "Membership" begin - codonset = CodonSet([mer"ACC"r, mer"UAG"r, mer"UUU"r]) - @test mer"ACC"r in codonset - @test mer"UAG"r in codonset - @test mer"UUU"r in codonset - @test !in(mer"GAA"r, codonset) - @test !in(mer"AAA"r, codonset) -end - -@testset "Modifying" begin - # Push - s1 = CodonSet([mer"GGA"r, mer"UGU"r]) - s2 = push(s1, mer"GGA"r) - @test s1 == s2 - s3 = push(s2, mer"GAG"r) - @test Set(s3) == Set([mer"GGA"r, mer"UGU"r, mer"GGA"r, mer"GAG"r]) - - # Delete - s4 = delete(s3, mer"GAG"r) - @test s2 == s4 - s5 = delete(s4, mer"UGU"r) - @test only(s5) == mer"GGA"r - s6 = delete(s5, mer"UUU"r) - @test s5 == s6 - s7 = delete(s6, mer"GGA"r) - @test isempty(s7) -end - -@testset "Set operations" begin - for c1 in SAMPLE_CODONSETS, c2 in SAMPLE_CODONSETS - s1, s2 = Set(c1), Set(c2) - for operation in [union, intersect, setdiff, symdiff] - @test Set(operation(c1, c2)) == operation(s1, s2) - end - @test issubset(c1, c2) == issubset(s1, s2) + @testset "Membership" begin + codonset = CodonSet([mer"ACC"r, mer"UAG"r, mer"UUU"r]) + @test mer"ACC"r in codonset + @test mer"UAG"r in codonset + @test mer"UUU"r in codonset + @test !in(mer"GAA"r, codonset) + @test !in(mer"AAA"r, codonset) end -end - -@testset "Filter" begin - predicates = [ - (i -> i[2] == RNA_G), - (i -> isodd(length(i))), # always true for codons - (i -> i[1] == i[3]), - (i -> i[2] != RNA_A) - ] - for codonset in SAMPLE_CODONSETS, predicate in predicates - @test Set(filter(predicate, codonset)) == filter(predicate, Set(codonset)) + + @testset "Modifying" begin + # Push + s1 = CodonSet([mer"GGA"r, mer"UGU"r]) + s2 = push(s1, mer"GGA"r) + @test s1 == s2 + s3 = push(s2, mer"GAG"r) + @test Set(s3) == Set([mer"GGA"r, mer"UGU"r, mer"GGA"r, mer"GAG"r]) + + # Delete + s4 = delete(s3, mer"GAG"r) + @test s2 == s4 + s5 = delete(s4, mer"UGU"r) + @test only(s5) == mer"GGA"r + s6 = delete(s5, mer"UUU"r) + @test s5 == s6 + s7 = delete(s6, mer"GGA"r) + @test isempty(s7) end -end + @testset "Set operations" begin + for c1 in SAMPLE_CODONSETS, c2 in SAMPLE_CODONSETS + s1, s2 = Set(c1), Set(c2) + for operation in [union, intersect, setdiff, symdiff] + @test Set(operation(c1, c2)) == operation(s1, s2) + end + @test issubset(c1, c2) == issubset(s1, s2) + end + end + + @testset "Filter" begin + predicates = [ + (i -> i[2] == RNA_G), + (i -> isodd(length(i))), # always true for codons + (i -> i[1] == i[3]), + (i -> i[2] != RNA_A), + ] + for codonset in SAMPLE_CODONSETS, predicate in predicates + @test Set(filter(predicate, codonset)) == filter(predicate, Set(codonset)) + end + end end # CodonSet @testset "Reverse translation" begin @@ -143,7 +140,7 @@ end # CodonSet code2 = ReverseGeneticCode(BioSequences.trematode_mitochondrial_genetic_code) for (rvcode, fwcode) in [ (Kmers.rev_standard_genetic_code, BioSequences.standard_genetic_code), - (code2, BioSequences.trematode_mitochondrial_genetic_code) + (code2, BioSequences.trematode_mitochondrial_genetic_code), ] @test reverse_translate(aa"", rvcode) == CodonSet[] observed = Dict{AminoAcid, CodonSet}() @@ -155,7 +152,7 @@ end # CodonSet # Length and iteration of ReverseGeneticCode @test length(rvcode) == length(symbols(AminoAcidAlphabet())) - 1 # all but AA_Gap - @test sort!(collect(rvcode), by=first) == sort!(collect(observed), by=first) + @test sort!(collect(rvcode); by=first) == sort!(collect(observed); by=first) flipped = Dict(v => k for (k, v) in observed) for (codonset, aa) in flipped @@ -181,13 +178,34 @@ end # CodonSet (AA_J, [AA_I, AA_L]), (AA_Z, [AA_E, AA_Q]), (AA_B, [AA_D, AA_N]), - (AA_X, [ - AA_A, AA_R, AA_N, AA_D, AA_C, AA_Q, AA_E, AA_G, AA_H, AA_I, AA_L, AA_K, - AA_M, AA_F, AA_P, AA_S, AA_T, AA_W, AA_Y, AA_V - ]) - ] + ( + AA_X, + [ + AA_A, + AA_R, + AA_N, + AA_D, + AA_C, + AA_Q, + AA_E, + AA_G, + AA_H, + AA_I, + AA_L, + AA_K, + AA_M, + AA_F, + AA_P, + AA_S, + AA_T, + AA_W, + AA_Y, + AA_V, + ], + ), + ] c1 = only(reverse_translate(LongAA([ambig]), rvcode)) - c2 = foldl(elements, init=CodonSet()) do old, aa + c2 = foldl(elements; init=CodonSet()) do old, aa union(old, reverse_translate(aa, rvcode)) end @test c1 == c2 diff --git a/test/utils.jl b/test/utils.jl index a2d74b3..2373053 100644 --- a/test/utils.jl +++ b/test/utils.jl @@ -1,5 +1,9 @@ +function random_seq(A::Alphabet, n::Integer) + randseq(A, SamplerUniform(symbols(A)), n) +end + # Return a random DNA/RNA sequence of the given length. -function random_seq(n::Integer, nts, probs, outtype = String) +function random_seq(n::Integer, nts, probs, outtype=String) cumprobs = cumsum(probs) x = Vector{Char}(undef, n) for i in 1:n @@ -8,8 +12,7 @@ function random_seq(n::Integer, nts, probs, outtype = String) return outtype(x) end -function random_seq(::Type{A}, n::Integer) where {A<:Alphabet} - # TODO: Resolve the use of symbols(A()). +function random_seq(::Type{A}, n::Integer) where {A <: Alphabet} nts = symbols(A()) probs = Vector{Float64}(undef, length(nts)) fill!(probs, 1 / length(nts)) @@ -25,10 +28,33 @@ function random_rna(n, probs=[0.24, 0.24, 0.24, 0.24, 0.04]) end function random_aa(len) - return random_seq(len, - ['A', 'R', 'N', 'D', 'C', 'Q', 'E', 'G', 'H', 'I', - 'L', 'K', 'M', 'F', 'P', 'S', 'T', 'W', 'Y', 'V', 'X' ], - push!(fill(0.049, 20), 0.02)) + return random_seq( + len, + [ + 'A', + 'R', + 'N', + 'D', + 'C', + 'Q', + 'E', + 'G', + 'H', + 'I', + 'L', + 'K', + 'M', + 'F', + 'P', + 'S', + 'T', + 'W', + 'Y', + 'V', + 'X', + ], + push!(fill(0.049, 20), 0.02), + ) end function random_dna_symbols(n, probs=[0.24, 0.24, 0.24, 0.24, 0.04]) @@ -39,13 +65,35 @@ function random_rna_symbols(n, probs=[0.24, 0.24, 0.24, 0.24, 0.04]) return random_seq(n, ['A', 'C', 'G', 'U', 'N'], probs, Vector{RNA}) end -function random_rna_symbols(n, probs=[0.24, 0.24, 0.24, 0.24, 0.04]) - return random_seq(n, ['A', 'C', 'G', 'U', 'N'], probs, Vector{RNA}) -end - function random_aa_symbols(n, probs=[0.24, 0.24, 0.24, 0.24, 0.04]) - return random_seq(n, ['A', 'R', 'N', 'D', 'C', 'Q', 'E', 'G', 'H', 'I', - 'L', 'K', 'M', 'F', 'P', 'S', 'T', 'W', 'Y', 'V', 'X' ], probs, Vector{AminoAcid}) + return random_seq( + n, + [ + 'A', + 'R', + 'N', + 'D', + 'C', + 'Q', + 'E', + 'G', + 'H', + 'I', + 'L', + 'K', + 'M', + 'F', + 'P', + 'S', + 'T', + 'W', + 'Y', + 'V', + 'X', + ], + probs, + Vector{AminoAcid}, + ) end function random_dna_kmer(len) @@ -72,4 +120,4 @@ function rna_complement(seq::AbstractString) seqc[i] = complementer[c] end return String(seqc) -end \ No newline at end of file +end