From 0012a55cad9ce17caa5b1a9b0fb046726b01ab0d Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Mon, 2 Nov 2026 16:52:24 +1100 Subject: [PATCH 01/24] Refactor batch iterator filtering and update project configuration Updates project settings such as the name, activation timeout, and supported languages. Modifies the FilterOn method in the batch iterator to handle sequence filtering and pushing to the iterator. --- .serena/project.yml | 130 ++++++++++++++++++++++------------- pkg/obiiter/batchiterator.go | 3 +- 2 files changed, 85 insertions(+), 48 deletions(-) diff --git a/.serena/project.yml b/.serena/project.yml index f09dc2a..4674942 100644 --- a/.serena/project.yml +++ b/.serena/project.yml @@ -1,39 +1,6 @@ -# the name by which the project can be referenced within Serena +# the name by which the project can be referenced within Serena/when chatting with the LLM. project_name: "obitools4" - -# list of languages for which language servers are started; choose from: -# al angular ansible bash clojure -# cpp cpp_ccls crystal csharp csharp_omnisharp -# dart elixir elm erlang fortran -# fsharp go groovy haskell haxe -# hlsl html java json julia -# kotlin lean4 lua luau markdown -# matlab msl nix ocaml pascal -# perl php php_phpactor powershell python -# python_jedi python_ty r rego ruby -# ruby_solargraph rust scala scss solidity -# svelte swift systemverilog terraform toml -# typescript typescript_vts vue yaml zig -# (This list may be outdated. For the current list, see values of Language enum here: -# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py -# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.) -# Note: -# - For C, use cpp -# - For JavaScript, use typescript -# - For Angular projects, use angular (subsumes typescript+html; requires `npm install` in the project root) -# - For Svelte projects, use svelte (subsumes typescript/javascript for .svelte projects; requires npm) -# - For SCSS / Sass / plain CSS, use scss (some-sass-language-server handles all three) -# - For Free Pascal/Lazarus, use pascal -# Special requirements: -# Some languages require additional setup/installations. -# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers -# When using multiple languages, the first language server that supports a given file will be used for that file. -# The first language is the default language and the respective language server will be used as a fallback. -# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored. -languages: -- go - # the encoding used by text files in the project # For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings encoding: "utf-8" @@ -55,23 +22,19 @@ ignore_all_files_in_gitignore: true # advanced configuration option allowing to configure language server-specific options. # Maps the language key to the options. -# Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available. -# No documentation on options means no options are available. +# The settings are considered only if the project is trusted (see global configuration to define trusted projects). +# See https://oraios.github.io/serena/02-usage/050_configuration.html#language-server-specific-settings ls_specific_settings: {} -# list of additional workspace folder paths for cross-package reference support (e.g. in monorepos). -# Paths can be absolute or relative to the project root. -# Each folder is registered as an LSP workspace folder, enabling language servers to discover -# symbols and references across package boundaries. -# Currently supported for: TypeScript. -# Example: -# additional_workspace_folders: -# - ../sibling-package -# - ../shared-lib -additional_workspace_folders: [] - # list of additional paths to ignore in this project. # Same syntax as gitignore, so you can use * and **. +# Important: quote patterns that start with `*`, otherwise YAML treats them as aliases. +# Example: +# ignored_paths: +# - "examples/**" +# - ".worktrees/**" +# - "**/bin/**" +# - "**/obj/**" # Note: global ignored_paths from serena_config.yml are also applied additively. ignored_paths: [] @@ -131,3 +94,76 @@ read_only_memory_patterns: [] # Extends the list from the global configuration, merging the two lists. # Example: ["_archive/.*", "_episodes/.*"] ignored_memory_patterns: [] + +# list of additional workspace folder paths for cross-package reference support. +# Paths can be absolute or relative to the project root. +# Each folder is registered as an LSP workspace folder, enabling language servers to discover +# symbols and references across package boundaries, but these folders are not indexed by Serena, +# i.e. the respective symbols will not be found using Serena's symbol search tools. +# Example: +# additional_workspace_folders: +# - ../sibling-package +# - ../shared-lib +ls_additional_workspace_folders: [] + +# list of language servers to start when using the LSP backend; choose from: +# ada al angular ansible bash +# bsl clojure cpp cpp_ccls crystal +# csharp csharp_omnisharp cue dart deno +# elixir elm erlang fortran fsharp +# gdscript gleam go groovy haskell +# haxe hlsl html java json +# julia kotlin latex lean4 lua +# luau markdown matlab msl nextflow +# nix ocaml pascal perl php +# php_phpactor php_phpantom powershell python python_basedpyright +# python_jedi python_pyrefly python_ty qml r +# rego ruby ruby_solargraph rust scala +# scss solidity svelte swift systemverilog +# terraform toml typescript typescript_vts vue +# wolfram yaml zig +# (This list may be outdated; generated with scripts/print_language_list.py; +# For the current list, see values of the LanguageServerId enum here: +# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py) +# For some languages, there are several alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.) +# Note: +# - For C, use cpp +# - For JavaScript, use typescript +# - For Angular projects, use angular (subsumes typescript+html; requires `npm install` in the project root) +# - For Svelte projects, use svelte (subsumes typescript/javascript for .svelte projects; requires npm) +# - For Deno projects, use deno (serves the same .ts/.js files as typescript; requires the deno CLI on PATH) +# - For SCSS / Sass / plain CSS, use scss (some-sass-language-server handles all three) +# - For Free Pascal/Lazarus, use pascal +# Special requirements: +# Some language servers require additional setup/installations. +# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers +# When using multiple language servers, the first language server that supports a given file will be used for that file. +# The first language server is the default language and the respective language server will be used as a fallback. +# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored. +language_servers: +- go + +# list of workspace folder paths (LSP backend only). +# These folders will be used to build up Serena's symbol index. +# Paths must be within the project root and should thus be relative to the project root. +# Furthermore, the paths should not be filtered by ignore settings. +# Default setting: The entire project root folder (".") is considered. +# In (large) monorepos, this can be used to index only subfolders of the project root, e.g. +# ls_workspace_folders: +# - "./subproject1" +# - "./subproject2" +ls_workspace_folders: +- . + +# optional shell command to run before the language backend (LSP or JetBrains) is initialised. +# the command runs in the project root directory and is only executed if the project is trusted +# (see trusted_project_path_patterns in the global configuration). +# serena waits for the command to exit: a non-zero exit code is logged as an error but does not +# abort activation. a per-project timeout (activation_command_timeout, default 180s) is the safety +# backstop for non-terminating commands; on expiry the process is killed and activation continues. +# example: activation_command: "npx nx run-many -t build" +activation_command: + +# maximum time in seconds to wait for activation_command to complete before killing it (default 180s). +# must be a positive number. +activation_command_timeout: 180.0 diff --git a/pkg/obiiter/batchiterator.go b/pkg/obiiter/batchiterator.go index e7d51a1..606fd68 100644 --- a/pkg/obiiter/batchiterator.go +++ b/pkg/obiiter/batchiterator.go @@ -683,7 +683,8 @@ func (iterator IBioSequence) FilterOn(predicate obiseq.SequencePredicate, } seqs.slice = slice[:j] - trueIter.pointer.channel <- seqs + trueIter.Push(seqs) + // trueIter.pointer.channel <- seqs } trueIter.Done() From a0d8187fd61b290929ea6b61a51cbd58efeed402 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Mon, 2 Nov 2026 20:59:48 +1100 Subject: [PATCH 02/24] Implement k-mer distance metrics and constrained generation Adds support for Hamming distance, constrained k-mer generation, and optimized Longest Common Subsequence (LCS) calculations using scalar, batch, and SIMD methods. This includes new methods for k-mer distance calculation, structures for k-mer constraints, and SIMD-based batch processing for LCS to improve performance and correctness. --- pkg/obikmer/debruijn.go | 9 -- pkg/obikmer/enumkmer.go | 91 ++++++++++++++++ pkg/obikmer/enumkmer_test.go | 85 +++++++++++++++ pkg/obikmer/kmer_distance.go | 104 ++++++++++++++++++ pkg/obikmer/kmer_distance_test.go | 112 ++++++++++++++++++++ pkg/obikmer/kmer_lcs_archsimd_arm64.go | 62 +++++++++++ pkg/obikmer/kmer_lcs_archsimd_arm64_test.go | 31 ++++++ pkg/obikmer/kmer_lcs_obialign_test.go | 29 +++++ pkg/obikmer/kmer_lcs_scalar.go | 7 ++ pkg/obikmer/kmer_lcs_simd.go | 80 ++++++++++++++ pkg/obitools/obicsv/options.go | 10 -- 11 files changed, 601 insertions(+), 19 deletions(-) create mode 100644 pkg/obikmer/enumkmer.go create mode 100644 pkg/obikmer/enumkmer_test.go create mode 100644 pkg/obikmer/kmer_distance.go create mode 100644 pkg/obikmer/kmer_distance_test.go create mode 100644 pkg/obikmer/kmer_lcs_archsimd_arm64.go create mode 100644 pkg/obikmer/kmer_lcs_archsimd_arm64_test.go create mode 100644 pkg/obikmer/kmer_lcs_obialign_test.go create mode 100644 pkg/obikmer/kmer_lcs_scalar.go create mode 100644 pkg/obikmer/kmer_lcs_simd.go diff --git a/pkg/obikmer/debruijn.go b/pkg/obikmer/debruijn.go index a707ca7..c2e8a9e 100644 --- a/pkg/obikmer/debruijn.go +++ b/pkg/obikmer/debruijn.go @@ -5,7 +5,6 @@ import ( "container/heap" "fmt" "math" - "math/bits" "os" "slices" @@ -610,14 +609,6 @@ func (graph *DeBruijnGraph) WriteGml(filename string) error { return err } -// Calculating the hamming distance between two k-mers. -func (g *DeBruijnGraph) HammingDistance(kmer1, kmer2 uint64) int { - ident := ^((kmer1 & kmer2) | (^kmer1 & ^kmer2)) - ident |= (ident >> 1) - ident &= 0x5555555555555555 & g.kmermask - return bits.OnesCount64(ident) -} - type UInt64Heap []uint64 func (h UInt64Heap) Len() int { return len(h) } diff --git a/pkg/obikmer/enumkmer.go b/pkg/obikmer/enumkmer.go new file mode 100644 index 0000000..678bf08 --- /dev/null +++ b/pkg/obikmer/enumkmer.go @@ -0,0 +1,91 @@ +package obikmer + +import "iter" + +// KmerConstraints describes lexicographic properties a k-mer must satisfy. +// +// MaxCount is indexed by the 2-bit base code (A=0, C=1, G=2, T=3): +// a negative value means unlimited, 0 forbids the base altogether +// (so MaxCount[2] = 0 restricts the alphabet to A, C, T). +// +// MaxRun is the maximum number of consecutive identical bases; +// a value <= 0 means unlimited. +// +// The zero value forbids every base: use NoKmerConstraints as a starting point. +type KmerConstraints struct { + MaxCount [4]int + MaxRun int +} + +// NoKmerConstraints returns constraints accepting every k-mer. +func NoKmerConstraints() KmerConstraints { + return KmerConstraints{MaxCount: [4]int{-1, -1, -1, -1}} +} + +// IterConstrainedKmers returns an iterator over all k-mers of size k +// satisfying the constraints c, encoded as in EncodeKmer, in increasing +// numerical order. +// +// K-mers are built base by base and a branch is abandoned as soon as a +// constraint is violated (both MaxCount and MaxRun are monotone: a violating +// prefix cannot be repaired by extension), so the cost is proportional to the +// number of explored valid prefixes, not to 4^k. +// +// k must be between 1 and 31, otherwise the iterator is empty. +// +// Example: +// +// c := NoKmerConstraints() +// c.MaxCount[2] = 0 // no G +// c.MaxRun = 3 +// for kmer := range IterConstrainedKmers(8, c) { ... } +func IterConstrainedKmers(k int, c KmerConstraints) iter.Seq[uint64] { + return func(yield func(uint64) bool) { + if k < 1 || k > 31 { + return + } + + // Nothing to enumerate if the per-base caps cannot even sum to k. + capacity := 0 + for _, m := range c.MaxCount { + if m < 0 || m >= k { + capacity += k + } else { + capacity += m + } + } + if capacity < k { + return + } + + var counts [4]int + + var walk func(depth int, kmer uint64, last, run int) bool + walk = func(depth int, kmer uint64, last, run int) bool { + if depth == k { + return yield(kmer) + } + for b := range 4 { + if c.MaxCount[b] >= 0 && counts[b] >= c.MaxCount[b] { + continue + } + r := 1 + if b == last { + r = run + 1 + } + if c.MaxRun > 0 && r > c.MaxRun { + continue + } + counts[b]++ + ok := walk(depth+1, kmer<<2|uint64(b), b, r) + counts[b]-- + if !ok { + return false + } + } + return true + } + + walk(0, 0, -1, 0) + } +} diff --git a/pkg/obikmer/enumkmer_test.go b/pkg/obikmer/enumkmer_test.go new file mode 100644 index 0000000..d2d528d --- /dev/null +++ b/pkg/obikmer/enumkmer_test.go @@ -0,0 +1,85 @@ +package obikmer + +import ( + "strings" + "testing" +) + +func collectConstrained(k int, c KmerConstraints) []uint64 { + var out []uint64 + for km := range IterConstrainedKmers(k, c) { + out = append(out, km) + } + return out +} + +func TestIterConstrainedKmersUnconstrained(t *testing.T) { + got := collectConstrained(4, NoKmerConstraints()) + if len(got) != 256 { + t.Fatalf("expected 256 k-mers, got %d", len(got)) + } + for i, km := range got { + if km != uint64(i) { + t.Fatalf("not in increasing order at %d: %d", i, km) + } + } +} + +func TestIterConstrainedKmersNoG(t *testing.T) { + c := NoKmerConstraints() + c.MaxCount[2] = 0 + got := collectConstrained(5, c) + if len(got) != 243 { + t.Fatalf("expected 3^5 = 243 k-mers, got %d", len(got)) + } + for _, km := range got { + if strings.Contains(string(DecodeKmer(km, 5, nil)), "g") { + t.Fatalf("k-mer %d contains G", km) + } + } +} + +func TestIterConstrainedKmersMaxRun(t *testing.T) { + c := NoKmerConstraints() + c.MaxRun = 2 + // a(n) = 3*(a(n-1)+a(n-2)), a(1)=4, a(2)=16 + want := map[int]int{3: 60, 4: 228} + for k, n := range want { + if got := len(collectConstrained(k, c)); got != n { + t.Fatalf("k=%d: expected %d, got %d", k, n, got) + } + } +} + +func TestIterConstrainedKmersMaxCount(t *testing.T) { + c := NoKmerConstraints() + c.MaxCount[0] = 1 // at most one A + for _, km := range collectConstrained(6, c) { + if strings.Count(string(DecodeKmer(km, 6, nil)), "a") > 1 { + t.Fatalf("k-mer %d has more than one A", km) + } + } +} + +func TestIterConstrainedKmersImpossible(t *testing.T) { + c := KmerConstraints{MaxCount: [4]int{1, 1, 1, 0}} + if got := collectConstrained(4, c); len(got) != 0 { + t.Fatalf("expected no k-mer, got %d", len(got)) + } + if got := collectConstrained(0, NoKmerConstraints()); len(got) != 0 { + t.Fatalf("expected no k-mer for k=0") + } +} + +func TestIterConstrainedKmersEarlyStop(t *testing.T) { + n := 0 + for range IterConstrainedKmers(10, NoKmerConstraints()) { + n++ + if n == 5 { + break + } + } + if n != 5 { + t.Fatalf("early stop failed: %d", n) + } +} diff --git a/pkg/obikmer/kmer_distance.go b/pkg/obikmer/kmer_distance.go new file mode 100644 index 0000000..8d17644 --- /dev/null +++ b/pkg/obikmer/kmer_distance.go @@ -0,0 +1,104 @@ +package obikmer + +import "math/bits" + +// KmerLCSResult is the result of a k-mer LCS alignment: the length of the +// Longest Common Subsequence and the length of the shortest alignment +// achieving it (same semantics as obialign.FastLCSScore). +type KmerLCSResult struct { + LCS int + AlignLen int +} + +// Distance returns AlignLen - LCS, the LCS-mode edit distance. +func (r KmerLCSResult) Distance() int { + return r.AlignLen - r.LCS +} + +// HammingDistance returns the number of positions at which the two k-mers +// differ. Both k-mers are encoded with 2 bits per base (see EncodeKmer); +// bits above the 2*k lowest ones are ignored. k must be between 0 and 32. +func HammingDistance(a, b uint64, k int) int { + diff := a ^ b + diff = (diff | diff>>1) & 0x5555555555555555 + diff &= ^uint64(0) >> (64 - 2*k) + return bits.OnesCount64(diff) +} + +// KmerLCS aligns two k-mers of the same size in LCS mode (match +1, mismatch +// and indel 0) and returns the LCS length together with the length of the +// shortest alignment among those reaching it, so that the distance is +// AlignLen - LCS. +// +// It is the scalar reference implementation, straightforward on purpose: +// it is the oracle against which the vectorised versions are tested. +// It returns {-1, -1} if k is not between 1 and 31. +func KmerLCS(a, b uint64, k int) KmerLCSResult { + if k < 1 || k > 31 { + return KmerLCSResult{-1, -1} + } + + var ba, bb [32]uint8 + for i := k - 1; i >= 0; i-- { + ba[i] = uint8(a & 3) + bb[i] = uint8(b & 3) + a >>= 2 + b >>= 2 + } + + // One DP row: best (score, alignment length) per column. + var score, length [32]int + for j := 0; j <= k; j++ { + length[j] = j + } + + better := func(s1, l1, s2, l2 int) bool { + return s1 > s2 || (s1 == s2 && l1 < l2) + } + + for i := 1; i <= k; i++ { + diagS, diagL := score[0], length[0] + score[0], length[0] = 0, i + + for j := 1; j <= k; j++ { + upS, upL := score[j], length[j] + + bestS, bestL := diagS, diagL+1 + if ba[j-1] == bb[i-1] { + bestS++ + } + if better(upS, upL+1, bestS, bestL) { + bestS, bestL = upS, upL+1 + } + if better(score[j-1], length[j-1]+1, bestS, bestL) { + bestS, bestL = score[j-1], length[j-1]+1 + } + + diagS, diagL = upS, upL + score[j], length[j] = bestS, bestL + } + } + + return KmerLCSResult{score[k], length[k]} +} + +// KmerLCSBatch computes KmerLCS(query, partners[i], k) for every partner and +// stores it in out[i]. out must be at least as long as partners. +// +// When built with GOEXPERIMENT=simd the partners are aligned several at a +// time, one per SIMD lane; otherwise the scalar reference is used. +// It returns false, leaving out untouched, if k is not between 1 and 31 or +// if out is too short. +func KmerLCSBatch(query uint64, partners []uint64, k int, out []KmerLCSResult) bool { + if k < 1 || k > 31 || len(out) < len(partners) { + return false + } + kmerLCSBatch(query, partners, k, out) + return true +} + +func kmerLCSBatchScalar(query uint64, partners []uint64, k int, out []KmerLCSResult) { + for i, p := range partners { + out[i] = KmerLCS(query, p, k) + } +} diff --git a/pkg/obikmer/kmer_distance_test.go b/pkg/obikmer/kmer_distance_test.go new file mode 100644 index 0000000..a46835a --- /dev/null +++ b/pkg/obikmer/kmer_distance_test.go @@ -0,0 +1,112 @@ +package obikmer + +import ( + "math/rand" + "testing" +) + +func randomKmer(rng *rand.Rand, k int) uint64 { + return rng.Uint64() & (uint64(1)<<(2*uint(k)) - 1) +} + +func TestHammingDistance(t *testing.T) { + rng := rand.New(rand.NewSource(1)) + for _, k := range []int{1, 2, 7, 21, 31} { + for n := 0; n < 200; n++ { + a, b := randomKmer(rng, k), randomKmer(rng, k) + sa, sb := DecodeKmer(a, k, nil), DecodeKmer(b, k, nil) + want := 0 + for i := range sa { + if sa[i] != sb[i] { + want++ + } + } + if got := HammingDistance(a, b, k); got != want { + t.Fatalf("k=%d %s/%s: got %d, want %d", k, sa, sb, got, want) + } + } + } +} + +func TestHammingDistanceIgnoresHighBits(t *testing.T) { + if got := HammingDistance(0xFF00, 0x0000, 4); got != 0 { + t.Fatalf("bits above 2k must be ignored, got %d", got) + } +} + +func TestKmerLCSKnown(t *testing.T) { + enc := func(s string) uint64 { return EncodeKmer([]byte(s), len(s)) } + cases := []struct { + a, b string + want KmerLCSResult + }{ + {"ACGT", "ACGT", KmerLCSResult{4, 4}}, + {"AAAA", "CCCC", KmerLCSResult{0, 4}}, + {"ACGT", "ACGA", KmerLCSResult{3, 4}}, + {"ACGT", "CGTA", KmerLCSResult{3, 5}}, // one shift: 1 indel on each side + } + for _, c := range cases { + if got := KmerLCS(enc(c.a), enc(c.b), 4); got != c.want { + t.Errorf("%s/%s: got %+v, want %+v", c.a, c.b, got, c.want) + } + } + if got := KmerLCS(0, 0, 32); got.LCS != -1 { + t.Errorf("k=32 must be rejected, got %+v", got) + } +} + +// The batch implementation (SIMD when built with GOEXPERIMENT=simd) must agree +// with the scalar reference for all k and for batch sizes around the lane count. +func TestKmerLCSBatchMatchesReference(t *testing.T) { + rng := rand.New(rand.NewSource(2)) + for _, k := range []int{1, 2, 3, 5, 8, 16, 21, 30, 31} { + for _, n := range []int{0, 1, 3, 7, 8, 9, 16, 17, 100} { + query := randomKmer(rng, k) + partners := make([]uint64, n) + for i := range partners { + partners[i] = randomKmer(rng, k) + } + if n > 2 { + partners[0] = query // identical + } + out := make([]KmerLCSResult, n) + if !KmerLCSBatch(query, partners, k, out) { + t.Fatalf("k=%d n=%d: batch refused", k, n) + } + for i, p := range partners { + if want := KmerLCS(query, p, k); out[i] != want { + t.Fatalf("k=%d n=%d partner %d: got %+v, want %+v", k, n, i, out[i], want) + } + } + } + } +} + +func TestKmerLCSBatchRejectsBadInput(t *testing.T) { + if KmerLCSBatch(0, []uint64{1}, 0, make([]KmerLCSResult, 1)) { + t.Fatal("k=0 must be rejected") + } + if KmerLCSBatch(0, []uint64{1, 2}, 4, make([]KmerLCSResult, 1)) { + t.Fatal("short output must be rejected") + } +} + +func benchBatch(b *testing.B, k int, f func(uint64, []uint64, int, []KmerLCSResult)) { + rng := rand.New(rand.NewSource(3)) + partners := make([]uint64, 1024) + for i := range partners { + partners[i] = randomKmer(rng, k) + } + out := make([]KmerLCSResult, len(partners)) + query := randomKmer(rng, k) + b.ResetTimer() + for b.Loop() { + f(query, partners, k, out) + } + b.ReportMetric(float64(b.Elapsed().Nanoseconds())/float64(b.N*len(partners)), "ns/pair") +} + +func BenchmarkKmerLCSScalar21(b *testing.B) { benchBatch(b, 21, kmerLCSBatchScalar) } +func BenchmarkKmerLCSBatch21(b *testing.B) { benchBatch(b, 21, kmerLCSBatch) } +func BenchmarkKmerLCSScalar11(b *testing.B) { benchBatch(b, 11, kmerLCSBatchScalar) } +func BenchmarkKmerLCSBatch11(b *testing.B) { benchBatch(b, 11, kmerLCSBatch) } diff --git a/pkg/obikmer/kmer_lcs_archsimd_arm64.go b/pkg/obikmer/kmer_lcs_archsimd_arm64.go new file mode 100644 index 0000000..3438d24 --- /dev/null +++ b/pkg/obikmer/kmer_lcs_archsimd_arm64.go @@ -0,0 +1,62 @@ +//go:build goexperiment.simd && arm64 + +package obikmer + +import "simd/archsimd" + +// neonLanes is the number of 16-bit lanes of a NEON 128-bit register. +const neonLanes = 8 + +// kmerLCSBatchNEON is kmerLCSBatch written directly on archsimd NEON types +// (see kmer_lcs_simd.go for the cell encoding and recurrence). +func kmerLCSBatchNEON(query uint64, partners []uint64, k int, out []KmerLCSResult) { + var qbase [32]uint16 + for i := 0; i < k; i++ { + qbase[i] = uint16(query>>(2*uint(k-1-i))) & 3 + } + + pbase := make([]uint16, k*neonLanes) + row := make([]uint16, (k+1)*neonLanes) + one := archsimd.BroadcastUint16x8(1) + bonus := archsimd.BroadcastUint16x8(lcsScoreUnit) + + for start := 0; start < len(partners); start += neonLanes { + chunk := partners[start:min(start+neonLanes, len(partners))] + + for lane, p := range chunk { + for j := 0; j < k; j++ { + pbase[j*neonLanes+lane] = uint16(p>>(2*uint(k-1-j))) & 3 + } + } + + for j := 0; j <= k; j++ { + archsimd.BroadcastUint16x8(uint16(lcsLenBase - j)).Store(row[j*neonLanes:]) + } + + for i := 1; i <= k; i++ { + qv := archsimd.BroadcastUint16x8(qbase[i-1]) + diag := archsimd.LoadUint16x8(row) + left := archsimd.BroadcastUint16x8(uint16(lcsLenBase - i)) + left.Store(row) + + for j := 1; j <= k; j++ { + up := archsimd.LoadUint16x8(row[j*neonLanes:]) + pv := archsimd.LoadUint16x8(pbase[(j-1)*neonLanes:]) + match := qv.Equal(pv) + best := diag.Add(bonus.Masked(match)).Max(up).Max(left).Sub(one) + best.Store(row[j*neonLanes:]) + diag = up + left = best + } + } + + last := row[k*neonLanes:] + for lane := range chunk { + v := int(last[lane]) + out[start+lane] = KmerLCSResult{ + LCS: v / lcsScoreUnit, + AlignLen: lcsLenBase - v%lcsScoreUnit, + } + } + } +} diff --git a/pkg/obikmer/kmer_lcs_archsimd_arm64_test.go b/pkg/obikmer/kmer_lcs_archsimd_arm64_test.go new file mode 100644 index 0000000..1210b94 --- /dev/null +++ b/pkg/obikmer/kmer_lcs_archsimd_arm64_test.go @@ -0,0 +1,31 @@ +//go:build goexperiment.simd && arm64 + +package obikmer + +import ( + "math/rand" + "testing" +) + +func TestKmerLCSBatchNEONMatchesReference(t *testing.T) { + rng := rand.New(rand.NewSource(5)) + for _, k := range []int{1, 2, 5, 11, 21, 31} { + for _, n := range []int{0, 1, 7, 8, 9, 100} { + query := randomKmer(rng, k) + partners := make([]uint64, n) + for i := range partners { + partners[i] = randomKmer(rng, k) + } + out := make([]KmerLCSResult, n) + kmerLCSBatchNEON(query, partners, k, out) + for i, p := range partners { + if want := KmerLCS(query, p, k); out[i] != want { + t.Fatalf("k=%d n=%d partner %d: got %+v, want %+v", k, n, i, out[i], want) + } + } + } + } +} + +func BenchmarkKmerLCSNEON21(b *testing.B) { benchBatch(b, 21, kmerLCSBatchNEON) } +func BenchmarkKmerLCSNEON11(b *testing.B) { benchBatch(b, 11, kmerLCSBatchNEON) } diff --git a/pkg/obikmer/kmer_lcs_obialign_test.go b/pkg/obikmer/kmer_lcs_obialign_test.go new file mode 100644 index 0000000..3a5eba6 --- /dev/null +++ b/pkg/obikmer/kmer_lcs_obialign_test.go @@ -0,0 +1,29 @@ +package obikmer_test + +import ( + "math/rand" + "testing" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obialign" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obikmer" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq" +) + +// KmerLCS must reproduce obialign.FastLCSScore on the decoded sequences. +func TestKmerLCSMatchesFastLCSScore(t *testing.T) { + rng := rand.New(rand.NewSource(4)) + for _, k := range []int{1, 2, 4, 9, 15, 21, 31} { + mask := uint64(1)<<(2*uint(k)) - 1 + for n := 0; n < 300; n++ { + a, b := rng.Uint64()&mask, rng.Uint64()&mask + sa := obiseq.NewBioSequence("a", obikmer.DecodeKmer(a, k, nil), "") + sb := obiseq.NewBioSequence("b", obikmer.DecodeKmer(b, k, nil), "") + lcs, alen := obialign.FastLCSScore(sa, sb, -1, nil) + got := obikmer.KmerLCS(a, b, k) + if got.LCS != lcs || got.AlignLen != alen { + t.Fatalf("k=%d %s/%s: KmerLCS=%+v, FastLCSScore=(%d,%d)", + k, sa.String(), sb.String(), got, lcs, alen) + } + } + } +} diff --git a/pkg/obikmer/kmer_lcs_scalar.go b/pkg/obikmer/kmer_lcs_scalar.go new file mode 100644 index 0000000..831a131 --- /dev/null +++ b/pkg/obikmer/kmer_lcs_scalar.go @@ -0,0 +1,7 @@ +//go:build !goexperiment.simd + +package obikmer + +func kmerLCSBatch(query uint64, partners []uint64, k int, out []KmerLCSResult) { + kmerLCSBatchScalar(query, partners, k, out) +} diff --git a/pkg/obikmer/kmer_lcs_simd.go b/pkg/obikmer/kmer_lcs_simd.go new file mode 100644 index 0000000..6127c62 --- /dev/null +++ b/pkg/obikmer/kmer_lcs_simd.go @@ -0,0 +1,80 @@ +//go:build goexperiment.simd + +package obikmer + +import "simd" + +// Each lane of a vector holds the state of one (query, partner) alignment as +// +// score<<6 | (63 - alignment length) +// +// so that taking the maximum of two cells ranks them by score first and, for +// equal scores, by shortest alignment. With k <= 31 the score is <= 31 and the +// alignment length <= 62, hence the value fits in 11 bits and the low field +// is never exhausted. The three DP moves then all reduce to +// +// cell(i,j) = max(diag + 64*match, up, left) - 1 +// +// (every move lengthens the alignment by one; only a diagonal match adds 1 +// to the score). +const ( + lcsScoreUnit = 64 + lcsLenBase = 63 +) + +func kmerLCSBatch(query uint64, partners []uint64, k int, out []KmerLCSResult) { + var probe simd.Uint16s + lanes := probe.Len() + + var qbase [32]uint16 + for i := 0; i < k; i++ { + qbase[i] = uint16(query>>(2*uint(k-1-i))) & 3 + } + + // partner bases, transposed: pbase[j*lanes+lane] is base j of the partner in `lane` + pbase := make([]uint16, k*lanes) + // one DP row, one vector per column + row := make([]uint16, (k+1)*lanes) + one := simd.BroadcastUint16s(1) + bonus := simd.BroadcastUint16s(lcsScoreUnit) + + for start := 0; start < len(partners); start += lanes { + chunk := partners[start:min(start+lanes, len(partners))] + + for lane, p := range chunk { + for j := 0; j < k; j++ { + pbase[j*lanes+lane] = uint16(p>>(2*uint(k-1-j))) & 3 + } + } + + for j := 0; j <= k; j++ { + simd.BroadcastUint16s(uint16(lcsLenBase - j)).Store(row[j*lanes:]) + } + + for i := 1; i <= k; i++ { + qv := simd.BroadcastUint16s(qbase[i-1]) + diag := simd.LoadUint16s(row) + left := simd.BroadcastUint16s(uint16(lcsLenBase - i)) + left.Store(row) + + for j := 1; j <= k; j++ { + up := simd.LoadUint16s(row[j*lanes:]) + pv := simd.LoadUint16s(pbase[(j-1)*lanes:]) + match := qv.Equal(pv) + best := diag.Add(bonus.Masked(match)).Max(up).Max(left).Sub(one) + best.Store(row[j*lanes:]) + diag = up + left = best + } + } + + last := row[k*lanes:] + for lane := range chunk { + v := int(last[lane]) + out[start+lane] = KmerLCSResult{ + LCS: v / lcsScoreUnit, + AlignLen: lcsLenBase - v%lcsScoreUnit, + } + } + } +} diff --git a/pkg/obitools/obicsv/options.go b/pkg/obitools/obicsv/options.go index 1928fad..395f446 100644 --- a/pkg/obitools/obicsv/options.go +++ b/pkg/obitools/obicsv/options.go @@ -93,11 +93,6 @@ func CLIPrintTaxon() bool { _outputTaxon = true } - if i := obiutils.LookFor(_keepOnly, "scientific_name"); i >= 0 { - _keepOnly = obiutils.RemoveIndex(_keepOnly, i) - _outputTaxon = true - } - return _outputTaxon } func CLIPrintQuality() bool { @@ -131,11 +126,6 @@ func CLIToBeKeptAttributes() []string { _outputTaxon = true } - if i := obiutils.LookFor(_keepOnly, "scientific_name"); i >= 0 { - _keepOnly = obiutils.RemoveIndex(_keepOnly, i) - _outputTaxon = true - } - return _keepOnly } From f76e7f03c619e0643d01088fb5f5edf6147d8db6 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Mon, 2 Nov 2026 21:12:31 +1100 Subject: [PATCH 03/24] Feature: Introduce k-mer distance and SIMD LCS logic Introduces k-mer distance structures and SIMD implementation for LCS simulation. Adds support for concurrent, read-only k-mer partners, implements data preparation for SIMD alignment, and adds comprehensive testing and benchmarking capabilities. --- pkg/obikmer/kmer_distance.go | 32 +++++ pkg/obikmer/kmer_distance_test.go | 59 +++++++++ pkg/obikmer/kmer_lcs_archsimd_arm64.go | 62 --------- pkg/obikmer/kmer_lcs_archsimd_arm64_test.go | 31 ----- pkg/obikmer/kmer_lcs_scalar.go | 14 ++ pkg/obikmer/kmer_lcs_simd.go | 136 ++++++++++++-------- 6 files changed, 190 insertions(+), 144 deletions(-) delete mode 100644 pkg/obikmer/kmer_lcs_archsimd_arm64.go delete mode 100644 pkg/obikmer/kmer_lcs_archsimd_arm64_test.go diff --git a/pkg/obikmer/kmer_distance.go b/pkg/obikmer/kmer_distance.go index 8d17644..b024bf0 100644 --- a/pkg/obikmer/kmer_distance.go +++ b/pkg/obikmer/kmer_distance.go @@ -97,6 +97,38 @@ func KmerLCSBatch(query uint64, partners []uint64, k int, out []KmerLCSResult) b return true } +// KmerLCSPartners is a fixed set of k-mers prepared once to be aligned against +// many queries with KmerLCSBatch-like speed, without redoing the per-call +// reorganisation of the partners. It is read-only after creation and can be +// shared between goroutines. +type KmerLCSPartners struct { + prepared kmerLCSPrepared + n int +} + +// NewKmerLCSPartners prepares partners for repeated alignment. +// It returns nil if k is not between 1 and 31. The slice is not copied when +// built without GOEXPERIMENT=simd, so it must not be modified afterwards. +func NewKmerLCSPartners(partners []uint64, k int) *KmerLCSPartners { + if k < 1 || k > 31 { + return nil + } + return &KmerLCSPartners{prepared: prepareKmerLCS(partners, k), n: len(partners)} +} + +// Len returns the number of prepared partners. +func (p *KmerLCSPartners) Len() int { return p.n } + +// Align computes the alignment of query against every prepared partner and +// stores it in out. It returns false if out is shorter than Len(). +func (p *KmerLCSPartners) Align(query uint64, out []KmerLCSResult) bool { + if len(out) < p.n { + return false + } + p.prepared.align(query, out[:p.n]) + return true +} + func kmerLCSBatchScalar(query uint64, partners []uint64, k int, out []KmerLCSResult) { for i, p := range partners { out[i] = KmerLCS(query, p, k) diff --git a/pkg/obikmer/kmer_distance_test.go b/pkg/obikmer/kmer_distance_test.go index a46835a..ea11d51 100644 --- a/pkg/obikmer/kmer_distance_test.go +++ b/pkg/obikmer/kmer_distance_test.go @@ -110,3 +110,62 @@ func BenchmarkKmerLCSScalar21(b *testing.B) { benchBatch(b, 21, kmerLCSBatchScal func BenchmarkKmerLCSBatch21(b *testing.B) { benchBatch(b, 21, kmerLCSBatch) } func BenchmarkKmerLCSScalar11(b *testing.B) { benchBatch(b, 11, kmerLCSBatchScalar) } func BenchmarkKmerLCSBatch11(b *testing.B) { benchBatch(b, 11, kmerLCSBatch) } + +func TestKmerLCSPartnersMatchesReference(t *testing.T) { + rng := rand.New(rand.NewSource(6)) + for _, k := range []int{1, 5, 21, 31} { + for _, n := range []int{0, 1, 7, 8, 9, 50} { + partners := make([]uint64, n) + for i := range partners { + partners[i] = randomKmer(rng, k) + } + prep := NewKmerLCSPartners(partners, k) + out := make([]KmerLCSResult, n) + for q := 0; q < 3; q++ { // reuse the same preparation for several queries + query := randomKmer(rng, k) + if !prep.Align(query, out) { + t.Fatalf("k=%d n=%d: Align refused", k, n) + } + for i, p := range partners { + if want := KmerLCS(query, p, k); out[i] != want { + t.Fatalf("k=%d n=%d partner %d: got %+v, want %+v", k, n, i, out[i], want) + } + } + } + } + } + if NewKmerLCSPartners(nil, 0) != nil || NewKmerLCSPartners(nil, 32) != nil { + t.Fatal("invalid k must give nil") + } +} + +// All-pairs scenario: many queries against the same partners. +func benchAllPairs(b *testing.B, k int, prepared bool) { + rng := rand.New(rand.NewSource(7)) + partners := make([]uint64, 1024) + for i := range partners { + partners[i] = randomKmer(rng, k) + } + queries := make([]uint64, 64) + for i := range queries { + queries[i] = randomKmer(rng, k) + } + out := make([]KmerLCSResult, len(partners)) + prep := NewKmerLCSPartners(partners, k) + b.ResetTimer() + for b.Loop() { + for _, q := range queries { + if prepared { + prep.Align(q, out) + } else { + KmerLCSBatch(q, partners, k, out) + } + } + } + b.ReportMetric(float64(b.Elapsed().Nanoseconds())/float64(b.N*len(partners)*len(queries)), "ns/pair") +} + +func BenchmarkAllPairsBatch21(b *testing.B) { benchAllPairs(b, 21, false) } +func BenchmarkAllPairsPrepared21(b *testing.B) { benchAllPairs(b, 21, true) } +func BenchmarkAllPairsBatch11(b *testing.B) { benchAllPairs(b, 11, false) } +func BenchmarkAllPairsPrepared11(b *testing.B) { benchAllPairs(b, 11, true) } diff --git a/pkg/obikmer/kmer_lcs_archsimd_arm64.go b/pkg/obikmer/kmer_lcs_archsimd_arm64.go deleted file mode 100644 index 3438d24..0000000 --- a/pkg/obikmer/kmer_lcs_archsimd_arm64.go +++ /dev/null @@ -1,62 +0,0 @@ -//go:build goexperiment.simd && arm64 - -package obikmer - -import "simd/archsimd" - -// neonLanes is the number of 16-bit lanes of a NEON 128-bit register. -const neonLanes = 8 - -// kmerLCSBatchNEON is kmerLCSBatch written directly on archsimd NEON types -// (see kmer_lcs_simd.go for the cell encoding and recurrence). -func kmerLCSBatchNEON(query uint64, partners []uint64, k int, out []KmerLCSResult) { - var qbase [32]uint16 - for i := 0; i < k; i++ { - qbase[i] = uint16(query>>(2*uint(k-1-i))) & 3 - } - - pbase := make([]uint16, k*neonLanes) - row := make([]uint16, (k+1)*neonLanes) - one := archsimd.BroadcastUint16x8(1) - bonus := archsimd.BroadcastUint16x8(lcsScoreUnit) - - for start := 0; start < len(partners); start += neonLanes { - chunk := partners[start:min(start+neonLanes, len(partners))] - - for lane, p := range chunk { - for j := 0; j < k; j++ { - pbase[j*neonLanes+lane] = uint16(p>>(2*uint(k-1-j))) & 3 - } - } - - for j := 0; j <= k; j++ { - archsimd.BroadcastUint16x8(uint16(lcsLenBase - j)).Store(row[j*neonLanes:]) - } - - for i := 1; i <= k; i++ { - qv := archsimd.BroadcastUint16x8(qbase[i-1]) - diag := archsimd.LoadUint16x8(row) - left := archsimd.BroadcastUint16x8(uint16(lcsLenBase - i)) - left.Store(row) - - for j := 1; j <= k; j++ { - up := archsimd.LoadUint16x8(row[j*neonLanes:]) - pv := archsimd.LoadUint16x8(pbase[(j-1)*neonLanes:]) - match := qv.Equal(pv) - best := diag.Add(bonus.Masked(match)).Max(up).Max(left).Sub(one) - best.Store(row[j*neonLanes:]) - diag = up - left = best - } - } - - last := row[k*neonLanes:] - for lane := range chunk { - v := int(last[lane]) - out[start+lane] = KmerLCSResult{ - LCS: v / lcsScoreUnit, - AlignLen: lcsLenBase - v%lcsScoreUnit, - } - } - } -} diff --git a/pkg/obikmer/kmer_lcs_archsimd_arm64_test.go b/pkg/obikmer/kmer_lcs_archsimd_arm64_test.go deleted file mode 100644 index 1210b94..0000000 --- a/pkg/obikmer/kmer_lcs_archsimd_arm64_test.go +++ /dev/null @@ -1,31 +0,0 @@ -//go:build goexperiment.simd && arm64 - -package obikmer - -import ( - "math/rand" - "testing" -) - -func TestKmerLCSBatchNEONMatchesReference(t *testing.T) { - rng := rand.New(rand.NewSource(5)) - for _, k := range []int{1, 2, 5, 11, 21, 31} { - for _, n := range []int{0, 1, 7, 8, 9, 100} { - query := randomKmer(rng, k) - partners := make([]uint64, n) - for i := range partners { - partners[i] = randomKmer(rng, k) - } - out := make([]KmerLCSResult, n) - kmerLCSBatchNEON(query, partners, k, out) - for i, p := range partners { - if want := KmerLCS(query, p, k); out[i] != want { - t.Fatalf("k=%d n=%d partner %d: got %+v, want %+v", k, n, i, out[i], want) - } - } - } - } -} - -func BenchmarkKmerLCSNEON21(b *testing.B) { benchBatch(b, 21, kmerLCSBatchNEON) } -func BenchmarkKmerLCSNEON11(b *testing.B) { benchBatch(b, 11, kmerLCSBatchNEON) } diff --git a/pkg/obikmer/kmer_lcs_scalar.go b/pkg/obikmer/kmer_lcs_scalar.go index 831a131..0095723 100644 --- a/pkg/obikmer/kmer_lcs_scalar.go +++ b/pkg/obikmer/kmer_lcs_scalar.go @@ -5,3 +5,17 @@ package obikmer func kmerLCSBatch(query uint64, partners []uint64, k int, out []KmerLCSResult) { kmerLCSBatchScalar(query, partners, k, out) } + +// kmerLCSPrepared has nothing to precompute without SIMD. +type kmerLCSPrepared struct { + k int + partners []uint64 +} + +func prepareKmerLCS(partners []uint64, k int) kmerLCSPrepared { + return kmerLCSPrepared{k: k, partners: partners} +} + +func (p *kmerLCSPrepared) align(query uint64, out []KmerLCSResult) { + kmerLCSBatchScalar(query, p.partners, p.k, out) +} diff --git a/pkg/obikmer/kmer_lcs_simd.go b/pkg/obikmer/kmer_lcs_simd.go index 6127c62..e2f3d91 100644 --- a/pkg/obikmer/kmer_lcs_simd.go +++ b/pkg/obikmer/kmer_lcs_simd.go @@ -22,59 +22,93 @@ const ( lcsLenBase = 63 ) -func kmerLCSBatch(query uint64, partners []uint64, k int, out []KmerLCSResult) { +func simdLanes() int { var probe simd.Uint16s - lanes := probe.Len() + return probe.Len() +} - var qbase [32]uint16 - for i := 0; i < k; i++ { - qbase[i] = uint16(query>>(2*uint(k-1-i))) & 3 - } - - // partner bases, transposed: pbase[j*lanes+lane] is base j of the partner in `lane` - pbase := make([]uint16, k*lanes) - // one DP row, one vector per column - row := make([]uint16, (k+1)*lanes) - one := simd.BroadcastUint16s(1) - bonus := simd.BroadcastUint16s(lcsScoreUnit) - - for start := 0; start < len(partners); start += lanes { - chunk := partners[start:min(start+lanes, len(partners))] - - for lane, p := range chunk { - for j := 0; j < k; j++ { - pbase[j*lanes+lane] = uint16(p>>(2*uint(k-1-j))) & 3 - } - } - - for j := 0; j <= k; j++ { - simd.BroadcastUint16s(uint16(lcsLenBase - j)).Store(row[j*lanes:]) - } - - for i := 1; i <= k; i++ { - qv := simd.BroadcastUint16s(qbase[i-1]) - diag := simd.LoadUint16s(row) - left := simd.BroadcastUint16s(uint16(lcsLenBase - i)) - left.Store(row) - - for j := 1; j <= k; j++ { - up := simd.LoadUint16s(row[j*lanes:]) - pv := simd.LoadUint16s(pbase[(j-1)*lanes:]) - match := qv.Equal(pv) - best := diag.Add(bonus.Masked(match)).Max(up).Max(left).Sub(one) - best.Store(row[j*lanes:]) - diag = up - left = best - } - } - - last := row[k*lanes:] - for lane := range chunk { - v := int(last[lane]) - out[start+lane] = KmerLCSResult{ - LCS: v / lcsScoreUnit, - AlignLen: lcsLenBase - v%lcsScoreUnit, - } +// transposeChunk writes the bases of up to `lanes` partners in dst, laid out +// so that dst[j*lanes+lane] is base j of the partner in `lane`. +func transposeChunk(chunk []uint64, k, lanes int, dst []uint16) { + for lane, p := range chunk { + for j := 0; j < k; j++ { + dst[j*lanes+lane] = uint16(p>>(2*uint(k-1-j))) & 3 } } } + +// alignChunk aligns the query against the partners of one transposed chunk +// and writes one result per lane in out (len(out) <= lanes). +func alignChunk(query uint64, pbase, row []uint16, k, lanes int, out []KmerLCSResult) { + one := simd.BroadcastUint16s(1) + bonus := simd.BroadcastUint16s(lcsScoreUnit) + + for j := 0; j <= k; j++ { + simd.BroadcastUint16s(uint16(lcsLenBase - j)).Store(row[j*lanes:]) + } + + for i := 1; i <= k; i++ { + qv := simd.BroadcastUint16s(uint16(query>>(2*uint(k-i))) & 3) + diag := simd.LoadUint16s(row) + left := simd.BroadcastUint16s(uint16(lcsLenBase - i)) + left.Store(row) + + for j := 1; j <= k; j++ { + up := simd.LoadUint16s(row[j*lanes:]) + pv := simd.LoadUint16s(pbase[(j-1)*lanes:]) + match := qv.Equal(pv) + best := diag.Add(bonus.Masked(match)).Max(up).Max(left).Sub(one) + best.Store(row[j*lanes:]) + diag = up + left = best + } + } + + last := row[k*lanes:] + for lane := range out { + v := int(last[lane]) + out[lane] = KmerLCSResult{ + LCS: v / lcsScoreUnit, + AlignLen: lcsLenBase - v%lcsScoreUnit, + } + } +} + +func kmerLCSBatch(query uint64, partners []uint64, k int, out []KmerLCSResult) { + lanes := simdLanes() + pbase := make([]uint16, k*lanes) + row := make([]uint16, (k+1)*lanes) + + for start := 0; start < len(partners); start += lanes { + end := min(start+lanes, len(partners)) + transposeChunk(partners[start:end], k, lanes, pbase) + alignChunk(query, pbase, row, k, lanes, out[start:end]) + } +} + +// kmerLCSPrepared holds partners already transposed, chunk after chunk. +type kmerLCSPrepared struct { + k, lanes, n int + pbase []uint16 // chunk c at [c*k*lanes : (c+1)*k*lanes] +} + +func prepareKmerLCS(partners []uint64, k int) kmerLCSPrepared { + lanes := simdLanes() + chunks := (len(partners) + lanes - 1) / lanes + p := kmerLCSPrepared{k: k, lanes: lanes, n: len(partners), + pbase: make([]uint16, chunks*k*lanes)} + for c := 0; c < chunks; c++ { + end := min((c+1)*lanes, len(partners)) + transposeChunk(partners[c*lanes:end], k, lanes, p.pbase[c*k*lanes:(c+1)*k*lanes]) + } + return p +} + +func (p *kmerLCSPrepared) align(query uint64, out []KmerLCSResult) { + row := make([]uint16, (p.k+1)*p.lanes) + size := p.k * p.lanes + for c, start := 0, 0; start < p.n; c, start = c+1, start+p.lanes { + end := min(start+p.lanes, p.n) + alignChunk(query, p.pbase[c*size:(c+1)*size], row, p.k, p.lanes, out[start:end]) + } +} From 3087c70daa1e2c33172e923a4e5aa00a52ae7d43 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Mon, 2 Nov 2026 22:12:43 +1100 Subject: [PATCH 04/24] Feature: Implement constrained k-mer generation and clique search This commit introduces core functionality for oligonucleotide family tagging, constrained k-mer generation, and clique finding. It also includes updates to the Go language version and refactors the logic for k-mer dissimilarity metrics. --- Release-notes.md | 17 ++ cmd/obitools/obioligotag/main.go | 33 +++ go.mod | 2 +- go.work | 2 +- obitests/obitools/obioligotag/test.sh | 205 ++++++++++++++++++ pkg/obigraph/clique.go | 176 +++++++++++++++ pkg/obigraph/clique_test.go | 143 ++++++++++++ pkg/obikmer/enumkmer.go | 60 ++++- pkg/obikmer/kmer_constraints.go | 141 ++++++++++++ pkg/obikmer/kmer_constraints_test.go | 123 +++++++++++ pkg/obikmer/kmer_dissimilarity.go | 84 +++++++ pkg/obikmer/kmer_dissimilarity_test.go | 113 ++++++++++ pkg/obiseq/iupac_nog.go | 13 ++ pkg/obitools/obioligotag/input.go | 148 +++++++++++++ pkg/obitools/obioligotag/obioligotag.go | 217 +++++++++++++++++++ pkg/obitools/obioligotag/obioligotag_test.go | 177 +++++++++++++++ pkg/obitools/obioligotag/options.go | 150 +++++++++++++ 17 files changed, 1791 insertions(+), 13 deletions(-) create mode 100644 cmd/obitools/obioligotag/main.go create mode 100755 obitests/obitools/obioligotag/test.sh create mode 100644 pkg/obigraph/clique.go create mode 100644 pkg/obigraph/clique_test.go create mode 100644 pkg/obikmer/kmer_constraints.go create mode 100644 pkg/obikmer/kmer_constraints_test.go create mode 100644 pkg/obikmer/kmer_dissimilarity.go create mode 100644 pkg/obikmer/kmer_dissimilarity_test.go create mode 100644 pkg/obitools/obioligotag/input.go create mode 100644 pkg/obitools/obioligotag/obioligotag.go create mode 100644 pkg/obitools/obioligotag/obioligotag_test.go create mode 100644 pkg/obitools/obioligotag/options.go diff --git a/Release-notes.md b/Release-notes.md index 143576a..1666e24 100644 --- a/Release-notes.md +++ b/Release-notes.md @@ -2,6 +2,23 @@ ## New changes +### New features + +- New `obioligotag` command, a re-implementation of the `oligotag` command of + OBITools 2. It builds a family of oligonucleotides (tags) of at least + `--family-size` members that are pairwise at a distance of at least + `--distance`. Candidates are either generated (`--oligo-size`, up to 12) or + read from a file (`--oligo-list`, sequence file or one oligonucleotide per + line). They can be restricted by alphabet (`--alphabet`, for instance `ACT` + to forbid G), GC content, homopolymers and IUPAC patterns. The distance is + either the Hamming distance or the LCS-mode alignment distance + (`--distance-type`). + +- New functions in `obikmer` to compare and enumerate k-mers encoded as + `uint64`: `HammingDistance`, `KmerLCS` and `KmerLCSBatch` (SIMD when built with + `GOEXPERIMENT=simd`), `IterConstrainedKmers`, and in `obigraph` a clique search + (`FindClique`) that does not need the adjacency matrix. + ### Bug fixes - In `obipairing` correct the misspelling of the `obiparing_*` tags where the `i` diff --git a/cmd/obitools/obioligotag/main.go b/cmd/obitools/obioligotag/main.go new file mode 100644 index 0000000..04cce0f --- /dev/null +++ b/cmd/obitools/obioligotag/main.go @@ -0,0 +1,33 @@ +package main + +import ( + "os" + + log "github.com/sirupsen/logrus" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obioptions" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obitools/obiconvert" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obitools/obioligotag" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiutils" +) + +func main() { + optionParser := obioptions.GenerateOptionParser( + "obioligotag", + "designs a family of oligonucleotides (tags) at a minimum distance from each other", + obioligotag.OptionSet) + + _, args := optionParser(os.Args) + if len(args) > 0 { + log.Fatalf("unexpected arguments %v: use --oligo-list to provide candidate oligonucleotides", args) + } + + oligos, err := obioligotag.CLIOligoFamily() + if err != nil { + log.Fatalf("%v", err) + } + + obiconvert.CLIWriteBioSequences(obioligotag.CLIOligosToIterator(oligos), true) + + obiutils.WaitForLastPipe() +} diff --git a/go.mod b/go.mod index 86a7baf..36dd212 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module git.metabarcoding.org/obitools/obitools4/obitools4 -go 1.26.1 +go 1.27 require ( github.com/DavidGamba/go-getoptions v0.33.0 diff --git a/go.work b/go.work index e4243b4..47208ec 100644 --- a/go.work +++ b/go.work @@ -1,3 +1,3 @@ -go 1.26.1 +go 1.27 use . diff --git a/obitests/obitools/obioligotag/test.sh b/obitests/obitools/obioligotag/test.sh new file mode 100755 index 0000000..86fd52b --- /dev/null +++ b/obitests/obitools/obioligotag/test.sh @@ -0,0 +1,205 @@ +#!/bin/bash + +# +# Here give the name of the test serie +# + +TEST_NAME=obioligotag +CMD=obioligotag + +###### +# +# Some variable and function definitions: please don't change them +# +###### +TEST_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" +OBITOOLS_DIR="${TEST_DIR/obitest*/}build" +export PATH="${OBITOOLS_DIR}:${PATH}" + +MCMD="$(echo "${CMD:0:4}" | tr '[:lower:]' '[:upper:]')$(echo "${CMD:4}" | tr '[:upper:]' '[:lower:]')" + +TMPDIR="$(mktemp -d)" +ntest=0 +success=0 +failed=0 + +cleanup() { + echo "========================================" 1>&2 + echo "## Results of the $TEST_NAME tests:" 1>&2 + + echo 1>&2 + echo "- $ntest tests run" 1>&2 + echo "- $success successfully completed" 1>&2 + echo "- $failed failed tests" 1>&2 + echo 1>&2 + echo "Cleaning up the temporary directory..." 1>&2 + echo 1>&2 + echo "========================================" 1>&2 + + rm -rf "$TMPDIR" # Suppress the temporary directory + + if [ $failed -gt 0 ]; then + log "$TEST_NAME tests failed" + log + log + exit 1 + fi + + log + log + + exit 0 +} + +log() { + echo -e "[$TEST_NAME @ $(date)] $*" 1>&2 +} + +log "Testing $TEST_NAME..." +log "Test directory is $TEST_DIR" +log "obitools directory is $OBITOOLS_DIR" +log "Temporary directory is $TMPDIR" +log "files: $(find $TEST_DIR | awk -F'/' '{print $NF}' | tail -n +2)" + +###################################################################### +#### +#### Below are the tests +#### +#### Before each test : +#### - increment the variable ntest +#### +#### Run the command as the condition of an if / then /else +#### - The command must return 0 on success +#### - The command must return an exit code different from 0 on failure +#### - The datafiles are stored in the same directory than the test script +#### - The test script directory is stored in the TEST_DIR variable +#### - If result files have to be produced they must be stored +#### in the temporary directory (TMPDIR variable) +#### +#### then clause is executed on success of the command +#### - Write a success message using the log function +#### - increment the variable success +#### +#### else clause is executed on failure of the command +#### - Write a failure message using the log function +#### - increment the variable failed +#### +###################################################################### + + + +# +# Every result is checked on its properties (size, alphabet, constraints), +# not against a reference file, because several equivalent families exist. +# + +printf 'acgtac\nacgtat\nttttgg\nggcaac\ncaccat\nacgtac\n' > "${TMPDIR}/oligos.txt" +printf '>a\nACGTAC\n>b\nACGTAT\n>c\nTTTTGG\n>d\nGGCAAC\n>e\nCACCAT\n' > "${TMPDIR}/oligos.fasta" + +((ntest++)) +if $CMD -h > "${TMPDIR}/help.txt" 2>&1 +then + log "$MCMD: printing help OK" + ((success++)) +else + log "$MCMD: printing help failed" + ((failed++)) +fi + +((ntest++)) +if $CMD -s 5 -f 24 -d 3 -g 3 -p 2 > "${TMPDIR}/tags.fasta" 2> /dev/null \ + && [ "$(grep -vc '>' "${TMPDIR}/tags.fasta")" -ge 24 ] \ + && ! grep -v '>' "${TMPDIR}/tags.fasta" | grep -qE '(a{3}|c{3}|g{3}|t{3})' +then + log "$MCMD: generating a family with homopolymer constraint OK" + ((success++)) +else + log "$MCMD: generating a family with homopolymer constraint failed" + ((failed++)) +fi + +((ntest++)) +if $CMD -s 7 -f 60 -d 3 --alphabet ACT > "${TMPDIR}/noG.fasta" 2> /dev/null \ + && [ "$(grep -vc '>' "${TMPDIR}/noG.fasta")" -ge 60 ] \ + && ! grep -v '>' "${TMPDIR}/noG.fasta" | grep -q 'g' +then + log "$MCMD: restricted alphabet OK" + ((success++)) +else + log "$MCMD: restricted alphabet failed" + ((failed++)) +fi + +((ntest++)) +if $CMD -s 6 -f 20 -d 3 --distance-type lcs > "${TMPDIR}/lcs.fasta" 2> /dev/null \ + && [ "$(grep -vc '>' "${TMPDIR}/lcs.fasta")" -ge 20 ] +then + log "$MCMD: LCS distance OK" + ((success++)) +else + log "$MCMD: LCS distance failed" + ((failed++)) +fi + +((ntest++)) +if $CMD -s 7 -f 40 -d 3 -r cnnnnnn > "${TMPDIR}/rejected.fasta" 2> /dev/null \ + && [ "$(grep -vc '>' "${TMPDIR}/rejected.fasta")" -ge 40 ] \ + && ! grep -v '>' "${TMPDIR}/rejected.fasta" | grep -q '^c' +then + log "$MCMD: rejected pattern OK" + ((success++)) +else + log "$MCMD: rejected pattern failed" + ((failed++)) +fi + +((ntest++)) +if $CMD -L "${TMPDIR}/oligos.txt" -f 3 -d 3 > "${TMPDIR}/from_text.fasta" 2> /dev/null \ + && [ "$(grep -vc '>' "${TMPDIR}/from_text.fasta")" -ge 3 ] +then + log "$MCMD: selecting oligonucleotides from a text file OK" + ((success++)) +else + log "$MCMD: selecting oligonucleotides from a text file failed" + ((failed++)) +fi + +((ntest++)) +if $CMD -L "${TMPDIR}/oligos.fasta" -f 3 -d 3 > "${TMPDIR}/from_fasta.fasta" 2> /dev/null \ + && [ "$(grep -vc '>' "${TMPDIR}/from_fasta.fasta")" -ge 3 ] +then + log "$MCMD: selecting oligonucleotides from a fasta file OK" + ((success++)) +else + log "$MCMD: selecting oligonucleotides from a fasta file failed" + ((failed++)) +fi + +((ntest++)) +if ! $CMD -s 5 -f 100 -d 3 > /dev/null 2>&1 +then + log "$MCMD: family size beyond the Hamming bound refused OK" + ((success++)) +else + log "$MCMD: family size beyond the Hamming bound not refused" + ((failed++)) +fi + +((ntest++)) +if ! $CMD -s 5 -L "${TMPDIR}/oligos.txt" -f 3 > /dev/null 2>&1 +then + log "$MCMD: --oligo-size with --oligo-list refused OK" + ((success++)) +else + log "$MCMD: --oligo-size with --oligo-list not refused" + ((failed++)) +fi + +######################################### +# +# At the end of the tests +# the cleanup function is called +# +######################################### + +cleanup diff --git a/pkg/obigraph/clique.go b/pkg/obigraph/clique.go new file mode 100644 index 0000000..8572dc0 --- /dev/null +++ b/pkg/obigraph/clique.go @@ -0,0 +1,176 @@ +package obigraph + +import ( + "context" + "math/rand" + "sync" +) + +// NeighborFilter defines an implicit graph over the vertices 0..n-1. +// It appends to out[:0] the vertices of candidates that are adjacent to v, +// keeping their order, and returns the resulting slice. It is called +// concurrently by several workers and must not modify candidates. +// +// Describing the adjacency by a filter, instead of a matrix, keeps the memory +// in O(n) and lets the caller evaluate adjacency in bulk (SIMD, for instance). +type NeighborFilter func(v int32, candidates, out []int32) []int32 + +// CliqueOptions tunes FindClique. +type CliqueOptions struct { + // Workers is the number of concurrent searches (default 1). + Workers int + // Seed seeds the random vertex orders of the restarts. + Seed int64 +} + +// CliqueResult is the outcome of FindClique. +type CliqueResult struct { + // Clique is the largest clique found (a clique of size >= target if Reached). + Clique []int32 + // Reached is true if len(Clique) >= target. + Reached bool + // Exhausted is true if the whole search space has been explored. If Reached + // is false, it proves that no clique of the target size exists. + Exhausted bool +} + +// FindClique looks for a clique of at least target vertices in the graph of n +// vertices described by filter. It stops as soon as one is found, when the +// search space is exhausted, or when ctx is done; in the last case, the largest +// clique met so far is returned. +// +// The search is a depth first search with backtracking, pruned when the +// current clique plus the remaining candidates cannot reach target. The first +// pass of the first worker follows the natural order of the vertices (greedy +// "lexicographic" construction); every other pass restarts from a random +// order, with a work budget that doubles at each restart, so that the search +// is complete in the long run. +func FindClique(ctx context.Context, n int, filter NeighborFilter, target int, opts CliqueOptions) CliqueResult { + if target <= 0 { + return CliqueResult{Reached: true, Exhausted: true} + } + if n <= 0 { + return CliqueResult{Exhausted: true} + } + + workers := max(opts.Workers, 1) + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + var ( + mutex sync.Mutex + best []int32 + exhausted bool + wg sync.WaitGroup + ) + + publish := func(clique []int32) { + mutex.Lock() + if len(clique) > len(best) { + best = append(best[:0], clique...) + } + mutex.Unlock() + } + + for w := range workers { + wg.Go(func() { + s := &cliqueSearch{ + ctx: ctx, + filter: filter, + target: target, + publish: func(c []int32) { + publish(c) + if len(c) >= target { + cancel() + } + }, + } + rng := rand.New(rand.NewSource(opts.Seed + int64(w))) + candidates := make([]int32, n) + for i := range candidates { + candidates[i] = int32(i) + } + + for restart := 0; ctx.Err() == nil; restart++ { + if w > 0 || restart > 0 { + rng.Shuffle(n, func(i, j int) { candidates[i], candidates[j] = candidates[j], candidates[i] }) + } + s.budget = int64(n) * int64(target) << min(restart, 20) + s.work = 0 + s.aborted = false + s.search(0, candidates) + + if !s.aborted && ctx.Err() == nil { + mutex.Lock() + exhausted = true + mutex.Unlock() + cancel() + } + } + }) + } + wg.Wait() + + return CliqueResult{ + Clique: best, + Reached: len(best) >= target, + Exhausted: exhausted, + } +} + +type cliqueSearch struct { + ctx context.Context + filter NeighborFilter + target int + publish func([]int32) + + current []int32 // the clique under construction + buffers [][]int32 // candidate list of each depth + bestLen int + + budget int64 // maximum work (number of candidates tested) for this restart + work int64 + aborted bool // budget or context exhausted before the end of the exploration +} + +func (s *cliqueSearch) search(depth int, candidates []int32) { + for i := range candidates { + if depth+len(candidates)-i < s.target { + return + } + if s.work > s.budget || s.ctx.Err() != nil { + s.aborted = true + return + } + + v := candidates[i] + s.current = append(s.current[:depth], v) + if len(s.current) > s.bestLen { + s.bestLen = len(s.current) + s.publish(s.current) + if s.bestLen >= s.target { + s.aborted = true // the search is over: do not report an exhaustion + return + } + } + + rest := candidates[i+1:] + if depth+1+len(rest) < s.target { + continue + } + for len(s.buffers) <= depth+1 { + s.buffers = append(s.buffers, nil) + } + next := s.filter(v, rest, s.buffers[depth+1][:0]) + s.buffers[depth+1] = next + s.work += int64(len(rest)) + + if depth+1+len(next) < s.target { + continue + } + s.search(depth+1, next) + if s.aborted { + return + } + } +} diff --git a/pkg/obigraph/clique_test.go b/pkg/obigraph/clique_test.go new file mode 100644 index 0000000..b2c6cd2 --- /dev/null +++ b/pkg/obigraph/clique_test.go @@ -0,0 +1,143 @@ +package obigraph + +import ( + "context" + "math/bits" + "math/rand" + "testing" + "time" +) + +// adjacency matrix filter for small graphs +func matrixFilter(adj [][]bool) NeighborFilter { + return func(v int32, candidates, out []int32) []int32 { + for _, c := range candidates { + if adj[v][c] { + out = append(out, c) + } + } + return out + } +} + +func randomGraph(rng *rand.Rand, n int, p float64) [][]bool { + adj := make([][]bool, n) + for i := range adj { + adj[i] = make([]bool, n) + } + for i := 0; i < n; i++ { + for j := i + 1; j < n; j++ { + if rng.Float64() < p { + adj[i][j], adj[j][i] = true, true + } + } + } + return adj +} + +// maximum clique size by brute force over all the subsets (n <= 20) +func bruteMaxClique(adj [][]bool) int { + n := len(adj) + best := 0 + for mask := 1; mask < 1<>i&1 == 0 { + continue + } + for j := i + 1; j < n; j++ { + if mask>>j&1 == 1 && !adj[i][j] { + ok = false + break + } + } + } + if ok { + best = size + } + } + return best +} + +func isClique(adj [][]bool, c []int32) bool { + seen := map[int32]bool{} + for i, a := range c { + if seen[a] { + return false + } + seen[a] = true + for _, b := range c[i+1:] { + if !adj[a][b] { + return false + } + } + } + return true +} + +func TestFindCliqueAgainstBruteForce(t *testing.T) { + rng := rand.New(rand.NewSource(1)) + for trial := 0; trial < 40; trial++ { + n := 8 + rng.Intn(10) + adj := randomGraph(rng, n, 0.3+0.5*rng.Float64()) + maxSize := bruteMaxClique(adj) + filter := matrixFilter(adj) + + for _, workers := range []int{1, 3} { + r := FindClique(context.Background(), n, filter, maxSize, CliqueOptions{Workers: workers}) + if !r.Reached || len(r.Clique) < maxSize || !isClique(adj, r.Clique) { + t.Fatalf("trial %d: target %d not reached: %+v", trial, maxSize, r) + } + + r = FindClique(context.Background(), n, filter, maxSize+1, CliqueOptions{Workers: workers}) + if r.Reached || !r.Exhausted { + t.Fatalf("trial %d: size %d cannot exist, got %+v", trial, maxSize+1, r) + } + // best effort: a valid clique, not necessarily a maximum one + if len(r.Clique) < 1 || len(r.Clique) > maxSize || !isClique(adj, r.Clique) { + t.Fatalf("trial %d: invalid best clique %v (maximum is %d)", trial, r.Clique, maxSize) + } + } + } +} + +func TestFindCliqueTrivialTargets(t *testing.T) { + adj := randomGraph(rand.New(rand.NewSource(2)), 5, 0.5) + if r := FindClique(context.Background(), 5, matrixFilter(adj), 0, CliqueOptions{}); !r.Reached { + t.Fatal("target 0 is always reached") + } + if r := FindClique(context.Background(), 0, matrixFilter(adj), 1, CliqueOptions{}); r.Reached { + t.Fatal("no clique in an empty graph") + } + if r := FindClique(context.Background(), 5, matrixFilter(adj), 1, CliqueOptions{}); !r.Reached || len(r.Clique) != 1 { + t.Fatalf("a single vertex is a clique: %+v", r) + } +} + +// A hopeless search must stop with the deadline and return a valid clique. +// (When the target is far above the clique number of a sparse graph, pruning +// proves the absence at once: see TestFindCliqueAgainstBruteForce.) +func TestFindCliqueTimeout(t *testing.T) { + // dense graph: its largest clique is far below the target, but pruning only + // bites after many levels, so the search cannot finish within the deadline + const n = 300 + adj := randomGraph(rand.New(rand.NewSource(3)), n, 0.9) + ctx, cancel := context.WithTimeout(context.Background(), 300*time.Millisecond) + defer cancel() + + start := time.Now() + r := FindClique(ctx, n, matrixFilter(adj), 150, CliqueOptions{Workers: 2}) + if elapsed := time.Since(start); elapsed > 3*time.Second { + t.Fatalf("deadline not honoured: %v", elapsed) + } + if r.Reached || r.Exhausted { + t.Fatalf("size 150 cannot be reached nor proven absent in time: %+v", r) + } + if len(r.Clique) < 2 || !isClique(adj, r.Clique) { + t.Fatalf("invalid best clique %v", r.Clique) + } +} diff --git a/pkg/obikmer/enumkmer.go b/pkg/obikmer/enumkmer.go index 678bf08..f0c18f5 100644 --- a/pkg/obikmer/enumkmer.go +++ b/pkg/obikmer/enumkmer.go @@ -8,18 +8,32 @@ import "iter" // a negative value means unlimited, 0 forbids the base altogether // (so MaxCount[2] = 0 restricts the alphabet to A, C, T). // +// MaxGC is the maximum number of C or G bases; a negative value means +// unlimited. +// // MaxRun is the maximum number of consecutive identical bases; -// a value <= 0 means unlimited. +// a value <= 0 means unlimited. MinRun requires at least one run of that +// length; a value <= 1 means no requirement. +// +// Accepted and Rejected are optional per-position sets of bases (4 bits masks, +// bit b set when the base of code b is in the set, see ParseIUPACMasks). +// When set, their length must be k. A k-mer is accepted only if each of its +// bases belongs to the Accepted set of its position, and is refused if every +// one of its bases belongs to the Rejected set of its position. // // The zero value forbids every base: use NoKmerConstraints as a starting point. type KmerConstraints struct { MaxCount [4]int + MaxGC int MaxRun int + MinRun int + Accepted []uint8 + Rejected []uint8 } // NoKmerConstraints returns constraints accepting every k-mer. func NoKmerConstraints() KmerConstraints { - return KmerConstraints{MaxCount: [4]int{-1, -1, -1, -1}} + return KmerConstraints{MaxCount: [4]int{-1, -1, -1, -1}, MaxGC: -1} } // IterConstrainedKmers returns an iterator over all k-mers of size k @@ -27,11 +41,14 @@ func NoKmerConstraints() KmerConstraints { // numerical order. // // K-mers are built base by base and a branch is abandoned as soon as a -// constraint is violated (both MaxCount and MaxRun are monotone: a violating -// prefix cannot be repaired by extension), so the cost is proportional to the -// number of explored valid prefixes, not to 4^k. +// constraint is violated or cannot be met any more (MaxCount, MaxGC, MaxRun +// and Accepted are monotone: a violating prefix cannot be repaired by +// extension; MinRun is checked against the positions left), so the cost is +// proportional to the number of explored prefixes, not to 4^k. Rejected is +// checked on complete k-mers. // -// k must be between 1 and 31, otherwise the iterator is empty. +// k must be between 1 and 31, otherwise the iterator is empty. It is also +// empty if Accepted or Rejected is set with a length different from k. // // Example: // @@ -41,7 +58,7 @@ func NoKmerConstraints() KmerConstraints { // for kmer := range IterConstrainedKmers(8, c) { ... } func IterConstrainedKmers(k int, c KmerConstraints) iter.Seq[uint64] { return func(yield func(uint64) bool) { - if k < 1 || k > 31 { + if k < 1 || k > 31 || !c.validPositions(k) { return } @@ -59,16 +76,27 @@ func IterConstrainedKmers(k int, c KmerConstraints) iter.Seq[uint64] { } var counts [4]int + gc := 0 - var walk func(depth int, kmer uint64, last, run int) bool - walk = func(depth int, kmer uint64, last, run int) bool { + var walk func(depth int, kmer uint64, last, run, longest int) bool + walk = func(depth int, kmer uint64, last, run, longest int) bool { if depth == k { + if c.Rejected != nil && matchesAllMasks(kmer, k, c.Rejected) { + return true + } return yield(kmer) } for b := range 4 { if c.MaxCount[b] >= 0 && counts[b] >= c.MaxCount[b] { continue } + if c.Accepted != nil && c.Accepted[depth]>>uint(b)&1 == 0 { + continue + } + isGC := b == 1 || b == 2 + if isGC && c.MaxGC >= 0 && gc >= c.MaxGC { + continue + } r := 1 if b == last { r = run + 1 @@ -76,9 +104,19 @@ func IterConstrainedKmers(k int, c KmerConstraints) iter.Seq[uint64] { if c.MaxRun > 0 && r > c.MaxRun { continue } + l := max(longest, r) + if c.MinRun > 1 && l < c.MinRun && r+(k-depth-1) < c.MinRun { + continue + } counts[b]++ - ok := walk(depth+1, kmer<<2|uint64(b), b, r) + if isGC { + gc++ + } + ok := walk(depth+1, kmer<<2|uint64(b), b, r, l) counts[b]-- + if isGC { + gc-- + } if !ok { return false } @@ -86,6 +124,6 @@ func IterConstrainedKmers(k int, c KmerConstraints) iter.Seq[uint64] { return true } - walk(0, 0, -1, 0) + walk(0, 0, -1, 0, 0) } } diff --git a/pkg/obikmer/kmer_constraints.go b/pkg/obikmer/kmer_constraints.go new file mode 100644 index 0000000..6fcf25c --- /dev/null +++ b/pkg/obikmer/kmer_constraints.go @@ -0,0 +1,141 @@ +package obikmer + +import ( + "fmt" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq" +) + +// ParseIUPACMasks converts an IUPAC nucleotide pattern into one 4 bits mask per +// position (bit 0 is A, bit 1 is C, bit 2 is G, bit 3 is T), the format used by +// KmerConstraints.Accepted and Rejected. +func ParseIUPACMasks(pattern string) ([]uint8, error) { + masks := make([]uint8, len(pattern)) + for i := 0; i < len(pattern); i++ { + m := obiseq.IUPACNucMask(pattern[i]) + if m == 0 { + return nil, fmt.Errorf("invalid IUPAC nucleotide code %q at position %d of pattern %q", + pattern[i], i+1, pattern) + } + masks[i] = m + } + return masks, nil +} + +// RestrictAlphabet forbids every base that is not denoted by one of the IUPAC +// codes of alphabet. For instance "ACT" (or "H") forbids G. +func (c *KmerConstraints) RestrictAlphabet(alphabet string) error { + masks, err := ParseIUPACMasks(alphabet) + if err != nil { + return err + } + var allowed uint8 + for _, m := range masks { + allowed |= m + } + if allowed == 0 { + return fmt.Errorf("empty alphabet") + } + for b := range 4 { + if allowed>>uint(b)&1 == 0 { + c.MaxCount[b] = 0 + } + } + return nil +} + +// validPositions tells whether Accepted and Rejected, when set, have length k. +func (c *KmerConstraints) validPositions(k int) bool { + return (c.Accepted == nil || len(c.Accepted) == k) && + (c.Rejected == nil || len(c.Rejected) == k) +} + +// matchesAllMasks tells whether every base of the k-mer belongs to the set +// given for its position. +func matchesAllMasks(kmer uint64, k int, masks []uint8) bool { + for i := k - 1; i >= 0; i-- { + if masks[i]>>(kmer&3)&1 == 0 { + return false + } + kmer >>= 2 + } + return true +} + +// Accepts tells whether a k-mer of size k satisfies every constraint. +// It is the predicate whose solutions IterConstrainedKmers enumerates, to be +// used to filter k-mers coming from elsewhere. +func (c *KmerConstraints) Accepts(kmer uint64, k int) bool { + if k < 1 || k > 31 || !c.validPositions(k) { + return false + } + + var counts [4]int + gc, run, longest, last := 0, 0, 0, -1 + for i := k - 1; i >= 0; i-- { + b := int(kmer>>(2*uint(i))) & 3 + if c.Accepted != nil && c.Accepted[k-1-i]>>uint(b)&1 == 0 { + return false + } + counts[b]++ + if c.MaxCount[b] >= 0 && counts[b] > c.MaxCount[b] { + return false + } + if b == 1 || b == 2 { + gc++ + if c.MaxGC >= 0 && gc > c.MaxGC { + return false + } + } + if b == last { + run++ + } else { + run = 1 + } + last = b + longest = max(longest, run) + if c.MaxRun > 0 && run > c.MaxRun { + return false + } + } + + if c.MinRun > 1 && longest < c.MinRun { + return false + } + if c.Rejected != nil && matchesAllMasks(kmer, k, c.Rejected) { + return false + } + return true +} + +// HammingBound returns the sphere-packing (Hamming) upper bound on the number +// of words of length k over an alphabet of alphabetSize letters that can be +// pairwise at Hamming distance >= d. No larger family exists; a family of +// that size may not exist either. +func HammingBound(alphabetSize, k, d int) float64 { + if d <= 1 { + total := 1.0 + for i := 0; i < k; i++ { + total *= float64(alphabetSize) + } + return total + } + if d%2 == 0 { // A(k, d) <= A(k-1, d-1) + k-- + d-- + } + if k < 1 { + return 1 + } + t := (d - 1) / 2 + total, ball, binom, pow := 1.0, 1.0, 1.0, 1.0 + for i := 0; i < k; i++ { + total *= float64(alphabetSize) + } + for i := 1; i <= t && i <= k; i++ { + binom = binom * float64(k-i+1) / float64(i) + pow *= float64(alphabetSize - 1) + ball += binom * pow + } + return total / ball +} diff --git a/pkg/obikmer/kmer_constraints_test.go b/pkg/obikmer/kmer_constraints_test.go new file mode 100644 index 0000000..0d1f06a --- /dev/null +++ b/pkg/obikmer/kmer_constraints_test.go @@ -0,0 +1,123 @@ +package obikmer + +import ( + "math" + "math/rand" + "testing" +) + +// IterConstrainedKmers must enumerate exactly the k-mers accepted by Accepts. +func TestIterConstrainedKmersMatchesAccepts(t *testing.T) { + rng := rand.New(rand.NewSource(10)) + patterns := []string{"", "nnnnnn", "yrnnnn", "cnnnnn", "annnnt"} + + for trial := 0; trial < 60; trial++ { + k := 6 + c := NoKmerConstraints() + for b := range 4 { + if rng.Intn(3) == 0 { + c.MaxCount[b] = rng.Intn(4) + } + } + if rng.Intn(2) == 0 { + c.MaxGC = rng.Intn(5) + } + if rng.Intn(2) == 0 { + c.MaxRun = 1 + rng.Intn(3) + } + if rng.Intn(2) == 0 { + c.MinRun = 2 + rng.Intn(3) + } + if p := patterns[rng.Intn(len(patterns))]; p != "" { + c.Accepted, _ = ParseIUPACMasks(p) + } + if p := patterns[rng.Intn(len(patterns))]; p != "" && rng.Intn(2) == 0 { + c.Rejected, _ = ParseIUPACMasks(p) + } + + var want []uint64 + for km := uint64(0); km < 1<<(2*uint(k)); km++ { + if c.Accepts(km, k) { + want = append(want, km) + } + } + got := collectConstrained(k, c) + if len(got) != len(want) { + t.Fatalf("trial %d %+v: got %d k-mers, want %d", trial, c, len(got), len(want)) + } + for i := range got { + if got[i] != want[i] { + t.Fatalf("trial %d %+v: mismatch at %d: %d != %d", trial, c, i, got[i], want[i]) + } + } + } +} + +func TestConstraintsGCAndRuns(t *testing.T) { + enc := func(s string) uint64 { return EncodeKmer([]byte(s), len(s)) } + + c := NoKmerConstraints() + c.MaxGC = 2 + if !c.Accepts(enc("ACGTAA"), 6) || c.Accepts(enc("ACGCAA"), 6) { + t.Fatal("MaxGC not enforced") + } + + c = NoKmerConstraints() + c.MinRun = 3 + if !c.Accepts(enc("ACCCGT"), 6) || c.Accepts(enc("ACCGGT"), 6) { + t.Fatal("MinRun not enforced") + } + + c = NoKmerConstraints() + c.Rejected, _ = ParseIUPACMasks("cnnnnn") // no C in first position + if c.Accepts(enc("CAAAAA"), 6) || !c.Accepts(enc("AAAAAA"), 6) { + t.Fatal("Rejected not enforced") + } +} + +func TestRestrictAlphabetAndPatterns(t *testing.T) { + c := NoKmerConstraints() + if err := c.RestrictAlphabet("ACT"); err != nil { + t.Fatal(err) + } + if got := len(collectConstrained(4, c)); got != 81 { + t.Fatalf("expected 3^4 = 81 k-mers, got %d", got) + } + c = NoKmerConstraints() + if err := c.RestrictAlphabet("H"); err != nil { // H = A, C or T + t.Fatal(err) + } + if c.MaxCount != [4]int{-1, -1, 0, -1} { + t.Fatalf("unexpected counts %v", c.MaxCount) + } + if err := c.RestrictAlphabet("AX"); err == nil { + t.Fatal("invalid IUPAC code must be refused") + } + if _, err := ParseIUPACMasks("acgz"); err == nil { + t.Fatal("invalid IUPAC code must be refused") + } + // wrong pattern length + c = NoKmerConstraints() + c.Accepted, _ = ParseIUPACMasks("nnn") + if got := collectConstrained(4, c); len(got) != 0 { + t.Fatalf("pattern of wrong length must give nothing, got %d", len(got)) + } +} + +func TestHammingBound(t *testing.T) { + cases := []struct { + q, k, d int + want float64 + }{ + {4, 7, 3, 16384.0 / 22}, + {4, 8, 4, 16384.0 / 22}, // d even: reduces to (7, 3) + {4, 9, 5, 262144.0 / 352}, + {3, 9, 3, 19683.0 / 19}, + {4, 5, 1, 1024}, + } + for _, c := range cases { + if got := HammingBound(c.q, c.k, c.d); math.Abs(got-c.want) > 1e-6 { + t.Errorf("HammingBound(%d,%d,%d) = %v, want %v", c.q, c.k, c.d, got, c.want) + } + } +} diff --git a/pkg/obikmer/kmer_dissimilarity.go b/pkg/obikmer/kmer_dissimilarity.go new file mode 100644 index 0000000..5f1c57a --- /dev/null +++ b/pkg/obikmer/kmer_dissimilarity.go @@ -0,0 +1,84 @@ +package obikmer + +import ( + "fmt" + "strings" + "sync" +) + +// KmerMetric selects the distance used to compare two k-mers. +type KmerMetric int + +const ( + // HammingMetric counts the positions at which two k-mers differ. + HammingMetric KmerMetric = iota + // LCSMetric is the alignment length minus the LCS length (see KmerLCS). + LCSMetric +) + +// ParseKmerMetric converts "hamming" or "lcs" (case insensitive) to a KmerMetric. +func ParseKmerMetric(name string) (KmerMetric, error) { + switch strings.ToLower(name) { + case "hamming": + return HammingMetric, nil + case "lcs": + return LCSMetric, nil + } + return HammingMetric, fmt.Errorf("unknown distance type %q (expected hamming or lcs)", name) +} + +func (m KmerMetric) String() string { + if m == LCSMetric { + return "lcs" + } + return "hamming" +} + +type lcsScratch struct { + partners []uint64 + results []KmerLCSResult +} + +// NewKmerDissimilarityFilter describes the graph whose vertices are kmers[i] +// (k-mers of size k, 1 <= k <= 31) and where two vertices are linked when +// their distance is at least dmin. The returned function has the signature of +// obigraph.NeighborFilter: it appends to out[:0] the candidates (indices in +// kmers) linked to v. It is safe for concurrent use. +func NewKmerDissimilarityFilter(kmers []uint64, k int, metric KmerMetric, dmin int) func(v int32, candidates, out []int32) []int32 { + if metric == HammingMetric { + return func(v int32, candidates, out []int32) []int32 { + q := kmers[v] + out = out[:0] + for _, c := range candidates { + if HammingDistance(q, kmers[c], k) >= dmin { + out = append(out, c) + } + } + return out + } + } + + pool := sync.Pool{New: func() any { return new(lcsScratch) }} + return func(v int32, candidates, out []int32) []int32 { + s := pool.Get().(*lcsScratch) + defer pool.Put(s) + + s.partners = s.partners[:0] + for _, c := range candidates { + s.partners = append(s.partners, kmers[c]) + } + if cap(s.results) < len(candidates) { + s.results = make([]KmerLCSResult, len(candidates)) + } + s.results = s.results[:len(candidates)] + KmerLCSBatch(kmers[v], s.partners, k, s.results) + + out = out[:0] + for i, c := range candidates { + if s.results[i].Distance() >= dmin { + out = append(out, c) + } + } + return out + } +} diff --git a/pkg/obikmer/kmer_dissimilarity_test.go b/pkg/obikmer/kmer_dissimilarity_test.go new file mode 100644 index 0000000..94a92e6 --- /dev/null +++ b/pkg/obikmer/kmer_dissimilarity_test.go @@ -0,0 +1,113 @@ +package obikmer_test + +import ( + "context" + "math/rand" + "testing" + "time" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obigraph" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obikmer" +) + +func allKmers(k int) []uint64 { + kmers := make([]uint64, 0, 1<<(2*uint(k))) + for km := range obikmer.IterConstrainedKmers(k, obikmer.NoKmerConstraints()) { + kmers = append(kmers, km) + } + return kmers +} + +func TestDissimilarityFilterMatchesDistances(t *testing.T) { + const k = 7 + rng := rand.New(rand.NewSource(1)) + kmers := make([]uint64, 300) + for i := range kmers { + kmers[i] = rng.Uint64() & (1<<(2*k) - 1) + } + candidates := make([]int32, 0, len(kmers)) + for i := 1; i < len(kmers); i++ { + candidates = append(candidates, int32(i)) + } + + for _, metric := range []obikmer.KmerMetric{obikmer.HammingMetric, obikmer.LCSMetric} { + for _, dmin := range []int{1, 3, 5} { + filter := obikmer.NewKmerDissimilarityFilter(kmers, k, metric, dmin) + got := filter(0, candidates, nil) + + var want []int32 + for _, c := range candidates { + var d int + if metric == obikmer.HammingMetric { + d = obikmer.HammingDistance(kmers[0], kmers[c], k) + } else { + d = obikmer.KmerLCS(kmers[0], kmers[c], k).Distance() + } + if d >= dmin { + want = append(want, c) + } + } + if len(got) != len(want) { + t.Fatalf("%v d>=%d: got %d vertices, want %d", metric, dmin, len(got), len(want)) + } + for i := range got { + if got[i] != want[i] { + t.Fatalf("%v d>=%d: mismatch at %d", metric, dmin, i) + } + } + } + } +} + +// A family of 5-mers pairwise at Hamming distance >= 3, built with FindClique. +func TestFindClique_HammingFamily(t *testing.T) { + const k, dmin, target = 5, 3, 32 + kmers := allKmers(k) + filter := obikmer.NewKmerDissimilarityFilter(kmers, k, obikmer.HammingMetric, dmin) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + r := obigraph.FindClique(ctx, len(kmers), filter, target, obigraph.CliqueOptions{Workers: 2}) + if !r.Reached { + t.Fatalf("family of %d not found (best %d, exhausted %v)", target, len(r.Clique), r.Exhausted) + } + for i, a := range r.Clique { + for _, b := range r.Clique[i+1:] { + if obikmer.HammingDistance(kmers[a], kmers[b], k) < dmin { + t.Fatalf("%d and %d are too close", kmers[a], kmers[b]) + } + } + } +} + +func TestFindClique_LCSFamily(t *testing.T) { + const k, dmin, target = 6, 3, 20 + kmers := allKmers(k) + filter := obikmer.NewKmerDissimilarityFilter(kmers, k, obikmer.LCSMetric, dmin) + + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + r := obigraph.FindClique(ctx, len(kmers), filter, target, obigraph.CliqueOptions{Workers: 2}) + if !r.Reached { + t.Fatalf("family of %d not found (best %d, exhausted %v)", target, len(r.Clique), r.Exhausted) + } + for i, a := range r.Clique { + for _, b := range r.Clique[i+1:] { + if obikmer.KmerLCS(kmers[a], kmers[b], k).Distance() < dmin { + t.Fatalf("%d and %d are too close", kmers[a], kmers[b]) + } + } + } +} + +func TestParseKmerMetric(t *testing.T) { + if m, err := obikmer.ParseKmerMetric("LCS"); err != nil || m != obikmer.LCSMetric { + t.Fatal("lcs not parsed") + } + if m, err := obikmer.ParseKmerMetric("hamming"); err != nil || m != obikmer.HammingMetric { + t.Fatal("hamming not parsed") + } + if _, err := obikmer.ParseKmerMetric("levenshtein"); err == nil { + t.Fatal("unknown metric must be refused") + } +} diff --git a/pkg/obiseq/iupac_nog.go b/pkg/obiseq/iupac_nog.go index a3fba9b..7565263 100644 --- a/pkg/obiseq/iupac_nog.go +++ b/pkg/obiseq/iupac_nog.go @@ -26,3 +26,16 @@ func SameIUPACNuc(a, b byte) bool { } return a == b } + +// IUPACNucMask returns the set of nucleotides denoted by the IUPAC code b +// (case insensitive, U is read as T) as a 4 bits mask: bit 0 is A, bit 1 is C, +// bit 2 is G and bit 3 is T. It returns 0 if b is not an IUPAC nucleotide code. +func IUPACNucMask(b byte) byte { + if (b >= 'A') && (b <= 'Z') { + b |= 32 + } + if (b < 'a') || (b > 'z') { + return 0 + } + return _iupac[b-'a'] +} diff --git a/pkg/obitools/obioligotag/input.go b/pkg/obitools/obioligotag/input.go new file mode 100644 index 0000000..992ba8e --- /dev/null +++ b/pkg/obitools/obioligotag/input.go @@ -0,0 +1,148 @@ +package obioligotag + +import ( + "bufio" + "fmt" + "io" + "slices" + "strings" + + log "github.com/sirupsen/logrus" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obikmer" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obitools/obiconvert" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiutils" +) + +// maxOligoSize is the longest oligonucleotide handled. +const maxOligoSize = 12 + +// isPlainOligoLine tells whether a line looks like an oligonucleotide: only +// nucleotide letters, which a sequence file title line or a header never is. +func isPlainOligoLine(line string) bool { + if line == "" { + return false + } + for i := 0; i < len(line); i++ { + if obiseq.IUPACNucMask(line[i]) == 0 { + return false + } + } + return true +} + +// isPlainOligoFile guesses whether filename is a text file with one +// oligonucleotide per line, by looking at its first significant line. +func isPlainOligoFile(filename string) (bool, error) { + file, err := obiutils.Ropen(filename) + if err != nil { + return false, err + } + defer file.Close() + + reader := bufio.NewReader(file) + for { + line, err := reader.ReadString('\n') + line = strings.TrimSpace(line) + if line != "" && !strings.HasPrefix(line, "#") { + return isPlainOligoLine(line), nil + } + if err != nil { + if err == io.EOF { + return false, fmt.Errorf("file %s is empty", filename) + } + return false, err + } + } +} + +// oligoCollector accumulates encoded oligonucleotides of a single size. +type oligoCollector struct { + size int + kmers []uint64 + skipped int +} + +func (oc *oligoCollector) add(seq []byte) error { + if len(seq) == 0 { + return nil + } + if len(seq) > maxOligoSize { + return fmt.Errorf("oligonucleotide %q is longer than the maximum size %d", seq, maxOligoSize) + } + if oc.size == 0 { + oc.size = len(seq) + } + if len(seq) != oc.size { + return fmt.Errorf("all the oligonucleotides must have the same length: found %d and %d", oc.size, len(seq)) + } + for _, b := range seq { + switch b | 32 { + case 'a', 'c', 'g', 't', 'u': + default: + oc.skipped++ + return nil + } + } + oc.kmers = append(oc.kmers, obikmer.EncodeKmer(seq, len(seq))) + return nil +} + +// readOligoList reads the oligonucleotides of filename, either as a sequence +// file or as a text file with one oligonucleotide per line. Oligonucleotides +// containing something else than A, C, G, T or U are skipped, and +// duplicates removed. The result is sorted. +func readOligoList(filename string) (kmers []uint64, size int, err error) { + plain := false + if obiconvert.CLIInputFormat() == "guessed" { + plain, err = isPlainOligoFile(filename) + if err != nil { + return nil, 0, err + } + } + + oc := &oligoCollector{} + + if plain { + file, err := obiutils.Ropen(filename) + if err != nil { + return nil, 0, err + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + if err := oc.add([]byte(line)); err != nil { + return nil, 0, err + } + } + if err := scanner.Err(); err != nil { + return nil, 0, err + } + } else { + sequences, err := obiconvert.CLIReadBioSequences(filename) + if err != nil { + return nil, 0, err + } + for sequences.Next() { + for _, s := range sequences.Get().Slice() { + if err := oc.add(s.Sequence()); err != nil { + return nil, 0, err + } + } + } + } + + if oc.skipped > 0 { + log.Warnf("%d oligonucleotides containing characters other than A, C, G, T were ignored", oc.skipped) + } + + slices.Sort(oc.kmers) + oc.kmers = slices.Compact(oc.kmers) + return oc.kmers, oc.size, nil +} diff --git a/pkg/obitools/obioligotag/obioligotag.go b/pkg/obitools/obioligotag/obioligotag.go new file mode 100644 index 0000000..bb3e93b --- /dev/null +++ b/pkg/obitools/obioligotag/obioligotag.go @@ -0,0 +1,217 @@ +package obioligotag + +import ( + "context" + "fmt" + "math" + "slices" + "time" + + log "github.com/sirupsen/logrus" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obidefault" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obigraph" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiiter" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obikmer" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq" +) + +// buildConstraints translates the command line options into constraints on +// oligonucleotides of the given size. +func buildConstraints(size int) (obikmer.KmerConstraints, error) { + c := obikmer.NoKmerConstraints() + + if err := c.RestrictAlphabet(CLIAlphabet()); err != nil { + return c, fmt.Errorf("invalid --alphabet: %w", err) + } + + c.MaxGC = CLIGCMax() + if CLIHomopolymerMax() > 0 { + c.MaxRun = CLIHomopolymerMax() + } + c.MinRun = CLIHomopolymerMin() + + if pattern := CLIAccepted(); pattern != "" { + if len(pattern) != size { + return c, fmt.Errorf("--accepted pattern length (%d) differs from the oligonucleotide size (%d)", len(pattern), size) + } + masks, err := obikmer.ParseIUPACMasks(pattern) + if err != nil { + return c, fmt.Errorf("invalid --accepted: %w", err) + } + c.Accepted = masks + } + + if pattern := CLIRejected(); pattern != "" { + if len(pattern) != size { + return c, fmt.Errorf("--rejected pattern length (%d) differs from the oligonucleotide size (%d)", len(pattern), size) + } + masks, err := obikmer.ParseIUPACMasks(pattern) + if err != nil { + return c, fmt.Errorf("invalid --rejected: %w", err) + } + c.Rejected = masks + } + + return c, nil +} + +// candidateOligos returns the sorted candidate oligonucleotides and their size: +// either every oligonucleotide satisfying the constraints, or those of the +// --oligo-list file satisfying them. +func candidateOligos() ([]uint64, int, error) { + size := CLIOligoSize() + list := CLIOligoList() + + switch { + case list != "" && size != 0: + return nil, 0, fmt.Errorf("--oligo-size and --oligo-list cannot be used together") + case list == "" && size == 0: + return nil, 0, fmt.Errorf("either --oligo-size or --oligo-list is required") + case list == "" && (size < 1 || size > maxOligoSize): + return nil, 0, fmt.Errorf("--oligo-size must be between 1 and %d", maxOligoSize) + } + + var kmers []uint64 + if list != "" { + var err error + kmers, size, err = readOligoList(list) + if err != nil { + return nil, 0, fmt.Errorf("cannot read %s: %w", list, err) + } + log.Infof("%d oligonucleotides of size %d read from %s", len(kmers), size, list) + } + + constraints, err := buildConstraints(size) + if err != nil { + return nil, 0, err + } + + if list == "" { + kmers = slices.Collect(obikmer.IterConstrainedKmers(size, constraints)) + } else { + kmers = slices.DeleteFunc(kmers, func(km uint64) bool { return !constraints.Accepts(km, size) }) + } + + if len(kmers) == 0 { + return nil, 0, fmt.Errorf("no oligonucleotide satisfies the constraints") + } + log.Infof("%d candidate oligonucleotides of size %d", len(kmers), size) + return kmers, size, nil +} + +// checkFeasibility stops the program early when the family size is beyond what +// the Hamming bound allows. +func checkFeasibility(size int, metric obikmer.KmerMetric) error { + if metric != obikmer.HammingMetric { + return nil + } + alphabet := obikmer.NoKmerConstraints() + if err := alphabet.RestrictAlphabet(CLIAlphabet()); err != nil { + return err + } + q := 0 + for _, m := range alphabet.MaxCount { + if m != 0 { + q++ + } + } + if CLIOligoList() != "" { + q = 4 // the candidates of a list are not restricted by the alphabet of the generator + } + bound := obikmer.HammingBound(q, size, CLIDistance()) + if float64(CLIFamilySize()) > math.Floor(bound+1e-9) { + return fmt.Errorf("no family of %d oligonucleotides of size %d at Hamming distance >= %d exists "+ + "(sphere-packing bound: %.0f)", CLIFamilySize(), size, CLIDistance(), math.Floor(bound+1e-9)) + } + return nil +} + +// CLIOligoFamily builds the family of oligonucleotides described by the +// command line options: it returns oligonucleotides, sorted, at a distance of at +// least --distance from each other. If no family of the required size is +// found before the time limit, the largest family found is returned together +// with a warning. +func CLIOligoFamily() ([]string, error) { + target := CLIFamilySize() + dmin := CLIDistance() + + if target < 1 { + return nil, fmt.Errorf("--family-size is required and must be at least 1") + } + if dmin < 1 { + return nil, fmt.Errorf("--distance must be at least 1") + } + metric, err := obikmer.ParseKmerMetric(CLIDistanceType()) + if err != nil { + return nil, err + } + + kmers, size, err := candidateOligos() + if err != nil { + return nil, err + } + if err := checkFeasibility(size, metric); err != nil { + return nil, err + } + + ctx := context.Background() + if CLITimeout() > 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, time.Duration(CLITimeout())*time.Second) + defer cancel() + } + + filter := obikmer.NewKmerDissimilarityFilter(kmers, size, metric, dmin) + result := obigraph.FindClique(ctx, len(kmers), filter, target, + obigraph.CliqueOptions{Workers: obidefault.ParallelWorkers(), Seed: 1}) + + switch { + case result.Reached: + log.Infof("family of %d oligonucleotides found", len(result.Clique)) + case result.Exhausted: + log.Warnf("no family of %d oligonucleotides exists with these constraints: "+ + "the best family found has %d oligonucleotides", target, len(result.Clique)) + default: + log.Warnf("time limit reached before finding a family of %d oligonucleotides: "+ + "the best family found has %d oligonucleotides", target, len(result.Clique)) + } + + family := make([]uint64, len(result.Clique)) + for i, v := range result.Clique { + family[i] = kmers[v] + } + slices.Sort(family) + + oligos := make([]string, len(family)) + for i, km := range family { + oligos[i] = string(obikmer.DecodeKmer(km, size, nil)) + } + return oligos, nil +} + +// CLIOligosToIterator wraps oligonucleotides in a sequence iterator, with the +// identifiers oligo_001, oligo_002... +func CLIOligosToIterator(oligos []string) obiiter.IBioSequence { + width := len(fmt.Sprint(len(oligos))) + batchSize := max(obidefault.BatchSize(), 1) + + iterator := obiiter.MakeIBioSequence() + iterator.Add(1) + + go func() { + defer iterator.Done() + for order, start := 0, 0; start < len(oligos); order, start = order+1, start+batchSize { + end := min(start+batchSize, len(oligos)) + slice := make(obiseq.BioSequenceSlice, 0, end-start) + for i := start; i < end; i++ { + slice = append(slice, + obiseq.NewBioSequence(fmt.Sprintf("oligo_%0*d", width, i+1), []byte(oligos[i]), "")) + } + iterator.Push(obiiter.MakeBioSequenceBatch("obioligotag", order, slice)) + } + }() + + go iterator.WaitAndClose() + return iterator +} diff --git a/pkg/obitools/obioligotag/obioligotag_test.go b/pkg/obitools/obioligotag/obioligotag_test.go new file mode 100644 index 0000000..5310d07 --- /dev/null +++ b/pkg/obitools/obioligotag/obioligotag_test.go @@ -0,0 +1,177 @@ +package obioligotag + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obikmer" +) + +// defaults puts every option back to its default value. +func defaults() { + SetOligoSize(0) + SetOligoList("") + SetFamilySize(0) + SetDistance(1) + SetDistanceType("hamming") + SetAlphabet("ACGT") + SetGCMax(-1) + SetAccepted("") + SetRejected("") + SetHomopolymerMax(-1) + SetHomopolymerMin(0) + SetTimeout(60) +} + +// reset starts a test from the default options and restores them at its end. +func reset(t *testing.T) { + t.Helper() + defaults() + t.Cleanup(defaults) +} + +func hamming(a, b string) int { + d := 0 + for i := range a { + if a[i] != b[i] { + d++ + } + } + return d +} + +func TestOligoFamilyHamming(t *testing.T) { + reset(t) + SetOligoSize(5) + SetFamilySize(24) + SetDistance(3) + SetGCMax(3) + SetHomopolymerMax(2) + + family, err := CLIOligoFamily() + if err != nil { + t.Fatal(err) + } + if len(family) < 24 { + t.Fatalf("expected at least 24 oligonucleotides, got %d", len(family)) + } + for i, a := range family { + if strings.Count(a, "g")+strings.Count(a, "c") > 3 { + t.Errorf("%s has too many G/C", a) + } + for _, h := range []string{"aaa", "ccc", "ggg", "ttt"} { + if strings.Contains(a, h) { + t.Errorf("%s has a homopolymer longer than 2", a) + } + } + for _, b := range family[i+1:] { + if hamming(a, b) < 3 { + t.Errorf("%s and %s are too close", a, b) + } + } + } +} + +func TestOligoFamilyLCSWithoutG(t *testing.T) { + reset(t) + SetOligoSize(6) + SetFamilySize(20) + SetDistance(3) + SetDistanceType("lcs") + SetAlphabet("ACT") + + family, err := CLIOligoFamily() + if err != nil { + t.Fatal(err) + } + if len(family) < 20 { + t.Fatalf("expected at least 20 oligonucleotides, got %d", len(family)) + } + for i, a := range family { + if strings.Contains(a, "g") { + t.Errorf("%s contains a G", a) + } + for _, b := range family[i+1:] { + ka, kb := obikmer.EncodeKmer([]byte(a), 6), obikmer.EncodeKmer([]byte(b), 6) + if obikmer.KmerLCS(ka, kb, 6).Distance() < 3 { + t.Errorf("%s and %s are too close", a, b) + } + } + } +} + +func TestOligoListPlainTextAndFasta(t *testing.T) { + dir := t.TempDir() + plain := filepath.Join(dir, "oligos.txt") + fasta := filepath.Join(dir, "oligos.fasta") + os.WriteFile(plain, []byte("# tags\nacgtac\nacgtat\nttttgg\nggcaac\ncaccat\n\nacgtac\nacgNac\n"), 0o644) + os.WriteFile(fasta, []byte(">a\nACGTAC\n>b\nACGTAT\n>c\nTTTTGG\n>d\nGGCAAC\n>e\nCACCAT\n"), 0o644) + + for _, file := range []string{plain, fasta} { + reset(t) + SetOligoList(file) + SetFamilySize(3) + SetDistance(3) + + kmers, size, err := candidateOligos() + if err != nil { + t.Fatalf("%s: %v", file, err) + } + if size != 6 || len(kmers) != 5 { // duplicates removed, 'acgNac' ignored in the text file + t.Fatalf("%s: got %d oligonucleotides of size %d", file, len(kmers), size) + } + family, err := CLIOligoFamily() + if err != nil || len(family) < 3 { + t.Fatalf("%s: family %v, error %v", file, family, err) + } + } +} + +func TestOligoListFilteredByConstraints(t *testing.T) { + reset(t) + file := filepath.Join(t.TempDir(), "oligos.txt") + os.WriteFile(file, []byte("aaaaaa\nacgtac\nacgtat\n"), 0o644) + SetOligoList(file) + SetFamilySize(1) + SetHomopolymerMax(2) + + kmers, _, err := candidateOligos() + if err != nil || len(kmers) != 2 { + t.Fatalf("expected the homopolymer to be filtered out, got %d (%v)", len(kmers), err) + } +} + +func TestOligoTagErrors(t *testing.T) { + cases := map[string]func(){ + "no size nor list": func() { SetFamilySize(2) }, + "size and list": func() { SetFamilySize(2); SetOligoSize(5); SetOligoList("x") }, + "no family size": func() { SetOligoSize(5) }, + "size too large": func() { SetFamilySize(2); SetOligoSize(13) }, + "bad alphabet": func() { SetFamilySize(2); SetOligoSize(5); SetAlphabet("ACX") }, + "bad pattern length": func() { SetFamilySize(2); SetOligoSize(5); SetAccepted("nnn") }, + "bad distance type": func() { SetFamilySize(2); SetOligoSize(5); SetDistanceType("levenshtein") }, + "beyond Hamming bound": func() { SetFamilySize(100); SetOligoSize(5); SetDistance(3) }, + "empty candidate set": func() { SetFamilySize(2); SetOligoSize(4); SetAlphabet("A"); SetHomopolymerMax(2) }, + } + for name, setup := range cases { + t.Run(name, func(t *testing.T) { + reset(t) + setup() + if family, err := CLIOligoFamily(); err == nil { + t.Errorf("expected an error, got %v", family) + } + }) + } +} + +func TestOligoIsPlainLine(t *testing.T) { + for line, want := range map[string]bool{ + "acgt": true, "ACGTRYN": true, ">seq1": false, "@read": false, "a,b": false, "": false, + } { + if got := isPlainOligoLine(line); got != want { + t.Errorf("isPlainOligoLine(%q) = %v, want %v", line, got, want) + } + } +} diff --git a/pkg/obitools/obioligotag/options.go b/pkg/obitools/obioligotag/options.go new file mode 100644 index 0000000..4ba194a --- /dev/null +++ b/pkg/obitools/obioligotag/options.go @@ -0,0 +1,150 @@ +// obioligotag function utility package. +// +// The obitools/obioligotag package contains every +// functions specificaly required by the obioligotag utility. +package obioligotag + +import ( + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obitools/obiconvert" + "github.com/DavidGamba/go-getoptions" +) + +var __oligo_size__ = 0 +var __oligo_list__ = "" +var __family_size__ = 0 +var __distance__ = 1 +var __distance_type__ = "hamming" +var __alphabet__ = "ACGT" +var __gc_max__ = -1 +var __accepted__ = "" +var __rejected__ = "" +var __homopolymer__ = -1 +var __homopolymer_min__ = 0 +var __timeout__ = 60 + +// OligoTagOptionSet registers the options specific to obioligotag. +func OligoTagOptionSet(options *getoptions.GetOpt) { + options.IntVar(&__oligo_size__, "oligo-size", __oligo_size__, + options.Alias("s"), + options.ArgName("SIZE"), + options.Description("Length of the oligonucleotides to generate (1 to 12). "+ + "Cannot be combined with --oligo-list.")) + + options.StringVar(&__oligo_list__, "oligo-list", __oligo_list__, + options.Alias("L"), + options.ArgName("FILENAME"), + options.Description("File of candidate oligonucleotides, in any sequence file format "+ + "or as a text file with one oligonucleotide per line (format detected automatically). "+ + "The family is selected among them. Cannot be combined with --oligo-size.")) + + options.IntVar(&__family_size__, "family-size", __family_size__, + options.Alias("f"), + options.ArgName("SIZE"), + options.Description("Minimum number of oligonucleotides of the family to build.")) + + options.IntVar(&__distance__, "distance", __distance__, + options.Alias("d"), + options.ArgName("DISTANCE"), + options.Description("Minimum distance between any two oligonucleotides of the family.")) + + options.StringVar(&__distance_type__, "distance-type", __distance_type__, + options.ArgName("TYPE"), + options.Description("Distance between oligonucleotides: 'hamming' (number of differing positions) "+ + "or 'lcs' (alignment length minus the length of the longest common subsequence).")) + + options.StringVar(&__alphabet__, "alphabet", __alphabet__, + options.ArgName("LETTERS"), + options.Description("Nucleotides allowed in the oligonucleotides, as IUPAC codes "+ + "(for instance ACT, or H, to forbid G).")) + + options.IntVar(&__gc_max__, "gc-max", __gc_max__, + options.Alias("g"), + options.ArgName("COUNT"), + options.Description("Maximum number of G or C per oligonucleotide.")) + + options.StringVar(&__accepted__, "accepted", __accepted__, + options.Alias("a"), + options.ArgName("IUPAC_PATTERN"), + options.Description("Selected oligonucleotides must match this IUPAC pattern "+ + "(of the same length as the oligonucleotides).")) + + options.StringVar(&__rejected__, "rejected", __rejected__, + options.Alias("r"), + options.ArgName("IUPAC_PATTERN"), + options.Description("Selected oligonucleotides must not match this IUPAC pattern "+ + "(of the same length as the oligonucleotides).")) + + options.IntVar(&__homopolymer__, "homopolymer", __homopolymer__, + options.Alias("p"), + options.ArgName("LENGTH"), + options.Description("Excludes the oligonucleotides containing a homopolymer longer than LENGTH.")) + + options.IntVar(&__homopolymer_min__, "homopolymer-min", __homopolymer_min__, + options.Alias("P"), + options.ArgName("LENGTH"), + options.Description("Keeps only the oligonucleotides containing a homopolymer of at least LENGTH.")) + + options.IntVar(&__timeout__, "timeout", __timeout__, + options.Alias("T"), + options.ArgName("SECONDS"), + options.Description("Time limit, in seconds, for finding a family of the required size "+ + "(0 for no limit). When it is reached, the best family found is returned with a warning.")) +} + +// OptionSet registers every option of obioligotag. +func OptionSet(options *getoptions.GetOpt) { + OligoTagOptionSet(options) + obiconvert.InputOptionSet(options) + obiconvert.OutputOptionSet(options) +} + +// CLIOligoSize returns the size of the oligonucleotides to generate. +func CLIOligoSize() int { return __oligo_size__ } + +// CLIOligoList returns the name of the file of candidate oligonucleotides. +func CLIOligoList() string { return __oligo_list__ } + +// CLIFamilySize returns the minimum size of the family to build. +func CLIFamilySize() int { return __family_size__ } + +// CLIDistance returns the minimum distance between two oligonucleotides. +func CLIDistance() int { return __distance__ } + +// CLIDistanceType returns the name of the distance used. +func CLIDistanceType() string { return __distance_type__ } + +// CLIAlphabet returns the IUPAC codes of the allowed nucleotides. +func CLIAlphabet() string { return __alphabet__ } + +// CLIGCMax returns the maximum number of G or C (negative if unlimited). +func CLIGCMax() int { return __gc_max__ } + +// CLIAccepted returns the IUPAC pattern the oligonucleotides must match. +func CLIAccepted() string { return __accepted__ } + +// CLIRejected returns the IUPAC pattern the oligonucleotides must not match. +func CLIRejected() string { return __rejected__ } + +// CLIHomopolymerMax returns the longest homopolymer allowed (<= 0 if unlimited). +func CLIHomopolymerMax() int { return __homopolymer__ } + +// CLIHomopolymerMin returns the length of the homopolymer required (<= 1 if none). +func CLIHomopolymerMin() int { return __homopolymer_min__ } + +// CLITimeout returns the time limit in seconds (0 if none). +func CLITimeout() int { return __timeout__ } + +// Setters, mainly for testing. + +func SetOligoSize(v int) { __oligo_size__ = v } +func SetOligoList(v string) { __oligo_list__ = v } +func SetFamilySize(v int) { __family_size__ = v } +func SetDistance(v int) { __distance__ = v } +func SetDistanceType(v string) { __distance_type__ = v } +func SetAlphabet(v string) { __alphabet__ = v } +func SetGCMax(v int) { __gc_max__ = v } +func SetAccepted(v string) { __accepted__ = v } +func SetRejected(v string) { __rejected__ = v } +func SetHomopolymerMax(v int) { __homopolymer__ = v } +func SetHomopolymerMin(v int) { __homopolymer_min__ = v } +func SetTimeout(v int) { __timeout__ = v } From db2b398dd0f5e379a742a9adaaf5d04880d25929 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Mon, 2 Nov 2026 22:25:02 +1100 Subject: [PATCH 05/24] Update Go version to 1.27 and enable SIMD compilation This commit updates the project to use Go version 1.27 for all build and testing processes. It also introduces support for experimental SIMD code compilation, enabling it by default while ensuring a portable scalar fallback is available. Documentation has been updated to reflect the new Go version and the use of the experimental SIMD package. --- .github/workflows/obitest.yml | 2 +- .github/workflows/release.yml | 7 ++++--- Makefile | 8 ++++++++ doc/book/installation.qmd | 2 +- install_obitools.sh | 15 +++++++++++++++ 5 files changed, 29 insertions(+), 5 deletions(-) diff --git a/.github/workflows/obitest.yml b/.github/workflows/obitest.yml index 9d0f12a..2827f65 100644 --- a/.github/workflows/obitest.yml +++ b/.github/workflows/obitest.yml @@ -12,7 +12,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: '1.23' + go-version: '1.27' - name: Checkout obitools4 project uses: actions/checkout@v5 - name: Run tests diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c8855d6..a215b51 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: "1.26" + go-version: "1.27" - name: Checkout obitools4 project uses: actions/checkout@v5 - name: Run tests @@ -54,7 +54,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: "1.26" + go-version: "1.27" - name: Extract version from tag id: get_version @@ -78,7 +78,8 @@ jobs: -v "$(pwd):/src" \ -w /src \ -e VERSION="${VERSION}" \ - golang:1.26-alpine \ + -e GOEXPERIMENT=simd \ + golang:1.27-alpine \ sh -c "apk add --no-cache gcc musl-dev zlib-dev zlib-static make && \ make LDFLAGS='-linkmode=external -extldflags=-static' obitools" mkdir -p artifacts diff --git a/Makefile b/Makefile index 2a7817b..96a0768 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,14 @@ NC := \033[0m GOFLAGS= LDFLAGS= + +# The SIMD code (pkg/obikmer, simd package of Go >= 1.27) is only compiled with +# GOEXPERIMENT=simd; without it, a portable scalar code is used instead. +# The binaries select the vector width at run time (and fall back on an +# emulation without SIMD hardware), so they run on any CPU. +# Use `make GOEXPERIMENT= obitools` to build without SIMD. +GOEXPERIMENT?=simd +export GOEXPERIMENT GOCMD=go GOBUILD=$(GOCMD) build $(GOFLAGS) $(if $(LDFLAGS),-ldflags="$(LDFLAGS)") GOGENERATE=$(GOCMD) generate diff --git a/doc/book/installation.qmd b/doc/book/installation.qmd index f015194..b96b2f5 100644 --- a/doc/book/installation.qmd +++ b/doc/book/installation.qmd @@ -8,7 +8,7 @@ All the sources of the [*OBITools4*](http://metabarcoding.org/obitools4) can be ## Prerequisites -The *OBITools4* are developped using the [GO programming language](https://go.dev/), we stick to the latest version of the language, today the $1.21.4$. If you want to download and compile the sources yourself, you first need to install the corresponding compiler on your system. Some parts of the soft are also written in C, therefore a recent C compiler is also requested, GCC on Linux or Windows, the Developer Tools on Mac. +The *OBITools4* are developped using the [GO programming language](https://go.dev/), we stick to the latest version of the language, today the $1.27$. If you want to download and compile the sources yourself, you first need to install the corresponding compiler on your system. Some computations (distances between k-mers) use the experimental `simd` package of Go: the `Makefile` compiles them with `GOEXPERIMENT=simd`, and the installation script uses a Go 1.27 compiler for that reason. The resulting programs run on any processor, a portable code being used when no SIMD instruction is available. Some parts of the soft are also written in C, therefore a recent C compiler is also requested, GCC on Linux or Windows, the Developer Tools on Mac. Whatever the installation you decide for, you will have to ensure that a C compiler is available on your system. diff --git a/install_obitools.sh b/install_obitools.sh index 9248d44..9c410d4 100755 --- a/install_obitools.sh +++ b/install_obitools.sh @@ -2,6 +2,11 @@ # Default values URL="https://go.dev/dl/" +# Series of the Go compiler used to build OBITools4. The SIMD code relies on the +# experimental simd package of Go (GOEXPERIMENT=simd), whose API may change +# between Go versions: the build is therefore tied to the series the code was +# written for rather than to the latest release. +GO_SERIES="1.27" GITHUB_REPO="https://github.com/metabarcoding/obitools4" INSTALL_DIR="/usr/local" OBITOOLS_PREFIX="" @@ -174,10 +179,16 @@ GOFILE=$(curl -s "$URL" \ | grep 'class="download"' \ | grep "\.tar\.gz" \ | sed -E 's@^.*/dl/(go[1-9].+\.tar\.gz)".*$@\1@' \ + | awk -v prefix="go${GO_SERIES}." 'index($0, prefix) == 1' \ | grep -i "$OS" \ | grep -i "$ARCH" \ | head -1) +if [ -z "$GOFILE" ]; then + echo "Error: no Go ${GO_SERIES} archive found for ${OS}/${ARCH} at ${URL}" 1>&2 + exit 1 +fi + GOURL=$(curl -s "${URL}${GOFILE}" \ | sed -E 's@^.*href="(.*\.tar\.gz)".*$@\1@') @@ -191,8 +202,12 @@ export PATH export GOPATH="$(pwd)/gopath" export GOCACHE="$(pwd)/cache" export GOTOOLCHAIN=local +# OBITools4 is built with the SIMD code enabled (it falls back on a portable +# code at run time if the processor has no SIMD instructions). +export GOEXPERIMENT=simd echo "GOROOT=$GOROOT" 1>&2 +echo "GOEXPERIMENT=$GOEXPERIMENT" 1>&2 echo "GOCACHE=$GOCACHE" 1>&2 mkdir -p "$GOPATH" "$GOCACHE" From 1d692d7e0b6312dfde80886263bda78e46d03d7f Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Mon, 2 Nov 2026 22:33:00 +1100 Subject: [PATCH 06/24] Implement progress tracking for clique finding Introduces a mechanism for tracking progress during clique finding operations. This includes implementing a progress bar state management system, handling visibility checks, and integrating progress reporting into the clique finding logic and result processing. --- pkg/obidefault/progressbar.go | 21 ++++++ pkg/obigraph/clique.go | 7 ++ pkg/obigraph/clique_test.go | 20 ++++++ pkg/obitools/obioligotag/obioligotag.go | 8 ++- pkg/obitools/obioligotag/progress.go | 86 +++++++++++++++++++++++++ 5 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 pkg/obitools/obioligotag/progress.go diff --git a/pkg/obidefault/progressbar.go b/pkg/obidefault/progressbar.go index 3bce334..d752525 100644 --- a/pkg/obidefault/progressbar.go +++ b/pkg/obidefault/progressbar.go @@ -1,5 +1,7 @@ package obidefault +import "os" + var __no_progress_bar__ = false func ProgressBar() bool { @@ -17,3 +19,22 @@ func SetNoProgressBar(b bool) { func NoProgressBarPtr() *bool { return &__no_progress_bar__ } + +// ProgressBarVisible tells whether a progress bar can be displayed: it is not +// disabled by --no-progressbar, the standard error output is a terminal and +// the standard output is not piped to another command. +func ProgressBarVisible() bool { + if !ProgressBar() { + return false + } + + if e, err := os.Stderr.Stat(); err != nil || (e.Mode()&os.ModeCharDevice) != os.ModeCharDevice { + return false + } + + if o, err := os.Stdout.Stat(); err == nil && (o.Mode()&os.ModeNamedPipe) == os.ModeNamedPipe { + return false + } + + return true +} diff --git a/pkg/obigraph/clique.go b/pkg/obigraph/clique.go index 8572dc0..3297f18 100644 --- a/pkg/obigraph/clique.go +++ b/pkg/obigraph/clique.go @@ -21,6 +21,10 @@ type CliqueOptions struct { Workers int // Seed seeds the random vertex orders of the restarts. Seed int64 + // Progress, if set, is called with the size of the best clique each time a + // larger one is found. Calls are serialized and made while a lock is held: + // it must return quickly and must not call FindClique. + Progress func(best int) } // CliqueResult is the outcome of FindClique. @@ -68,6 +72,9 @@ func FindClique(ctx context.Context, n int, filter NeighborFilter, target int, o mutex.Lock() if len(clique) > len(best) { best = append(best[:0], clique...) + if opts.Progress != nil { + opts.Progress(len(best)) + } } mutex.Unlock() } diff --git a/pkg/obigraph/clique_test.go b/pkg/obigraph/clique_test.go index b2c6cd2..0d9f0ad 100644 --- a/pkg/obigraph/clique_test.go +++ b/pkg/obigraph/clique_test.go @@ -141,3 +141,23 @@ func TestFindCliqueTimeout(t *testing.T) { t.Fatalf("invalid best clique %v", r.Clique) } } + +func TestFindCliqueProgress(t *testing.T) { + adj := randomGraph(rand.New(rand.NewSource(4)), 40, 0.7) + + var seen []int + r := FindClique(context.Background(), 40, matrixFilter(adj), 5, CliqueOptions{ + Progress: func(best int) { seen = append(seen, best) }, + }) + if !r.Reached { + t.Fatalf("size 5 should be reached: %+v", r) + } + if len(seen) == 0 || seen[len(seen)-1] != len(r.Clique) { + t.Fatalf("progress %v does not end at the clique size %d", seen, len(r.Clique)) + } + for i := 1; i < len(seen); i++ { + if seen[i] <= seen[i-1] { + t.Fatalf("progress must be strictly increasing: %v", seen) + } + } +} diff --git a/pkg/obitools/obioligotag/obioligotag.go b/pkg/obitools/obioligotag/obioligotag.go index bb3e93b..bd6dbd7 100644 --- a/pkg/obitools/obioligotag/obioligotag.go +++ b/pkg/obitools/obioligotag/obioligotag.go @@ -163,8 +163,14 @@ func CLIOligoFamily() ([]string, error) { } filter := obikmer.NewKmerDissimilarityFilter(kmers, size, metric, dmin) + progress := startCliqueProgress(target) result := obigraph.FindClique(ctx, len(kmers), filter, target, - obigraph.CliqueOptions{Workers: obidefault.ParallelWorkers(), Seed: 1}) + obigraph.CliqueOptions{ + Workers: obidefault.ParallelWorkers(), + Seed: 1, + Progress: progress.Update, + }) + progress.Stop(result.Reached) switch { case result.Reached: diff --git a/pkg/obitools/obioligotag/progress.go b/pkg/obitools/obioligotag/progress.go new file mode 100644 index 0000000..bcf5208 --- /dev/null +++ b/pkg/obitools/obioligotag/progress.go @@ -0,0 +1,86 @@ +package obioligotag + +import ( + "fmt" + "os" + "sync" + "time" + + "github.com/schollz/progressbar/v3" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obidefault" +) + +// cliqueProgress displays the progress of the search of a family: the bar goes +// from 0 to the required family size and follows the size of the best family +// found so far. A nil *cliqueProgress does nothing, which is what is used when +// no progress bar has to be displayed. +type cliqueProgress struct { + bar *progressbar.ProgressBar + max int + stop chan struct{} + done sync.WaitGroup +} + +// startCliqueProgress starts the progress bar of a search for a family of +// target oligonucleotides, or returns nil if no bar can be displayed. +func startCliqueProgress(target int) *cliqueProgress { + if !obidefault.ProgressBarVisible() { + return nil + } + + p := &cliqueProgress{ + max: target, + stop: make(chan struct{}), + bar: progressbar.NewOptions(target, + progressbar.OptionSetWriter(os.Stderr), + progressbar.OptionSetWidth(15), + progressbar.OptionShowCount(), + progressbar.OptionSetElapsedTime(true), + progressbar.OptionSetPredictTime(false), + progressbar.OptionThrottle(100*time.Millisecond), + progressbar.OptionSetDescription("[Searching a family of oligonucleotides]"), + ), + } + + // The search may stay a long time on the same family: keep the elapsed + // time alive. + p.done.Add(1) + go func() { + defer p.done.Done() + ticker := time.NewTicker(500 * time.Millisecond) + defer ticker.Stop() + for { + select { + case <-ticker.C: + p.bar.Add(0) + case <-p.stop: + return + } + } + }() + + return p +} + +// Update reports the size of the best family found so far. +func (p *cliqueProgress) Update(best int) { + if p != nil { + p.bar.Set(min(best, p.max)) + } +} + +// Stop ends the display. The bar is filled only if the family was found. +func (p *cliqueProgress) Stop(reached bool) { + if p == nil { + return + } + close(p.stop) + p.done.Wait() + if reached { + p.bar.Finish() + } else { + p.bar.Exit() + } + fmt.Fprintln(os.Stderr) +} From f3250984a5e0ecbce027ed2ed5b4e1ef8177bf69 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Mon, 2 Nov 2026 22:54:00 +1100 Subject: [PATCH 07/24] Implement robust clique search configuration and testing The changes introduce new configuration options for clique searching, state tracking for search outcomes, and logic for handling unreachable targets. Tests are added to verify search modes, failure conditions, and the correctness of coloring bounds. --- pkg/obigraph/clique.go | 210 +++++++++++++++++++++--- pkg/obigraph/clique_test.go | 86 ++++++++++ pkg/obikmer/kmer_dissimilarity_test.go | 26 +++ pkg/obitools/obioligotag/obioligotag.go | 18 +- 4 files changed, 311 insertions(+), 29 deletions(-) diff --git a/pkg/obigraph/clique.go b/pkg/obigraph/clique.go index 3297f18..4631368 100644 --- a/pkg/obigraph/clique.go +++ b/pkg/obigraph/clique.go @@ -4,6 +4,7 @@ import ( "context" "math/rand" "sync" + "sync/atomic" ) // NeighborFilter defines an implicit graph over the vertices 0..n-1. @@ -25,8 +26,25 @@ type CliqueOptions struct { // larger one is found. Calls are serialized and made while a lock is held: // it must return quickly and must not call FindClique. Progress func(best int) + + // ColorLimit enables the coloring bound for the candidate lists of at most + // ColorLimit vertices (0 disables it). The vertices of a list are split into + // classes of pairwise non-adjacent vertices: a clique takes at most one + // vertex per class, so the number of classes bounds its size. Computing the + // classes costs up to the square of the list length in calls to the filter, + // which is why only short lists are colored. + ColorLimit int + + // mode forces the kind of search (tests only): 0 alternates, modeFocused + // and modeImproving restrict the search to one of them. + mode int } +const ( + modeFocused = 1 + modeImproving = 2 +) + // CliqueResult is the outcome of FindClique. type CliqueResult struct { // Clique is the largest clique found (a clique of size >= target if Reached). @@ -36,6 +54,9 @@ type CliqueResult struct { // Exhausted is true if the whole search space has been explored. If Reached // is false, it proves that no clique of the target size exists. Exhausted bool + // Maximum is true if Clique is proven to be a maximum clique of the graph: + // no larger clique exists. + Maximum bool } // FindClique looks for a clique of at least target vertices in the graph of n @@ -43,12 +64,28 @@ type CliqueResult struct { // search space is exhausted, or when ctx is done; in the last case, the largest // clique met so far is returned. // -// The search is a depth first search with backtracking, pruned when the -// current clique plus the remaining candidates cannot reach target. The first -// pass of the first worker follows the natural order of the vertices (greedy -// "lexicographic" construction); every other pass restarts from a random -// order, with a work budget that doubles at each restart, so that the search -// is complete in the long run. +// The search is a depth first search with backtracking, which comes in two +// flavours that the restarts alternate: +// +// - the focused search prunes every branch that cannot reach target, which +// is the most efficient way to reach it but gives poor cliques when target +// is out of reach; +// - the improving search only prunes the branches that cannot beat the best +// clique found so far (by any worker), so it keeps improving that clique +// whatever target is. When it ends, the best clique is a maximum one. +// +// When a focused search proves that no clique of target vertices exists, every +// search turns to the improving flavour, so that the largest clique is returned +// (and proved to be so) instead of an arbitrary one. +// +// On short candidate lists (see CliqueOptions.ColorLimit), a coloring bound +// prunes the branches that cannot be large enough, and orders the vertices so +// that the most constrained ones are tried first. +// +// The first pass of the first worker follows the natural order of the vertices +// (greedy "lexicographic" construction); every other pass restarts from a +// random order, with a work budget that doubles at each restart, so that the +// search is complete in the long run. func FindClique(ctx context.Context, n int, filter NeighborFilter, target int, opts CliqueOptions) CliqueResult { if target <= 0 { return CliqueResult{Reached: true, Exhausted: true} @@ -64,14 +101,20 @@ func FindClique(ctx context.Context, n int, filter NeighborFilter, target int, o var ( mutex sync.Mutex best []int32 + bestSize atomic.Int32 exhausted bool + maximum bool wg sync.WaitGroup + // set when a focused search has proved that target cannot be reached: + // every search then turns to finding the largest clique + unreachable atomic.Bool ) publish := func(clique []int32) { mutex.Lock() if len(clique) > len(best) { best = append(best[:0], clique...) + bestSize.Store(int32(len(best))) if opts.Progress != nil { opts.Progress(len(best)) } @@ -82,9 +125,12 @@ func FindClique(ctx context.Context, n int, filter NeighborFilter, target int, o for w := range workers { wg.Go(func() { s := &cliqueSearch{ - ctx: ctx, - filter: filter, - target: target, + ctx: ctx, + filter: filter, + target: target, + bestSize: &bestSize, + colorLimit: opts.ColorLimit, + unreachable: &unreachable, publish: func(c []int32) { publish(c) if len(c) >= target { @@ -93,15 +139,24 @@ func FindClique(ctx context.Context, n int, filter NeighborFilter, target int, o }, } rng := rand.New(rand.NewSource(opts.Seed + int64(w))) + // the search starts from the end of the list: lowest vertex first candidates := make([]int32, n) for i := range candidates { - candidates[i] = int32(i) + candidates[i] = int32(n - 1 - i) } for restart := 0; ctx.Err() == nil; restart++ { if w > 0 || restart > 0 { rng.Shuffle(n, func(i, j int) { candidates[i], candidates[j] = candidates[j], candidates[i] }) } + switch opts.mode { + case modeFocused: + s.improving = false + case modeImproving: + s.improving = true + default: + s.improving = unreachable.Load() || (w+restart)%2 == 1 + } s.budget = int64(n) * int64(target) << min(restart, 20) s.work = 0 s.aborted = false @@ -110,8 +165,17 @@ func FindClique(ctx context.Context, n int, filter NeighborFilter, target int, o if !s.aborted && ctx.Err() == nil { mutex.Lock() exhausted = true + if s.improving { + maximum = true + } mutex.Unlock() - cancel() + + if s.improving || opts.mode == modeFocused { + cancel() + } else { + // target is out of reach: look for the largest clique + unreachable.Store(true) + } } } }) @@ -122,47 +186,143 @@ func FindClique(ctx context.Context, n int, filter NeighborFilter, target int, o Clique: best, Reached: len(best) >= target, Exhausted: exhausted, + Maximum: maximum, } } type cliqueSearch struct { - ctx context.Context - filter NeighborFilter - target int - publish func([]int32) + ctx context.Context + filter NeighborFilter + target int + bestSize *atomic.Int32 // size of the best clique found by any worker + publish func([]int32) + colorLimit int + + unreachable *atomic.Bool // target is proved out of reach + + improving bool // see FindClique: the kind of the current pass current []int32 // the clique under construction buffers [][]int32 // candidate list of each depth - bestLen int + sorted [][]int32 // candidate list of each depth, sorted by color + colors [][]int32 // color (from 1) of each vertex of sorted + classes [][]int32 // scratch space of the coloring + probe []int32 // scratch space of the coloring budget int64 // maximum work (number of candidates tested) for this restart work int64 aborted bool // budget or context exhausted before the end of the exploration } +// threshold is the size a clique must reach to be worth exploring: the target +// for the focused search, one more than the best clique for the improving one. +func (s *cliqueSearch) threshold() int { + if s.improving { + return min(s.target, int(s.bestSize.Load())+1) + } + return s.target +} + +// mustStop tells whether the current pass has to be interrupted: its work +// budget is spent, or the pass must stop whatever its budget (see mustAbort). +func (s *cliqueSearch) mustStop() bool { + return s.work > s.budget || s.mustAbort() +} + +// mustAbort tells whether the pass has to be interrupted whatever its budget: +// the context is done, or it is a focused pass and its target has been proved +// out of reach. +func (s *cliqueSearch) mustAbort() bool { + return s.ctx.Err() != nil || (!s.improving && s.unreachable.Load()) +} + +// colorSort splits candidates into classes of pairwise non-adjacent vertices +// (greedy sequential coloring) and returns them class after class, with the +// color of each vertex. Any clique takes at most one vertex of each class. +// +// The coloring can be long: it stops, and sets s.aborted, as soon as the search +// has to be aborted. Its cost is not charged to the work budget of the pass, +// which limits the exploration of the tree, not the bound computed at each node. +func (s *cliqueSearch) colorSort(depth int, candidates []int32) (sorted, colors []int32) { + k := 0 + for _, v := range candidates { + if s.mustAbort() { + s.aborted = true + return nil, nil + } + placed := false + for c := 0; c < k; c++ { + // v joins the class if it is adjacent to none of its members + s.probe = s.filter(v, s.classes[c], s.probe[:0]) + if len(s.probe) == 0 { + s.classes[c] = append(s.classes[c], v) + placed = true + break + } + } + if !placed { + if k == len(s.classes) { + s.classes = append(s.classes, nil) + } + s.classes[k] = append(s.classes[k][:0], v) + k++ + } + } + + for len(s.sorted) <= depth { + s.sorted = append(s.sorted, nil) + s.colors = append(s.colors, nil) + } + sorted, colors = s.sorted[depth][:0], s.colors[depth][:0] + for c := 0; c < k; c++ { + for _, v := range s.classes[c] { + sorted = append(sorted, v) + colors = append(colors, int32(c+1)) + } + } + s.sorted[depth], s.colors[depth] = sorted, colors + return sorted, colors +} + +// search extends the current clique of depth vertices with vertices taken in +// candidates, all adjacent to the vertices of the current clique. The list is +// read from its end: the vertex at position i is combined with the vertices +// before it, so that every clique is met once. func (s *cliqueSearch) search(depth int, candidates []int32) { - for i := range candidates { - if depth+len(candidates)-i < s.target { + var colors []int32 + if s.colorLimit > 0 && len(candidates) > 1 && len(candidates) <= s.colorLimit { + candidates, colors = s.colorSort(depth, candidates) + if s.aborted { return } - if s.work > s.budget || s.ctx.Err() != nil { + } + + for i := len(candidates) - 1; i >= 0; i-- { + // largest clique the vertices 0..i can make + bound := i + 1 + if colors != nil { + bound = int(colors[i]) + } + if depth+bound < s.threshold() { + return + } + if s.mustStop() { s.aborted = true return } v := candidates[i] s.current = append(s.current[:depth], v) - if len(s.current) > s.bestLen { - s.bestLen = len(s.current) + if len(s.current) > int(s.bestSize.Load()) { s.publish(s.current) - if s.bestLen >= s.target { + if len(s.current) >= s.target { s.aborted = true // the search is over: do not report an exhaustion return } } - rest := candidates[i+1:] - if depth+1+len(rest) < s.target { + rest := candidates[:i] + if depth+1+len(rest) < s.threshold() { continue } for len(s.buffers) <= depth+1 { @@ -172,7 +332,7 @@ func (s *cliqueSearch) search(depth int, candidates []int32) { s.buffers[depth+1] = next s.work += int64(len(rest)) - if depth+1+len(next) < s.target { + if depth+1+len(next) < s.threshold() { continue } s.search(depth+1, next) diff --git a/pkg/obigraph/clique_test.go b/pkg/obigraph/clique_test.go index 0d9f0ad..c71e2b0 100644 --- a/pkg/obigraph/clique_test.go +++ b/pkg/obigraph/clique_test.go @@ -161,3 +161,89 @@ func TestFindCliqueProgress(t *testing.T) { } } } + +// The improving search must end with a proven maximum clique, even when the +// target is out of reach; the focused one only proves the absence of a clique +// of the target size. +func TestFindCliqueModes(t *testing.T) { + rng := rand.New(rand.NewSource(5)) + for trial := 0; trial < 40; trial++ { + n := 8 + rng.Intn(10) + adj := randomGraph(rng, n, 0.3+0.5*rng.Float64()) + maxSize := bruteMaxClique(adj) + filter := matrixFilter(adj) + + for _, workers := range []int{1, 3} { + for _, limit := range []int{0, 5, 1 << 20} { + r := FindClique(context.Background(), n, filter, maxSize+1, + CliqueOptions{Workers: workers, ColorLimit: limit, mode: modeImproving}) + if r.Reached || !r.Exhausted || !r.Maximum { + t.Fatalf("trial %d limit %d: improving search should prove the maximum: %+v", trial, limit, r) + } + if len(r.Clique) != maxSize || !isClique(adj, r.Clique) { + t.Fatalf("trial %d limit %d: expected a maximum clique of %d, got %v", trial, limit, maxSize, r.Clique) + } + + r = FindClique(context.Background(), n, filter, maxSize+1, + CliqueOptions{Workers: workers, ColorLimit: limit, mode: modeFocused}) + if r.Reached || !r.Exhausted || r.Maximum { + t.Fatalf("trial %d limit %d: focused search proves absence only: %+v", trial, limit, r) + } + + r = FindClique(context.Background(), n, filter, maxSize, + CliqueOptions{Workers: workers, ColorLimit: limit, mode: modeImproving}) + if !r.Reached || len(r.Clique) < maxSize || !isClique(adj, r.Clique) { + t.Fatalf("trial %d limit %d: target %d not reached by the improving search: %+v", trial, limit, maxSize, r) + } + } + } + } +} + +// When the target is out of reach, the default search must end with a proven +// maximum clique, even if the coloring bound proves the absence of the target +// before any clique has been found. +func TestFindCliqueUnreachableTargetGivesMaximum(t *testing.T) { + rng := rand.New(rand.NewSource(6)) + for trial := 0; trial < 40; trial++ { + n := 8 + rng.Intn(10) + adj := randomGraph(rng, n, 0.3+0.5*rng.Float64()) + maxSize := bruteMaxClique(adj) + + for _, workers := range []int{1, 3} { + for _, limit := range []int{0, 5, 1 << 20} { + r := FindClique(context.Background(), n, matrixFilter(adj), maxSize+1, + CliqueOptions{Workers: workers, ColorLimit: limit}) + if r.Reached || !r.Exhausted || !r.Maximum { + t.Fatalf("trial %d workers %d limit %d: expected a proven maximum: %+v", trial, workers, limit, r) + } + if len(r.Clique) != maxSize || !isClique(adj, r.Clique) { + t.Fatalf("trial %d workers %d limit %d: expected a maximum clique of %d, got %v", + trial, workers, limit, maxSize, r.Clique) + } + } + } + } +} + +// A long coloring must not delay the deadline. +func TestFindCliqueColoringHonoursDeadline(t *testing.T) { + const n = 3000 + adj := randomGraph(rand.New(rand.NewSource(7)), n, 0.5) + base := matrixFilter(adj) + slow := func(v int32, candidates, out []int32) []int32 { + time.Sleep(200 * time.Microsecond) + return base(v, candidates, out) + } + + ctx, cancel := context.WithTimeout(context.Background(), 300*time.Millisecond) + defer cancel() + start := time.Now() + r := FindClique(ctx, n, slow, 100, CliqueOptions{Workers: 2, ColorLimit: 1 << 20}) + if elapsed := time.Since(start); elapsed > 2*time.Second { + t.Fatalf("deadline not honoured by the coloring: %v", elapsed) + } + if r.Reached || !isClique(adj, r.Clique) { + t.Fatalf("unexpected result %+v", r) + } +} diff --git a/pkg/obikmer/kmer_dissimilarity_test.go b/pkg/obikmer/kmer_dissimilarity_test.go index 94a92e6..1c52306 100644 --- a/pkg/obikmer/kmer_dissimilarity_test.go +++ b/pkg/obikmer/kmer_dissimilarity_test.go @@ -111,3 +111,29 @@ func TestParseKmerMetric(t *testing.T) { t.Fatal("unknown metric must be refused") } } + +// With a target out of reach (17 4-mers at Hamming distance >= 3 do not exist, +// 16 do), the search must return the best family it can build, not a poor one +// stuck by the pruning on the target, and the coloring bound must prove that +// the target is out of reach. +func TestFindClique_BestWhenTargetOutOfReach(t *testing.T) { + const k, dmin = 4, 3 + kmers := allKmers(k) + filter := obikmer.NewKmerDissimilarityFilter(kmers, k, obikmer.HammingMetric, dmin) + + for _, limit := range []int{0, 1024} { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + r := obigraph.FindClique(ctx, len(kmers), filter, 17, obigraph.CliqueOptions{Workers: 2, ColorLimit: limit}) + cancel() + if r.Reached { + t.Fatal("17 4-mers at Hamming distance >= 3 cannot exist (sphere-packing bound 19, true maximum 16)") + } + if len(r.Clique) < 16 { + t.Fatalf("limit %d: expected the family of 16 to be found, got %d (exhausted %v, maximum %v)", + limit, len(r.Clique), r.Exhausted, r.Maximum) + } + if limit > 0 && !r.Exhausted { + t.Fatalf("limit %d: the coloring bound should prove that 17 is out of reach", limit) + } + } +} diff --git a/pkg/obitools/obioligotag/obioligotag.go b/pkg/obitools/obioligotag/obioligotag.go index bd6dbd7..f23326e 100644 --- a/pkg/obitools/obioligotag/obioligotag.go +++ b/pkg/obitools/obioligotag/obioligotag.go @@ -16,6 +16,12 @@ import ( "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq" ) +// cliqueColorLimit is the length of the longest list of candidates for which the +// search computes its coloring bound. Measured on 7-mers and 8-mers, the larger +// it is the better the families found in a given time, with little gain beyond +// 1024 (while the cost of a coloring grows with the square of the length). +const cliqueColorLimit = 1024 + // buildConstraints translates the command line options into constraints on // oligonucleotides of the given size. func buildConstraints(size int) (obikmer.KmerConstraints, error) { @@ -166,21 +172,25 @@ func CLIOligoFamily() ([]string, error) { progress := startCliqueProgress(target) result := obigraph.FindClique(ctx, len(kmers), filter, target, obigraph.CliqueOptions{ - Workers: obidefault.ParallelWorkers(), - Seed: 1, - Progress: progress.Update, + Workers: obidefault.ParallelWorkers(), + Seed: 1, + ColorLimit: cliqueColorLimit, + Progress: progress.Update, }) progress.Stop(result.Reached) switch { case result.Reached: log.Infof("family of %d oligonucleotides found", len(result.Clique)) + case result.Maximum: + log.Warnf("no family of %d oligonucleotides exists with these constraints: "+ + "the largest one has %d oligonucleotides (proven maximum)", target, len(result.Clique)) case result.Exhausted: log.Warnf("no family of %d oligonucleotides exists with these constraints: "+ "the best family found has %d oligonucleotides", target, len(result.Clique)) default: log.Warnf("time limit reached before finding a family of %d oligonucleotides: "+ - "the best family found has %d oligonucleotides", target, len(result.Clique)) + "the best family found has %d oligonucleotides (not proven maximum)", target, len(result.Clique)) } family := make([]uint64, len(result.Clique)) From 75bb635a6cfcb517c8366d292ec3784e3d7123c9 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Mon, 2 Nov 2026 23:48:51 +1100 Subject: [PATCH 08/24] Implement Banded LCS calculation with scalar and SIMD support Introduces the BandedLCS function for efficient LCS scoring, supporting both scalar and SIMD implementations. Adds comprehensive unit tests, benchmarking utilities, and error handling for sequence constraints. --- pkg/obialign/fastlcs_bench_test.go | 111 +++++++++++ pkg/obialign/fastlcsegf.go | 23 ++- pkg/obialign/lcsband.go | 175 ++++++++++++++++++ pkg/obialign/lcsband_nosimd.go | 13 ++ pkg/obialign/lcsband_scalar.go | 53 ++++++ pkg/obialign/lcsband_simd.go | 107 +++++++++++ pkg/obialign/lcsband_test.go | 288 +++++++++++++++++++++++++++++ 7 files changed, 763 insertions(+), 7 deletions(-) create mode 100644 pkg/obialign/fastlcs_bench_test.go create mode 100644 pkg/obialign/lcsband.go create mode 100644 pkg/obialign/lcsband_nosimd.go create mode 100644 pkg/obialign/lcsband_scalar.go create mode 100644 pkg/obialign/lcsband_simd.go create mode 100644 pkg/obialign/lcsband_test.go diff --git a/pkg/obialign/fastlcs_bench_test.go b/pkg/obialign/fastlcs_bench_test.go new file mode 100644 index 0000000..1c4c554 --- /dev/null +++ b/pkg/obialign/fastlcs_bench_test.go @@ -0,0 +1,111 @@ +package obialign + +import ( + "fmt" + "math/rand" + "testing" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq" +) + +// randomDNA returns a random ACGT sequence of length n. +func randomDNA(rng *rand.Rand, n int) []byte { + const nuc = "ACGT" + s := make([]byte, n) + for i := range s { + s[i] = nuc[rng.Intn(4)] + } + return s +} + +// mutate returns a copy of s in which each position is, with probability +// rate, substituted, deleted or followed by an inserted base (equiprobable). +func mutate(rng *rand.Rand, s []byte, rate float64) []byte { + const nuc = "ACGT" + out := make([]byte, 0, len(s)+len(s)/10) + for _, c := range s { + if rng.Float64() >= rate { + out = append(out, c) + continue + } + switch rng.Intn(3) { + case 0: + out = append(out, nuc[rng.Intn(4)]) + case 1: // deletion + case 2: + out = append(out, c, nuc[rng.Intn(4)]) + } + } + return out +} + +// benchPairs builds a fixed set of pairs. "similar" pairs differ by about +// 2% of mutated positions, "random" pairs are unrelated sequences. +func benchPairs(n, length int, similar bool) [][2]*obiseq.BioSequence { + rng := rand.New(rand.NewSource(42)) + pairs := make([][2]*obiseq.BioSequence, n) + for i := range pairs { + a := randomDNA(rng, length) + var b []byte + if similar { + b = mutate(rng, a, 0.02) + } else { + b = randomDNA(rng, length) + } + pairs[i] = [2]*obiseq.BioSequence{ + obiseq.NewBioSequence("a", a, ""), + obiseq.NewBioSequence("b", b, ""), + } + } + return pairs +} + +// BenchmarkLegacyLCS measures the historical implementation (the one +// FastLCSScore used before BandedLCS), kept as the "before" reference. +func BenchmarkLegacyLCS(b *testing.B) { + for _, similar := range []bool{true, false} { + kind := "random" + if similar { + kind = "similar" + } + for _, length := range []int{100, 500, 2000} { + pairs := benchPairs(64, length, similar) + for _, maxErr := range []int{-1, 5, length / 20, length / 5} { + name := fmt.Sprintf("%s/len=%d/maxErr=%d", kind, length, maxErr) + b.Run(name, func(b *testing.B) { + var buffer []uint64 + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + p := pairs[i%len(pairs)] + FastLCSEGFScoreByte(p[0].Sequence(), p[1].Sequence(), maxErr, false, &buffer) + } + }) + } + } + } +} + +// BenchmarkBandedLCS runs on the same pairs as BenchmarkLegacyLCS. +func BenchmarkBandedLCS(b *testing.B) { + for _, similar := range []bool{true, false} { + kind := "random" + if similar { + kind = "similar" + } + for _, length := range []int{100, 500, 2000} { + pairs := benchPairs(64, length, similar) + for _, maxErr := range []int{-1, 5, length / 20, length / 5} { + name := fmt.Sprintf("%s/len=%d/maxErr=%d", kind, length, maxErr) + b.Run(name, func(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + p := pairs[i%len(pairs)] + BandedLCS(p[0].Sequence(), p[1].Sequence(), maxErr) + } + }) + } + } + } +} diff --git a/pkg/obialign/fastlcsegf.go b/pkg/obialign/fastlcsegf.go index bce5d06..691f79b 100644 --- a/pkg/obialign/fastlcsegf.go +++ b/pkg/obialign/fastlcsegf.go @@ -2,6 +2,7 @@ package obialign import ( "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq" + log "github.com/sirupsen/logrus" ) // FastLCSEGFScoreByte calculates the score of the Longest Common Subsequence (LCS) between two byte slices. @@ -355,27 +356,35 @@ func FastLCSEGFScore(seqA, seqB *obiseq.BioSequence, maxError int, buffer *[]uin return FastLCSEGFScoreByte(seqA.Sequence(), seqB.Sequence(), maxError, true, buffer) } -// FastLCSScore calculates the score of the longest common subsequence between two bio sequences. +// FastLCSScore calculates the score of the longest common subsequence between two bio sequences, +// and the length of the shortest alignment achieving it (the distance is alilen - lcs). // -// if maxError > 0, the maximum allowed error between the sequences is maxError. -// Otherwise, no error checking is done. -// If the actual number of errors is larger than maxError, -1 is returned for both values. +// If maxError >= 0, the result is exact when its distance is at most maxError, +// and (-1, -1) is returned otherwise. If maxError < 0, no limit is applied. +// See BandedLCS, which does the work. // // The score matrix is: -// - Matching: 1 +// - Matching: 1 (bases are compared with the IUPAC rules) // - Mismatch or gap: 0 // +// The program stops with a fatal error if a sequence is longer than +// MaxBandedLCSLength. +// // Parameters: // - seqA: The first bio sequence. // - seqB: The second bio sequence. // - maxError: The maximum allowed error between the sequences. If set to -1, no limit is applied. -// - buffer: A pointer to a uint64 slice to store intermediate results. If nil, a new slice is created. +// - buffer: Unused, kept for compatibility; the working memory is pooled. // // Returns: // - The score of the longest common subsequence. // - The length of the shortest alignment corresponding to the LCS. func FastLCSScore(seqA, seqB *obiseq.BioSequence, maxError int, buffer *[]uint64) (int, int) { - score, alilen, _ := FastLCSEGFScoreByte(seqA.Sequence(), seqB.Sequence(), maxError, false, buffer) + score, alilen, err := BandedLCS(seqA.Sequence(), seqB.Sequence(), maxError) + if err != nil { + log.Fatalf("FastLCSScore on sequences %s (%d bp) and %s (%d bp): %v (maximum %d bp)", + seqA.Id(), seqA.Len(), seqB.Id(), seqB.Len(), err, MaxBandedLCSLength) + } return score, alilen } diff --git a/pkg/obialign/lcsband.go b/pkg/obialign/lcsband.go new file mode 100644 index 0000000..3c0c095 --- /dev/null +++ b/pkg/obialign/lcsband.go @@ -0,0 +1,175 @@ +package obialign + +import ( + "errors" + "sync" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq" +) + +// MaxBandedLCSLength is the longest sequence BandedLCS accepts. The score and +// the alignment length are packed on 16 bits each, and the alignment length +// can reach the sum of both sequence lengths. +const MaxBandedLCSLength = 1<<15 - 1 + +// ErrSequenceTooLong is returned by BandedLCS when a sequence is longer than +// MaxBandedLCSLength. +var ErrSequenceTooLong = errors.New("obialign: sequence too long for BandedLCS") + +// A DP cell is packed on 32 bits as +// +// score<<16 | (lcsLenBase - alignment length) +// +// so that the maximum of two cells ranks them by score first and, for equal +// scores, by shortest alignment. The three moves then reduce to +// +// cell(i,j) = max(diag + match<<16, up, left) - 1 +// +// since every move lengthens the alignment by one and only a diagonal match +// adds one to the score. The value 0 stands for an unavailable cell (outside +// the band): it loses against any real cell. Every cell of the band has at +// least one real neighbour (the band is connected and contains (0,0)), so +// the subtraction never wraps for a cell that is used; the scalar reference +// still clamps the maximum to 1 as a safety net. +const ( + lcsScoreUnit = uint32(1) << 16 + lcsLenBase = uint32(0xFFFF) +) + +// bandedLCSWorkspace holds the scratch memory of one alignment. +type bandedLCSWorkspace struct { + aRev, bCode []uint32 // encoded bases (aRev is reversed), SIMD kernel only + rows [3][]uint32 // antidiagonal (SIMD) or row (scalar) buffers + widened int // number of alignments that needed a second pass +} + +var bandedLCSPool = sync.Pool{New: func() any { return new(bandedLCSWorkspace) }} + +func growUint32(s []uint32, n int) []uint32 { + if cap(s) < n { + return make([]uint32, n) + } + return s[:n] +} + +// iupacMask gives for each byte its 4-bit nucleotide set (A=1, C=2, G=4, +// T=8), or 0 when the byte is not an IUPAC letter. For two bytes of non-zero +// mask, SameIUPACNuc(a, b) is exactly (mask[a] & mask[b]) != 0. +var iupacMask = buildIUPACMask() + +func buildIUPACMask() (m [256]uint8) { + probes := [4]byte{'a', 'c', 'g', 't'} + for c := 0; c < 256; c++ { + if !(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z') { + continue + } + for bit, p := range probes { + if obiseq.SameIUPACNuc(byte(c), p) { + m[c] |= 1 << uint(bit) + } + } + } + return m +} + +// allRegularIUPAC reports whether every base has a non-zero IUPAC mask, in +// which case base compatibility is a plain AND of masks. +func allRegularIUPAC(s []byte) bool { + for _, c := range s { + if iupacMask[c] == 0 { + return false + } + } + return true +} + +// bandOffsets returns the diagonal offsets k = j - i covered by a band of +// half-width g around the diagonals 0..delta, with sequence A (columns j) not +// shorter than sequence B (rows i), clipped to the matrix. +func bandOffsets(lA, lB, g int) (kmin, kmax int) { + return max(-g, -lB), min(lA-lB+g, lA) +} + +// BandedLCS computes the score of the longest common subsequence of two +// nucleotide sequences, and the length of the shortest alignment achieving +// it (match: +1, mismatch and gap: 0; bases are compared with the IUPAC +// rules of obiseq.SameIUPACNuc). The distance is alilen - lcs. +// +// If maxError >= 0, the result is the optimal (lcs, alilen) when its distance +// is at most maxError, and (-1, -1) otherwise. This is exact, not a +// heuristic: the DP is restricted to a band, and a bound on what a path +// leaving the band can score proves when the band was wide enough. maxError < 0 +// means no limit. +// +// An error is returned when a sequence is longer than MaxBandedLCSLength. +func BandedLCS(seqA, seqB []byte, maxError int) (lcs, alilen int, err error) { + if len(seqA) > MaxBandedLCSLength || len(seqB) > MaxBandedLCSLength { + return -1, -1, ErrSequenceTooLong + } + if len(seqA) < len(seqB) { + seqA, seqB = seqB, seqA + } + ws := bandedLCSPool.Get().(*bandedLCSWorkspace) + lcs, alilen = bandedLCS(seqA, seqB, maxError, ws, haveSIMD) + bandedLCSPool.Put(ws) + return lcs, alilen, nil +} + +// bandedLCS expects len(a) >= len(b), both within MaxBandedLCSLength. +// +// With delta = lA - lB and a band of half-width g (offsets -g .. delta+g): +// - an alignment with at most E errors needs at least delta+2h gaps to reach +// the offset delta+h or -h, so it lies in the band of g0 = (E-delta)/2; +// - a path leaving a band of half-width g makes at least g+1 gaps on the B +// side, hence scores at most lB-g-1 matches, and has more than E errors +// as soon as g >= g0. +// +// Let Q be the best path of the band g0. If its distance exceeds E, the +// global optimum cannot be within E either (it would be in the band and be +// Q): the answer is -1. Otherwise, if score(Q) >= lB-g0-1 no outside path can +// beat Q (a tie in score loses on length, the outside path having more errors +// than Q): Q is the global optimum. Else the band is widened once to +// g1 = min(E-delta, lB-score(Q)-1); since score(Q) >= lA-E for any alignment +// within E errors, g1 always certifies the result. +func bandedLCS(a, b []byte, maxError int, ws *bandedLCSWorkspace, useSIMD bool) (int, int) { + lA, lB := len(a), len(b) + delta := lA - lB + if maxError >= 0 && maxError < delta { + return -1, -1 + } + if lB == 0 { + return 0, lA + } + simdOK := useSIMD && allRegularIUPAC(a) && allRegularIUPAC(b) + if simdOK { + encodeBases(ws, a, b) + } + run := func(g int) (int, int) { + kmin, kmax := bandOffsets(lA, lB, g) + var key uint32 + if simdOK { + key = bandedLCSSIMD(lA, lB, kmin, kmax, ws) + } else { + key = bandedLCSScalar(a, b, kmin, kmax, ws) + } + return int(key >> 16), int(lcsLenBase - key&0xFFFF) + } + + if maxError < 0 { + return run(lA) + } + g0 := (maxError - delta) / 2 + lcs, alilen := run(g0) + if alilen-lcs > maxError { + return -1, -1 + } + if lcs >= lB-g0-1 { + return lcs, alilen + } + ws.widened++ + lcs, alilen = run(min(maxError-delta, lB-lcs-1)) + if alilen-lcs > maxError { + return -1, -1 + } + return lcs, alilen +} diff --git a/pkg/obialign/lcsband_nosimd.go b/pkg/obialign/lcsband_nosimd.go new file mode 100644 index 0000000..818a5e2 --- /dev/null +++ b/pkg/obialign/lcsband_nosimd.go @@ -0,0 +1,13 @@ +//go:build !goexperiment.simd + +package obialign + +const haveSIMD = false + +// encodeBases and bandedLCSSIMD are never called without GOEXPERIMENT=simd +// (haveSIMD is false); they exist to keep the dispatcher uniform. +func encodeBases(ws *bandedLCSWorkspace, a, b []byte) {} + +func bandedLCSSIMD(lA, lB, kmin, kmax int, ws *bandedLCSWorkspace) uint32 { + return 0 +} diff --git a/pkg/obialign/lcsband_scalar.go b/pkg/obialign/lcsband_scalar.go new file mode 100644 index 0000000..d6220d0 --- /dev/null +++ b/pkg/obialign/lcsband_scalar.go @@ -0,0 +1,53 @@ +package obialign + +import "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq" + +// bandedLCSScalar is the portable reference of the banded DP, row by row. It +// returns the packed cell (lB, lA). len(a) >= len(b) >= 1. +// +// Cells outside the band hold 0 (unavailable). Row i spans the columns +// [i+kmin, i+kmax] clipped to [1, lA]; the cell just left of that span and +// the one just right of it are reset so that the next row never reads a stale +// value. +func bandedLCSScalar(a, b []byte, kmin, kmax int, ws *bandedLCSWorkspace) uint32 { + lA, lB := len(a), len(b) + + prev := growUint32(ws.rows[0], lA+2) + cur := growUint32(ws.rows[1], lA+2) + ws.rows[0], ws.rows[1] = prev, cur + + for j := 0; j <= lA; j++ { + prev[j] = 0 + if j <= kmax { + prev[j] = lcsLenBase - uint32(j) + } + } + prev[lA+1] = 0 + + for i := 1; i <= lB; i++ { + if -i >= kmin { + cur[0] = lcsLenBase - uint32(i) + } else { + cur[0] = 0 + } + jlo := max(1, i+kmin) + jhi := min(lA, i+kmax) + if jlo > 1 { + cur[jlo-1] = 0 + } + bi := b[i-1] + for j := jlo; j <= jhi; j++ { + d := prev[j-1] + if obiseq.SameIUPACNuc(a[j-1], bi) { + d += lcsScoreUnit + } + m := max(d, prev[j], cur[j-1], 1) + cur[j] = m - 1 + } + if jhi < lA { + cur[jhi+1] = 0 + } + prev, cur = cur, prev + } + return prev[lA] +} diff --git a/pkg/obialign/lcsband_simd.go b/pkg/obialign/lcsband_simd.go new file mode 100644 index 0000000..42b4a09 --- /dev/null +++ b/pkg/obialign/lcsband_simd.go @@ -0,0 +1,107 @@ +//go:build goexperiment.simd + +package obialign + +import "simd" + +const haveSIMD = true + +func simdLanes() int { + var probe simd.Uint32s + return probe.Len() +} + +// encodeIUPAC writes in dst the 4-bit IUPAC mask of each base of s, reversed +// if rev is set, followed by zero padding up to len(dst). +func encodeIUPAC(dst []uint32, s []byte, rev bool) { + n := len(s) + for x := range dst { + dst[x] = 0 + } + if rev { + for x, c := range s { + dst[n-1-x] = uint32(iupacMask[c]) + } + return + } + for x, c := range s { + dst[x] = uint32(iupacMask[c]) + } +} + +// encodeBases encodes both sequences once for all the passes of an alignment. +func encodeBases(ws *bandedLCSWorkspace, a, b []byte) { + vl := simdLanes() + ws.aRev = growUint32(ws.aRev, len(a)+vl) + ws.bCode = growUint32(ws.bCode, len(b)+vl) + encodeIUPAC(ws.aRev, a, true) + encodeIUPAC(ws.bCode, b, false) +} + +// bandedLCSSIMD runs the banded DP along antidiagonals t = i + j. Lane i of +// the buffer of antidiagonal t holds the cell (i, t-i), so that +// +// diag = buffer[t-2][i-1] up = buffer[t-1][i-1] left = buffer[t-1][i] +// +// are plain unaligned loads. Along an antidiagonal the offset j-i = t-2i +// must stay in [kmin, kmax], which gives the window [lo, hi] of rows. The +// vector loop may run past hi; the lanes beyond are then reset to 0 +// (unavailable), as is the lane just before the window, because the next +// antidiagonals read them as neighbours. Lane 0 and lane t carry the first +// row and the first column of the matrix. +// +// Requires encodeBases to have been called, lA >= lB >= 1 and kmin <= 0 <= +// kmax. +func bandedLCSSIMD(lA, lB, kmin, kmax int, ws *bandedLCSWorkspace) uint32 { + vl := simdLanes() + aRev, bCode := ws.aRev, ws.bCode + + size := lB + 2*vl + 2 + for r := range ws.rows { + ws.rows[r] = growUint32(ws.rows[r], size) + clear(ws.rows[r]) + } + p2, p1, cur := ws.rows[0], ws.rows[1], ws.rows[2] + + p2[0] = lcsLenBase // cell (0,0) + if kmax >= 1 { + p1[0] = lcsLenBase - 1 // cell (0,1) + } + if kmin <= -1 { + p1[1] = lcsLenBase - 1 // cell (1,0) + } + + zero := simd.BroadcastUint32s(0) + one := simd.BroadcastUint32s(1) + unit := simd.BroadcastUint32s(lcsScoreUnit) + + for t := 2; t <= lA+lB; t++ { + lo := max(1, t-lA, (t-kmax+1)>>1) + hi := min(lB, t-1, (t-kmin)>>1) + + for i := lo; i <= hi; i += vl { + ca := simd.LoadUint32s(aRev[lA-t+i:]) + cb := simd.LoadUint32s(bCode[i-1:]) + inc := unit.Masked(ca.And(cb).NotEqual(zero)) + d := simd.LoadUint32s(p2[i-1:]).Add(inc) + u := simd.LoadUint32s(p1[i-1:]) + l := simd.LoadUint32s(p1[i:]) + d.Max(u).Max(l).Sub(one).Store(cur[i:]) + } + + zero.Store(cur[hi+1:]) + if lo > 1 { + cur[lo-1] = 0 + } + cur[0] = 0 + if t <= lA && t <= kmax { + cur[0] = lcsLenBase - uint32(t) + } + if t <= lB && -t >= kmin { + cur[t] = lcsLenBase - uint32(t) + } + + p2, p1, cur = p1, cur, p2 + } + return p1[lB] +} diff --git a/pkg/obialign/lcsband_test.go b/pkg/obialign/lcsband_test.go new file mode 100644 index 0000000..f957717 --- /dev/null +++ b/pkg/obialign/lcsband_test.go @@ -0,0 +1,288 @@ +package obialign + +import ( + "math/rand" + "testing" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq" +) + +// bandDP is the oracle: a plain DP over the whole matrix, written +// independently of the production code, in which only the cells with a +// diagonal offset j-i in [kmin, kmax] exist (kmin=-lB, kmax=lA when +// unbounded). It returns the optimal LCS score and the shortest alignment +// length achieving it among the alignments staying in the band. +func bandDP(a, b []byte, kmin, kmax int) (int, int) { + lA, lB := len(a), len(b) + type cell struct { + s, l int + ok bool + } + better := func(x, y cell) bool { + return !y.ok || (x.ok && (x.s > y.s || (x.s == y.s && x.l < y.l))) + } + m := make([][]cell, lB+1) + for i := range m { + m[i] = make([]cell, lA+1) + } + for i := 0; i <= lB; i++ { + for j := 0; j <= lA; j++ { + if k := j - i; k < kmin || k > kmax { + continue + } + if i == 0 || j == 0 { + m[i][j] = cell{0, i + j, true} + continue + } + var best cell + if d := m[i-1][j-1]; d.ok { + best = cell{d.s, d.l + 1, true} + if obiseq.SameIUPACNuc(a[j-1], b[i-1]) { + best.s++ + } + } + if u := m[i-1][j]; u.ok && better(cell{u.s, u.l + 1, true}, best) { + best = cell{u.s, u.l + 1, true} + } + if l := m[i][j-1]; l.ok && better(cell{l.s, l.l + 1, true}, best) { + best = cell{l.s, l.l + 1, true} + } + m[i][j] = best + } + } + return m[lB][lA].s, m[lB][lA].l +} + +// fullDP is the unbanded optimum. +func fullDP(a, b []byte) (int, int) { return bandDP(a, b, -len(b), len(a)) } + +// expected is the contract of BandedLCS: the unbounded optimum when it has at +// most maxError errors, (-1,-1) otherwise. +func expected(a, b []byte, maxError int) (int, int) { + s, l := fullDP(a, b) + if maxError >= 0 && l-s > maxError { + return -1, -1 + } + return s, l +} + +func randomFrom(rng *rand.Rand, alphabet string, n int) []byte { + s := make([]byte, n) + for i := range s { + s[i] = alphabet[rng.Intn(len(alphabet))] + } + return s +} + +func mutateFrom(rng *rand.Rand, alphabet string, s []byte, rate float64) []byte { + out := make([]byte, 0, len(s)+8) + for _, c := range s { + if rng.Float64() >= rate { + out = append(out, c) + continue + } + switch rng.Intn(3) { + case 0: + out = append(out, alphabet[rng.Intn(len(alphabet))]) + case 1: + case 2: + out = append(out, c, alphabet[rng.Intn(len(alphabet))]) + } + } + return out +} + +var testAlphabets = []struct { + name, letters string + regular bool // only IUPAC letters of non-zero mask +}{ + {"acgt", "ACGT", true}, + {"low-complexity", "AC", true}, + {"mixed-case", "ACGTacgt", true}, + {"iupac", "ACGTRYSWKMBDHVNUacgtrywsn", true}, + {"exotic", "ACGT-.EXz*", false}, +} + +// TestBandedLCSDifferential checks the scalar reference against the oracle, +// and the dispatcher (SIMD kernel when built with GOEXPERIMENT=simd) against +// the scalar reference, for random pairs of unequal lengths and many bands. +func TestBandedLCSDifferential(t *testing.T) { + rng := rand.New(rand.NewSource(7)) + ws := new(bandedLCSWorkspace) + for _, alpha := range testAlphabets { + for it := 0; it < 3000; it++ { + lA := 1 + rng.Intn(90) + a := randomFrom(rng, alpha.letters, lA) + var b []byte + if rng.Intn(3) > 0 { + b = mutateFrom(rng, alpha.letters, a, 0.02+rng.Float64()*0.5) + } else { + b = randomFrom(rng, alpha.letters, 1+rng.Intn(90)) + } + if len(b) == 0 { + continue + } + if len(a) < len(b) { + a, b = b, a + } + delta := len(a) - len(b) + for _, maxError := range []int{-1, 0, 1, 2, 3, 5, 8, 13, 21, 40, 200, delta, delta + 1, delta + 2, delta + 3} { + ws, wl := expected(a, b, maxError) + ss, sl := bandedLCS(a, b, maxError, new(bandedLCSWorkspace), false) + if ss != ws || sl != wl { + t.Fatalf("%s scalar maxError=%d a=%q b=%q: got (%d,%d), oracle (%d,%d)", + alpha.name, maxError, a, b, ss, sl, ws, wl) + } + ds, dl := bandedLCS(a, b, maxError, new(bandedLCSWorkspace), haveSIMD) + if ds != ss || dl != sl { + t.Fatalf("%s dispatch(simd=%v) maxError=%d a=%q b=%q: got (%d,%d), scalar (%d,%d)", + alpha.name, haveSIMD, maxError, a, b, ds, dl, ss, sl) + } + } + } + } + _ = ws +} + +// TestBandedLCSLongPairs covers lengths well above the vector width, with +// reused workspaces (stale buffers must never leak between calls). +func TestBandedLCSLongPairs(t *testing.T) { + rng := rand.New(rand.NewSource(11)) + ws := new(bandedLCSWorkspace) + for it := 0; it < 60; it++ { + a := randomFrom(rng, "ACGT", 150+rng.Intn(500)) + b := mutateFrom(rng, "ACGT", a, 0.01+rng.Float64()*0.2) + if len(a) < len(b) { + a, b = b, a + } + for _, maxError := range []int{-1, 4, 20, 60, 150} { + wantS, wantL := expected(a, b, maxError) + gotS, gotL := bandedLCS(a, b, maxError, ws, haveSIMD) + if gotS != wantS || gotL != wantL { + t.Fatalf("maxError=%d lens=%d/%d: got (%d,%d), oracle (%d,%d)", + maxError, len(a), len(b), gotS, gotL, wantS, wantL) + } + } + } +} + +// TestBandedLCSMatchesLegacy compares with the historical implementation +// (FastLCSEGFScoreByte, not end-gap-free) wherever that one is exact: no +// limit, or a result within the limit. +func TestBandedLCSMatchesLegacy(t *testing.T) { + rng := rand.New(rand.NewSource(3)) + for it := 0; it < 4000; it++ { + a := randomFrom(rng, "ACGTRYN", 1+rng.Intn(120)) + b := mutateFrom(rng, "ACGTRYN", a, rng.Float64()*0.4) + if len(b) == 0 { + continue + } + for _, maxError := range []int{-1, 2, 7, 25} { + ls, ll, err := BandedLCS(a, b, maxError) + if err != nil { + t.Fatal(err) + } + if ls < 0 { + continue + } + os, ol, _ := FastLCSEGFScoreByte(a, b, maxError, false, nil) + if os != ls || ol != ll { + t.Fatalf("maxError=%d a=%q b=%q: BandedLCS (%d,%d), legacy (%d,%d)", + maxError, a, b, ls, ll, os, ol) + } + } + } +} + +func TestBandedLCSEdgeCases(t *testing.T) { + cases := []struct { + a, b string + maxError int + s, l int + }{ + {"", "", -1, 0, 0}, + {"ACGT", "", -1, 0, 4}, + {"ACGT", "", 3, -1, -1}, + {"ACGT", "", 4, 0, 4}, + {"ACGT", "ACGT", 0, 4, 4}, + {"ACGT", "ACGA", 0, -1, -1}, + {"ACGT", "ACGA", 1, 3, 4}, + {"ACGTT", "ACGT", 0, -1, -1}, // length difference above the limit + {"ACGTT", "ACGT", 1, 4, 5}, + } + for _, c := range cases { + s, l, err := BandedLCS([]byte(c.a), []byte(c.b), c.maxError) + if err != nil || s != c.s || l != c.l { + t.Errorf("%q/%q maxError=%d: got (%d,%d,%v), want (%d,%d)", c.a, c.b, c.maxError, s, l, err, c.s, c.l) + } + // the arguments are symmetric + s, l, _ = BandedLCS([]byte(c.b), []byte(c.a), c.maxError) + if s != c.s || l != c.l { + t.Errorf("swapped %q/%q maxError=%d: got (%d,%d), want (%d,%d)", c.b, c.a, c.maxError, s, l, c.s, c.l) + } + } +} + +func TestBandedLCSTooLong(t *testing.T) { + long := make([]byte, MaxBandedLCSLength+1) + for i := range long { + long[i] = 'A' + } + if _, _, err := BandedLCS(long, []byte("ACGT"), -1); err != ErrSequenceTooLong { + t.Errorf("A too long: err = %v", err) + } + if _, _, err := BandedLCS([]byte("ACGT"), long, 5); err != ErrSequenceTooLong { + t.Errorf("B too long: err = %v", err) + } + // the maximum length itself is accepted + ok := long[:MaxBandedLCSLength] + if s, l, err := BandedLCS(ok, ok, 0); err != nil || s != len(ok) || l != len(ok) { + t.Errorf("max length: got (%d,%d,%v)", s, l, err) + } +} + +// TestBandedLCSSecondPass checks the exactness over many bands, also on +// low-complexity sequences where the best LCS is not the closest alignment, +// and reports how often the band had to be widened (never more than once). +func TestBandedLCSSecondPass(t *testing.T) { + rng := rand.New(rand.NewSource(5)) + for _, alpha := range testAlphabets[:4] { + for _, rate := range []float64{0.02, 0.1, 0.3} { + ws := new(bandedLCSWorkspace) + var total int + for it := 0; it < 1500; it++ { + a := randomFrom(rng, alpha.letters, 20+rng.Intn(120)) + b := mutateFrom(rng, alpha.letters, a, rate) + if len(b) == 0 { + continue + } + if len(a) < len(b) { + a, b = b, a + } + for _, maxError := range []int{1, 3, 6, 10, 20, 40} { + want, wantL := expected(a, b, maxError) + got, gotL := bandedLCS(a, b, maxError, ws, haveSIMD) + if got != want || gotL != wantL { + t.Fatalf("%s maxError=%d a=%q b=%q: got (%d,%d), want (%d,%d)", + alpha.name, maxError, a, b, got, gotL, want, wantL) + } + total++ + } + } + t.Logf("%-15s mutation %.2f: %d bounded calls, %d widened (%.1f%%)", + alpha.name, rate, total, ws.widened, 100*float64(ws.widened)/float64(total)) + } + } +} + +// TestFastLCSScoreWrapper checks the BioSequence entry point. +func TestFastLCSScoreWrapper(t *testing.T) { + sa := obiseq.NewBioSequence("a", []byte("ACGTACGTAC"), "") + sb := obiseq.NewBioSequence("b", []byte("ACGTTCGTAC"), "") + if s, l := FastLCSScore(sa, sb, -1, nil); s != 9 || l != 10 { + t.Errorf("got (%d,%d), want (9,10)", s, l) + } + if s, l := FastLCSScore(sa, sb, 0, nil); s != -1 || l != -1 { + t.Errorf("got (%d,%d), want (-1,-1)", s, l) + } +} From 49225030ca8fee0cf5fa7fe2ca18da45e6916efb Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Tue, 3 Nov 2026 06:09:06 +1100 Subject: [PATCH 09/24] Implement core clustering logic and scoring features This update introduces the core clustering algorithm, including parallel execution, dynamic block sizing, and SIMD-accelerated scoring methods. It also adds robust sequence preparation, k-mer analysis, and comprehensive test data fixtures for validation. --- cmd/obitools/obiclust/main.go | 28 ++ obitests/obitools/obiclust/test.sh | 188 ++++++++ pkg/obialign/lcsband.go | 26 +- pkg/obialign/lcsband_nosimd.go | 4 + pkg/obialign/lcsband_test.go | 72 +++ pkg/obialign/lcsmin.go | 102 ++++ pkg/obialign/lcsscore_simd.go | 156 +++++++ pkg/obitools/obiclust/block.go | 46 ++ pkg/obitools/obiclust/block_nosimd.go | 10 + pkg/obitools/obiclust/block_simd.go | 25 + pkg/obitools/obiclust/centers.go | 136 ++++++ pkg/obitools/obiclust/cluster.go | 202 ++++++++ pkg/obitools/obiclust/kmer4.go | 47 ++ pkg/obitools/obiclust/kmer4_nosimd.go | 14 + pkg/obitools/obiclust/kmer4_simd.go | 35 ++ pkg/obitools/obiclust/kmer4_test.go | 71 +++ pkg/obitools/obiclust/obiclust.go | 224 +++++++++ pkg/obitools/obiclust/obiclust_test.go | 434 ++++++++++++++++++ pkg/obitools/obiclust/options.go | 246 +++++----- pkg/obitools/obiclust/output.go | 128 ++++++ pkg/obitools/obiclust/prepare.go | 178 +++++++ pkg/obitools/obiclust/score.go | 233 ++++++++++ pkg/obitools/obiclust/testdata/cases.txt | 9 + .../testdata/families_ascending.expected | 101 ++++ .../testdata/families_default.expected | 101 ++++ .../testdata/families_distance.expected | 101 ++++ .../obiclust/testdata/families_exact.expected | 101 ++++ .../testdata/families_longest.expected | 101 ++++ .../obiclust/testdata/families_ratio.expected | 101 ++++ .../testdata/families_rawdist.expected | 101 ++++ .../testdata/families_rawlcs.expected | 101 ++++ .../testdata/families_shortest.expected | 101 ++++ 32 files changed, 3404 insertions(+), 119 deletions(-) create mode 100644 cmd/obitools/obiclust/main.go create mode 100755 obitests/obitools/obiclust/test.sh create mode 100644 pkg/obialign/lcsmin.go create mode 100644 pkg/obialign/lcsscore_simd.go create mode 100644 pkg/obitools/obiclust/block.go create mode 100644 pkg/obitools/obiclust/block_nosimd.go create mode 100644 pkg/obitools/obiclust/block_simd.go create mode 100644 pkg/obitools/obiclust/centers.go create mode 100644 pkg/obitools/obiclust/cluster.go create mode 100644 pkg/obitools/obiclust/kmer4.go create mode 100644 pkg/obitools/obiclust/kmer4_nosimd.go create mode 100644 pkg/obitools/obiclust/kmer4_simd.go create mode 100644 pkg/obitools/obiclust/kmer4_test.go create mode 100644 pkg/obitools/obiclust/obiclust_test.go create mode 100644 pkg/obitools/obiclust/output.go create mode 100644 pkg/obitools/obiclust/prepare.go create mode 100644 pkg/obitools/obiclust/score.go create mode 100644 pkg/obitools/obiclust/testdata/cases.txt create mode 100644 pkg/obitools/obiclust/testdata/families_ascending.expected create mode 100644 pkg/obitools/obiclust/testdata/families_default.expected create mode 100644 pkg/obitools/obiclust/testdata/families_distance.expected create mode 100644 pkg/obitools/obiclust/testdata/families_exact.expected create mode 100644 pkg/obitools/obiclust/testdata/families_longest.expected create mode 100644 pkg/obitools/obiclust/testdata/families_ratio.expected create mode 100644 pkg/obitools/obiclust/testdata/families_rawdist.expected create mode 100644 pkg/obitools/obiclust/testdata/families_rawlcs.expected create mode 100644 pkg/obitools/obiclust/testdata/families_shortest.expected diff --git a/cmd/obitools/obiclust/main.go b/cmd/obitools/obiclust/main.go new file mode 100644 index 0000000..65b65cc --- /dev/null +++ b/cmd/obitools/obiclust/main.go @@ -0,0 +1,28 @@ +package main + +import ( + "os" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obioptions" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obitools/obiclust" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obitools/obiconvert" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiutils" +) + +func main() { + optionParser := obioptions.GenerateOptionParser( + "obiclust", + "star clustering of sequences (the algorithm of sumaclust, UCLUST and CD-HIT)", + obiclust.OptionSet) + + _, args := optionParser(os.Args) + + fs, err := obiconvert.CLIReadBioSequences(args...) + obiconvert.OpenSequenceDataErrorMessage(args, err) + + clustered := obiclust.CLIOBIClust(fs) + + obiconvert.CLIWriteBioSequences(clustered, true) + + obiutils.WaitForLastPipe() +} diff --git a/obitests/obitools/obiclust/test.sh b/obitests/obitools/obiclust/test.sh new file mode 100755 index 0000000..f9277a0 --- /dev/null +++ b/obitests/obitools/obiclust/test.sh @@ -0,0 +1,188 @@ +#!/bin/bash + +# +# Here give the name of the test serie +# + +TEST_NAME=obiclust +CMD=obiclust + +###### +# +# Some variable and function definitions: please don't change them +# +###### +TEST_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" +OBITOOLS_DIR="${TEST_DIR/obitest*/}build" +export PATH="${OBITOOLS_DIR}:${PATH}" + +MCMD="$(echo "${CMD:0:4}" | tr '[:lower:]' '[:upper:]')$(echo "${CMD:4}" | tr '[:upper:]' '[:lower:]')" + +TMPDIR="$(mktemp -d)" +ntest=0 +success=0 +failed=0 + +cleanup() { + echo "========================================" 1>&2 + echo "## Results of the $TEST_NAME tests:" 1>&2 + + echo 1>&2 + echo "- $ntest tests run" 1>&2 + echo "- $success successfully completed" 1>&2 + echo "- $failed failed tests" 1>&2 + echo 1>&2 + echo "Cleaning up the temporary directory..." 1>&2 + echo 1>&2 + echo "========================================" 1>&2 + + rm -rf "$TMPDIR" # Suppress the temporary directory + + if [ $failed -gt 0 ]; then + log "$TEST_NAME tests failed" + log + log + exit 1 + fi + + log + log + + exit 0 +} + +log() { + echo -e "[$TEST_NAME @ $(date)] $*" 1>&2 +} + +log "Testing $TEST_NAME..." +log "Test directory is $TEST_DIR" +log "obitools directory is $OBITOOLS_DIR" +log "Temporary directory is $TMPDIR" +log "files: $(find $TEST_DIR | awk -F'/' '{print $NF}' | tail -n +2)" + +###################################################################### +#### +#### Below are the tests +#### +#### Before each test : +#### - increment the variable ntest +#### +#### Run the command as the condition of an if / then /else +#### - The command must return 0 on success +#### - The command must return an exit code different from 0 on failure +#### - The datafiles are stored in the same directory than the test script +#### - The test script directory is stored in the TEST_DIR variable +#### - If result files have to be produced they must be stored +#### in the temporary directory (TMPDIR variable) +#### +#### then clause is executed on success of the command +#### - Write a success message using the log function +#### - increment the variable success +#### +#### else clause is executed on failure of the command +#### - Write a failure message using the log function +#### - increment the variable failed +#### +###################################################################### + + + +((ntest++)) +if $CMD -h > "${TMPDIR}/help.txt" 2>&1 +then + log "$MCMD: printing help OK" + ((success++)) +else + log "$MCMD: printing help failed" + ((failed++)) +fi + + +# +# Dataset used by the following tests: two families with a rare variant each, +# a duplicate, and a sequence with an ambiguous nucleotide that is discarded. +# +cat > "${TMPDIR}/data.fasta" <a1 count=20; +acgtacgtacgtaacctagcctagcctagggatcgatcgatcgatcgaatcg +>a2 count=3; +acgtacgtacgtaacctagcctagcctagggatcgatcgatcgttcgaatcg +>a3 count=2; +acgtacgtacgtaacctagcctagcctagggatcgatcgatcgatcgaatcg +>b1 count=15; +ttgcatgcatgctagctagctagcatgcatcgatcgatgctagctagtagct +>b2 count=1; +ttgcatgcatgctagctagctagcatgcatcgatcgatgctagctagtaact +>x1 count=4; +ttgcatgcatgctangctagctagcatgcatcgatcgatgctagctagtagc +DATA + +((ntest++)) +if $CMD --max-cpu 2 -O -t 0.95 "${TMPDIR}/data.fasta" > "${TMPDIR}/clusters.fasta" 2> "${TMPDIR}/clusters.log" +then + log "$MCMD: clustering runs OK" + ((success++)) +else + log "$MCMD: clustering failed" + ((failed++)) +fi + +((ntest++)) +if [ "$(grep -c '^>' "${TMPDIR}/clusters.fasta")" = "5" ] +then + log "$MCMD: the sequence with an ambiguous nucleotide is discarded OK" + ((success++)) +else + log "$MCMD: wrong number of sequences in the output" + ((failed++)) +fi + +((ntest++)) +if [ "$(grep '^>a[23] ' "${TMPDIR}/clusters.fasta" | grep -c 'cluster=a1;')" = "2" ] && + [ "$(grep '^>b2 ' "${TMPDIR}/clusters.fasta" | grep -c 'cluster=b1;')" = "1" ] && + grep '^>b1 ' "${TMPDIR}/clusters.fasta" | grep -q 'cluster_center=true;' && + grep '^>a1 ' "${TMPDIR}/clusters.fasta" | grep -q 'cluster_weight=25;' +then + log "$MCMD: sequences are attached to the right center OK" + ((success++)) +else + log "$MCMD: sequences are not attached to the right center" + ((failed++)) +fi + +((ntest++)) +if $CMD --max-cpu 2 -t 0.95 --otu-map "${TMPDIR}/otu.csv" --no-sequence-output "${TMPDIR}/data.fasta" > "${TMPDIR}/none.fasta" 2> /dev/null && + [ ! -s "${TMPDIR}/none.fasta" ] && + [ "$(head -n 1 "${TMPDIR}/otu.csv")" = "id,cluster,cluster_center,cluster_score,count" ] && + [ "$(sed -n 2,4p "${TMPDIR}/otu.csv" | cut -d, -f1,2 | tr '\n' ' ')" = "a1,a1 a3,a1 a2,a1 " ] && + [ "$(grep -c ',true,' "${TMPDIR}/otu.csv")" = "2" ] +then + log "$MCMD: CSV table of the clusters OK" + ((success++)) +else + log "$MCMD: CSV table of the clusters failed" + ((failed++)) +fi + +((ntest++)) +if $CMD --max-cpu 1 -O -t 0.95 "${TMPDIR}/data.fasta" 2> /dev/null | grep '^>' | sed 's/ .*//' > "${TMPDIR}/order1.txt" && + $CMD --max-cpu 8 -O -t 0.95 "${TMPDIR}/data.fasta" 2> /dev/null | grep '^>' | sed 's/ .*//' > "${TMPDIR}/order8.txt" && + cmp -s "${TMPDIR}/order1.txt" "${TMPDIR}/order8.txt" +then + log "$MCMD: the result does not depend on the number of threads OK" + ((success++)) +else + log "$MCMD: the result depends on the number of threads" + ((failed++)) +fi + + +######################################### +# +# At the end of the tests +# the cleanup function is called +# +######################################### + +cleanup diff --git a/pkg/obialign/lcsband.go b/pkg/obialign/lcsband.go index 3c0c095..1ab8692 100644 --- a/pkg/obialign/lcsband.go +++ b/pkg/obialign/lcsband.go @@ -40,7 +40,10 @@ const ( type bandedLCSWorkspace struct { aRev, bCode []uint32 // encoded bases (aRev is reversed), SIMD kernel only rows [3][]uint32 // antidiagonal (SIMD) or row (scalar) buffers - widened int // number of alignments that needed a second pass + aRev16 []uint16 // 16-bit encoded bases of the score-only kernel + bCode16 []uint16 + rows16 [3][]uint16 + widened int // number of alignments that needed a second pass } var bandedLCSPool = sync.Pool{New: func() any { return new(bandedLCSWorkspace) }} @@ -146,13 +149,7 @@ func bandedLCS(a, b []byte, maxError int, ws *bandedLCSWorkspace, useSIMD bool) } run := func(g int) (int, int) { kmin, kmax := bandOffsets(lA, lB, g) - var key uint32 - if simdOK { - key = bandedLCSSIMD(lA, lB, kmin, kmax, ws) - } else { - key = bandedLCSScalar(a, b, kmin, kmax, ws) - } - return int(key >> 16), int(lcsLenBase - key&0xFFFF) + return ws.align(a, b, kmin, kmax, simdOK) } if maxError < 0 { @@ -173,3 +170,16 @@ func bandedLCS(a, b []byte, maxError int, ws *bandedLCSWorkspace, useSIMD bool) } return lcs, alilen } + +// align runs the banded DP on the offsets [kmin, kmax] (kmin <= 0 <= kmax) and +// returns the best (lcs, alilen) of the band. The bases must have been encoded +// with encodeBases when simdOK is set. +func (ws *bandedLCSWorkspace) align(a, b []byte, kmin, kmax int, simdOK bool) (int, int) { + var key uint32 + if simdOK { + key = bandedLCSSIMD(len(a), len(b), kmin, kmax, ws) + } else { + key = bandedLCSScalar(a, b, kmin, kmax, ws) + } + return int(key >> 16), int(lcsLenBase - key&0xFFFF) +} diff --git a/pkg/obialign/lcsband_nosimd.go b/pkg/obialign/lcsband_nosimd.go index 818a5e2..bf4b10b 100644 --- a/pkg/obialign/lcsband_nosimd.go +++ b/pkg/obialign/lcsband_nosimd.go @@ -11,3 +11,7 @@ func encodeBases(ws *bandedLCSWorkspace, a, b []byte) {} func bandedLCSSIMD(lA, lB, kmin, kmax int, ws *bandedLCSWorkspace) uint32 { return 0 } + +func lcsScoreSIMD(a, b []byte, kmin, kmax, minLCS int, ws *bandedLCSWorkspace) int { + return -1 +} diff --git a/pkg/obialign/lcsband_test.go b/pkg/obialign/lcsband_test.go index f957717..9869d5e 100644 --- a/pkg/obialign/lcsband_test.go +++ b/pkg/obialign/lcsband_test.go @@ -286,3 +286,75 @@ func TestFastLCSScoreWrapper(t *testing.T) { t.Errorf("got (%d,%d), want (-1,-1)", s, l) } } + +// TestBandedLCSAtLeast checks the exactness of the bound expressed on the +// score: the optimum when it reaches minLCS, (-1,-1) otherwise. +func TestBandedLCSAtLeast(t *testing.T) { + rng := rand.New(rand.NewSource(13)) + var buf BandedLCSBuffer + for _, alpha := range testAlphabets { + for it := 0; it < 1500; it++ { + a := randomFrom(rng, alpha.letters, 1+rng.Intn(100)) + b := mutateFrom(rng, alpha.letters, a, rng.Float64()*0.5) + if len(b) == 0 { + continue + } + os, ol := fullDP(a, b) + for _, minLCS := range []int{-3, 0, 1, os - 1, os, os + 1, len(b) / 2, len(b), len(b) + 1} { + ws, wl := os, ol + if os < minLCS { + ws, wl = -1, -1 + } + for _, got := range [][2]int{func() [2]int { s, l, _ := buf.AtLeast(a, b, minLCS); return [2]int{s, l} }(), + func() [2]int { s, l, _ := BandedLCSAtLeast(b, a, minLCS); return [2]int{s, l} }()} { + if got[0] != ws || got[1] != wl { + t.Fatalf("%s minLCS=%d a=%q b=%q: got (%d,%d), want (%d,%d)", + alpha.name, minLCS, a, b, got[0], got[1], ws, wl) + } + } + } + } + } + if _, _, err := buf.AtLeast(make([]byte, MaxBandedLCSLength+1), []byte("a"), 0); err != ErrSequenceTooLong { + t.Errorf("too long: err = %v", err) + } +} + +// TestBandedLCSScoreAtLeast checks the score-only entry point, with sequences +// long enough for the early stop of the kernel to be exercised. +func TestBandedLCSScoreAtLeast(t *testing.T) { + rng := rand.New(rand.NewSource(17)) + var buf BandedLCSBuffer + check := func(name string, a, b []byte) { + os, _ := fullDP(a, b) + for _, minLCS := range []int{-1, 0, os - 3, os - 1, os, os + 1, os + 5, len(b) - 1, len(b), (len(b) * 9) / 10, len(b) + 1} { + want := os + if os < minLCS { + want = -1 + } + got, err := buf.ScoreAtLeast(a, b, minLCS) + if err != nil || got != want { + t.Fatalf("%s minLCS=%d a=%q b=%q: got %d (%v), want %d", name, minLCS, a, b, got, err, want) + } + } + } + for _, alpha := range testAlphabets { + for it := 0; it < 800; it++ { + a := randomFrom(rng, alpha.letters, 1+rng.Intn(60)) + b := mutateFrom(rng, alpha.letters, a, rng.Float64()*0.6) + if len(b) > 0 { + check(alpha.name, a, b) + } + } + } + for it := 0; it < 150; it++ { + a := randomFrom(rng, "ACGT", 100+rng.Intn(400)) + b := mutateFrom(rng, "ACGT", a, rng.Float64()*0.3) + if it%3 == 0 { + b = randomFrom(rng, "ACGT", 50+rng.Intn(300)) + } + if len(b) > 0 { + check("long", a, b) + } + } +} diff --git a/pkg/obialign/lcsmin.go b/pkg/obialign/lcsmin.go new file mode 100644 index 0000000..93f00f9 --- /dev/null +++ b/pkg/obialign/lcsmin.go @@ -0,0 +1,102 @@ +package obialign + +// BandedLCSBuffer is a working memory that can be reused from one alignment to +// the next to avoid any allocation. It must not be shared between goroutines. +type BandedLCSBuffer struct { + ws bandedLCSWorkspace +} + +// AtLeast is BandedLCSAtLeast on the buffer. +func (b *BandedLCSBuffer) AtLeast(seqA, seqB []byte, minLCS int) (lcs, alilen int, err error) { + return atLeast(seqA, seqB, minLCS, &b.ws) +} + +func atLeast(seqA, seqB []byte, minLCS int, ws *bandedLCSWorkspace) (lcs, alilen int, err error) { + if len(seqA) > MaxBandedLCSLength || len(seqB) > MaxBandedLCSLength { + return -1, -1, ErrSequenceTooLong + } + if len(seqA) < len(seqB) { + seqA, seqB = seqB, seqA + } + lcs, alilen = bandedLCSAtLeast(seqA, seqB, minLCS, ws, haveSIMD) + return lcs, alilen, nil +} + +// BandedLCSAtLeast computes the LCS score of two nucleotide sequences and the +// length of the shortest alignment achieving it, as BandedLCS does, but with +// the bound expressed on the score: the result is the exact optimum when its +// score is at least minLCS, and (-1, -1) otherwise. +// +// The DP only visits the diagonals an alignment of score minLCS can use (one +// whose path makes at most lB-minLCS gaps on one side and lA-minLCS on the +// other), so the cost decreases as minLCS grows. This is exact: every alignment +// of score >= minLCS lies in that band, hence so does the optimum when it +// reaches minLCS, and when the best path of the band is below minLCS so is +// the global optimum. +func BandedLCSAtLeast(seqA, seqB []byte, minLCS int) (lcs, alilen int, err error) { + ws := bandedLCSPool.Get().(*bandedLCSWorkspace) + lcs, alilen, err = atLeast(seqA, seqB, minLCS, ws) + bandedLCSPool.Put(ws) + return lcs, alilen, err +} + +// bandedLCSAtLeast expects len(a) >= len(b), both within MaxBandedLCSLength. +func bandedLCSAtLeast(a, b []byte, minLCS int, ws *bandedLCSWorkspace, useSIMD bool) (int, int) { + lA, lB := len(a), len(b) + minLCS = max(minLCS, 0) + if minLCS > lB { + return -1, -1 + } + if lB == 0 { + return 0, lA + } + simdOK := useSIMD && allRegularIUPAC(a) && allRegularIUPAC(b) + kmin, kmax := -(lB - minLCS), lA-minLCS + + if !simdOK { + lcs, alilen := ws.align(a, b, kmin, kmax, false) + if lcs < minLCS { + return -1, -1 + } + return lcs, alilen + } + + // The score alone is much cheaper than the score with the length of the + // alignment: the latter is only computed when the pair reaches minLCS. + if lcsScoreSIMD(a, b, kmin, kmax, minLCS, ws) < minLCS { + return -1, -1 + } + encodeBases(ws, a, b) + return ws.align(a, b, kmin, kmax, true) +} + +// ScoreAtLeast computes the LCS score of two nucleotide sequences when it is +// at least minLCS, and returns -1 otherwise. It is much cheaper than AtLeast, +// which also computes the length of the shortest alignment. +func (b *BandedLCSBuffer) ScoreAtLeast(seqA, seqB []byte, minLCS int) (int, error) { + if len(seqA) > MaxBandedLCSLength || len(seqB) > MaxBandedLCSLength { + return -1, ErrSequenceTooLong + } + if len(seqA) < len(seqB) { + seqA, seqB = seqB, seqA + } + lA, lB := len(seqA), len(seqB) + minLCS = max(minLCS, 0) + if minLCS > lB { + return -1, nil + } + if lB == 0 { + return 0, nil + } + kmin, kmax := -(lB - minLCS), lA-minLCS + var lcs int + if haveSIMD && allRegularIUPAC(seqA) && allRegularIUPAC(seqB) { + lcs = lcsScoreSIMD(seqA, seqB, kmin, kmax, minLCS, &b.ws) + } else { + lcs, _ = b.ws.align(seqA, seqB, kmin, kmax, false) + } + if lcs < minLCS { + return -1, nil + } + return lcs, nil +} diff --git a/pkg/obialign/lcsscore_simd.go b/pkg/obialign/lcsscore_simd.go new file mode 100644 index 0000000..ac4250e --- /dev/null +++ b/pkg/obialign/lcsscore_simd.go @@ -0,0 +1,156 @@ +//go:build goexperiment.simd + +package obialign + +import "simd" + +func simdLanes16() int { + var probe simd.Uint16s + return probe.Len() +} + +func growUint16(s []uint16, n int) []uint16 { + if cap(s) < n { + return make([]uint16, n) + } + return s[:n] +} + +func encodeIUPAC16(dst []uint16, s []byte, rev bool) { + n := len(s) + clear(dst) + if rev { + for x, c := range s { + dst[n-1-x] = uint16(iupacMask[c]) + } + return + } + for x, c := range s { + dst[x] = uint16(iupacMask[c]) + } +} + +// pruneEvery is the number of antidiagonals between two tests of the upper +// bound that stops a hopeless alignment. +const pruneEvery = 8 + +// lcsScoreSIMD runs the same banded DP as bandedLCSSIMD on the score only, +// with 16-bit lanes (twice the cells per instruction): +// +// cell(i,j) = max(diag + match, up, left) +// +// A cell outside the band is 0, which never beats a real cell. The score of a +// path ending at the cell (i, j) of an antidiagonal cannot exceed the score of +// the cell plus min(lB-i, lA-j), the characters left to pair. Every path +// crosses each antidiagonal inside the window, so when the best score of the +// window plus the largest of these remainders is under minLCS the final score +// is under minLCS: -1 is returned without computing the rest of the matrix. +// +// Requires lA >= lB >= 1, kmin <= 0 <= kmax, and bases with non-zero masks. +// Returns the best score of the band, or -1 if it is proven to be under +// minLCS. +func lcsScoreSIMD(a, b []byte, kmin, kmax, minLCS int, ws *bandedLCSWorkspace) int { + lA, lB := len(a), len(b) + vl := simdLanes16() + + ws.aRev16 = growUint16(ws.aRev16, lA+vl) + ws.bCode16 = growUint16(ws.bCode16, lB+vl) + encodeIUPAC16(ws.aRev16, a, true) + encodeIUPAC16(ws.bCode16, b, false) + aRev, bCode := ws.aRev16, ws.bCode16 + + size := lB + 2*vl + 2 + for r := range ws.rows16 { + ws.rows16[r] = growUint16(ws.rows16[r], size) + clear(ws.rows16[r]) + } + p2, p1, cur := ws.rows16[0], ws.rows16[1], ws.rows16[2] + + // the first row and the first column hold a score of 0: nothing to set + + zero := simd.BroadcastUint16s(0) + one := simd.BroadcastUint16s(1) + + for t := 2; t <= lA+lB; t++ { + lo := max(1, t-lA, (t-kmax+1)>>1) + hi := min(lB, t-1, (t-kmin)>>1) + + for i := lo; i <= hi; i += vl { + ca := simd.LoadUint16s(aRev[lA-t+i:]) + cb := simd.LoadUint16s(bCode[i-1:]) + inc := one.Masked(ca.And(cb).NotEqual(zero)) + d := simd.LoadUint16s(p2[i-1:]).Add(inc) + u := simd.LoadUint16s(p1[i-1:]) + l := simd.LoadUint16s(p1[i:]) + d.Max(u).Max(l).Store(cur[i:]) + } + + zero.Store(cur[hi+1:]) + if lo > 1 { + cur[lo-1] = 0 + } + + // A diagonal move skips an antidiagonal, but a path always visits + // one of two consecutive antidiagonals. + if t%pruneEvery == 0 && minLCS > 0 && + scoreBound(p1, t-1, lA, lB, kmin, kmax) < minLCS && + scoreBound(cur, t, lA, lB, kmin, kmax) < minLCS { + return -1 + } + + p2, p1, cur = p1, cur, p2 + } + return int(p1[lB]) +} + +// iota16 is 0, 1, 2... to build the row index of the lanes. +var iota16 = func() (t [64]uint16) { + for i := range t { + t[i] = uint16(i) + } + return t +}() + +// scoreBound is an upper bound of the final score of the paths going through +// the antidiagonal t, whose scores are in row. A path through the cell (i, j) +// scores at most the score of the cell plus min(lB-i, lA-j), the characters +// left to pair, so the bound is the maximum of this sum on the cells of the +// window. The sum is computed by vectors on the full chunks of the window. +func scoreBound(row []uint16, t, lA, lB, kmin, kmax int) int { + lo := max(1, t-lA, (t-kmax+1)>>1) + hi := min(lB, t-1, (t-kmin)>>1) + + bound := 0 + i := lo + if hi-lo >= simdLanes16() { + vl := simdLanes16() + idx := simd.LoadUint16s(iota16[:]).Add(simd.BroadcastUint16s(uint16(lo))) + step := simd.BroadcastUint16s(uint16(vl)) + remB := simd.BroadcastUint16s(uint16(lB)) + // lA-t+i is not negative on the window; the 16-bit arithmetic is modular + offA := simd.BroadcastUint16s(uint16(lA - t)) + acc := simd.BroadcastUint16s(0) + for ; i+vl-1 <= hi; i += vl { + left := remB.Sub(idx).Min(idx.Add(offA)) + acc = acc.Max(simd.LoadUint16s(row[i:]).Add(left)) + idx = idx.Add(step) + } + var lanes [64]uint16 + acc.Store(lanes[:]) + for _, v := range lanes[:vl] { + bound = max(bound, int(v)) + } + } + for ; i <= hi; i++ { + bound = max(bound, int(row[i])+min(lB-i, lA-t+i)) + } + + // the paths running along the first row or the first column have a score of 0 + if t <= lA && t <= kmax { + bound = max(bound, min(lB, lA-t)) + } + if t <= lB && -t >= kmin { + bound = max(bound, min(lB-t, lA)) + } + return bound +} diff --git a/pkg/obitools/obiclust/block.go b/pkg/obitools/obiclust/block.go new file mode 100644 index 0000000..487b5a5 --- /dev/null +++ b/pkg/obitools/obiclust/block.go @@ -0,0 +1,46 @@ +package obiclust + +// A block stores the 4-mer tables of a few centers, transposed: the counter +// of the word w for the lane j is data[w*width+j]. A vector instruction then +// handles the same word for all the centers of the block, and only the words +// that a sequence contains (a hundred for a short amplicon, out of 256) need +// to be visited to compare it with all the centers of the block. +type tableBlock struct { + data []uint8 +} + +func newTableBlock(width int) tableBlock { + return tableBlock{data: make([]uint8, 256*width)} +} + +// set stores the table t as the lane `lane` of the block. +func (b *tableBlock) set(width, lane int, t *table4) { + for w := range t.count { + b.data[w*width+lane] = t.count[w] + } +} + +// common computes the sums of the minima of the counters of the sequence +// given by its words with every lane of the block (see blockCommon). +func (b *tableBlock) common(width int, words []uint16, out []uint8) { + blockCommon(b.data, width, words, out) +} + +// sparseWords lists the words of a table as w<<8|count, so that the visit of +// a block only goes through the words present in the sequence. nil is +// returned when the counters add up to more than 255, which would overflow the +// 8-bit sums of the blocks. +func sparseWords(t *table4) []uint16 { + words := make([]uint16, 0, 128) + mass := 0 + for w, c := range t.count { + if c > 0 { + words = append(words, uint16(w)<<8|uint16(c)) + mass += int(c) + } + } + if mass > 255 { + return nil + } + return words +} diff --git a/pkg/obitools/obiclust/block_nosimd.go b/pkg/obitools/obiclust/block_nosimd.go new file mode 100644 index 0000000..7353d29 --- /dev/null +++ b/pkg/obitools/obiclust/block_nosimd.go @@ -0,0 +1,10 @@ +//go:build !goexperiment.simd + +package obiclust + +// simdBlockWidth is 0 without SIMD: the blocks are not used. +func simdBlockWidth() int { return 0 } + +func blockCommon(data []uint8, width int, words []uint16, out []uint8) { + panic("obiclust: blocks of tables require GOEXPERIMENT=simd") +} diff --git a/pkg/obitools/obiclust/block_simd.go b/pkg/obitools/obiclust/block_simd.go new file mode 100644 index 0000000..b462de0 --- /dev/null +++ b/pkg/obitools/obiclust/block_simd.go @@ -0,0 +1,25 @@ +//go:build goexperiment.simd + +package obiclust + +import "simd" + +// simdBlockWidth is the number of centers in a block: one per lane of a +// vector of bytes. +func simdBlockWidth() int { + var probe simd.Uint8s + return min(probe.Len(), 64) +} + +// blockCommon computes, for the sequence whose words are given, the sum of +// the minima of the counters with every lane of the block. It is the value of +// commonWords without the overflow term, as the sums fit in 8 bits (see +// sparseWords). out must hold at least `width` bytes. +func blockCommon(data []uint8, width int, words []uint16, out []uint8) { + acc := simd.BroadcastUint8s(0) + for _, e := range words { + w := simd.LoadUint8s(data[int(e>>8)*width:]) + acc = acc.Add(w.Min(simd.BroadcastUint8s(uint8(e)))) + } + acc.Store(out) +} diff --git a/pkg/obitools/obiclust/centers.go b/pkg/obitools/obiclust/centers.go new file mode 100644 index 0000000..c88cf83 --- /dev/null +++ b/pkg/obitools/obiclust/centers.go @@ -0,0 +1,136 @@ +package obiclust + +import ( + "math" + "sort" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obialign" +) + +// center is a copy of the entry of a center, with its own table: scanning +// the centers of a list then reads contiguous memory. +type center struct { + entry + idx int // index of the entry in the dataset + rank int // rank of the center in the order of creation +} + +const tabChunk = 1024 + +// A bucket holds the centers of one length, in the order of creation, and +// their tables by blocks (see tableBlock). +type bucket struct { + cents []center + blocks []tableBlock +} + +// centerSet stores the centers by length. A sequence can only be attached to a +// center whose length is compatible with its own (see lengthCompatible), so +// it only visits the buckets of these lengths, instead of all the centers. +type centerSet struct { + width int // centers per block of transposed tables, 0 if unused + byLen map[int]*bucket + lens []int // lengths that have a bucket, increasing + tabs [][]table4 // chunks holding the tables of the centers + n int +} + +func newCenterSet(width int) *centerSet { + return ¢erSet{width: width, byLen: make(map[int]*bucket)} +} + +// add stores a copy of the entry e, which is the entry idx of the dataset, as +// the next center, and returns it. +func (s *centerSet) add(e entry, idx int) center { + if s.n%tabChunk == 0 { + s.tabs = append(s.tabs, make([]table4, tabChunk)) + } + tab := &s.tabs[len(s.tabs)-1][s.n%tabChunk] + if e.table != nil { + *tab = *e.table + } + e.table = tab + + b := s.byLen[e.length] + if b == nil { + b = new(bucket) + s.byLen[e.length] = b + s.lens = append(s.lens, e.length) + sort.Ints(s.lens) + } + if w := s.width; w > 0 && e.table != nil { + if len(b.cents)%w == 0 { + b.blocks = append(b.blocks, newTableBlock(w)) + } + b.blocks[len(b.blocks)-1].set(w, len(b.cents)%w, tab) + } + + ctr := center{entry: e, idx: idx, rank: s.n} + b.cents = append(b.cents, ctr) + s.n++ + return ctr +} + +// scan compares entry i with all the centers of the set, and returns the +// first one it matches (in fast mode) or the best one (in exact mode, the +// first of them in case of tie), exactly as compare would on the list of the +// centers in the order of creation. +// +// Only the buckets of compatible lengths are visited: the other centers would +// give the worst score. In each bucket the first match is the one of lowest +// rank, so the first match of the whole set is the one of lowest rank among +// the buckets. In exact mode, the best of each bucket is compared, ties going +// to the lowest rank. +func (c *clustering) scan(i int, buf *obialign.BandedLCSBuffer) attachment { + q := &c.entries[i] + set := c.set + width := set.width + useBlocks := width > 0 && q.sparse != nil + + best := attachment{-1, c.sc.worst} + bestRank := math.MaxInt + var common [64]uint8 + + for _, length := range set.lens { + if !c.sc.lengthCompatible(q.length, length) { + continue + } + b := set.byLen[length] + local := attachment{-1, c.sc.worst} + localRank := 0 + + for k := range b.cents { + ctr := &b.cents[k] + if c.fast && ctr.rank > bestRank { + break // a center of lower rank has been found in another bucket + } + cw := -1 + if useBlocks { + if k%width == 0 { + b.blocks[k/width].common(width, q.sparse, common[:]) + } + cw = int(common[k%width]) + } + s := c.sc.pairScore(q, &ctr.entry, cw, buf) + if c.sc.selects(s, local.score) { + local, localRank = attachment{ctr.idx, s}, ctr.rank + if c.fast { + break + } + } + } + + if local.center < 0 { + continue + } + if c.fast { + if localRank < bestRank { + best, bestRank = local, localRank + } + } else if best.center < 0 || c.sc.beats(local.score, best.score) || + (local.score == best.score && localRank < bestRank) { + best, bestRank = local, localRank + } + } + return best +} diff --git a/pkg/obitools/obiclust/cluster.go b/pkg/obitools/obiclust/cluster.go new file mode 100644 index 0000000..2e8835e --- /dev/null +++ b/pkg/obitools/obiclust/cluster.go @@ -0,0 +1,202 @@ +package obiclust + +import ( + "sort" + "sync" + "sync/atomic" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obialign" +) + +// clustering is the state of the star clustering of the distinct sequences, +// taken in their sorted order. +// +// The reference algorithm is sequential: each sequence is compared with the +// centers created so far, in the order of their creation, and is attached to +// the first one it matches (or to the best one in exact mode), or becomes a +// new center. Its result is reproduced exactly while the comparisons are run +// in parallel, by processing the sequences by blocks: +// +// - phase A (parallel): every sequence of the block is compared with the +// centers existing when the block starts. This is independent from one +// sequence to the other, a sequence of the block cannot change the centers; +// - phase B (sequential): the sequences matching no center are then taken +// in their order. Each one is compared with the centers created earlier in +// the block, and becomes a new center if it still matches nothing. The +// new centers come after the old ones in the order of the centers, so +// the result is the one of the sequential algorithm; +// - phase C (parallel, exact mode only): the sequences attached in phase A +// are compared with the new centers that precede them, the best score of +// the whole list of centers being searched for. +// +// The result does not depend on the size of the blocks, which only trades the +// sequential work of phase B against the synchronizations. +type clustering struct { + entries []entry + sc *scoring + fast bool + workers int + // bounds of the size of the blocks, 0 for the defaults (tests only) + blockMin, blockMax int + + centers []int // indices of the centers, in the order of creation + set *centerSet + fresh []center // the centers created in the current block + + center []int // for each entry, the index of its center, -1 if it has none + score []float64 // for each entry, the score with its center + bufs []obialign.BandedLCSBuffer +} + +// result of the comparison of one entry with the list of centers +type attachment struct { + center int // index of the center, -1 if none + score float64 +} + +func newClustering(entries []entry, sc *scoring, fast bool, workers int) *clustering { + c := &clustering{ + entries: entries, + sc: sc, + fast: fast, + workers: max(workers, 1), + center: make([]int, len(entries)), + score: make([]float64, len(entries)), + bufs: make([]obialign.BandedLCSBuffer, max(workers, 1)), + } + for i := range c.center { + c.center[i] = -1 + } + c.set = newCenterSet(simdBlockWidth()) + return c +} + +func (c *clustering) addCenter(i int) { + c.fresh = append(c.fresh, c.set.add(c.entries[i], i)) + c.centers = append(c.centers, i) + c.center[i] = i + c.score[i] = c.sc.centerScore(c.entries[i].length) +} + +// compare looks for the best center of entry i among the given ones. The +// attachment `from` is the best one already known. The search stops at the +// first match in fast mode. +func (c *clustering) compare(i int, cents []center, from attachment, buf *obialign.BandedLCSBuffer) attachment { + best := from + for k := range cents { + s := c.sc.pairScore(&c.entries[i], ¢s[k].entry, -1, buf) + if c.sc.selects(s, best.score) { + best = attachment{cents[k].idx, s} + if c.fast { + break + } + } + } + return best +} + +// parallelFor runs f(i, buf) for i in [from, to) on the workers. +func (c *clustering) parallelFor(from, to int, f func(i int, buf *obialign.BandedLCSBuffer)) { + n := to - from + if c.workers == 1 || n == 1 { + for i := from; i < to; i++ { + f(i, &c.bufs[0]) + } + return + } + + var next atomic.Int64 + next.Store(int64(from)) + var wg sync.WaitGroup + for w := 0; w < min(c.workers, n); w++ { + wg.Add(1) + go func(buf *obialign.BandedLCSBuffer) { + defer wg.Done() + for { + i := int(next.Add(1)) - 1 + if i >= to { + return + } + f(i, buf) + } + }(&c.bufs[w]) + } + wg.Wait() +} + +// run clusters all the entries. progress is called with the number of +// entries processed. +func (c *clustering) run(progress func(done int)) { + n := len(c.entries) + if n == 0 { + return + } + + minBlock, maxBlock := 2*c.workers, 256*c.workers + if c.blockMax > 0 { + minBlock, maxBlock = c.blockMin, c.blockMax + } + block := minBlock + att := make([]attachment, maxBlock) + inA := make([]bool, maxBlock) // attached to a center existing before the block + var newCenters []int // indices of the centers created in the block + + for pos := 0; pos < n; { + end := min(n, pos+block) + c.fresh = c.fresh[:0] + att, inA := att[:end-pos], inA[:end-pos] + + // phase A + c.parallelFor(pos, end, func(i int, buf *obialign.BandedLCSBuffer) { + att[i-pos] = c.scan(i, buf) + inA[i-pos] = att[i-pos].center >= 0 + }) + + // phase B + unmatched := 0 + newCenters = newCenters[:0] + for i := pos; i < end; i++ { + a := &att[i-pos] + if inA[i-pos] { + continue + } + unmatched++ + *a = c.compare(i, c.fresh, *a, &c.bufs[0]) + if a.center < 0 && !c.entries[i].ambiguous { + newCenters = append(newCenters, i) + c.addCenter(i) + } + } + + // phase C + if !c.fast && len(newCenters) > 0 { + c.parallelFor(pos, end, func(i int, buf *obialign.BandedLCSBuffer) { + if inA[i-pos] { + before := c.fresh[:sort.SearchInts(newCenters, i)] + att[i-pos] = c.compare(i, before, att[i-pos], buf) + } + }) + } + + for i := pos; i < end; i++ { + if a := att[i-pos]; a.center >= 0 { + c.center[i] = a.center + c.score[i] = c.sc.memberScore(a.score) + } + } + + // Size of the next block. Phase B is sequential: for a block of b + // sequences of which a fraction u matches no old center and a fraction + // n becomes new centers, it makes about b*u*b*n/2 comparisons, against + // b*K/workers in the time of phase A (K old centers). The block is + // sized so that phase B stays around a tenth of phase A. + if fu, fn := float64(unmatched)/float64(end-pos), float64(len(newCenters))/float64(end-pos); fu*fn > 0 { + target := 0.2 * float64(len(c.centers)) / (float64(c.workers) * fu * fn) + block = int(min(float64(maxBlock), max(float64(minBlock), target))) + } else { + block = maxBlock + } + pos = end + progress(pos) + } +} diff --git a/pkg/obitools/obiclust/kmer4.go b/pkg/obitools/obiclust/kmer4.go new file mode 100644 index 0000000..71db5bd --- /dev/null +++ b/pkg/obitools/obiclust/kmer4.go @@ -0,0 +1,47 @@ +package obiclust + +// table4 is the 4-mer occurrence table of a sequence, as sumaclust builds it: +// one saturating counter per word, and the number of occurrences that did +// not fit in the counters. +type table4 struct { + count [256]uint8 + overflow int +} + +// buildTable4 counts the 4-mers of a sequence made of the letters a, c, g +// and t. The 2-bit code of a base is (letter>>1)&3, i.e. a=0, c=1, t=2, g=3, +// which is the encoding of sumaclust. +func buildTable4(seq []byte) *table4 { + t := new(table4) + t.fill(seq) + return t +} + +// fill counts the 4-mers of seq in a zeroed table. +func (t *table4) fill(seq []byte) { + if len(seq) < 4 { + return + } + w := uint(0) + for i, c := range seq { + w = (w<<2 | uint(c>>1)&3) & 0xFF + if i < 3 { + continue + } + if t.count[w] < 255 { + t.count[w]++ + } else { + t.overflow++ + } + } +} + +// minCommonWords is the minimum number of 4-mers that two sequences must +// share when the longest has length reflen and they have a common +// subsequence of length lcs (integer arithmetic of sumaclust, thresholdLCS4). +func minCommonWords(reflen, lcs int) int { + nbfrag := (reflen-lcs)*2 + 1 + smin := lcs / nbfrag + r := lcs - smin*nbfrag + return max(smin-2, 0)*r + max(smin-3, 0)*(nbfrag-r) +} diff --git a/pkg/obitools/obiclust/kmer4_nosimd.go b/pkg/obitools/obiclust/kmer4_nosimd.go new file mode 100644 index 0000000..01e32a4 --- /dev/null +++ b/pkg/obitools/obiclust/kmer4_nosimd.go @@ -0,0 +1,14 @@ +//go:build !goexperiment.simd + +package obiclust + +// commonWords is the upper bound sumaclust uses for the number of 4-mers +// shared by two sequences: the sum of the minima of the counters, plus the +// minimum of the two overflows. +func commonWords(t1, t2 *table4) int { + total := 0 + for i := range t1.count { + total += int(min(t1.count[i], t2.count[i])) + } + return total + min(t1.overflow, t2.overflow) +} diff --git a/pkg/obitools/obiclust/kmer4_simd.go b/pkg/obitools/obiclust/kmer4_simd.go new file mode 100644 index 0000000..96da178 --- /dev/null +++ b/pkg/obitools/obiclust/kmer4_simd.go @@ -0,0 +1,35 @@ +//go:build goexperiment.simd + +package obiclust + +import "simd" + +// commonWords is the upper bound sumaclust uses for the number of 4-mers +// shared by two sequences: the sum of the minima of the counters, plus the +// minimum of the two overflows. +// +// The minima of the 256 counters are computed by vectors of bytes, and summed +// in 16-bit lanes (at most 256*255 spread over the lanes, the lanes being at +// least 8 in number, so that none of them overflows). +func commonWords(t1, t2 *table4) int { + var probe simd.Uint8s + vl := probe.Len() + + acc := simd.BroadcastUint16s(0) + low := simd.BroadcastUint16s(0x00FF) + for i := 0; i < len(t1.count); i += vl { + m := simd.LoadUint8s(t1.count[i:]).Min(simd.LoadUint8s(t2.count[i:])).ReshapeToUint16s() + acc = acc.Add(m.And(low)).Add(m.ShiftAllRight(8)) + } + + // sum the pairs of 16-bit lanes into 32-bit lanes, then add those up + wide := acc.ReshapeToUint32s() + sums := wide.And(simd.BroadcastUint32s(0xFFFF)).Add(wide.ShiftAllRight(16)) + var lanes [32]uint32 + sums.Store(lanes[:]) + total := 0 + for _, v := range lanes[:sums.Len()] { + total += int(v) + } + return total + min(t1.overflow, t2.overflow) +} diff --git a/pkg/obitools/obiclust/kmer4_test.go b/pkg/obitools/obiclust/kmer4_test.go new file mode 100644 index 0000000..66e8f34 --- /dev/null +++ b/pkg/obitools/obiclust/kmer4_test.go @@ -0,0 +1,71 @@ +package obiclust + +import ( + "math/rand" + "testing" +) + +// commonWordsRef is the definition of commonWords, spelled out. +func commonWordsRef(t1, t2 *table4) int { + total := 0 + for i := range t1.count { + total += int(min(t1.count[i], t2.count[i])) + } + return total + min(t1.overflow, t2.overflow) +} + +func TestCommonWords(t *testing.T) { + rng := rand.New(rand.NewSource(3)) + seq := func(n int, alpha string) []byte { + s := make([]byte, n) + for i := range s { + s[i] = alpha[rng.Intn(len(alpha))] + } + return s + } + for it := 0; it < 3000; it++ { + alpha := []string{"acgt", "ac", "a"}[it%3] + t1 := buildTable4(seq(rng.Intn(1200), alpha)) + t2 := buildTable4(seq(rng.Intn(1200), alpha)) + if got, want := commonWords(t1, t2), commonWordsRef(t1, t2); got != want { + t.Fatalf("commonWords = %d, want %d", got, want) + } + } +} + +// TestBuildTable4 checks the table against the 4-mers counted the long way, +// including the saturation of the counters. +func TestBuildTable4(t *testing.T) { + code := map[byte]int{'a': 0, 'c': 1, 't': 2, 'g': 3} + rng := rand.New(rand.NewSource(4)) + for it := 0; it < 500; it++ { + alpha := []string{"acgt", "a", "ac"}[it%3] + n := rng.Intn(900) + s := make([]byte, n) + for i := range s { + s[i] = alpha[rng.Intn(len(alpha))] + } + var counts [256]int + for i := 0; i+4 <= n; i++ { + w := 0 + for _, c := range s[i : i+4] { + w = w<<2 | code[c] + } + counts[w]++ + } + tab := buildTable4(s) + over := 0 + for w, c := range counts { + if c > 255 { + over += c - 255 + c = 255 + } + if int(tab.count[w]) != c { + t.Fatalf("word %d: got %d, want %d", w, tab.count[w], c) + } + } + if tab.overflow != over { + t.Fatalf("overflow = %d, want %d", tab.overflow, over) + } + } +} diff --git a/pkg/obitools/obiclust/obiclust.go b/pkg/obitools/obiclust/obiclust.go index 5ffa951..1342db5 100644 --- a/pkg/obitools/obiclust/obiclust.go +++ b/pkg/obitools/obiclust/obiclust.go @@ -1 +1,225 @@ package obiclust + +import ( + "os" + "sync" + "sync/atomic" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obialign" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obidefault" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiiter" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obitools/obiconvert" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obitools/obicsv" + "github.com/schollz/progressbar/v3" + log "github.com/sirupsen/logrus" +) + +// buildEntries describes the distinct sequences of the groups, the 4-mer +// tables being computed in parallel. +func buildEntries(recs []record, groups []group, workers int) []entry { + entries := make([]entry, len(groups)) + tables := make([]table4, len(groups)) // contiguous, in the order the centers are scanned + + var next atomic.Int64 + var wg sync.WaitGroup + for w := 0; w < max(workers, 1); w++ { + wg.Add(1) + go func() { + defer wg.Done() + for { + g := int(next.Add(1)) - 1 + if g >= len(groups) { + return + } + head := &recs[groups[g].start] + var sparse []uint16 + if !head.ambiguous { + tables[g].fill(head.clean) + sparse = sparseWords(&tables[g]) + } + entries[g] = entry{ + seq: head.clean, + length: len(head.clean), + count: groups[g].count, + table: &tables[g], + sparse: sparse, + ambiguous: head.ambiguous, + } + } + }() + } + wg.Wait() + return entries +} + +// Params are the parameters of the clustering. +type Params struct { + Threshold float64 + Ratio float64 + Exact bool // attach a sequence to the best center, not to the first matching one + Distance bool // the score is a distance, not a similarity + Normalized bool // the score is normalized by the reference length + Reference reference + SortKey string // "count", "None", or an attribute + Ascending bool + AcceptAmbiguous bool // keep the sequences with IUPAC letters other than acgt (they cannot be centers) + Workers int +} + +// ParamsFromCLI collects the parameters from the command line options. +func ParamsFromCLI() Params { + return Params{ + Threshold: CLIThreshold(), + Ratio: CLIRatio(), + Exact: CLIExact(), + Distance: CLIDistance(), + Normalized: CLINormalized(), + Reference: CLIReference(), + SortKey: CLISortKey(), + Ascending: CLIAscending(), + AcceptAmbiguous: CLIAcceptAmbiguous(), + Workers: obidefault.ParallelWorkers(), + } +} + +// newScoring builds the scoring from the parameters, checking them as sumaclust does. +func newScoring(p Params, entries []entry) *scoring { + threshold := p.Threshold + + if threshold <= 0 || (p.Normalized && threshold > 1.0) { + log.Fatal("Please specify a threshold > 0, and < 1 when scores are normalized.") + } + + sc := &scoring{ + normalize: p.Normalized, + lcsmode: !p.Distance, + ref: p.Reference, + ratio: p.Ratio, + } + if !sc.lcsmode && sc.normalize { + threshold = 1.0 - threshold + } + sc.threshold = threshold + + lmax := 0 + for i := range entries { + lmax = max(lmax, entries[i].length) + } + if sc.similarityLike() { + sc.worst = 0.0 + } else { + sc.worst = float64(lmax) + } + sc.prepareLengths(lmax) + return sc +} + +func progressBar(n int) *progressbar.ProgressBar { + if !obiconvert.CLIProgressBar() { + return nil + } + return progressbar.NewOptions(n, + progressbar.OptionSetWriter(os.Stderr), + progressbar.OptionSetWidth(15), + progressbar.OptionShowIts(), + progressbar.OptionSetPredictTime(true), + progressbar.OptionSetDescription("[Clustering sequences]"), + ) +} + +// Result is a clustering of a dataset. +type Result struct { + recs []record + groups []group + clustered *clustering + Discarded int // number of sequences that were not usable +} + +// Clusters returns the number of clusters. +func (r *Result) Clusters() int { return len(r.clustered.centers) } + +// Cluster clusters the sequences with the star clustering algorithm of +// sumaclust (the same as UCLUST and CD-HIT). progress, if not nil, is called +// with the number of distinct sequences processed. +func Cluster(db obiseq.BioSequenceSlice, p Params, progress func(done, total int)) *Result { + recs, groups, discarded := prepare(db, p.SortKey, p.Ascending, p.AcceptAmbiguous) + + if discarded > 0 { + log.Warnf("Discarded %d sequences that did not contain only IUPAC nucleotides (or only 'acgt' without --accept-ambiguous)", discarded) + } + if len(groups) == 0 { + log.Fatal("No valid sequences.") + } + log.Infof("%d distinct sequences", len(groups)) + + entries := buildEntries(recs, groups, p.Workers) + for i := range entries { + if entries[i].length > obialign.MaxBandedLCSLength { + log.Fatalf("The sequence %s is too long (%d bp, maximum %d bp)", + recs[groups[i].start].seq.Id(), entries[i].length, obialign.MaxBandedLCSLength) + } + } + + sc := newScoring(p, entries) + if sc.similarityLike() { + log.Infof("Clustering sequences when similarity >= %f", sc.threshold) + } else { + log.Infof("Clustering sequences when distance <= %f", sc.threshold) + } + + cl := newClustering(entries, sc, !p.Exact, p.Workers) + cl.run(func(done int) { + if progress != nil { + progress(done, len(entries)) + } + }) + log.Infof("%d clusters created", len(cl.centers)) + + return &Result{recs: recs, groups: groups, clustered: cl, Discarded: discarded} +} + +// CLIOBIClust clusters the sequences and returns them annotated. +func CLIOBIClust(iterator obiiter.IBioSequence) obiiter.IBioSequence { + source, db := iterator.Load() + log.Infof("Sequence dataset of %d sequences loaded", len(db)) + + var bar *progressbar.ProgressBar + last := 0 + res := Cluster(db, ParamsFromCLI(), func(done, total int) { + if bar == nil { + bar = progressBar(total) + } + if bar != nil { + bar.Add(done - last) + } + last = done + }) + + if CLIOTUMap() != "" { + table, err := obicsv.WriteCSVToFile( + res.OTUMapIterator(source, obidefault.BatchSize()), + CLIOTUMap(), + obicsv.OptionsCompressed(obidefault.CompressOutput())) + if err != nil { + log.Fatalf("Cannot write the OTU map %s: %v", CLIOTUMap(), err) + } + table.Consume() + } + + if CLINoSequenceOutput() { + return emptyIterator() + } + return obiiter.IBatchOver(source, res.Annotate(), obidefault.BatchSize()) +} + +// emptyIterator returns an iterator that is already exhausted. +func emptyIterator() obiiter.IBioSequence { + iterator := obiiter.MakeIBioSequence() + iterator.Add(1) + go func() { + iterator.WaitAndClose() + }() + iterator.Done() + return iterator +} diff --git a/pkg/obitools/obiclust/obiclust_test.go b/pkg/obitools/obiclust/obiclust_test.go new file mode 100644 index 0000000..fff004e --- /dev/null +++ b/pkg/obitools/obiclust/obiclust_test.go @@ -0,0 +1,434 @@ +package obiclust + +import ( + "bufio" + "fmt" + "math/rand" + "os" + "path/filepath" + "strconv" + "strings" + "testing" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obialign" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq" +) + +// readTestFasta reads a fasta file whose headers are "id count=N; key=V;". +func readTestFasta(t testing.TB, filename string) obiseq.BioSequenceSlice { + t.Helper() + f, err := os.Open(filename) + if err != nil { + t.Fatal(err) + } + defer f.Close() + + var db obiseq.BioSequenceSlice + var header string + var seq strings.Builder + flush := func() { + if header == "" { + return + } + fields := strings.Fields(header) + s := obiseq.NewBioSequence(fields[0], []byte(seq.String()), "") + for _, f := range fields[1:] { + if k, v, ok := strings.Cut(strings.TrimSuffix(f, ";"), "="); ok { + if n, err := strconv.Atoi(v); err == nil { + s.SetAttribute(k, n) + } + } + } + db = append(db, s) + seq.Reset() + } + sc := bufio.NewScanner(f) + sc.Buffer(make([]byte, 1<<20), 1<<20) + for sc.Scan() { + if line := sc.Text(); strings.HasPrefix(line, ">") { + flush() + header = line[1:] + } else { + seq.WriteString(strings.TrimSpace(line)) + } + } + flush() + return db +} + +// paramsFromSumaclust translates the options of sumaclust. +func paramsFromSumaclust(t testing.TB, args []string) Params { + t.Helper() + p := Params{Threshold: 0.97, Ratio: 1.0, Normalized: true, SortKey: "count", Workers: 4} + for i := 0; i < len(args); i++ { + var err error + switch args[i] { + case "-t": + i++ + p.Threshold, err = strconv.ParseFloat(args[i], 64) + case "-R": + i++ + p.Ratio, err = strconv.ParseFloat(args[i], 64) + case "-s": + i++ + p.SortKey = args[i] + case "-e": + p.Exact = true + case "-l": + p.Reference = refShortest + case "-L": + p.Reference = refLongest + case "-a": + p.Reference = refAlignment + case "-d": + p.Distance = true + case "-r": + p.Normalized = false + case "-o": + p.Ascending = true + case "-g": + p.AcceptAmbiguous = true + default: + t.Fatalf("unknown option %s", args[i]) + } + if err != nil { + t.Fatal(err) + } + } + return p +} + +// TestSumaclustResults compares the clusterings with the outputs of +// sumaclust 1.0.36 (testdata/*.expected: id, cluster, score, center, weight +// and sequence of every record, in the order sumaclust writes them). +func TestSumaclustResults(t *testing.T) { + cases, err := os.ReadFile(filepath.Join("testdata", "cases.txt")) + if err != nil { + t.Fatal(err) + } + for _, line := range strings.Split(strings.TrimSpace(string(cases)), "\n") { + parts := strings.Split(line, "\t") + data, name, opts := parts[0], parts[1], strings.Fields(parts[2]) + t.Run(data+"/"+name, func(t *testing.T) { + db := readTestFasta(t, filepath.Join("testdata", data+".fasta")) + res := Cluster(db, paramsFromSumaclust(t, opts), nil) + + expected, err := os.ReadFile(filepath.Join("testdata", data+"_"+name+".expected")) + if err != nil { + t.Fatal(err) + } + want := strings.Split(strings.TrimSpace(string(expected)), "\n") + got := res.Annotate() + if len(got) != len(want) { + t.Fatalf("%d records, want %d", len(got), len(want)) + } + for i, s := range got { + score, _ := s.GetFloatAttribute("cluster_score") + center, _ := s.GetBoolAttribute("cluster_center") + c := map[bool]string{true: "T", false: "F"}[center] + cluster, _ := s.GetStringAttribute("cluster") + weight, _ := s.GetIntAttribute("cluster_weight") + line := fmt.Sprintf("%s\t%s\t%f\t%s\t%d\t%s", s.Id(), cluster, score, c, weight, s.String()) + if line != want[i] { + t.Fatalf("record %d:\n got %s\nwant %s", i, line, want[i]) + } + } + }) + } +} + +// referenceCluster transcribes the sequential algorithm of sumaclust. +func referenceCluster(entries []entry, sc *scoring, fast bool) (center []int, score []float64) { + n := len(entries) + center, score = make([]int, n), make([]float64, n) + var centers []int + var buf obialign.BandedLCSBuffer + for i := range entries { + scoremax, best := sc.worst, -1 + for _, c := range centers { + s := sc.pairScore(&entries[i], &entries[c], -1, &buf) + if sc.selects(s, scoremax) { + scoremax, best = s, c + if fast { + break + } + } + } + if best >= 0 { + center[i], score[i] = best, sc.memberScore(scoremax) + } else if entries[i].ambiguous { + center[i] = -1 + } else { + center[i], score[i] = i, sc.centerScore(entries[i].length) + centers = append(centers, i) + } + } + return center, score +} + +func randomEntries(rng *rand.Rand, n int) []entry { + var roots [][]byte + entries := make([]entry, 0, n) + for len(entries) < n { + var s []byte + if len(roots) == 0 || rng.Intn(5) == 0 { + s = make([]byte, 30+rng.Intn(50)) + for i := range s { + s[i] = "acgt"[rng.Intn(4)] + } + roots = append(roots, s) + } else { + root := roots[rng.Intn(len(roots))] + for _, c := range root { + switch r := rng.Intn(40); { + case r == 0: + s = append(s, "acgt"[rng.Intn(4)]) + case r == 1: + case r == 2: + s = append(s, c, "acgt"[rng.Intn(4)]) + default: + s = append(s, c) + } + } + } + tab := buildTable4(s) + entries = append(entries, entry{seq: s, length: len(s), count: 1 + rng.Intn(20), table: tab, sparse: sparseWords(tab)}) + } + return entries +} + +// TestParallelClusteringIsSequential checks that the clustering by blocks +// gives, whatever the number of workers and the size of the blocks, the +// result of the sequential algorithm. +func TestParallelClusteringIsSequential(t *testing.T) { + rng := rand.New(rand.NewSource(21)) + scorings := []scoring{ + {threshold: 0.9, normalize: true, lcsmode: true, ref: refAlignment, ratio: 1, worst: 0}, + {threshold: 0.85, normalize: true, lcsmode: true, ref: refShortest, ratio: 2, worst: 0}, + {threshold: 0.9, normalize: true, lcsmode: false, ref: refLongest, ratio: 1, worst: 0}, + {threshold: 35, normalize: false, lcsmode: true, ref: refAlignment, ratio: 1, worst: 0}, + {threshold: 5, normalize: false, lcsmode: false, ref: refAlignment, ratio: 1, worst: 80}, + } + for round := 0; round < 4; round++ { + entries := randomEntries(rng, 300) + for si := range scorings { + sc := scorings[si] + for _, fast := range []bool{true, false} { + wantCenter, wantScore := referenceCluster(entries, &sc, fast) + for _, workers := range []int{1, 3, 8} { + for _, blocks := range [][2]int{{0, 0}, {1, 1}, {2, 5}, {7, 7}, {50, 300}} { + cl := newClustering(entries, &sc, fast, workers) + cl.blockMin, cl.blockMax = blocks[0], blocks[1] + cl.run(func(int) {}) + for i := range entries { + if cl.center[i] != wantCenter[i] || cl.score[i] != wantScore[i] { + t.Fatalf("scoring %d fast=%v workers=%d blocks=%v: entry %d is (%d, %v), want (%d, %v)", + si, fast, workers, blocks, i, cl.center[i], cl.score[i], wantCenter[i], wantScore[i]) + } + } + } + } + } + } + } +} + +func TestClassify(t *testing.T) { + cases := []struct { + in string + accept bool + kept bool + ambiguous bool + }{ + {"acgt", false, true, false}, {"acgt", true, true, false}, + {"acgtn", false, false, false}, {"acgtn", true, true, true}, + {"acgtx", true, false, false}, {"nacgt", false, false, false}, + {"acgt-", true, false, false}, {"acgu", true, true, true}, + {"ACGT", true, false, false}, {"", true, false, false}, + {"rykmswbdhvn", true, true, true}, {"rykmswbdhvn", false, false, false}, + } + for _, c := range cases { + got, amb := classify([]byte(c.in), c.accept) + if (got != nil) != c.kept || amb != c.ambiguous { + t.Errorf("classify(%q, %v) = (%q, %v), want kept=%v ambiguous=%v", c.in, c.accept, got, amb, c.kept, c.ambiguous) + } + } +} + +func makeSeq(id, seq string, count int, key float64) *obiseq.BioSequence { + s := obiseq.NewBioSequence(id, []byte(seq), "") + s.SetAttribute("count", count) + s.SetAttribute("key", key) + return s +} + +func ids(groups []group, recs []record) string { + var out []string + for _, g := range groups { + out = append(out, recs[g.start].seq.Id()) + } + return strings.Join(out, " ") +} + +// TestPrepareOrder checks the order of the sequences, which does not depend +// on the stability of a sort: decreasing count, then position; the head of a +// group of identical sequences is the one with the extreme key, then the +// first one. +func TestPrepareOrder(t *testing.T) { + db := obiseq.BioSequenceSlice{ + makeSeq("a", "aaaacccc", 5, 1), + makeSeq("b", "gggg", 9, 2), + makeSeq("c", "aaaacccc", 5, 3), // same sequence as a, same count: a is the head + makeSeq("d", "tttt", 10, 4), + makeSeq("e", "cccc", 10, 5), // same cumulated count as d: after d, which comes first + makeSeq("f", "aaaacccc", 7, 6), + makeSeq("g", "nnnn", 3, 7), // discarded + makeSeq("h", "ccgg", 2, 8), + } + + check := func(key string, ascending bool, want string) { + recs, groups, discarded := prepare(db, key, ascending, false) + if discarded != 1 { + t.Errorf("discarded = %d, want 1", discarded) + } + if got := ids(groups, recs); got != want { + t.Errorf("key=%s ascending=%v: got %s, want %s", key, ascending, got, want) + } + } + // a, c and f are one group of count 17, headed by f (count 7) + check("count", false, "f d e b h") + check("count", true, "h b d e a") // the head of a group is then the record with the smallest count + // on the key attribute, the groups are ranked on the key of their head, the record with the largest key + check("key", false, "h f e d b") + check("key", true, "a b d e h") + // no sorting: first occurrence of each sequence + check("None", false, "a b d e h") + + // the identical sequences follow their head, with the largest count first + recs, groups, _ := prepare(db, "count", false, false) + var members []string + for r := groups[0].start; r < groups[0].end; r++ { + members = append(members, recs[r].seq.Id()) + } + if got := strings.Join(members, " "); got != "f a c" { + t.Errorf("group of f: %s, want f a c", got) + } +} + +// TestAmbiguousSequences checks that a sequence with IUPAC letters is attached +// to a cluster when it matches a center, and is left without cluster otherwise. +func TestAmbiguousSequences(t *testing.T) { + root := "acgtacgtaccgtagctagcatcgatcgatcgtagctagctagcatcg" + db := obiseq.BioSequenceSlice{ + makeSeq("center", root, 10, 0), + makeSeq("amb", root[:20]+"n"+root[21:], 5, 0), // matches the center + makeSeq("far", "ttttgggggccccaaaaattttgggggccccaaaaattttggggg", 4, 0), + makeSeq("ambfar", "nnnnttttggggcccccaaaa", 3, 0), // matches nothing, cannot be a center + } + p := Params{Threshold: 0.9, Ratio: 1, Normalized: true, SortKey: "count", AcceptAmbiguous: true, Workers: 2} + res := Cluster(db, p, nil) + out := res.Annotate() + got := map[string]string{} + for _, s := range out { + c, ok := s.GetStringAttribute("cluster") + if !ok { + c = "-" + } + got[s.Id()] = c + } + want := map[string]string{"center": "center", "amb": "center", "far": "far", "ambfar": "-"} + for id, w := range want { + if got[id] != w { + t.Errorf("%s: cluster %q, want %q", id, got[id], w) + } + } + if res.Clusters() != 2 { + t.Errorf("%d clusters, want 2", res.Clusters()) + } + for _, s := range out { + if s.Id() == "ambfar" { + if center, _ := s.GetBoolAttribute("cluster_center"); center || s.HasAttribute("cluster_weight") { + t.Error("ambfar must have cluster_center=false and no weight") + } + } + } +} + +// TestLengthTable checks that the values precomputed by length give the +// scores of the direct computation, in every mode. +func TestLengthTable(t *testing.T) { + rng := rand.New(rand.NewSource(33)) + entries := randomEntries(rng, 120) + for i := 0; i < 20; i++ { // some very short sequences, which skip the filter + s := entries[i].seq[:8+rng.Intn(10)] + entries[i] = entry{seq: s, length: len(s), count: 1, table: buildTable4(s)} + } + lmax := 0 + for i := range entries { + lmax = max(lmax, entries[i].length) + } + var buf obialign.BandedLCSBuffer + for _, ref := range []reference{refAlignment, refLongest, refShortest} { + for _, mode := range []scoring{ + {threshold: 0.9, normalize: true, lcsmode: true}, + {threshold: 0.8, normalize: true, lcsmode: false}, + {threshold: 30, normalize: false, lcsmode: true}, + {threshold: 6, normalize: false, lcsmode: false}, + } { + direct := mode + direct.ref, direct.ratio = ref, 3 + table := direct + table.prepareLengths(lmax) + if (ref == refShortest) != (table.byLength == nil) { + t.Fatalf("ref %v: table presence is wrong", ref) + } + for i := range entries { + for j := range entries { + a, b := direct.pairScore(&entries[i], &entries[j], -1, &buf), table.pairScore(&entries[i], &entries[j], -1, &buf) + if a != b { + t.Fatalf("ref %v mode %+v pair %d,%d: %v with the table, %v directly", ref, mode, i, j, b, a) + } + } + } + } + } +} + +// TestBlockCommon checks the sums of a block of transposed tables against +// commonWords. +func TestBlockCommon(t *testing.T) { + width := simdBlockWidth() + if width == 0 { + t.Skip("blocks of tables are used with GOEXPERIMENT=simd only") + } + rng := rand.New(rand.NewSource(8)) + seq := func(n int, alpha string) []byte { + s := make([]byte, n) + for i := range s { + s[i] = alpha[rng.Intn(len(alpha))] + } + return s + } + for it := 0; it < 200; it++ { + alpha := []string{"acgt", "ac", "a"}[it%3] + block := newTableBlock(width) + tabs := make([]*table4, width) + for j := range tabs { + tabs[j] = buildTable4(seq(rng.Intn(600), alpha)) + block.set(width, j, tabs[j]) + } + q := buildTable4(seq(rng.Intn(260), alpha)) + words := sparseWords(q) + if words == nil { + continue + } + out := make([]uint8, 64) + block.common(width, words, out) + for j, tab := range tabs { + want := commonWordsRef(q, tab) - min(q.overflow, tab.overflow) + if int(out[j]) != want { + t.Fatalf("lane %d: %d, want %d", j, out[j], want) + } + } + } +} diff --git a/pkg/obitools/obiclust/options.go b/pkg/obitools/obiclust/options.go index ea4f6d0..00a4217 100644 --- a/pkg/obitools/obiclust/options.go +++ b/pkg/obitools/obiclust/options.go @@ -3,102 +3,107 @@ package obiclust import ( "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obitools/obiconvert" "github.com/DavidGamba/go-getoptions" + log "github.com/sirupsen/logrus" ) -type ScoreNormalizationMode int +// reference is the length used to normalize a score (or to express a raw +// distance): the alignment length, the longest or the shortest sequence. +type reference int const ( - NoNormalization ScoreNormalizationMode = iota - NormalizedByShortest - NormalizedByLongest - NormalizedByAlignment + refAlignment reference = iota + refLongest + refShortest ) -var _sampleAttribute = "sample" -var _threshold = 3.0 -var _shortReference = false -var _longReference = false -var _alignReference = true -var _normalizedScore = false -var _distanceMode = false -var _exactMode = false -var _ratioMax = 1.0 -var _sorted_by_length = false -var _sorted_by_abundance = true -var _sortingAscending = false -var _onlyHead = false -var _saveGraph = "__@@NOSAVE@@__" -var _saveRatio = "__@@NOSAVE@@__" -var _minSample = 1 +var _threshold = 0.97 +var _ratio = 1.0 +var _exact = false +var _distance = false +var _raw = false +var _normalized = false +var _refShortest = false +var _refLongest = false +var _refAlignment = false +var _sortKey = "count" +var _ascending = false +var _acceptAmbiguous = false +var _otuMap = "" +var _noSequenceOutput = false func ObiclustOptionSet(options *getoptions.GetOpt) { - options.StringVar(&_sampleAttribute, "sample", _sampleAttribute, - options.Alias("s"), - options.Description("Attribute containing sample descriptions (default %s).")) - - options.Float64Var(&_threshold, "distance", _threshold, - options.Alias("--threshold"), - options.Description("Threshold to merge sequences into the same cluster (default: %d).")) - - options.Float64Var(&_ratioMax, "ratio", _ratioMax, - options.Alias("r"), - options.Description("Threshold ratio between counts (rare/abundant counts)"+ - " of two sequence records so that the less abundant one is a variant of "+ - "the more abundant (default: %3.2f).")) - - options.BoolVar(&_onlyHead, "head", _onlyHead, - options.Alias("H"), - options.Description("Select only sequences with the head status in at least one sample."), + options.Float64Var(&_threshold, "threshold", _threshold, + options.Alias("t"), + options.ArgName("THRESHOLD"), + options.Description("Score threshold for clustering. If the score is normalized and expressed in similarity (default), "+ + "it is an identity, e.g. 0.95 for an identity of 95%. If the score is normalized and expressed in distance, "+ + "it is (1.0 - identity), e.g. 0.05 for an identity of 95%. If the score is not normalized and expressed in similarity, "+ + "it is the length of the Longest Common Subsequence. If the score is not normalized and expressed in distance, "+ + "it is (reference length - LCS length)."), ) - options.StringVar(&_saveGraph, "save-graph", _saveGraph, - options.Description("Creates a directory containing the set of DAG used by the obiclean clustering algorithm. "+ - "The graph files follow the graphml format."), - ) + options.BoolVar(&_refShortest, "shortest", _refShortest, + options.Alias("l"), + options.Description("Reference sequence length is the shortest.")) - options.StringVar(&_saveRatio, "save-ratio", _saveRatio, - options.Description("Creates a file containing the set of abundance ratio on the graph edge. "+ - "The ratio file follows the csv format."), - ) + options.BoolVar(&_refLongest, "longest", _refLongest, + options.Alias("L"), + options.Description("Reference sequence length is the longest.")) - options.IntVar(&_minSample, "min-sample-count", _minSample, - options.Description("Minimum number of samples a sequence must be present in to be considered in the analysis."), - ) + options.BoolVar(&_refAlignment, "alignment", _refAlignment, + options.Alias("a"), + options.Description("Reference sequence length is the alignment length (default).")) - options.BoolVar(&_normalizedScore, "normalized-score", _normalizedScore, + options.BoolVar(&_normalized, "normalized", _normalized, options.Alias("n"), - options.Description("Use alignment score normalized by length"), - ) + options.Description("Score is normalized by the reference sequence length (default).")) - options.BoolVar(&_shortReference, "shortest", _shortReference, - options.Description("Use length of the shortest sequence to normalize alignment score."), - ) + options.BoolVar(&_raw, "raw", _raw, + options.Alias("r"), + options.Description("Raw score, not normalized.")) - options.BoolVar(&_longReference, "longest", _longReference, - options.Description("Use length of the longest sequence to normalize alignment score."), - ) + options.BoolVar(&_distance, "distance", _distance, + options.Alias("d"), + options.Description("Score is expressed in distance (default: score is expressed in similarity).")) - options.BoolVar(&_alignReference, "alignment", _alignReference, - options.Description("Use alignment length to normalize alignment score."), - ) + options.BoolVar(&_exact, "exact", _exact, + options.Alias("e"), + options.Description("A sequence is assigned to the cluster whose center presents the highest score "+ + "above the threshold, as opposed to the default 'fast' mode where it is assigned to the first "+ + "cluster found whose center presents a score above the threshold.")) - options.BoolVar(&_distanceMode, "distance", _distanceMode, - options.Description("Use alignment distance instead of similarity score."), - ) + options.Float64Var(&_ratio, "ratio", _ratio, + options.Alias("R"), + options.ArgName("RATIO"), + options.Description("Maximum ratio between the counts of two sequences so that the less abundant one "+ + "can be considered as a variant of the more abundant one (default: %3.2f).")) - options.BoolVar(&_exactMode, "exact", _exactMode, - options.Description("Use exact clustering algorithm (default is greedy)."), - ) + options.StringVar(&_sortKey, "sort", _sortKey, + options.Alias("s"), + options.ArgName("KEY"), + options.Description("Sort the sequences on KEY before clustering. KEY must be 'None' for no sorting, "+ + "or an attribute of the sequences, except for the count that can be computed (default: sorting on count). "+ + "The sequences are sorted in decreasing order (see --ascending), ties being ordered "+ + "as in the input.")) - options.BoolVar(&_sorted_by_length, "length-ordered", _sorted_by_length, - options.Description("Sort sequence by length before clustering."), - ) - options.BoolVar(&_sorted_by_abundance, "abundance-ordered", _sorted_by_abundance, - options.Description("Sort sequence by read counts (abundance) before clustering."), - ) - options.BoolVar(&_sortingAscending, "ascending-sorting", _sortingAscending, - options.Description("Sort order is ascending (default is descending)."), - ) + options.BoolVar(&_ascending, "ascending", _ascending, + options.Description("Sorting is in ascending order (default: descending).")) + + options.BoolVar(&_acceptAmbiguous, "accept-ambiguous", _acceptAmbiguous, + options.Alias("g"), + options.Description("Accept the sequences containing IUPAC letters other than a, c, g and t. "+ + "Such a sequence can be attached to a cluster (the alignment handles the IUPAC codes), "+ + "but cannot be the center of a cluster, and is left without cluster if it matches no center "+ + "(default: these sequences are discarded).")) + + options.StringVar(&_otuMap, "otu-map", _otuMap, + options.ArgName("FILENAME"), + options.Description("Write the clusters to FILENAME as a CSV table, one row per sequence "+ + "(id, cluster, cluster_center, cluster_score, count), grouped by cluster.")) + + options.BoolVar(&_noSequenceOutput, "no-sequence-output", _noSequenceOutput, + options.Alias("f"), + options.Description("Do not write the sequences on the standard output.")) } func OptionSet(options *getoptions.GetOpt) { @@ -107,44 +112,63 @@ func OptionSet(options *getoptions.GetOpt) { ObiclustOptionSet(options) } -// It returns the name of the attibute used to store sample name -func CLISampleAttribute() string { - return _sampleAttribute -} +// CLIThreshold returns the clustering threshold as given on the command line. +func CLIThreshold() float64 { return _threshold } -func CLIThreshold() float64 { - return _threshold -} +// CLIRatio returns the maximum abundance ratio between a sequence and the center it is attached to. +func CLIRatio() float64 { return _ratio } -// Returns true it the obliclean graphs must be saved -func CLISaveGraphToFiles() bool { - return _saveGraph != "__@@NOSAVE@@__" -} +// CLIExact returns true when a sequence must be attached to the best center and not to the first matching one. +func CLIExact() bool { return _exact } -// It returns the directory where the graph files are saved -func CLIGraphFilesDirectory() string { - return _saveGraph -} +// CLIDistance returns true when the score is expressed in distance. +func CLIDistance() bool { return _distance } -// Returns true it the table of ratio must be saved -func CLIIsSaveRatioTable() bool { - return _saveRatio != "__@@NOSAVE@@__" -} - -// It returns the filename of the file that stores the ratio table -func CLIRatioTableFilename() string { - return _saveRatio -} - -func CLINormalizationMode() ScoreNormalizationMode { - switch { - case _alignReference: - return NormalizedByAlignment - case _longReference: - return NormalizedByLongest - case _shortReference: - return NormalizedByShortest - default: - return NoNormalization +// CLINormalized returns true when the score is normalized by the reference length. +func CLINormalized() bool { + if _raw && _normalized { + log.Fatal("The options --raw and --normalized are exclusive") } + return !_raw } + +// CLIReference returns the length used as reference to normalize the score. +func CLIReference() reference { + n := 0 + ref := refAlignment + if _refShortest { + n++ + ref = refShortest + } + if _refLongest { + n++ + ref = refLongest + } + if _refAlignment { + n++ + } + if n > 1 { + log.Fatal("The options --shortest, --longest and --alignment are exclusive") + } + return ref +} + +// CLISortKey returns the key used to sort the sequences, or "None". +func CLISortKey() string { + if _sortKey == "none" { + return "None" + } + return _sortKey +} + +// CLIAscending returns true when the sort must be in ascending order. +func CLIAscending() bool { return _ascending } + +// CLIAcceptAmbiguous returns true when the sequences with IUPAC letters other than acgt are kept. +func CLIAcceptAmbiguous() bool { return _acceptAmbiguous } + +// CLIOTUMap returns the name of the OTU map file, empty if none is requested. +func CLIOTUMap() string { return _otuMap } + +// CLINoSequenceOutput returns true when the sequences must not be written. +func CLINoSequenceOutput() bool { return _noSequenceOutput } diff --git a/pkg/obitools/obiclust/output.go b/pkg/obitools/obiclust/output.go new file mode 100644 index 0000000..15084f1 --- /dev/null +++ b/pkg/obitools/obiclust/output.go @@ -0,0 +1,128 @@ +package obiclust + +import ( + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiitercsv" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq" +) + +// Annotate writes the result of the clustering on the sequences and returns +// them in the order sumaclust writes them: the groups of identical sequences +// in their sorted order, each group beginning with its head. +// +// Every sequence receives +// - cluster: the identifier of the center of its cluster; +// - cluster_score: the score with this center; +// - cluster_center: true for the center only; +// - cluster_weight: the cumulated count of the sequences of the cluster. +// +// A sequence with IUPAC letters that matches no center belongs to no cluster: +// it only gets cluster_center=false. +func (res *Result) Annotate() obiseq.BioSequenceSlice { + recs, groups, cl := res.recs, res.groups, res.clustered + weight := make([]int, len(groups)) + for g := range groups { + if cl.center[g] >= 0 { + weight[cl.center[g]] += groups[g].count + } + } + + out := make(obiseq.BioSequenceSlice, 0, len(recs)) + for g, grp := range groups { + ctr := cl.center[g] + + for r := grp.start; r < grp.end; r++ { + s := recs[r].seq + if ctr < 0 { + // a sequence with IUPAC letters that matches no center + s.RemoveAttribute("cluster") + s.RemoveAttribute("cluster_score") + s.RemoveAttribute("cluster_weight") + s.SetAttribute("cluster_center", false) + out = append(out, s) + continue + } + centerID := recs[groups[ctr].start].seq.Id() + s.SetAttribute("cluster", centerID) + s.SetAttribute("cluster_score", cl.score[g]) + s.SetAttribute("cluster_center", ctr == g && r == grp.start) + s.SetAttribute("cluster_weight", weight[ctr]) + out = append(out, s) + } + } + return out +} + +// OTUMapIterator describes the clusters as a CSV table, one row per sequence: +// - id: the identifier of the sequence; +// - cluster: the identifier of the center of its cluster; +// - cluster_center: true for the center only; +// - cluster_score: the score with this center; +// - count: the count of the sequence. +// +// The rows are grouped by cluster, in the order the clusters were created. In +// a cluster, the center comes first, followed by the sequences identical to +// it, then by the other members, each followed by its identical sequences. +// The sequences without cluster (see Annotate) come last, with no cluster nor +// score. +func (res *Result) OTUMapIterator(source string, batchSize int) *obiitercsv.ICSVRecord { + recs, groups, cl := res.recs, res.groups, res.clustered + + iterator := obiitercsv.NewICSVRecord() + iterator.Add(1) + for _, field := range []string{"id", "cluster", "cluster_center", "cluster_score", "count"} { + iterator.AppendField(field) + } + + members := make(map[int][]int, len(cl.centers)) + var orphans []int + for g := range groups { + if ctr := cl.center[g]; ctr >= 0 { + members[ctr] = append(members[ctr], g) + } else { + orphans = append(orphans, g) + } + } + + go func() { + iterator.WaitAndClose() + }() + + go func() { + order := 0 + batch := make([]obiitercsv.CSVRecord, 0, batchSize) + add := func(g int) { + ctr := cl.center[g] + for r := groups[g].start; r < groups[g].end; r++ { + row := obiitercsv.CSVRecord{ + "id": recs[r].seq.Id(), + "cluster_center": ctr == g && r == groups[g].start, + "count": recs[r].count, + } + if ctr >= 0 { + row["cluster"] = recs[groups[ctr].start].seq.Id() + row["cluster_score"] = cl.score[g] + } + batch = append(batch, row) + if len(batch) == batchSize { + iterator.Push(obiitercsv.MakeCSVRecordBatch(source, order, batch)) + order++ + batch = make([]obiitercsv.CSVRecord, 0, batchSize) + } + } + } + for _, ctr := range cl.centers { + for _, g := range members[ctr] { + add(g) + } + } + for _, g := range orphans { + add(g) + } + if len(batch) > 0 || order == 0 { + iterator.Push(obiitercsv.MakeCSVRecordBatch(source, order, batch)) + } + iterator.Done() + }() + + return iterator +} diff --git a/pkg/obitools/obiclust/prepare.go b/pkg/obitools/obiclust/prepare.go new file mode 100644 index 0000000..73a0f7a --- /dev/null +++ b/pkg/obitools/obiclust/prepare.go @@ -0,0 +1,178 @@ +package obiclust + +import ( + "bytes" + "sort" + "strconv" + "strings" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiutils" + log "github.com/sirupsen/logrus" +) + +// record is a sequence of the dataset. +type record struct { + seq *obiseq.BioSequence + pos int // position in the dataset + clean []byte // the nucleotides of the sequence, in lower case + count int + key float64 // value of the sorting key + ambiguous bool // contains IUPAC letters other than a, c, g and t +} + +// group is a run of identical sequences: recs[start] is its head, the +// following ones are the duplicates. count is the cumulated count, key the +// value of the sorting key of the head. +type group struct { + start, end int + count int + key float64 +} + +// nucleotide classes of a byte +const ( + notNucleotide = iota + plainNucleotide + ambiguousNucleotide +) + +var nucleotideClass = func() (t [256]uint8) { + for _, c := range "acgt" { + t[c] = plainNucleotide + } + for _, c := range "umrwsykvhdbn" { + t[c] = ambiguousNucleotide + } + return t +}() + +// classify returns the nucleotides of a sequence and tells if it contains +// IUPAC letters other than a, c, g and t. nil is returned if the sequence is +// empty, or contains anything else than IUPAC letters (and also IUPAC letters +// other than a, c, g and t unless ambiguous ones are accepted). +func classify(s []byte, acceptAmbiguous bool) (clean []byte, ambiguous bool) { + if len(s) == 0 { + return nil, false + } + for _, c := range s { + switch nucleotideClass[c] { + case notNucleotide: + return nil, false + case ambiguousNucleotide: + if !acceptAmbiguous { + return nil, false + } + ambiguous = true + } + } + return s, ambiguous +} + +// atof reads the leading number of a string as C atof does. +func atof(v interface{}) float64 { + if s, ok := v.(string); ok { + s = strings.TrimSpace(s) + for n := len(s); n > 0; n-- { + if f, err := strconv.ParseFloat(s[:n], 64); err == nil { + return f + } + } + return 0 + } + f, err := obiutils.InterfaceToFloat64(v) + if err != nil { + return 0 + } + return f +} + +// sortKeyOf returns the value of the sorting key of a sequence. +func sortKeyOf(s *obiseq.BioSequence, key string) float64 { + if key == "count" { + return float64(s.Count()) + } + v, ok := s.GetAttribute(key) + if !ok { + log.Fatalf("'%s' is not an attribute of the sequence %s", key, s.Id()) + } + return atof(v) +} + +// prepare reads the sequences, groups the identical ones and orders them. +// +// The order is fully determined, whatever the sorting algorithm: +// +// 1. the records of a group of identical sequences are ordered on the +// sorting key (decreasing, increasing if ascending), then on their +// position in the dataset; the first one is the head of the group, whose +// identifier names the cluster if it is a center; +// 2. the groups are ordered on the sorting key of their head (the count of +// the whole group if the key is "count"), then on the position of the +// head in the dataset. With the key "None" they are in the order of the +// first occurrence of their sequence. +// +// The sequences containing anything else than a, c, g and t are discarded, +// except if acceptAmbiguous is set and they only contain IUPAC letters: they +// are then kept, but cannot be the center of a cluster. +func prepare(db obiseq.BioSequenceSlice, key string, ascending, acceptAmbiguous bool) ([]record, []group, int) { + recs := make([]record, 0, len(db)) + discarded := 0 + + for pos, s := range db { + clean, ambiguous := classify(s.Sequence(), acceptAmbiguous) + if clean == nil { + discarded++ + continue + } + r := record{seq: s, pos: pos, clean: clean, count: s.Count(), ambiguous: ambiguous} + if key != "None" { + r.key = sortKeyOf(s, key) + } + recs = append(recs, r) + } + + // before reports whether a is ranked before b by the sorting key + before := func(a, b float64) bool { + if ascending { + return a < b + } + return a > b + } + + sort.Slice(recs, func(i, j int) bool { + a, b := &recs[i], &recs[j] + if c := bytes.Compare(a.clean, b.clean); c != 0 { + return c < 0 + } + if a.key != b.key && key != "None" { + return before(a.key, b.key) + } + return a.pos < b.pos + }) + + groups := make([]group, 0, len(recs)) + for i := 0; i < len(recs); { + j, count := i+1, recs[i].count + for j < len(recs) && bytes.Equal(recs[j].clean, recs[i].clean) { + count += recs[j].count + j++ + } + g := group{start: i, end: j, count: count, key: recs[i].key} + if key == "count" { + g.key = float64(count) + } + groups = append(groups, g) + i = j + } + + sort.Slice(groups, func(i, j int) bool { + a, b := &groups[i], &groups[j] + if key != "None" && a.key != b.key { + return before(a.key, b.key) + } + return recs[a.start].pos < recs[b.start].pos + }) + + return recs, groups, discarded +} diff --git a/pkg/obitools/obiclust/score.go b/pkg/obitools/obiclust/score.go new file mode 100644 index 0000000..5d13a08 --- /dev/null +++ b/pkg/obitools/obiclust/score.go @@ -0,0 +1,233 @@ +package obiclust + +import ( + "math" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obialign" + log "github.com/sirupsen/logrus" +) + +// entry is a distinct sequence of the dataset with what the clustering needs. +type entry struct { + seq []byte + length int + count int // cumulated count of the identical sequences + table *table4 + sparse []uint16 // words of the table (see sparseWords), nil if unusable + // ambiguous sequences contain IUPAC letters other than a, c, g and t: + // they have no 4-mer table, and cannot be the center of a cluster + ambiguous bool +} + +// scoring holds the way sumaclust scores a pair of sequences and decides +// whether it passes the threshold. It reproduces the arithmetic of the +// reference implementation (integer truncations, floating point +// comparisons) so that the same pairs are accepted with the same scores. +type scoring struct { + threshold float64 // threshold as used for the comparison (1 - t for a normalized distance) + normalize bool + lcsmode bool // true: score is a similarity, false: it is a distance + ref reference + ratio float64 + worst float64 // score of a pair that cannot be attached + + // byLength gives, for the pairs whose longest sequence has a length l, + // the values that depend on l only. It is built when the reference is not + // the shortest sequence, otherwise those values depend on both lengths. + byLength []lengthInfo +} + +type lengthInfo struct { + lcsMin int32 // see lcsMin + minCommon int32 // see minCommon +} + +// prepareLengths fills byLength for the lengths up to lmax. +func (sc *scoring) prepareLengths(lmax int) { + if sc.ref == refShortest { + return + } + sc.byLength = make([]lengthInfo, lmax+1) + for l := range sc.byLength { + sc.byLength[l] = lengthInfo{int32(sc.lcsMin(l, l)), int32(sc.minCommon(l, l))} + } +} + +// similarityLike tells if the comparison with the threshold is "score >= threshold". +func (sc *scoring) similarityLike() bool { return sc.lcsmode || sc.normalize } + +// lcsMin is the smallest LCS score that a pair of lengths l1 >= l2 can have +// and still pass the threshold (calculateLCSmin of sumaclust). +func (sc *scoring) lcsMin(l1, l2 int) int { + switch { + case sc.threshold <= 0: + return 0 + case sc.normalize: + if sc.ref == refShortest { + return int(sc.threshold * float64(l2)) + } + return int(sc.threshold * float64(l1)) + case sc.lcsmode: + return int(sc.threshold) + case sc.ref == refShortest: + return int(float64(l2) - sc.threshold) + default: + return int(float64(l1) - sc.threshold) + } +} + +// minCommon is the number of 4-mers that two sequences of lengths l1 >= l2 +// must share to be possibly similar enough (isPossibleSumathings). +func (sc *scoring) minCommon(l1, l2 int) int { + reflen := l2 + if sc.ref == refAlignment || sc.ref == refLongest { + reflen = l1 + } + var lcs int + if sc.normalize { + lcs = int(math.Ceil(float64(reflen) * sc.threshold)) + } else { + th := sc.threshold + if !sc.lcsmode { + th = float64(reflen) - th + } + lcs = int(th) + } + return minCommonWords(l1, lcs) +} + +// pairScore returns the score of a query sequence against a center, or the +// worst score when the pair cannot be attached. +// +// common, if not negative, is the sum of the minima of the 4-mer counters of +// the two sequences, already computed by a block of tables. +func (sc *scoring) pairScore(q, c *entry, common int, buf *obialign.BandedLCSBuffer) float64 { + // The quotient of two counts is not above 1 when the first is not above + // the second, which spares the division in the usual case. + if !(sc.ratio >= 1 && q.count <= c.count) && float64(q.count)/float64(c.count) > sc.ratio { + return sc.worst + } + + l1, l2 := q.length, c.length + long, short := q, c + if l1 < l2 { + l1, l2 = l2, l1 + long, short = c, q + } + var lcsMin, minCommon int + if sc.byLength != nil { + info := sc.byLength[l1] + lcsMin, minCommon = int(info.lcsMin), int(info.minCommon) + } else { + lcsMin, minCommon = sc.lcsMin(l1, l2), sc.minCommon(l1, l2) + } + if l2 < lcsMin { + return sc.worst + } + // The 4-mer filter counts identical words: it does not hold when IUPAC + // codes can match different letters. It lets short sequences pass. + if l1 >= 12 && l2 >= 12 && !long.ambiguous && !short.ambiguous { + if common >= 0 { + common += min(long.table.overflow, short.table.overflow) + } else { + common = commonWords(long.table, short.table) + } + if common < minCommon { + return sc.worst + } + } + + // The length of the alignment is needed only to normalize by it, or to + // express a distance with it; the score alone is much cheaper. + var lcs, alilen int + var err error + if sc.ref == refAlignment && (sc.normalize || !sc.lcsmode) { + lcs, alilen, err = buf.AtLeast(q.seq, c.seq, lcsMin) + } else { + lcs, err = buf.ScoreAtLeast(q.seq, c.seq, lcsMin) + } + if err != nil { + log.Fatalf("Cannot align two sequences: %v", err) + } + if lcs < 0 { + // The best alignment has a score under lcsMin: the pair cannot pass. + return sc.worst + } + + id := float64(lcs) + if !sc.lcsmode && !sc.normalize { + switch sc.ref { + case refAlignment: + id = float64(alilen) - id + case refLongest: + id = float64(l1) - id + case refShortest: + id = float64(l2) - id + } + } + if sc.normalize { + switch sc.ref { + case refAlignment: + id /= float64(alilen) + case refLongest: + id /= float64(l1) + case refShortest: + id /= float64(l2) + } + } + return id +} + +// beats tells if the score s is better than the score best (without any +// threshold). +func (sc *scoring) beats(s, best float64) bool { + if sc.similarityLike() { + return s > best + } + return s < best +} + +// lcsMinFor is lcsMin for sequences of lengths l1 >= l2, from the table when it exists. +func (sc *scoring) lcsMinFor(l1, l2 int) int { + if sc.byLength != nil { + return int(sc.byLength[l1].lcsMin) + } + return sc.lcsMin(l1, l2) +} + +// lengthCompatible tells if two sequences of these lengths may be similar enough: +// the shortest cannot be shorter than the minimum LCS score of the pair. +func (sc *scoring) lengthCompatible(la, lb int) bool { + l1, l2 := max(la, lb), min(la, lb) + return l2 >= sc.lcsMinFor(l1, l2) +} + +// selects tells if a pair of score s must replace the best pair found so far, +// of score best. +func (sc *scoring) selects(s, best float64) bool { + if sc.similarityLike() { + return s >= sc.threshold && s > best + } + return s <= sc.threshold && s < best +} + +// centerScore is the score of a sequence with itself. +func (sc *scoring) centerScore(length int) float64 { + switch { + case sc.normalize && sc.lcsmode: + return 1.0 + case !sc.lcsmode: + return 0.0 + default: + return float64(length) + } +} + +// memberScore is the score reported for a sequence attached to a center +// with the score s. +func (sc *scoring) memberScore(s float64) float64 { + if !sc.lcsmode && sc.normalize { + return 1.0 - s + } + return s +} diff --git a/pkg/obitools/obiclust/testdata/cases.txt b/pkg/obitools/obiclust/testdata/cases.txt new file mode 100644 index 0000000..5e543da --- /dev/null +++ b/pkg/obitools/obiclust/testdata/cases.txt @@ -0,0 +1,9 @@ +families default -t 0.9 +families exact -t 0.9 -e +families shortest -t 0.9 -l -e +families longest -t 0.85 -L +families distance -t 0.1 -d +families rawdist -t 4 -r -d -e +families rawlcs -t 40 -r +families ratio -t 0.9 -R 0.5 +families ascending -t 0.9 -o diff --git a/pkg/obitools/obiclust/testdata/families_ascending.expected b/pkg/obitools/obiclust/testdata/families_ascending.expected new file mode 100644 index 0000000..dfd6028 --- /dev/null +++ b/pkg/obitools/obiclust/testdata/families_ascending.expected @@ -0,0 +1,101 @@ +f5v2s55 f5v2s55 1.000000 T 666 cggcatcgcaccataagatggtaatcactaaaaaaatcggcctaacctgtccatatcgccact +f1v6s15 f1v6s15 1.000000 T 3070 gaatcagcttctaaccaagcgttgcagaaccggctactttaagcatgatgaatgcgtcgtaagtgatactcgacgattc +f7v2s66 f7v2s66 1.000000 T 3988 gcttcgtgggaaccagaacaaccgacatattccgaaccaccggacatacgatccttaaacc +f4v7s51 f4v7s51 1.000000 T 8599 cgtttattcgtaggatctactacgcatcaaagtatcccacttttgtaccccactaagagcaga +f5v1s54 f5v1s54 1.000000 T 9091 cggaacgcaccataagatggtaatcactaaaaaaatcggcctaacctgtccatatcgccactt +f7v1s65 f7v1s65 1.000000 T 11279 gcttcggggaaccagagacaaccgacatatttcgaaccaccgacatacgatcctttaaacc +f3v8s39 f3v8s39 1.000000 T 13623 cgaaaaaggtgaatgaaaggcctgggacgctcctggttggggcgctctaggtgtgtggcttgaacgttagacct +f4v4s48 f4v4s48 1.000000 T 15906 ctgtattcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactaagagaaa +f3v11s42 f3v11s42 1.000000 T 18309 cgaaaaaggtgaatgaaagtctgggacgctcctggtttgggacgctctaggtgttgtggcttgaacgttagcacct +f6v3s63 f6v3s63 1.000000 T 19024 catgtcaagagactggaaccccggtgatgtgaaaaaaggttttgggtcacgtctccatggctacg +f11v3s94 f11v3s94 1.000000 T 21189 agcgttagcaaagagcctggttaatcgccgtcgaaggatcggtggtgatcagagatgcgggcatccgtttc +f2v6s24 f2v6s24 1.000000 T 21984 tggactagtagacgtgtgcctcatcgacgcgtgagacactttgatttcttatacggcgtctcag +f0v8s8 f0v8s8 1.000000 T 24872 ggatcacagtcctacactgctcactccaaccccggccctgagtccgaggagagggtgcttcagagtatgtataccactg +f7v3s67 f7v3s67 1.000000 T 26251 gcttcgtgggaccagagacaaccgacatatttcgaaccaccgacatacgatccttaaacc +f2v9s27 f2v9s27 1.000000 T 26719 tggactagtagaccgtgtgcctaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f4v5s49 f4v5s49 1.000000 T 26933 ctatattcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactaagagaagca +f11v9s100 f11v9s100 1.000000 T 27405 agcgttagcaaaggagcctggttaatcgccctcgaggatcggtggtgatcagagatgcgggcatccgtttc +f2v12s30 f2v12s30 1.000000 T 27908 tggactagtagaccgtgtgccacaatcgacgctgaggacacgttgatttttatacggcgtctcag +f9v0s76 f9v0s76 1.000000 T 29993 gggatcgtggcaagcaacccttgccgcctcgactgcggctaaagacgcatgttccagtaataccta +f4v6s50 f4v6s50 1.000000 T 30723 ctgtattcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactaagacgaagag +f6v2s62 f6v2s62 1.000000 T 33001 catgtcgagagactggaaccccggtgatgtgaaaaaaggtcttgggtacgtcccatggctac +f11v5s96 f11v5s96 1.000000 T 33026 agcgttagcaaagagcctggttaatggccctcgaggatcggtggctgaccagagatgcgggcatccgtttc +f8v1s69 f8v1s69 1.000000 T 33813 aagaccttgaacacctctaagcgaaaattggatatcgagtcagggaacataaccacgggtgcgt +f11v4s95 f11v4s95 1.000000 T 34530 agcgttagcaaagagcctggttaatcccctcgagatcgggtggtgatcagagatgcgggcatccgtttc +f3v9s40 f3v9s40 1.000000 T 35203 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggggttgtggcttgaacgttagcacct +f11v2s93 f11v2s93 1.000000 T 35758 agcgttagcaaagagcctggttaatcgccctcggaggatcggtggtgatcagagatgcgggcatccgttc +f10v6s83 f10v6s83 1.000000 T 36011 ttccggacactcgcaatcttccaagcagaggtccagctaacacacagcaggcatttagtcctgatgggtagc +f3v7s38 f3v7s38 1.000000 T 36703 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggtgttcgtgcttgaacgttagcacct +f3v10s41 f3v10s41 1.000000 T 37492 cgaaaataaggtgaatgaaagggtgggacgctcctagtttggggcgctctaggtgttgtggcttgaacgttagcacct +f5v4s57 f5v4s57 1.000000 T 38613 cggcaacgcaccataagatggtaatcactaaaaaaatcggcctaacctgtccatatcgccactat +f10v1s78 f10v1s78 1.000000 T 43528 ttgccggacactcgcaatcctccaagcagaggtccagctaacacacagcaggcatttagtcccgatgaggtagc +f10v8s85 f10v8s85 1.000000 T 46704 ttgccggacactcgaatcttccaagcagaggtccagctaacacacagcaggcatactagtcccgatgggtagc +f6v0s60 f6v0s60 1.000000 T 48958 catgtcaagagactggaaccccggtgatgtgaaaaaaggtcttgggtacgtcccatggctacg +f8v6s74 f8v6s74 1.000000 T 49062 aagacccttgaacacctctaagcgaaaattggattcgagtcagggaacataaccacgggtgccgt +f1v0s9 f1v0s9 1.000000 T 49791 gaatcagcttctaaccaagcgatgcagaaccggctactttaagcattgatgaatgcgtcgtaagtgatactcgacgattc +f5v5s58 f5v5s58 1.000000 T 49852 cgcaacgcaccattaagatggtaatcactaaaaaaatcggcctaacctgtccaatcgccactt +f0v5s5 f0v5s5 1.000000 T 54120 ggatcacagtctacactgctcactcccccccggcccctgagtccgaggagagggtgcttcaaagtatgtataccactg +f4v1s45 f4v1s45 1.000000 T 55908 ctgtatttcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactatagagaaga +f11v6s97 f11v6s97 1.000000 T 58908 agcgttagcaaagaacctggttaatcgccctcgaggatcggtggtggatcggagatgcgggcatcgcgtttc +f3v4s35 f3v4s35 1.000000 T 59170 cgaaaaaggtgaagaaaggctgggacgctcctggttggcgcgctctaggtgttgtggcttgaacgttagcaccta +f3v1s32 f3v1s32 1.000000 T 59632 cgaaaaagagtgaatgaaatggctgggacgctcctggtttggggcgctctaggtgttgtggcttgaacgttagcacct +f8v4s72 f8v4s72 1.000000 T 59724 aagacttgaacacctctaagcgaaaattggattcgagtcagggaacataaccacgggtgcgt +f8v5s73 f8v5s73 1.000000 T 64084 aagaccttgtacacctctaagcgataaattggattcgagtcagggaacataaccacggatgcgt +f11v0s91 f11v0s91 1.000000 T 66160 agcgttagcaaagagcctggttaatcgccctcgaggatcggtggtgatcagagatgcgggcatccgtttc +f10v10s87 f10v10s87 1.000000 T 66794 tctgccggacactcgcaatcttccaagcagaggtccagctaacacacgcaggcatttatccgatgggtagc +f4v3s47 f4v3s47 1.000000 T 69396 ctgtattcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactaaagaaga +f8v3s71 f8v3s71 1.000000 T 70497 aagaccttgaacacctctaagcgaaattggattcgagtcagcgaacataacacgggtgcgt +f3v2s33 f3v2s33 1.000000 T 70860 cgaaaaaaggtaatgaaaggctgggatgctcctggtttggggcgctctaggtgttgtggcttgaacgttagcact +f11v1s92 f11v1s92 1.000000 T 71853 agcgttagcaaagagcctggttcatcgccctcgaggatcggtggtgatcaagagatgcggcatccgtttc +f7v0s64 f7v0s64 1.000000 T 72498 gcttcgtgggaaccagagacaaccgacatatttcgaaccaccgacatacgatccttaaacc +f11v8s99 f11v8s99 1.000000 T 72674 agcgttagcaaagagcctggttaatctgccctcgaggatcggtggtgatcagagatgcgggcatccgtttc +f8v2s70 f8v2s70 1.000000 T 74088 aagaccttgaacaacctctaagccgaaattggaattcgagtcagggaacataaccacgggtgcgat +f4v2s46 f4v2s46 1.000000 T 74433 ctgtattcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactaagagaaga +f10v12s89 f10v12s89 1.000000 T 74438 ttgccggacactcgcaatcttccaagcagaggtccagctaacacacacgcaggcatttagacccgatgggtagc +f8v7s75 f8v7s75 1.000000 T 75803 aagaccttgaacacctctaagcgaaaattggattccagtcagggaaccataaccacgggtgcgt +f3v5s36 f3v5s36 1.000000 T 76948 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggtgttatggcttgaaagttagcacct +f10v11s88 f10v11s88 1.000000 T 78894 ttgccggacactcgcaatcttccaagcagaggtccagctaactcacagccaggcatttagtcccgatgggtagc +f1v8s17 f1v8s17 1.000000 T 79492 gaatcaggttcttccaagcgctgcagaaccggctactttaagcattgatgaatgcgtcagtaagtgatacccgacgattc +f2v1s19 f1v8s17 1.000000 F 79492 gaatcaggttcttccaagcgctgcagaaccggctactttaagcattgatgaatgcgtcagtaagtgatacccgacgattc +f8v0s68 f8v0s68 1.000000 T 80207 aagaccttgaacacctctaagcgaaaattggattcgagtcagggaacataaccacgggtgcgt +f3v12s43 f3v12s43 1.000000 T 80835 cgaaaaaggtgaatgaaagactgggacgcctcctggtttgggacgctctaggtggttgtggcttgaacgttagcacct +f2v5s23 f2v5s23 1.000000 T 81657 gaatcagcttcaaccaagcgatgcagaaccgctgctttaagcatgatgaatcgtcgtaagtgatactcgacgattc +f1v7s16 f2v5s23 1.000000 F 81657 gaatcagcttcaaccaagcgatgcagaaccgctgctttaagcatgatgaatcgtcgtaagtgatactcgacgattc +f0v2s2 f0v2s2 1.000000 T 83299 ggatcacagtctacactgctcactccaaccccggcccctgagtccgaggagagggtgcttcagagtatgtataccactg +f0v0s0 f0v2s2 1.000000 F 83299 ggatcacagtctacactgctcactccaaccccggcccctgagtccgaggagagggtgcttcagagtatgtataccactg +f1v3s12 f1v3s12 1.000000 T 84418 ggatcacagtctacactgctccctccaaccccggcccctggtccgaggagagggtgcttcagagtattataccactg +f0v6s6 f1v3s12 1.000000 F 84418 ggatcacagtctacactgctccctccaaccccggcccctggtccgaggagagggtgcttcagagtattataccactg +f2v11s29 f2v11s29 1.000000 T 85592 tggactgtagaaccgtgtgcctcaatccacgcgtgaggacacgttgatttcttatacggcgtctcag +f4v8s52 f4v8s52 1.000000 T 86639 ctgatattcgtattgatctactacgtatcaaagtatcccacttttgtgaccccactaagagaaga +f10v5s82 f10v5s82 1.000000 T 87843 ttgccggacactcgcaatcttccaagcagaggtccagctaacacatagcaggcatttagtcccgatgggtagc +f10v4s81 f10v4s81 1.000000 T 88973 ttgccggacactggcaatcttccatagcagaggtccagctaacacacagcaggcatttagtcccgatgggtagc +f1v4s13 f1v4s13 1.000000 T 90139 gaatcagcttctcaaccaagcgatgcagaaccggctactttaagcattgatgaatgcgtcgtaagtgatactggacgattc +f10v13s90 f10v13s90 1.000000 T 92493 tgccggacactcgcaatctccaagcagagagtccaagctaacacacagcaggcatttgtcccgatgggtagc +f10v2s79 f10v2s79 1.000000 T 93964 ttgccggtcactcgcaatcttaccaaccagaggtccagctaacacacagcaggcatttagtcccgatgggtagc +f5v3s56 f5v3s56 1.000000 T 94426 ccgcaacgcaccataagatgtaatcactaagaaaaatcggcctaacctgtcatatcgccactt +f0v3s3 f0v3s3 1.000000 T 95466 ggatcacagtctacactgctcactccagaccccgcccctgagtccgaggagagggtgcttcagagtatgttaccactg +f0v1s1 f0v3s3 1.000000 F 95466 ggatcacagtctacactgctcactccagaccccgcccctgagtccgaggagagggtgcttcagagtatgttaccactg +f0v4s4 f0v3s3 1.000000 F 95466 ggatcacagtctacactgctcactccagaccccgcccctgagtccgaggagagggtgcttcagagtatgttaccactg +f1v5s14 f0v3s3 1.000000 F 95466 ggatcacagtctacactgctcactccagaccccgcccctgagtccgaggagagggtgcttcagagtatgttaccactg +f2v10s28 f2v10s28 1.000000 T 97350 ggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f5v6s59 f2v10s28 1.000000 F 97350 ggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f10v0s77 f10v0s77 1.000000 T 97674 ttgccggacactcgcaatcttccaagcagaggtccagctaacacacagcaggcatttagtcccgatgggtagc +f10v7s84 f10v7s84 1.000000 T 98678 gtgccggacactcgcaatcttcacaagcagaggtccagctaacacacagcagcatttagtcccgatgggtagc +f2v4s22 f2v4s22 1.000000 T 99661 tggactagtagaccgtgtcgcctcaatcgacgcgtgaggacacgttgattcttatacggcgtctcag +f11v7s98 f11v7s98 1.000000 T 167833 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgtctctaggttgttgtggctgaacgtagcact +f5v0s53 f11v7s98 1.000000 F 167833 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgtctctaggttgttgtggctgaacgtagcact +f3v6s37 f11v7s98 1.000000 F 167833 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgtctctaggttgttgtggctgaacgtagcact +f1v1s10 f1v1s10 1.000000 T 173457 ggatcacacgtctatcactgctcactccaaccccggcccttgacgtccgaggagagggtgcttctgagtatgtatacccaactg +f1v2s11 f1v1s10 1.000000 F 173457 ggatcacacgtctatcactgctcactccaaccccggcccttgacgtccgaggagagggtgcttctgagtatgtatacccaactg +f0v7s7 f1v1s10 1.000000 F 173457 ggatcacacgtctatcactgctcactccaaccccggcccttgacgtccgaggagagggtgcttctgagtatgtatacccaactg +f3v0s31 f3v0s31 1.000000 T 182106 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggtgttgtggcttgaacgttagcacct +f4v0s44 f3v0s31 1.000000 F 182106 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggtgttgtggcttgaacgttagcacct +f3v3s34 f3v0s31 1.000000 F 182106 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggtgttgtggcttgaacgttagcacct +f2v2s20 f2v2s20 1.000000 T 243753 tggactagtagaccgtgtgcctcaatcgacgcggaggacacgttgatttcttatacggcgtctcag +f6v1s61 f2v2s20 1.000000 F 243753 tggactagtagaccgtgtgcctcaatcgacgcggaggacacgttgatttcttatacggcgtctcag +f2v3s21 f2v2s20 1.000000 F 243753 tggactagtagaccgtgtgcctcaatcgacgcggaggacacgttgatttcttatacggcgtctcag +f10v9s86 f10v9s86 1.000000 T 260688 tggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f2v7s25 f10v9s86 1.000000 F 260688 tggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f2v0s18 f10v9s86 1.000000 F 260688 tggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f10v3s80 f10v9s86 1.000000 F 260688 tggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f2v8s26 f10v9s86 1.000000 F 260688 tggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag diff --git a/pkg/obitools/obiclust/testdata/families_default.expected b/pkg/obitools/obiclust/testdata/families_default.expected new file mode 100644 index 0000000..fc0c73b --- /dev/null +++ b/pkg/obitools/obiclust/testdata/families_default.expected @@ -0,0 +1,101 @@ +f2v8s26 f2v8s26 1.000000 T 863655 tggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f10v3s80 f2v8s26 1.000000 F 863655 tggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f2v0s18 f2v8s26 1.000000 F 863655 tggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f2v7s25 f2v8s26 1.000000 F 863655 tggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f10v9s86 f2v8s26 1.000000 F 863655 tggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f2v3s21 f2v8s26 0.985075 F 863655 tggactagtagaccgtgtgcctcaatcgacgcggaggacacgttgatttcttatacggcgtctcag +f6v1s61 f2v8s26 0.985075 F 863655 tggactagtagaccgtgtgcctcaatcgacgcggaggacacgttgatttcttatacggcgtctcag +f2v2s20 f2v8s26 0.985075 F 863655 tggactagtagaccgtgtgcctcaatcgacgcggaggacacgttgatttcttatacggcgtctcag +f3v3s34 f3v3s34 1.000000 T 838714 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggtgttgtggcttgaacgttagcacct +f4v0s44 f3v3s34 1.000000 F 838714 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggtgttgtggcttgaacgttagcacct +f3v0s31 f3v3s34 1.000000 F 838714 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggtgttgtggcttgaacgttagcacct +f0v7s7 f0v7s7 1.000000 T 281628 ggatcacacgtctatcactgctcactccaaccccggcccttgacgtccgaggagagggtgcttctgagtatgtatacccaactg +f1v2s11 f0v7s7 1.000000 F 281628 ggatcacacgtctatcactgctcactccaaccccggcccttgacgtccgaggagagggtgcttctgagtatgtatacccaactg +f1v1s10 f0v7s7 1.000000 F 281628 ggatcacacgtctatcactgctcactccaaccccggcccttgacgtccgaggagagggtgcttctgagtatgtatacccaactg +f3v6s37 f3v3s34 0.935897 F 838714 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgtctctaggttgttgtggctgaacgtagcact +f5v0s53 f3v3s34 0.935897 F 838714 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgtctctaggttgttgtggctgaacgtagcact +f11v7s98 f3v3s34 0.935897 F 838714 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgtctctaggttgttgtggctgaacgtagcact +f2v4s22 f2v8s26 0.970588 F 863655 tggactagtagaccgtgtcgcctcaatcgacgcgtgaggacacgttgattcttatacggcgtctcag +f10v7s84 f10v7s84 1.000000 T 905994 gtgccggacactcgcaatcttcacaagcagaggtccagctaacacacagcagcatttagtcccgatgggtagc +f10v0s77 f10v7s84 0.959459 F 905994 ttgccggacactcgcaatcttccaagcagaggtccagctaacacacagcaggcatttagtcccgatgggtagc +f5v6s59 f2v8s26 0.985075 F 863655 ggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f2v10s28 f2v8s26 0.985075 F 863655 ggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f1v5s14 f1v5s14 1.000000 T 234004 ggatcacagtctacactgctcactccagaccccgcccctgagtccgaggagagggtgcttcagagtatgttaccactg +f0v4s4 f1v5s14 1.000000 F 234004 ggatcacagtctacactgctcactccagaccccgcccctgagtccgaggagagggtgcttcagagtatgttaccactg +f0v1s1 f1v5s14 1.000000 F 234004 ggatcacagtctacactgctcactccagaccccgcccctgagtccgaggagagggtgcttcagagtatgttaccactg +f0v3s3 f1v5s14 1.000000 F 234004 ggatcacagtctacactgctcactccagaccccgcccctgagtccgaggagagggtgcttcagagtatgttaccactg +f5v3s56 f5v3s56 1.000000 T 192648 ccgcaacgcaccataagatgtaatcactaagaaaaatcggcctaacctgtcatatcgccactt +f10v2s79 f10v7s84 0.920000 F 905994 ttgccggtcactcgcaatcttaccaaccagaggtccagctaacacacagcaggcatttagtcccgatgggtagc +f10v13s90 f10v7s84 0.907895 F 905994 tgccggacactcgcaatctccaagcagagagtccaagctaacacacagcaggcatttgtcccgatgggtagc +f1v4s13 f1v4s13 1.000000 T 304149 gaatcagcttctcaaccaagcgatgcagaaccggctactttaagcattgatgaatgcgtcgtaagtgatactggacgattc +f10v4s81 f10v7s84 0.933333 F 905994 ttgccggacactggcaatcttccatagcagaggtccagctaacacacagcaggcatttagtcccgatgggtagc +f10v5s82 f10v7s84 0.945946 F 905994 ttgccggacactcgcaatcttccaagcagaggtccagctaacacatagcaggcatttagtcccgatgggtagc +f4v8s52 f4v8s52 1.000000 T 359938 ctgatattcgtattgatctactacgtatcaaagtatcccacttttgtgaccccactaagagaaga +f2v11s29 f2v8s26 0.955882 F 863655 tggactgtagaaccgtgtgcctcaatccacgcgtgaggacacgttgatttcttatacggcgtctcag +f0v6s6 f1v5s14 0.925000 F 234004 ggatcacagtctacactgctccctccaaccccggcccctggtccgaggagagggtgcttcagagtattataccactg +f1v3s12 f1v5s14 0.925000 F 234004 ggatcacagtctacactgctccctccaaccccggcccctggtccgaggagagggtgcttcagagtattataccactg +f0v0s0 f0v7s7 0.916667 F 281628 ggatcacagtctacactgctcactccaaccccggcccctgagtccgaggagagggtgcttcagagtatgtataccactg +f0v2s2 f0v7s7 0.916667 F 281628 ggatcacagtctacactgctcactccaaccccggcccctgagtccgaggagagggtgcttcagagtatgtataccactg +f1v7s16 f1v4s13 0.913580 F 304149 gaatcagcttcaaccaagcgatgcagaaccgctgctttaagcatgatgaatcgtcgtaagtgatactcgacgattc +f2v5s23 f1v4s13 0.913580 F 304149 gaatcagcttcaaccaagcgatgcagaaccgctgctttaagcatgatgaatcgtcgtaagtgatactcgacgattc +f3v12s43 f3v3s34 0.948718 F 838714 cgaaaaaggtgaatgaaagactgggacgcctcctggtttgggacgctctaggtggttgtggcttgaacgttagcacct +f8v0s68 f8v0s68 1.000000 T 507278 aagaccttgaacacctctaagcgaaaattggattcgagtcagggaacataaccacgggtgcgt +f2v1s19 f1v4s13 0.902439 F 304149 gaatcaggttcttccaagcgctgcagaaccggctactttaagcattgatgaatgcgtcagtaagtgatacccgacgattc +f1v8s17 f1v4s13 0.902439 F 304149 gaatcaggttcttccaagcgctgcagaaccggctactttaagcattgatgaatgcgtcagtaagtgatacccgacgattc +f10v11s88 f10v7s84 0.933333 F 905994 ttgccggacactcgcaatcttccaagcagaggtccagctaactcacagccaggcatttagtcccgatgggtagc +f3v5s36 f3v3s34 0.973684 F 838714 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggtgttatggcttgaaagttagcacct +f8v7s75 f8v0s68 0.968750 F 507278 aagaccttgaacacctctaagcgaaaattggattccagtcagggaaccataaccacgggtgcgt +f10v12s89 f10v7s84 0.933333 F 905994 ttgccggacactcgcaatcttccaagcagaggtccagctaacacacacgcaggcatttagacccgatgggtagc +f4v2s46 f4v8s52 0.938462 F 359938 ctgtattcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactaagagaaga +f8v2s70 f8v0s68 0.925373 F 507278 aagaccttgaacaacctctaagccgaaattggaattcgagtcagggaacataaccacgggtgcgat +f11v8s99 f11v8s99 1.000000 T 421503 agcgttagcaaagagcctggttaatctgccctcgaggatcggtggtgatcagagatgcgggcatccgtttc +f7v0s64 f7v0s64 1.000000 T 114016 gcttcgtgggaaccagagacaaccgacatatttcgaaccaccgacatacgatccttaaacc +f11v1s92 f11v8s99 0.944444 F 421503 agcgttagcaaagagcctggttcatcgccctcgaggatcggtggtgatcaagagatgcggcatccgtttc +f3v2s33 f3v3s34 0.948052 F 838714 cgaaaaaaggtaatgaaaggctgggatgctcctggtttggggcgctctaggtgttgtggcttgaacgttagcact +f8v3s71 f8v0s68 0.952381 F 507278 aagaccttgaacacctctaagcgaaattggattcgagtcagcgaacataacacgggtgcgt +f4v3s47 f4v8s52 0.923077 F 359938 ctgtattcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactaaagaaga +f10v10s87 f10v7s84 0.906667 F 905994 tctgccggacactcgcaatcttccaagcagaggtccagctaacacacgcaggcatttatccgatgggtagc +f11v0s91 f11v8s99 0.985915 F 421503 agcgttagcaaagagcctggttaatcgccctcgaggatcggtggtgatcagagatgcgggcatccgtttc +f8v5s73 f8v0s68 0.953125 F 507278 aagaccttgtacacctctaagcgataaattggattcgagtcagggaacataaccacggatgcgt +f8v4s72 f8v0s68 0.984127 F 507278 aagacttgaacacctctaagcgaaaattggattcgagtcagggaacataaccacgggtgcgt +f3v1s32 f3v3s34 0.974359 F 838714 cgaaaaagagtgaatgaaatggctgggacgctcctggtttggggcgctctaggtgttgtggcttgaacgttagcacct +f3v4s35 f3v3s34 0.948052 F 838714 cgaaaaaggtgaagaaaggctgggacgctcctggttggcgcgctctaggtgttgtggcttgaacgttagcaccta +f11v6s97 f11v8s99 0.931507 F 421503 agcgttagcaaagaacctggttaatcgccctcgaggatcggtggtggatcggagatgcgggcatcgcgtttc +f4v1s45 f4v8s52 0.910448 F 359938 ctgtatttcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactatagagaaga +f0v5s5 f1v5s14 0.925000 F 234004 ggatcacagtctacactgctcactcccccccggcccctgagtccgaggagagggtgcttcaaagtatgtataccactg +f5v5s58 f5v3s56 0.909091 F 192648 cgcaacgcaccattaagatggtaatcactaaaaaaatcggcctaacctgtccaatcgccactt +f1v0s9 f1v4s13 0.975309 F 304149 gaatcagcttctaaccaagcgatgcagaaccggctactttaagcattgatgaatgcgtcgtaagtgatactcgacgattc +f8v6s74 f8v0s68 0.969231 F 507278 aagacccttgaacacctctaagcgaaaattggattcgagtcagggaacataaccacgggtgccgt +f6v0s60 f6v0s60 1.000000 T 100983 catgtcaagagactggaaccccggtgatgtgaaaaaaggtcttgggtacgtcccatggctacg +f10v8s85 f10v7s84 0.920000 F 905994 ttgccggacactcgaatcttccaagcagaggtccagctaacacacagcaggcatactagtcccgatgggtagc +f10v1s78 f10v7s84 0.933333 F 905994 ttgccggacactcgcaatcctccaagcagaggtccagctaacacacagcaggcatttagtcccgatgaggtagc +f5v4s57 f5v3s56 0.924242 F 192648 cggcaacgcaccataagatggtaatcactaaaaaaatcggcctaacctgtccatatcgccactat +f3v10s41 f3v3s34 0.948718 F 838714 cgaaaataaggtgaatgaaagggtgggacgctcctagtttggggcgctctaggtgttgtggcttgaacgttagcacct +f3v7s38 f3v3s34 0.974026 F 838714 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggtgttcgtgcttgaacgttagcacct +f10v6s83 f10v7s84 0.932432 F 905994 ttccggacactcgcaatcttccaagcagaggtccagctaacacacagcaggcatttagtcctgatgggtagc +f11v2s93 f11v8s99 0.958333 F 421503 agcgttagcaaagagcctggttaatcgccctcggaggatcggtggtgatcagagatgcgggcatccgttc +f3v9s40 f3v3s34 0.986842 F 838714 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggggttgtggcttgaacgttagcacct +f11v4s95 f11v8s99 0.944444 F 421503 agcgttagcaaagagcctggttaatcccctcgagatcgggtggtgatcagagatgcgggcatccgtttc +f8v1s69 f8v0s68 0.984375 F 507278 aagaccttgaacacctctaagcgaaaattggatatcgagtcagggaacataaccacgggtgcgt +f11v5s96 f11v8s99 0.944444 F 421503 agcgttagcaaagagcctggttaatggccctcgaggatcggtggctgaccagagatgcgggcatccgtttc +f6v2s62 f6v0s60 0.968254 F 100983 catgtcgagagactggaaccccggtgatgtgaaaaaaggtcttgggtacgtcccatggctac +f4v6s50 f4v8s52 0.910448 F 359938 ctgtattcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactaagacgaagag +f9v0s76 f9v0s76 1.000000 T 29993 gggatcgtggcaagcaacccttgccgcctcgactgcggctaaagacgcatgttccagtaataccta +f2v12s30 f2v8s26 0.955224 F 863655 tggactagtagaccgtgtgccacaatcgacgctgaggacacgttgatttttatacggcgtctcag +f11v9s100 f11v8s99 0.972222 F 421503 agcgttagcaaaggagcctggttaatcgccctcgaggatcggtggtgatcagagatgcgggcatccgtttc +f4v5s49 f4v8s52 0.924242 F 359938 ctatattcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactaagagaagca +f2v9s27 f2v8s26 0.985075 F 863655 tggactagtagaccgtgtgcctaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f7v3s67 f7v0s64 0.983607 F 114016 gcttcgtgggaccagagacaaccgacatatttcgaaccaccgacatacgatccttaaacc +f0v8s8 f0v7s7 0.905882 F 281628 ggatcacagtcctacactgctcactccaaccccggccctgagtccgaggagagggtgcttcagagtatgtataccactg +f2v6s24 f2v8s26 0.940299 F 863655 tggactagtagacgtgtgcctcatcgacgcgtgagacactttgatttcttatacggcgtctcag +f11v3s94 f11v8s99 0.958333 F 421503 agcgttagcaaagagcctggttaatcgccgtcgaaggatcggtggtgatcagagatgcgggcatccgtttc +f6v3s63 f6v0s60 0.953846 F 100983 catgtcaagagactggaaccccggtgatgtgaaaaaaggttttgggtcacgtctccatggctacg +f3v11s42 f3v3s34 0.973684 F 838714 cgaaaaaggtgaatgaaagtctgggacgctcctggtttgggacgctctaggtgttgtggcttgaacgttagcacct +f4v4s48 f4v8s52 0.923077 F 359938 ctgtattcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactaagagaaa +f3v8s39 f3v3s34 0.948052 F 838714 cgaaaaaggtgaatgaaaggcctgggacgctcctggttggggcgctctaggtgtgtggcttgaacgttagacct +f7v1s65 f7v0s64 0.967742 F 114016 gcttcggggaaccagagacaaccgacatatttcgaaccaccgacatacgatcctttaaacc +f5v1s54 f5v3s56 0.923077 F 192648 cggaacgcaccataagatggtaatcactaaaaaaatcggcctaacctgtccatatcgccactt +f4v7s51 f4v7s51 1.000000 T 8599 cgtttattcgtaggatctactacgcatcaaagtatcccacttttgtaccccactaagagcaga +f7v2s66 f7v0s64 0.951613 F 114016 gcttcgtgggaaccagaacaaccgacatattccgaaccaccggacatacgatccttaaacc +f1v6s15 f1v4s13 0.950617 F 304149 gaatcagcttctaaccaagcgttgcagaaccggctactttaagcatgatgaatgcgtcgtaagtgatactcgacgattc +f5v2s55 f5v3s56 0.907692 F 192648 cggcatcgcaccataagatggtaatcactaaaaaaatcggcctaacctgtccatatcgccact diff --git a/pkg/obitools/obiclust/testdata/families_distance.expected b/pkg/obitools/obiclust/testdata/families_distance.expected new file mode 100644 index 0000000..1b82708 --- /dev/null +++ b/pkg/obitools/obiclust/testdata/families_distance.expected @@ -0,0 +1,101 @@ +f2v8s26 f2v8s26 0.000000 T 863655 tggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f10v3s80 f2v8s26 0.000000 F 863655 tggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f2v0s18 f2v8s26 0.000000 F 863655 tggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f2v7s25 f2v8s26 0.000000 F 863655 tggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f10v9s86 f2v8s26 0.000000 F 863655 tggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f2v3s21 f2v8s26 0.014925 F 863655 tggactagtagaccgtgtgcctcaatcgacgcggaggacacgttgatttcttatacggcgtctcag +f6v1s61 f2v8s26 0.014925 F 863655 tggactagtagaccgtgtgcctcaatcgacgcggaggacacgttgatttcttatacggcgtctcag +f2v2s20 f2v8s26 0.014925 F 863655 tggactagtagaccgtgtgcctcaatcgacgcggaggacacgttgatttcttatacggcgtctcag +f3v3s34 f3v3s34 0.000000 T 838714 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggtgttgtggcttgaacgttagcacct +f4v0s44 f3v3s34 0.000000 F 838714 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggtgttgtggcttgaacgttagcacct +f3v0s31 f3v3s34 0.000000 F 838714 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggtgttgtggcttgaacgttagcacct +f0v7s7 f0v7s7 0.000000 T 281628 ggatcacacgtctatcactgctcactccaaccccggcccttgacgtccgaggagagggtgcttctgagtatgtatacccaactg +f1v2s11 f0v7s7 0.000000 F 281628 ggatcacacgtctatcactgctcactccaaccccggcccttgacgtccgaggagagggtgcttctgagtatgtatacccaactg +f1v1s10 f0v7s7 0.000000 F 281628 ggatcacacgtctatcactgctcactccaaccccggcccttgacgtccgaggagagggtgcttctgagtatgtatacccaactg +f3v6s37 f3v3s34 0.064103 F 838714 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgtctctaggttgttgtggctgaacgtagcact +f5v0s53 f3v3s34 0.064103 F 838714 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgtctctaggttgttgtggctgaacgtagcact +f11v7s98 f3v3s34 0.064103 F 838714 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgtctctaggttgttgtggctgaacgtagcact +f2v4s22 f2v8s26 0.029412 F 863655 tggactagtagaccgtgtcgcctcaatcgacgcgtgaggacacgttgattcttatacggcgtctcag +f10v7s84 f10v7s84 0.000000 T 905994 gtgccggacactcgcaatcttcacaagcagaggtccagctaacacacagcagcatttagtcccgatgggtagc +f10v0s77 f10v7s84 0.040541 F 905994 ttgccggacactcgcaatcttccaagcagaggtccagctaacacacagcaggcatttagtcccgatgggtagc +f5v6s59 f2v8s26 0.014925 F 863655 ggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f2v10s28 f2v8s26 0.014925 F 863655 ggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f1v5s14 f1v5s14 0.000000 T 234004 ggatcacagtctacactgctcactccagaccccgcccctgagtccgaggagagggtgcttcagagtatgttaccactg +f0v4s4 f1v5s14 0.000000 F 234004 ggatcacagtctacactgctcactccagaccccgcccctgagtccgaggagagggtgcttcagagtatgttaccactg +f0v1s1 f1v5s14 0.000000 F 234004 ggatcacagtctacactgctcactccagaccccgcccctgagtccgaggagagggtgcttcagagtatgttaccactg +f0v3s3 f1v5s14 0.000000 F 234004 ggatcacagtctacactgctcactccagaccccgcccctgagtccgaggagagggtgcttcagagtatgttaccactg +f5v3s56 f5v3s56 0.000000 T 192648 ccgcaacgcaccataagatgtaatcactaagaaaaatcggcctaacctgtcatatcgccactt +f10v2s79 f10v7s84 0.080000 F 905994 ttgccggtcactcgcaatcttaccaaccagaggtccagctaacacacagcaggcatttagtcccgatgggtagc +f10v13s90 f10v7s84 0.092105 F 905994 tgccggacactcgcaatctccaagcagagagtccaagctaacacacagcaggcatttgtcccgatgggtagc +f1v4s13 f1v4s13 0.000000 T 304149 gaatcagcttctcaaccaagcgatgcagaaccggctactttaagcattgatgaatgcgtcgtaagtgatactggacgattc +f10v4s81 f10v7s84 0.066667 F 905994 ttgccggacactggcaatcttccatagcagaggtccagctaacacacagcaggcatttagtcccgatgggtagc +f10v5s82 f10v7s84 0.054054 F 905994 ttgccggacactcgcaatcttccaagcagaggtccagctaacacatagcaggcatttagtcccgatgggtagc +f4v8s52 f4v8s52 0.000000 T 359938 ctgatattcgtattgatctactacgtatcaaagtatcccacttttgtgaccccactaagagaaga +f2v11s29 f2v8s26 0.044118 F 863655 tggactgtagaaccgtgtgcctcaatccacgcgtgaggacacgttgatttcttatacggcgtctcag +f0v6s6 f1v5s14 0.075000 F 234004 ggatcacagtctacactgctccctccaaccccggcccctggtccgaggagagggtgcttcagagtattataccactg +f1v3s12 f1v5s14 0.075000 F 234004 ggatcacagtctacactgctccctccaaccccggcccctggtccgaggagagggtgcttcagagtattataccactg +f0v0s0 f0v7s7 0.083333 F 281628 ggatcacagtctacactgctcactccaaccccggcccctgagtccgaggagagggtgcttcagagtatgtataccactg +f0v2s2 f0v7s7 0.083333 F 281628 ggatcacagtctacactgctcactccaaccccggcccctgagtccgaggagagggtgcttcagagtatgtataccactg +f1v7s16 f1v4s13 0.086420 F 304149 gaatcagcttcaaccaagcgatgcagaaccgctgctttaagcatgatgaatcgtcgtaagtgatactcgacgattc +f2v5s23 f1v4s13 0.086420 F 304149 gaatcagcttcaaccaagcgatgcagaaccgctgctttaagcatgatgaatcgtcgtaagtgatactcgacgattc +f3v12s43 f3v3s34 0.051282 F 838714 cgaaaaaggtgaatgaaagactgggacgcctcctggtttgggacgctctaggtggttgtggcttgaacgttagcacct +f8v0s68 f8v0s68 0.000000 T 507278 aagaccttgaacacctctaagcgaaaattggattcgagtcagggaacataaccacgggtgcgt +f2v1s19 f1v4s13 0.097561 F 304149 gaatcaggttcttccaagcgctgcagaaccggctactttaagcattgatgaatgcgtcagtaagtgatacccgacgattc +f1v8s17 f1v4s13 0.097561 F 304149 gaatcaggttcttccaagcgctgcagaaccggctactttaagcattgatgaatgcgtcagtaagtgatacccgacgattc +f10v11s88 f10v7s84 0.066667 F 905994 ttgccggacactcgcaatcttccaagcagaggtccagctaactcacagccaggcatttagtcccgatgggtagc +f3v5s36 f3v3s34 0.026316 F 838714 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggtgttatggcttgaaagttagcacct +f8v7s75 f8v0s68 0.031250 F 507278 aagaccttgaacacctctaagcgaaaattggattccagtcagggaaccataaccacgggtgcgt +f10v12s89 f10v7s84 0.066667 F 905994 ttgccggacactcgcaatcttccaagcagaggtccagctaacacacacgcaggcatttagacccgatgggtagc +f4v2s46 f4v8s52 0.061538 F 359938 ctgtattcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactaagagaaga +f8v2s70 f8v0s68 0.074627 F 507278 aagaccttgaacaacctctaagccgaaattggaattcgagtcagggaacataaccacgggtgcgat +f11v8s99 f11v8s99 0.000000 T 421503 agcgttagcaaagagcctggttaatctgccctcgaggatcggtggtgatcagagatgcgggcatccgtttc +f7v0s64 f7v0s64 0.000000 T 114016 gcttcgtgggaaccagagacaaccgacatatttcgaaccaccgacatacgatccttaaacc +f11v1s92 f11v8s99 0.055556 F 421503 agcgttagcaaagagcctggttcatcgccctcgaggatcggtggtgatcaagagatgcggcatccgtttc +f3v2s33 f3v3s34 0.051948 F 838714 cgaaaaaaggtaatgaaaggctgggatgctcctggtttggggcgctctaggtgttgtggcttgaacgttagcact +f8v3s71 f8v0s68 0.047619 F 507278 aagaccttgaacacctctaagcgaaattggattcgagtcagcgaacataacacgggtgcgt +f4v3s47 f4v8s52 0.076923 F 359938 ctgtattcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactaaagaaga +f10v10s87 f10v7s84 0.093333 F 905994 tctgccggacactcgcaatcttccaagcagaggtccagctaacacacgcaggcatttatccgatgggtagc +f11v0s91 f11v8s99 0.014085 F 421503 agcgttagcaaagagcctggttaatcgccctcgaggatcggtggtgatcagagatgcgggcatccgtttc +f8v5s73 f8v0s68 0.046875 F 507278 aagaccttgtacacctctaagcgataaattggattcgagtcagggaacataaccacggatgcgt +f8v4s72 f8v0s68 0.015873 F 507278 aagacttgaacacctctaagcgaaaattggattcgagtcagggaacataaccacgggtgcgt +f3v1s32 f3v3s34 0.025641 F 838714 cgaaaaagagtgaatgaaatggctgggacgctcctggtttggggcgctctaggtgttgtggcttgaacgttagcacct +f3v4s35 f3v3s34 0.051948 F 838714 cgaaaaaggtgaagaaaggctgggacgctcctggttggcgcgctctaggtgttgtggcttgaacgttagcaccta +f11v6s97 f11v8s99 0.068493 F 421503 agcgttagcaaagaacctggttaatcgccctcgaggatcggtggtggatcggagatgcgggcatcgcgtttc +f4v1s45 f4v8s52 0.089552 F 359938 ctgtatttcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactatagagaaga +f0v5s5 f1v5s14 0.075000 F 234004 ggatcacagtctacactgctcactcccccccggcccctgagtccgaggagagggtgcttcaaagtatgtataccactg +f5v5s58 f5v3s56 0.090909 F 192648 cgcaacgcaccattaagatggtaatcactaaaaaaatcggcctaacctgtccaatcgccactt +f1v0s9 f1v4s13 0.024691 F 304149 gaatcagcttctaaccaagcgatgcagaaccggctactttaagcattgatgaatgcgtcgtaagtgatactcgacgattc +f8v6s74 f8v0s68 0.030769 F 507278 aagacccttgaacacctctaagcgaaaattggattcgagtcagggaacataaccacgggtgccgt +f6v0s60 f6v0s60 0.000000 T 100983 catgtcaagagactggaaccccggtgatgtgaaaaaaggtcttgggtacgtcccatggctacg +f10v8s85 f10v7s84 0.080000 F 905994 ttgccggacactcgaatcttccaagcagaggtccagctaacacacagcaggcatactagtcccgatgggtagc +f10v1s78 f10v7s84 0.066667 F 905994 ttgccggacactcgcaatcctccaagcagaggtccagctaacacacagcaggcatttagtcccgatgaggtagc +f5v4s57 f5v3s56 0.075758 F 192648 cggcaacgcaccataagatggtaatcactaaaaaaatcggcctaacctgtccatatcgccactat +f3v10s41 f3v3s34 0.051282 F 838714 cgaaaataaggtgaatgaaagggtgggacgctcctagtttggggcgctctaggtgttgtggcttgaacgttagcacct +f3v7s38 f3v3s34 0.025974 F 838714 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggtgttcgtgcttgaacgttagcacct +f10v6s83 f10v7s84 0.067568 F 905994 ttccggacactcgcaatcttccaagcagaggtccagctaacacacagcaggcatttagtcctgatgggtagc +f11v2s93 f11v8s99 0.041667 F 421503 agcgttagcaaagagcctggttaatcgccctcggaggatcggtggtgatcagagatgcgggcatccgttc +f3v9s40 f3v3s34 0.013158 F 838714 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggggttgtggcttgaacgttagcacct +f11v4s95 f11v8s99 0.055556 F 421503 agcgttagcaaagagcctggttaatcccctcgagatcgggtggtgatcagagatgcgggcatccgtttc +f8v1s69 f8v0s68 0.015625 F 507278 aagaccttgaacacctctaagcgaaaattggatatcgagtcagggaacataaccacgggtgcgt +f11v5s96 f11v8s99 0.055556 F 421503 agcgttagcaaagagcctggttaatggccctcgaggatcggtggctgaccagagatgcgggcatccgtttc +f6v2s62 f6v0s60 0.031746 F 100983 catgtcgagagactggaaccccggtgatgtgaaaaaaggtcttgggtacgtcccatggctac +f4v6s50 f4v8s52 0.089552 F 359938 ctgtattcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactaagacgaagag +f9v0s76 f9v0s76 0.000000 T 29993 gggatcgtggcaagcaacccttgccgcctcgactgcggctaaagacgcatgttccagtaataccta +f2v12s30 f2v8s26 0.044776 F 863655 tggactagtagaccgtgtgccacaatcgacgctgaggacacgttgatttttatacggcgtctcag +f11v9s100 f11v8s99 0.027778 F 421503 agcgttagcaaaggagcctggttaatcgccctcgaggatcggtggtgatcagagatgcgggcatccgtttc +f4v5s49 f4v8s52 0.075758 F 359938 ctatattcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactaagagaagca +f2v9s27 f2v8s26 0.014925 F 863655 tggactagtagaccgtgtgcctaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f7v3s67 f7v0s64 0.016393 F 114016 gcttcgtgggaccagagacaaccgacatatttcgaaccaccgacatacgatccttaaacc +f0v8s8 f0v7s7 0.094118 F 281628 ggatcacagtcctacactgctcactccaaccccggccctgagtccgaggagagggtgcttcagagtatgtataccactg +f2v6s24 f2v8s26 0.059701 F 863655 tggactagtagacgtgtgcctcatcgacgcgtgagacactttgatttcttatacggcgtctcag +f11v3s94 f11v8s99 0.041667 F 421503 agcgttagcaaagagcctggttaatcgccgtcgaaggatcggtggtgatcagagatgcgggcatccgtttc +f6v3s63 f6v0s60 0.046154 F 100983 catgtcaagagactggaaccccggtgatgtgaaaaaaggttttgggtcacgtctccatggctacg +f3v11s42 f3v3s34 0.026316 F 838714 cgaaaaaggtgaatgaaagtctgggacgctcctggtttgggacgctctaggtgttgtggcttgaacgttagcacct +f4v4s48 f4v8s52 0.076923 F 359938 ctgtattcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactaagagaaa +f3v8s39 f3v3s34 0.051948 F 838714 cgaaaaaggtgaatgaaaggcctgggacgctcctggttggggcgctctaggtgtgtggcttgaacgttagacct +f7v1s65 f7v0s64 0.032258 F 114016 gcttcggggaaccagagacaaccgacatatttcgaaccaccgacatacgatcctttaaacc +f5v1s54 f5v3s56 0.076923 F 192648 cggaacgcaccataagatggtaatcactaaaaaaatcggcctaacctgtccatatcgccactt +f4v7s51 f4v7s51 0.000000 T 8599 cgtttattcgtaggatctactacgcatcaaagtatcccacttttgtaccccactaagagcaga +f7v2s66 f7v0s64 0.048387 F 114016 gcttcgtgggaaccagaacaaccgacatattccgaaccaccggacatacgatccttaaacc +f1v6s15 f1v4s13 0.049383 F 304149 gaatcagcttctaaccaagcgttgcagaaccggctactttaagcatgatgaatgcgtcgtaagtgatactcgacgattc +f5v2s55 f5v3s56 0.092308 F 192648 cggcatcgcaccataagatggtaatcactaaaaaaatcggcctaacctgtccatatcgccact diff --git a/pkg/obitools/obiclust/testdata/families_exact.expected b/pkg/obitools/obiclust/testdata/families_exact.expected new file mode 100644 index 0000000..e8c2b06 --- /dev/null +++ b/pkg/obitools/obiclust/testdata/families_exact.expected @@ -0,0 +1,101 @@ +f2v8s26 f2v8s26 1.000000 T 863655 tggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f10v3s80 f2v8s26 1.000000 F 863655 tggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f2v0s18 f2v8s26 1.000000 F 863655 tggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f2v7s25 f2v8s26 1.000000 F 863655 tggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f10v9s86 f2v8s26 1.000000 F 863655 tggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f2v3s21 f2v8s26 0.985075 F 863655 tggactagtagaccgtgtgcctcaatcgacgcggaggacacgttgatttcttatacggcgtctcag +f6v1s61 f2v8s26 0.985075 F 863655 tggactagtagaccgtgtgcctcaatcgacgcggaggacacgttgatttcttatacggcgtctcag +f2v2s20 f2v8s26 0.985075 F 863655 tggactagtagaccgtgtgcctcaatcgacgcggaggacacgttgatttcttatacggcgtctcag +f3v3s34 f3v3s34 1.000000 T 838714 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggtgttgtggcttgaacgttagcacct +f4v0s44 f3v3s34 1.000000 F 838714 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggtgttgtggcttgaacgttagcacct +f3v0s31 f3v3s34 1.000000 F 838714 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggtgttgtggcttgaacgttagcacct +f0v7s7 f0v7s7 1.000000 T 173457 ggatcacacgtctatcactgctcactccaaccccggcccttgacgtccgaggagagggtgcttctgagtatgtatacccaactg +f1v2s11 f0v7s7 1.000000 F 173457 ggatcacacgtctatcactgctcactccaaccccggcccttgacgtccgaggagagggtgcttctgagtatgtatacccaactg +f1v1s10 f0v7s7 1.000000 F 173457 ggatcacacgtctatcactgctcactccaaccccggcccttgacgtccgaggagagggtgcttctgagtatgtatacccaactg +f3v6s37 f3v3s34 0.935897 F 838714 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgtctctaggttgttgtggctgaacgtagcact +f5v0s53 f3v3s34 0.935897 F 838714 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgtctctaggttgttgtggctgaacgtagcact +f11v7s98 f3v3s34 0.935897 F 838714 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgtctctaggttgttgtggctgaacgtagcact +f2v4s22 f2v8s26 0.970588 F 863655 tggactagtagaccgtgtcgcctcaatcgacgcgtgaggacacgttgattcttatacggcgtctcag +f10v7s84 f10v7s84 1.000000 T 905994 gtgccggacactcgcaatcttcacaagcagaggtccagctaacacacagcagcatttagtcccgatgggtagc +f10v0s77 f10v7s84 0.959459 F 905994 ttgccggacactcgcaatcttccaagcagaggtccagctaacacacagcaggcatttagtcccgatgggtagc +f5v6s59 f2v8s26 0.985075 F 863655 ggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f2v10s28 f2v8s26 0.985075 F 863655 ggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f1v5s14 f1v5s14 1.000000 T 342175 ggatcacagtctacactgctcactccagaccccgcccctgagtccgaggagagggtgcttcagagtatgttaccactg +f0v4s4 f1v5s14 1.000000 F 342175 ggatcacagtctacactgctcactccagaccccgcccctgagtccgaggagagggtgcttcagagtatgttaccactg +f0v1s1 f1v5s14 1.000000 F 342175 ggatcacagtctacactgctcactccagaccccgcccctgagtccgaggagagggtgcttcagagtatgttaccactg +f0v3s3 f1v5s14 1.000000 F 342175 ggatcacagtctacactgctcactccagaccccgcccctgagtccgaggagagggtgcttcagagtatgttaccactg +f5v3s56 f5v3s56 1.000000 T 192648 ccgcaacgcaccataagatgtaatcactaagaaaaatcggcctaacctgtcatatcgccactt +f10v2s79 f10v7s84 0.920000 F 905994 ttgccggtcactcgcaatcttaccaaccagaggtccagctaacacacagcaggcatttagtcccgatgggtagc +f10v13s90 f10v7s84 0.907895 F 905994 tgccggacactcgcaatctccaagcagagagtccaagctaacacacagcaggcatttgtcccgatgggtagc +f1v4s13 f1v4s13 1.000000 T 304149 gaatcagcttctcaaccaagcgatgcagaaccggctactttaagcattgatgaatgcgtcgtaagtgatactggacgattc +f10v4s81 f10v7s84 0.933333 F 905994 ttgccggacactggcaatcttccatagcagaggtccagctaacacacagcaggcatttagtcccgatgggtagc +f10v5s82 f10v7s84 0.945946 F 905994 ttgccggacactcgcaatcttccaagcagaggtccagctaacacatagcaggcatttagtcccgatgggtagc +f4v8s52 f4v8s52 1.000000 T 359938 ctgatattcgtattgatctactacgtatcaaagtatcccacttttgtgaccccactaagagaaga +f2v11s29 f2v8s26 0.955882 F 863655 tggactgtagaaccgtgtgcctcaatccacgcgtgaggacacgttgatttcttatacggcgtctcag +f0v6s6 f1v5s14 0.925000 F 342175 ggatcacagtctacactgctccctccaaccccggcccctggtccgaggagagggtgcttcagagtattataccactg +f1v3s12 f1v5s14 0.925000 F 342175 ggatcacagtctacactgctccctccaaccccggcccctggtccgaggagagggtgcttcagagtattataccactg +f0v0s0 f1v5s14 0.962500 F 342175 ggatcacagtctacactgctcactccaaccccggcccctgagtccgaggagagggtgcttcagagtatgtataccactg +f0v2s2 f1v5s14 0.962500 F 342175 ggatcacagtctacactgctcactccaaccccggcccctgagtccgaggagagggtgcttcagagtatgtataccactg +f1v7s16 f1v4s13 0.913580 F 304149 gaatcagcttcaaccaagcgatgcagaaccgctgctttaagcatgatgaatcgtcgtaagtgatactcgacgattc +f2v5s23 f1v4s13 0.913580 F 304149 gaatcagcttcaaccaagcgatgcagaaccgctgctttaagcatgatgaatcgtcgtaagtgatactcgacgattc +f3v12s43 f3v3s34 0.948718 F 838714 cgaaaaaggtgaatgaaagactgggacgcctcctggtttgggacgctctaggtggttgtggcttgaacgttagcacct +f8v0s68 f8v0s68 1.000000 T 507278 aagaccttgaacacctctaagcgaaaattggattcgagtcagggaacataaccacgggtgcgt +f2v1s19 f1v4s13 0.902439 F 304149 gaatcaggttcttccaagcgctgcagaaccggctactttaagcattgatgaatgcgtcagtaagtgatacccgacgattc +f1v8s17 f1v4s13 0.902439 F 304149 gaatcaggttcttccaagcgctgcagaaccggctactttaagcattgatgaatgcgtcagtaagtgatacccgacgattc +f10v11s88 f10v7s84 0.933333 F 905994 ttgccggacactcgcaatcttccaagcagaggtccagctaactcacagccaggcatttagtcccgatgggtagc +f3v5s36 f3v3s34 0.973684 F 838714 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggtgttatggcttgaaagttagcacct +f8v7s75 f8v0s68 0.968750 F 507278 aagaccttgaacacctctaagcgaaaattggattccagtcagggaaccataaccacgggtgcgt +f10v12s89 f10v7s84 0.933333 F 905994 ttgccggacactcgcaatcttccaagcagaggtccagctaacacacacgcaggcatttagacccgatgggtagc +f4v2s46 f4v8s52 0.938462 F 359938 ctgtattcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactaagagaaga +f8v2s70 f8v0s68 0.925373 F 507278 aagaccttgaacaacctctaagccgaaattggaattcgagtcagggaacataaccacgggtgcgat +f11v8s99 f11v8s99 1.000000 T 421503 agcgttagcaaagagcctggttaatctgccctcgaggatcggtggtgatcagagatgcgggcatccgtttc +f7v0s64 f7v0s64 1.000000 T 114016 gcttcgtgggaaccagagacaaccgacatatttcgaaccaccgacatacgatccttaaacc +f11v1s92 f11v8s99 0.944444 F 421503 agcgttagcaaagagcctggttcatcgccctcgaggatcggtggtgatcaagagatgcggcatccgtttc +f3v2s33 f3v3s34 0.948052 F 838714 cgaaaaaaggtaatgaaaggctgggatgctcctggtttggggcgctctaggtgttgtggcttgaacgttagcact +f8v3s71 f8v0s68 0.952381 F 507278 aagaccttgaacacctctaagcgaaattggattcgagtcagcgaacataacacgggtgcgt +f4v3s47 f4v8s52 0.923077 F 359938 ctgtattcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactaaagaaga +f10v10s87 f10v7s84 0.906667 F 905994 tctgccggacactcgcaatcttccaagcagaggtccagctaacacacgcaggcatttatccgatgggtagc +f11v0s91 f11v8s99 0.985915 F 421503 agcgttagcaaagagcctggttaatcgccctcgaggatcggtggtgatcagagatgcgggcatccgtttc +f8v5s73 f8v0s68 0.953125 F 507278 aagaccttgtacacctctaagcgataaattggattcgagtcagggaacataaccacggatgcgt +f8v4s72 f8v0s68 0.984127 F 507278 aagacttgaacacctctaagcgaaaattggattcgagtcagggaacataaccacgggtgcgt +f3v1s32 f3v3s34 0.974359 F 838714 cgaaaaagagtgaatgaaatggctgggacgctcctggtttggggcgctctaggtgttgtggcttgaacgttagcacct +f3v4s35 f3v3s34 0.948052 F 838714 cgaaaaaggtgaagaaaggctgggacgctcctggttggcgcgctctaggtgttgtggcttgaacgttagcaccta +f11v6s97 f11v8s99 0.931507 F 421503 agcgttagcaaagaacctggttaatcgccctcgaggatcggtggtggatcggagatgcgggcatcgcgtttc +f4v1s45 f4v8s52 0.910448 F 359938 ctgtatttcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactatagagaaga +f0v5s5 f1v5s14 0.925000 F 342175 ggatcacagtctacactgctcactcccccccggcccctgagtccgaggagagggtgcttcaaagtatgtataccactg +f5v5s58 f5v3s56 0.909091 F 192648 cgcaacgcaccattaagatggtaatcactaaaaaaatcggcctaacctgtccaatcgccactt +f1v0s9 f1v4s13 0.975309 F 304149 gaatcagcttctaaccaagcgatgcagaaccggctactttaagcattgatgaatgcgtcgtaagtgatactcgacgattc +f8v6s74 f8v0s68 0.969231 F 507278 aagacccttgaacacctctaagcgaaaattggattcgagtcagggaacataaccacgggtgccgt +f6v0s60 f6v0s60 1.000000 T 100983 catgtcaagagactggaaccccggtgatgtgaaaaaaggtcttgggtacgtcccatggctacg +f10v8s85 f10v7s84 0.920000 F 905994 ttgccggacactcgaatcttccaagcagaggtccagctaacacacagcaggcatactagtcccgatgggtagc +f10v1s78 f10v7s84 0.933333 F 905994 ttgccggacactcgcaatcctccaagcagaggtccagctaacacacagcaggcatttagtcccgatgaggtagc +f5v4s57 f5v3s56 0.924242 F 192648 cggcaacgcaccataagatggtaatcactaaaaaaatcggcctaacctgtccatatcgccactat +f3v10s41 f3v3s34 0.948718 F 838714 cgaaaataaggtgaatgaaagggtgggacgctcctagtttggggcgctctaggtgttgtggcttgaacgttagcacct +f3v7s38 f3v3s34 0.974026 F 838714 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggtgttcgtgcttgaacgttagcacct +f10v6s83 f10v7s84 0.932432 F 905994 ttccggacactcgcaatcttccaagcagaggtccagctaacacacagcaggcatttagtcctgatgggtagc +f11v2s93 f11v8s99 0.958333 F 421503 agcgttagcaaagagcctggttaatcgccctcggaggatcggtggtgatcagagatgcgggcatccgttc +f3v9s40 f3v3s34 0.986842 F 838714 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggggttgtggcttgaacgttagcacct +f11v4s95 f11v8s99 0.944444 F 421503 agcgttagcaaagagcctggttaatcccctcgagatcgggtggtgatcagagatgcgggcatccgtttc +f8v1s69 f8v0s68 0.984375 F 507278 aagaccttgaacacctctaagcgaaaattggatatcgagtcagggaacataaccacgggtgcgt +f11v5s96 f11v8s99 0.944444 F 421503 agcgttagcaaagagcctggttaatggccctcgaggatcggtggctgaccagagatgcgggcatccgtttc +f6v2s62 f6v0s60 0.968254 F 100983 catgtcgagagactggaaccccggtgatgtgaaaaaaggtcttgggtacgtcccatggctac +f4v6s50 f4v8s52 0.910448 F 359938 ctgtattcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactaagacgaagag +f9v0s76 f9v0s76 1.000000 T 29993 gggatcgtggcaagcaacccttgccgcctcgactgcggctaaagacgcatgttccagtaataccta +f2v12s30 f2v8s26 0.955224 F 863655 tggactagtagaccgtgtgccacaatcgacgctgaggacacgttgatttttatacggcgtctcag +f11v9s100 f11v8s99 0.972222 F 421503 agcgttagcaaaggagcctggttaatcgccctcgaggatcggtggtgatcagagatgcgggcatccgtttc +f4v5s49 f4v8s52 0.924242 F 359938 ctatattcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactaagagaagca +f2v9s27 f2v8s26 0.985075 F 863655 tggactagtagaccgtgtgcctaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f7v3s67 f7v0s64 0.983607 F 114016 gcttcgtgggaccagagacaaccgacatatttcgaaccaccgacatacgatccttaaacc +f0v8s8 f1v5s14 0.950000 F 342175 ggatcacagtcctacactgctcactccaaccccggccctgagtccgaggagagggtgcttcagagtatgtataccactg +f2v6s24 f2v8s26 0.940299 F 863655 tggactagtagacgtgtgcctcatcgacgcgtgagacactttgatttcttatacggcgtctcag +f11v3s94 f11v8s99 0.958333 F 421503 agcgttagcaaagagcctggttaatcgccgtcgaaggatcggtggtgatcagagatgcgggcatccgtttc +f6v3s63 f6v0s60 0.953846 F 100983 catgtcaagagactggaaccccggtgatgtgaaaaaaggttttgggtcacgtctccatggctacg +f3v11s42 f3v3s34 0.973684 F 838714 cgaaaaaggtgaatgaaagtctgggacgctcctggtttgggacgctctaggtgttgtggcttgaacgttagcacct +f4v4s48 f4v8s52 0.923077 F 359938 ctgtattcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactaagagaaa +f3v8s39 f3v3s34 0.948052 F 838714 cgaaaaaggtgaatgaaaggcctgggacgctcctggttggggcgctctaggtgtgtggcttgaacgttagacct +f7v1s65 f7v0s64 0.967742 F 114016 gcttcggggaaccagagacaaccgacatatttcgaaccaccgacatacgatcctttaaacc +f5v1s54 f5v3s56 0.923077 F 192648 cggaacgcaccataagatggtaatcactaaaaaaatcggcctaacctgtccatatcgccactt +f4v7s51 f4v7s51 1.000000 T 8599 cgtttattcgtaggatctactacgcatcaaagtatcccacttttgtaccccactaagagcaga +f7v2s66 f7v0s64 0.951613 F 114016 gcttcgtgggaaccagaacaaccgacatattccgaaccaccggacatacgatccttaaacc +f1v6s15 f1v4s13 0.950617 F 304149 gaatcagcttctaaccaagcgttgcagaaccggctactttaagcatgatgaatgcgtcgtaagtgatactcgacgattc +f5v2s55 f5v3s56 0.907692 F 192648 cggcatcgcaccataagatggtaatcactaaaaaaatcggcctaacctgtccatatcgccact diff --git a/pkg/obitools/obiclust/testdata/families_longest.expected b/pkg/obitools/obiclust/testdata/families_longest.expected new file mode 100644 index 0000000..f209e8e --- /dev/null +++ b/pkg/obitools/obiclust/testdata/families_longest.expected @@ -0,0 +1,101 @@ +f2v8s26 f2v8s26 1.000000 T 863655 tggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f10v3s80 f2v8s26 1.000000 F 863655 tggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f2v0s18 f2v8s26 1.000000 F 863655 tggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f2v7s25 f2v8s26 1.000000 F 863655 tggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f10v9s86 f2v8s26 1.000000 F 863655 tggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f2v3s21 f2v8s26 0.985075 F 863655 tggactagtagaccgtgtgcctcaatcgacgcggaggacacgttgatttcttatacggcgtctcag +f6v1s61 f2v8s26 0.985075 F 863655 tggactagtagaccgtgtgcctcaatcgacgcggaggacacgttgatttcttatacggcgtctcag +f2v2s20 f2v8s26 0.985075 F 863655 tggactagtagaccgtgtgcctcaatcgacgcggaggacacgttgatttcttatacggcgtctcag +f3v3s34 f3v3s34 1.000000 T 838714 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggtgttgtggcttgaacgttagcacct +f4v0s44 f3v3s34 1.000000 F 838714 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggtgttgtggcttgaacgttagcacct +f3v0s31 f3v3s34 1.000000 F 838714 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggtgttgtggcttgaacgttagcacct +f0v7s7 f0v7s7 1.000000 T 515632 ggatcacacgtctatcactgctcactccaaccccggcccttgacgtccgaggagagggtgcttctgagtatgtatacccaactg +f1v2s11 f0v7s7 1.000000 F 515632 ggatcacacgtctatcactgctcactccaaccccggcccttgacgtccgaggagagggtgcttctgagtatgtatacccaactg +f1v1s10 f0v7s7 1.000000 F 515632 ggatcacacgtctatcactgctcactccaaccccggcccttgacgtccgaggagagggtgcttctgagtatgtatacccaactg +f3v6s37 f3v3s34 0.960526 F 838714 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgtctctaggttgttgtggctgaacgtagcact +f5v0s53 f3v3s34 0.960526 F 838714 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgtctctaggttgttgtggctgaacgtagcact +f11v7s98 f3v3s34 0.960526 F 838714 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgtctctaggttgttgtggctgaacgtagcact +f2v4s22 f2v8s26 0.985075 F 863655 tggactagtagaccgtgtcgcctcaatcgacgcgtgaggacacgttgattcttatacggcgtctcag +f10v7s84 f10v7s84 1.000000 T 905994 gtgccggacactcgcaatcttcacaagcagaggtccagctaacacacagcagcatttagtcccgatgggtagc +f10v0s77 f10v7s84 0.972603 F 905994 ttgccggacactcgcaatcttccaagcagaggtccagctaacacacagcaggcatttagtcccgatgggtagc +f5v6s59 f2v8s26 0.985075 F 863655 ggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f2v10s28 f2v8s26 0.985075 F 863655 ggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f1v5s14 f0v7s7 0.892857 F 515632 ggatcacagtctacactgctcactccagaccccgcccctgagtccgaggagagggtgcttcagagtatgttaccactg +f0v4s4 f0v7s7 0.892857 F 515632 ggatcacagtctacactgctcactccagaccccgcccctgagtccgaggagagggtgcttcagagtatgttaccactg +f0v1s1 f0v7s7 0.892857 F 515632 ggatcacagtctacactgctcactccagaccccgcccctgagtccgaggagagggtgcttcagagtatgttaccactg +f0v3s3 f0v7s7 0.892857 F 515632 ggatcacagtctacactgctcactccagaccccgcccctgagtccgaggagagggtgcttcagagtatgttaccactg +f5v3s56 f5v3s56 1.000000 T 192648 ccgcaacgcaccataagatgtaatcactaagaaaaatcggcctaacctgtcatatcgccactt +f10v2s79 f10v7s84 0.932432 F 905994 ttgccggtcactcgcaatcttaccaaccagaggtccagctaacacacagcaggcatttagtcccgatgggtagc +f10v13s90 f10v7s84 0.945205 F 905994 tgccggacactcgcaatctccaagcagagagtccaagctaacacacagcaggcatttgtcccgatgggtagc +f1v4s13 f1v4s13 1.000000 T 304149 gaatcagcttctcaaccaagcgatgcagaaccggctactttaagcattgatgaatgcgtcgtaagtgatactggacgattc +f10v4s81 f10v7s84 0.945946 F 905994 ttgccggacactggcaatcttccatagcagaggtccagctaacacacagcaggcatttagtcccgatgggtagc +f10v5s82 f10v7s84 0.958904 F 905994 ttgccggacactcgcaatcttccaagcagaggtccagctaacacatagcaggcatttagtcccgatgggtagc +f4v8s52 f4v8s52 1.000000 T 368537 ctgatattcgtattgatctactacgtatcaaagtatcccacttttgtgaccccactaagagaaga +f2v11s29 f2v8s26 0.970149 F 863655 tggactgtagaaccgtgtgcctcaatccacgcgtgaggacacgttgatttcttatacggcgtctcag +f0v6s6 f0v7s7 0.880952 F 515632 ggatcacagtctacactgctccctccaaccccggcccctggtccgaggagagggtgcttcagagtattataccactg +f1v3s12 f0v7s7 0.880952 F 515632 ggatcacagtctacactgctccctccaaccccggcccctggtccgaggagagggtgcttcagagtattataccactg +f0v0s0 f0v7s7 0.916667 F 515632 ggatcacagtctacactgctcactccaaccccggcccctgagtccgaggagagggtgcttcagagtatgtataccactg +f0v2s2 f0v7s7 0.916667 F 515632 ggatcacagtctacactgctcactccaaccccggcccctgagtccgaggagagggtgcttcagagtatgtataccactg +f1v7s16 f1v4s13 0.913580 F 304149 gaatcagcttcaaccaagcgatgcagaaccgctgctttaagcatgatgaatcgtcgtaagtgatactcgacgattc +f2v5s23 f1v4s13 0.913580 F 304149 gaatcagcttcaaccaagcgatgcagaaccgctgctttaagcatgatgaatcgtcgtaagtgatactcgacgattc +f3v12s43 f3v3s34 0.948718 F 838714 cgaaaaaggtgaatgaaagactgggacgcctcctggtttgggacgctctaggtggttgtggcttgaacgttagcacct +f8v0s68 f8v0s68 1.000000 T 507278 aagaccttgaacacctctaagcgaaaattggattcgagtcagggaacataaccacgggtgcgt +f2v1s19 f1v4s13 0.913580 F 304149 gaatcaggttcttccaagcgctgcagaaccggctactttaagcattgatgaatgcgtcagtaagtgatacccgacgattc +f1v8s17 f1v4s13 0.913580 F 304149 gaatcaggttcttccaagcgctgcagaaccggctactttaagcattgatgaatgcgtcagtaagtgatacccgacgattc +f10v11s88 f10v7s84 0.945946 F 905994 ttgccggacactcgcaatcttccaagcagaggtccagctaactcacagccaggcatttagtcccgatgggtagc +f3v5s36 f3v3s34 0.973684 F 838714 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggtgttatggcttgaaagttagcacct +f8v7s75 f8v0s68 0.968750 F 507278 aagaccttgaacacctctaagcgaaaattggattccagtcagggaaccataaccacgggtgcgt +f10v12s89 f10v7s84 0.945946 F 905994 ttgccggacactcgcaatcttccaagcagaggtccagctaacacacacgcaggcatttagacccgatgggtagc +f4v2s46 f4v8s52 0.938462 F 368537 ctgtattcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactaagagaaga +f8v2s70 f8v0s68 0.939394 F 507278 aagaccttgaacaacctctaagccgaaattggaattcgagtcagggaacataaccacgggtgcgat +f11v8s99 f11v8s99 1.000000 T 421503 agcgttagcaaagagcctggttaatctgccctcgaggatcggtggtgatcagagatgcgggcatccgtttc +f7v0s64 f7v0s64 1.000000 T 114016 gcttcgtgggaaccagagacaaccgacatatttcgaaccaccgacatacgatccttaaacc +f11v1s92 f11v8s99 0.957746 F 421503 agcgttagcaaagagcctggttcatcgccctcgaggatcggtggtgatcaagagatgcggcatccgtttc +f3v2s33 f3v3s34 0.960526 F 838714 cgaaaaaaggtaatgaaaggctgggatgctcctggtttggggcgctctaggtgttgtggcttgaacgttagcact +f8v3s71 f8v0s68 0.952381 F 507278 aagaccttgaacacctctaagcgaaattggattcgagtcagcgaacataacacgggtgcgt +f4v3s47 f4v8s52 0.923077 F 368537 ctgtattcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactaaagaaga +f10v10s87 f10v7s84 0.931507 F 905994 tctgccggacactcgcaatcttccaagcagaggtccagctaacacacgcaggcatttatccgatgggtagc +f11v0s91 f11v8s99 0.985915 F 421503 agcgttagcaaagagcctggttaatcgccctcgaggatcggtggtgatcagagatgcgggcatccgtttc +f8v5s73 f8v0s68 0.953125 F 507278 aagaccttgtacacctctaagcgataaattggattcgagtcagggaacataaccacggatgcgt +f8v4s72 f8v0s68 0.984127 F 507278 aagacttgaacacctctaagcgaaaattggattcgagtcagggaacataaccacgggtgcgt +f3v1s32 f3v3s34 0.974359 F 838714 cgaaaaagagtgaatgaaatggctgggacgctcctggtttggggcgctctaggtgttgtggcttgaacgttagcacct +f3v4s35 f3v3s34 0.960526 F 838714 cgaaaaaggtgaagaaaggctgggacgctcctggttggcgcgctctaggtgttgtggcttgaacgttagcaccta +f11v6s97 f11v8s99 0.944444 F 421503 agcgttagcaaagaacctggttaatcgccctcgaggatcggtggtggatcggagatgcgggcatcgcgtttc +f4v1s45 f4v8s52 0.938462 F 368537 ctgtatttcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactatagagaaga +f0v5s5 f0v7s7 0.880952 F 515632 ggatcacagtctacactgctcactcccccccggcccctgagtccgaggagagggtgcttcaaagtatgtataccactg +f5v5s58 f5v3s56 0.952381 F 192648 cgcaacgcaccattaagatggtaatcactaaaaaaatcggcctaacctgtccaatcgccactt +f1v0s9 f1v4s13 0.975309 F 304149 gaatcagcttctaaccaagcgatgcagaaccggctactttaagcattgatgaatgcgtcgtaagtgatactcgacgattc +f8v6s74 f8v0s68 0.969231 F 507278 aagacccttgaacacctctaagcgaaaattggattcgagtcagggaacataaccacgggtgccgt +f6v0s60 f6v0s60 1.000000 T 100983 catgtcaagagactggaaccccggtgatgtgaaaaaaggtcttgggtacgtcccatggctacg +f10v8s85 f10v7s84 0.945205 F 905994 ttgccggacactcgaatcttccaagcagaggtccagctaacacacagcaggcatactagtcccgatgggtagc +f10v1s78 f10v7s84 0.945946 F 905994 ttgccggacactcgcaatcctccaagcagaggtccagctaacacacagcaggcatttagtcccgatgaggtagc +f5v4s57 f5v3s56 0.938462 F 192648 cggcaacgcaccataagatggtaatcactaaaaaaatcggcctaacctgtccatatcgccactat +f3v10s41 f3v3s34 0.948718 F 838714 cgaaaataaggtgaatgaaagggtgggacgctcctagtttggggcgctctaggtgttgtggcttgaacgttagcacct +f3v7s38 f3v3s34 0.986842 F 838714 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggtgttcgtgcttgaacgttagcacct +f10v6s83 f10v7s84 0.945205 F 905994 ttccggacactcgcaatcttccaagcagaggtccagctaacacacagcaggcatttagtcctgatgggtagc +f11v2s93 f11v8s99 0.971831 F 421503 agcgttagcaaagagcctggttaatcgccctcggaggatcggtggtgatcagagatgcgggcatccgttc +f3v9s40 f3v3s34 0.986842 F 838714 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggggttgtggcttgaacgttagcacct +f11v4s95 f11v8s99 0.957746 F 421503 agcgttagcaaagagcctggttaatcccctcgagatcgggtggtgatcagagatgcgggcatccgtttc +f8v1s69 f8v0s68 0.984375 F 507278 aagaccttgaacacctctaagcgaaaattggatatcgagtcagggaacataaccacgggtgcgt +f11v5s96 f11v8s99 0.957746 F 421503 agcgttagcaaagagcctggttaatggccctcgaggatcggtggctgaccagagatgcgggcatccgtttc +f6v2s62 f6v0s60 0.968254 F 100983 catgtcgagagactggaaccccggtgatgtgaaaaaaggtcttgggtacgtcccatggctac +f4v6s50 f4v8s52 0.938462 F 368537 ctgtattcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactaagacgaagag +f9v0s76 f9v0s76 1.000000 T 29993 gggatcgtggcaagcaacccttgccgcctcgactgcggctaaagacgcatgttccagtaataccta +f2v12s30 f2v8s26 0.955224 F 863655 tggactagtagaccgtgtgccacaatcgacgctgaggacacgttgatttttatacggcgtctcag +f11v9s100 f11v8s99 0.985915 F 421503 agcgttagcaaaggagcctggttaatcgccctcgaggatcggtggtgatcagagatgcgggcatccgtttc +f4v5s49 f4v8s52 0.938462 F 368537 ctatattcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactaagagaagca +f2v9s27 f2v8s26 0.985075 F 863655 tggactagtagaccgtgtgcctaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f7v3s67 f7v0s64 0.983607 F 114016 gcttcgtgggaccagagacaaccgacatatttcgaaccaccgacatacgatccttaaacc +f0v8s8 f0v7s7 0.916667 F 515632 ggatcacagtcctacactgctcactccaaccccggccctgagtccgaggagagggtgcttcagagtatgtataccactg +f2v6s24 f2v8s26 0.940299 F 863655 tggactagtagacgtgtgcctcatcgacgcgtgagacactttgatttcttatacggcgtctcag +f11v3s94 f11v8s99 0.971831 F 421503 agcgttagcaaagagcctggttaatcgccgtcgaaggatcggtggtgatcagagatgcgggcatccgtttc +f6v3s63 f6v0s60 0.953846 F 100983 catgtcaagagactggaaccccggtgatgtgaaaaaaggttttgggtcacgtctccatggctacg +f3v11s42 f3v3s34 0.973684 F 838714 cgaaaaaggtgaatgaaagtctgggacgctcctggtttgggacgctctaggtgttgtggcttgaacgttagcacct +f4v4s48 f4v8s52 0.923077 F 368537 ctgtattcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactaagagaaa +f3v8s39 f3v3s34 0.960526 F 838714 cgaaaaaggtgaatgaaaggcctgggacgctcctggttggggcgctctaggtgtgtggcttgaacgttagacct +f7v1s65 f7v0s64 0.983607 F 114016 gcttcggggaaccagagacaaccgacatatttcgaaccaccgacatacgatcctttaaacc +f5v1s54 f5v3s56 0.952381 F 192648 cggaacgcaccataagatggtaatcactaaaaaaatcggcctaacctgtccatatcgccactt +f4v7s51 f4v8s52 0.892308 F 368537 cgtttattcgtaggatctactacgcatcaaagtatcccacttttgtaccccactaagagcaga +f7v2s66 f7v0s64 0.967213 F 114016 gcttcgtgggaaccagaacaaccgacatattccgaaccaccggacatacgatccttaaacc +f1v6s15 f1v4s13 0.950617 F 304149 gaatcagcttctaaccaagcgttgcagaaccggctactttaagcatgatgaatgcgtcgtaagtgatactcgacgattc +f5v2s55 f5v3s56 0.936508 F 192648 cggcatcgcaccataagatggtaatcactaaaaaaatcggcctaacctgtccatatcgccact diff --git a/pkg/obitools/obiclust/testdata/families_ratio.expected b/pkg/obitools/obiclust/testdata/families_ratio.expected new file mode 100644 index 0000000..a129dde --- /dev/null +++ b/pkg/obitools/obiclust/testdata/families_ratio.expected @@ -0,0 +1,101 @@ +f2v8s26 f2v8s26 1.000000 T 619902 tggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f10v3s80 f2v8s26 1.000000 F 619902 tggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f2v0s18 f2v8s26 1.000000 F 619902 tggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f2v7s25 f2v8s26 1.000000 F 619902 tggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f10v9s86 f2v8s26 1.000000 F 619902 tggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f2v3s21 f2v3s21 1.000000 T 243753 tggactagtagaccgtgtgcctcaatcgacgcggaggacacgttgatttcttatacggcgtctcag +f6v1s61 f2v3s21 1.000000 F 243753 tggactagtagaccgtgtgcctcaatcgacgcggaggacacgttgatttcttatacggcgtctcag +f2v2s20 f2v3s21 1.000000 F 243753 tggactagtagaccgtgtgcctcaatcgacgcggaggacacgttgatttcttatacggcgtctcag +f3v3s34 f3v3s34 1.000000 T 670881 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggtgttgtggcttgaacgttagcacct +f4v0s44 f3v3s34 1.000000 F 670881 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggtgttgtggcttgaacgttagcacct +f3v0s31 f3v3s34 1.000000 F 670881 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggtgttgtggcttgaacgttagcacct +f0v7s7 f0v7s7 1.000000 T 281628 ggatcacacgtctatcactgctcactccaaccccggcccttgacgtccgaggagagggtgcttctgagtatgtatacccaactg +f1v2s11 f0v7s7 1.000000 F 281628 ggatcacacgtctatcactgctcactccaaccccggcccttgacgtccgaggagagggtgcttctgagtatgtatacccaactg +f1v1s10 f0v7s7 1.000000 F 281628 ggatcacacgtctatcactgctcactccaaccccggcccttgacgtccgaggagagggtgcttctgagtatgtatacccaactg +f3v6s37 f3v6s37 1.000000 T 167833 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgtctctaggttgttgtggctgaacgtagcact +f5v0s53 f3v6s37 1.000000 F 167833 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgtctctaggttgttgtggctgaacgtagcact +f11v7s98 f3v6s37 1.000000 F 167833 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgtctctaggttgttgtggctgaacgtagcact +f2v4s22 f2v8s26 0.970588 F 619902 tggactagtagaccgtgtcgcctcaatcgacgcgtgaggacacgttgattcttatacggcgtctcag +f10v7s84 f10v7s84 1.000000 T 224921 gtgccggacactcgcaatcttcacaagcagaggtccagctaacacacagcagcatttagtcccgatgggtagc +f10v0s77 f10v0s77 1.000000 T 97674 ttgccggacactcgcaatcttccaagcagaggtccagctaacacacagcaggcatttagtcccgatgggtagc +f5v6s59 f2v8s26 0.985075 F 619902 ggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f2v10s28 f2v8s26 0.985075 F 619902 ggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f1v5s14 f1v5s14 1.000000 T 95466 ggatcacagtctacactgctcactccagaccccgcccctgagtccgaggagagggtgcttcagagtatgttaccactg +f0v4s4 f1v5s14 1.000000 F 95466 ggatcacagtctacactgctcactccagaccccgcccctgagtccgaggagagggtgcttcagagtatgttaccactg +f0v1s1 f1v5s14 1.000000 F 95466 ggatcacagtctacactgctcactccagaccccgcccctgagtccgaggagagggtgcttcagagtatgttaccactg +f0v3s3 f1v5s14 1.000000 F 95466 ggatcacagtctacactgctcactccagaccccgcccctgagtccgaggagagggtgcttcagagtatgttaccactg +f5v3s56 f5v3s56 1.000000 T 142796 ccgcaacgcaccataagatgtaatcactaagaaaaatcggcctaacctgtcatatcgccactt +f10v2s79 f10v2s79 1.000000 T 93964 ttgccggtcactcgcaatcttaccaaccagaggtccagctaacacacagcaggcatttagtcccgatgggtagc +f10v13s90 f10v13s90 1.000000 T 92493 tgccggacactcgcaatctccaagcagagagtccaagctaacacacagcaggcatttgtcccgatgggtagc +f1v4s13 f1v4s13 1.000000 T 93209 gaatcagcttctcaaccaagcgatgcagaaccggctactttaagcattgatgaatgcgtcgtaagtgatactggacgattc +f10v4s81 f10v4s81 1.000000 T 88973 ttgccggacactggcaatcttccatagcagaggtccagctaacacacagcaggcatttagtcccgatgggtagc +f10v5s82 f10v5s82 1.000000 T 87843 ttgccggacactcgcaatcttccaagcagaggtccagctaacacatagcaggcatttagtcccgatgggtagc +f4v8s52 f4v8s52 1.000000 T 160201 ctgatattcgtattgatctactacgtatcaaagtatcccacttttgtgaccccactaagagaaga +f2v11s29 f2v8s26 0.955882 F 619902 tggactgtagaaccgtgtgcctcaatccacgcgtgaggacacgttgatttcttatacggcgtctcag +f0v6s6 f0v6s6 1.000000 T 84418 ggatcacagtctacactgctccctccaaccccggcccctggtccgaggagagggtgcttcagagtattataccactg +f1v3s12 f0v6s6 1.000000 F 84418 ggatcacagtctacactgctccctccaaccccggcccctggtccgaggagagggtgcttcagagtattataccactg +f0v0s0 f0v7s7 0.916667 F 281628 ggatcacagtctacactgctcactccaaccccggcccctgagtccgaggagagggtgcttcagagtatgtataccactg +f0v2s2 f0v7s7 0.916667 F 281628 ggatcacagtctacactgctcactccaaccccggcccctgagtccgaggagagggtgcttcagagtatgtataccactg +f1v7s16 f1v7s16 1.000000 T 81657 gaatcagcttcaaccaagcgatgcagaaccgctgctttaagcatgatgaatcgtcgtaagtgatactcgacgattc +f2v5s23 f1v7s16 1.000000 F 81657 gaatcagcttcaaccaagcgatgcagaaccgctgctttaagcatgatgaatcgtcgtaagtgatactcgacgattc +f3v12s43 f3v3s34 0.948718 F 670881 cgaaaaaggtgaatgaaagactgggacgcctcctggtttgggacgctctaggtggttgtggcttgaacgttagcacct +f8v0s68 f8v0s68 1.000000 T 114020 aagaccttgaacacctctaagcgaaaattggattcgagtcagggaacataaccacgggtgcgt +f2v1s19 f2v1s19 1.000000 T 79492 gaatcaggttcttccaagcgctgcagaaccggctactttaagcattgatgaatgcgtcagtaagtgatacccgacgattc +f1v8s17 f2v1s19 1.000000 F 79492 gaatcaggttcttccaagcgctgcagaaccggctactttaagcattgatgaatgcgtcagtaagtgatacccgacgattc +f10v11s88 f10v11s88 1.000000 T 78894 ttgccggacactcgcaatcttccaagcagaggtccagctaactcacagccaggcatttagtcccgatgggtagc +f3v5s36 f3v3s34 0.973684 F 670881 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggtgttatggcttgaaagttagcacct +f8v7s75 f8v7s75 1.000000 T 75803 aagaccttgaacacctctaagcgaaaattggattccagtcagggaaccataaccacgggtgcgt +f10v12s89 f10v12s89 1.000000 T 74438 ttgccggacactcgcaatcttccaagcagaggtccagctaacacacacgcaggcatttagacccgatgggtagc +f4v2s46 f4v2s46 1.000000 T 83032 ctgtattcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactaagagaaga +f8v2s70 f8v2s70 1.000000 T 74088 aagaccttgaacaacctctaagccgaaattggaattcgagtcagggaacataaccacgggtgcgat +f11v8s99 f11v8s99 1.000000 T 224582 agcgttagcaaagagcctggttaatctgccctcgaggatcggtggtgatcagagatgcgggcatccgtttc +f7v0s64 f7v0s64 1.000000 T 114016 gcttcgtgggaaccagagacaaccgacatatttcgaaccaccgacatacgatccttaaacc +f11v1s92 f11v1s92 1.000000 T 71853 agcgttagcaaagagcctggttcatcgccctcgaggatcggtggtgatcaagagatgcggcatccgtttc +f3v2s33 f3v3s34 0.948052 F 670881 cgaaaaaaggtaatgaaaggctgggatgctcctggtttggggcgctctaggtgttgtggcttgaacgttagcact +f8v3s71 f8v3s71 1.000000 T 70497 aagaccttgaacacctctaagcgaaattggattcgagtcagcgaacataacacgggtgcgt +f4v3s47 f4v3s47 1.000000 T 69396 ctgtattcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactaaagaaga +f10v10s87 f10v10s87 1.000000 T 66794 tctgccggacactcgcaatcttccaagcagaggtccagctaacacacgcaggcatttatccgatgggtagc +f11v0s91 f11v0s91 1.000000 T 66160 agcgttagcaaagagcctggttaatcgccctcgaggatcggtggtgatcagagatgcgggcatccgtttc +f8v5s73 f8v5s73 1.000000 T 64084 aagaccttgtacacctctaagcgataaattggattcgagtcagggaacataaccacggatgcgt +f8v4s72 f8v4s72 1.000000 T 59724 aagacttgaacacctctaagcgaaaattggattcgagtcagggaacataaccacgggtgcgt +f3v1s32 f3v3s34 0.974359 F 670881 cgaaaaagagtgaatgaaatggctgggacgctcctggtttggggcgctctaggtgttgtggcttgaacgttagcacct +f3v4s35 f3v3s34 0.948052 F 670881 cgaaaaaggtgaagaaaggctgggacgctcctggttggcgcgctctaggtgttgtggcttgaacgttagcaccta +f11v6s97 f11v6s97 1.000000 T 58908 agcgttagcaaagaacctggttaatcgccctcgaggatcggtggtggatcggagatgcgggcatcgcgtttc +f4v1s45 f4v1s45 1.000000 T 55908 ctgtatttcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactatagagaaga +f0v5s5 f0v5s5 1.000000 T 54120 ggatcacagtctacactgctcactcccccccggcccctgagtccgaggagagggtgcttcaaagtatgtataccactg +f5v5s58 f5v5s58 1.000000 T 49852 cgcaacgcaccattaagatggtaatcactaaaaaaatcggcctaacctgtccaatcgccactt +f1v0s9 f1v0s9 1.000000 T 49791 gaatcagcttctaaccaagcgatgcagaaccggctactttaagcattgatgaatgcgtcgtaagtgatactcgacgattc +f8v6s74 f8v6s74 1.000000 T 49062 aagacccttgaacacctctaagcgaaaattggattcgagtcagggaacataaccacgggtgccgt +f6v0s60 f6v0s60 1.000000 T 67982 catgtcaagagactggaaccccggtgatgtgaaaaaaggtcttgggtacgtcccatggctacg +f10v8s85 f10v7s84 0.920000 F 224921 ttgccggacactcgaatcttccaagcagaggtccagctaacacacagcaggcatactagtcccgatgggtagc +f10v1s78 f10v7s84 0.933333 F 224921 ttgccggacactcgcaatcctccaagcagaggtccagctaacacacagcaggcatttagtcccgatgaggtagc +f5v4s57 f5v3s56 0.924242 F 142796 cggcaacgcaccataagatggtaatcactaaaaaaatcggcctaacctgtccatatcgccactat +f3v10s41 f3v3s34 0.948718 F 670881 cgaaaataaggtgaatgaaagggtgggacgctcctagtttggggcgctctaggtgttgtggcttgaacgttagcacct +f3v7s38 f3v3s34 0.974026 F 670881 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggtgttcgtgcttgaacgttagcacct +f10v6s83 f10v7s84 0.932432 F 224921 ttccggacactcgcaatcttccaagcagaggtccagctaacacacagcaggcatttagtcctgatgggtagc +f11v2s93 f11v8s99 0.958333 F 224582 agcgttagcaaagagcctggttaatcgccctcggaggatcggtggtgatcagagatgcgggcatccgttc +f3v9s40 f3v3s34 0.986842 F 670881 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggggttgtggcttgaacgttagcacct +f11v4s95 f11v8s99 0.944444 F 224582 agcgttagcaaagagcctggttaatcccctcgagatcgggtggtgatcagagatgcgggcatccgtttc +f8v1s69 f8v0s68 0.984375 F 114020 aagaccttgaacacctctaagcgaaaattggatatcgagtcagggaacataaccacgggtgcgt +f11v5s96 f11v8s99 0.944444 F 224582 agcgttagcaaagagcctggttaatggccctcgaggatcggtggctgaccagagatgcgggcatccgtttc +f6v2s62 f6v2s62 1.000000 T 33001 catgtcgagagactggaaccccggtgatgtgaaaaaaggtcttgggtacgtcccatggctac +f4v6s50 f4v8s52 0.910448 F 160201 ctgtattcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactaagacgaagag +f9v0s76 f9v0s76 1.000000 T 29993 gggatcgtggcaagcaacccttgccgcctcgactgcggctaaagacgcatgttccagtaataccta +f2v12s30 f2v8s26 0.955224 F 619902 tggactagtagaccgtgtgccacaatcgacgctgaggacacgttgatttttatacggcgtctcag +f11v9s100 f11v8s99 0.972222 F 224582 agcgttagcaaaggagcctggttaatcgccctcgaggatcggtggtgatcagagatgcgggcatccgtttc +f4v5s49 f4v8s52 0.924242 F 160201 ctatattcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactaagagaagca +f2v9s27 f2v8s26 0.985075 F 619902 tggactagtagaccgtgtgcctaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f7v3s67 f7v0s64 0.983607 F 114016 gcttcgtgggaccagagacaaccgacatatttcgaaccaccgacatacgatccttaaacc +f0v8s8 f0v7s7 0.905882 F 281628 ggatcacagtcctacactgctcactccaaccccggccctgagtccgaggagagggtgcttcagagtatgtataccactg +f2v6s24 f2v8s26 0.940299 F 619902 tggactagtagacgtgtgcctcatcgacgcgtgagacactttgatttcttatacggcgtctcag +f11v3s94 f11v8s99 0.958333 F 224582 agcgttagcaaagagcctggttaatcgccgtcgaaggatcggtggtgatcagagatgcgggcatccgtttc +f6v3s63 f6v0s60 0.953846 F 67982 catgtcaagagactggaaccccggtgatgtgaaaaaaggttttgggtcacgtctccatggctacg +f3v11s42 f3v3s34 0.973684 F 670881 cgaaaaaggtgaatgaaagtctgggacgctcctggtttgggacgctctaggtgttgtggcttgaacgttagcacct +f4v4s48 f4v8s52 0.923077 F 160201 ctgtattcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactaagagaaa +f3v8s39 f3v3s34 0.948052 F 670881 cgaaaaaggtgaatgaaaggcctgggacgctcctggttggggcgctctaggtgtgtggcttgaacgttagacct +f7v1s65 f7v0s64 0.967742 F 114016 gcttcggggaaccagagacaaccgacatatttcgaaccaccgacatacgatcctttaaacc +f5v1s54 f5v3s56 0.923077 F 142796 cggaacgcaccataagatggtaatcactaaaaaaatcggcctaacctgtccatatcgccactt +f4v7s51 f4v2s46 0.937500 F 83032 cgtttattcgtaggatctactacgcatcaaagtatcccacttttgtaccccactaagagcaga +f7v2s66 f7v0s64 0.951613 F 114016 gcttcgtgggaaccagaacaaccgacatattccgaaccaccggacatacgatccttaaacc +f1v6s15 f1v4s13 0.950617 F 93209 gaatcagcttctaaccaagcgttgcagaaccggctactttaagcatgatgaatgcgtcgtaagtgatactcgacgattc +f5v2s55 f5v3s56 0.907692 F 142796 cggcatcgcaccataagatggtaatcactaaaaaaatcggcctaacctgtccatatcgccact diff --git a/pkg/obitools/obiclust/testdata/families_rawdist.expected b/pkg/obitools/obiclust/testdata/families_rawdist.expected new file mode 100644 index 0000000..243b7d2 --- /dev/null +++ b/pkg/obitools/obiclust/testdata/families_rawdist.expected @@ -0,0 +1,101 @@ +f2v8s26 f2v8s26 0.000000 T 863655 tggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f10v3s80 f2v8s26 0.000000 F 863655 tggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f2v0s18 f2v8s26 0.000000 F 863655 tggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f2v7s25 f2v8s26 0.000000 F 863655 tggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f10v9s86 f2v8s26 0.000000 F 863655 tggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f2v3s21 f2v8s26 1.000000 F 863655 tggactagtagaccgtgtgcctcaatcgacgcggaggacacgttgatttcttatacggcgtctcag +f6v1s61 f2v8s26 1.000000 F 863655 tggactagtagaccgtgtgcctcaatcgacgcggaggacacgttgatttcttatacggcgtctcag +f2v2s20 f2v8s26 1.000000 F 863655 tggactagtagaccgtgtgcctcaatcgacgcggaggacacgttgatttcttatacggcgtctcag +f3v3s34 f3v3s34 0.000000 T 670881 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggtgttgtggcttgaacgttagcacct +f4v0s44 f3v3s34 0.000000 F 670881 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggtgttgtggcttgaacgttagcacct +f3v0s31 f3v3s34 0.000000 F 670881 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggtgttgtggcttgaacgttagcacct +f0v7s7 f0v7s7 0.000000 T 173457 ggatcacacgtctatcactgctcactccaaccccggcccttgacgtccgaggagagggtgcttctgagtatgtatacccaactg +f1v2s11 f0v7s7 0.000000 F 173457 ggatcacacgtctatcactgctcactccaaccccggcccttgacgtccgaggagagggtgcttctgagtatgtatacccaactg +f1v1s10 f0v7s7 0.000000 F 173457 ggatcacacgtctatcactgctcactccaaccccggcccttgacgtccgaggagagggtgcttctgagtatgtatacccaactg +f3v6s37 f3v6s37 0.000000 T 167833 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgtctctaggttgttgtggctgaacgtagcact +f5v0s53 f3v6s37 0.000000 F 167833 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgtctctaggttgttgtggctgaacgtagcact +f11v7s98 f3v6s37 0.000000 F 167833 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgtctctaggttgttgtggctgaacgtagcact +f2v4s22 f2v8s26 2.000000 F 863655 tggactagtagaccgtgtcgcctcaatcgacgcgtgaggacacgttgattcttatacggcgtctcag +f10v7s84 f10v7s84 0.000000 T 196352 gtgccggacactcgcaatcttcacaagcagaggtccagctaacacacagcagcatttagtcccgatgggtagc +f10v0s77 f10v7s84 3.000000 F 196352 ttgccggacactcgcaatcttccaagcagaggtccagctaacacacagcaggcatttagtcccgatgggtagc +f5v6s59 f2v8s26 1.000000 F 863655 ggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f2v10s28 f2v8s26 1.000000 F 863655 ggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f1v5s14 f1v5s14 0.000000 T 203637 ggatcacagtctacactgctcactccagaccccgcccctgagtccgaggagagggtgcttcagagtatgttaccactg +f0v4s4 f1v5s14 0.000000 F 203637 ggatcacagtctacactgctcactccagaccccgcccctgagtccgaggagagggtgcttcagagtatgttaccactg +f0v1s1 f1v5s14 0.000000 F 203637 ggatcacagtctacactgctcactccagaccccgcccctgagtccgaggagagggtgcttcagagtatgttaccactg +f0v3s3 f1v5s14 0.000000 F 203637 ggatcacagtctacactgctcactccagaccccgcccctgagtccgaggagagggtgcttcagagtatgttaccactg +f5v3s56 f5v3s56 0.000000 T 94426 ccgcaacgcaccataagatgtaatcactaagaaaaatcggcctaacctgtcatatcgccactt +f10v2s79 f10v2s79 0.000000 T 93964 ttgccggtcactcgcaatcttaccaaccagaggtccagctaacacacagcaggcatttagtcccgatgggtagc +f10v13s90 f10v13s90 0.000000 T 92493 tgccggacactcgcaatctccaagcagagagtccaagctaacacacagcaggcatttgtcccgatgggtagc +f1v4s13 f1v4s13 0.000000 T 143000 gaatcagcttctcaaccaagcgatgcagaaccggctactttaagcattgatgaatgcgtcgtaagtgatactggacgattc +f10v4s81 f10v4s81 0.000000 T 409687 ttgccggacactggcaatcttccatagcagaggtccagctaacacacagcaggcatttagtcccgatgggtagc +f10v5s82 f10v4s81 3.000000 F 409687 ttgccggacactcgcaatcttccaagcagaggtccagctaacacatagcaggcatttagtcccgatgggtagc +f4v8s52 f4v8s52 0.000000 T 161072 ctgatattcgtattgatctactacgtatcaaagtatcccacttttgtgaccccactaagagaaga +f2v11s29 f2v8s26 3.000000 F 863655 tggactgtagaaccgtgtgcctcaatccacgcgtgaggacacgttgatttcttatacggcgtctcag +f0v6s6 f0v6s6 0.000000 T 84418 ggatcacagtctacactgctccctccaaccccggcccctggtccgaggagagggtgcttcagagtattataccactg +f1v3s12 f0v6s6 0.000000 F 84418 ggatcacagtctacactgctccctccaaccccggcccctggtccgaggagagggtgcttcagagtattataccactg +f0v0s0 f1v5s14 3.000000 F 203637 ggatcacagtctacactgctcactccaaccccggcccctgagtccgaggagagggtgcttcagagtatgtataccactg +f0v2s2 f1v5s14 3.000000 F 203637 ggatcacagtctacactgctcactccaaccccggcccctgagtccgaggagagggtgcttcagagtatgtataccactg +f1v7s16 f1v7s16 0.000000 T 81657 gaatcagcttcaaccaagcgatgcagaaccgctgctttaagcatgatgaatcgtcgtaagtgatactcgacgattc +f2v5s23 f1v7s16 0.000000 F 81657 gaatcagcttcaaccaagcgatgcagaaccgctgctttaagcatgatgaatcgtcgtaagtgatactcgacgattc +f3v12s43 f3v3s34 4.000000 F 670881 cgaaaaaggtgaatgaaagactgggacgcctcctggtttgggacgctctaggtggttgtggcttgaacgttagcacct +f8v0s68 f8v0s68 0.000000 T 433190 aagaccttgaacacctctaagcgaaaattggattcgagtcagggaacataaccacgggtgcgt +f2v1s19 f2v1s19 0.000000 T 79492 gaatcaggttcttccaagcgctgcagaaccggctactttaagcattgatgaatgcgtcagtaagtgatacccgacgattc +f1v8s17 f2v1s19 0.000000 F 79492 gaatcaggttcttccaagcgctgcagaaccggctactttaagcattgatgaatgcgtcagtaagtgatacccgacgattc +f10v11s88 f10v4s81 4.000000 F 409687 ttgccggacactcgcaatcttccaagcagaggtccagctaactcacagccaggcatttagtcccgatgggtagc +f3v5s36 f3v3s34 2.000000 F 670881 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggtgttatggcttgaaagttagcacct +f8v7s75 f8v0s68 2.000000 F 433190 aagaccttgaacacctctaagcgaaaattggattccagtcagggaaccataaccacgggtgcgt +f10v12s89 f10v4s81 4.000000 F 409687 ttgccggacactcgcaatcttccaagcagaggtccagctaacacacacgcaggcatttagacccgatgggtagc +f4v2s46 f4v8s52 4.000000 F 161072 ctgtattcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactaagagaaga +f8v2s70 f8v2s70 0.000000 T 74088 aagaccttgaacaacctctaagccgaaattggaattcgagtcagggaacataaccacgggtgcgat +f11v8s99 f11v8s99 0.000000 T 362595 agcgttagcaaagagcctggttaatctgccctcgaggatcggtggtgatcagagatgcgggcatccgtttc +f7v0s64 f7v0s64 0.000000 T 114016 gcttcgtgggaaccagagacaaccgacatatttcgaaccaccgacatacgatccttaaacc +f11v1s92 f11v8s99 4.000000 F 362595 agcgttagcaaagagcctggttcatcgccctcgaggatcggtggtgatcaagagatgcggcatccgtttc +f3v2s33 f3v3s34 4.000000 F 670881 cgaaaaaaggtaatgaaaggctgggatgctcctggtttggggcgctctaggtgttgtggcttgaacgttagcact +f8v3s71 f8v0s68 3.000000 F 433190 aagaccttgaacacctctaagcgaaattggattcgagtcagcgaacataacacgggtgcgt +f4v3s47 f4v3s47 0.000000 T 198866 ctgtattcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactaaagaaga +f10v10s87 f10v10s87 0.000000 T 66794 tctgccggacactcgcaatcttccaagcagaggtccagctaacacacgcaggcatttatccgatgggtagc +f11v0s91 f11v8s99 1.000000 F 362595 agcgttagcaaagagcctggttaatcgccctcgaggatcggtggtgatcagagatgcgggcatccgtttc +f8v5s73 f8v0s68 3.000000 F 433190 aagaccttgtacacctctaagcgataaattggattcgagtcagggaacataaccacggatgcgt +f8v4s72 f8v0s68 1.000000 F 433190 aagacttgaacacctctaagcgaaaattggattcgagtcagggaacataaccacgggtgcgt +f3v1s32 f3v3s34 2.000000 F 670881 cgaaaaagagtgaatgaaatggctgggacgctcctggtttggggcgctctaggtgttgtggcttgaacgttagcacct +f3v4s35 f3v3s34 4.000000 F 670881 cgaaaaaggtgaagaaaggctgggacgctcctggttggcgcgctctaggtgttgtggcttgaacgttagcaccta +f11v6s97 f11v6s97 0.000000 T 58908 agcgttagcaaagaacctggttaatcgccctcgaggatcggtggtggatcggagatgcgggcatcgcgtttc +f4v1s45 f4v3s47 3.000000 F 198866 ctgtatttcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactatagagaaga +f0v5s5 f0v5s5 0.000000 T 54120 ggatcacagtctacactgctcactcccccccggcccctgagtccgaggagagggtgcttcaaagtatgtataccactg +f5v5s58 f5v5s58 0.000000 T 97556 cgcaacgcaccattaagatggtaatcactaaaaaaatcggcctaacctgtccaatcgccactt +f1v0s9 f1v4s13 2.000000 F 143000 gaatcagcttctaaccaagcgatgcagaaccggctactttaagcattgatgaatgcgtcgtaagtgatactcgacgattc +f8v6s74 f8v0s68 2.000000 F 433190 aagacccttgaacacctctaagcgaaaattggattcgagtcagggaacataaccacgggtgccgt +f6v0s60 f6v0s60 0.000000 T 100983 catgtcaagagactggaaccccggtgatgtgaaaaaaggtcttgggtacgtcccatggctacg +f10v8s85 f10v8s85 0.000000 T 46704 ttgccggacactcgaatcttccaagcagaggtccagctaacacacagcaggcatactagtcccgatgggtagc +f10v1s78 f10v4s81 4.000000 F 409687 ttgccggacactcgcaatcctccaagcagaggtccagctaacacacagcaggcatttagtcccgatgaggtagc +f5v4s57 f5v5s58 4.000000 F 97556 cggcaacgcaccataagatggtaatcactaaaaaaatcggcctaacctgtccatatcgccactat +f3v10s41 f3v3s34 4.000000 F 670881 cgaaaataaggtgaatgaaagggtgggacgctcctagtttggggcgctctaggtgttgtggcttgaacgttagcacct +f3v7s38 f3v3s34 2.000000 F 670881 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggtgttcgtgcttgaacgttagcacct +f10v6s83 f10v4s81 4.000000 F 409687 ttccggacactcgcaatcttccaagcagaggtccagctaacacacagcaggcatttagtcctgatgggtagc +f11v2s93 f11v8s99 3.000000 F 362595 agcgttagcaaagagcctggttaatcgccctcggaggatcggtggtgatcagagatgcgggcatccgttc +f3v9s40 f3v3s34 1.000000 F 670881 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggggttgtggcttgaacgttagcacct +f11v4s95 f11v8s99 4.000000 F 362595 agcgttagcaaagagcctggttaatcccctcgagatcgggtggtgatcagagatgcgggcatccgtttc +f8v1s69 f8v0s68 1.000000 F 433190 aagaccttgaacacctctaagcgaaaattggatatcgagtcagggaacataaccacgggtgcgt +f11v5s96 f11v8s99 4.000000 F 362595 agcgttagcaaagagcctggttaatggccctcgaggatcggtggctgaccagagatgcgggcatccgtttc +f6v2s62 f6v0s60 2.000000 F 100983 catgtcgagagactggaaccccggtgatgtgaaaaaaggtcttgggtacgtcccatggctac +f4v6s50 f4v3s47 3.000000 F 198866 ctgtattcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactaagacgaagag +f9v0s76 f9v0s76 0.000000 T 29993 gggatcgtggcaagcaacccttgccgcctcgactgcggctaaagacgcatgttccagtaataccta +f2v12s30 f2v8s26 3.000000 F 863655 tggactagtagaccgtgtgccacaatcgacgctgaggacacgttgatttttatacggcgtctcag +f11v9s100 f11v8s99 2.000000 F 362595 agcgttagcaaaggagcctggttaatcgccctcgaggatcggtggtgatcagagatgcgggcatccgtttc +f4v5s49 f4v3s47 3.000000 F 198866 ctatattcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactaagagaagca +f2v9s27 f2v8s26 1.000000 F 863655 tggactagtagaccgtgtgcctaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f7v3s67 f7v0s64 1.000000 F 114016 gcttcgtgggaccagagacaaccgacatatttcgaaccaccgacatacgatccttaaacc +f0v8s8 f1v5s14 4.000000 F 203637 ggatcacagtcctacactgctcactccaaccccggccctgagtccgaggagagggtgcttcagagtatgtataccactg +f2v6s24 f2v8s26 4.000000 F 863655 tggactagtagacgtgtgcctcatcgacgcgtgagacactttgatttcttatacggcgtctcag +f11v3s94 f11v8s99 3.000000 F 362595 agcgttagcaaagagcctggttaatcgccgtcgaaggatcggtggtgatcagagatgcgggcatccgtttc +f6v3s63 f6v0s60 3.000000 F 100983 catgtcaagagactggaaccccggtgatgtgaaaaaaggttttgggtcacgtctccatggctacg +f3v11s42 f3v3s34 2.000000 F 670881 cgaaaaaggtgaatgaaagtctgggacgctcctggtttgggacgctctaggtgttgtggcttgaacgttagcacct +f4v4s48 f4v3s47 2.000000 F 198866 ctgtattcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactaagagaaa +f3v8s39 f3v3s34 4.000000 F 670881 cgaaaaaggtgaatgaaaggcctgggacgctcctggttggggcgctctaggtgtgtggcttgaacgttagacct +f7v1s65 f7v0s64 2.000000 F 114016 gcttcggggaaccagagacaaccgacatatttcgaaccaccgacatacgatcctttaaacc +f5v1s54 f5v5s58 3.000000 F 97556 cggaacgcaccataagatggtaatcactaaaaaaatcggcctaacctgtccatatcgccactt +f4v7s51 f4v7s51 0.000000 T 8599 cgtttattcgtaggatctactacgcatcaaagtatcccacttttgtaccccactaagagcaga +f7v2s66 f7v0s64 3.000000 F 114016 gcttcgtgggaaccagaacaaccgacatattccgaaccaccggacatacgatccttaaacc +f1v6s15 f1v4s13 4.000000 F 143000 gaatcagcttctaaccaagcgttgcagaaccggctactttaagcatgatgaatgcgtcgtaagtgatactcgacgattc +f5v2s55 f5v2s55 0.000000 T 666 cggcatcgcaccataagatggtaatcactaaaaaaatcggcctaacctgtccatatcgccact diff --git a/pkg/obitools/obiclust/testdata/families_rawlcs.expected b/pkg/obitools/obiclust/testdata/families_rawlcs.expected new file mode 100644 index 0000000..9fabe7e --- /dev/null +++ b/pkg/obitools/obiclust/testdata/families_rawlcs.expected @@ -0,0 +1,101 @@ +f2v8s26 f2v8s26 67.000000 T 4505217 tggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f10v3s80 f2v8s26 67.000000 F 4505217 tggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f2v0s18 f2v8s26 67.000000 F 4505217 tggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f2v7s25 f2v8s26 67.000000 F 4505217 tggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f10v9s86 f2v8s26 67.000000 F 4505217 tggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f2v3s21 f2v8s26 66.000000 F 4505217 tggactagtagaccgtgtgcctcaatcgacgcggaggacacgttgatttcttatacggcgtctcag +f6v1s61 f2v8s26 66.000000 F 4505217 tggactagtagaccgtgtgcctcaatcgacgcggaggacacgttgatttcttatacggcgtctcag +f2v2s20 f2v8s26 66.000000 F 4505217 tggactagtagaccgtgtgcctcaatcgacgcggaggacacgttgatttcttatacggcgtctcag +f3v3s34 f2v8s26 42.000000 F 4505217 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggtgttgtggcttgaacgttagcacct +f4v0s44 f2v8s26 42.000000 F 4505217 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggtgttgtggcttgaacgttagcacct +f3v0s31 f2v8s26 42.000000 F 4505217 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggtgttgtggcttgaacgttagcacct +f0v7s7 f2v8s26 48.000000 F 4505217 ggatcacacgtctatcactgctcactccaaccccggcccttgacgtccgaggagagggtgcttctgagtatgtatacccaactg +f1v2s11 f2v8s26 48.000000 F 4505217 ggatcacacgtctatcactgctcactccaaccccggcccttgacgtccgaggagagggtgcttctgagtatgtatacccaactg +f1v1s10 f2v8s26 48.000000 F 4505217 ggatcacacgtctatcactgctcactccaaccccggcccttgacgtccgaggagagggtgcttctgagtatgtatacccaactg +f3v6s37 f2v8s26 43.000000 F 4505217 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgtctctaggttgttgtggctgaacgtagcact +f5v0s53 f2v8s26 43.000000 F 4505217 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgtctctaggttgttgtggctgaacgtagcact +f11v7s98 f2v8s26 43.000000 F 4505217 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgtctctaggttgttgtggctgaacgtagcact +f2v4s22 f2v8s26 66.000000 F 4505217 tggactagtagaccgtgtcgcctcaatcgacgcgtgaggacacgttgattcttatacggcgtctcag +f10v7s84 f2v8s26 43.000000 F 4505217 gtgccggacactcgcaatcttcacaagcagaggtccagctaacacacagcagcatttagtcccgatgggtagc +f10v0s77 f2v8s26 43.000000 F 4505217 ttgccggacactcgcaatcttccaagcagaggtccagctaacacacagcaggcatttagtcccgatgggtagc +f5v6s59 f2v8s26 66.000000 F 4505217 ggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f2v10s28 f2v8s26 66.000000 F 4505217 ggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f1v5s14 f2v8s26 46.000000 F 4505217 ggatcacagtctacactgctcactccagaccccgcccctgagtccgaggagagggtgcttcagagtatgttaccactg +f0v4s4 f2v8s26 46.000000 F 4505217 ggatcacagtctacactgctcactccagaccccgcccctgagtccgaggagagggtgcttcagagtatgttaccactg +f0v1s1 f2v8s26 46.000000 F 4505217 ggatcacagtctacactgctcactccagaccccgcccctgagtccgaggagagggtgcttcagagtatgttaccactg +f0v3s3 f2v8s26 46.000000 F 4505217 ggatcacagtctacactgctcactccagaccccgcccctgagtccgaggagagggtgcttcagagtatgttaccactg +f5v3s56 f5v3s56 63.000000 T 229172 ccgcaacgcaccataagatgtaatcactaagaaaaatcggcctaacctgtcatatcgccactt +f10v2s79 f2v8s26 43.000000 F 4505217 ttgccggtcactcgcaatcttaccaaccagaggtccagctaacacacagcaggcatttagtcccgatgggtagc +f10v13s90 f2v8s26 43.000000 F 4505217 tgccggacactcgcaatctccaagcagagagtccaagctaacacacagcaggcatttgtcccgatgggtagc +f1v4s13 f2v8s26 49.000000 F 4505217 gaatcagcttctcaaccaagcgatgcagaaccggctactttaagcattgatgaatgcgtcgtaagtgatactggacgattc +f10v4s81 f2v8s26 44.000000 F 4505217 ttgccggacactggcaatcttccatagcagaggtccagctaacacacagcaggcatttagtcccgatgggtagc +f10v5s82 f2v8s26 43.000000 F 4505217 ttgccggacactcgcaatcttccaagcagaggtccagctaacacatagcaggcatttagtcccgatgggtagc +f4v8s52 f4v8s52 65.000000 T 254973 ctgatattcgtattgatctactacgtatcaaagtatcccacttttgtgaccccactaagagaaga +f2v11s29 f2v8s26 65.000000 F 4505217 tggactgtagaaccgtgtgcctcaatccacgcgtgaggacacgttgatttcttatacggcgtctcag +f0v6s6 f2v8s26 47.000000 F 4505217 ggatcacagtctacactgctccctccaaccccggcccctggtccgaggagagggtgcttcagagtattataccactg +f1v3s12 f2v8s26 47.000000 F 4505217 ggatcacagtctacactgctccctccaaccccggcccctggtccgaggagagggtgcttcagagtattataccactg +f0v0s0 f2v8s26 47.000000 F 4505217 ggatcacagtctacactgctcactccaaccccggcccctgagtccgaggagagggtgcttcagagtatgtataccactg +f0v2s2 f2v8s26 47.000000 F 4505217 ggatcacagtctacactgctcactccaaccccggcccctgagtccgaggagagggtgcttcagagtatgtataccactg +f1v7s16 f2v8s26 46.000000 F 4505217 gaatcagcttcaaccaagcgatgcagaaccgctgctttaagcatgatgaatcgtcgtaagtgatactcgacgattc +f2v5s23 f2v8s26 46.000000 F 4505217 gaatcagcttcaaccaagcgatgcagaaccgctgctttaagcatgatgaatcgtcgtaagtgatactcgacgattc +f3v12s43 f2v8s26 44.000000 F 4505217 cgaaaaaggtgaatgaaagactgggacgcctcctggtttgggacgctctaggtggttgtggcttgaacgttagcacct +f8v0s68 f2v8s26 40.000000 F 4505217 aagaccttgaacacctctaagcgaaaattggattcgagtcagggaacataaccacgggtgcgt +f2v1s19 f2v8s26 47.000000 F 4505217 gaatcaggttcttccaagcgctgcagaaccggctactttaagcattgatgaatgcgtcagtaagtgatacccgacgattc +f1v8s17 f2v8s26 47.000000 F 4505217 gaatcaggttcttccaagcgctgcagaaccggctactttaagcattgatgaatgcgtcagtaagtgatacccgacgattc +f10v11s88 f2v8s26 43.000000 F 4505217 ttgccggacactcgcaatcttccaagcagaggtccagctaactcacagccaggcatttagtcccgatgggtagc +f3v5s36 f2v8s26 43.000000 F 4505217 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggtgttatggcttgaaagttagcacct +f8v7s75 f5v3s56 41.000000 F 229172 aagaccttgaacacctctaagcgaaaattggattccagtcagggaaccataaccacgggtgcgt +f10v12s89 f2v8s26 43.000000 F 4505217 ttgccggacactcgcaatcttccaagcagaggtccagctaacacacacgcaggcatttagacccgatgggtagc +f4v2s46 f4v8s52 61.000000 F 254973 ctgtattcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactaagagaaga +f8v2s70 f2v8s26 40.000000 F 4505217 aagaccttgaacaacctctaagccgaaattggaattcgagtcagggaacataaccacgggtgcgat +f11v8s99 f2v8s26 45.000000 F 4505217 agcgttagcaaagagcctggttaatctgccctcgaggatcggtggtgatcagagatgcgggcatccgtttc +f7v0s64 f7v0s64 61.000000 T 114016 gcttcgtgggaaccagagacaaccgacatatttcgaaccaccgacatacgatccttaaacc +f11v1s92 f2v8s26 45.000000 F 4505217 agcgttagcaaagagcctggttcatcgccctcgaggatcggtggtgatcaagagatgcggcatccgtttc +f3v2s33 f2v8s26 42.000000 F 4505217 cgaaaaaaggtaatgaaaggctgggatgctcctggtttggggcgctctaggtgttgtggcttgaacgttagcact +f8v3s71 f2v8s26 40.000000 F 4505217 aagaccttgaacacctctaagcgaaattggattcgagtcagcgaacataacacgggtgcgt +f4v3s47 f4v8s52 60.000000 F 254973 ctgtattcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactaaagaaga +f10v10s87 f2v8s26 43.000000 F 4505217 tctgccggacactcgcaatcttccaagcagaggtccagctaacacacgcaggcatttatccgatgggtagc +f11v0s91 f2v8s26 45.000000 F 4505217 agcgttagcaaagagcctggttaatcgccctcgaggatcggtggtgatcagagatgcgggcatccgtttc +f8v5s73 f2v8s26 40.000000 F 4505217 aagaccttgtacacctctaagcgataaattggattcgagtcagggaacataaccacggatgcgt +f8v4s72 f8v4s72 62.000000 T 59724 aagacttgaacacctctaagcgaaaattggattcgagtcagggaacataaccacgggtgcgt +f3v1s32 f2v8s26 44.000000 F 4505217 cgaaaaagagtgaatgaaatggctgggacgctcctggtttggggcgctctaggtgttgtggcttgaacgttagcacct +f3v4s35 f2v8s26 43.000000 F 4505217 cgaaaaaggtgaagaaaggctgggacgctcctggttggcgcgctctaggtgttgtggcttgaacgttagcaccta +f11v6s97 f2v8s26 45.000000 F 4505217 agcgttagcaaagaacctggttaatcgccctcgaggatcggtggtggatcggagatgcgggcatcgcgtttc +f4v1s45 f2v8s26 40.000000 F 4505217 ctgtatttcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactatagagaaga +f0v5s5 f2v8s26 45.000000 F 4505217 ggatcacagtctacactgctcactcccccccggcccctgagtccgaggagagggtgcttcaaagtatgtataccactg +f5v5s58 f5v3s56 60.000000 F 229172 cgcaacgcaccattaagatggtaatcactaaaaaaatcggcctaacctgtccaatcgccactt +f1v0s9 f2v8s26 47.000000 F 4505217 gaatcagcttctaaccaagcgatgcagaaccggctactttaagcattgatgaatgcgtcgtaagtgatactcgacgattc +f8v6s74 f2v8s26 41.000000 F 4505217 aagacccttgaacacctctaagcgaaaattggattcgagtcagggaacataaccacgggtgccgt +f6v0s60 f2v8s26 41.000000 F 4505217 catgtcaagagactggaaccccggtgatgtgaaaaaaggtcttgggtacgtcccatggctacg +f10v8s85 f2v8s26 43.000000 F 4505217 ttgccggacactcgaatcttccaagcagaggtccagctaacacacagcaggcatactagtcccgatgggtagc +f10v1s78 f2v8s26 44.000000 F 4505217 ttgccggacactcgcaatcctccaagcagaggtccagctaacacacagcaggcatttagtcccgatgaggtagc +f5v4s57 f2v8s26 41.000000 F 4505217 cggcaacgcaccataagatggtaatcactaaaaaaatcggcctaacctgtccatatcgccactat +f3v10s41 f2v8s26 43.000000 F 4505217 cgaaaataaggtgaatgaaagggtgggacgctcctagtttggggcgctctaggtgttgtggcttgaacgttagcacct +f3v7s38 f2v8s26 43.000000 F 4505217 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggtgttcgtgcttgaacgttagcacct +f10v6s83 f2v8s26 44.000000 F 4505217 ttccggacactcgcaatcttccaagcagaggtccagctaacacacagcaggcatttagtcctgatgggtagc +f11v2s93 f2v8s26 45.000000 F 4505217 agcgttagcaaagagcctggttaatcgccctcggaggatcggtggtgatcagagatgcgggcatccgttc +f3v9s40 f2v8s26 42.000000 F 4505217 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggggttgtggcttgaacgttagcacct +f11v4s95 f2v8s26 44.000000 F 4505217 agcgttagcaaagagcctggttaatcccctcgagatcgggtggtgatcagagatgcgggcatccgtttc +f8v1s69 f2v8s26 40.000000 F 4505217 aagaccttgaacacctctaagcgaaaattggatatcgagtcagggaacataaccacgggtgcgt +f11v5s96 f2v8s26 44.000000 F 4505217 agcgttagcaaagagcctggttaatggccctcgaggatcggtggctgaccagagatgcgggcatccgtttc +f6v2s62 f2v8s26 40.000000 F 4505217 catgtcgagagactggaaccccggtgatgtgaaaaaaggtcttgggtacgtcccatggctac +f4v6s50 f2v8s26 40.000000 F 4505217 ctgtattcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactaagacgaagag +f9v0s76 f2v8s26 44.000000 F 4505217 gggatcgtggcaagcaacccttgccgcctcgactgcggctaaagacgcatgttccagtaataccta +f2v12s30 f2v8s26 64.000000 F 4505217 tggactagtagaccgtgtgccacaatcgacgctgaggacacgttgatttttatacggcgtctcag +f11v9s100 f2v8s26 45.000000 F 4505217 agcgttagcaaaggagcctggttaatcgccctcgaggatcggtggtgatcagagatgcgggcatccgtttc +f4v5s49 f2v8s26 40.000000 F 4505217 ctatattcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactaagagaagca +f2v9s27 f2v8s26 66.000000 F 4505217 tggactagtagaccgtgtgcctaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f7v3s67 f7v0s64 60.000000 F 114016 gcttcgtgggaccagagacaaccgacatatttcgaaccaccgacatacgatccttaaacc +f0v8s8 f2v8s26 47.000000 F 4505217 ggatcacagtcctacactgctcactccaaccccggccctgagtccgaggagagggtgcttcagagtatgtataccactg +f2v6s24 f2v8s26 63.000000 F 4505217 tggactagtagacgtgtgcctcatcgacgcgtgagacactttgatttcttatacggcgtctcag +f11v3s94 f2v8s26 46.000000 F 4505217 agcgttagcaaagagcctggttaatcgccgtcgaaggatcggtggtgatcagagatgcgggcatccgtttc +f6v3s63 f2v8s26 42.000000 F 4505217 catgtcaagagactggaaccccggtgatgtgaaaaaaggttttgggtcacgtctccatggctacg +f3v11s42 f2v8s26 44.000000 F 4505217 cgaaaaaggtgaatgaaagtctgggacgctcctggtttgggacgctctaggtgttgtggcttgaacgttagcacct +f4v4s48 f4v8s52 60.000000 F 254973 ctgtattcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactaagagaaa +f3v8s39 f2v8s26 43.000000 F 4505217 cgaaaaaggtgaatgaaaggcctgggacgctcctggttggggcgctctaggtgtgtggcttgaacgttagacct +f7v1s65 f7v0s64 60.000000 F 114016 gcttcggggaaccagagacaaccgacatatttcgaaccaccgacatacgatcctttaaacc +f5v1s54 f5v3s56 60.000000 F 229172 cggaacgcaccataagatggtaatcactaaaaaaatcggcctaacctgtccatatcgccactt +f4v7s51 f4v8s52 58.000000 F 254973 cgtttattcgtaggatctactacgcatcaaagtatcccacttttgtaccccactaagagcaga +f7v2s66 f7v0s64 59.000000 F 114016 gcttcgtgggaaccagaacaaccgacatattccgaaccaccggacatacgatccttaaacc +f1v6s15 f2v8s26 46.000000 F 4505217 gaatcagcttctaaccaagcgttgcagaaccggctactttaagcatgatgaatgcgtcgtaagtgatactcgacgattc +f5v2s55 f2v8s26 40.000000 F 4505217 cggcatcgcaccataagatggtaatcactaaaaaaatcggcctaacctgtccatatcgccact diff --git a/pkg/obitools/obiclust/testdata/families_shortest.expected b/pkg/obitools/obiclust/testdata/families_shortest.expected new file mode 100644 index 0000000..8b34382 --- /dev/null +++ b/pkg/obitools/obiclust/testdata/families_shortest.expected @@ -0,0 +1,101 @@ +f2v8s26 f2v8s26 1.000000 T 863655 tggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f10v3s80 f2v8s26 1.000000 F 863655 tggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f2v0s18 f2v8s26 1.000000 F 863655 tggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f2v7s25 f2v8s26 1.000000 F 863655 tggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f10v9s86 f2v8s26 1.000000 F 863655 tggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f2v3s21 f2v8s26 1.000000 F 863655 tggactagtagaccgtgtgcctcaatcgacgcggaggacacgttgatttcttatacggcgtctcag +f6v1s61 f2v8s26 1.000000 F 863655 tggactagtagaccgtgtgcctcaatcgacgcggaggacacgttgatttcttatacggcgtctcag +f2v2s20 f2v8s26 1.000000 F 863655 tggactagtagaccgtgtgcctcaatcgacgcggaggacacgttgatttcttatacggcgtctcag +f3v3s34 f3v3s34 1.000000 T 838714 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggtgttgtggcttgaacgttagcacct +f4v0s44 f3v3s34 1.000000 F 838714 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggtgttgtggcttgaacgttagcacct +f3v0s31 f3v3s34 1.000000 F 838714 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggtgttgtggcttgaacgttagcacct +f0v7s7 f0v7s7 1.000000 T 515632 ggatcacacgtctatcactgctcactccaaccccggcccttgacgtccgaggagagggtgcttctgagtatgtatacccaactg +f1v2s11 f0v7s7 1.000000 F 515632 ggatcacacgtctatcactgctcactccaaccccggcccttgacgtccgaggagagggtgcttctgagtatgtatacccaactg +f1v1s10 f0v7s7 1.000000 F 515632 ggatcacacgtctatcactgctcactccaaccccggcccttgacgtccgaggagagggtgcttctgagtatgtatacccaactg +f3v6s37 f3v3s34 0.973333 F 838714 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgtctctaggttgttgtggctgaacgtagcact +f5v0s53 f3v3s34 0.973333 F 838714 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgtctctaggttgttgtggctgaacgtagcact +f11v7s98 f3v3s34 0.973333 F 838714 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgtctctaggttgttgtggctgaacgtagcact +f2v4s22 f2v8s26 0.985075 F 863655 tggactagtagaccgtgtcgcctcaatcgacgcgtgaggacacgttgattcttatacggcgtctcag +f10v7s84 f10v7s84 1.000000 T 905994 gtgccggacactcgcaatcttcacaagcagaggtccagctaacacacagcagcatttagtcccgatgggtagc +f10v0s77 f10v7s84 0.972603 F 905994 ttgccggacactcgcaatcttccaagcagaggtccagctaacacacagcaggcatttagtcccgatgggtagc +f5v6s59 f2v8s26 1.000000 F 863655 ggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f2v10s28 f2v8s26 1.000000 F 863655 ggactagtagaccgtgtgcctcaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f1v5s14 f0v7s7 0.961538 F 515632 ggatcacagtctacactgctcactccagaccccgcccctgagtccgaggagagggtgcttcagagtatgttaccactg +f0v4s4 f0v7s7 0.961538 F 515632 ggatcacagtctacactgctcactccagaccccgcccctgagtccgaggagagggtgcttcagagtatgttaccactg +f0v1s1 f0v7s7 0.961538 F 515632 ggatcacagtctacactgctcactccagaccccgcccctgagtccgaggagagggtgcttcagagtatgttaccactg +f0v3s3 f0v7s7 0.961538 F 515632 ggatcacagtctacactgctcactccagaccccgcccctgagtccgaggagagggtgcttcagagtatgttaccactg +f5v3s56 f5v3s56 1.000000 T 192648 ccgcaacgcaccataagatgtaatcactaagaaaaatcggcctaacctgtcatatcgccactt +f10v2s79 f10v7s84 0.945205 F 905994 ttgccggtcactcgcaatcttaccaaccagaggtccagctaacacacagcaggcatttagtcccgatgggtagc +f10v13s90 f10v7s84 0.958333 F 905994 tgccggacactcgcaatctccaagcagagagtccaagctaacacacagcaggcatttgtcccgatgggtagc +f1v4s13 f1v4s13 1.000000 T 304149 gaatcagcttctcaaccaagcgatgcagaaccggctactttaagcattgatgaatgcgtcgtaagtgatactggacgattc +f10v4s81 f10v7s84 0.958904 F 905994 ttgccggacactggcaatcttccatagcagaggtccagctaacacacagcaggcatttagtcccgatgggtagc +f10v5s82 f10v7s84 0.958904 F 905994 ttgccggacactcgcaatcttccaagcagaggtccagctaacacatagcaggcatttagtcccgatgggtagc +f4v8s52 f4v8s52 1.000000 T 368537 ctgatattcgtattgatctactacgtatcaaagtatcccacttttgtgaccccactaagagaaga +f2v11s29 f2v8s26 0.970149 F 863655 tggactgtagaaccgtgtgcctcaatccacgcgtgaggacacgttgatttcttatacggcgtctcag +f0v6s6 f0v7s7 0.961039 F 515632 ggatcacagtctacactgctccctccaaccccggcccctggtccgaggagagggtgcttcagagtattataccactg +f1v3s12 f0v7s7 0.961039 F 515632 ggatcacagtctacactgctccctccaaccccggcccctggtccgaggagagggtgcttcagagtattataccactg +f0v0s0 f0v7s7 0.974684 F 515632 ggatcacagtctacactgctcactccaaccccggcccctgagtccgaggagagggtgcttcagagtatgtataccactg +f0v2s2 f0v7s7 0.974684 F 515632 ggatcacagtctacactgctcactccaaccccggcccctgagtccgaggagagggtgcttcagagtatgtataccactg +f1v7s16 f1v4s13 0.973684 F 304149 gaatcagcttcaaccaagcgatgcagaaccgctgctttaagcatgatgaatcgtcgtaagtgatactcgacgattc +f2v5s23 f1v4s13 0.973684 F 304149 gaatcagcttcaaccaagcgatgcagaaccgctgctttaagcatgatgaatcgtcgtaagtgatactcgacgattc +f3v12s43 f3v3s34 0.973684 F 838714 cgaaaaaggtgaatgaaagactgggacgcctcctggtttgggacgctctaggtggttgtggcttgaacgttagcacct +f8v0s68 f8v0s68 1.000000 T 507278 aagaccttgaacacctctaagcgaaaattggattcgagtcagggaacataaccacgggtgcgt +f2v1s19 f1v4s13 0.925000 F 304149 gaatcaggttcttccaagcgctgcagaaccggctactttaagcattgatgaatgcgtcagtaagtgatacccgacgattc +f1v8s17 f1v4s13 0.925000 F 304149 gaatcaggttcttccaagcgctgcagaaccggctactttaagcattgatgaatgcgtcagtaagtgatacccgacgattc +f10v11s88 f10v7s84 0.958904 F 905994 ttgccggacactcgcaatcttccaagcagaggtccagctaactcacagccaggcatttagtcccgatgggtagc +f3v5s36 f3v3s34 0.973684 F 838714 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggtgttatggcttgaaagttagcacct +f8v7s75 f8v0s68 0.984127 F 507278 aagaccttgaacacctctaagcgaaaattggattccagtcagggaaccataaccacgggtgcgt +f10v12s89 f10v7s84 0.958904 F 905994 ttgccggacactcgcaatcttccaagcagaggtccagctaacacacacgcaggcatttagacccgatgggtagc +f4v2s46 f4v8s52 0.968254 F 368537 ctgtattcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactaagagaaga +f8v2s70 f8v0s68 0.984127 F 507278 aagaccttgaacaacctctaagccgaaattggaattcgagtcagggaacataaccacgggtgcgat +f11v8s99 f11v8s99 1.000000 T 421503 agcgttagcaaagagcctggttaatctgccctcgaggatcggtggtgatcagagatgcgggcatccgtttc +f7v0s64 f7v0s64 1.000000 T 114016 gcttcgtgggaaccagagacaaccgacatatttcgaaccaccgacatacgatccttaaacc +f11v1s92 f11v8s99 0.971429 F 421503 agcgttagcaaagagcctggttcatcgccctcgaggatcggtggtgatcaagagatgcggcatccgtttc +f3v2s33 f3v3s34 0.973333 F 838714 cgaaaaaaggtaatgaaaggctgggatgctcctggtttggggcgctctaggtgttgtggcttgaacgttagcact +f8v3s71 f8v0s68 0.983607 F 507278 aagaccttgaacacctctaagcgaaattggattcgagtcagcgaacataacacgggtgcgt +f4v3s47 f4v8s52 0.967742 F 368537 ctgtattcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactaaagaaga +f10v10s87 f10v7s84 0.957746 F 905994 tctgccggacactcgcaatcttccaagcagaggtccagctaacacacgcaggcatttatccgatgggtagc +f11v0s91 f11v8s99 1.000000 F 421503 agcgttagcaaagagcctggttaatcgccctcgaggatcggtggtgatcagagatgcgggcatccgtttc +f8v5s73 f8v0s68 0.968254 F 507278 aagaccttgtacacctctaagcgataaattggattcgagtcagggaacataaccacggatgcgt +f8v4s72 f8v0s68 1.000000 F 507278 aagacttgaacacctctaagcgaaaattggattcgagtcagggaacataaccacgggtgcgt +f3v1s32 f3v3s34 1.000000 F 838714 cgaaaaagagtgaatgaaatggctgggacgctcctggtttggggcgctctaggtgttgtggcttgaacgttagcacct +f3v4s35 f3v3s34 0.973333 F 838714 cgaaaaaggtgaagaaaggctgggacgctcctggttggcgcgctctaggtgttgtggcttgaacgttagcaccta +f11v6s97 f11v8s99 0.957746 F 421503 agcgttagcaaagaacctggttaatcgccctcgaggatcggtggtggatcggagatgcgggcatcgcgtttc +f4v1s45 f4v8s52 0.938462 F 368537 ctgtatttcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactatagagaaga +f0v5s5 f0v7s7 0.948718 F 515632 ggatcacagtctacactgctcactcccccccggcccctgagtccgaggagagggtgcttcaaagtatgtataccactg +f5v5s58 f5v3s56 0.952381 F 192648 cgcaacgcaccattaagatggtaatcactaaaaaaatcggcctaacctgtccaatcgccactt +f1v0s9 f1v4s13 0.987500 F 304149 gaatcagcttctaaccaagcgatgcagaaccggctactttaagcattgatgaatgcgtcgtaagtgatactcgacgattc +f8v6s74 f8v0s68 1.000000 F 507278 aagacccttgaacacctctaagcgaaaattggattcgagtcagggaacataaccacgggtgccgt +f6v0s60 f6v0s60 1.000000 T 100983 catgtcaagagactggaaccccggtgatgtgaaaaaaggtcttgggtacgtcccatggctacg +f10v8s85 f10v7s84 0.945205 F 905994 ttgccggacactcgaatcttccaagcagaggtccagctaacacacagcaggcatactagtcccgatgggtagc +f10v1s78 f10v7s84 0.958904 F 905994 ttgccggacactcgcaatcctccaagcagaggtccagctaacacacagcaggcatttagtcccgatgaggtagc +f5v4s57 f5v3s56 0.968254 F 192648 cggcaacgcaccataagatggtaatcactaaaaaaatcggcctaacctgtccatatcgccactat +f3v10s41 f3v3s34 0.973684 F 838714 cgaaaataaggtgaatgaaagggtgggacgctcctagtttggggcgctctaggtgttgtggcttgaacgttagcacct +f3v7s38 f3v3s34 0.986842 F 838714 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggtgttcgtgcttgaacgttagcacct +f10v6s83 f10v7s84 0.958333 F 905994 ttccggacactcgcaatcttccaagcagaggtccagctaacacacagcaggcatttagtcctgatgggtagc +f11v2s93 f11v8s99 0.985714 F 421503 agcgttagcaaagagcctggttaatcgccctcggaggatcggtggtgatcagagatgcgggcatccgttc +f3v9s40 f3v3s34 0.986842 F 838714 cgaaaaaggtgaatgaaaggctgggacgctcctggtttggggcgctctaggggttgtggcttgaacgttagcacct +f11v4s95 f11v8s99 0.985507 F 421503 agcgttagcaaagagcctggttaatcccctcgagatcgggtggtgatcagagatgcgggcatccgtttc +f8v1s69 f8v0s68 1.000000 F 507278 aagaccttgaacacctctaagcgaaaattggatatcgagtcagggaacataaccacgggtgcgt +f11v5s96 f11v8s99 0.957746 F 421503 agcgttagcaaagagcctggttaatggccctcgaggatcggtggctgaccagagatgcgggcatccgtttc +f6v2s62 f6v0s60 0.983871 F 100983 catgtcgagagactggaaccccggtgatgtgaaaaaaggtcttgggtacgtcccatggctac +f4v6s50 f4v8s52 0.938462 F 368537 ctgtattcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactaagacgaagag +f9v0s76 f9v0s76 1.000000 T 29993 gggatcgtggcaagcaacccttgccgcctcgactgcggctaaagacgcatgttccagtaataccta +f2v12s30 f2v8s26 0.984615 F 863655 tggactagtagaccgtgtgccacaatcgacgctgaggacacgttgatttttatacggcgtctcag +f11v9s100 f11v8s99 0.985915 F 421503 agcgttagcaaaggagcctggttaatcgccctcgaggatcggtggtgatcagagatgcgggcatccgtttc +f4v5s49 f4v8s52 0.953125 F 368537 ctatattcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactaagagaagca +f2v9s27 f2v8s26 1.000000 F 863655 tggactagtagaccgtgtgcctaatcgacgcgtgaggacacgttgatttcttatacggcgtctcag +f7v3s67 f7v0s64 1.000000 F 114016 gcttcgtgggaccagagacaaccgacatatttcgaaccaccgacatacgatccttaaacc +f0v8s8 f0v7s7 0.974684 F 515632 ggatcacagtcctacactgctcactccaaccccggccctgagtccgaggagagggtgcttcagagtatgtataccactg +f2v6s24 f2v8s26 0.984375 F 863655 tggactagtagacgtgtgcctcatcgacgcgtgagacactttgatttcttatacggcgtctcag +f11v3s94 f11v8s99 0.971831 F 421503 agcgttagcaaagagcctggttaatcgccgtcgaaggatcggtggtgatcagagatgcgggcatccgtttc +f6v3s63 f6v0s60 0.984127 F 100983 catgtcaagagactggaaccccggtgatgtgaaaaaaggttttgggtcacgtctccatggctacg +f3v11s42 f3v3s34 0.973684 F 838714 cgaaaaaggtgaatgaaagtctgggacgctcctggtttgggacgctctaggtgttgtggcttgaacgttagcacct +f4v4s48 f4v8s52 0.967742 F 368537 ctgtattcgtagtgatctactacgcatcaaagtatcccacttttgtaccccactaagagaaa +f3v8s39 f3v3s34 0.986486 F 838714 cgaaaaaggtgaatgaaaggcctgggacgctcctggttggggcgctctaggtgtgtggcttgaacgttagacct +f7v1s65 f7v0s64 0.983607 F 114016 gcttcggggaaccagagacaaccgacatatttcgaaccaccgacatacgatcctttaaacc +f5v1s54 f5v3s56 0.952381 F 192648 cggaacgcaccataagatggtaatcactaaaaaaatcggcctaacctgtccatatcgccactt +f4v7s51 f4v8s52 0.920635 F 368537 cgtttattcgtaggatctactacgcatcaaagtatcccacttttgtaccccactaagagcaga +f7v2s66 f7v0s64 0.967213 F 114016 gcttcgtgggaaccagaacaaccgacatattccgaaccaccggacatacgatccttaaacc +f1v6s15 f1v4s13 0.974684 F 304149 gaatcagcttctaaccaagcgttgcagaaccggctactttaagcatgatgaatgcgtcgtaagtgatactcgacgattc +f5v2s55 f5v3s56 0.936508 F 192648 cggcatcgcaccataagatggtaatcactaaaaaaatcggcctaacctgtccatatcgccact From e511650f5b3b20a430d7e13a980f9d31b12e28c6 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Tue, 3 Nov 2026 09:22:32 +1100 Subject: [PATCH 10/24] Refactor similarity engine and implement parallel clustering Introduces parallel processing for clustering, refines scoring mechanisms, and expands test coverage for the alignment functionality. This change introduces parallel processing capabilities within the clustering logic, updates the core similarity engine with new data structures and scoring methods, and implements a comprehensive test suite for the alignment tool. It also adds new command-line options for configuring alignment parameters and reference lengths. --- Makefile | 15 +- OBIPRIMERS_NOTES.md | 183 +++ cmd/obitools/obialign/main.go | 26 + obitests/obitools/obialign/test.sh | 206 ++++ pkg/obialign/lcsmin.go | 5 +- pkg/obiformats/batch_of_files_reader.go | 8 + pkg/obiformats/batch_of_files_reader_test.go | 38 + .../obiclust => obisimilarity}/block.go | 20 +- .../block_nosimd.go | 4 +- .../obiclust => obisimilarity}/block_simd.go | 6 +- pkg/obisimilarity/entry.go | 95 ++ pkg/obisimilarity/index.go | 106 ++ pkg/obisimilarity/obisimilarity_test.go | 233 ++++ pkg/obisimilarity/scoring.go | 284 +++++ pkg/obisimilarity/table4.go | 54 + pkg/obisimilarity/table4_nosimd.go | 14 + .../table4_simd.go} | 16 +- pkg/obithermo/model.go | 222 ++++ pkg/obithermo/model_test.go | 203 ++++ pkg/obithermo/nn_table.go | 126 ++ pkg/obithermo/testdata/c_reference.tsv | 400 ++++++ pkg/obithermo/testdata/ecoprimers_tm.tsv | 13 + pkg/obitools/obialign/obialign.go | 340 ++++++ pkg/obitools/obialign/obialign_test.go | 271 +++++ pkg/obitools/obialign/options.go | 132 ++ pkg/obitools/obialign/testdata/cases.txt | 24 + .../obialign/testdata/one_all.expected | 903 ++++++++++++++ .../obialign/testdata/one_alldist.expected | 903 ++++++++++++++ .../obialign/testdata/one_allraw.expected | 903 ++++++++++++++ .../testdata/one_allshortest.expected | 903 ++++++++++++++ .../obialign/testdata/one_default.expected | 224 ++++ .../obialign/testdata/one_distance.expected | 224 ++++ .../obialign/testdata/one_longest.expected | 249 ++++ .../obialign/testdata/one_low.expected | 249 ++++ .../obialign/testdata/one_rawdist.expected | 161 +++ .../obialign/testdata/one_rawlcs.expected | 454 +++++++ .../testdata/one_rawlcs_shortest.expected | 249 ++++ .../obialign/testdata/one_shortest.expected | 249 ++++ .../obialign/testdata/two_all.expected | 1075 +++++++++++++++++ .../obialign/testdata/two_alldist.expected | 1075 +++++++++++++++++ .../obialign/testdata/two_allraw.expected | 1075 +++++++++++++++++ .../testdata/two_allshortest.expected | 1075 +++++++++++++++++ .../obialign/testdata/two_default.expected | 0 .../obialign/testdata/two_distance.expected | 0 .../obialign/testdata/two_longest.expected | 0 .../obialign/testdata/two_low.expected | 0 .../obialign/testdata/two_rawdist.expected | 0 .../obialign/testdata/two_rawlcs.expected | 265 ++++ .../testdata/two_rawlcs_shortest.expected | 0 .../obialign/testdata/two_shortest.expected | 0 pkg/obitools/obiclust/centers.go | 136 --- pkg/obitools/obiclust/cluster.go | 35 +- pkg/obitools/obiclust/kmer4.go | 47 - pkg/obitools/obiclust/kmer4_nosimd.go | 14 - pkg/obitools/obiclust/kmer4_test.go | 71 -- pkg/obitools/obiclust/obiclust.go | 99 +- pkg/obitools/obiclust/obiclust_test.go | 153 +-- pkg/obitools/obiclust/options.go | 21 +- pkg/obitools/obiclust/prepare.go | 46 +- pkg/obitools/obiclust/scan.go | 63 + pkg/obitools/obiclust/score.go | 233 ---- 61 files changed, 13397 insertions(+), 801 deletions(-) create mode 100644 OBIPRIMERS_NOTES.md create mode 100644 cmd/obitools/obialign/main.go create mode 100755 obitests/obitools/obialign/test.sh create mode 100644 pkg/obiformats/batch_of_files_reader_test.go rename pkg/{obitools/obiclust => obisimilarity}/block.go (66%) rename pkg/{obitools/obiclust => obisimilarity}/block_nosimd.go (69%) rename pkg/{obitools/obiclust => obisimilarity}/block_simd.go (80%) create mode 100644 pkg/obisimilarity/entry.go create mode 100644 pkg/obisimilarity/index.go create mode 100644 pkg/obisimilarity/obisimilarity_test.go create mode 100644 pkg/obisimilarity/scoring.go create mode 100644 pkg/obisimilarity/table4.go create mode 100644 pkg/obisimilarity/table4_nosimd.go rename pkg/{obitools/obiclust/kmer4_simd.go => obisimilarity/table4_simd.go} (65%) create mode 100644 pkg/obithermo/model.go create mode 100644 pkg/obithermo/model_test.go create mode 100644 pkg/obithermo/nn_table.go create mode 100644 pkg/obithermo/testdata/c_reference.tsv create mode 100644 pkg/obithermo/testdata/ecoprimers_tm.tsv create mode 100644 pkg/obitools/obialign/obialign.go create mode 100644 pkg/obitools/obialign/obialign_test.go create mode 100644 pkg/obitools/obialign/options.go create mode 100644 pkg/obitools/obialign/testdata/cases.txt create mode 100644 pkg/obitools/obialign/testdata/one_all.expected create mode 100644 pkg/obitools/obialign/testdata/one_alldist.expected create mode 100644 pkg/obitools/obialign/testdata/one_allraw.expected create mode 100644 pkg/obitools/obialign/testdata/one_allshortest.expected create mode 100644 pkg/obitools/obialign/testdata/one_default.expected create mode 100644 pkg/obitools/obialign/testdata/one_distance.expected create mode 100644 pkg/obitools/obialign/testdata/one_longest.expected create mode 100644 pkg/obitools/obialign/testdata/one_low.expected create mode 100644 pkg/obitools/obialign/testdata/one_rawdist.expected create mode 100644 pkg/obitools/obialign/testdata/one_rawlcs.expected create mode 100644 pkg/obitools/obialign/testdata/one_rawlcs_shortest.expected create mode 100644 pkg/obitools/obialign/testdata/one_shortest.expected create mode 100644 pkg/obitools/obialign/testdata/two_all.expected create mode 100644 pkg/obitools/obialign/testdata/two_alldist.expected create mode 100644 pkg/obitools/obialign/testdata/two_allraw.expected create mode 100644 pkg/obitools/obialign/testdata/two_allshortest.expected create mode 100644 pkg/obitools/obialign/testdata/two_default.expected create mode 100644 pkg/obitools/obialign/testdata/two_distance.expected create mode 100644 pkg/obitools/obialign/testdata/two_longest.expected create mode 100644 pkg/obitools/obialign/testdata/two_low.expected create mode 100644 pkg/obitools/obialign/testdata/two_rawdist.expected create mode 100644 pkg/obitools/obialign/testdata/two_rawlcs.expected create mode 100644 pkg/obitools/obialign/testdata/two_rawlcs_shortest.expected create mode 100644 pkg/obitools/obialign/testdata/two_shortest.expected delete mode 100644 pkg/obitools/obiclust/centers.go delete mode 100644 pkg/obitools/obiclust/kmer4.go delete mode 100644 pkg/obitools/obiclust/kmer4_nosimd.go delete mode 100644 pkg/obitools/obiclust/kmer4_test.go create mode 100644 pkg/obitools/obiclust/scan.go delete mode 100644 pkg/obitools/obiclust/score.go diff --git a/Makefile b/Makefile index 96a0768..cb00a3e 100644 --- a/Makefile +++ b/Makefile @@ -44,13 +44,18 @@ OBITOOLS_DIRS:=$(sort $(patsubst %/,%,$(dir $(OBITOOLS_SRC)))) OBITOOLS:=$(notdir $(OBITOOLS_DIRS)) +# Name of the target of a package: its path under pkg/, so that two packages of +# the same name in different directories (pkg/obialign and +# pkg/obitools/obialign) do not share a target. +pkgname=$(subst /,-,$(patsubst pkg/%,%,$(1))) + define MAKE_PKG_RULE -pkg-$(notdir $(1)): $(1) pkg/obioptions/version.go - @echo -n - Building package $(notdir $(1))... +pkg-$(call pkgname,$(1)): $(1) pkg/obioptions/version.go + @echo -n - Building package $(call pkgname,$(1))... @$(GOBUILD) ./$(1) \ - 2> pkg-$(notdir $(1)).log \ - || cat pkg-$(notdir $(1)).log - @rm -f pkg-$(notdir $(1)).log + 2> pkg-$(call pkgname,$(1)).log \ + || cat pkg-$(call pkgname,$(1)).log + @rm -f pkg-$(call pkgname,$(1)).log @echo Done. endef diff --git a/OBIPRIMERS_NOTES.md b/OBIPRIMERS_NOTES.md new file mode 100644 index 0000000..196f647 --- /dev/null +++ b/OBIPRIMERS_NOTES.md @@ -0,0 +1,183 @@ +# obiprimers — notes de travail + +Ré-implémentation d'ecoPrimers dans obitools4. Ce fichier garde l'état des recherches, +des mesures et de l'avancement. Dernière mise à jour : 2026-11-03. + +Sources de référence : +- Article : `~/Sync/travail/__MOI__/ecoprimers/Documents/Riaz et al. 2011 ... .pdf` (NAR 39:e145) +- Code C : `~/Sync/travail/__MOI__/ecoprimers/src/` (binaire : `/usr/local/bin/ecoprimers`) +- Tutoriel et données : `~/Documents/Enseignement/DNA metabarcoding spring school/ecoPrimers-Fish/` + (`fish-primers.qmd`, `vertebrata.*` au format ecoPCR, `mito.vert.fasta.gz`, + `ncbitaxo_20250211.tgz`, sorties de référence `Teleostei.ecoprimers`, `Cyprinidae.ecoprimers`) + +## 1. Algorithme d'ecoPrimers (lu dans le code) + +1. **Mots stricts** (`strictprimers.c`, `filtering.c`) : mots de longueur `lp` (18 par défaut, <= 31) + encodés 2 bits/base, forme canonique `min(D,R)`, dédupliqués par séquence, présents sans + erreur dans >= 70 % des séquences exemples (`-q`). Pré-filtre : table de comptage des 13-mers + (4^13 entrées) qui écarte la plupart des mots avant la fusion triée (smoothsort + merge). +2. **Recherche approchée** (`aproxpattern.c`, `apat_search.c`) : chaque mot strict est cherché sur + les 2 brins, <= `e` substitutions (Manber/agrep bit-parallèle), dans toutes les séquences + (exemples et contre-exemples). Une amorce est « good » si elle touche >= 90 % des exemples + (`-s`) et <= 10 % des contre-exemples (`-x`). Les amorces seulement « conservées » restent + utilisables : une paire n'a besoin que d'une amorce « good ». `omask` vaut 0 dans cette + recherche : l'option `-3` est vérifiée plus tard, au niveau de la paire. +3. **Appariement** (`pairs.c`) : par séquence, occurrences triées par position ; paires de + distance dans (`lmin`, `lmax`) avec au moins une amorce « good » ; clé de paire canonique, + stockage dans un arbre (`tsearch`) avec un amplicon par occurrence. +4. **Indices** (`taxstats.c`) : Bc = taxons exemples amplifiés / taxons exemples (au rang `-t`, + espèce par défaut) ; Bs = taxons dont l'amplicon n'est partagé avec aucun autre taxon / + taxons exemples ; yule = in/|E| - out/|C| ; tri sur yule x Bs ; seuil `-T` sur Bs (0,6). +5. **Tm** (`libthermo`) : plus proches voisins, sel SantaLucia ou Owczarzy ; Tm exact et pire cas. + +Faiblesses relevées (à ne pas reproduire) : +- Recherche approchée mot par mot sur toutes les séquences : c'est le goulot en temps. +- Un amplicon stocké par occurrence et par paire : c'est le goulot en mémoire. +- `taxonomycoverage` et `taxonomyspecificity` rebalayent toute la base à chaque paire. +- Mots <= 31 bases ; sortie en tableau figé de 21 colonnes. + +Non lus (à vérifier avant de les porter ou de les écarter) : `merge.c`, `smothsort.c`, +`ahocorasick.c`, `thermostats.c`, `nnparams.c`, `PrimerSets.c` (sets d'amorces, désactivé dans le +`main` : l'option `-p` est commentée). + +## 2. Briques obitools4 réutilisables + +| Besoin | Brique | +|---|---| +| Lecture, formats, batch, parallélisme | `obiformats`, `obiiter`, `obidefault` | +| Taxonomie, rangs, sous-clades | `obitax`, `obiseq/taxonomy_*`, options `--taxonomy`, `--restrict-to-taxon`, `--ignore-taxon` | +| K-mers canoniques (k <= 31) | `obikmer.EncodeCanonicalKmers`, `IterCanonicalKmers`, `ReverseComplement` | +| Comptage par partition, quorum, différence | `obikmer.KmerSetGroupBuilder`, `QuorumAtLeast/AtMost`, `Difference` | +| Contraintes sur amorces (GC, runs, IUPAC) | `obikmer.KmerConstraints` | +| Recherche approchée (indels, 2 brins) | `obiapat.ApatPattern` (cgo) | +| Distances entre mots | `obikmer.HammingDistance`, `KmerLCSBatch` (SIMD) | +| Alignement borné | `obialign.BandedLCS` | +| Patterns de CLI | `obipcr`, `obioligotag`, `obiclust` | + +**Aucun calcul de Tm n'existe** dans obitools4 (grep santalucia / nearest-neighbor / meltingtemp : +rien). D'où le nouveau paquet générique `pkg/obithermo`. + +SIMD : opt-in (`GOEXPERIMENT=simd`), repli scalaire portable. Même règle pour tout nouveau noyau. + +## 3. Mesures de référence d'ecoPrimers (cette machine) + +Commande : `ecoprimers -d vertebrata -e 3 -3 2 -l 30 -L 150 -c -r `, deux processus +lancés en parallèle, un cÅ“ur chacun. Sorties dans `/tmp/ecoprimers-bench/` (temporaire). + +| Jeu | Séquences | Paires totales | Paires retenues | Temps CPU | Mémoire max | +|---|---|---|---|---|---| +| Cyprinidae (`-r 7953`) | 325 | 42 682 | 3 448 | 134 s | 1,86 Go | +| Teleostei (`-r 32443`) | 3 909 | 9 407 | 407 | 350 s | 2,11 Go | + +Les sorties sont **identiques** (diff sur les lignes hors commentaires) à +`Cyprinidae.ecoprimers` et `Teleostei.ecoprimers` du tutoriel. Ce sont les cibles de non-régression du +mode A. + +Observation : Cyprinidae a 12 fois moins de séquences mais 4,5 fois plus de paires que +Teleostei, pour une mémoire comparable. Le goulot mémoire est donc le nombre de paires et le +stockage des amplicons, pas la taille de la base. + +## 4. Sémantique de `-r`, `-i`, `-E` (testée, cas Salmo, taxid 8028) + +`readdnadb.c` ne garde que les séquences qui sont exemples ou contre-exemples. + +| Options | Exemples | Contre-exemples | +|---|---|---| +| `-r 8028` | 7 (6 espèces) | 0 : tout le reste est **supprimé** | +| `-r 8028 -i 7742` | 7 | 7 815 (7 697 espèces) : tous les autres vertébrés | +| `-r 8028 -i 8015` | 7 | 76 (69 espèces) : les autres Salmonidae | +| `-r 8015 -E 8028` | 76 | 7 : sens inverse, Salmo devient contre-exemple | + +Avec `-r 8028 -i 8015` : 15 534 mots stricts (quorum 4/7 sur 7 séquences seulement), et un quorum +de contre-exemples de 7/76 (le défaut `-x 0,1` laisse passer 9 % du groupe proche, trop peu +exigeant pour une amorce spécifique). Les runs *Salmo* n'ont pas été menés jusqu'au bout (arrêtés +après l'en-tête). + +Pièges de la base : *Salmostoma bacalia* est un cyprinidé (nom en « Salmo », taxid différent) : filtrer +par taxid, jamais par nom. Le tutoriel annonce 7 823 génomes ; `mito.vert.fasta.gz` en contient 7 822. + +## 5. Plan d'implémentation + +Paquets prévus : +- `pkg/obithermo` : plus proches voisins, corrections de sel, Tm exact et pire cas. Générique. +- `pkg/obiprimer` : cÅ“ur (`words.go`, `match.go`, `pairs.go`, `score.go`, `types.go`). +- `pkg/obitools/obiprimers` : `options.go`, `obiprimers.go`, `output.go`. +- `cmd/obitools/obiprimers/main.go`. + +Deux modes : +- **A, conservé** (ecoPrimers) : k-mers présents dans >= q séquences exemples, via les partitions + disque d'obikmer (remplace tri en place, smoothsort, table 4^13) ; recherche approchée par + `obiapat` d'abord (version fidèle), optimisation par index seulement si elle est mesurée plus + rapide. +- **B, spécifique** (une espèce ou quelques espèces proches) : ancres = k-mers de T absents de C + (`Difference`) plus k-mers à distance de Hamming >= d de tout k-mer de C (mismatches en 3' + plus discriminants) ; paires entre ancres spécifiques, ou ancre spécifique + amorce conservée. + Le critère « good » est inversé. Définition précise de « spécifique » à valider avant le code. + Cas de test : *Salmo* (cible) contre le reste des Salmonidae. + +Sortie : **CSV** comme tous les obitools, ordre des colonnes d'ecoPrimers, en-têtes nommés. +Génomes circulaires (`-c`) gérés de bout en bout. Validation aval : `obipcr -e 5` sur chaque +paire, comme dans le tutoriel. + +Ordre : +1. `obithermo` (tests contre `CalculTM.xls` et colonnes Tm de `Teleostei.ecoprimers`) +2. Mode A, mots stricts sur `KmerSetGroup` (liste identique à ecoPrimers) +3. Recherche approchée (`obiapat`, puis index si mesuré utile) +4. Appariement + indices (index taxon -> séquences calculé une fois) +5. CLI et sortie CSV +6. Mode B (spécification validée d'abord) +7. Parallélisme (`Split()`, `obidefault.ParallelWorkers()`) +8. Hors v1 : sets d'amorces (`PrimerSets.c`), options `-b/-M/-k`. + +Règles du projet à respecter : fichiers < 500 lignes, pas de commit sans demande, pas de +matérialisation complète en mémoire quand le flux suffit, pas de réimplémentation de l'existant. + +## 6. Avancement + +- [x] Lecture de l'article et des sources C principales +- [x] Inventaire des briques obitools4 +- [x] Mesures de référence ecoPrimers (Cyprinidae, Teleostei) : identiques aux références +- [x] Sémantique `-r/-i/-E` testée sur *Salmo* +- [x] `obithermo` (`pkg/obithermo`) : terminé, tests verts, voir section 8 +- [ ] Mode A +- [ ] Mode B + +## 7. Questions ouvertes + +- Mode B : critère « spécifique » strict (T contre C) ou score continu (distance minimale en 3' à C) ? +- Mode B : valeur par défaut de `-x` (0,1 d'ecoPrimers trop permissive pour une cible étroite) ? +- Recherche approchée : l'index par voisinage bat-il agrep ? À mesurer, pas à supposer. + +## 8. `pkg/obithermo` (étape 1, terminée) + +Fichiers : `model.go` (modèle, ~230 lignes), `nn_table.go` (112 paramètres, **généré** depuis +`nnparams.c` par un script jetable hors dépôt : `/tmp/obithermo-gen/gen.py`), `model_test.go`, +`testdata/c_reference.tsv` (400 cas calculés par la vraie libthermo C compilée), +`testdata/ecoprimers_tm.tsv` (13 amorces et Tm imprimés par ecoPrimers sur Teleostei). + +API : `NewModel(primerConc, targetConc, salt, method)`, `DefaultModel()`, `(*Model).SelfTm(primer)`, +`(*Model).Tm(primer, site)` (site = segment de la cible lu dans l'orientation de l'amorce), +`GCCount(seq)`. Retour en degrés Celsius, `NaN` si le modèle ne prédit aucun duplex stable. +Modèle immuable, sans allocation (17,7 ns par appel `Tm`, M3 Max), sûr en concurrence. + +Validation : +- 400 cas aléatoires (amorces de 12 à 25 b, 0 à n mismatches, deux jeux de conditions) : + écart maximal avec le C ~5e-6 K. Cause vérifiée : le C écrit ses constantes en `float` + (`-21.3f`, `1.987f`, `-5.9f`). En émulant ces arrondis en float32, l'écart tombe à 7e-13 K. + Le port est donc fidèle ; il utilise des constantes décimales exactes. Tolérance de test : 1e-4 K. +- Les 13 Tm imprimés par ecoPrimers sont retrouvés à +/- 0,05 °C près. + +Écarts volontaires avec ecoPrimers (à confirmer) : +1. **273,15 au lieu de 273,0** : ecoPrimers calcule `Tm(K) - 273.0`. Les Tm de `obithermo` sont donc + 0,15 °C plus bas que ceux d'ecoPrimers (le test tient compte du décalage). Une constante, + facile à inverser si la compatibilité exacte des colonnes Tm prime. +2. **Owczarzy corrigé** : dans ecoPrimers, `gcContent` n'est jamais affecté (grep sur tout `src/`), + donc `-m 2` lit de la mémoire non initialisée (`CNNParams` est sur la pile dans `main`). + `obithermo` utilise la fraction GC de l'amorce. Non comparable au C ; testé par invariants + (les deux corrections s'annulent à 1 M, le sel augmente la Tm). +3. **Pas de gaps, bulges ni extrémités pendantes** : jamais atteints par `SelfTM`/`TwoTM` (les + séquences sont nettoyées en ACGT). Bases non ACGT : erreur, alors que le C lit hors tableau. +4. Tm minimale d'un modèle en échec : `NaN`, là où le C renvoie 0 K (soit -273 °C affiché). + +Pour plus tard : les k-mers d'obikmer sont codés A,C,G,T = 0..3, comme les tables de `obithermo` ; +une variante `TmCode(uint64, k)` évitera le décodage quand le cÅ“ur appellera Tm par paire. diff --git a/cmd/obitools/obialign/main.go b/cmd/obitools/obialign/main.go new file mode 100644 index 0000000..77d18bb --- /dev/null +++ b/cmd/obitools/obialign/main.go @@ -0,0 +1,26 @@ +package main + +import ( + "os" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obioptions" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obitools/obialign" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obitools/obiconvert" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiutils" +) + +func main() { + optionParser := obioptions.GenerateOptionParser( + "obialign", + "pairwise similarity of nucleotide sequences, as a CSV table (the principle of sumatra)", + obialign.OptionSet) + + _, args := optionParser(os.Args) + + fs, err := obiconvert.CLIReadBioSequences(args...) + obiconvert.OpenSequenceDataErrorMessage(args, err) + + obialign.CLIWriteTable(obialign.CLIOBIAlign(fs)) + + obiutils.WaitForLastPipe() +} diff --git a/obitests/obitools/obialign/test.sh b/obitests/obitools/obialign/test.sh new file mode 100755 index 0000000..aa2b556 --- /dev/null +++ b/obitests/obitools/obialign/test.sh @@ -0,0 +1,206 @@ +#!/bin/bash + +# +# Here give the name of the test serie +# + +TEST_NAME=obialign +CMD=obialign + +###### +# +# Some variable and function definitions: please don't change them +# +###### +TEST_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" +OBITOOLS_DIR="${TEST_DIR/obitest*/}build" +export PATH="${OBITOOLS_DIR}:${PATH}" + +MCMD="$(echo "${CMD:0:4}" | tr '[:lower:]' '[:upper:]')$(echo "${CMD:4}" | tr '[:upper:]' '[:lower:]')" + +TMPDIR="$(mktemp -d)" +ntest=0 +success=0 +failed=0 + +cleanup() { + echo "========================================" 1>&2 + echo "## Results of the $TEST_NAME tests:" 1>&2 + + echo 1>&2 + echo "- $ntest tests run" 1>&2 + echo "- $success successfully completed" 1>&2 + echo "- $failed failed tests" 1>&2 + echo 1>&2 + echo "Cleaning up the temporary directory..." 1>&2 + echo 1>&2 + echo "========================================" 1>&2 + + rm -rf "$TMPDIR" # Suppress the temporary directory + + if [ $failed -gt 0 ]; then + log "$TEST_NAME tests failed" + log + log + exit 1 + fi + + log + log + + exit 0 +} + +log() { + echo -e "[$TEST_NAME @ $(date)] $*" 1>&2 +} + +log "Testing $TEST_NAME..." +log "Test directory is $TEST_DIR" +log "obitools directory is $OBITOOLS_DIR" +log "Temporary directory is $TMPDIR" +log "files: $(find $TEST_DIR | awk -F'/' '{print $NF}' | tail -n +2)" + +###################################################################### +#### +#### Below are the tests +#### +#### Before each test : +#### - increment the variable ntest +#### +#### Run the command as the condition of an if / then /else +#### - The command must return 0 on success +#### - The command must return an exit code different from 0 on failure +#### - The datafiles are stored in the same directory than the test script +#### - The test script directory is stored in the TEST_DIR variable +#### - If result files have to be produced they must be stored +#### in the temporary directory (TMPDIR variable) +#### +#### then clause is executed on success of the command +#### - Write a success message using the log function +#### - increment the variable success +#### +#### else clause is executed on failure of the command +#### - Write a failure message using the log function +#### - increment the variable failed +#### +###################################################################### + + + +((ntest++)) +if $CMD -h > "${TMPDIR}/help.txt" 2>&1 +then + log "$MCMD: printing help OK" + ((success++)) +else + log "$MCMD: printing help failed" + ((failed++)) +fi + + +# +# Dataset used by the following tests: two families of similar sequences, a +# sequence identical to another one, and a sequence with an ambiguous +# nucleotide that is discarded. +# +cat > "${TMPDIR}/data.fasta" <a1 count=20; +acgtacgtacgtaacctagcctagcctagggatcgatcgatcgatcgaatcg +>a2 count=3; +acgtacgtacgtaacctagcctagcctagggatcgatcgatcgttcgaatcg +>a3 count=2; +acgtacgtacgtaacctagcctagcctagggatcgatcgatcgatcgaatcg +>b1 count=15; +ttgcatgcatgctagctagctagcatgcatcgatcgatgctagctagtagct +>b2 count=1; +ttgcatgcatgctagctagctagcatgcatcgatcgatgctagctagtaact +>x1 count=4; +ttgcatgcatgctangctagctagcatgcatcgatcgatgctagctagtagc +DATA + +cat > "${TMPDIR}/ref.fasta" <r1 +acgtacgtacgtaacctagcctagcctagggatcgatcgatcgatcgaatcg +>r2 +ttgcatgcatgctagctagctagcatgcatcgatcgatgctagctagtagct +>r3 +ggggccccggggccccggggccccggggccccggggccccggggcccc +DATA + +((ntest++)) +if $CMD --max-cpu 2 "${TMPDIR}/data.fasta" > "${TMPDIR}/all.csv" 2> /dev/null && + [ "$(head -n 1 "${TMPDIR}/all.csv")" = "id1,id2,score" ] && + [ "$(tail -n +2 "${TMPDIR}/all.csv" | wc -l | tr -d ' ')" = "10" ] +then + log "$MCMD: without threshold all the pairs of the 5 usable sequences are written OK" + ((success++)) +else + log "$MCMD: the table without threshold is wrong" + ((failed++)) +fi + +((ntest++)) +if $CMD --max-cpu 2 -t 0.95 "${TMPDIR}/data.fasta" > "${TMPDIR}/t95.csv" 2> /dev/null && + [ "$(tail -n +2 "${TMPDIR}/t95.csv" | cut -d, -f1,2 | tr '\n' ' ')" = "a1,a2 a1,a3 a2,a3 b1,b2 " ] && + grep -q '^a1,a3,1$' "${TMPDIR}/t95.csv" +then + log "$MCMD: only the pairs reaching the threshold are written, in order OK" + ((success++)) +else + log "$MCMD: the table with a threshold is wrong" + ((failed++)) +fi + +((ntest++)) +if $CMD --max-cpu 2 -t 0.95 -x "${TMPDIR}/data.fasta" 2> /dev/null | head -n 2 | tail -n 1 | grep -q '^a1,a2,[0-9.]*,20,3,52,52$' +then + log "$MCMD: extra columns OK" + ((success++)) +else + log "$MCMD: extra columns failed" + ((failed++)) +fi + +((ntest++)) +if $CMD --max-cpu 2 -g -t 0.9 "${TMPDIR}/data.fasta" 2> /dev/null | grep -q '^b1,x1,' +then + log "$MCMD: a sequence with an IUPAC letter is kept with --accept-ambiguous OK" + ((success++)) +else + log "$MCMD: --accept-ambiguous failed" + ((failed++)) +fi + +((ntest++)) +if $CMD --max-cpu 2 -R "${TMPDIR}/ref.fasta" -t 0.95 "${TMPDIR}/data.fasta" 2> /dev/null | tail -n +2 | cut -d, -f1,2 | tr '\n' ' ' > "${TMPDIR}/two.txt" && + [ "$(cat "${TMPDIR}/two.txt")" = "a1,r1 a2,r1 a3,r1 b1,r2 b2,r2 " ] +then + log "$MCMD: comparison with a reference file OK" + ((success++)) +else + log "$MCMD: comparison with a reference file failed" + ((failed++)) +fi + +((ntest++)) +if $CMD --max-cpu 1 "${TMPDIR}/data.fasta" 2> /dev/null > "${TMPDIR}/cpu1.csv" && + $CMD --max-cpu 8 "${TMPDIR}/data.fasta" 2> /dev/null > "${TMPDIR}/cpu8.csv" && + cmp -s "${TMPDIR}/cpu1.csv" "${TMPDIR}/cpu8.csv" +then + log "$MCMD: the table does not depend on the number of threads OK" + ((success++)) +else + log "$MCMD: the table depends on the number of threads" + ((failed++)) +fi + + +######################################### +# +# At the end of the tests +# the cleanup function is called +# +######################################### + +cleanup diff --git a/pkg/obialign/lcsmin.go b/pkg/obialign/lcsmin.go index 93f00f9..409e8df 100644 --- a/pkg/obialign/lcsmin.go +++ b/pkg/obialign/lcsmin.go @@ -62,8 +62,9 @@ func bandedLCSAtLeast(a, b []byte, minLCS int, ws *bandedLCSWorkspace, useSIMD b } // The score alone is much cheaper than the score with the length of the - // alignment: the latter is only computed when the pair reaches minLCS. - if lcsScoreSIMD(a, b, kmin, kmax, minLCS, ws) < minLCS { + // alignment: the latter is only computed when the pair reaches minLCS. With + // no minimum there is nothing to reject, and the pass would be wasted. + if minLCS > 0 && lcsScoreSIMD(a, b, kmin, kmax, minLCS, ws) < minLCS { return -1, -1 } encodeBases(ws, a, b) diff --git a/pkg/obiformats/batch_of_files_reader.go b/pkg/obiformats/batch_of_files_reader.go index ff464fb..4278a8f 100644 --- a/pkg/obiformats/batch_of_files_reader.go +++ b/pkg/obiformats/batch_of_files_reader.go @@ -48,6 +48,14 @@ func ReadSequencesBatchFromFiles(filenames []string, log.Printf("Start reading of file : %s", filename) + // The batches of a file reach us in any order (parallel parsing + // of the headers). They are numbered here in arrival order, so + // the order of the file must be restored first, unless the + // order of the data is not wanted (several concurrent readers). + if concurrent_readers == 1 { + iter = iter.SortBatches() + } + for iter.Next() { batch := iter.Get() batchiter.Push(batch.Reorder(nextCounter())) diff --git a/pkg/obiformats/batch_of_files_reader_test.go b/pkg/obiformats/batch_of_files_reader_test.go new file mode 100644 index 0000000..0ddfd5b --- /dev/null +++ b/pkg/obiformats/batch_of_files_reader_test.go @@ -0,0 +1,38 @@ +package obiformats + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "testing" +) + +// With a single reader, the sequences of several files must come out in the +// order of the files, whatever the order in which the batches are parsed. +func TestReadSequencesBatchFromFilesKeepsOrder(t *testing.T) { + dir := t.TempDir() + var names []string + for _, prefix := range []string{"a", "b"} { + var sb strings.Builder + for i := 0; i < 25000; i++ { + fmt.Fprintf(&sb, ">%s%07d {\"k\":%d}\n%s\n", prefix, i, i, strings.Repeat("acgt", 40)) + } + name := filepath.Join(dir, prefix+".fasta") + if err := os.WriteFile(name, []byte(sb.String()), 0o600); err != nil { + t.Fatal(err) + } + names = append(names, name) + } + + it := ReadSequencesBatchFromFiles(names, nil, 1) + _, db := it.Load() + if len(db) != 50000 { + t.Fatalf("expected 50000 sequences, got %d", len(db)) + } + for i := 1; i < len(db); i++ { + if db[i-1].Id() >= db[i].Id() { + t.Fatalf("out of order at %d: %s then %s", i, db[i-1].Id(), db[i].Id()) + } + } +} diff --git a/pkg/obitools/obiclust/block.go b/pkg/obisimilarity/block.go similarity index 66% rename from pkg/obitools/obiclust/block.go rename to pkg/obisimilarity/block.go index 487b5a5..ca86ea3 100644 --- a/pkg/obitools/obiclust/block.go +++ b/pkg/obisimilarity/block.go @@ -1,10 +1,10 @@ -package obiclust +package obisimilarity -// A block stores the 4-mer tables of a few centers, transposed: the counter +// A block stores the 4-mer tables of a few sequences, transposed: the counter // of the word w for the lane j is data[w*width+j]. A vector instruction then -// handles the same word for all the centers of the block, and only the words +// handles the same word for all the sequences of the block, and only the words // that a sequence contains (a hundred for a short amplicon, out of 256) need -// to be visited to compare it with all the centers of the block. +// to be visited to compare it with all the sequences of the block. type tableBlock struct { data []uint8 } @@ -14,9 +14,9 @@ func newTableBlock(width int) tableBlock { } // set stores the table t as the lane `lane` of the block. -func (b *tableBlock) set(width, lane int, t *table4) { - for w := range t.count { - b.data[w*width+lane] = t.count[w] +func (b *tableBlock) set(width, lane int, t *Table4) { + for w := range t.Count { + b.data[w*width+lane] = t.Count[w] } } @@ -26,14 +26,14 @@ func (b *tableBlock) common(width int, words []uint16, out []uint8) { blockCommon(b.data, width, words, out) } -// sparseWords lists the words of a table as w<<8|count, so that the visit of +// SparseWords lists the words of a table as w<<8|count, so that the visit of // a block only goes through the words present in the sequence. nil is // returned when the counters add up to more than 255, which would overflow the // 8-bit sums of the blocks. -func sparseWords(t *table4) []uint16 { +func SparseWords(t *Table4) []uint16 { words := make([]uint16, 0, 128) mass := 0 - for w, c := range t.count { + for w, c := range t.Count { if c > 0 { words = append(words, uint16(w)<<8|uint16(c)) mass += int(c) diff --git a/pkg/obitools/obiclust/block_nosimd.go b/pkg/obisimilarity/block_nosimd.go similarity index 69% rename from pkg/obitools/obiclust/block_nosimd.go rename to pkg/obisimilarity/block_nosimd.go index 7353d29..fe49231 100644 --- a/pkg/obitools/obiclust/block_nosimd.go +++ b/pkg/obisimilarity/block_nosimd.go @@ -1,10 +1,10 @@ //go:build !goexperiment.simd -package obiclust +package obisimilarity // simdBlockWidth is 0 without SIMD: the blocks are not used. func simdBlockWidth() int { return 0 } func blockCommon(data []uint8, width int, words []uint16, out []uint8) { - panic("obiclust: blocks of tables require GOEXPERIMENT=simd") + panic("obisimilarity: blocks of tables require GOEXPERIMENT=simd") } diff --git a/pkg/obitools/obiclust/block_simd.go b/pkg/obisimilarity/block_simd.go similarity index 80% rename from pkg/obitools/obiclust/block_simd.go rename to pkg/obisimilarity/block_simd.go index b462de0..7247cce 100644 --- a/pkg/obitools/obiclust/block_simd.go +++ b/pkg/obisimilarity/block_simd.go @@ -1,10 +1,10 @@ //go:build goexperiment.simd -package obiclust +package obisimilarity import "simd" -// simdBlockWidth is the number of centers in a block: one per lane of a +// simdBlockWidth is the number of sequences in a block: one per lane of a // vector of bytes. func simdBlockWidth() int { var probe simd.Uint8s @@ -14,7 +14,7 @@ func simdBlockWidth() int { // blockCommon computes, for the sequence whose words are given, the sum of // the minima of the counters with every lane of the block. It is the value of // commonWords without the overflow term, as the sums fit in 8 bits (see -// sparseWords). out must hold at least `width` bytes. +// SparseWords). out must hold at least `width` bytes. func blockCommon(data []uint8, width int, words []uint16, out []uint8) { acc := simd.BroadcastUint8s(0) for _, e := range words { diff --git a/pkg/obisimilarity/entry.go b/pkg/obisimilarity/entry.go new file mode 100644 index 0000000..d032640 --- /dev/null +++ b/pkg/obisimilarity/entry.go @@ -0,0 +1,95 @@ +package obisimilarity + +import ( + "sync" + "sync/atomic" +) + +// Entry is a distinct sequence of a dataset, with what the comparisons need. +type Entry struct { + Seq []byte + Length int + Count int // count of the sequence (cumulated for identical sequences) + Table *Table4 + Sparse []uint16 // words of the table (see SparseWords), nil if unusable + // Ambiguous sequences contain IUPAC letters other than a, c, g and t: they + // have no 4-mer table, and the filter does not apply to them. + Ambiguous bool +} + +const ( + notNucleotide = iota + plainNucleotide + ambiguousNucleotide +) + +var nucleotideClass = func() (t [256]uint8) { + for _, c := range "acgt" { + t[c] = plainNucleotide + } + for _, c := range "umrwsykvhdbn" { + t[c] = ambiguousNucleotide + } + return t +}() + +// Classify tells if a sequence (in lower case) can be compared. It returns +// ok=false for an empty sequence and for a sequence with anything else than +// IUPAC letters; IUPAC letters other than a, c, g and t are only accepted if +// acceptAmbiguous is set, and then ambiguous is true. +func Classify(s []byte, acceptAmbiguous bool) (ok, ambiguous bool) { + if len(s) == 0 { + return false, false + } + for _, c := range s { + switch nucleotideClass[c] { + case notNucleotide: + return false, false + case ambiguousNucleotide: + if !acceptAmbiguous { + return false, false + } + ambiguous = true + } + } + return true, ambiguous +} + +// BuildEntries describes n sequences, their 4-mer tables being computed in +// parallel. get returns the sequence i, its count, and whether it is +// ambiguous. The tables are stored contiguously. +func BuildEntries(n int, get func(i int) (seq []byte, count int, ambiguous bool), workers int) []Entry { + entries := make([]Entry, n) + tables := make([]Table4, n) + + var next atomic.Int64 + var wg sync.WaitGroup + for w := 0; w < max(workers, 1); w++ { + wg.Add(1) + go func() { + defer wg.Done() + for { + i := int(next.Add(1)) - 1 + if i >= n { + return + } + seq, count, ambiguous := get(i) + var sparse []uint16 + if !ambiguous { + tables[i].Fill(seq) + sparse = SparseWords(&tables[i]) + } + entries[i] = Entry{ + Seq: seq, + Length: len(seq), + Count: count, + Table: &tables[i], + Sparse: sparse, + Ambiguous: ambiguous, + } + } + }() + } + wg.Wait() + return entries +} diff --git a/pkg/obisimilarity/index.go b/pkg/obisimilarity/index.go new file mode 100644 index 0000000..25f41b1 --- /dev/null +++ b/pkg/obisimilarity/index.go @@ -0,0 +1,106 @@ +package obisimilarity + +import "sort" + +// Member is a copy of the entry of a sequence in an Index, with its own table. +type Member struct { + Entry + ID int // identifier given by the caller + Rank int // rank in the order of insertion +} + +const tabChunk = 1024 + +// A Bucket holds the members of one length, in the order of insertion, and +// their tables by blocks (see tableBlock). +type Bucket struct { + Members []Member + blocks []tableBlock + width int +} + +// Commons caches the result of the filter on one block of a bucket. +type Commons struct { + v [64]uint8 + blk int +} + +// Reset must be called when changing of bucket. +func (c *Commons) Reset() { c.blk = -1 } + +// Common returns the sum of the minima of the 4-mer counters of the sequence +// whose Sparse words are given and of the member k of the bucket (without the +// overflow term), or -1 if it is not available, in which case PairScore +// computes it. +func (b *Bucket) Common(k int, sparse []uint16, c *Commons) int { + if b.width == 0 || sparse == nil { + return -1 + } + if blk := k / b.width; blk != c.blk { + b.blocks[blk].common(b.width, sparse, c.v[:]) + c.blk = blk + } + return int(c.v[k%b.width]) +} + +// Index stores sequences by length. A sequence can only be similar to the ones +// whose length is compatible with its own (see Scoring.LengthCompatible), so +// it only visits the buckets of these lengths, instead of all the sequences, +// and the filter works on blocks of sequences at once. +// +// An Index can grow, but not while it is read. +type Index struct { + width int // sequences per block of transposed tables, 0 if unused + byLen map[int]*Bucket + lens []int // lengths that have a bucket, increasing + tabs [][]Table4 // chunks holding the tables of the members + n int +} + +// NewIndex returns an empty index. +func NewIndex() *Index { + return &Index{width: simdBlockWidth(), byLen: make(map[int]*Bucket)} +} + +// Len is the number of sequences in the index. +func (x *Index) Len() int { return x.n } + +// Lengths returns the lengths that have a bucket, in increasing order. +func (x *Index) Lengths() []int { return x.lens } + +// Bucket returns the bucket of the sequences of the given length. +func (x *Index) Bucket(length int) *Bucket { return x.byLen[length] } + +// Add stores a copy of the entry e as the next member of the index, with the +// identifier id, and returns it. +func (x *Index) Add(e Entry, id int) Member { + if x.n%tabChunk == 0 { + x.tabs = append(x.tabs, make([]Table4, tabChunk)) + } + tab := &x.tabs[len(x.tabs)-1][x.n%tabChunk] + if e.Table != nil { + *tab = *e.Table + } + e.Table = tab + + b := x.byLen[e.Length] + if b == nil { + b = &Bucket{width: x.width} + x.byLen[e.Length] = b + x.lens = append(x.lens, e.Length) + sort.Ints(x.lens) + } + if w := x.width; w > 0 && !e.Ambiguous { + if len(b.Members)%w == 0 { + b.blocks = append(b.blocks, newTableBlock(w)) + } + b.blocks[len(b.blocks)-1].set(w, len(b.Members)%w, tab) + } else if w > 0 && len(b.Members)%w == 0 { + b.blocks = append(b.blocks, newTableBlock(w)) + } + + m := Member{Entry: e, ID: id, Rank: x.n} + b.Members = append(b.Members, m) + x.n++ + return m +} diff --git a/pkg/obisimilarity/obisimilarity_test.go b/pkg/obisimilarity/obisimilarity_test.go new file mode 100644 index 0000000..dcfc1a2 --- /dev/null +++ b/pkg/obisimilarity/obisimilarity_test.go @@ -0,0 +1,233 @@ +package obisimilarity + +import ( + "math/rand" + "testing" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obialign" +) + +func randomSeq(rng *rand.Rand, n int, alpha string) []byte { + s := make([]byte, n) + for i := range s { + s[i] = alpha[rng.Intn(len(alpha))] + } + return s +} + +// commonWordsRef is the definition of commonWords, spelled out. +func commonWordsRef(t1, t2 *Table4) int { + total := 0 + for i := range t1.Count { + total += int(min(t1.Count[i], t2.Count[i])) + } + return total + min(t1.Overflow, t2.Overflow) +} + +func TestCommonWords(t *testing.T) { + rng := rand.New(rand.NewSource(3)) + for it := 0; it < 3000; it++ { + alpha := []string{"acgt", "ac", "a"}[it%3] + t1 := NewTable4(randomSeq(rng, rng.Intn(1200), alpha)) + t2 := NewTable4(randomSeq(rng, rng.Intn(1200), alpha)) + if got, want := commonWords(t1, t2), commonWordsRef(t1, t2); got != want { + t.Fatalf("commonWords = %d, want %d", got, want) + } + } +} + +// TestNewTable4 checks the table against the 4-mers counted the long way, +// including the saturation of the counters. +func TestNewTable4(t *testing.T) { + code := map[byte]int{'a': 0, 'c': 1, 't': 2, 'g': 3} + rng := rand.New(rand.NewSource(4)) + for it := 0; it < 500; it++ { + alpha := []string{"acgt", "a", "ac"}[it%3] + s := randomSeq(rng, rng.Intn(900), alpha) + var counts [256]int + for i := 0; i+4 <= len(s); i++ { + w := 0 + for _, c := range s[i : i+4] { + w = w<<2 | code[c] + } + counts[w]++ + } + tab := NewTable4(s) + over := 0 + for w, c := range counts { + if c > 255 { + over += c - 255 + c = 255 + } + if int(tab.Count[w]) != c { + t.Fatalf("word %d: got %d, want %d", w, tab.Count[w], c) + } + } + if tab.Overflow != over { + t.Fatalf("overflow = %d, want %d", tab.Overflow, over) + } + } +} + +// TestBlockCommon checks the sums of a block of transposed tables against +// commonWords. +func TestBlockCommon(t *testing.T) { + width := simdBlockWidth() + if width == 0 { + t.Skip("blocks of tables are used with GOEXPERIMENT=simd only") + } + rng := rand.New(rand.NewSource(8)) + for it := 0; it < 200; it++ { + alpha := []string{"acgt", "ac", "a"}[it%3] + block := newTableBlock(width) + tabs := make([]*Table4, width) + for j := range tabs { + tabs[j] = NewTable4(randomSeq(rng, rng.Intn(600), alpha)) + block.set(width, j, tabs[j]) + } + q := NewTable4(randomSeq(rng, rng.Intn(260), alpha)) + words := SparseWords(q) + if words == nil { + continue + } + out := make([]uint8, 64) + block.common(width, words, out) + for j, tab := range tabs { + want := commonWordsRef(q, tab) - min(q.Overflow, tab.Overflow) + if int(out[j]) != want { + t.Fatalf("lane %d: %d, want %d", j, out[j], want) + } + } + } +} + +func TestClassify(t *testing.T) { + cases := []struct { + in string + accept bool + kept bool + ambiguous bool + }{ + {"acgt", false, true, false}, {"acgt", true, true, false}, + {"acgtn", false, false, false}, {"acgtn", true, true, true}, + {"acgtx", true, false, false}, {"nacgt", false, false, false}, + {"acgt-", true, false, false}, {"acgu", true, true, true}, + {"ACGT", true, false, false}, {"", true, false, false}, + {"rykmswbdhvn", true, true, true}, {"rykmswbdhvn", false, false, false}, + } + for _, c := range cases { + kept, amb := Classify([]byte(c.in), c.accept) + if kept != c.kept || amb != c.ambiguous { + t.Errorf("Classify(%q, %v) = (%v, %v), want kept=%v ambiguous=%v", c.in, c.accept, kept, amb, c.kept, c.ambiguous) + } + } +} + +func randomEntries(rng *rand.Rand, n int) []Entry { + return BuildEntries(n, func(int) ([]byte, int, bool) { + return randomSeq(rng, 8+rng.Intn(80), "acgt"), 1 + rng.Intn(20), false + }, 2) +} + +// TestLengthTable checks that the values precomputed by length give the +// scores of the direct computation, in every mode. +func TestLengthTable(t *testing.T) { + rng := rand.New(rand.NewSource(33)) + // sequences made of the same few roots, so that many pairs are similar + roots := [][]byte{randomSeq(rng, 70, "acgt"), randomSeq(rng, 55, "acgt")} + entries := BuildEntries(120, func(i int) ([]byte, int, bool) { + var s []byte + for _, c := range roots[i%2] { + switch r := rng.Intn(30); { + case r == 0: + s = append(s, "acgt"[rng.Intn(4)]) + case r == 1: + default: + s = append(s, c) + } + } + if i < 20 { // very short sequences skip the filter + s = s[:8+rng.Intn(10)] + } + return s, 1 + rng.Intn(20), false + }, 2) + + var buf obialign.BandedLCSBuffer + for _, ref := range []Reference{RefAlignment, RefLongest, RefShortest} { + for _, p := range []Params{ + {Threshold: 0.9, Normalized: true}, + {Threshold: 0.8, Normalized: true, Distance: true}, + {Threshold: 30}, + {Threshold: 6, Distance: true}, + {Normalized: true}, // no threshold + } { + p.Reference, p.Ratio = ref, 3 + table, err := NewScoring(p, entries) + if err != nil { + t.Fatal(err) + } + direct := *table + direct.byLength = nil // forces the direct computation + if (ref == RefShortest) != (table.byLength == nil) { + t.Fatalf("ref %v: table presence is wrong", ref) + } + for i := range entries { + for j := range entries { + a, aok := direct.PairScore(&entries[i], &entries[j], -1, &buf) + b, bok := table.PairScore(&entries[i], &entries[j], -1, &buf) + if a != b || aok != bok { + t.Fatalf("ref %v params %+v pair %d,%d: (%v,%v) with the table, (%v,%v) directly", ref, p, i, j, b, bok, a, aok) + } + } + } + } + } +} + +// TestIndexCommon checks that the filter computed by blocks gives the same +// scores as the direct computation, for every pair, whatever the order the +// members are read in. +func TestIndexCommon(t *testing.T) { + rng := rand.New(rand.NewSource(7)) + entries := randomEntries(rng, 150) + sc, err := NewScoring(Params{Threshold: 0.6, Normalized: true, Reference: RefAlignment, Ratio: 100}, entries) + if err != nil { + t.Fatal(err) + } + index := NewIndex() + for i := range entries { + index.Add(entries[i], i) + } + if index.Len() != len(entries) { + t.Fatalf("Len = %d", index.Len()) + } + + var buf obialign.BandedLCSBuffer + var commons Commons + for i := range entries { + q := &entries[i] + for _, length := range index.Lengths() { + b := index.Bucket(length) + commons.Reset() + for k := len(b.Members) - 1; k >= 0; k-- { // backwards: blocks are not read in order + m := &b.Members[k] + got, gok := sc.PairScore(q, &m.Entry, b.Common(k, q.Sparse, &commons), &buf) + want, wok := sc.PairScore(q, &entries[m.ID], -1, &buf) + if got != want || gok != wok { + t.Fatalf("pair %d,%d: (%v,%v) with the blocks, (%v,%v) directly", i, m.ID, got, gok, want, wok) + } + } + } + } +} + +func TestNewScoringChecks(t *testing.T) { + for _, p := range []Params{{Threshold: -1}, {Threshold: 1.5, Normalized: true}} { + if _, err := NewScoring(p, nil); err == nil { + t.Errorf("%+v must be rejected", p) + } + } + if _, err := NewScoring(Params{Threshold: 12}, nil); err != nil { + t.Errorf("a raw threshold above 1 is valid: %v", err) + } +} diff --git a/pkg/obisimilarity/scoring.go b/pkg/obisimilarity/scoring.go new file mode 100644 index 0000000..4aefbe8 --- /dev/null +++ b/pkg/obisimilarity/scoring.go @@ -0,0 +1,284 @@ +package obisimilarity + +import ( + "errors" + "math" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obialign" + log "github.com/sirupsen/logrus" +) + +// Reference is the length used to normalize a score (or to express a raw +// distance): the alignment length, the longest or the shortest sequence. +type Reference int + +const ( + RefAlignment Reference = iota + RefLongest + RefShortest +) + +// Params describe how a pair of sequences is scored and compared to a threshold. +type Params struct { + // Threshold: with a normalized similarity an identity (0.95 for 95%), with + // a normalized distance 1 - identity, with a raw similarity the length of + // the LCS, with a raw distance the number of differences with the + // reference length. 0 means no threshold: all the pairs are reported. + Threshold float64 + Normalized bool // the score is divided by the reference length + Distance bool // the score is a distance, not a similarity + Reference Reference + // Ratio is the maximum ratio between the counts of a sequence and of the + // one it is compared to (clustering only): a larger ratio gives no match. + Ratio float64 +} + +// Scoring scores the pairs of sequences and decides whether they pass the +// threshold. It must not be modified once built. +type Scoring struct { + threshold float64 // threshold as used for the comparison (1 - t for a normalized distance) + normalize bool + lcsmode bool // true: score is a similarity, false: it is a distance + ref Reference + ratio float64 + worst float64 // score of a pair that does not pass + all bool // no threshold: every pair is reported + + // byLength gives, for the pairs whose longest sequence has a length l, + // the values that depend on l only. It is built when the reference is not + // the shortest sequence, otherwise those values depend on both lengths. + byLength []lengthInfo +} + +type lengthInfo struct { + lcsMin int32 // see lcsMin + minCommon int32 // see minCommon +} + +// NewScoring builds the scoring of the sequences `entries`, which are the +// ones to be compared (the longest of them is needed for a raw distance). +func NewScoring(p Params, entries []Entry) (*Scoring, error) { + if p.Threshold < 0 || (p.Normalized && p.Threshold > 1.0) { + return nil, errors.New("the threshold must not be negative, and must be at most 1 when scores are normalized") + } + + sc := &Scoring{ + normalize: p.Normalized, + lcsmode: !p.Distance, + ref: p.Reference, + ratio: p.Ratio, + } + sc.threshold = p.Threshold + sc.all = p.Threshold == 0 + if !sc.lcsmode && sc.normalize && p.Threshold > 0 { + sc.threshold = 1.0 - p.Threshold + } + + lmax := 0 + for i := range entries { + lmax = max(lmax, entries[i].Length) + } + if !sc.similarityLike() { + sc.worst = float64(lmax) + } + + if sc.ref != RefShortest { + sc.byLength = make([]lengthInfo, lmax+1) + for l := range sc.byLength { + sc.byLength[l] = lengthInfo{int32(sc.lcsMin(l, l)), int32(sc.minCommon(l, l))} + } + } + return sc, nil +} + +// Worst is the score of a pair that does not pass the threshold. +func (sc *Scoring) Worst() float64 { return sc.worst } + +// Threshold is the threshold the scores are compared to. +func (sc *Scoring) Threshold() float64 { return sc.threshold } + +// SimilarityLike tells if the comparison with the threshold is "score >= threshold". +func (sc *Scoring) SimilarityLike() bool { return sc.similarityLike() } + +func (sc *Scoring) similarityLike() bool { return sc.lcsmode || sc.normalize } + +// lcsMin is the smallest LCS score that a pair of lengths l1 >= l2 can have +// and still pass the threshold. +func (sc *Scoring) lcsMin(l1, l2 int) int { + switch { + case sc.threshold <= 0: + return 0 + case sc.normalize: + if sc.ref == RefShortest { + return int(sc.threshold * float64(l2)) + } + return int(sc.threshold * float64(l1)) + case sc.lcsmode: + return int(sc.threshold) + case sc.ref == RefShortest: + return int(float64(l2) - sc.threshold) + default: + return int(float64(l1) - sc.threshold) + } +} + +// minCommon is the number of 4-mers that two sequences of lengths l1 >= l2 +// must share to be possibly similar enough. +func (sc *Scoring) minCommon(l1, l2 int) int { + if sc.all { + return 0 // no threshold, no filter + } + reflen := l2 + if sc.ref == RefAlignment || sc.ref == RefLongest { + reflen = l1 + } + var lcs int + if sc.normalize { + lcs = int(math.Ceil(float64(reflen) * sc.threshold)) + } else { + th := sc.threshold + if !sc.lcsmode { + th = float64(reflen) - th + } + lcs = int(th) + } + return minCommonWords(l1, lcs) +} + +// lcsMinFor is lcsMin for sequences of lengths l1 >= l2, from the table when it exists. +func (sc *Scoring) lcsMinFor(l1, l2 int) int { + if sc.byLength != nil { + return int(sc.byLength[l1].lcsMin) + } + return sc.lcsMin(l1, l2) +} + +// LengthCompatible tells if two sequences of these lengths may be similar +// enough: the shortest cannot be shorter than the minimum LCS score of the pair. +func (sc *Scoring) LengthCompatible(la, lb int) bool { + l1, l2 := max(la, lb), min(la, lb) + return l2 >= sc.lcsMinFor(l1, l2) +} + +// PairScore scores the pair (q, c). ok is false when the pair is known not to +// reach the threshold, in which case no score is given. +// +// common, if not negative, is the sum of the minima of the 4-mer counters of +// the two sequences, already computed by a block of tables (see Index). +func (sc *Scoring) PairScore(q, c *Entry, common int, buf *obialign.BandedLCSBuffer) (score float64, ok bool) { + // The quotient of two counts is not above 1 when the first is not above + // the second, which spares the division in the usual case. + if !(sc.ratio >= 1 && q.Count <= c.Count) && float64(q.Count)/float64(c.Count) > sc.ratio { + return 0, false + } + + l1, l2 := q.Length, c.Length + long, short := q, c + if l1 < l2 { + l1, l2 = l2, l1 + long, short = c, q + } + var lcsMin, minCommon int + if sc.byLength != nil { + info := sc.byLength[l1] + lcsMin, minCommon = int(info.lcsMin), int(info.minCommon) + } else { + lcsMin, minCommon = sc.lcsMin(l1, l2), sc.minCommon(l1, l2) + } + if l2 < lcsMin { + return 0, false + } + // The 4-mer filter counts identical words: it does not hold when IUPAC + // codes can match different letters. It lets short sequences pass. + if l1 >= 12 && l2 >= 12 && !long.Ambiguous && !short.Ambiguous { + if common >= 0 { + common += min(long.Table.Overflow, short.Table.Overflow) + } else { + common = commonWords(long.Table, short.Table) + } + if common < minCommon { + return 0, false + } + } + + // The length of the alignment is needed only to normalize by it, or to + // express a distance with it; the score alone is much cheaper. + var lcs, alilen int + var err error + if sc.ref == RefAlignment && (sc.normalize || !sc.lcsmode) { + lcs, alilen, err = buf.AtLeast(q.Seq, c.Seq, lcsMin) + } else { + lcs, err = buf.ScoreAtLeast(q.Seq, c.Seq, lcsMin) + } + if err != nil { + log.Fatalf("Cannot align two sequences: %v", err) + } + if lcs < 0 { + // The best alignment has a score under lcsMin: the pair cannot pass. + return 0, false + } + + id := float64(lcs) + if !sc.lcsmode && !sc.normalize { + switch sc.ref { + case RefAlignment: + id = float64(alilen) - id + case RefLongest: + id = float64(l1) - id + case RefShortest: + id = float64(l2) - id + } + } + if sc.normalize { + switch sc.ref { + case RefAlignment: + id /= float64(alilen) + case RefLongest: + id /= float64(l1) + case RefShortest: + id /= float64(l2) + } + } + return id, true +} + +// Passes tells if the score s of a pair reaches the threshold. +func (sc *Scoring) Passes(s float64) bool { + if sc.all { + return true + } + if sc.similarityLike() { + return s >= sc.threshold + } + return s <= sc.threshold +} + +// Beats tells if the score s is better than the score best (without any +// threshold). +func (sc *Scoring) Beats(s, best float64) bool { + if sc.similarityLike() { + return s > best + } + return s < best +} + +// SelfScore is the score of a sequence with itself. +func (sc *Scoring) SelfScore(length int) float64 { + switch { + case sc.normalize && sc.lcsmode: + return 1.0 + case !sc.lcsmode: + return 0.0 + default: + return float64(length) + } +} + +// Reported is the score to report for a pair whose score is s: a normalized +// distance is computed as a similarity against the converted threshold. +func (sc *Scoring) Reported(s float64) float64 { + if !sc.lcsmode && sc.normalize { + return 1.0 - s + } + return s +} diff --git a/pkg/obisimilarity/table4.go b/pkg/obisimilarity/table4.go new file mode 100644 index 0000000..6b8dcfc --- /dev/null +++ b/pkg/obisimilarity/table4.go @@ -0,0 +1,54 @@ +// Package obisimilarity holds what the tools that compare nucleotide sequences +// by the length of their longest common subsequence (obiclust, obialign) have +// in common: the scoring of a pair against a threshold, with the lossless +// 4-mer filter that avoids most of the alignments, and the index that finds +// the sequences a given one can be similar to. +// +// The scoring follows the arithmetic of sumaclust and sumatra (integer +// truncations, floating point comparisons) so that the same pairs are +// accepted with the same scores. +package obisimilarity + +// Table4 is the 4-mer occurrence table of a sequence: one saturating counter +// per word, and the number of occurrences that did not fit in the counters. +type Table4 struct { + Count [256]uint8 + Overflow int +} + +// NewTable4 counts the 4-mers of a sequence made of the letters a, c, g +// and t. The 2-bit code of a base is (letter>>1)&3, i.e. a=0, c=1, t=2, g=3. +func NewTable4(seq []byte) *Table4 { + t := new(Table4) + t.Fill(seq) + return t +} + +// Fill counts the 4-mers of seq in a zeroed table. +func (t *Table4) Fill(seq []byte) { + if len(seq) < 4 { + return + } + w := uint(0) + for i, c := range seq { + w = (w<<2 | uint(c>>1)&3) & 0xFF + if i < 3 { + continue + } + if t.Count[w] < 255 { + t.Count[w]++ + } else { + t.Overflow++ + } + } +} + +// minCommonWords is the minimum number of 4-mers that two sequences must +// share when the longest has length reflen and they have a common +// subsequence of length lcs. +func minCommonWords(reflen, lcs int) int { + nbfrag := (reflen-lcs)*2 + 1 + smin := lcs / nbfrag + r := lcs - smin*nbfrag + return max(smin-2, 0)*r + max(smin-3, 0)*(nbfrag-r) +} diff --git a/pkg/obisimilarity/table4_nosimd.go b/pkg/obisimilarity/table4_nosimd.go new file mode 100644 index 0000000..7596e0d --- /dev/null +++ b/pkg/obisimilarity/table4_nosimd.go @@ -0,0 +1,14 @@ +//go:build !goexperiment.simd + +package obisimilarity + +// commonWords is the upper bound used for the number of 4-mers shared by two +// sequences: the sum of the minima of the counters, plus the minimum of the +// two overflows. +func commonWords(t1, t2 *Table4) int { + total := 0 + for i := range t1.Count { + total += int(min(t1.Count[i], t2.Count[i])) + } + return total + min(t1.Overflow, t2.Overflow) +} diff --git a/pkg/obitools/obiclust/kmer4_simd.go b/pkg/obisimilarity/table4_simd.go similarity index 65% rename from pkg/obitools/obiclust/kmer4_simd.go rename to pkg/obisimilarity/table4_simd.go index 96da178..b912804 100644 --- a/pkg/obitools/obiclust/kmer4_simd.go +++ b/pkg/obisimilarity/table4_simd.go @@ -1,24 +1,24 @@ //go:build goexperiment.simd -package obiclust +package obisimilarity import "simd" -// commonWords is the upper bound sumaclust uses for the number of 4-mers -// shared by two sequences: the sum of the minima of the counters, plus the -// minimum of the two overflows. +// commonWords is the upper bound used for the number of 4-mers shared by two +// sequences: the sum of the minima of the counters, plus the minimum of the +// two overflows. // // The minima of the 256 counters are computed by vectors of bytes, and summed // in 16-bit lanes (at most 256*255 spread over the lanes, the lanes being at // least 8 in number, so that none of them overflows). -func commonWords(t1, t2 *table4) int { +func commonWords(t1, t2 *Table4) int { var probe simd.Uint8s vl := probe.Len() acc := simd.BroadcastUint16s(0) low := simd.BroadcastUint16s(0x00FF) - for i := 0; i < len(t1.count); i += vl { - m := simd.LoadUint8s(t1.count[i:]).Min(simd.LoadUint8s(t2.count[i:])).ReshapeToUint16s() + for i := 0; i < len(t1.Count); i += vl { + m := simd.LoadUint8s(t1.Count[i:]).Min(simd.LoadUint8s(t2.Count[i:])).ReshapeToUint16s() acc = acc.Add(m.And(low)).Add(m.ShiftAllRight(8)) } @@ -31,5 +31,5 @@ func commonWords(t1, t2 *table4) int { for _, v := range lanes[:sums.Len()] { total += int(v) } - return total + min(t1.overflow, t2.overflow) + return total + min(t1.Overflow, t2.Overflow) } diff --git a/pkg/obithermo/model.go b/pkg/obithermo/model.go new file mode 100644 index 0000000..22c8299 --- /dev/null +++ b/pkg/obithermo/model.go @@ -0,0 +1,222 @@ +// Package obithermo computes DNA duplex melting temperatures with the +// nearest-neighbor model used by ecoPrimers (SantaLucia 1998 parameters, with +// the SantaLucia or Owczarzy 2008 salt corrections). +// +// Only the situations ecoPrimers actually evaluates are supported: a primer +// against its perfect complement (SelfTm), and a primer against a genomic site +// of the same length that may carry mismatches (Tm). Gaps, bulges and dangling +// ends are not modeled. +package obithermo + +import ( + "fmt" + "math" +) + +// SaltMethod selects the monovalent salt correction. +type SaltMethod int + +const ( + // SantaLucia applies a per-step entropy correction 0.368*ln[Na+]. + SantaLucia SaltMethod = 1 + // Owczarzy applies the GC-dependent correction of Owczarzy et al. 2008. + Owczarzy SaltMethod = 2 +) + +// Default experimental conditions, those of ecoPrimers. +const ( + DefaultPrimerConc = 8e-7 // total primer strand concentration (M) + DefaultTargetConc = 0.0 // target strand concentration (M) + DefaultSalt = 0.05 // monovalent cation concentration (M) +) + +const ( + gasConstant = 1.987 // cal/mol/K + initEntropy = -5.9 // duplex initiation, cal/mol/K + kelvinOffset = 273.15 // ecoPrimers subtracts 273.0 (see package notes) + owczarzyGCSlope = 4.29e-5 // (4.29*fGC - 3.95)e-5 * ln[Na] + owczarzyOffset = 3.95e-5 + owczarzyQuadratic = 9.40e-6 // 9.4e-6 * ln[Na]^2 +) + +// Model holds the thermodynamic tables and the experimental conditions. +// A Model is immutable after NewModel and safe for concurrent use. +type Model struct { + method SaltMethod + rlogc float64 // R*ln(Ct factor), included in the initial entropy + logNa float64 // ln[Na+] + + // Indexed by (top0<<6 | top1<<4 | bot0<<2 | bot1) with A,C,G,T = 0..3. + // dS already includes the SantaLucia salt term when that method is selected. + dH [256]float64 + dS [256]float64 +} + +// NewModel builds a Model for the given strand concentrations (M), monovalent +// salt concentration (M) and salt correction method. +func NewModel(primerConc, targetConc, salt float64, method SaltMethod) (*Model, error) { + if method != SantaLucia && method != Owczarzy { + return nil, fmt.Errorf("obithermo: unknown salt method %d", method) + } + if salt <= 0 { + return nil, fmt.Errorf("obithermo: salt concentration must be positive, got %g", salt) + } + + // Effective strand concentration, as in nparam_InitParams. + var ctFactor float64 + switch { + case primerConc == targetConc: + ctFactor = primerConc / 2 + case primerConc > targetConc: + ctFactor = primerConc - targetConc/2 + default: + ctFactor = targetConc - primerConc/2 + } + if ctFactor <= 0 { + return nil, fmt.Errorf("obithermo: strand concentrations must be positive (primer %g, target %g)", + primerConc, targetConc) + } + + m := &Model{ + method: method, + rlogc: gasConstant * math.Log(ctFactor), + logNa: math.Log(salt), + } + m.fillTables() + return m, nil +} + +// DefaultModel returns a Model with the ecoPrimers default conditions and the +// SantaLucia salt correction. +func DefaultModel() *Model { + m, err := NewModel(DefaultPrimerConc, DefaultTargetConc, DefaultSalt, SantaLucia) + if err != nil { + panic(err) // the defaults are valid + } + return m +} + +var baseCode = func() (t [256]int8) { + for i := range t { + t[i] = -1 + } + for i, c := range "ACGT" { + t[c] = int8(i) + t[c+'a'-'A'] = int8(i) + } + return +}() + +func stepIndex(t0, t1, b0, b1 int) int { return t0<<6 | t1<<4 | b0<<2 | b1 } + +func (m *Model) fillTables() { + // Double mismatches (no Watson-Crick pair at either end of the step) are + // treated as interior-loop expansions. Complement of code c is 3-c. + const loopH, loopS = 0.0, -0.97 + for x := range 4 { + for y := range 4 { + for a := range 4 { + for b := range 4 { + if x+a != 3 && y+b != 3 { + i := stepIndex(x, y, a, b) + m.dH[i], m.dS[i] = loopH, loopS + } + } + } + } + } + + // Explicit parameters override the defaults (Watson-Crick and single mismatches). + for _, e := range nnEntries { + i := stepIndex(int(baseCode[e.top[0]]), int(baseCode[e.top[1]]), + int(baseCode[e.bottom[0]]), int(baseCode[e.bottom[1]])) + m.dH[i], m.dS[i] = e.dH, e.dS + } + + // SantaLucia salt correction: 0.5*kfac per side of each step, i.e. kfac. + if m.method == SantaLucia { + kfac := 0.368 * m.logNa + for i := range m.dS { + m.dS[i] += kfac + } + } +} + +// GCCount returns the number of G and C (upper or lower case) in seq. +func GCCount(seq []byte) int { + n := 0 + for _, c := range seq { + switch c { + case 'G', 'C', 'g', 'c': + n++ + } + } + return n +} + +// SelfTm returns the melting temperature (degrees Celsius) of primer against +// its perfect complement. It returns NaN when the model predicts no stable duplex. +func (m *Model) SelfTm(primer []byte) (float64, error) { + return m.duplex(primer, primer, true) +} + +// Tm returns the melting temperature (degrees Celsius) of primer against the +// site, a stretch of the target read in the same orientation as the primer +// (primer and site have the same length; mismatches are primer[i] != site[i]). +// It returns NaN when the model predicts no stable duplex. +func (m *Model) Tm(primer, site []byte) (float64, error) { + if len(site) != len(primer) { + return 0, fmt.Errorf("obithermo: primer (%d) and site (%d) lengths differ", len(primer), len(site)) + } + return m.duplex(primer, site, false) +} + +func (m *Model) duplex(primer, site []byte, perfect bool) (float64, error) { + n := len(primer) + if n < 2 { + return 0, fmt.Errorf("obithermo: primer too short (%d), need at least 2 bases", n) + } + + dH := 0.0 + dS := initEntropy + m.rlogc + + p0 := int(baseCode[primer[0]]) + s0 := p0 + if !perfect { + s0 = int(baseCode[site[0]]) + } + if p0 < 0 || s0 < 0 { + return 0, fmt.Errorf("obithermo: non-ACGT base at position 0") + } + + for i := 1; i < n; i++ { + p1 := int(baseCode[primer[i]]) + s1 := p1 + if !perfect { + s1 = int(baseCode[site[i]]) + } + if p1 < 0 || s1 < 0 { + return 0, fmt.Errorf("obithermo: non-ACGT base at position %d", i) + } + // The partner strand carries the complement of the site. + idx := stepIndex(p0, p1, 3-s0, 3-s1) + dH += m.dH[idx] + dS += m.dS[idx] + p0, s0 = p1, s1 + } + + if m.method == Owczarzy { + fgc := float64(GCCount(primer)) / float64(n) + dS += dH * ((owczarzyGCSlope*fgc-owczarzyOffset)*m.logNa + owczarzyQuadratic*m.logNa*m.logNa) + } + + // One-state transition, as nparam_CalcTM: valid only for dS < 0 and Tm >= 0. + if dS >= 0 { + return math.NaN(), nil + } + tm := dH / dS + if tm < 0 { + return math.NaN(), nil + } + return tm - kelvinOffset, nil +} diff --git a/pkg/obithermo/model_test.go b/pkg/obithermo/model_test.go new file mode 100644 index 0000000..b8b342d --- /dev/null +++ b/pkg/obithermo/model_test.go @@ -0,0 +1,203 @@ +package obithermo + +import ( + "bufio" + "math" + "os" + "strconv" + "strings" + "testing" +) + +func readTSV(t *testing.T, path string) [][]string { + t.Helper() + f, err := os.Open(path) + if err != nil { + t.Fatal(err) + } + defer f.Close() + var rows [][]string + sc := bufio.NewScanner(f) + for sc.Scan() { + if line := strings.TrimSpace(sc.Text()); line != "" { + rows = append(rows, strings.Split(line, "\t")) + } + } + if err := sc.Err(); err != nil { + t.Fatal(err) + } + return rows +} + +func mustFloat(t *testing.T, s string) float64 { + t.Helper() + v, err := strconv.ParseFloat(s, 64) + if err != nil { + t.Fatal(err) + } + return v +} + +// checkAgainstC compares one Go result (Celsius) with the raw Kelvin value +// returned by the C library. The C code returns 0 when the model fails. +// +// The tolerance (1e-4 K) covers the single-precision literals of the C source +// (-21.3f, 1.987f, -5.9f ...): emulating them in float32 brings the gap down +// to 1e-12 K, so the Go port uses the same model with exact decimal constants. +func checkAgainstC(t *testing.T, what string, got float64, cKelvin float64) { + t.Helper() + if cKelvin <= 0 { + if !math.IsNaN(got) { + t.Errorf("%s: C model failed, Go returned %g", what, got) + } + return + } + if math.IsNaN(got) { + t.Errorf("%s: Go model failed, C returned %g K", what, cKelvin) + return + } + if d := math.Abs(got + 273.15 - cKelvin); d > 1e-4 { + t.Errorf("%s: Go %.9f C %.9f K (diff %g)", what, got+273.15, cKelvin, d) + } +} + +// testdata/c_reference.tsv was produced by calling nparam_CalcSelfTM and +// nparam_CalcTwoTM of ecoPrimers' libthermo on random primers and sites with +// 0..n mismatches: primer, site, self Tm, two Tm (default conditions), and +// two Tm with 1e-6 M / 2e-7 M strands and 0.2 M salt. +func TestAgainstEcoPrimersC(t *testing.T) { + def := DefaultModel() + alt, err := NewModel(1e-6, 2e-7, 0.2, SantaLucia) + if err != nil { + t.Fatal(err) + } + + rows := readTSV(t, "testdata/c_reference.tsv") + if len(rows) < 100 { + t.Fatalf("reference file too short: %d", len(rows)) + } + for _, r := range rows { + primer, site := []byte(r[0]), []byte(r[1]) + + got, err := def.SelfTm(primer) + if err != nil { + t.Fatal(err) + } + checkAgainstC(t, "self "+r[0], got, mustFloat(t, r[2])) + + got, err = def.Tm(primer, site) + if err != nil { + t.Fatal(err) + } + checkAgainstC(t, "two "+r[0]+"/"+r[1], got, mustFloat(t, r[3])) + + got, err = alt.Tm(primer, site) + if err != nil { + t.Fatal(err) + } + checkAgainstC(t, "alt "+r[0]+"/"+r[1], got, mustFloat(t, r[4])) + } +} + +// ecoPrimers prints Tm as (Kelvin - 273.0) with one decimal; obithermo uses the +// exact 273.15 offset, so the printed value is our result + 0.15. +func TestAgainstTutorialOutput(t *testing.T) { + m := DefaultModel() + rows := readTSV(t, "testdata/ecoprimers_tm.tsv") + if len(rows) == 0 { + t.Fatal("empty fixture") + } + for _, r := range rows { + got, err := m.SelfTm([]byte(r[0])) + if err != nil { + t.Fatal(err) + } + want := mustFloat(t, r[1]) + if d := math.Abs(got + 0.15 - want); d > 0.0501 { + t.Errorf("%s: printed %.1f, got %.3f (+0.15 = %.3f)", r[0], want, got, got+0.15) + } + } +} + +func TestSaltMethodsAgreeAtOneMolar(t *testing.T) { + sl, _ := NewModel(DefaultPrimerConc, DefaultTargetConc, 1.0, SantaLucia) + ow, _ := NewModel(DefaultPrimerConc, DefaultTargetConc, 1.0, Owczarzy) + p := []byte("ACACCGCCCGTCACTCTC") + a, _ := sl.SelfTm(p) + b, _ := ow.SelfTm(p) + if math.Abs(a-b) > 1e-9 { + t.Errorf("at 1 M both corrections vanish: %g vs %g", a, b) + } +} + +func TestSaltRaisesTm(t *testing.T) { + p := []byte("ACACCGCCCGTCACTCTC") + for _, method := range []SaltMethod{SantaLucia, Owczarzy} { + low, _ := NewModel(DefaultPrimerConc, DefaultTargetConc, 0.02, method) + high, _ := NewModel(DefaultPrimerConc, DefaultTargetConc, 0.5, method) + a, _ := low.SelfTm(p) + b, _ := high.SelfTm(p) + if !(b > a) { + t.Errorf("method %d: Tm should grow with salt, got %g then %g", method, a, b) + } + } +} + +func TestMismatchLowersTm(t *testing.T) { + m := DefaultModel() + p := []byte("ACACCGCCCGTCACTCTC") + perfect, _ := m.Tm(p, p) + mm, _ := m.Tm(p, []byte("ACACCGCCCGTGACTCTC")) + if !(mm < perfect) { + t.Errorf("a mismatch must lower Tm: perfect %g, mismatch %g", perfect, mm) + } +} + +func TestInputErrors(t *testing.T) { + m := DefaultModel() + if _, err := m.SelfTm([]byte("A")); err == nil { + t.Error("expected error for a 1-base primer") + } + if _, err := m.SelfTm([]byte("ACGNT")); err == nil { + t.Error("expected error for a non-ACGT base") + } + if _, err := m.Tm([]byte("ACGT"), []byte("ACG")); err == nil { + t.Error("expected error for different lengths") + } + if _, err := NewModel(0, 0, 0.05, SantaLucia); err == nil { + t.Error("expected error for zero strand concentration") + } + if _, err := NewModel(1e-6, 0, -1, SantaLucia); err == nil { + t.Error("expected error for negative salt") + } + if _, err := NewModel(1e-6, 0, 0.05, SaltMethod(9)); err == nil { + t.Error("expected error for unknown salt method") + } +} + +func TestLowerCaseAccepted(t *testing.T) { + m := DefaultModel() + a, _ := m.SelfTm([]byte("ACACCGCCCGTCACTCTC")) + b, _ := m.SelfTm([]byte("acaccgcccgtcactctc")) + if a != b { + t.Errorf("case must not matter: %g vs %g", a, b) + } +} + +func TestGCCount(t *testing.T) { + if n := GCCount([]byte("ACgtCCaT")); n != 4 { + t.Errorf("GCCount = %d, want 4", n) + } +} + +func BenchmarkTm(b *testing.B) { + m := DefaultModel() + p := []byte("ACACCGCCCGTCACTCTC") + s := []byte("ACACCGCCCGTGACTCTC") + b.ReportAllocs() + for b.Loop() { + if _, err := m.Tm(p, s); err != nil { + b.Fatal(err) + } + } +} diff --git a/pkg/obithermo/nn_table.go b/pkg/obithermo/nn_table.go new file mode 100644 index 0000000..373602f --- /dev/null +++ b/pkg/obithermo/nn_table.go @@ -0,0 +1,126 @@ +// Code generated from ecoPrimers libthermo/nnparams.c (nparam_InitParams); DO NOT EDIT. + +package obithermo + +// nnEntry is one explicit nearest-neighbor parameter: the 5'->3' dinucleotide of +// the top strand, the 3'->5' dinucleotide facing it, enthalpy (cal/mol) and +// entropy (cal/mol/K). +type nnEntry struct { + top, bottom string + dH, dS float64 +} + +var nnEntries = [...]nnEntry{ + {"AA", "TT", -7600.0, -21.3}, // AA/TT + {"AC", "TG", -8400.0, -22.4}, // AC/TG + {"AG", "TC", -7800.0, -21.0}, // AG/TC + {"AT", "TA", -7200.0, -20.4}, // AT/TA + {"CA", "GT", -8500.0, -22.7}, // CA/GT + {"CC", "GG", -8000.0, -19.9}, // CC/GG + {"CG", "GC", -10600.0, -27.2}, // CG/GC + {"CT", "GA", -7800.0, -21.0}, // CT/GA + {"GA", "CT", -8200.0, -22.2}, // GA/CT + {"GC", "CG", -9800.0, -24.4}, // GC/CG + {"GG", "CC", -8000.0, -19.9}, // GG/CC + {"GT", "CA", -8400.0, -22.4}, // GT/CA + {"TA", "AT", -7200.0, -21.3}, // TA/AT + {"TC", "AG", -8200.0, -22.2}, // TC/AG + {"TG", "AC", -8500.0, -22.7}, // TG/AC + {"TT", "AA", -7600.0, -21.3}, // TT/AA + {"AA", "CT", 7600.0, 20.2}, // AA/CT + {"AA", "TC", 2300.0, 4.6}, // AA/TC + {"AC", "CG", -700.0, -3.8}, // AC/CG + {"AC", "TA", 5300.0, 14.6}, // AC/TA + {"AG", "CC", 600.0, -0.6}, // AG/CC + {"AT", "CA", 5300.0, 14.6}, // AT/CA + {"CA", "AT", 3400.0, 8.0}, // CA/AT + {"CA", "GC", 1900.0, 3.7}, // CA/GC + {"CC", "AG", 5200.0, 14.2}, // CC/AG + {"CC", "GA", 600.0, -0.6}, // CC/GA + {"CG", "AC", 1900.0, 3.7}, // CG/AC + {"CT", "AA", 2300.0, 4.6}, // CT/AA + {"GA", "CC", 5200.0, 14.2}, // GA/CC + {"GC", "CA", -700.0, -3.8}, // GC/CA + {"TA", "AC", 3400.0, 8.0}, // TA/AC + {"TC", "AA", 7600.0, 20.2}, // TC/AA + {"AC", "TT", 700.0, 0.2}, // AC/TT + {"AT", "TC", -1200.0, -6.2}, // AT/TC + {"CA", "TT", 1000.0, 0.7}, // CA/TT + {"CC", "GT", -800.0, -4.5}, // CC/GT + {"CC", "TG", 5200.0, 13.5}, // CC/TG + {"CG", "TC", -1500.0, -6.1}, // CG/TC + {"CT", "GC", -1500.0, -6.1}, // CT/GC + {"CT", "TA", -1200.0, -6.2}, // CT/TA + {"GC", "CT", 2300.0, 5.4}, // GC/CT + {"GT", "CC", 5200.0, 13.5}, // GT/CC + {"TA", "CT", 1200.0, 0.7}, // TA/CT + {"TC", "CG", 2300.0, 5.4}, // TC/CG + {"TC", "AT", 1200.0, 0.7}, // TC/AT + {"TG", "CC", -800.0, -4.5}, // TG/CC + {"TT", "CA", 700.0, 0.2}, // TT/CA + {"TT", "AC", 1000.0, 0.7}, // TT/AC + {"AA", "GT", 3000.0, 7.4}, // AA/GT + {"AA", "TG", -600.0, -2.3}, // AA/TG + {"AC", "GG", 500.0, 3.2}, // AC/GG + {"AG", "GC", -4000.0, -13.2}, // AG/GC + {"AG", "TA", -700.0, -2.3}, // AG/TA + {"AT", "GA", -700.0, -2.3}, // AT/GA + {"CA", "GG", -700.0, -2.3}, // CA/GG + {"CG", "GA", -4000.0, -13.2}, // CG/GA + {"GA", "AT", 700.0, 0.7}, // GA/AT + {"GA", "CG", -600.0, -1.0}, // GA/CG + {"GC", "AG", -600.0, -1.0}, // GC/AG + {"GG", "AC", -700.0, -2.3}, // GG/AC + {"GG", "CA", 500.0, 3.2}, // GG/CA + {"GT", "AA", -600.0, -2.3}, // GT/AA + {"TA", "AG", 700.0, 0.7}, // TA/AG + {"TG", "AA", 3000.0, 7.4}, // TG/AA + {"AG", "TT", 1000.0, 0.9}, // AG/TT + {"AT", "TG", -2500.0, -8.3}, // AT/TG + {"CG", "GT", -4100.0, -11.7}, // CG/GT + {"CT", "GG", -2800.0, -8.0}, // CT/GG + {"GA", "TT", -1300.0, -5.3}, // GA/TT + {"GC", "TG", -4400.0, -12.3}, // GC/TG + {"GG", "CT", 3300.0, 10.4}, // GG/CT + {"GG", "TC", -2800.0, -8.0}, // GG/TC + {"GT", "CG", -4400.0, -12.3}, // GT/CG + {"GT", "TA", -2500.0, -8.3}, // GT/TA + {"TA", "GT", -100.0, -1.7}, // TA/GT + {"TC", "GG", 3300.0, 10.4}, // TC/GG + {"TG", "AT", -100.0, -1.7}, // TG/AT + {"TG", "GC", -4100.0, -11.7}, // TG/GC + {"TT", "AG", -1300.0, -5.3}, // TT/AG + {"TT", "GA", 1000.0, 0.9}, // TT/GA + {"AA", "AT", 4700.0, 12.9}, // AA/AT + {"AA", "TA", 1200.0, 1.7}, // AA/TA + {"AC", "AG", -2900.0, -9.8}, // AC/AG + {"AG", "AC", -900.0, -4.2}, // AG/AC + {"AT", "AA", 1200.0, 1.7}, // AT/AA + {"CA", "GA", -900.0, -4.2}, // CA/GA + {"GA", "CA", -2900.0, -9.8}, // GA/CA + {"TA", "AA", 4700.0, 12.9}, // TA/AA + {"AC", "TC", 0.0, -4.4}, // AC/TC + {"CA", "CT", 6100.0, 16.4}, // CA/CT + {"CC", "CG", 3600.0, 8.9}, // CC/CG + {"CC", "GC", -1500.0, -7.2}, // CC/GC + {"CG", "CC", -1500.0, -7.2}, // CG/CC + {"CT", "CA", 0.0, -4.4}, // CT/CA + {"GC", "CC", 3600.0, 8.9}, // GC/CC + {"TC", "AC", 6100.0, 16.4}, // TC/AC + {"AG", "TG", -3100.0, -9.5}, // AG/TG + {"CG", "GG", -4900.0, -15.3}, // CG/GG + {"GA", "GT", 1600.0, 3.6}, // GA/GT + {"GC", "GG", -6000.0, -15.8}, // GC/GG + {"GG", "CG", -6000.0, -15.8}, // GG/CG + {"GG", "GC", -4900.0, -15.3}, // GG/GC + {"GT", "GA", -3100.0, -9.5}, // GT/GA + {"TG", "AG", 1600.0, 3.6}, // TG/AG + {"AT", "TT", -2700.0, -10.8}, // AT/TT + {"CT", "GT", -5000.0, -15.8}, // CT/GT + {"GT", "CT", -2200.0, -8.4}, // GT/CT + {"TA", "TT", 200.0, -1.5}, // TA/TT + {"TC", "TG", -2200.0, -8.4}, // TC/TG + {"TG", "TC", -5000.0, -15.8}, // TG/TC + {"TT", "AT", 200.0, -1.5}, // TT/AT + {"TT", "TA", -2700.0, -10.8}, // TT/TA +} diff --git a/pkg/obithermo/testdata/c_reference.tsv b/pkg/obithermo/testdata/c_reference.tsv new file mode 100644 index 0000000..53123fc --- /dev/null +++ b/pkg/obithermo/testdata/c_reference.tsv @@ -0,0 +1,400 @@ +GATTCAAATGACGGCAGC GATTCAAATGACGGCAGC 328.590295502325 328.590295502325 335.520082032693 +GCCGGGAGTCCCTGAGAG GCCGGGGGTCCCTTAGTG 335.202600908825 307.571309416976 317.582817372006 +CCGGAAATGTGCCATCTGCGTGCGA CCGGAAATGTGCCATCTGCGTGCGA 341.857354066828 341.857354066828 349.067971091170 +GCAGCGTAAGAGGAGGGC GCCGAGTGAGAGGACGGC 334.238236820572 292.689122005507 302.959373794547 +ATCGGGATCTCAAAACCATC ATCCGGATCCCAAAACTATC 328.374164082672 303.012377993885 311.743534784977 +TTTACTTCTCTCAAGGCC TTTACTTCGCTCACGGCC 324.870543621461 310.322643307651 318.434500513589 +GATATTATCCGG TTCCTCATTAGG 306.633848409535 131.298936687567 140.958918657051 +ACCAGATTCACCGTTAAA ACCAGATTCACCGTTAAA 324.666065281835 324.666065281835 331.625141334598 +GCAGAAGGAATTCGTCTTAAAGTTT GCAGAAGGAATTCGTCTTAAAGTTT 330.221644057193 330.221644057193 337.429293354313 +GTTACGCCCGTGGACAGA GTTACGCCCGTGGACAGA 333.152006826526 333.152006826526 340.131722535940 +TACTGGCCAAGTGTTTCGGGCTACC TACTGGACAAGGGTTTCGAGCTACC 338.543873371072 317.159669988483 326.737809053749 +CGGGCGAAAGACCTAACTCGTC GCCCATGTTTAAACGGTTCAAC 335.961664387167 99.726835837839 110.941608232133 +ACCACAGAAGAGCCTCAG ACCACAGAAGAGCCTCAG 329.675966490972 329.675966490972 336.774887202068 +TCCGTATCCGTGTGCTAT TCCGTATCCGTGTGCTAT 328.758681136274 328.758681136274 335.670742367360 +AGCACAACCATCTTGCCA AACCAGCAATTGGGCAAC 330.726812945211 0.000000000000 0.000000000000 +AAATCCGCGGTTACTTAG AAATCCCCGGATACTTAG 325.912290117405 310.432539561454 317.816914468819 +TGGTTGCGGAATGGCGACACGCTAG TGGTTGCGGAATGGCGACACGCTAG 341.368691736083 341.368691736083 348.576311955777 +CCACAAGGCACACCGAGGGCCCAAC CCACAAGGCACACCGAGGGCCCAAC 343.836245623632 343.836245623632 351.324029371624 +TACCTTTGGTTAACCGCG TACCTTTGGTTGACCGCG 328.806445188317 321.784293018138 329.972272766148 +ACCGCCAAACGGTAGTCCTT ACCGCCAAACGTTATTCCTT 334.638358155254 325.067933219113 333.495286023143 +GGAATCACCGAATTC GGAATCACCAAATTC 319.872552670165 310.456121372350 317.724729105623 +CGCTGGCGGGGACGCTTA CGCTGGCGGGGACGCTTA 338.850878426355 338.850878426355 345.870729715750 +CCCAGAACCGAGTGCTGA CCCAGAACCTAGTGCTGA 332.759551850704 324.515696448123 332.085468128098 +AGGACTATAGCC AGGACTATAGCC 311.340568942550 311.340568942550 317.961816433927 +AAGTAAGCCAAACTC AAGTAAGCCAAACTC 318.374897033808 318.374897033808 325.174916603994 +GGCAAACAGTGGCGTCTCTA GGCAAACATTGGCGTCTCGA 333.444435842161 322.587904260466 330.767259471616 +GCGGGCTGGGCGACGCCT GCGGGCTGGGCGACGCCT 344.353158968164 344.353158968164 351.458901919666 +AAAACCCGTTCTTGAGAA ATAACCCGTTCTTGAGAA 325.196643716015 318.373852240644 326.309549286768 +TCTACGGGCAGG TCTACGGGCAGG 320.684836039138 320.684836039138 327.333083788784 +CTGTATCATTCAAGACCTTGTG CTGGATCATTCATGACCTTCTA 327.587191928324 300.073898600295 309.615050320053 +GAGACCTCGTTTATATGTGCCG GAGATCTCCTTTATATGTTCCG 331.709967011022 307.489712436204 316.778347789381 +GATGCCATGCAACGCGGTCGAAATA GATGCCAAGCAACGCCGTCGAAATA 338.385858507321 328.510784583696 335.857144351146 +CACCCATGGGTCGAGGAA CCTTCATGGGTCAAGGAA 332.434732686040 303.559662982167 313.037414209458 +AACTTTACCACTTTGTCGGA AACTTTACCACTGTGTCGAA 327.854258269700 313.748415282978 322.159067997528 +CTCCCAAGAATAAGGCCA CTCCCAAGAATAAGGCTA 327.279305268693 315.945719545119 323.849648970559 +CTGCTCCCGACCTGCCGTTG CTGCTCCCGACCTGCAGTTG 339.148624544930 330.290260008245 337.867535931326 +TTAGGAGACTCTTCAGCCTCCTTGT TTAGGACACTATTAAGCCTCCTTGT 334.398890499698 315.037835993209 323.891875192977 +TGGACGATTGCG AGAATTGCGGCC 320.244905957781 146.501396490593 156.665817684297 +ATTGTCAACTGTAGGTCA ATTGTCTAGTGTAGGTCA 323.939323737395 300.681567922172 308.863167608541 +ACAATGTATTTTCTCGGG ACAATGTATTTTCTCGGG 323.362512238862 323.362512238862 330.327169046076 +CGTGTGAGTGGCCCGAGTTGTCTCT CGTGCGAGTGGCCCGAGTTGTCTTT 340.130848609675 332.245514068114 340.636316881815 +TATCAGACGTGGCAATGC TATCAGACTTGACAATGT 328.992885301719 304.405743905969 312.701350705029 +CGTCGTCCCTCTCAGCCG CGTCGTCCCTCTCAGCCG 336.200821936899 336.200821936899 343.186897048375 +TAAGGCTAGCCAGAACCACG TGAGGCTAGCCAGAACCACG 332.598253740841 326.985556902569 335.280653989150 +GTTCAGGGGAATGAGAGATA TTACAACTGCCAACTTCGCT 326.722966616537 109.952678878773 121.594862151643 +CCCAGCGCGAATTTGTCA CCCATCGCGAACTCGGCC 332.540722470730 292.630570524742 303.511783076339 +TGTCGTCGCTACCAGTTA TGCCTTCCCTACCAGTTA 328.953316297354 307.160700342629 315.442693299698 +CATGCACCGTGAGTCTGC CGTTCACCGGGAGTCCGC 332.846939286468 291.434652139556 302.526073291148 +GAGAGTTGCTGTTGATTA TAGAGTTGCTGTTGATTA 323.031379731052 319.294880722061 326.526947280607 +GCTCGGTCCTACGCCGACGT GCCTGATCTAAATATGCCTG 339.747877109584 158.568208002892 173.058219990461 +TCTTTGAGCTTCCCATCT TATTTGAGGTTCCCATCT 326.307771557426 300.329179385918 308.617669678883 +GTTTCCTCGGGT ATGTCCCCGGTC 317.903207078845 250.921863687541 262.030341678318 +AGTCTGCATCCAGGATAGTGTTCAA AGTCTGCGTCCAGGATAGTGTTCAA 333.474575396610 328.822975749317 336.887405627890 +CTAGGGCACCAACGCACG CTAGGTCACCAACGCACG 335.143347846422 329.701891848980 337.431099508189 +GATTGCTGGACCCGACAAAC ATCGACTGCGTCAATCTGAT 332.811424650986 125.989102558259 139.175967010614 +TTATTTCTCCGAGATCCA TTATTGCTCCGGGATCGA 323.096708880271 281.710374029821 291.583720089868 +AACCTCCGCTCAGCCCAT AACCTCCGCTCAGCCCAT 334.559812073966 334.559812073966 341.720616471431 +ACATGCGGAGACGATAGA GAGCATCTTCGTGTCCCC 328.777637355840 13.663795446228 15.557079997899 +GCGATCCGGCCCGAAGGACGGTTAC GCGATCCGGCCCGAAGGACGGTTAC 343.160858693872 343.160858693872 350.466895660629 +TAGGCGCTACCAAAGAGACC TACGCGATACCAAAGAGACC 332.314713425979 316.910949741281 324.757961637335 +GCTTGACGGAGGGATTGCATACGTC GCTTGACGGAGGGCTTGCATAGGTC 337.905459739261 327.298908614928 335.399197844049 +CTTACGGTTTTGTGG CTTACGGTTTTGTGG 320.490499863910 320.490499863910 327.265086426630 +AGAATGCTTTGCACGGGAAAAAGAC AGAATGCTTTGCCCGGGAACAACAC 334.833702111547 324.032364174889 333.126901145695 +CCATCCGGGACCGTGTTA GTGGTGGACAGAAAGCAC 332.523437582445 67.486926898379 75.000548050396 +AGCAAATATTTAAAACTG AGCAAATATTTAAAACTG 316.766614411011 316.766614411011 323.642414073658 +ACTGACAAAAGGGACCTTTAGA CCCGAACAAAGGGACCTTGAGG 329.363491007070 296.438968024933 307.489562539207 +ATAGGACTCGGACGAAAG ATAGGACTCGGACGAAAG 326.913461392949 326.913461392949 333.867158802657 +TATAAGCTAGTTGAG TACCAGCTACTCGAG 312.936278497921 279.566780789215 288.458312256098 +AAAGCTGCCGTG GTCGATTGTCGG 320.846677260689 11.139130534401 12.281612095844 +AAAACTGGCACG AAAACTGGCACG 317.241607884236 317.241607884236 323.710646130150 +ATTCAGATAATGAAGGAC ATTAAGATAATGAACGAC 319.569864618292 300.532814450507 308.266203286509 +ACTCCGCCAGTAAAGAGCGATC ACGCCGCCAGTAAAGATCGATC 334.956435857922 324.130818124085 332.402089448274 +AAACAGGTGGTGCTTCTGGT AAGCAGGTGGCGCTTCCGGT 332.663618849926 317.895372912569 326.451852134041 +ACGTAAACTAGCGATCCTAC ACGCAAAACAGCGATCCTTC 327.648394671924 306.841804464239 316.109126582555 +ACTCTTCTGTAGTAATTT ACTCTTCTGTATTAATTT 318.111014934586 311.385476013207 318.854355601485 +GCCTGGATGAAA CCTCGCATTATA 314.875733564946 135.466989302349 145.485320466877 +ACAAACATCTTT ATAATTATCTTT 306.751127645023 230.133355279539 240.007445742650 +TTTATGTTGCACCCG GCCCTCCTGCAAGGG 322.725425600228 198.227797952416 210.186099128768 +CTCTGTGATTGC CTCTGTGATTAC 313.233837007613 299.818390981614 306.829729109027 +CAGGGGGTGTCTTCTTGCCA CACGGGGTGCCTTCTTACTA 335.170765818115 304.025892909583 313.686365730924 +ATAAACTTAAGCGGGGCT ATAAACTTAAGCGGGGCT 327.141275752020 327.141275752020 334.197558650535 +GTGGTACGTCTCGCACCGTATT GTGGTACGTCTCGCACCGTGTT 335.183465966560 331.876024565197 339.826055532683 +AGCCCCTACCGTGAGGTATGAA AGCCCCTACCGTGAGCAATGAA 334.930026765640 329.325029384246 337.230038559345 +CTACGTTTGGACTTTCAG TCCAATTCATGTACACCA 324.309529994597 130.265141330095 141.767454087853 +GGAGGCGTCGTCTAATGTGGCTGCA GGAGGCGTCGTCTAATGTGGCTGCA 340.715437268799 340.715437268799 347.974364433377 +TCACGCCCGTTACCATGC TCGCGCCAGTTACCATTC 334.223290685080 301.060585660525 310.242722525263 +GTGTGGAATAAC GTGTGGAACAAT 308.856970626568 289.055396946314 297.087438077958 +AACTAGGCACAGCGGAAGTGTTACA AACTACGCACAGCGGAATGGTTACA 336.064499532413 324.487528333723 332.510759640255 +AGTTGGGACCGCGGACCTCAGACGA ACTTGGGACCGCGGACCTCAGACGA 342.586917960981 339.716507068413 347.297152857653 +CGCAGGCCGCCCTATCCG GTGGGCAACCGTGATTAT 339.659476639126 111.730621568890 122.520566332669 +ACTCGGTTAAAGAAGTAGCGGG ACTCGGTTAAAGAAGTAGCGGG 332.661444291378 332.661444291378 339.831677127033 +GTCGCACTCACGCCAAGG GGCGCACTCACGCCAAGG 335.434179979611 327.968420416066 335.946784667129 +TTATGTGCCGTC TTATGTGCCGTC 315.550568136423 315.550568136423 321.978396903837 +ATACACTCCCTC ATACACTCCCTC 311.742937528520 311.742937528520 318.358302050000 +ATATATAAAACGCTT ATATATAAAACGCTT 310.835103721529 310.835103721529 317.439661067059 +TTGACCCAGCTCTGACAA TTGACCCAGCTCTGACCA 329.300611062468 323.357283025809 331.214889354503 +TCACCAAGGACATCGGTGAA CACTAATCTCTAGGCTACTG 331.893316827967 0.000000000000 0.000000000000 +AAATACTAATATCACGAGGCCT AAATACTAATATCACGAGGCCT 326.991443235419 326.991443235419 334.132256945596 +CATCTAAGGTTCCCCACG CTCCTAAGGTTCCCCCTA 328.386823878643 301.442125171364 311.286145433228 +TCAGGCAAGGCTAAAGAT TCATGCAAGGCTGAACCT 326.725811212726 296.261200224359 306.662120559413 +ACCGGTGTCTGACGAAGA CCCGGAGTCTGACGAAGA 331.461574428762 323.233483550268 330.841087730303 +TGCCATAACAGT TGCCATAACAGT 313.107834134787 313.107834134787 319.615154304331 +ATAACTCTGCTCTATAAA ATAACTCTGCTCTATAAA 317.633729090723 317.633729090723 324.531712635958 +TCCTGCGCAAATTACTAGAACCTAA GTCTCAGCGAGAACTCTATTAGGAT 332.050504643625 0.000000000000 0.000000000000 +GTATCGAGGTCGAGGAAAGCGCTGT GTATCAAGGTCGAGGAAAGCGCTGT 338.362288220608 334.066037941649 341.579718818818 +CAGCCTTTCAGG CAGACTTTCAGG 316.248469201820 292.111362062705 300.080584403201 +CTTGCGGTACATCGCAGTGTGAATA CTTGCGGTACATCGCAGTGTGAATA 335.474585471307 335.474585471307 342.613324305508 +TAAGAACTAGCACGCATAAG TTAGAACCAGTACGAAGAAT 326.081723019865 254.023237958651 266.323327012842 +TACATGTACATTTCGGCC TACATGTACATTTCGGCC 325.896662787897 325.896662787897 332.832044747960 +TAGCATGCGATTCCCTAAGAAG TATCATGCGATTCCCTAGGAAG 330.590797080638 318.020175239024 326.375059746915 +CGGTGTTGTACGCCGACC CAGTGGTGTATGGAGTCC 335.122388615945 245.504228310044 257.618564855382 +ACTTTACGCCCGTAG ACTTTACGCCCGTAG 324.104530886916 324.104530886916 330.841602836160 +GGTCTTGGTGGACAGATG AGTCTTGGTGTACAGATC 328.628141921468 313.674320134018 322.206848229767 +GTTCGGTCATGC GTTCGGTCATGC 317.988264837295 317.988264837295 324.445499068321 +CTCACGCTTCTCATTATT CTCACGCTTCTTTTTATT 323.247552234877 309.106689004420 317.389249379550 +TGCGAGCGTGAT TGTGAGCGTGAT 322.408515345848 302.521839005369 309.729023044907 +AGATAGGGGGCCCTCGAACCGCCCG AGATAGGGGGCCCTCGAACCGCCCG 345.102909485864 345.102909485864 352.627470314597 +AACCTACACAGG AACCTACACAGG 313.161040203465 313.161040203465 319.768122899840 +TCCGCCCGCCAC TCCGCCCGCCTC 330.719187584648 320.094784422350 327.317364425992 +ATATCGTGAGCCGTGTACCCGG ATAGCGCGATCCGTATACCCGC 336.660788072052 313.231213020956 322.468970039473 +GGAAGCGTTGACACCGAC GGAAGCGTTGACACCGAC 332.770993213501 332.770993213501 339.705211527389 +CGAGGCAAGGGATACGCAGCTCAGT CGAGACAAGGGATACGCAGCTCACT 340.305756168638 332.795008096728 340.846081667398 +CTACCGGCGTAGATATGC CTACCGGCGTAGATGGGA 328.520812530739 311.151850739834 319.645074203379 +GACTTAAGCGACTAC ACGGCTTAGCGTGGC 318.887440179040 10.664421060955 12.014569510602 +CTTAGGATTCTGGCTAATTTACGGT CTTAGGTTTCTGGCTAATTTACGGT 330.595304320228 326.378730172844 334.038450345878 +ATCCGCACGGAC TTACCGGACCCC 323.089768931227 0.000000000000 0.000000000000 +TCCCAGCTTTTGCAGTGGGATT TCCTAGCTTTTGCAGTGGGATC 335.062847253676 324.545742095465 332.720886054794 +TGGCTTCACGGC TGGCTTCACGGC 323.785812253507 323.785812253507 330.353929757972 +TAGGTTATTCCGACCATA ATGCGGGACACTGCCATG 322.422210409371 192.368508212637 205.559476403263 +AGCCTTTCGACTGAC AGCCTTTCGATTGAC 324.335884950598 312.595672426393 320.534233002808 +CGTGCAGCTTTCATG CGTGCAGCGTTCTTG 324.919682340793 305.582789800617 313.597104921942 +TCTACGTGGCGGGGACCGGG TCTACGCGGCTGTGAGCCGG 341.347076387025 299.430239048585 309.018341106927 +TCAAATTAAAGTTGGACTGACA TCAAATTAAAGTTGGACTGACA 326.350254889911 326.350254889911 333.467128526408 +CGGGGGCCGTGGTCACTGCTTTCAG CGGGAGCCCTGGTCACTGTTTTGAG 343.571008380319 318.537337281338 328.795128334431 +ACATTGCAGTCA TACAAATATGCA 314.282094430945 54.422588938214 59.864081596928 +GTTCCATAAGACTTA GTTCCATATGATTTA 313.492331047121 283.603344310509 292.314146114969 +TATTTGAAAGACATAGGGGCGT CGGTACACTGGTGCCGGAGGCA 329.927702442206 21.671006660567 25.185742081802 +AATGGCAATTGCGTA AATGGCAATTGCGTA 322.153531008784 322.153531008784 328.861389966064 +TGACCTCACAAATACGGGGTCG TGACCTCACAAATAAGGGGTCG 334.840837473162 327.359477549248 335.051513807130 +CTCTGAAAGAACCGGGCA CTTTGAAAGAACCGGGCA 330.717616406698 321.166071332522 329.380562417680 +TCCCCGATGCTTAAGACACGCGTGT TTCCCAAGGCTTAATACACTCGTGT 339.959965745411 305.790225053607 316.153725711522 +TGCACTTCGAGG TGCACTTCGAGG 318.856723935199 318.856723935199 325.349637181717 +AAGGGTCACCAGAAA AATAAGGTTCCTGCT 322.240165848681 53.802401211576 60.076587053449 +TAATAAGTCGTG TTATAAGTGTTG 306.473400984667 233.666269447857 244.153793035378 +CATGATGCCTAGTATGAT CATGATGCCTAGTAAGAT 321.893431646493 315.240330549201 322.344428969018 +TATGCGCGAACCTAC TATGCGCGAACCTCC 324.777133033446 320.584177151486 328.154789861490 +TGGCAACTCCCGCACATGCCCT TAGCAACTCCCGCCCCTGCCCT 341.428702525130 330.240007837364 339.446160220950 +AACGGTCCATAGTGT TTACTGCCTCTATCA 322.301936674505 92.184229884404 101.375195399772 +CTGGTTAGCGACTAGGGCGA CTGGTTAGCGACCAGGACGA 334.491616458730 325.265096818567 333.491322070668 +CTACGTGACATGGCCCCT CTACGTGACATGGCCCCT 332.520578697054 332.520578697054 339.639446777780 +CCCTCCTGGAAGAATCCATAGTATC TCCTCCTGGAAGAATGCATAGTATA 331.837538166233 315.597835592823 324.711900293230 +TCCGCGGTGCGGCGTCTA TCCGCGGTGCGGCGTCTA 340.070741817013 340.070741817013 346.961988197618 +TGAGGGCGGCTCGAAAGAGATAGGC TGAAGGCGGCTCGAAAGAGATAGGC 339.908445380864 335.397387022299 343.087954453266 +AGGATTGAATGCGGATTCACAATGA AGGATTGAATGCGGATTCACCATGA 333.075743741665 329.267518388258 337.096639984117 +CTGGCACGTGAAAGTGTA CTGGCACGTGAAAGTGTA 328.758681136274 328.758681136274 335.670742367360 +TATGAGCTCGCATTGACGAT TATCAGCTCGCATTGACGTT 330.199735252295 317.575655447674 325.732088199432 +CTCCGATACGCCCAACGTTCCT CTCATATACGCCCAACGATCCT 337.111273784768 319.264148345607 327.480755860446 +CTTGTAAGGAACCGATCGTGAAACT ATAAGATATATTTGTTAAATAGGAG 333.190199559369 0.000000000000 0.000000000000 +CGATCGGGGTTCGGTGCTAAGG CGATCGGGGTTCGGTGCTAAGG 337.958135639451 337.958135639451 345.185613424927 +GTGTCACTTAGTGGC GTGTCACTTAGGGGC 322.599730343108 309.940994981687 317.650231320028 +GTAAGGTGCTACGTGGGA GTAAGGTGGTACGTGGGA 329.791738219571 317.210999985374 324.885748886244 +GGGGACCACGGAGCGCTT GGACACCACGGAGCGCTT 338.493908487715 328.075398097466 336.508505160201 +ATGATTCGGCTGATGTTG ATAATCCGACTGACGTTA 326.617373214063 284.388619463998 294.654772560060 +CGTTGCACCTGTGCAGAC GGATGGACCTGTGGAAAC 333.382064119484 257.494983943418 268.960440677818 +GGGTATGGAACCGAGCTT GGGTATGGAACCGAGCTT 330.144774353530 330.144774353530 337.264117120300 +CCGTTTAGTACTTGAGCT TCGTTTAGTTATTGAGCT 325.101412938739 306.110310829587 314.815405000962 +TGCTCGCCGTTGATG TGCTCGCCGTTGATG 328.982207099090 328.982207099090 335.675432532816 +GCGGTATCCTGTTTAATAGCAC GCGGTATCCTGTTTAATAGCAC 330.022926941214 330.022926941214 337.146106169306 +ATTGCGTTGCCCAACGGGAAAC CTTCTAGATCTCGTGTTCTGTC 337.501498070251 30.100236916645 34.669973146917 +GAAGCGTACTAC TAATCGTACTAC 312.135520730872 290.578334792837 298.230744679790 +TCGCTTAAGTTGGACCCG TCGCTTAAGTTGGACCCG 330.825518413055 330.825518413055 337.820664158968 +TCAAAATATCGACGATCGGGTA TCAAAATATCGACTATCGGGAA 329.618484688871 316.847687836049 324.679351875168 +CTGTAAGTGCGT CTGTAAATGCTT 315.552108712421 284.478519286229 292.079870838245 +ATCCGTTGTCTGTAT TCCTATCTTGTCTCA 319.236231890115 0.000000000000 0.000000000000 +GCTGACGATCACCACTGAGT GCTGACGATCACCACTGAGT 332.887137941413 332.887137941413 339.934427106782 +GGGACCGTCTTGCCTGAA GGGACCGTCTTGCCTGAA 333.069804290103 333.069804290103 340.197004324922 +GACACTTCATGTTGAAGGAA GAGACATCAGGCTGAAGGAA 326.623921634303 290.160218586875 299.116365655134 +AAAGTTAATACT AAAGTTAATGGC 300.439776727757 274.074904518474 281.916563483524 +TTCACTCAGGCCGGGACT TTCACTCAGGCCGGGACT 333.775772265987 333.775772265987 340.923230531829 +TGAGGTGTACCTTCGATAACCCCTG TGAGGTGTACCTTCGATAACCCCTG 335.429777164410 335.429777164410 342.806589346267 +GGCGTTAGACTGGGACTCAT GGCGTTAGACTGGGACTCAT 332.314063072189 332.314063072189 339.465122151725 +GCGGAAAATTCCTCCTGGAGGC GCGGAAAATTCCTCCTGGAGGC 336.555274112350 336.555274112350 343.900546225317 +ACCCGCCGGGTTATG AACCGCCTGGTTATG 330.420331626846 303.591907390707 312.104888476139 +GTGAATCTGTGCCATGTAAGGAGTA GTGAATCTGTGCCATGTAAGGAGTA 332.502726598592 332.502726598592 339.772881635043 +TGTGCATATGCGAAGCAGCA TCTGCATATCCGAATCAGAA 333.922832979848 300.308875168811 310.038091502733 +TTCCGGGAGAGTCATTCAAGCGATT AAGCGAGGGGAGACCCTTCTTTACG 335.964252182230 0.000000000000 0.000000000000 +TATCACTTACTCCCCGGA TATCACTTACTCCCCGGA 327.110291836484 327.110291836484 334.201971889462 +ACGCATTAGTCTACCGTCTTGC ACGCATAAGTCTACCGTCATGC 333.304407097527 323.852674403909 331.529871399707 +ATAATAGATGAT ATAATAGATGAT 298.724754718266 298.724754718266 305.105559067848 +ACTTCATTGGCCTCTTTC GGGATAGGCCATTAGAAA 325.919067175754 0.000000000000 0.000000000000 +ATTTGCGTTAAGGAGACTCA ATTTGCGTTAAGGAGACTCA 327.736465255252 327.736465255252 334.763124999138 +GGGAGCCACGTCCCGCACTT TGGGTCTGTTCTACGGGTCC 340.351804998615 66.406913767236 75.840004728444 +AATGGTAGCTTGGAACAACT AATGGTATCTTGGAACAACG 327.871691250641 318.316542767859 326.227958663349 +ATACAATACCAA GTAAAACCCTAA 304.232095602747 144.099149360347 153.765346779177 +TTCTAGCGTCCCGGTACT TTCTAGCGTCCCGGTACT 330.589206350792 330.589206350792 337.589326383339 +TGGCCTTTTTTTAAGTAAAT TGGCCTTTTTTTAAGTAAAT 321.872839855264 321.872839855264 328.960538235470 +ATGCCTCCTTGTAATACAGCTAGGG ATGCCTCCTTGTAATACAGCTAGGG 333.823387389837 333.823387389837 341.214372867702 +GAGATAACACGCCAGGTTAA GGGATAACACGTCAGGTTAA 328.361333311021 307.593342393239 316.302295915689 +CACGGGCATCAAATG CACGGGCATCAAATG 324.403592056034 324.403592056034 331.200512200928 +TTAACTGTCGGTACTCTT TTAACTGTTGTTATTCTT 323.363845103162 280.866882856653 291.385325188001 +CACGATAGTCGTTGA TTCAAAGGTGAGTAA 321.130711058809 128.711478226556 138.526211474293 +GCCGGCCTAAGGACG GCCGGCCTAAGGACG 331.645745178000 331.645745178000 338.559056448220 +ACGCAAAAAGCGCAT ACGCACAAAGCGCAT 327.022026932331 320.836952659324 328.322467858379 +AGTCTTAAACGTTTATTG AGTCTTAATCGTTTATCG 318.625758737664 304.665747911265 312.350081264934 +CTGATCCGAGTTTGGCAGCT CTGAACCGAGTTTGGCAGCC 333.515248215999 325.848131196554 333.400292317441 +AATCCCCCTGCGCGGTGG AATCTCCCATCGCGGTGG 338.914827359853 316.871536337894 325.825478445471 +CCTATTGCGAGTGGAGTTTTTC CCTATTGCGAGTGGAGTTTTTC 330.398722345826 330.398722345826 337.555411082006 +TTTGTAGCACTCTCTAAG TTGGCACCGCTCTCGAGG 321.958335092061 240.192027201542 251.110839474907 +TATAAATCCAAACCG TATAAATCCAAACCG 314.941565560136 314.941565560136 321.679961211234 +TCTGGTCTACGGACCAGA TCAGGAATGCGGACCAGA 329.776739773812 300.216331414806 308.948988559444 +GCGATTCTGGAAATT GCGATTCTGGAAATT 319.582046008775 319.582046008775 326.323905265890 +AACTTATTGTAAGTGGCT ATCTTATTGTAAGTGGCT 321.893436025311 314.515961362535 321.963655827352 +AGTAGATCCACGCTC GGTGACGGTATAAGT 322.922652409125 0.000000000000 0.000000000000 +AGACAGCCAATTTGGACC AGACAGCCAATTTGGACC 328.624785345222 328.624785345222 335.740638937702 +TCTCCCCATCTGGTTGTGACAC CCTCCCCAACTGGCTGTGACAC 334.475047649994 322.522934177182 331.074093710435 +GTCCGGAGGGAAATT GTCCGGAGGGAAATT 323.695895292588 323.695895292588 330.657945722303 +GCATTGGTGTCTGAGAGACATC GCATTGGTGTCTGAGAGACATC 331.823924496399 331.823924496399 338.987388491055 +TACTCTTCGTTG TACTCTTCGTTG 310.392977904224 310.392977904224 316.757886088883 +ACCTACTACAAT ACCTACTACAAT 306.714175636802 306.714175636802 313.206330066357 +CCTTACACTATGCACCCG CCTTACACTATGCACCCG 328.839039592404 328.839039592404 335.870639509527 +ACCTGTAAGATCGAGTGATATCGGA ACCTGTAATATCGAGTGATATTGGC 332.453410863819 317.742424213661 326.757759926067 +ACCCGGAATCAC ACCCGGAATCCC 318.062682068531 309.456479129025 317.168301011525 +TTGCATCTAAACATTAAAGCAGAAG TTATGAGCCTTCCTAATCCCGCAGC 328.558653084235 92.581987798659 103.905955829058 +CCGCGCCAATAT ATCACAAAGGAA 320.315607213397 0.000000000000 0.000000000000 +ACCAGCCTAACTTTTGTT GCCAGCCTAACTGTTTTT 325.132735655685 301.134269729448 310.106656597025 +GCTTAATGCTGGCTACGC GCTTAATGCTGGCTATGC 330.692022417212 320.449633481302 328.448263187090 +TGGTCCGAACGGCAACAC TGGTCCGAACGGCAACAC 334.376696093757 334.376696093757 341.354168304032 +ACCCGGCCAGGG ACCCGGCCAGGG 328.950241125024 328.950241125024 335.887941031174 +ATACCATCGCCTAAGATTGGCC ATACCATAGCCTAAAATTAGCC 332.848856616901 309.476145134398 318.241854893043 +AAACCGGCAGTA AATCCGGCAGTA 317.201144426162 305.365706144159 312.555666368026 +CACAACAACGTTCAA CACAACAACGTTCAA 320.677535999798 320.677535999798 327.306012831155 +TGGGTCGCGTCCCAG CAGTACCCAACCCCG 333.123346113342 246.742853521647 257.809384715704 +CTCCTAAACATGAAGAACCAGAAAT CTCCTAAACATGAAGAACTAGAAAT 329.191307133388 321.504803533715 329.322148156357 +TACTATTTCGTTTGC TACTATTTCGACCTC 316.071427737447 286.990612725496 295.372324621468 +TCCCTGGCAGCAGGGTAGTTTTGAA TGGGCCAACGCTAACTGGTGCCTTG 337.792849188337 121.771136708049 135.395255297683 +GCTACATTACCGGCTCTC GCTACATTACCGGCTGCC 328.627216292430 317.865179052925 326.512498741613 +TGAGGCGGGTTATGCGTTAATG TGAGGCGGGTTATGCGTTAATT 333.877486753212 330.936597263706 338.449826872471 +CACTTATCCCGCAATAAT AACTTATAACGCAAGAAT 323.209491396864 285.261497168294 294.371363704620 +CGCGCCCGACGATCCACT CCCGCCTGACGTTCCACC 338.845192827705 310.574091806929 319.266524881313 +AGGCTCCTCTATGAA AGTCTCCTCTATAAA 320.534065615391 298.120651463631 306.645491242189 +TGGTAATTGCCTGAG TGGTAATTGCCTGAG 320.915868643696 320.915868643696 327.776118922002 +GTTCATTCTCTGGAGGGC AAGCCGGTACCTGCGGGG 328.693062285185 175.795089391355 189.885811626097 +GAACGCTGGTCGTCACAT GAACGCTGGTCGTCACAT 331.556430142525 331.556430142525 338.434764177077 +ATTGGGTGATCGTAAGTAAG ATTGGGTGATCGTAAGTAAG 325.612525475036 325.612525475036 332.668318324958 +AAGATATGTTTGTAGGCCGAAC AAGATATGTTTGTAGGCCGAAC 329.104322856711 329.104322856711 336.238620405649 +GTGTGGGTGCCC GTGTGGGTGCCC 324.309319433451 324.309319433451 331.095278789318 +GGCGTCCAGAAT GGCGTCCAGAAT 318.801851843066 318.801851843066 325.371455269089 +TGGCTCTGGATAGCTCCCCC AAGGACTATACAAAGCCAGT 336.053481241646 140.181856955550 152.924856339728 +GTAATCTACCATATG GGAATCTACCATATG 311.238267655019 297.565035184391 305.303043725135 +TAGAGACGTTTCCTCGCCTTTATCG CTTAGACACTCAGGTGTTGTTGGCA 334.313541801127 188.908254593789 203.829299320958 +TGCCAGCTTGTGGCGAGTAT TGCCAGCTTGTGGCGAGTAT 335.045486995831 335.045486995831 342.139981881034 +TTCCGAGCCTGCCGAACAGG TTCCGAGCCTGCACACCCGG 337.981474268738 315.139472928922 324.695198064855 +TAGGCGACGGCTAACTGG ATTAACACAGAATGTAAC 332.611090573720 117.192190429110 129.003910625976 +CTATCAAGCCTCAGGTATCGCT CTATCAAGCCACAGGTATCGCT 332.013904222950 327.008869759689 334.361264460119 +GCGTTGAACAGTGCGTACGT GCGTTGAACAGTGCGTACGG 334.922740354371 331.487402480161 338.817709417777 +ATGACCTGTACGCTG TCACACCCTCGGTCG 324.096877104678 76.811640858322 84.402210669196 +TAGTTATCTTCTACCTGATGTAAGC TAATTATCTTCTACCAGATGTTAGT 327.657446380070 306.418404503197 315.329248976153 +CGGTGCAGTGCAAAATGCGAGTAGT CGGTGCAGTGCAAAGTGCGCGTAGT 338.234766348875 331.605213173016 339.892622743888 +TTGTTGCCATGTAAA TTGTTGCCATGTAAA 318.513544450161 318.513544450161 325.252135026214 +GTGAAACGTTTGCTGTGG GTGAAACGTTTGCTGTGG 328.992886751469 328.992886751469 335.904977195626 +GTTGGAATTGCGGCAAAG GTTGGAATTGCGGCAAAG 329.419693351400 329.419693351400 336.405013425441 +ATTTCCCGCAGCTTCGAC AGTTCCTGCAGCGTTGAC 331.921604125857 275.704268703669 286.426092962088 +GGGCTTCAACTCTGCAGA AACTTCCGATAGTACCCG 330.715768712068 0.000000000000 0.000000000000 +GTAGTACGTCACTTAGGGTATA GTAGTACGTCACTTAGGGTCTA 326.370618283308 323.603568352225 331.248373111091 +GTGATATTTTCACTGCTAGG GTGATATGTTCACTGCTAGG 324.660738933852 318.097303613935 325.739236970914 +TTAGAGCCTAGATATCGC TGAGAGCCTACATATCGC 323.737203572163 308.667021683076 316.877776506437 +GGCCGGTTTGAT GGCCGGTTTGAT 319.628239742093 319.628239742093 326.261640206404 +CGATAGTGGAAG CGTTTGTGGAAG 311.281036639251 282.148143916626 290.555750610531 +TTTATAGACGGTCCA TTAATAGAGGGTCCA 318.271295635730 289.396103709646 297.309467179028 +CTGGTGTAAATTTTTATT CTGGTGTAAATTTTTATT 316.242691168716 316.242691168716 323.159373336702 +CACGCAAAGACGAGTCGAGA CGCGCTAAGACGAGTCGAGC 333.509565254824 317.485155999984 326.212807278748 +GCCCCTGTCCGGACG GCCCCTGTCCGGGAG 334.323987481155 318.296185453565 326.861053252518 +AAGCCGGCACTA TAGCCGGCACTA 320.841087637638 315.681196183552 323.022375994306 +CCCCCTTGTTCT CCCACTGGTTCT 316.846355213233 271.731156926471 280.431509587266 +TTTGGGTCCGATACGTTA TTTGGGTCCGACAGGTTC 326.526907539640 304.936001347139 313.235900619784 +CTAGTAAGGTATAGAACT CTAGTAAGGTATAGAAAT 317.080850733325 306.909216639793 314.322636687809 +ACACTGCAACATGCAAGGCA TCTCTGCACCATACTAAGCA 334.279193488462 287.410425546330 298.337166942414 +CGGATACGTGAGCTGACCGTCACTG AGGGTTCATGAGCTGACCGTCACTG 339.003807135660 321.603408699629 331.135793238806 +CACAATCCTTTCATAGGTCTGATGT CACAAACCTAACATATGTCTGGTGT 330.769444740710 302.850977615703 312.757796843091 +ATAGGTGTATTACTTCCC ATAAGTGCATTACTTCCC 320.813390192036 307.309069291562 315.330380672069 +TGCCCGTGCTAATTTCGG TGCCCGTGCGAATTTCGA 331.916704163337 321.788326255281 329.840680268581 +CATACCTCCCGTGTGACC CAGACCTCCTGTCTGAGC 330.790040366578 275.685519938138 285.797334919463 +CATGCGAATGAACGTGGTCAAC CATGCGAATGAACGTGGTCAAC 333.744727353783 333.744727353783 340.783101574584 +GGTCCAGCCAGC GGGCCGGCCAGA 323.997990076396 258.335190180963 269.085542668344 +TCAGCCTCTTACTACGAA TCAGCCTCTTACTACGAA 325.504908314887 325.504908314887 332.433608839830 +TTCCACTCGGTA TTTAACTCGTCA 315.163712062905 240.754025130773 251.279252359821 +GTACCCATCCGTTAACCACATT GTACCCATCAGTTAACCACATT 330.965975035173 322.116695925385 329.685541486863 +TCGGATTTGTGG TCGTATATGCGT 315.144792522878 261.302945400146 270.745105924733 +CAGGGGTTTGTAGCTTGCAT AAAAGGTCGACATGAAACTC 331.592676687332 178.298382734095 192.121517719714 +GTATGTTTAGATTCCGGT GTATGTTCACATTCCGGT 322.407865448171 309.819055451666 317.927267476480 +ATTTGAAGGTTGTTCATT ATTTGAAGGTTGTTTATT 320.745098479303 308.460034775676 316.604473343016 +ACGGTGGCCGGCGACCTCAGCCGCA TCGGTCTCCGGCGAGCTCAGTCGCA 349.319476545950 322.805970408218 332.569513311728 +GGTGAAACATGGGTGGTATGATAAT GGTGAAACATGGGTGATATGATAAT 331.043479784795 326.332708138854 334.184817037240 +AAGTGTAATCGACTA AAGTGTAATCGACTA 315.631885291475 315.631885291475 322.265747968200 +GAAGAAAATCCGTTTAGT GAAGAAATTTCGATTAAT 320.919322767873 264.519341261783 274.695121470457 +GTATACCTACCGCCAGGCGTAA GTATACCTACCGCCAGGCGTAA 334.199822075507 334.199822075507 341.373552239777 +GGGCTTGCAAATGCACTAGT GGGCTCGCAAACGCACATGT 332.244946736482 316.459896257268 324.959991904042 +AGTAACATTACCCCTCTT AGTAACATTACCCCTCTT 322.762790159849 322.762790159849 329.855166947719 +ATCGTTCCAGATTCA ATAGTTCCACATTGA 319.593588349254 263.703483135864 273.889968541208 +ATCTAGACAAAT CTCTAGACAAAT 303.769798710798 299.975537128382 306.767744251403 +GACTCGTGCCCAAAGGCG GACTCGGGCCTAAACGCG 335.453032103533 305.944507861049 314.863300937474 +ATACCTGAAAGATACATAGGGAGGG ATACCCTAACGATACATAGGGAGGG 330.838488925824 319.951646252133 328.421632500441 +CTCCTTCTATATCTTTACGC CTCCTTCTATATCTTTACGC 323.614245457101 323.614245457101 330.629799804542 +GGAACGACAGCGGAC GGAGCGACATCGGAG 329.215799373567 299.203726696696 307.870410467643 +AATTAAACTTCACTTGCTGCTA AATTAAACCGCACTTGCTGCTA 326.774684409898 319.760152873130 327.648310708541 +CGGGGTGGTTTTAACGCCTT CGGGGTGGTTTTAACGCCTT 334.293634381856 334.293634381856 341.479503422465 +TTACAAGTGTAC TTACAAGTGTAC 306.490429220235 306.490429220235 312.860219905519 +GGTTTTACGCACTCGTTT GGTTGTACGCACCCGTTT 327.475666665468 315.253671257952 323.597273287183 +GCGACCGGGATATTCGTG GCGACTGTGATATTCGTG 332.081729064974 312.612274551236 320.900746018201 +AAATCATAGATGTTTGTGTG AAATCTTAGACGTTTGTGTG 322.490478539460 311.233326719324 319.056840527317 +CCGCGGCTGGCCAGTGCG CCGGGGCTGGTCAGTGCG 343.595298549215 320.692138927062 329.428906251676 +TTAGCGCCCCCT TTAGAGGCCCGT 323.638703533546 223.193289824279 232.649538093037 +CGACTCCTTGCG CAACTCGTTGCG 321.093094443631 287.627523199553 294.941907978827 +CAAATAGGGGAGAAATAGGGATGAT CAAATAGGGGAGACATAGGGATGAT 329.829565362939 326.229605874518 334.271529764424 +TTATGACTCTAT TTATGACTCTAT 302.351970012031 302.351970012031 308.749474038831 +CTTCGCACGGCTCTGAGGTG CTTCGCACGGCTCTGAGGTG 336.758970783676 336.758970783676 343.832413942225 +GGTAGATCGTCTAACACA GGTGGATAGTCTAGCACA 324.183145729044 288.591703366550 297.757637466896 +TCCGAAGTTATTTGT TCCGAAGTTATTCGC 317.020120014950 307.809184902247 315.094442186122 +ACGCTATAGGTATCCAATGTGC TGAGGGGTTGCCTCGCGCGAGA 330.668816473345 168.517116213496 182.239452513402 +ATCCCGGTGTTT CTCGCGTTCTTT 316.963839881018 238.291774003272 248.409713749044 +TGTAAATATTGAGAACAT CTACTTGTAAAAATTATC 316.552099039474 4.196619607978 4.793908669173 +TAAGATGGCGGTGCTAAGGT TAAGATGGCGGTGCTAAGCT 331.748855931570 328.095422595895 335.422537658975 +CAGAGAATCGTATTG TAGAGAATCGTATTG 316.058437382107 309.336246420051 316.478881368674 +ACACTATCGCAT ACACTATTGCAT 313.287559156327 289.002177419194 296.901322522936 +TTTGCAACAAACTGCGGG TTTGCAACAAACTGCGGG 330.987598215230 330.987598215230 337.964638501570 +TGCGCATTGACCGTATGA TGAGCATTGCAAATATGA 330.724505651688 291.707022981886 301.075127848151 +CTGCAACGTGGCTGACGTGG CTGCAACGAGGCTGACGTGG 337.828829590951 332.158127068020 339.396841315526 +AATCTAGGTATTCTGTGCACACTAG AATCTAGGTATTCGGTAGACACTAG 330.093652788304 312.242546896813 320.851841260874 +ACGGCGAAACTGTCCCGT ACGGCGAAACTGTCCCGT 335.434181438533 335.434181438533 342.388102607306 +AACGAGTACTCTTTGCACGCGC AACGAGTACTCTTTGCAAGCGC 336.704192051484 329.670890289599 337.116780737979 +ACGGCCACCCGCGTAAGCCAGT ACGACCACCCGCGCTAGCCAGT 343.225833795195 335.051197281573 343.935168403898 +AGGTGGAAACACCTCTTTATAG AGGTGGAAACACCTCTTTATAG 327.767855921026 327.767855921026 335.018188736768 +GATGCACCTCTTCCTTGT GATGCACCTAGTCCTTGT 328.015528597840 315.813502381439 323.875578738504 +CACACGTTAAATCAGCCGTGAC CACACGTTAAATCAGCCGTGAC 333.178564800892 333.178564800892 340.213130099053 +GTGAAAAACGCTAAAACT GAGAAAAACGCTGAAACT 322.651310853696 305.102360687419 313.386535231522 +CCTCTAAGCGTAAACTGGCGTA CCTCTAAGCGTAAACTGGCGTA 332.731489287653 332.731489287653 339.833318025496 +CTTAACGGATCAGGTGCC CTTAACGGATGAGGTACC 329.145552153189 309.298607409059 318.329957990940 +ACCAGCGCGGGTCCAAGCCGGG AACAGCGCGGGTCAAAGCCCGG 346.277068986675 323.989295670246 332.757718044482 +GGTCGCCCTTGGTTG GGTCGCCCTTGGTTG 329.378530415267 329.378530415267 336.338999066477 +GGTCTTTCCTGATAG GGTCTTTCCTGCTAG 317.502443546558 312.248143518990 319.891057909696 +ATGGGGTCCCTTGGATAA ATGGGCTCACTTGGAGAA 327.935969186382 299.834873075580 308.768543906097 +AGACTTTATCTGCGTTAG CGACTTAATCTGCATTGG 322.530698528657 290.771157085417 299.660782150129 +TTCTAGCCTGTGCCATTCATTCGCG TGTCTTTAATCATTCACTGCATTTT 337.662752230513 0.000000000000 0.000000000000 +GTGGCGTGATCCGCCTATGATG GTGGCGTGATACGCCTATGATG 336.412147017699 328.612405834565 336.461928234461 +ATTATCCTGCAGAGCAGTGA ATTATCCTGCAGAGCAGTGA 329.338095898179 329.338095898179 336.434344794516 +TGGGGTCACGCATGGCACCG TCGTTTCCGCGTAGACACAT 340.932042495783 219.946305376159 234.105527008232 +GATCAGTCATGCCGGGCA TCGCTCACTGGGAGCTTT 333.615526310877 70.980108149928 78.760105051004 +GAGAAACTACCAACCACC GAGAAACTACCAACCACC 326.141893376412 326.141893376412 333.228102476867 +ATTGACTAGCCTGGAGGC ATTGACTCGCCTGGAGGC 329.814592678115 325.021169884577 332.608693895943 +ACGGAGTCTATTGTC AAGGAGTCTATTGTC 319.985406470302 306.562131243465 313.933774764732 +TGGCTGATTGGCCATTATTC TGGGTGATTGGCCATTATTC 329.296931346104 317.833565606159 325.604567901985 +TGTTACGGACGGACG TGTCAGGGACGCACG 327.360275144725 294.262459737383 302.641243348176 +AACGCTCCTTCTGACGCTCTGTACC AACTCTCCTGCTGACGCTCTGCACC 338.281645508427 324.671859746526 333.350952165099 +CTTTCTACAAGCTTCCTCAGCCGAT CTTTCTACAAGCTTCCTCAGCCGAT 334.726254329200 334.726254329200 342.014376175497 +CTGGGAGCGTGT CTGGGAGCGTGT 322.225101677135 322.225101677135 328.814148322577 +GCAAGTGCTCCATACGTCAGGA GCAAGTGCTCCATACGTCAGGA 335.151235041562 335.151235041562 342.311691193007 +TTGAACTCGAAGTCATTCCAGCCCA TTGTACTTGAAGCCATTCCAGCCCA 336.086839523526 320.167401115734 329.871603131488 +CGCGAGTGTTAATTTTCATC CGCGAGTGTTAATTTTCATC 326.706848503872 326.706848503872 333.607112791387 +CTCGAAATCAGCCTA CAGGCAGATCCGATA 320.433152901209 106.785241479223 115.721577071620 +CCCACGTTGGATCCCTCA CACACCCAGCGTTCCCCT 332.434732686040 168.247008949882 182.904021311056 +TCTTCCCGAAGGCAACTA TTGCTAGCCACCGCTACC 328.742342595200 0.000000000000 0.000000000000 +TCGGTCGCCCGGCCA TCGGTCGCCCGGCCA 338.280658715332 338.280658715332 345.187830452143 +TTCCGGTCTAGAGAT GTCCCGTCTAGAGAT 320.290802240471 304.664615132156 311.959714415134 +TGCCACGTTTGG TTCCACCTTTGG 320.673142294482 289.947588641272 298.070057172917 +CTGAGAACATGG GTTAAGTTAAAA 311.861058614240 0.000000000000 0.000000000000 +TTCCCATTTGCAACG TTCCCATTTGCAACG 323.462465099785 323.462465099785 330.249305701134 +AAGAGTTACTGGATTACC ATTACATGTTGAGCTAAC 321.460730667492 45.295339819983 50.838550888654 +AAAACTCATTACCGTCAATGTGCCG AAAACTCATTACCGTCAATGTGCCG 334.427238998430 334.427238998430 341.612309670089 +CGGGAAATCATGCACTTC CGGGAAATCATGCAGTTC 327.837268831791 315.451695083719 322.783155312403 +TCTTGTCCATCAGACAGA ACTTGTCGACCAGACAGA 325.674390994389 302.134938899268 310.971599041593 +TAACTTTGCCGTGCGTTC TTGCATAGCATGCAATTT 330.185153783687 164.313401929230 176.255433198370 +GAGATTGACGACTAACCT GAGATTGACGACTAACCT 324.455556483854 324.455556483854 331.390094199272 +CCGCGAGTCCACGGCGTAAG CCGCGAGTCCACGGCGTAAG 339.144645429285 339.144645429285 346.143799055883 +TGACGGCGAGTGAGTTTT GGATTAGTCATCATCGCG 330.479181965474 47.596928980319 53.756086430128 +AGCAACGTCTAAACTCGATT AGCAACGTCTAAACTCGATT 328.165361220407 328.165361220407 335.096430259205 +GACTGCTAGCTGGCT GAATTATCACGTTCC 326.800285243793 145.119103179615 155.884495817015 +ACCCTCGTAATTATTCCG GCCTCGCTAGAGAGGGAA 324.895708998494 137.294573703070 148.579615770821 +AGCGGGCCGGCGCCCACGGTTAAAT AGCGGGACGGCTCCCACGGATAAAT 346.469332548688 327.821835178262 336.666611815380 +ATACACCAGTCTCCACTGTGTCAGC ATACACCAGTCTCCACTGTGTCAGC 336.084026510573 336.084026510573 343.378799103379 +AGGCAACGGTGTTTTTAGTTTCAAG AGGCAACGGTGCTTTTAGTTTCAAG 332.791413039949 329.450267766259 337.042137604359 +CATAGTATGGTTCATAATTTAA CAAAGACGGGATTATCGGACTT 319.805110852434 183.915523269303 197.052684250500 +ACGCCCCCCGGATTCGGC ACGCCCCGCGGATTCGGC 341.276243356091 331.161466849465 338.916726408046 +TTGTCTTTCGAGCGG TTGTGTGTCCAACCA 325.282149224729 222.156190895896 232.598751363606 +CTCCCGGGCGCCAGATGG CTCCCGGGCGCCAGATGG 339.311369482363 339.311369482363 346.533954436015 +GCACACTTTGTCGGTAAGTGAGGAC GCACACTTTGTCGATAGCTGAGGAC 336.007697297418 324.718456248143 333.231550897913 +ATATAGTCTCCGTGT ATATAGTCTCCGTGT 317.670914323835 317.670914323835 324.391677867921 +ATTCGAGGCGGCTCTGAGGT GTTCGAGGCGGCTCCGTGGT 336.308093587336 324.901517012421 333.408982676338 +CGCTAGCTCTCAGCATAA CGCTAGCTCTCAGCTTAA 327.750384254086 320.869735908381 328.340764122397 +TACTCACTCTAATTCAAC TACTAACTCCAATTGATC 319.232623419269 263.254120288054 273.886368603122 +CTTGGAATCTTGTTCCGC CTTGGAATCTTGTTCCGC 327.748687917447 327.748687917447 334.748570070981 +TCTCTCTCTACTGAAGATGT TCTCTCTCTACTGAAGATGT 324.955441779254 324.955441779254 332.010856900828 +AGATAGCTAAACTATGGTATTT AGATAGCTAAACTATTGTATTT 322.379524984208 314.920600083519 322.617736532767 +TCCAGTAGGCCTGCACACCC TCCTGTAGGCCTGCACACCC 337.082762403533 331.065336200951 338.847431016473 +CCGGTCGGTTAA CCGAGCGGTCAA 318.298891942113 278.422489888824 287.988343096102 +TGCGGTATTACACAGTAC TGCGGTACTAGACCGGAC 325.257155822510 277.211655556984 287.317024862518 +TAGGTTGAGATGTTCGGGCG TAGGTTGAGATGTGCGGGCG 332.954776706522 326.896199598776 334.681687928661 +TCGAACCTCTAA TCGAACCTCTAA 311.195901304215 311.195901304215 317.645142918670 +GGCCGTTGGGTGAGATGT GGCCGTTGGGTGAGATGT 333.461534051827 333.461534051827 340.580014223162 +TTACGATTAGTAGAC TTACGATTAGTAGAC 313.227722774298 313.227722774298 319.837422500332 +GCTGGCAGACTTCCGGGA GCAGGTAGACTGGCGGGA 335.491766864185 300.268242946003 309.735154150044 diff --git a/pkg/obithermo/testdata/ecoprimers_tm.tsv b/pkg/obithermo/testdata/ecoprimers_tm.tsv new file mode 100644 index 0000000..55229e4 --- /dev/null +++ b/pkg/obithermo/testdata/ecoprimers_tm.tsv @@ -0,0 +1,13 @@ +AAACTGGGATTAGATACC 48.5 +ACACCTCGACCTGACGTT 58.8 +ACTTACCATGTTACGACT 50.8 +CAAACTGGGATTAGATAC 47.5 +CCAAGTGCACCTTCCGGT 60.7 +CCTGTTCTAGAACCGATA 50.8 +CTACACCTCGACCTGACG 57.5 +CTTTTGCCACAGAGACGG 57.0 +GCCAAGTCCTTTGGGTTT 56.2 +GGGATTAGATACCCCACT 52.6 +GTTCTAGAACAGGCTCCT 53.6 +TCGTACCTTTTGCATCAT 52.2 +TTCTAGAACAGGCTCCTC 53.3 diff --git a/pkg/obitools/obialign/obialign.go b/pkg/obitools/obialign/obialign.go new file mode 100644 index 0000000..7d79db0 --- /dev/null +++ b/pkg/obitools/obialign/obialign.go @@ -0,0 +1,340 @@ +package obialign + +import ( + "os" + "slices" + "sort" + "sync" + "sync/atomic" + + align "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obialign" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obidefault" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiiter" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiitercsv" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq" + sim "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obisimilarity" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obitools/obiconvert" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obitools/obicsv" + "github.com/schollz/progressbar/v3" + log "github.com/sirupsen/logrus" +) + +// Params are the parameters of the comparison. +type Params struct { + Threshold float64 // 0: no threshold + Normalized bool + Distance bool + Reference sim.Reference + AcceptAmbiguous bool + ExtraColumns bool + Workers int +} + +// ParamsFromCLI collects the parameters from the command line options. +func ParamsFromCLI() Params { + return Params{ + Threshold: CLIThreshold(), + Normalized: CLINormalized(), + Distance: CLIDistance(), + Reference: CLIReference(), + AcceptAmbiguous: CLIAcceptAmbiguous(), + ExtraColumns: CLIExtraColumns(), + Workers: obidefault.ParallelWorkers(), + } +} + +// dataset is a set of usable sequences with their description. +type dataset struct { + seqs obiseq.BioSequenceSlice + entries []sim.Entry +} + +// readDataset keeps the sequences that can be compared. +func readDataset(db obiseq.BioSequenceSlice, name string, p Params) dataset { + var d dataset + ambiguous := make([]bool, 0, len(db)) + for _, s := range db { + ok, amb := sim.Classify(s.Sequence(), p.AcceptAmbiguous) + if !ok { + continue + } + d.seqs = append(d.seqs, s) + ambiguous = append(ambiguous, amb) + } + if discarded := len(db) - len(d.seqs); discarded > 0 { + log.Warnf("Discarded %d sequences of %s that did not contain only IUPAC nucleotides (or only 'acgt' without --accept-ambiguous)", + discarded, name) + } + if len(d.seqs) == 0 { + log.Fatalf("No valid sequences in %s.", name) + } + d.entries = sim.BuildEntries(len(d.seqs), func(i int) ([]byte, int, bool) { + return d.seqs[i].Sequence(), d.seqs[i].Count(), ambiguous[i] + }, p.Workers) + for i := range d.entries { + if d.entries[i].Length > align.MaxBandedLCSLength { + log.Fatalf("The sequence %s is too long (%d bp, maximum %d bp)", + d.seqs[i].Id(), d.entries[i].Length, align.MaxBandedLCSLength) + } + } + return d +} + +// pair is a pair of sequences that reaches the threshold: the index of the +// second one, and the score. +type pair struct { + j int + score float64 +} + +// aligner compares the sequences of a dataset with the ones of an index. +type aligner struct { + queries []sim.Entry + index *sim.Index + self bool // the index holds the queries: only the pairs i < j are reported + sc *sim.Scoring +} + +// row finds the sequences of the index that reach the threshold with the +// query i, and appends them to out in the order of the index. +// +// Only the buckets of compatible lengths are visited, and the 4-mer filter +// works on blocks of sequences. +func (a *aligner) row(i int, buf *align.BandedLCSBuffer, commons *sim.Commons, out []pair) []pair { + q := &a.queries[i] + out = out[:0] + for _, length := range a.index.Lengths() { + if !a.sc.LengthCompatible(q.Length, length) { + continue + } + b := a.index.Bucket(length) + commons.Reset() + start := 0 + if a.self { + // the members are in the order of the dataset + start = sort.Search(len(b.Members), func(k int) bool { return b.Members[k].ID > i }) + } + for k := start; k < len(b.Members); k++ { + m := &b.Members[k] + if s, ok := a.sc.PairScore(q, &m.Entry, b.Common(k, q.Sparse, commons), buf); ok && a.sc.Passes(s) { + out = append(out, pair{m.ID, a.sc.Reported(s)}) + } + } + } + slices.SortFunc(out, func(x, y pair) int { return x.j - y.j }) + return out +} + +func progressBar(n int) *progressbar.ProgressBar { + if !obiconvert.CLIProgressBar() { + return nil + } + return progressbar.NewOptions(n, + progressbar.OptionSetWriter(os.Stderr), + progressbar.OptionSetWidth(15), + progressbar.OptionShowIts(), + progressbar.OptionSetPredictTime(true), + progressbar.OptionSetDescription("[Comparing sequences]"), + ) +} + +// maxPairsWithoutThreshold is the number of pairs above which a missing +// threshold is reported, the table having then as many rows. +const maxPairsWithoutThreshold = 1e8 + +// maxRowsPerRun is the number of rows of the table that a run is expected to +// produce at most when every pair is reported. +const maxRowsPerRun = 100000 + +// runShape chooses how many sequences a run covers, and how many runs a +// worker may be ahead of the oldest unfinished one. Without threshold every +// pair is a row of the table, so the runs are shortened to keep their size +// reasonable; otherwise many pairs are rejected and the runs are longer, to +// reduce the synchronizations. +func runShape(n, targets int, noThreshold bool, workers int) (rowsPerRun, window int) { + rowsPerRun = min(max(n/(workers*32), 1), 256) + if noThreshold { + rowsPerRun = min(rowsPerRun, max(maxRowsPerRun/max(targets, 1), 1)) + } + return rowsPerRun, 4 * workers +} + +// Compare compares every sequence of db1 with every sequence of db2, or with +// the following ones of db1 if db2 is nil, and returns the pairs that reach the +// threshold as a table with the columns id1, id2 and score (and count1, +// count2, length1 and length2 if p.ExtraColumns). +// +// The rows are in the order of the sequences of db1, then of the ones of +// db2, whatever the number of workers. +// +// The workers take runs of rows of the matrix of the pairs, as in sumatra, +// but each run is sent to the writer with its rank: the writer puts them back +// in order, so that the output neither depends on the timing nor needs a lock +// on the output. +func Compare(db1, db2 obiseq.BioSequenceSlice, source string, p Params, onProgress func(done int)) *obiitercsv.ICSVRecord { + d1 := readDataset(db1, "the input", p) + d2 := d1 + self := db2 == nil + if !self { + d2 = readDataset(db2, "the reference file", p) + } + log.Infof("%d sequences", len(d1.entries)) + if !self { + log.Infof("%d reference sequences", len(d2.entries)) + } + + all := d1.entries + if !self { + all = append(slices.Clone(d1.entries), d2.entries...) + } + sc, err := sim.NewScoring(sim.Params{ + Threshold: p.Threshold, + Normalized: p.Normalized, + Distance: p.Distance, + Reference: p.Reference, + Ratio: 1e300, // no limit on the counts + }, all) + if err != nil { + log.Fatalf("Wrong threshold: %v", err) + } + + index := sim.NewIndex() + for j := range d2.entries { + index.Add(d2.entries[j], j) + } + a := &aligner{queries: d1.entries, index: index, self: self, sc: sc} + + iterator := obiitercsv.NewICSVRecord() + for _, f := range []string{"id1", "id2", "score"} { + iterator.AppendField(f) + } + if p.ExtraColumns { + for _, f := range []string{"count1", "count2", "length1", "length2"} { + iterator.AppendField(f) + } + } + + n := len(d1.entries) + workers := max(p.Workers, 1) + + pairs := float64(n) * float64(len(d2.entries)) + if self { + pairs = float64(n) * float64(n-1) / 2 + } + log.Infof("%.0f pairs of sequences to compare", pairs) + if p.Threshold == 0 && pairs > maxPairsWithoutThreshold { + log.Warnf("No threshold has been given: the table will have %.0f rows, "+ + "use the option --threshold to keep only the similar pairs", pairs) + } + + rowsPerRun, window := runShape(n, len(d2.entries), p.Threshold == 0, workers) + runs := (n + rowsPerRun - 1) / rowsPerRun + + iterator.Add(workers) + go func() { + iterator.WaitAndClose() + }() + + // The runs are sent to the writer with their rank, and the writer holds + // those that arrive before the previous ones. To bound that memory, a + // worker does not start a run more than `window` runs after the oldest run + // that is not finished. + var ( + lock sync.Mutex + progress = sync.NewCond(&lock) + finished = make([]bool, runs) + oldest int + rows int + ) + var next atomic.Int64 + var wg sync.WaitGroup + for w := 0; w < workers; w++ { + wg.Add(1) + go func() { + defer wg.Done() + defer iterator.Done() + var buf align.BandedLCSBuffer + var commons sim.Commons + var pairs []pair + for { + run := int(next.Add(1)) - 1 + if run >= runs { + return + } + lock.Lock() + for run-oldest >= window { + progress.Wait() + } + lock.Unlock() + + from, to := run*rowsPerRun, min((run+1)*rowsPerRun, n) + batch := make([]obiitercsv.CSVRecord, 0, to-from) + for i := from; i < to; i++ { + pairs = a.row(i, &buf, &commons, pairs) + for _, pr := range pairs { + rec := obiitercsv.CSVRecord{ + "id1": d1.seqs[i].Id(), + "id2": d2.seqs[pr.j].Id(), + "score": pr.score, + } + if p.ExtraColumns { + rec["count1"], rec["count2"] = d1.entries[i].Count, d2.entries[pr.j].Count + rec["length1"], rec["length2"] = d1.entries[i].Length, d2.entries[pr.j].Length + } + batch = append(batch, rec) + } + } + iterator.Push(obiitercsv.MakeCSVRecordBatch(source, run, batch)) + + lock.Lock() + finished[run] = true + for oldest < runs && finished[oldest] { + oldest++ + } + rows += to - from + done := rows + progress.Broadcast() + lock.Unlock() + if onProgress != nil { + onProgress(done) + } + } + }() + } + return iterator +} + +// CLIOBIAlign compares the sequences as the command line asks, and returns the table of the pairs. +func CLIOBIAlign(iterator obiiter.IBioSequence) *obiitercsv.ICSVRecord { + source, db := iterator.Load() + log.Infof("Sequence dataset of %d sequences loaded", len(db)) + + var db2 obiseq.BioSequenceSlice + if CLIHasReferenceDB() { + db2 = CLIReferenceDB() + if db2 == nil { + db2 = obiseq.BioSequenceSlice{} + } + } + + var bar *progressbar.ProgressBar + barChecked := false // the bar is created once, even when it is disabled + last := int64(0) + var lock sync.Mutex + return Compare(db, db2, source, ParamsFromCLI(), func(done int) { + lock.Lock() + defer lock.Unlock() + if !barChecked { + bar, barChecked = progressBar(len(db)), true + } + if bar != nil { + bar.Add(int(int64(done) - last)) + } + last = int64(done) + }) +} + +// CLIWriteTable writes the table to the output file given by --out, or to the standard output. +func CLIWriteTable(table *obiitercsv.ICSVRecord) { + obicsv.CLICSVWriter(table, true) +} diff --git a/pkg/obitools/obialign/obialign_test.go b/pkg/obitools/obialign/obialign_test.go new file mode 100644 index 0000000..679a086 --- /dev/null +++ b/pkg/obitools/obialign/obialign_test.go @@ -0,0 +1,271 @@ +package obialign + +import ( + "bufio" + "fmt" + "math/rand" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + "testing" + + align "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obialign" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiitercsv" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq" + sim "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obisimilarity" +) + +// readTestFasta reads a fasta file whose headers are "id count=N; ...". +func readTestFasta(t testing.TB, filename string) obiseq.BioSequenceSlice { + t.Helper() + f, err := os.Open(filename) + if err != nil { + t.Fatal(err) + } + defer f.Close() + + var db obiseq.BioSequenceSlice + var header string + var seq strings.Builder + flush := func() { + if header == "" { + return + } + fields := strings.Fields(header) + s := obiseq.NewBioSequence(fields[0], []byte(seq.String()), "") + for _, f := range fields[1:] { + if k, v, ok := strings.Cut(strings.TrimSuffix(f, ";"), "="); ok { + if n, err := strconv.Atoi(v); err == nil { + s.SetAttribute(k, n) + } + } + } + db = append(db, s) + seq.Reset() + } + sc := bufio.NewScanner(f) + for sc.Scan() { + if line := sc.Text(); strings.HasPrefix(line, ">") { + flush() + header = line[1:] + } else { + seq.WriteString(strings.TrimSpace(line)) + } + } + flush() + return db +} + +// collect reads the whole table, whatever the order the batches arrive in, +// and returns its rows in the order of the output file. +func collect(it *obiitercsv.ICSVRecord) []obiitercsv.CSVRecord { + batches := map[int][]obiitercsv.CSVRecord{} + for it.Next() { + b := it.Get() + batches[b.Order()] = b.Slice() + } + orders := make([]int, 0, len(batches)) + for o := range batches { + orders = append(orders, o) + } + sort.Ints(orders) + var rows []obiitercsv.CSVRecord + for k, o := range orders { + if o != k { + panic(fmt.Sprintf("batch %d is missing", k)) + } + rows = append(rows, batches[o]...) + } + return rows +} + +// sumatraParams translates the options of sumatra. +func sumatraParams(t testing.TB, args []string) Params { + t.Helper() + p := Params{Normalized: true, Workers: 4} + for i := 0; i < len(args); i++ { + var err error + switch args[i] { + case "-t": + i++ + p.Threshold, err = strconv.ParseFloat(args[i], 64) + case "-l": + p.Reference = sim.RefShortest + case "-L": + p.Reference = sim.RefLongest + case "-d": + p.Distance = true + case "-r": + p.Normalized = false + case "-x": + p.ExtraColumns = true + default: + t.Fatalf("unknown option %s", args[i]) + } + if err != nil { + t.Fatal(err) + } + } + return p +} + +// TestSumatraResults compares the tables with the outputs of sumatra 1.0.36 +// (testdata/*.expected), except that the identical sequences obey the +// threshold of a raw similarity, which sumatra does not check. +func TestSumatraResults(t *testing.T) { + cases, err := os.ReadFile(filepath.Join("testdata", "cases.txt")) + if err != nil { + t.Fatal(err) + } + a := readTestFasta(t, filepath.Join("testdata", "a.fasta")) + b := readTestFasta(t, filepath.Join("testdata", "b.fasta")) + + for _, line := range strings.Split(strings.TrimSpace(string(cases)), "\n") { + parts := strings.Split(line, "\t") + kind, name, opts := parts[0], parts[1], strings.Fields(parts[2]) + t.Run(kind+"/"+name, func(t *testing.T) { + var db2 obiseq.BioSequenceSlice + if kind == "two" { + db2 = b + } + p := sumatraParams(t, opts) + rows := collect(Compare(a, db2, "test", p, nil)) + + expected, err := os.ReadFile(filepath.Join("testdata", kind+"_"+name+".expected")) + if err != nil { + t.Fatal(err) + } + want := strings.Split(strings.TrimSuffix(string(expected), "\n"), "\n") + if len(expected) == 0 { + want = nil + } + if len(rows) != len(want) { + t.Fatalf("%d rows, want %d", len(rows), len(want)) + } + for i, r := range rows { + got := fmt.Sprintf("%s\t%s\t%f", r["id1"], r["id2"], r["score"]) + if p.ExtraColumns { + got += fmt.Sprintf("\t%d\t%d\t%d\t%d", r["count1"], r["count2"], r["length1"], r["length2"]) + } + if got != want[i] { + t.Fatalf("row %d:\n got %s\nwant %s", i, got, want[i]) + } + } + }) + } +} + +func randomDataset(rng *rand.Rand, n int, ambiguousRate float64) obiseq.BioSequenceSlice { + var roots [][]byte + db := make(obiseq.BioSequenceSlice, 0, n) + for len(db) < n { + var s []byte + if len(roots) == 0 || rng.Intn(4) == 0 { + s = make([]byte, 30+rng.Intn(60)) + for i := range s { + s[i] = "acgt"[rng.Intn(4)] + } + roots = append(roots, s) + } else { + for _, c := range roots[rng.Intn(len(roots))] { + switch r := rng.Intn(30); { + case r == 0: + s = append(s, "acgt"[rng.Intn(4)]) + case r == 1: + default: + s = append(s, c) + } + } + } + if rng.Float64() < ambiguousRate && len(s) > 3 { + s[rng.Intn(len(s))] = "nrykm"[rng.Intn(5)] + } + seq := obiseq.NewBioSequence(fmt.Sprintf("s%d", len(db)), s, "") + seq.SetAttribute("count", 1+rng.Intn(9)) + db = append(db, seq) + } + return db +} + +// naive compares every pair one by one, with the scoring and no index. +func naive(db1, db2 obiseq.BioSequenceSlice, p Params) []string { + self := db2 == nil + if self { + db2 = db1 + } + d1, d2 := readDataset(db1, "1", p), readDataset(db2, "2", p) + all := append(append([]sim.Entry{}, d1.entries...), d2.entries...) + sc, err := sim.NewScoring(sim.Params{Threshold: p.Threshold, Normalized: p.Normalized, Distance: p.Distance, + Reference: p.Reference, Ratio: 1e300}, all) + if err != nil { + panic(err) + } + var buf align.BandedLCSBuffer + var out []string + for i := range d1.entries { + for j := range d2.entries { + if self && j <= i { + continue + } + if s, ok := sc.PairScore(&d1.entries[i], &d2.entries[j], -1, &buf); ok && sc.Passes(s) { + out = append(out, fmt.Sprintf("%s\t%s\t%f", d1.seqs[i].Id(), d2.seqs[j].Id(), sc.Reported(s))) + } + } + } + return out +} + +// TestCompareMatchesNaive checks the scan by length and by blocks against the +// comparison of all the pairs, in every mode, with and without ambiguous +// sequences, whatever the number of workers. +func TestCompareMatchesNaive(t *testing.T) { + rng := rand.New(rand.NewSource(11)) + for _, ambiguous := range []float64{0, 0.2} { + db1 := randomDataset(rng, 90, ambiguous) + db2 := randomDataset(rng, 40, ambiguous) + for _, ref := range []sim.Reference{sim.RefAlignment, sim.RefLongest, sim.RefShortest} { + for _, base := range []Params{ + {Threshold: 0.9, Normalized: true}, + {Threshold: 0.7, Normalized: true, Distance: true}, + {Threshold: 30}, + {Threshold: 6, Distance: true}, + {Normalized: true}, + {Distance: true}, + } { + base.Reference, base.AcceptAmbiguous = ref, ambiguous > 0 + for _, second := range []obiseq.BioSequenceSlice{nil, db2} { + want := naive(db1, second, base) + for _, workers := range []int{1, 3, 8} { + p := base + p.Workers = workers + rows := collect(Compare(db1, second, "test", p, nil)) + if len(rows) != len(want) { + t.Fatalf("%+v two=%v workers=%d: %d rows, want %d", base, second != nil, workers, len(rows), len(want)) + } + for i, r := range rows { + if got := fmt.Sprintf("%s\t%s\t%f", r["id1"], r["id2"], r["score"]); got != want[i] { + t.Fatalf("%+v two=%v workers=%d row %d: %s, want %s", base, second != nil, workers, i, got, want[i]) + } + } + } + } + } + } + } +} + +func TestRunShape(t *testing.T) { + // with a threshold, the runs are long + if rows, window := runShape(40000, 40000, false, 16); rows != 78 || window != 64 { + t.Errorf("with a threshold: %d rows, window %d", rows, window) + } + // without threshold, a run produces at most about maxRowsPerRun rows + if rows, _ := runShape(40000, 40000, true, 16); rows*40000 > 3*maxRowsPerRun { + t.Errorf("without threshold: %d rows per run give too large a table", rows) + } + if rows, _ := runShape(10, 10, true, 4); rows < 1 { + t.Errorf("a run must cover one sequence at least, got %d", rows) + } +} diff --git a/pkg/obitools/obialign/options.go b/pkg/obitools/obialign/options.go new file mode 100644 index 0000000..5f2037c --- /dev/null +++ b/pkg/obitools/obialign/options.go @@ -0,0 +1,132 @@ +package obialign + +import ( + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq" + sim "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obisimilarity" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obitools/obiconvert" + "github.com/DavidGamba/go-getoptions" + log "github.com/sirupsen/logrus" +) + +var _threshold = 0.0 +var _refShortest = false +var _refLongest = false +var _refAlignment = false +var _normalized = false +var _raw = false +var _distance = false +var _acceptAmbiguous = false +var _referenceDB = "" +var _extraColumns = false + +func ObialignOptionSet(options *getoptions.GetOpt) { + options.Float64Var(&_threshold, "threshold", _threshold, + options.Alias("t"), + options.ArgName("THRESHOLD"), + options.Description("Score threshold: only the pairs of sequences whose score reaches it are reported "+ + "(default: no threshold, all the pairs are reported). If the score is normalized and expressed in similarity (default), "+ + "it is an identity, e.g. 0.95 for an identity of 95%. If the score is normalized and expressed in distance, "+ + "it is (1.0 - identity), e.g. 0.05 for an identity of 95%. If the score is not normalized and expressed in similarity, "+ + "it is the length of the Longest Common Subsequence. If the score is not normalized and expressed in distance, "+ + "it is (reference length - LCS length).")) + + options.BoolVar(&_refShortest, "shortest", _refShortest, + options.Alias("l"), + options.Description("Reference sequence length is the shortest.")) + + options.BoolVar(&_refLongest, "longest", _refLongest, + options.Alias("L"), + options.Description("Reference sequence length is the longest.")) + + options.BoolVar(&_refAlignment, "alignment", _refAlignment, + options.Alias("a"), + options.Description("Reference sequence length is the alignment length (default).")) + + options.BoolVar(&_normalized, "normalized", _normalized, + options.Alias("n"), + options.Description("Score is normalized by the reference sequence length (default).")) + + options.BoolVar(&_raw, "raw", _raw, + options.Alias("r"), + options.Description("Raw score, not normalized.")) + + options.BoolVar(&_distance, "distance", _distance, + options.Alias("d"), + options.Description("Score is expressed in distance (default: score is expressed in similarity).")) + + options.BoolVar(&_acceptAmbiguous, "accept-ambiguous", _acceptAmbiguous, + options.Alias("g"), + options.Description("Accept the sequences containing IUPAC letters other than a, c, g and t; "+ + "the alignment handles the IUPAC codes (default: these sequences are discarded).")) + + options.StringVar(&_referenceDB, "reference-db", _referenceDB, + options.Alias("R"), + options.ArgName("FILENAME"), + options.Description("Compare every sequence of the input with every sequence of this file "+ + "(default: compare the sequences of the input with each other).")) + + options.BoolVar(&_extraColumns, "extra-columns", _extraColumns, + options.Alias("x"), + options.Description("Add the columns count1, count2, length1 and length2 to the table.")) +} + +func OptionSet(options *getoptions.GetOpt) { + obiconvert.InputOptionSet(options) + obiconvert.OutputModeOptionSet(options, true) + ObialignOptionSet(options) +} + +// CLIThreshold returns the score threshold, 0 if there is none. +func CLIThreshold() float64 { return _threshold } + +// CLINormalized returns true when the score is normalized by the reference length. +func CLINormalized() bool { + if _raw && _normalized { + log.Fatal("The options --raw and --normalized are exclusive") + } + return !_raw +} + +// CLIDistance returns true when the score is expressed in distance. +func CLIDistance() bool { return _distance } + +// CLIReference returns the length used as reference to normalize the score. +func CLIReference() sim.Reference { + n := 0 + ref := sim.RefAlignment + if _refShortest { + n++ + ref = sim.RefShortest + } + if _refLongest { + n++ + ref = sim.RefLongest + } + if _refAlignment { + n++ + } + if n > 1 { + log.Fatal("The options --shortest, --longest and --alignment are exclusive") + } + return ref +} + +// CLIAcceptAmbiguous returns true when the sequences with IUPAC letters other than acgt are kept. +func CLIAcceptAmbiguous() bool { return _acceptAmbiguous } + +// CLIExtraColumns returns true when the counts and the lengths must be written. +func CLIExtraColumns() bool { return _extraColumns } + +// CLIHasReferenceDB returns true when a second dataset has been given. +func CLIHasReferenceDB() bool { return _referenceDB != "" } + +// CLIReferenceDB reads the second dataset. +func CLIReferenceDB() obiseq.BioSequenceSlice { + // Read as the main input is, with the same input options. + iterator, err := obiconvert.CLIReadBioSequences(_referenceDB) + if err != nil { + log.Fatalf("Cannot open the reference file %s: %v", _referenceDB, err) + } + _, db := iterator.Load() + return db +} diff --git a/pkg/obitools/obialign/testdata/cases.txt b/pkg/obitools/obialign/testdata/cases.txt new file mode 100644 index 0000000..8cbf6a1 --- /dev/null +++ b/pkg/obitools/obialign/testdata/cases.txt @@ -0,0 +1,24 @@ +one all -x +one default -t 0.9 -x +one low -t 0.6 +one shortest -t 0.9 -l +one longest -t 0.85 -L +one distance -t 0.1 -d +one rawdist -t 4 -r -d +one rawlcs -t 40 -r +one rawlcs_shortest -t 45 -r -l +one alldist -d -r +one allraw -r +one allshortest -l +two all -x +two default -t 0.9 -x +two low -t 0.6 +two shortest -t 0.9 -l +two longest -t 0.85 -L +two distance -t 0.1 -d +two rawdist -t 4 -r -d +two rawlcs -t 40 -r +two rawlcs_shortest -t 45 -r -l +two alldist -d -r +two allraw -r +two allshortest -l diff --git a/pkg/obitools/obialign/testdata/one_all.expected b/pkg/obitools/obialign/testdata/one_all.expected new file mode 100644 index 0000000..632772a --- /dev/null +++ b/pkg/obitools/obialign/testdata/one_all.expected @@ -0,0 +1,903 @@ +f3v10s35 f3v5s30 0.865672 87579 39802 65 63 +f3v10s35 f1v8s15 0.521127 87579 66109 65 57 +f3v10s35 f2v1s23 0.500000 87579 35696 65 58 +f3v10s35 f4v2s42 0.506667 87579 17302 65 61 +f3v10s35 f0v4s4 0.500000 87579 86916 65 69 +f3v10s35 f1v6s13 0.528571 87579 80246 65 56 +f3v10s35 f3v8s33 0.910448 87579 88635 65 66 +f3v10s35 f1v1s8 0.527027 87579 17651 65 60 +f3v10s35 f1v14s21 0.506667 87579 85030 65 56 +f3v10s35 f0v1s1 0.500000 87579 43883 65 69 +f3v10s35 f1v5s12 0.506667 87579 1410 65 60 +f3v10s35 f1v4s11 0.500000 87579 74527 65 58 +f3v10s35 f1v9s16 0.506494 87579 65777 65 60 +f3v10s35 f1v10s17 0.520000 87579 98659 65 60 +f3v10s35 f1v0s7 0.500000 87579 84420 65 58 +f3v10s35 f3v9s34 0.910448 87579 40677 65 65 +f3v10s35 f4v0s40 0.500000 87579 75843 65 60 +f3v10s35 f1v7s14 0.500000 87579 42277 65 58 +f3v10s35 f3v2s27 0.910448 87579 74030 65 65 +f3v10s35 f4v1s41 0.493506 87579 45686 65 61 +f3v10s35 f2v0s22 0.506329 87579 43219 65 69 +f3v10s35 f1v2s9 0.500000 87579 99108 65 69 +f3v10s35 f1v3s10 0.500000 87579 43230 65 58 +f3v10s35 f3v0s25 0.925373 87579 62755 65 66 +f3v10s35 f3v3s28 0.897059 87579 66465 65 66 +f3v10s35 f3v13s38 0.898551 87579 26948 65 68 +f3v10s35 f3v7s32 0.910448 87579 53710 65 65 +f3v10s35 f3v1s26 0.910448 87579 41283 65 65 +f3v10s35 f3v6s31 0.910448 87579 94783 65 65 +f3v10s35 f1v13s20 0.493333 87579 80913 65 56 +f3v10s35 f2v2s24 0.500000 87579 68414 65 49 +f3v10s35 f3v11s36 0.867647 87579 91927 65 65 +f3v10s35 f0v5s5 0.500000 87579 2668 65 69 +f3v10s35 f1v11s18 0.500000 87579 51814 65 58 +f3v10s35 f0v6s6 0.475000 87579 16273 65 69 +f3v10s35 f3v4s29 0.884058 87579 94356 65 68 +f3v10s35 f0v2s2 0.506329 87579 10896 65 69 +f3v10s35 f3v12s37 0.897059 87579 74202 65 67 +f3v10s35 f0v3s3 0.500000 87579 73040 65 67 +f3v10s35 f3v14s39 0.925373 87579 55015 65 66 +f3v10s35 f0v0s0 0.506329 87579 1332 65 69 +f3v10s35 f1v12s19 0.513889 87579 58178 65 57 +f3v5s30 f1v8s15 0.520548 39802 66109 63 57 +f3v5s30 f2v1s23 0.513514 39802 35696 63 58 +f3v5s30 f4v2s42 0.513889 39802 17302 63 61 +f3v5s30 f0v4s4 0.493671 39802 86916 63 69 +f3v5s30 f1v6s13 0.520000 39802 80246 63 56 +f3v5s30 f3v8s33 0.924242 39802 88635 63 66 +f3v5s30 f1v1s8 0.513158 39802 17651 63 60 +f3v5s30 f1v14s21 0.521127 39802 85030 63 56 +f3v5s30 f0v1s1 0.493671 39802 43883 63 69 +f3v5s30 f1v5s12 0.500000 39802 1410 63 60 +f3v5s30 f1v4s11 0.513514 39802 74527 63 58 +f3v5s30 f1v9s16 0.506667 39802 65777 63 60 +f3v5s30 f1v10s17 0.506494 39802 98659 63 60 +f3v5s30 f1v0s7 0.513514 39802 84420 63 58 +f3v5s30 f3v9s34 0.909091 39802 40677 63 65 +f3v5s30 f4v0s40 0.521127 39802 75843 63 60 +f3v5s30 f1v7s14 0.513514 39802 42277 63 58 +f3v5s30 f3v2s27 0.924242 39802 74030 63 65 +f3v5s30 f4v1s41 0.513889 39802 45686 63 61 +f3v5s30 f2v0s22 0.506329 39802 43219 63 69 +f3v5s30 f1v2s9 0.493671 39802 99108 63 69 +f3v5s30 f1v3s10 0.513514 39802 43230 63 58 +f3v5s30 f3v0s25 0.939394 39802 62755 63 66 +f3v5s30 f3v3s28 0.910448 39802 66465 63 66 +f3v5s30 f3v13s38 0.911765 39802 26948 63 68 +f3v5s30 f3v7s32 0.909091 39802 53710 63 65 +f3v5s30 f3v1s26 0.910448 39802 41283 63 65 +f3v5s30 f3v6s31 0.909091 39802 94783 63 65 +f3v5s30 f1v13s20 0.513514 39802 80913 63 56 +f3v5s30 f2v2s24 0.515152 39802 68414 63 49 +f3v5s30 f3v11s36 0.893939 39802 91927 63 65 +f3v5s30 f0v5s5 0.500000 39802 2668 63 69 +f3v5s30 f1v11s18 0.513514 39802 51814 63 58 +f3v5s30 f0v6s6 0.506173 39802 16273 63 69 +f3v5s30 f3v4s29 0.897059 39802 94356 63 68 +f3v5s30 f0v2s2 0.506329 39802 10896 63 69 +f3v5s30 f3v12s37 0.910448 39802 74202 63 67 +f3v5s30 f0v3s3 0.506329 39802 73040 63 67 +f3v5s30 f3v14s39 0.939394 39802 55015 63 66 +f3v5s30 f0v0s0 0.506329 39802 1332 63 69 +f3v5s30 f1v12s19 0.506849 39802 58178 63 57 +f1v8s15 f2v1s23 0.982759 66109 35696 57 58 +f1v8s15 f4v2s42 0.478873 66109 17302 57 61 +f1v8s15 f0v4s4 0.487179 66109 86916 57 69 +f1v8s15 f1v6s13 0.947368 66109 80246 57 56 +f1v8s15 f3v8s33 0.541667 66109 88635 57 66 +f1v8s15 f1v1s8 0.916667 66109 17651 57 60 +f1v8s15 f1v14s21 0.931034 66109 85030 57 56 +f1v8s15 f0v1s1 0.487179 66109 43883 57 69 +f1v8s15 f1v5s12 0.916667 66109 1410 57 60 +f1v8s15 f1v4s11 0.982759 66109 74527 57 58 +f1v8s15 f1v9s16 0.933333 66109 65777 57 60 +f1v8s15 f1v10s17 0.916667 66109 98659 57 60 +f1v8s15 f1v0s7 0.982759 66109 84420 57 58 +f1v8s15 f3v9s34 0.547945 66109 40677 57 65 +f1v8s15 f4v0s40 0.486111 66109 75843 57 60 +f1v8s15 f1v7s14 0.982759 66109 42277 57 58 +f1v8s15 f3v2s27 0.541667 66109 74030 57 65 +f1v8s15 f4v1s41 0.479452 66109 45686 57 61 +f1v8s15 f2v0s22 0.493671 66109 43219 57 69 +f1v8s15 f1v2s9 0.487179 66109 99108 57 69 +f1v8s15 f1v3s10 0.982759 66109 43230 57 58 +f1v8s15 f3v0s25 0.534247 66109 62755 57 66 +f1v8s15 f3v3s28 0.527027 66109 66465 57 66 +f1v8s15 f3v13s38 0.506329 66109 26948 57 68 +f1v8s15 f3v7s32 0.547945 66109 53710 57 65 +f1v8s15 f3v1s26 0.541667 66109 41283 57 65 +f1v8s15 f3v6s31 0.547945 66109 94783 57 65 +f1v8s15 f1v13s20 0.948276 66109 80913 57 56 +f1v8s15 f2v2s24 0.515152 66109 68414 57 49 +f1v8s15 f3v11s36 0.527027 66109 91927 57 65 +f1v8s15 f0v5s5 0.493671 66109 2668 57 69 +f1v8s15 f1v11s18 0.965517 66109 51814 57 58 +f1v8s15 f0v6s6 0.481013 66109 16273 57 69 +f1v8s15 f3v4s29 0.533333 66109 94356 57 68 +f1v8s15 f0v2s2 0.493671 66109 10896 57 69 +f1v8s15 f3v12s37 0.540541 66109 74202 57 67 +f1v8s15 f0v3s3 0.506494 66109 73040 57 67 +f1v8s15 f3v14s39 0.534247 66109 55015 57 66 +f1v8s15 f0v0s0 0.493671 66109 1332 57 69 +f1v8s15 f1v12s19 0.948276 66109 58178 57 57 +f2v1s23 f4v2s42 0.492754 35696 17302 58 61 +f2v1s23 f0v4s4 0.481013 35696 86916 58 69 +f2v1s23 f1v6s13 0.931034 35696 80246 58 56 +f2v1s23 f3v8s33 0.534247 35696 88635 58 66 +f2v1s23 f1v1s8 0.933333 35696 17651 58 60 +f2v1s23 f1v14s21 0.948276 35696 85030 58 56 +f2v1s23 f0v1s1 0.481013 35696 43883 58 69 +f2v1s23 f1v5s12 0.933333 35696 1410 58 60 +f2v1s23 f1v4s11 1.000000 35696 74527 58 58 +f2v1s23 f1v9s16 0.950000 35696 65777 58 60 +f2v1s23 f1v10s17 0.933333 35696 98659 58 60 +f2v1s23 f1v0s7 1.000000 35696 84420 58 58 +f2v1s23 f3v9s34 0.540541 35696 40677 58 65 +f2v1s23 f4v0s40 0.486111 35696 75843 58 60 +f2v1s23 f1v7s14 1.000000 35696 42277 58 58 +f2v1s23 f3v2s27 0.534247 35696 74030 58 65 +f2v1s23 f4v1s41 0.507246 35696 45686 58 61 +f2v1s23 f2v0s22 0.487500 35696 43219 58 69 +f2v1s23 f1v2s9 0.481013 35696 99108 58 69 +f2v1s23 f1v3s10 1.000000 35696 43230 58 58 +f2v1s23 f3v0s25 0.527027 35696 62755 58 66 +f2v1s23 f3v3s28 0.520000 35696 66465 58 66 +f2v1s23 f3v13s38 0.506329 35696 26948 58 68 +f2v1s23 f3v7s32 0.540541 35696 53710 58 65 +f2v1s23 f3v1s26 0.534247 35696 41283 58 65 +f2v1s23 f3v6s31 0.540541 35696 94783 58 65 +f2v1s23 f1v13s20 0.965517 35696 80913 58 56 +f2v1s23 f2v2s24 0.507463 35696 68414 58 49 +f2v1s23 f3v11s36 0.520000 35696 91927 58 65 +f2v1s23 f0v5s5 0.487500 35696 2668 58 69 +f2v1s23 f1v11s18 0.982759 35696 51814 58 58 +f2v1s23 f0v6s6 0.475000 35696 16273 58 69 +f2v1s23 f3v4s29 0.526316 35696 94356 58 68 +f2v1s23 f0v2s2 0.487500 35696 10896 58 69 +f2v1s23 f3v12s37 0.533333 35696 74202 58 67 +f2v1s23 f0v3s3 0.500000 35696 73040 58 67 +f2v1s23 f3v14s39 0.527027 35696 55015 58 66 +f2v1s23 f0v0s0 0.487500 35696 1332 58 69 +f2v1s23 f1v12s19 0.965517 35696 58178 58 57 +f4v2s42 f0v4s4 0.520000 17302 86916 61 69 +f4v2s42 f1v6s13 0.478261 17302 80246 61 56 +f4v2s42 f3v8s33 0.500000 17302 88635 61 66 +f4v2s42 f1v1s8 0.480000 17302 17651 61 60 +f4v2s42 f1v14s21 0.500000 17302 85030 61 56 +f4v2s42 f0v1s1 0.520000 17302 43883 61 69 +f4v2s42 f1v5s12 0.485714 17302 1410 61 60 +f4v2s42 f1v4s11 0.492754 17302 74527 61 58 +f4v2s42 f1v9s16 0.493151 17302 65777 61 60 +f4v2s42 f1v10s17 0.466667 17302 98659 61 60 +f4v2s42 f1v0s7 0.492754 17302 84420 61 58 +f4v2s42 f3v9s34 0.472973 17302 40677 61 65 +f4v2s42 f4v0s40 0.967213 17302 75843 61 60 +f4v2s42 f1v7s14 0.492754 17302 42277 61 58 +f4v2s42 f3v2s27 0.493151 17302 74030 61 65 +f4v2s42 f4v1s41 0.951613 17302 45686 61 61 +f4v2s42 f2v0s22 0.546667 17302 43219 61 69 +f4v2s42 f1v2s9 0.520000 17302 99108 61 69 +f4v2s42 f1v3s10 0.492754 17302 43230 61 58 +f4v2s42 f3v0s25 0.486486 17302 62755 61 66 +f4v2s42 f3v3s28 0.486486 17302 66465 61 66 +f4v2s42 f3v13s38 0.474359 17302 26948 61 68 +f4v2s42 f3v7s32 0.472973 17302 53710 61 65 +f4v2s42 f3v1s26 0.493151 17302 41283 61 65 +f4v2s42 f3v6s31 0.472973 17302 94783 61 65 +f4v2s42 f1v13s20 0.492537 17302 80913 61 56 +f4v2s42 f2v2s24 0.530303 17302 68414 61 49 +f4v2s42 f3v11s36 0.493151 17302 91927 61 65 +f4v2s42 f0v5s5 0.526316 17302 2668 61 69 +f4v2s42 f1v11s18 0.478873 17302 51814 61 58 +f4v2s42 f0v6s6 0.539474 17302 16273 61 69 +f4v2s42 f3v4s29 0.481013 17302 94356 61 68 +f4v2s42 f0v2s2 0.546667 17302 10896 61 69 +f4v2s42 f3v12s37 0.486842 17302 74202 61 67 +f4v2s42 f0v3s3 0.527027 17302 73040 61 67 +f4v2s42 f3v14s39 0.486486 17302 55015 61 66 +f4v2s42 f0v0s0 0.533333 17302 1332 61 69 +f4v2s42 f1v12s19 0.492537 17302 58178 61 57 +f0v4s4 f1v6s13 0.493333 86916 80246 69 56 +f0v4s4 f3v8s33 0.506173 86916 88635 69 66 +f0v4s4 f1v1s8 0.481013 86916 17651 69 60 +f0v4s4 f1v14s21 0.493506 86916 85030 69 56 +f0v4s4 f0v1s1 1.000000 86916 43883 69 69 +f0v4s4 f1v5s12 0.475610 86916 1410 69 60 +f0v4s4 f1v4s11 0.481013 86916 74527 69 58 +f0v4s4 f1v9s16 0.493671 86916 65777 69 60 +f0v4s4 f1v10s17 0.481481 86916 98659 69 60 +f0v4s4 f1v0s7 0.481013 86916 84420 69 58 +f0v4s4 f3v9s34 0.500000 86916 40677 69 65 +f0v4s4 f4v0s40 0.481013 86916 75843 69 60 +f0v4s4 f1v7s14 0.481013 86916 42277 69 58 +f0v4s4 f3v2s27 0.500000 86916 74030 69 65 +f0v4s4 f4v1s41 0.475000 86916 45686 69 61 +f0v4s4 f2v0s22 0.943662 86916 43219 69 69 +f0v4s4 f1v2s9 1.000000 86916 99108 69 69 +f0v4s4 f1v3s10 0.481013 86916 43230 69 58 +f0v4s4 f3v0s25 0.493827 86916 62755 69 66 +f0v4s4 f3v3s28 0.500000 86916 66465 69 66 +f0v4s4 f3v13s38 0.493827 86916 26948 69 68 +f0v4s4 f3v7s32 0.500000 86916 53710 69 65 +f0v4s4 f3v1s26 0.481481 86916 41283 69 65 +f0v4s4 f3v6s31 0.500000 86916 94783 69 65 +f0v4s4 f1v13s20 0.480519 86916 80913 69 56 +f0v4s4 f2v2s24 0.486111 86916 68414 69 49 +f0v4s4 f3v11s36 0.468354 86916 91927 69 65 +f0v4s4 f0v5s5 0.943662 86916 2668 69 69 +f0v4s4 f1v11s18 0.493506 86916 51814 69 58 +f0v4s4 f0v6s6 0.901408 86916 16273 69 69 +f0v4s4 f3v4s29 0.525000 86916 94356 69 68 +f0v4s4 f0v2s2 0.943662 86916 10896 69 69 +f0v4s4 f3v12s37 0.493827 86916 74202 69 67 +f0v4s4 f0v3s3 0.915493 86916 73040 69 67 +f0v4s4 f3v14s39 0.493827 86916 55015 69 66 +f0v4s4 f0v0s0 0.971429 86916 1332 69 69 +f0v4s4 f1v12s19 0.457831 86916 58178 69 57 +f1v6s13 f3v8s33 0.541667 80246 88635 56 66 +f1v6s13 f1v1s8 0.866667 80246 17651 56 60 +f1v6s13 f1v14s21 0.879310 80246 85030 56 56 +f1v6s13 f0v1s1 0.493333 80246 43883 56 69 +f1v6s13 f1v5s12 0.866667 80246 1410 56 60 +f1v6s13 f1v4s11 0.931034 80246 74527 56 58 +f1v6s13 f1v9s16 0.883333 80246 65777 56 60 +f1v6s13 f1v10s17 0.883333 80246 98659 56 60 +f1v6s13 f1v0s7 0.931034 80246 84420 56 58 +f1v6s13 f3v9s34 0.541667 80246 40677 56 65 +f1v6s13 f4v0s40 0.472222 80246 75843 56 60 +f1v6s13 f1v7s14 0.931034 80246 42277 56 58 +f1v6s13 f3v2s27 0.549296 80246 74030 56 65 +f1v6s13 f4v1s41 0.465753 80246 45686 56 61 +f1v6s13 f2v0s22 0.493506 80246 43219 56 69 +f1v6s13 f1v2s9 0.493333 80246 99108 56 69 +f1v6s13 f1v3s10 0.931034 80246 43230 56 58 +f1v6s13 f3v0s25 0.541667 80246 62755 56 66 +f1v6s13 f3v3s28 0.534247 80246 66465 56 66 +f1v6s13 f3v13s38 0.527027 80246 26948 56 68 +f1v6s13 f3v7s32 0.541667 80246 53710 56 65 +f1v6s13 f3v1s26 0.549296 80246 41283 56 65 +f1v6s13 f3v6s31 0.541667 80246 94783 56 65 +f1v6s13 f1v13s20 0.896552 80246 80913 56 56 +f1v6s13 f2v2s24 0.507692 80246 68414 56 49 +f1v6s13 f3v11s36 0.534247 80246 91927 56 65 +f1v6s13 f0v5s5 0.500000 80246 2668 56 69 +f1v6s13 f1v11s18 0.913793 80246 51814 56 58 +f1v6s13 f0v6s6 0.493506 80246 16273 56 69 +f1v6s13 f3v4s29 0.540541 80246 94356 56 68 +f1v6s13 f0v2s2 0.493506 80246 10896 56 69 +f1v6s13 f3v12s37 0.547945 80246 74202 56 67 +f1v6s13 f0v3s3 0.506849 80246 73040 56 67 +f1v6s13 f3v14s39 0.541667 80246 55015 56 66 +f1v6s13 f0v0s0 0.500000 80246 1332 56 69 +f1v6s13 f1v12s19 0.896552 80246 58178 56 57 +f3v8s33 f1v1s8 0.547945 88635 17651 66 60 +f3v8s33 f1v14s21 0.535211 88635 85030 66 56 +f3v8s33 f0v1s1 0.506173 88635 43883 66 69 +f3v8s33 f1v5s12 0.527027 88635 1410 66 60 +f3v8s33 f1v4s11 0.534247 88635 74527 66 58 +f3v8s33 f1v9s16 0.506494 88635 65777 66 60 +f3v8s33 f1v10s17 0.555556 88635 98659 66 60 +f3v8s33 f1v0s7 0.534247 88635 84420 66 58 +f3v8s33 f3v9s34 0.954545 88635 40677 66 65 +f3v8s33 f4v0s40 0.468354 88635 75843 66 60 +f3v8s33 f1v7s14 0.534247 88635 42277 66 58 +f3v8s33 f3v2s27 0.969697 88635 74030 66 65 +f3v8s33 f4v1s41 0.462500 88635 45686 66 61 +f3v8s33 f2v0s22 0.525000 88635 43219 66 69 +f3v8s33 f1v2s9 0.506173 88635 99108 66 69 +f3v8s33 f1v3s10 0.534247 88635 43230 66 58 +f3v8s33 f3v0s25 0.984848 88635 62755 66 66 +f3v8s33 f3v3s28 0.955224 88635 66465 66 66 +f3v8s33 f3v13s38 0.955882 88635 26948 66 68 +f3v8s33 f3v7s32 0.954545 88635 53710 66 65 +f3v8s33 f3v1s26 0.954545 88635 41283 66 65 +f3v8s33 f3v6s31 0.954545 88635 94783 66 65 +f3v8s33 f1v13s20 0.527778 88635 80913 66 56 +f3v8s33 f2v2s24 0.500000 88635 68414 66 49 +f3v8s33 f3v11s36 0.925373 88635 91927 66 65 +f3v8s33 f0v5s5 0.512500 88635 2668 66 69 +f3v8s33 f1v11s18 0.534247 88635 51814 66 58 +f3v8s33 f0v6s6 0.488095 88635 16273 66 69 +f3v8s33 f3v4s29 0.941176 88635 94356 66 68 +f3v8s33 f0v2s2 0.525000 88635 10896 66 69 +f3v8s33 f3v12s37 0.955224 88635 74202 66 67 +f3v8s33 f0v3s3 0.518987 88635 73040 66 67 +f3v8s33 f3v14s39 0.984848 88635 55015 66 66 +f3v8s33 f0v0s0 0.525000 88635 1332 66 69 +f3v8s33 f1v12s19 0.527778 88635 58178 66 57 +f1v1s8 f1v14s21 0.916667 17651 85030 60 56 +f1v1s8 f0v1s1 0.481013 17651 43883 60 69 +f1v1s8 f1v5s12 0.870968 17651 1410 60 60 +f1v1s8 f1v4s11 0.933333 17651 74527 60 58 +f1v1s8 f1v9s16 0.887097 17651 65777 60 60 +f1v1s8 f1v10s17 0.870968 17651 98659 60 60 +f1v1s8 f1v0s7 0.933333 17651 84420 60 58 +f1v1s8 f3v9s34 0.561644 17651 40677 60 65 +f1v1s8 f4v0s40 0.493151 17651 75843 60 60 +f1v1s8 f1v7s14 0.933333 17651 42277 60 58 +f1v1s8 f3v2s27 0.555556 17651 74030 60 65 +f1v1s8 f4v1s41 0.486486 17651 45686 60 61 +f1v1s8 f2v0s22 0.481481 17651 43219 60 69 +f1v1s8 f1v2s9 0.481013 17651 99108 60 69 +f1v1s8 f1v3s10 0.933333 17651 43230 60 58 +f1v1s8 f3v0s25 0.547945 17651 62755 60 66 +f1v1s8 f3v3s28 0.540541 17651 66465 60 66 +f1v1s8 f3v13s38 0.546667 17651 26948 60 68 +f1v1s8 f3v7s32 0.561644 17651 53710 60 65 +f1v1s8 f3v1s26 0.563380 17651 41283 60 65 +f1v1s8 f3v6s31 0.561644 17651 94783 60 65 +f1v1s8 f1v13s20 0.900000 17651 80913 60 56 +f1v1s8 f2v2s24 0.478261 17651 68414 60 49 +f1v1s8 f3v11s36 0.534247 17651 91927 60 65 +f1v1s8 f0v5s5 0.481481 17651 2668 60 69 +f1v1s8 f1v11s18 0.916667 17651 51814 60 58 +f1v1s8 f0v6s6 0.493506 17651 16273 60 69 +f1v1s8 f3v4s29 0.554054 17651 94356 60 68 +f1v1s8 f0v2s2 0.481481 17651 10896 60 69 +f1v1s8 f3v12s37 0.546667 17651 74202 60 67 +f1v1s8 f0v3s3 0.493671 17651 73040 60 67 +f1v1s8 f3v14s39 0.547945 17651 55015 60 66 +f1v1s8 f0v0s0 0.487500 17651 1332 60 69 +f1v1s8 f1v12s19 0.916667 17651 58178 60 57 +f1v14s21 f0v1s1 0.493506 85030 43883 56 69 +f1v14s21 f1v5s12 0.883333 85030 1410 56 60 +f1v14s21 f1v4s11 0.948276 85030 74527 56 58 +f1v14s21 f1v9s16 0.900000 85030 65777 56 60 +f1v14s21 f1v10s17 0.883333 85030 98659 56 60 +f1v14s21 f1v0s7 0.948276 85030 84420 56 58 +f1v14s21 f3v9s34 0.549296 85030 40677 56 65 +f1v14s21 f4v0s40 0.500000 85030 75843 56 60 +f1v14s21 f1v7s14 0.948276 85030 42277 56 58 +f1v14s21 f3v2s27 0.542857 85030 74030 56 65 +f1v14s21 f4v1s41 0.507463 85030 45686 56 61 +f1v14s21 f2v0s22 0.493671 85030 43219 56 69 +f1v14s21 f1v2s9 0.493506 85030 99108 56 69 +f1v14s21 f1v3s10 0.948276 85030 43230 56 58 +f1v14s21 f3v0s25 0.535211 85030 62755 56 66 +f1v14s21 f3v3s28 0.527778 85030 66465 56 66 +f1v14s21 f3v13s38 0.520548 85030 26948 56 68 +f1v14s21 f3v7s32 0.549296 85030 53710 56 65 +f1v14s21 f3v1s26 0.550725 85030 41283 56 65 +f1v14s21 f3v6s31 0.549296 85030 94783 56 65 +f1v14s21 f1v13s20 0.913793 85030 80913 56 56 +f1v14s21 f2v2s24 0.523077 85030 68414 56 49 +f1v14s21 f3v11s36 0.513514 85030 91927 56 65 +f1v14s21 f0v5s5 0.500000 85030 2668 56 69 +f1v14s21 f1v11s18 0.931034 85030 51814 56 58 +f1v14s21 f0v6s6 0.506667 85030 16273 56 69 +f1v14s21 f3v4s29 0.534247 85030 94356 56 68 +f1v14s21 f0v2s2 0.493671 85030 10896 56 69 +f1v14s21 f3v12s37 0.534247 85030 74202 56 67 +f1v14s21 f0v3s3 0.513158 85030 73040 56 67 +f1v14s21 f3v14s39 0.535211 85030 55015 56 66 +f1v14s21 f0v0s0 0.500000 85030 1332 56 69 +f1v14s21 f1v12s19 0.913793 85030 58178 56 57 +f0v1s1 f1v5s12 0.475610 43883 1410 69 60 +f0v1s1 f1v4s11 0.481013 43883 74527 69 58 +f0v1s1 f1v9s16 0.493671 43883 65777 69 60 +f0v1s1 f1v10s17 0.481481 43883 98659 69 60 +f0v1s1 f1v0s7 0.481013 43883 84420 69 58 +f0v1s1 f3v9s34 0.500000 43883 40677 69 65 +f0v1s1 f4v0s40 0.481013 43883 75843 69 60 +f0v1s1 f1v7s14 0.481013 43883 42277 69 58 +f0v1s1 f3v2s27 0.500000 43883 74030 69 65 +f0v1s1 f4v1s41 0.475000 43883 45686 69 61 +f0v1s1 f2v0s22 0.943662 43883 43219 69 69 +f0v1s1 f1v2s9 1.000000 43883 99108 69 69 +f0v1s1 f1v3s10 0.481013 43883 43230 69 58 +f0v1s1 f3v0s25 0.493827 43883 62755 69 66 +f0v1s1 f3v3s28 0.500000 43883 66465 69 66 +f0v1s1 f3v13s38 0.493827 43883 26948 69 68 +f0v1s1 f3v7s32 0.500000 43883 53710 69 65 +f0v1s1 f3v1s26 0.481481 43883 41283 69 65 +f0v1s1 f3v6s31 0.500000 43883 94783 69 65 +f0v1s1 f1v13s20 0.480519 43883 80913 69 56 +f0v1s1 f2v2s24 0.486111 43883 68414 69 49 +f0v1s1 f3v11s36 0.468354 43883 91927 69 65 +f0v1s1 f0v5s5 0.943662 43883 2668 69 69 +f0v1s1 f1v11s18 0.493506 43883 51814 69 58 +f0v1s1 f0v6s6 0.901408 43883 16273 69 69 +f0v1s1 f3v4s29 0.525000 43883 94356 69 68 +f0v1s1 f0v2s2 0.943662 43883 10896 69 69 +f0v1s1 f3v12s37 0.493827 43883 74202 69 67 +f0v1s1 f0v3s3 0.915493 43883 73040 69 67 +f0v1s1 f3v14s39 0.493827 43883 55015 69 66 +f0v1s1 f0v0s0 0.971429 43883 1332 69 69 +f0v1s1 f1v12s19 0.457831 43883 58178 69 57 +f1v5s12 f1v4s11 0.933333 1410 74527 60 58 +f1v5s12 f1v9s16 0.887097 1410 65777 60 60 +f1v5s12 f1v10s17 0.887097 1410 98659 60 60 +f1v5s12 f1v0s7 0.933333 1410 84420 60 58 +f1v5s12 f3v9s34 0.533333 1410 40677 60 65 +f1v5s12 f4v0s40 0.486111 1410 75843 60 60 +f1v5s12 f1v7s14 0.933333 1410 42277 60 58 +f1v5s12 f3v2s27 0.527027 1410 74030 60 65 +f1v5s12 f4v1s41 0.479452 1410 45686 60 61 +f1v5s12 f2v0s22 0.481928 1410 43219 60 69 +f1v5s12 f1v2s9 0.475610 1410 99108 60 69 +f1v5s12 f1v3s10 0.933333 1410 43230 60 58 +f1v5s12 f3v0s25 0.520000 1410 62755 60 66 +f1v5s12 f3v3s28 0.506667 1410 66465 60 66 +f1v5s12 f3v13s38 0.512821 1410 26948 60 68 +f1v5s12 f3v7s32 0.533333 1410 53710 60 65 +f1v5s12 f3v1s26 0.520548 1410 41283 60 65 +f1v5s12 f3v6s31 0.533333 1410 94783 60 65 +f1v5s12 f1v13s20 0.900000 1410 80913 60 56 +f1v5s12 f2v2s24 0.470588 1410 68414 60 49 +f1v5s12 f3v11s36 0.527027 1410 91927 60 65 +f1v5s12 f0v5s5 0.487500 1410 2668 60 69 +f1v5s12 f1v11s18 0.916667 1410 51814 60 58 +f1v5s12 f0v6s6 0.475610 1410 16273 60 69 +f1v5s12 f3v4s29 0.512821 1410 94356 60 68 +f1v5s12 f0v2s2 0.481928 1410 10896 60 69 +f1v5s12 f3v12s37 0.533333 1410 74202 60 67 +f1v5s12 f0v3s3 0.493671 1410 73040 60 67 +f1v5s12 f3v14s39 0.520000 1410 55015 60 66 +f1v5s12 f0v0s0 0.481928 1410 1332 60 69 +f1v5s12 f1v12s19 0.916667 1410 58178 60 57 +f1v4s11 f1v9s16 0.950000 74527 65777 58 60 +f1v4s11 f1v10s17 0.933333 74527 98659 58 60 +f1v4s11 f1v0s7 1.000000 74527 84420 58 58 +f1v4s11 f3v9s34 0.540541 74527 40677 58 65 +f1v4s11 f4v0s40 0.486111 74527 75843 58 60 +f1v4s11 f1v7s14 1.000000 74527 42277 58 58 +f1v4s11 f3v2s27 0.534247 74527 74030 58 65 +f1v4s11 f4v1s41 0.507246 74527 45686 58 61 +f1v4s11 f2v0s22 0.487500 74527 43219 58 69 +f1v4s11 f1v2s9 0.481013 74527 99108 58 69 +f1v4s11 f1v3s10 1.000000 74527 43230 58 58 +f1v4s11 f3v0s25 0.527027 74527 62755 58 66 +f1v4s11 f3v3s28 0.520000 74527 66465 58 66 +f1v4s11 f3v13s38 0.506329 74527 26948 58 68 +f1v4s11 f3v7s32 0.540541 74527 53710 58 65 +f1v4s11 f3v1s26 0.534247 74527 41283 58 65 +f1v4s11 f3v6s31 0.540541 74527 94783 58 65 +f1v4s11 f1v13s20 0.965517 74527 80913 58 56 +f1v4s11 f2v2s24 0.507463 74527 68414 58 49 +f1v4s11 f3v11s36 0.520000 74527 91927 58 65 +f1v4s11 f0v5s5 0.487500 74527 2668 58 69 +f1v4s11 f1v11s18 0.982759 74527 51814 58 58 +f1v4s11 f0v6s6 0.475000 74527 16273 58 69 +f1v4s11 f3v4s29 0.526316 74527 94356 58 68 +f1v4s11 f0v2s2 0.487500 74527 10896 58 69 +f1v4s11 f3v12s37 0.533333 74527 74202 58 67 +f1v4s11 f0v3s3 0.500000 74527 73040 58 67 +f1v4s11 f3v14s39 0.527027 74527 55015 58 66 +f1v4s11 f0v0s0 0.487500 74527 1332 58 69 +f1v4s11 f1v12s19 0.965517 74527 58178 58 57 +f1v9s16 f1v10s17 0.887097 65777 98659 60 60 +f1v9s16 f1v0s7 0.950000 65777 84420 60 58 +f1v9s16 f3v9s34 0.527027 65777 40677 60 65 +f1v9s16 f4v0s40 0.493151 65777 75843 60 60 +f1v9s16 f1v7s14 0.950000 65777 42277 60 58 +f1v9s16 f3v2s27 0.513158 65777 74030 60 65 +f1v9s16 f4v1s41 0.514286 65777 45686 60 61 +f1v9s16 f2v0s22 0.493827 65777 43219 60 69 +f1v9s16 f1v2s9 0.493671 65777 99108 60 69 +f1v9s16 f1v3s10 0.950000 65777 43230 60 58 +f1v9s16 f3v0s25 0.506494 65777 62755 60 66 +f1v9s16 f3v3s28 0.500000 65777 66465 60 66 +f1v9s16 f3v13s38 0.506329 65777 26948 60 68 +f1v9s16 f3v7s32 0.527027 65777 53710 60 65 +f1v9s16 f3v1s26 0.520548 65777 41283 60 65 +f1v9s16 f3v6s31 0.527027 65777 94783 60 65 +f1v9s16 f1v13s20 0.916667 65777 80913 60 56 +f1v9s16 f2v2s24 0.500000 65777 68414 60 49 +f1v9s16 f3v11s36 0.500000 65777 91927 60 65 +f1v9s16 f0v5s5 0.500000 65777 2668 60 69 +f1v9s16 f1v11s18 0.933333 65777 51814 60 58 +f1v9s16 f0v6s6 0.481481 65777 16273 60 69 +f1v9s16 f3v4s29 0.512821 65777 94356 60 68 +f1v9s16 f0v2s2 0.493827 65777 10896 60 69 +f1v9s16 f3v12s37 0.506329 65777 74202 60 67 +f1v9s16 f0v3s3 0.518987 65777 73040 60 67 +f1v9s16 f3v14s39 0.506494 65777 55015 60 66 +f1v9s16 f0v0s0 0.500000 65777 1332 60 69 +f1v9s16 f1v12s19 0.916667 65777 58178 60 57 +f1v10s17 f1v0s7 0.933333 98659 84420 60 58 +f1v10s17 f3v9s34 0.561644 98659 40677 60 65 +f1v10s17 f4v0s40 0.472973 98659 75843 60 60 +f1v10s17 f1v7s14 0.933333 98659 42277 60 58 +f1v10s17 f3v2s27 0.555556 98659 74030 60 65 +f1v10s17 f4v1s41 0.473684 98659 45686 60 61 +f1v10s17 f2v0s22 0.475610 98659 43219 60 69 +f1v10s17 f1v2s9 0.481481 98659 99108 60 69 +f1v10s17 f1v3s10 0.933333 98659 43230 60 58 +f1v10s17 f3v0s25 0.547945 98659 62755 60 66 +f1v10s17 f3v3s28 0.519481 98659 66465 60 66 +f1v10s17 f3v13s38 0.518987 98659 26948 60 68 +f1v10s17 f3v7s32 0.561644 98659 53710 60 65 +f1v10s17 f3v1s26 0.541667 98659 41283 60 65 +f1v10s17 f3v6s31 0.561644 98659 94783 60 65 +f1v10s17 f1v13s20 0.900000 98659 80913 60 56 +f1v10s17 f2v2s24 0.492537 98659 68414 60 49 +f1v10s17 f3v11s36 0.539474 98659 91927 60 65 +f1v10s17 f0v5s5 0.481481 98659 2668 60 69 +f1v10s17 f1v11s18 0.916667 98659 51814 60 58 +f1v10s17 f0v6s6 0.464286 98659 16273 60 69 +f1v10s17 f3v4s29 0.533333 98659 94356 60 68 +f1v10s17 f0v2s2 0.475610 98659 10896 60 69 +f1v10s17 f3v12s37 0.561644 98659 74202 60 67 +f1v10s17 f0v3s3 0.500000 98659 73040 60 67 +f1v10s17 f3v14s39 0.547945 98659 55015 60 66 +f1v10s17 f0v0s0 0.487805 98659 1332 60 69 +f1v10s17 f1v12s19 0.916667 98659 58178 60 57 +f1v0s7 f3v9s34 0.540541 84420 40677 58 65 +f1v0s7 f4v0s40 0.486111 84420 75843 58 60 +f1v0s7 f1v7s14 1.000000 84420 42277 58 58 +f1v0s7 f3v2s27 0.534247 84420 74030 58 65 +f1v0s7 f4v1s41 0.507246 84420 45686 58 61 +f1v0s7 f2v0s22 0.487500 84420 43219 58 69 +f1v0s7 f1v2s9 0.481013 84420 99108 58 69 +f1v0s7 f1v3s10 1.000000 84420 43230 58 58 +f1v0s7 f3v0s25 0.527027 84420 62755 58 66 +f1v0s7 f3v3s28 0.520000 84420 66465 58 66 +f1v0s7 f3v13s38 0.506329 84420 26948 58 68 +f1v0s7 f3v7s32 0.540541 84420 53710 58 65 +f1v0s7 f3v1s26 0.534247 84420 41283 58 65 +f1v0s7 f3v6s31 0.540541 84420 94783 58 65 +f1v0s7 f1v13s20 0.965517 84420 80913 58 56 +f1v0s7 f2v2s24 0.507463 84420 68414 58 49 +f1v0s7 f3v11s36 0.520000 84420 91927 58 65 +f1v0s7 f0v5s5 0.487500 84420 2668 58 69 +f1v0s7 f1v11s18 0.982759 84420 51814 58 58 +f1v0s7 f0v6s6 0.475000 84420 16273 58 69 +f1v0s7 f3v4s29 0.526316 84420 94356 58 68 +f1v0s7 f0v2s2 0.487500 84420 10896 58 69 +f1v0s7 f3v12s37 0.533333 84420 74202 58 67 +f1v0s7 f0v3s3 0.500000 84420 73040 58 67 +f1v0s7 f3v14s39 0.527027 84420 55015 58 66 +f1v0s7 f0v0s0 0.487500 84420 1332 58 69 +f1v0s7 f1v12s19 0.965517 84420 58178 58 57 +f3v9s34 f4v0s40 0.472973 40677 75843 65 60 +f3v9s34 f1v7s14 0.540541 40677 42277 65 58 +f3v9s34 f3v2s27 0.954545 40677 74030 65 65 +f3v9s34 f4v1s41 0.479452 40677 45686 65 61 +f3v9s34 f2v0s22 0.512821 40677 43219 65 69 +f3v9s34 f1v2s9 0.500000 40677 99108 65 69 +f3v9s34 f1v3s10 0.540541 40677 43230 65 58 +f3v9s34 f3v0s25 0.969697 40677 62755 65 66 +f3v9s34 f3v3s28 0.940299 40677 66465 65 66 +f3v9s34 f3v13s38 0.941176 40677 26948 65 68 +f3v9s34 f3v7s32 1.000000 40677 53710 65 65 +f3v9s34 f3v1s26 0.939394 40677 41283 65 65 +f3v9s34 f3v6s31 1.000000 40677 94783 65 65 +f3v9s34 f1v13s20 0.534247 40677 80913 65 56 +f3v9s34 f2v2s24 0.507463 40677 68414 65 49 +f3v9s34 f3v11s36 0.910448 40677 91927 65 65 +f3v9s34 f0v5s5 0.512821 40677 2668 65 69 +f3v9s34 f1v11s18 0.540541 40677 51814 65 58 +f3v9s34 f0v6s6 0.487500 40677 16273 65 69 +f3v9s34 f3v4s29 0.926471 40677 94356 65 68 +f3v9s34 f0v2s2 0.512821 40677 10896 65 69 +f3v9s34 f3v12s37 0.940299 40677 74202 65 67 +f3v9s34 f0v3s3 0.512500 40677 73040 65 67 +f3v9s34 f3v14s39 0.969697 40677 55015 65 66 +f3v9s34 f0v0s0 0.518987 40677 1332 65 69 +f3v9s34 f1v12s19 0.534247 40677 58178 65 57 +f4v0s40 f1v7s14 0.486111 75843 42277 60 58 +f4v0s40 f3v2s27 0.493151 75843 74030 60 65 +f4v0s40 f4v1s41 0.983607 75843 45686 60 61 +f4v0s40 f2v0s22 0.520000 75843 43219 60 69 +f4v0s40 f1v2s9 0.481013 75843 99108 60 69 +f4v0s40 f1v3s10 0.486111 75843 43230 60 58 +f4v0s40 f3v0s25 0.486486 75843 62755 60 66 +f4v0s40 f3v3s28 0.486486 75843 66465 60 66 +f4v0s40 f3v13s38 0.474359 75843 26948 60 68 +f4v0s40 f3v7s32 0.472973 75843 53710 60 65 +f4v0s40 f3v1s26 0.493151 75843 41283 60 65 +f4v0s40 f3v6s31 0.472973 75843 94783 60 65 +f4v0s40 f1v13s20 0.492754 75843 80913 60 56 +f4v0s40 f2v2s24 0.515152 75843 68414 60 49 +f4v0s40 f3v11s36 0.493151 75843 91927 60 65 +f4v0s40 f0v5s5 0.506667 75843 2668 60 69 +f4v0s40 f1v11s18 0.472973 75843 51814 60 58 +f4v0s40 f0v6s6 0.520000 75843 16273 60 69 +f4v0s40 f3v4s29 0.475000 75843 94356 60 68 +f4v0s40 f0v2s2 0.520000 75843 10896 60 69 +f4v0s40 f3v12s37 0.486842 75843 74202 60 67 +f4v0s40 f0v3s3 0.506849 75843 73040 60 67 +f4v0s40 f3v14s39 0.486486 75843 55015 60 66 +f4v0s40 f0v0s0 0.513514 75843 1332 60 69 +f4v0s40 f1v12s19 0.492537 75843 58178 60 57 +f1v7s14 f3v2s27 0.534247 42277 74030 58 65 +f1v7s14 f4v1s41 0.507246 42277 45686 58 61 +f1v7s14 f2v0s22 0.487500 42277 43219 58 69 +f1v7s14 f1v2s9 0.481013 42277 99108 58 69 +f1v7s14 f1v3s10 1.000000 42277 43230 58 58 +f1v7s14 f3v0s25 0.527027 42277 62755 58 66 +f1v7s14 f3v3s28 0.520000 42277 66465 58 66 +f1v7s14 f3v13s38 0.506329 42277 26948 58 68 +f1v7s14 f3v7s32 0.540541 42277 53710 58 65 +f1v7s14 f3v1s26 0.534247 42277 41283 58 65 +f1v7s14 f3v6s31 0.540541 42277 94783 58 65 +f1v7s14 f1v13s20 0.965517 42277 80913 58 56 +f1v7s14 f2v2s24 0.507463 42277 68414 58 49 +f1v7s14 f3v11s36 0.520000 42277 91927 58 65 +f1v7s14 f0v5s5 0.487500 42277 2668 58 69 +f1v7s14 f1v11s18 0.982759 42277 51814 58 58 +f1v7s14 f0v6s6 0.475000 42277 16273 58 69 +f1v7s14 f3v4s29 0.526316 42277 94356 58 68 +f1v7s14 f0v2s2 0.487500 42277 10896 58 69 +f1v7s14 f3v12s37 0.533333 42277 74202 58 67 +f1v7s14 f0v3s3 0.500000 42277 73040 58 67 +f1v7s14 f3v14s39 0.527027 42277 55015 58 66 +f1v7s14 f0v0s0 0.487500 42277 1332 58 69 +f1v7s14 f1v12s19 0.965517 42277 58178 58 57 +f3v2s27 f4v1s41 0.486486 74030 45686 65 61 +f3v2s27 f2v0s22 0.525641 74030 43219 65 69 +f3v2s27 f1v2s9 0.500000 74030 99108 65 69 +f3v2s27 f1v3s10 0.534247 74030 43230 65 58 +f3v2s27 f3v0s25 0.984848 74030 62755 65 66 +f3v2s27 f3v3s28 0.955224 74030 66465 65 66 +f3v2s27 f3v13s38 0.955882 74030 26948 65 68 +f3v2s27 f3v7s32 0.954545 74030 53710 65 65 +f3v2s27 f3v1s26 0.969231 74030 41283 65 65 +f3v2s27 f3v6s31 0.954545 74030 94783 65 65 +f3v2s27 f1v13s20 0.527778 74030 80913 65 56 +f3v2s27 f2v2s24 0.507246 74030 68414 65 49 +f3v2s27 f3v11s36 0.925373 74030 91927 65 65 +f3v2s27 f0v5s5 0.512821 74030 2668 65 69 +f3v2s27 f1v11s18 0.534247 74030 51814 65 58 +f3v2s27 f0v6s6 0.487805 74030 16273 65 69 +f3v2s27 f3v4s29 0.941176 74030 94356 65 68 +f3v2s27 f0v2s2 0.525641 74030 10896 65 69 +f3v2s27 f3v12s37 0.955224 74030 74202 65 67 +f3v2s27 f0v3s3 0.512500 74030 73040 65 67 +f3v2s27 f3v14s39 0.984848 74030 55015 65 66 +f3v2s27 f0v0s0 0.518987 74030 1332 65 69 +f3v2s27 f1v12s19 0.527778 74030 58178 65 57 +f4v1s41 f2v0s22 0.513158 45686 43219 61 69 +f4v1s41 f1v2s9 0.475000 45686 99108 61 69 +f4v1s41 f1v3s10 0.507246 45686 43230 61 58 +f4v1s41 f3v0s25 0.480000 45686 62755 61 66 +f4v1s41 f3v3s28 0.480000 45686 66465 61 66 +f4v1s41 f3v13s38 0.468354 45686 26948 61 68 +f4v1s41 f3v7s32 0.479452 45686 53710 61 65 +f4v1s41 f3v1s26 0.486486 45686 41283 61 65 +f4v1s41 f3v6s31 0.479452 45686 94783 61 65 +f4v1s41 f1v13s20 0.507463 45686 80913 61 56 +f4v1s41 f2v2s24 0.507463 45686 68414 61 49 +f4v1s41 f3v11s36 0.493151 45686 91927 61 65 +f4v1s41 f0v5s5 0.500000 45686 2668 61 69 +f4v1s41 f1v11s18 0.492958 45686 51814 61 58 +f4v1s41 f0v6s6 0.513158 45686 16273 61 69 +f4v1s41 f3v4s29 0.469136 45686 94356 61 68 +f4v1s41 f0v2s2 0.513158 45686 10896 61 69 +f4v1s41 f3v12s37 0.480519 45686 74202 61 67 +f4v1s41 f0v3s3 0.500000 45686 73040 61 67 +f4v1s41 f3v14s39 0.480000 45686 55015 61 66 +f4v1s41 f0v0s0 0.506667 45686 1332 61 69 +f4v1s41 f1v12s19 0.492537 45686 58178 61 57 +f2v0s22 f1v2s9 0.943662 43219 99108 69 69 +f2v0s22 f1v3s10 0.487500 43219 43230 69 58 +f2v0s22 f3v0s25 0.518987 43219 62755 69 66 +f2v0s22 f3v3s28 0.525641 43219 66465 69 66 +f2v0s22 f3v13s38 0.512500 43219 26948 69 68 +f2v0s22 f3v7s32 0.512821 43219 53710 69 65 +f2v0s22 f3v1s26 0.506329 43219 41283 69 65 +f2v0s22 f3v6s31 0.512821 43219 94783 69 65 +f2v0s22 f1v13s20 0.487179 43219 80913 69 56 +f2v0s22 f2v2s24 0.486111 43219 68414 69 49 +f2v0s22 f3v11s36 0.487179 43219 91927 69 65 +f2v0s22 f0v5s5 0.943662 43219 2668 69 69 +f2v0s22 f1v11s18 0.500000 43219 51814 69 58 +f2v0s22 f0v6s6 0.901408 43219 16273 69 69 +f2v0s22 f3v4s29 0.551282 43219 94356 69 68 +f2v0s22 f0v2s2 1.000000 43219 10896 69 69 +f2v0s22 f3v12s37 0.525000 43219 74202 69 67 +f2v0s22 f0v3s3 0.915493 43219 73040 69 67 +f2v0s22 f3v14s39 0.518987 43219 55015 69 66 +f2v0s22 f0v0s0 0.971429 43219 1332 69 69 +f2v0s22 f1v12s19 0.487500 43219 58178 69 57 +f1v2s9 f1v3s10 0.481013 99108 43230 69 58 +f1v2s9 f3v0s25 0.493827 99108 62755 69 66 +f1v2s9 f3v3s28 0.500000 99108 66465 69 66 +f1v2s9 f3v13s38 0.493827 99108 26948 69 68 +f1v2s9 f3v7s32 0.500000 99108 53710 69 65 +f1v2s9 f3v1s26 0.481481 99108 41283 69 65 +f1v2s9 f3v6s31 0.500000 99108 94783 69 65 +f1v2s9 f1v13s20 0.480519 99108 80913 69 56 +f1v2s9 f2v2s24 0.486111 99108 68414 69 49 +f1v2s9 f3v11s36 0.468354 99108 91927 69 65 +f1v2s9 f0v5s5 0.943662 99108 2668 69 69 +f1v2s9 f1v11s18 0.493506 99108 51814 69 58 +f1v2s9 f0v6s6 0.901408 99108 16273 69 69 +f1v2s9 f3v4s29 0.525000 99108 94356 69 68 +f1v2s9 f0v2s2 0.943662 99108 10896 69 69 +f1v2s9 f3v12s37 0.493827 99108 74202 69 67 +f1v2s9 f0v3s3 0.915493 99108 73040 69 67 +f1v2s9 f3v14s39 0.493827 99108 55015 69 66 +f1v2s9 f0v0s0 0.971429 99108 1332 69 69 +f1v2s9 f1v12s19 0.457831 99108 58178 69 57 +f1v3s10 f3v0s25 0.527027 43230 62755 58 66 +f1v3s10 f3v3s28 0.520000 43230 66465 58 66 +f1v3s10 f3v13s38 0.506329 43230 26948 58 68 +f1v3s10 f3v7s32 0.540541 43230 53710 58 65 +f1v3s10 f3v1s26 0.534247 43230 41283 58 65 +f1v3s10 f3v6s31 0.540541 43230 94783 58 65 +f1v3s10 f1v13s20 0.965517 43230 80913 58 56 +f1v3s10 f2v2s24 0.507463 43230 68414 58 49 +f1v3s10 f3v11s36 0.520000 43230 91927 58 65 +f1v3s10 f0v5s5 0.487500 43230 2668 58 69 +f1v3s10 f1v11s18 0.982759 43230 51814 58 58 +f1v3s10 f0v6s6 0.475000 43230 16273 58 69 +f1v3s10 f3v4s29 0.526316 43230 94356 58 68 +f1v3s10 f0v2s2 0.487500 43230 10896 58 69 +f1v3s10 f3v12s37 0.533333 43230 74202 58 67 +f1v3s10 f0v3s3 0.500000 43230 73040 58 67 +f1v3s10 f3v14s39 0.527027 43230 55015 58 66 +f1v3s10 f0v0s0 0.487500 43230 1332 58 69 +f1v3s10 f1v12s19 0.965517 43230 58178 58 57 +f3v0s25 f3v3s28 0.970149 62755 66465 66 66 +f3v0s25 f3v13s38 0.970588 62755 26948 66 68 +f3v0s25 f3v7s32 0.969697 62755 53710 66 65 +f3v0s25 f3v1s26 0.969697 62755 41283 66 65 +f3v0s25 f3v6s31 0.969697 62755 94783 66 65 +f3v0s25 f1v13s20 0.520548 62755 80913 66 56 +f3v0s25 f2v2s24 0.514706 62755 68414 66 49 +f3v0s25 f3v11s36 0.940299 62755 91927 66 65 +f3v0s25 f0v5s5 0.506329 62755 2668 66 69 +f3v0s25 f1v11s18 0.527027 62755 51814 66 58 +f3v0s25 f0v6s6 0.481928 62755 16273 66 69 +f3v0s25 f3v4s29 0.955882 62755 94356 66 68 +f3v0s25 f0v2s2 0.518987 62755 10896 66 69 +f3v0s25 f3v12s37 0.970149 62755 74202 66 67 +f3v0s25 f0v3s3 0.506173 62755 73040 66 67 +f3v0s25 f3v14s39 1.000000 62755 55015 66 66 +f3v0s25 f0v0s0 0.512500 62755 1332 66 69 +f3v0s25 f1v12s19 0.520548 62755 58178 66 57 +f3v3s28 f3v13s38 0.955882 66465 26948 66 68 +f3v3s28 f3v7s32 0.940299 66465 53710 66 65 +f3v3s28 f3v1s26 0.940299 66465 41283 66 65 +f3v3s28 f3v6s31 0.940299 66465 94783 66 65 +f3v3s28 f1v13s20 0.513514 66465 80913 66 56 +f3v3s28 f2v2s24 0.521739 66465 68414 66 49 +f3v3s28 f3v11s36 0.911765 66465 91927 66 65 +f3v3s28 f0v5s5 0.506329 66465 2668 66 69 +f3v3s28 f1v11s18 0.506494 66465 51814 66 58 +f3v3s28 f0v6s6 0.487805 66465 16273 66 69 +f3v3s28 f3v4s29 0.941176 66465 94356 66 68 +f3v3s28 f0v2s2 0.525641 66465 10896 66 69 +f3v3s28 f3v12s37 0.941176 66465 74202 66 67 +f3v3s28 f0v3s3 0.512500 66465 73040 66 67 +f3v3s28 f3v14s39 0.970149 66465 55015 66 66 +f3v3s28 f0v0s0 0.518987 66465 1332 66 69 +f3v3s28 f1v12s19 0.500000 66465 58178 66 57 +f3v13s38 f3v7s32 0.941176 26948 53710 68 65 +f3v13s38 f3v1s26 0.941176 26948 41283 68 65 +f3v13s38 f3v6s31 0.941176 26948 94783 68 65 +f3v13s38 f1v13s20 0.500000 26948 80913 68 56 +f3v13s38 f2v2s24 0.514286 26948 68414 68 49 +f3v13s38 f3v11s36 0.913043 26948 91927 68 65 +f3v13s38 f0v5s5 0.506329 26948 2668 68 69 +f3v13s38 f1v11s18 0.506329 26948 51814 68 58 +f3v13s38 f0v6s6 0.481928 26948 16273 68 69 +f3v13s38 f3v4s29 0.942029 26948 94356 68 68 +f3v13s38 f0v2s2 0.512500 26948 10896 68 69 +f3v13s38 f3v12s37 0.942029 26948 74202 68 67 +f3v13s38 f0v3s3 0.506173 26948 73040 68 67 +f3v13s38 f3v14s39 0.970588 26948 55015 68 66 +f3v13s38 f0v0s0 0.512500 26948 1332 68 69 +f3v13s38 f1v12s19 0.527027 26948 58178 68 57 +f3v7s32 f3v1s26 0.939394 53710 41283 65 65 +f3v7s32 f3v6s31 1.000000 53710 94783 65 65 +f3v7s32 f1v13s20 0.534247 53710 80913 65 56 +f3v7s32 f2v2s24 0.507463 53710 68414 65 49 +f3v7s32 f3v11s36 0.910448 53710 91927 65 65 +f3v7s32 f0v5s5 0.512821 53710 2668 65 69 +f3v7s32 f1v11s18 0.540541 53710 51814 65 58 +f3v7s32 f0v6s6 0.487500 53710 16273 65 69 +f3v7s32 f3v4s29 0.926471 53710 94356 65 68 +f3v7s32 f0v2s2 0.512821 53710 10896 65 69 +f3v7s32 f3v12s37 0.940299 53710 74202 65 67 +f3v7s32 f0v3s3 0.512500 53710 73040 65 67 +f3v7s32 f3v14s39 0.969697 53710 55015 65 66 +f3v7s32 f0v0s0 0.518987 53710 1332 65 69 +f3v7s32 f1v12s19 0.534247 53710 58178 65 57 +f3v1s26 f3v6s31 0.939394 41283 94783 65 65 +f3v1s26 f1v13s20 0.527778 41283 80913 65 56 +f3v1s26 f2v2s24 0.514706 41283 68414 65 49 +f3v1s26 f3v11s36 0.910448 41283 91927 65 65 +f3v1s26 f0v5s5 0.493671 41283 2668 65 69 +f3v1s26 f1v11s18 0.513158 41283 51814 65 58 +f3v1s26 f0v6s6 0.469880 41283 16273 65 69 +f3v1s26 f3v4s29 0.926471 41283 94356 65 68 +f3v1s26 f0v2s2 0.506329 41283 10896 65 69 +f3v1s26 f3v12s37 0.940299 41283 74202 65 67 +f3v1s26 f0v3s3 0.506329 41283 73040 65 67 +f3v1s26 f3v14s39 0.969697 41283 55015 65 66 +f3v1s26 f0v0s0 0.500000 41283 1332 65 69 +f3v1s26 f1v12s19 0.527778 41283 58178 65 57 +f3v6s31 f1v13s20 0.534247 94783 80913 65 56 +f3v6s31 f2v2s24 0.507463 94783 68414 65 49 +f3v6s31 f3v11s36 0.910448 94783 91927 65 65 +f3v6s31 f0v5s5 0.512821 94783 2668 65 69 +f3v6s31 f1v11s18 0.540541 94783 51814 65 58 +f3v6s31 f0v6s6 0.487500 94783 16273 65 69 +f3v6s31 f3v4s29 0.926471 94783 94356 65 68 +f3v6s31 f0v2s2 0.512821 94783 10896 65 69 +f3v6s31 f3v12s37 0.940299 94783 74202 65 67 +f3v6s31 f0v3s3 0.512500 94783 73040 65 67 +f3v6s31 f3v14s39 0.969697 94783 55015 65 66 +f3v6s31 f0v0s0 0.518987 94783 1332 65 69 +f3v6s31 f1v12s19 0.534247 94783 58178 65 57 +f1v13s20 f2v2s24 0.523077 80913 68414 56 49 +f1v13s20 f3v11s36 0.513514 80913 91927 56 65 +f1v13s20 f0v5s5 0.500000 80913 2668 56 69 +f1v13s20 f1v11s18 0.948276 80913 51814 56 58 +f1v13s20 f0v6s6 0.474359 80913 16273 56 69 +f1v13s20 f3v4s29 0.520000 80913 94356 56 68 +f1v13s20 f0v2s2 0.487179 80913 10896 56 69 +f1v13s20 f3v12s37 0.527027 80913 74202 56 67 +f1v13s20 f0v3s3 0.500000 80913 73040 56 67 +f1v13s20 f3v14s39 0.520548 80913 55015 56 66 +f1v13s20 f0v0s0 0.487179 80913 1332 56 69 +f1v13s20 f1v12s19 0.931034 80913 58178 56 57 +f2v2s24 f3v11s36 0.507463 68414 91927 49 65 +f2v2s24 f0v5s5 0.460526 68414 2668 49 69 +f2v2s24 f1v11s18 0.507463 68414 51814 49 58 +f2v2s24 f0v6s6 0.479452 68414 16273 49 69 +f2v2s24 f3v4s29 0.514286 68414 94356 49 68 +f2v2s24 f0v2s2 0.486111 68414 10896 49 69 +f2v2s24 f3v12s37 0.521739 68414 74202 49 67 +f2v2s24 f0v3s3 0.492754 68414 73040 49 67 +f2v2s24 f3v14s39 0.514706 68414 55015 49 66 +f2v2s24 f0v0s0 0.486111 68414 1332 49 69 +f2v2s24 f1v12s19 0.500000 68414 58178 49 57 +f3v11s36 f0v5s5 0.469136 91927 2668 65 69 +f3v11s36 f1v11s18 0.520000 91927 51814 65 58 +f3v11s36 f0v6s6 0.463415 91927 16273 65 69 +f3v11s36 f3v4s29 0.898551 91927 94356 65 68 +f3v11s36 f0v2s2 0.487179 91927 10896 65 69 +f3v11s36 f3v12s37 0.911765 91927 74202 65 67 +f3v11s36 f0v3s3 0.481013 91927 73040 65 67 +f3v11s36 f3v14s39 0.940299 91927 55015 65 66 +f3v11s36 f0v0s0 0.487179 91927 1332 65 69 +f3v11s36 f1v12s19 0.513514 91927 58178 65 57 +f0v5s5 f1v11s18 0.500000 2668 51814 69 58 +f0v5s5 f0v6s6 0.901408 2668 16273 69 69 +f0v5s5 f3v4s29 0.538462 2668 94356 69 68 +f0v5s5 f0v2s2 0.943662 2668 10896 69 69 +f0v5s5 f3v12s37 0.500000 2668 74202 69 67 +f0v5s5 f0v3s3 0.915493 2668 73040 69 67 +f0v5s5 f3v14s39 0.506329 2668 55015 69 66 +f0v5s5 f0v0s0 0.971429 2668 1332 69 69 +f0v5s5 f1v12s19 0.475000 2668 58178 69 57 +f1v11s18 f0v6s6 0.493506 51814 16273 58 69 +f1v11s18 f3v4s29 0.526316 51814 94356 58 68 +f1v11s18 f0v2s2 0.500000 51814 10896 58 69 +f1v11s18 f3v12s37 0.533333 51814 74202 58 67 +f1v11s18 f0v3s3 0.513158 51814 73040 58 67 +f1v11s18 f3v14s39 0.527027 51814 55015 58 66 +f1v11s18 f0v0s0 0.500000 51814 1332 58 69 +f1v11s18 f1v12s19 0.948276 51814 58178 58 57 +f0v6s6 f3v4s29 0.512195 16273 94356 69 68 +f0v6s6 f0v2s2 0.901408 16273 10896 69 69 +f0v6s6 f3v12s37 0.476190 16273 74202 69 67 +f0v6s6 f0v3s3 0.873239 16273 73040 69 67 +f0v6s6 f3v14s39 0.481928 16273 55015 69 66 +f0v6s6 f0v0s0 0.928571 16273 1332 69 69 +f0v6s6 f1v12s19 0.457831 16273 58178 69 57 +f3v4s29 f0v2s2 0.551282 94356 10896 68 69 +f3v4s29 f3v12s37 0.927536 94356 74202 68 67 +f3v4s29 f0v3s3 0.530864 94356 73040 68 67 +f3v4s29 f3v14s39 0.955882 94356 55015 68 66 +f3v4s29 f0v0s0 0.544304 94356 1332 68 69 +f3v4s29 f1v12s19 0.520000 94356 58178 68 57 +f0v2s2 f3v12s37 0.525000 10896 74202 69 67 +f0v2s2 f0v3s3 0.915493 10896 73040 69 67 +f0v2s2 f3v14s39 0.518987 10896 55015 69 66 +f0v2s2 f0v0s0 0.971429 10896 1332 69 69 +f0v2s2 f1v12s19 0.487500 10896 58178 69 57 +f3v12s37 f0v3s3 0.500000 74202 73040 67 67 +f3v12s37 f3v14s39 0.970149 74202 55015 67 66 +f3v12s37 f0v0s0 0.512500 74202 1332 67 69 +f3v12s37 f1v12s19 0.527027 74202 58178 67 57 +f0v3s3 f3v14s39 0.506173 73040 55015 67 66 +f0v3s3 f0v0s0 0.942857 73040 1332 67 69 +f0v3s3 f1v12s19 0.487179 73040 58178 67 57 +f3v14s39 f0v0s0 0.512500 55015 1332 66 69 +f3v14s39 f1v12s19 0.520548 55015 58178 66 57 +f0v0s0 f1v12s19 0.464286 1332 58178 69 57 diff --git a/pkg/obitools/obialign/testdata/one_alldist.expected b/pkg/obitools/obialign/testdata/one_alldist.expected new file mode 100644 index 0000000..5a38a2b --- /dev/null +++ b/pkg/obitools/obialign/testdata/one_alldist.expected @@ -0,0 +1,903 @@ +f3v10s35 f3v5s30 9.000000 +f3v10s35 f1v8s15 34.000000 +f3v10s35 f2v1s23 38.000000 +f3v10s35 f4v2s42 37.000000 +f3v10s35 f0v4s4 40.000000 +f3v10s35 f1v6s13 33.000000 +f3v10s35 f3v8s33 6.000000 +f3v10s35 f1v1s8 35.000000 +f3v10s35 f1v14s21 37.000000 +f3v10s35 f0v1s1 40.000000 +f3v10s35 f1v5s12 37.000000 +f3v10s35 f1v4s11 38.000000 +f3v10s35 f1v9s16 38.000000 +f3v10s35 f1v10s17 36.000000 +f3v10s35 f1v0s7 38.000000 +f3v10s35 f3v9s34 6.000000 +f3v10s35 f4v0s40 38.000000 +f3v10s35 f1v7s14 38.000000 +f3v10s35 f3v2s27 6.000000 +f3v10s35 f4v1s41 39.000000 +f3v10s35 f2v0s22 39.000000 +f3v10s35 f1v2s9 40.000000 +f3v10s35 f1v3s10 38.000000 +f3v10s35 f3v0s25 5.000000 +f3v10s35 f3v3s28 7.000000 +f3v10s35 f3v13s38 7.000000 +f3v10s35 f3v7s32 6.000000 +f3v10s35 f3v1s26 6.000000 +f3v10s35 f3v6s31 6.000000 +f3v10s35 f1v13s20 38.000000 +f3v10s35 f2v2s24 35.000000 +f3v10s35 f3v11s36 9.000000 +f3v10s35 f0v5s5 39.000000 +f3v10s35 f1v11s18 38.000000 +f3v10s35 f0v6s6 42.000000 +f3v10s35 f3v4s29 8.000000 +f3v10s35 f0v2s2 39.000000 +f3v10s35 f3v12s37 7.000000 +f3v10s35 f0v3s3 40.000000 +f3v10s35 f3v14s39 5.000000 +f3v10s35 f0v0s0 39.000000 +f3v10s35 f1v12s19 35.000000 +f3v5s30 f1v8s15 35.000000 +f3v5s30 f2v1s23 36.000000 +f3v5s30 f4v2s42 35.000000 +f3v5s30 f0v4s4 40.000000 +f3v5s30 f1v6s13 36.000000 +f3v5s30 f3v8s33 5.000000 +f3v5s30 f1v1s8 37.000000 +f3v5s30 f1v14s21 34.000000 +f3v5s30 f0v1s1 40.000000 +f3v5s30 f1v5s12 38.000000 +f3v5s30 f1v4s11 36.000000 +f3v5s30 f1v9s16 37.000000 +f3v5s30 f1v10s17 38.000000 +f3v5s30 f1v0s7 36.000000 +f3v5s30 f3v9s34 6.000000 +f3v5s30 f4v0s40 34.000000 +f3v5s30 f1v7s14 36.000000 +f3v5s30 f3v2s27 5.000000 +f3v5s30 f4v1s41 35.000000 +f3v5s30 f2v0s22 39.000000 +f3v5s30 f1v2s9 40.000000 +f3v5s30 f1v3s10 36.000000 +f3v5s30 f3v0s25 4.000000 +f3v5s30 f3v3s28 6.000000 +f3v5s30 f3v13s38 6.000000 +f3v5s30 f3v7s32 6.000000 +f3v5s30 f3v1s26 6.000000 +f3v5s30 f3v6s31 6.000000 +f3v5s30 f1v13s20 36.000000 +f3v5s30 f2v2s24 32.000000 +f3v5s30 f3v11s36 7.000000 +f3v5s30 f0v5s5 39.000000 +f3v5s30 f1v11s18 36.000000 +f3v5s30 f0v6s6 40.000000 +f3v5s30 f3v4s29 7.000000 +f3v5s30 f0v2s2 39.000000 +f3v5s30 f3v12s37 6.000000 +f3v5s30 f0v3s3 39.000000 +f3v5s30 f3v14s39 4.000000 +f3v5s30 f0v0s0 39.000000 +f3v5s30 f1v12s19 36.000000 +f1v8s15 f2v1s23 1.000000 +f1v8s15 f4v2s42 37.000000 +f1v8s15 f0v4s4 40.000000 +f1v8s15 f1v6s13 3.000000 +f1v8s15 f3v8s33 33.000000 +f1v8s15 f1v1s8 5.000000 +f1v8s15 f1v14s21 4.000000 +f1v8s15 f0v1s1 40.000000 +f1v8s15 f1v5s12 5.000000 +f1v8s15 f1v4s11 1.000000 +f1v8s15 f1v9s16 4.000000 +f1v8s15 f1v10s17 5.000000 +f1v8s15 f1v0s7 1.000000 +f1v8s15 f3v9s34 33.000000 +f1v8s15 f4v0s40 37.000000 +f1v8s15 f1v7s14 1.000000 +f1v8s15 f3v2s27 33.000000 +f1v8s15 f4v1s41 38.000000 +f1v8s15 f2v0s22 40.000000 +f1v8s15 f1v2s9 40.000000 +f1v8s15 f1v3s10 1.000000 +f1v8s15 f3v0s25 34.000000 +f1v8s15 f3v3s28 35.000000 +f1v8s15 f3v13s38 39.000000 +f1v8s15 f3v7s32 33.000000 +f1v8s15 f3v1s26 33.000000 +f1v8s15 f3v6s31 33.000000 +f1v8s15 f1v13s20 3.000000 +f1v8s15 f2v2s24 32.000000 +f1v8s15 f3v11s36 35.000000 +f1v8s15 f0v5s5 40.000000 +f1v8s15 f1v11s18 2.000000 +f1v8s15 f0v6s6 41.000000 +f1v8s15 f3v4s29 35.000000 +f1v8s15 f0v2s2 40.000000 +f1v8s15 f3v12s37 34.000000 +f1v8s15 f0v3s3 38.000000 +f1v8s15 f3v14s39 34.000000 +f1v8s15 f0v0s0 40.000000 +f1v8s15 f1v12s19 3.000000 +f2v1s23 f4v2s42 35.000000 +f2v1s23 f0v4s4 41.000000 +f2v1s23 f1v6s13 4.000000 +f2v1s23 f3v8s33 34.000000 +f2v1s23 f1v1s8 4.000000 +f2v1s23 f1v14s21 3.000000 +f2v1s23 f0v1s1 41.000000 +f2v1s23 f1v5s12 4.000000 +f2v1s23 f1v4s11 0.000000 +f2v1s23 f1v9s16 3.000000 +f2v1s23 f1v10s17 4.000000 +f2v1s23 f1v0s7 0.000000 +f2v1s23 f3v9s34 34.000000 +f2v1s23 f4v0s40 37.000000 +f2v1s23 f1v7s14 0.000000 +f2v1s23 f3v2s27 34.000000 +f2v1s23 f4v1s41 34.000000 +f2v1s23 f2v0s22 41.000000 +f2v1s23 f1v2s9 41.000000 +f2v1s23 f1v3s10 0.000000 +f2v1s23 f3v0s25 35.000000 +f2v1s23 f3v3s28 36.000000 +f2v1s23 f3v13s38 39.000000 +f2v1s23 f3v7s32 34.000000 +f2v1s23 f3v1s26 34.000000 +f2v1s23 f3v6s31 34.000000 +f2v1s23 f1v13s20 2.000000 +f2v1s23 f2v2s24 33.000000 +f2v1s23 f3v11s36 36.000000 +f2v1s23 f0v5s5 41.000000 +f2v1s23 f1v11s18 1.000000 +f2v1s23 f0v6s6 42.000000 +f2v1s23 f3v4s29 36.000000 +f2v1s23 f0v2s2 41.000000 +f2v1s23 f3v12s37 35.000000 +f2v1s23 f0v3s3 39.000000 +f2v1s23 f3v14s39 35.000000 +f2v1s23 f0v0s0 41.000000 +f2v1s23 f1v12s19 2.000000 +f4v2s42 f0v4s4 36.000000 +f4v2s42 f1v6s13 36.000000 +f4v2s42 f3v8s33 37.000000 +f4v2s42 f1v1s8 39.000000 +f4v2s42 f1v14s21 33.000000 +f4v2s42 f0v1s1 36.000000 +f4v2s42 f1v5s12 36.000000 +f4v2s42 f1v4s11 35.000000 +f4v2s42 f1v9s16 37.000000 +f4v2s42 f1v10s17 40.000000 +f4v2s42 f1v0s7 35.000000 +f4v2s42 f3v9s34 39.000000 +f4v2s42 f4v0s40 2.000000 +f4v2s42 f1v7s14 35.000000 +f4v2s42 f3v2s27 37.000000 +f4v2s42 f4v1s41 3.000000 +f4v2s42 f2v0s22 34.000000 +f4v2s42 f1v2s9 36.000000 +f4v2s42 f1v3s10 35.000000 +f4v2s42 f3v0s25 38.000000 +f4v2s42 f3v3s28 38.000000 +f4v2s42 f3v13s38 41.000000 +f4v2s42 f3v7s32 39.000000 +f4v2s42 f3v1s26 37.000000 +f4v2s42 f3v6s31 39.000000 +f4v2s42 f1v13s20 34.000000 +f4v2s42 f2v2s24 31.000000 +f4v2s42 f3v11s36 37.000000 +f4v2s42 f0v5s5 36.000000 +f4v2s42 f1v11s18 37.000000 +f4v2s42 f0v6s6 35.000000 +f4v2s42 f3v4s29 41.000000 +f4v2s42 f0v2s2 34.000000 +f4v2s42 f3v12s37 39.000000 +f4v2s42 f0v3s3 35.000000 +f4v2s42 f3v14s39 38.000000 +f4v2s42 f0v0s0 35.000000 +f4v2s42 f1v12s19 34.000000 +f0v4s4 f1v6s13 38.000000 +f0v4s4 f3v8s33 40.000000 +f0v4s4 f1v1s8 41.000000 +f0v4s4 f1v14s21 39.000000 +f0v4s4 f0v1s1 0.000000 +f0v4s4 f1v5s12 43.000000 +f0v4s4 f1v4s11 41.000000 +f0v4s4 f1v9s16 40.000000 +f0v4s4 f1v10s17 42.000000 +f0v4s4 f1v0s7 41.000000 +f0v4s4 f3v9s34 40.000000 +f0v4s4 f4v0s40 41.000000 +f0v4s4 f1v7s14 41.000000 +f0v4s4 f3v2s27 40.000000 +f0v4s4 f4v1s41 42.000000 +f0v4s4 f2v0s22 4.000000 +f0v4s4 f1v2s9 0.000000 +f0v4s4 f1v3s10 41.000000 +f0v4s4 f3v0s25 41.000000 +f0v4s4 f3v3s28 40.000000 +f0v4s4 f3v13s38 41.000000 +f0v4s4 f3v7s32 40.000000 +f0v4s4 f3v1s26 42.000000 +f0v4s4 f3v6s31 40.000000 +f0v4s4 f1v13s20 40.000000 +f0v4s4 f2v2s24 37.000000 +f0v4s4 f3v11s36 42.000000 +f0v4s4 f0v5s5 4.000000 +f0v4s4 f1v11s18 39.000000 +f0v4s4 f0v6s6 7.000000 +f0v4s4 f3v4s29 38.000000 +f0v4s4 f0v2s2 4.000000 +f0v4s4 f3v12s37 41.000000 +f0v4s4 f0v3s3 6.000000 +f0v4s4 f3v14s39 41.000000 +f0v4s4 f0v0s0 2.000000 +f0v4s4 f1v12s19 45.000000 +f1v6s13 f3v8s33 33.000000 +f1v6s13 f1v1s8 8.000000 +f1v6s13 f1v14s21 7.000000 +f1v6s13 f0v1s1 38.000000 +f1v6s13 f1v5s12 8.000000 +f1v6s13 f1v4s11 4.000000 +f1v6s13 f1v9s16 7.000000 +f1v6s13 f1v10s17 7.000000 +f1v6s13 f1v0s7 4.000000 +f1v6s13 f3v9s34 33.000000 +f1v6s13 f4v0s40 38.000000 +f1v6s13 f1v7s14 4.000000 +f1v6s13 f3v2s27 32.000000 +f1v6s13 f4v1s41 39.000000 +f1v6s13 f2v0s22 39.000000 +f1v6s13 f1v2s9 38.000000 +f1v6s13 f1v3s10 4.000000 +f1v6s13 f3v0s25 33.000000 +f1v6s13 f3v3s28 34.000000 +f1v6s13 f3v13s38 35.000000 +f1v6s13 f3v7s32 33.000000 +f1v6s13 f3v1s26 32.000000 +f1v6s13 f3v6s31 33.000000 +f1v6s13 f1v13s20 6.000000 +f1v6s13 f2v2s24 32.000000 +f1v6s13 f3v11s36 34.000000 +f1v6s13 f0v5s5 38.000000 +f1v6s13 f1v11s18 5.000000 +f1v6s13 f0v6s6 39.000000 +f1v6s13 f3v4s29 34.000000 +f1v6s13 f0v2s2 39.000000 +f1v6s13 f3v12s37 33.000000 +f1v6s13 f0v3s3 36.000000 +f1v6s13 f3v14s39 33.000000 +f1v6s13 f0v0s0 38.000000 +f1v6s13 f1v12s19 6.000000 +f3v8s33 f1v1s8 33.000000 +f3v8s33 f1v14s21 33.000000 +f3v8s33 f0v1s1 40.000000 +f3v8s33 f1v5s12 35.000000 +f3v8s33 f1v4s11 34.000000 +f3v8s33 f1v9s16 38.000000 +f3v8s33 f1v10s17 32.000000 +f3v8s33 f1v0s7 34.000000 +f3v8s33 f3v9s34 3.000000 +f3v8s33 f4v0s40 42.000000 +f3v8s33 f1v7s14 34.000000 +f3v8s33 f3v2s27 2.000000 +f3v8s33 f4v1s41 43.000000 +f3v8s33 f2v0s22 38.000000 +f3v8s33 f1v2s9 40.000000 +f3v8s33 f1v3s10 34.000000 +f3v8s33 f3v0s25 1.000000 +f3v8s33 f3v3s28 3.000000 +f3v8s33 f3v13s38 3.000000 +f3v8s33 f3v7s32 3.000000 +f3v8s33 f3v1s26 3.000000 +f3v8s33 f3v6s31 3.000000 +f3v8s33 f1v13s20 34.000000 +f3v8s33 f2v2s24 34.000000 +f3v8s33 f3v11s36 5.000000 +f3v8s33 f0v5s5 39.000000 +f3v8s33 f1v11s18 34.000000 +f3v8s33 f0v6s6 43.000000 +f3v8s33 f3v4s29 4.000000 +f3v8s33 f0v2s2 38.000000 +f3v8s33 f3v12s37 3.000000 +f3v8s33 f0v3s3 38.000000 +f3v8s33 f3v14s39 1.000000 +f3v8s33 f0v0s0 38.000000 +f3v8s33 f1v12s19 34.000000 +f1v1s8 f1v14s21 5.000000 +f1v1s8 f0v1s1 41.000000 +f1v1s8 f1v5s12 8.000000 +f1v1s8 f1v4s11 4.000000 +f1v1s8 f1v9s16 7.000000 +f1v1s8 f1v10s17 8.000000 +f1v1s8 f1v0s7 4.000000 +f1v1s8 f3v9s34 32.000000 +f1v1s8 f4v0s40 37.000000 +f1v1s8 f1v7s14 4.000000 +f1v1s8 f3v2s27 32.000000 +f1v1s8 f4v1s41 38.000000 +f1v1s8 f2v0s22 42.000000 +f1v1s8 f1v2s9 41.000000 +f1v1s8 f1v3s10 4.000000 +f1v1s8 f3v0s25 33.000000 +f1v1s8 f3v3s28 34.000000 +f1v1s8 f3v13s38 34.000000 +f1v1s8 f3v7s32 32.000000 +f1v1s8 f3v1s26 31.000000 +f1v1s8 f3v6s31 32.000000 +f1v1s8 f1v13s20 6.000000 +f1v1s8 f2v2s24 36.000000 +f1v1s8 f3v11s36 34.000000 +f1v1s8 f0v5s5 42.000000 +f1v1s8 f1v11s18 5.000000 +f1v1s8 f0v6s6 39.000000 +f1v1s8 f3v4s29 33.000000 +f1v1s8 f0v2s2 42.000000 +f1v1s8 f3v12s37 34.000000 +f1v1s8 f0v3s3 40.000000 +f1v1s8 f3v14s39 33.000000 +f1v1s8 f0v0s0 41.000000 +f1v1s8 f1v12s19 5.000000 +f1v14s21 f0v1s1 39.000000 +f1v14s21 f1v5s12 7.000000 +f1v14s21 f1v4s11 3.000000 +f1v14s21 f1v9s16 6.000000 +f1v14s21 f1v10s17 7.000000 +f1v14s21 f1v0s7 3.000000 +f1v14s21 f3v9s34 32.000000 +f1v14s21 f4v0s40 33.000000 +f1v14s21 f1v7s14 3.000000 +f1v14s21 f3v2s27 32.000000 +f1v14s21 f4v1s41 33.000000 +f1v14s21 f2v0s22 40.000000 +f1v14s21 f1v2s9 39.000000 +f1v14s21 f1v3s10 3.000000 +f1v14s21 f3v0s25 33.000000 +f1v14s21 f3v3s28 34.000000 +f1v14s21 f3v13s38 35.000000 +f1v14s21 f3v7s32 32.000000 +f1v14s21 f3v1s26 31.000000 +f1v14s21 f3v6s31 32.000000 +f1v14s21 f1v13s20 5.000000 +f1v14s21 f2v2s24 31.000000 +f1v14s21 f3v11s36 36.000000 +f1v14s21 f0v5s5 39.000000 +f1v14s21 f1v11s18 4.000000 +f1v14s21 f0v6s6 37.000000 +f1v14s21 f3v4s29 34.000000 +f1v14s21 f0v2s2 40.000000 +f1v14s21 f3v12s37 34.000000 +f1v14s21 f0v3s3 37.000000 +f1v14s21 f3v14s39 33.000000 +f1v14s21 f0v0s0 39.000000 +f1v14s21 f1v12s19 5.000000 +f0v1s1 f1v5s12 43.000000 +f0v1s1 f1v4s11 41.000000 +f0v1s1 f1v9s16 40.000000 +f0v1s1 f1v10s17 42.000000 +f0v1s1 f1v0s7 41.000000 +f0v1s1 f3v9s34 40.000000 +f0v1s1 f4v0s40 41.000000 +f0v1s1 f1v7s14 41.000000 +f0v1s1 f3v2s27 40.000000 +f0v1s1 f4v1s41 42.000000 +f0v1s1 f2v0s22 4.000000 +f0v1s1 f1v2s9 0.000000 +f0v1s1 f1v3s10 41.000000 +f0v1s1 f3v0s25 41.000000 +f0v1s1 f3v3s28 40.000000 +f0v1s1 f3v13s38 41.000000 +f0v1s1 f3v7s32 40.000000 +f0v1s1 f3v1s26 42.000000 +f0v1s1 f3v6s31 40.000000 +f0v1s1 f1v13s20 40.000000 +f0v1s1 f2v2s24 37.000000 +f0v1s1 f3v11s36 42.000000 +f0v1s1 f0v5s5 4.000000 +f0v1s1 f1v11s18 39.000000 +f0v1s1 f0v6s6 7.000000 +f0v1s1 f3v4s29 38.000000 +f0v1s1 f0v2s2 4.000000 +f0v1s1 f3v12s37 41.000000 +f0v1s1 f0v3s3 6.000000 +f0v1s1 f3v14s39 41.000000 +f0v1s1 f0v0s0 2.000000 +f0v1s1 f1v12s19 45.000000 +f1v5s12 f1v4s11 4.000000 +f1v5s12 f1v9s16 7.000000 +f1v5s12 f1v10s17 7.000000 +f1v5s12 f1v0s7 4.000000 +f1v5s12 f3v9s34 35.000000 +f1v5s12 f4v0s40 37.000000 +f1v5s12 f1v7s14 4.000000 +f1v5s12 f3v2s27 35.000000 +f1v5s12 f4v1s41 38.000000 +f1v5s12 f2v0s22 43.000000 +f1v5s12 f1v2s9 43.000000 +f1v5s12 f1v3s10 4.000000 +f1v5s12 f3v0s25 36.000000 +f1v5s12 f3v3s28 37.000000 +f1v5s12 f3v13s38 38.000000 +f1v5s12 f3v7s32 35.000000 +f1v5s12 f3v1s26 35.000000 +f1v5s12 f3v6s31 35.000000 +f1v5s12 f1v13s20 6.000000 +f1v5s12 f2v2s24 36.000000 +f1v5s12 f3v11s36 35.000000 +f1v5s12 f0v5s5 41.000000 +f1v5s12 f1v11s18 5.000000 +f1v5s12 f0v6s6 43.000000 +f1v5s12 f3v4s29 38.000000 +f1v5s12 f0v2s2 43.000000 +f1v5s12 f3v12s37 35.000000 +f1v5s12 f0v3s3 40.000000 +f1v5s12 f3v14s39 36.000000 +f1v5s12 f0v0s0 43.000000 +f1v5s12 f1v12s19 5.000000 +f1v4s11 f1v9s16 3.000000 +f1v4s11 f1v10s17 4.000000 +f1v4s11 f1v0s7 0.000000 +f1v4s11 f3v9s34 34.000000 +f1v4s11 f4v0s40 37.000000 +f1v4s11 f1v7s14 0.000000 +f1v4s11 f3v2s27 34.000000 +f1v4s11 f4v1s41 34.000000 +f1v4s11 f2v0s22 41.000000 +f1v4s11 f1v2s9 41.000000 +f1v4s11 f1v3s10 0.000000 +f1v4s11 f3v0s25 35.000000 +f1v4s11 f3v3s28 36.000000 +f1v4s11 f3v13s38 39.000000 +f1v4s11 f3v7s32 34.000000 +f1v4s11 f3v1s26 34.000000 +f1v4s11 f3v6s31 34.000000 +f1v4s11 f1v13s20 2.000000 +f1v4s11 f2v2s24 33.000000 +f1v4s11 f3v11s36 36.000000 +f1v4s11 f0v5s5 41.000000 +f1v4s11 f1v11s18 1.000000 +f1v4s11 f0v6s6 42.000000 +f1v4s11 f3v4s29 36.000000 +f1v4s11 f0v2s2 41.000000 +f1v4s11 f3v12s37 35.000000 +f1v4s11 f0v3s3 39.000000 +f1v4s11 f3v14s39 35.000000 +f1v4s11 f0v0s0 41.000000 +f1v4s11 f1v12s19 2.000000 +f1v9s16 f1v10s17 7.000000 +f1v9s16 f1v0s7 3.000000 +f1v9s16 f3v9s34 35.000000 +f1v9s16 f4v0s40 37.000000 +f1v9s16 f1v7s14 3.000000 +f1v9s16 f3v2s27 37.000000 +f1v9s16 f4v1s41 34.000000 +f1v9s16 f2v0s22 41.000000 +f1v9s16 f1v2s9 40.000000 +f1v9s16 f1v3s10 3.000000 +f1v9s16 f3v0s25 38.000000 +f1v9s16 f3v3s28 39.000000 +f1v9s16 f3v13s38 39.000000 +f1v9s16 f3v7s32 35.000000 +f1v9s16 f3v1s26 35.000000 +f1v9s16 f3v6s31 35.000000 +f1v9s16 f1v13s20 5.000000 +f1v9s16 f2v2s24 35.000000 +f1v9s16 f3v11s36 39.000000 +f1v9s16 f0v5s5 40.000000 +f1v9s16 f1v11s18 4.000000 +f1v9s16 f0v6s6 42.000000 +f1v9s16 f3v4s29 38.000000 +f1v9s16 f0v2s2 41.000000 +f1v9s16 f3v12s37 39.000000 +f1v9s16 f0v3s3 38.000000 +f1v9s16 f3v14s39 38.000000 +f1v9s16 f0v0s0 40.000000 +f1v9s16 f1v12s19 5.000000 +f1v10s17 f1v0s7 4.000000 +f1v10s17 f3v9s34 32.000000 +f1v10s17 f4v0s40 39.000000 +f1v10s17 f1v7s14 4.000000 +f1v10s17 f3v2s27 32.000000 +f1v10s17 f4v1s41 40.000000 +f1v10s17 f2v0s22 43.000000 +f1v10s17 f1v2s9 42.000000 +f1v10s17 f1v3s10 4.000000 +f1v10s17 f3v0s25 33.000000 +f1v10s17 f3v3s28 37.000000 +f1v10s17 f3v13s38 38.000000 +f1v10s17 f3v7s32 32.000000 +f1v10s17 f3v1s26 33.000000 +f1v10s17 f3v6s31 32.000000 +f1v10s17 f1v13s20 6.000000 +f1v10s17 f2v2s24 34.000000 +f1v10s17 f3v11s36 35.000000 +f1v10s17 f0v5s5 42.000000 +f1v10s17 f1v11s18 5.000000 +f1v10s17 f0v6s6 45.000000 +f1v10s17 f3v4s29 35.000000 +f1v10s17 f0v2s2 43.000000 +f1v10s17 f3v12s37 32.000000 +f1v10s17 f0v3s3 40.000000 +f1v10s17 f3v14s39 33.000000 +f1v10s17 f0v0s0 42.000000 +f1v10s17 f1v12s19 5.000000 +f1v0s7 f3v9s34 34.000000 +f1v0s7 f4v0s40 37.000000 +f1v0s7 f1v7s14 0.000000 +f1v0s7 f3v2s27 34.000000 +f1v0s7 f4v1s41 34.000000 +f1v0s7 f2v0s22 41.000000 +f1v0s7 f1v2s9 41.000000 +f1v0s7 f1v3s10 0.000000 +f1v0s7 f3v0s25 35.000000 +f1v0s7 f3v3s28 36.000000 +f1v0s7 f3v13s38 39.000000 +f1v0s7 f3v7s32 34.000000 +f1v0s7 f3v1s26 34.000000 +f1v0s7 f3v6s31 34.000000 +f1v0s7 f1v13s20 2.000000 +f1v0s7 f2v2s24 33.000000 +f1v0s7 f3v11s36 36.000000 +f1v0s7 f0v5s5 41.000000 +f1v0s7 f1v11s18 1.000000 +f1v0s7 f0v6s6 42.000000 +f1v0s7 f3v4s29 36.000000 +f1v0s7 f0v2s2 41.000000 +f1v0s7 f3v12s37 35.000000 +f1v0s7 f0v3s3 39.000000 +f1v0s7 f3v14s39 35.000000 +f1v0s7 f0v0s0 41.000000 +f1v0s7 f1v12s19 2.000000 +f3v9s34 f4v0s40 39.000000 +f3v9s34 f1v7s14 34.000000 +f3v9s34 f3v2s27 3.000000 +f3v9s34 f4v1s41 38.000000 +f3v9s34 f2v0s22 38.000000 +f3v9s34 f1v2s9 40.000000 +f3v9s34 f1v3s10 34.000000 +f3v9s34 f3v0s25 2.000000 +f3v9s34 f3v3s28 4.000000 +f3v9s34 f3v13s38 4.000000 +f3v9s34 f3v7s32 0.000000 +f3v9s34 f3v1s26 4.000000 +f3v9s34 f3v6s31 0.000000 +f3v9s34 f1v13s20 34.000000 +f3v9s34 f2v2s24 33.000000 +f3v9s34 f3v11s36 6.000000 +f3v9s34 f0v5s5 38.000000 +f3v9s34 f1v11s18 34.000000 +f3v9s34 f0v6s6 41.000000 +f3v9s34 f3v4s29 5.000000 +f3v9s34 f0v2s2 38.000000 +f3v9s34 f3v12s37 4.000000 +f3v9s34 f0v3s3 39.000000 +f3v9s34 f3v14s39 2.000000 +f3v9s34 f0v0s0 38.000000 +f3v9s34 f1v12s19 34.000000 +f4v0s40 f1v7s14 37.000000 +f4v0s40 f3v2s27 37.000000 +f4v0s40 f4v1s41 1.000000 +f4v0s40 f2v0s22 36.000000 +f4v0s40 f1v2s9 41.000000 +f4v0s40 f1v3s10 37.000000 +f4v0s40 f3v0s25 38.000000 +f4v0s40 f3v3s28 38.000000 +f4v0s40 f3v13s38 41.000000 +f4v0s40 f3v7s32 39.000000 +f4v0s40 f3v1s26 37.000000 +f4v0s40 f3v6s31 39.000000 +f4v0s40 f1v13s20 35.000000 +f4v0s40 f2v2s24 32.000000 +f4v0s40 f3v11s36 37.000000 +f4v0s40 f0v5s5 37.000000 +f4v0s40 f1v11s18 39.000000 +f4v0s40 f0v6s6 36.000000 +f4v0s40 f3v4s29 42.000000 +f4v0s40 f0v2s2 36.000000 +f4v0s40 f3v12s37 39.000000 +f4v0s40 f0v3s3 36.000000 +f4v0s40 f3v14s39 38.000000 +f4v0s40 f0v0s0 36.000000 +f4v0s40 f1v12s19 34.000000 +f1v7s14 f3v2s27 34.000000 +f1v7s14 f4v1s41 34.000000 +f1v7s14 f2v0s22 41.000000 +f1v7s14 f1v2s9 41.000000 +f1v7s14 f1v3s10 0.000000 +f1v7s14 f3v0s25 35.000000 +f1v7s14 f3v3s28 36.000000 +f1v7s14 f3v13s38 39.000000 +f1v7s14 f3v7s32 34.000000 +f1v7s14 f3v1s26 34.000000 +f1v7s14 f3v6s31 34.000000 +f1v7s14 f1v13s20 2.000000 +f1v7s14 f2v2s24 33.000000 +f1v7s14 f3v11s36 36.000000 +f1v7s14 f0v5s5 41.000000 +f1v7s14 f1v11s18 1.000000 +f1v7s14 f0v6s6 42.000000 +f1v7s14 f3v4s29 36.000000 +f1v7s14 f0v2s2 41.000000 +f1v7s14 f3v12s37 35.000000 +f1v7s14 f0v3s3 39.000000 +f1v7s14 f3v14s39 35.000000 +f1v7s14 f0v0s0 41.000000 +f1v7s14 f1v12s19 2.000000 +f3v2s27 f4v1s41 38.000000 +f3v2s27 f2v0s22 37.000000 +f3v2s27 f1v2s9 40.000000 +f3v2s27 f1v3s10 34.000000 +f3v2s27 f3v0s25 1.000000 +f3v2s27 f3v3s28 3.000000 +f3v2s27 f3v13s38 3.000000 +f3v2s27 f3v7s32 3.000000 +f3v2s27 f3v1s26 2.000000 +f3v2s27 f3v6s31 3.000000 +f3v2s27 f1v13s20 34.000000 +f3v2s27 f2v2s24 34.000000 +f3v2s27 f3v11s36 5.000000 +f3v2s27 f0v5s5 38.000000 +f3v2s27 f1v11s18 34.000000 +f3v2s27 f0v6s6 42.000000 +f3v2s27 f3v4s29 4.000000 +f3v2s27 f0v2s2 37.000000 +f3v2s27 f3v12s37 3.000000 +f3v2s27 f0v3s3 39.000000 +f3v2s27 f3v14s39 1.000000 +f3v2s27 f0v0s0 38.000000 +f3v2s27 f1v12s19 34.000000 +f4v1s41 f2v0s22 37.000000 +f4v1s41 f1v2s9 42.000000 +f4v1s41 f1v3s10 34.000000 +f4v1s41 f3v0s25 39.000000 +f4v1s41 f3v3s28 39.000000 +f4v1s41 f3v13s38 42.000000 +f4v1s41 f3v7s32 38.000000 +f4v1s41 f3v1s26 38.000000 +f4v1s41 f3v6s31 38.000000 +f4v1s41 f1v13s20 33.000000 +f4v1s41 f2v2s24 33.000000 +f4v1s41 f3v11s36 37.000000 +f4v1s41 f0v5s5 38.000000 +f4v1s41 f1v11s18 36.000000 +f4v1s41 f0v6s6 37.000000 +f4v1s41 f3v4s29 43.000000 +f4v1s41 f0v2s2 37.000000 +f4v1s41 f3v12s37 40.000000 +f4v1s41 f0v3s3 37.000000 +f4v1s41 f3v14s39 39.000000 +f4v1s41 f0v0s0 37.000000 +f4v1s41 f1v12s19 34.000000 +f2v0s22 f1v2s9 4.000000 +f2v0s22 f1v3s10 41.000000 +f2v0s22 f3v0s25 38.000000 +f2v0s22 f3v3s28 37.000000 +f2v0s22 f3v13s38 39.000000 +f2v0s22 f3v7s32 38.000000 +f2v0s22 f3v1s26 39.000000 +f2v0s22 f3v6s31 38.000000 +f2v0s22 f1v13s20 40.000000 +f2v0s22 f2v2s24 37.000000 +f2v0s22 f3v11s36 40.000000 +f2v0s22 f0v5s5 4.000000 +f2v0s22 f1v11s18 39.000000 +f2v0s22 f0v6s6 7.000000 +f2v0s22 f3v4s29 35.000000 +f2v0s22 f0v2s2 0.000000 +f2v0s22 f3v12s37 38.000000 +f2v0s22 f0v3s3 6.000000 +f2v0s22 f3v14s39 38.000000 +f2v0s22 f0v0s0 2.000000 +f2v0s22 f1v12s19 41.000000 +f1v2s9 f1v3s10 41.000000 +f1v2s9 f3v0s25 41.000000 +f1v2s9 f3v3s28 40.000000 +f1v2s9 f3v13s38 41.000000 +f1v2s9 f3v7s32 40.000000 +f1v2s9 f3v1s26 42.000000 +f1v2s9 f3v6s31 40.000000 +f1v2s9 f1v13s20 40.000000 +f1v2s9 f2v2s24 37.000000 +f1v2s9 f3v11s36 42.000000 +f1v2s9 f0v5s5 4.000000 +f1v2s9 f1v11s18 39.000000 +f1v2s9 f0v6s6 7.000000 +f1v2s9 f3v4s29 38.000000 +f1v2s9 f0v2s2 4.000000 +f1v2s9 f3v12s37 41.000000 +f1v2s9 f0v3s3 6.000000 +f1v2s9 f3v14s39 41.000000 +f1v2s9 f0v0s0 2.000000 +f1v2s9 f1v12s19 45.000000 +f1v3s10 f3v0s25 35.000000 +f1v3s10 f3v3s28 36.000000 +f1v3s10 f3v13s38 39.000000 +f1v3s10 f3v7s32 34.000000 +f1v3s10 f3v1s26 34.000000 +f1v3s10 f3v6s31 34.000000 +f1v3s10 f1v13s20 2.000000 +f1v3s10 f2v2s24 33.000000 +f1v3s10 f3v11s36 36.000000 +f1v3s10 f0v5s5 41.000000 +f1v3s10 f1v11s18 1.000000 +f1v3s10 f0v6s6 42.000000 +f1v3s10 f3v4s29 36.000000 +f1v3s10 f0v2s2 41.000000 +f1v3s10 f3v12s37 35.000000 +f1v3s10 f0v3s3 39.000000 +f1v3s10 f3v14s39 35.000000 +f1v3s10 f0v0s0 41.000000 +f1v3s10 f1v12s19 2.000000 +f3v0s25 f3v3s28 2.000000 +f3v0s25 f3v13s38 2.000000 +f3v0s25 f3v7s32 2.000000 +f3v0s25 f3v1s26 2.000000 +f3v0s25 f3v6s31 2.000000 +f3v0s25 f1v13s20 35.000000 +f3v0s25 f2v2s24 33.000000 +f3v0s25 f3v11s36 4.000000 +f3v0s25 f0v5s5 39.000000 +f3v0s25 f1v11s18 35.000000 +f3v0s25 f0v6s6 43.000000 +f3v0s25 f3v4s29 3.000000 +f3v0s25 f0v2s2 38.000000 +f3v0s25 f3v12s37 2.000000 +f3v0s25 f0v3s3 40.000000 +f3v0s25 f3v14s39 0.000000 +f3v0s25 f0v0s0 39.000000 +f3v0s25 f1v12s19 35.000000 +f3v3s28 f3v13s38 3.000000 +f3v3s28 f3v7s32 4.000000 +f3v3s28 f3v1s26 4.000000 +f3v3s28 f3v6s31 4.000000 +f3v3s28 f1v13s20 36.000000 +f3v3s28 f2v2s24 33.000000 +f3v3s28 f3v11s36 6.000000 +f3v3s28 f0v5s5 39.000000 +f3v3s28 f1v11s18 38.000000 +f3v3s28 f0v6s6 42.000000 +f3v3s28 f3v4s29 4.000000 +f3v3s28 f0v2s2 37.000000 +f3v3s28 f3v12s37 4.000000 +f3v3s28 f0v3s3 39.000000 +f3v3s28 f3v14s39 2.000000 +f3v3s28 f0v0s0 38.000000 +f3v3s28 f1v12s19 38.000000 +f3v13s38 f3v7s32 4.000000 +f3v13s38 f3v1s26 4.000000 +f3v13s38 f3v6s31 4.000000 +f3v13s38 f1v13s20 39.000000 +f3v13s38 f2v2s24 34.000000 +f3v13s38 f3v11s36 6.000000 +f3v13s38 f0v5s5 39.000000 +f3v13s38 f1v11s18 39.000000 +f3v13s38 f0v6s6 43.000000 +f3v13s38 f3v4s29 4.000000 +f3v13s38 f0v2s2 39.000000 +f3v13s38 f3v12s37 4.000000 +f3v13s38 f0v3s3 40.000000 +f3v13s38 f3v14s39 2.000000 +f3v13s38 f0v0s0 39.000000 +f3v13s38 f1v12s19 35.000000 +f3v7s32 f3v1s26 4.000000 +f3v7s32 f3v6s31 0.000000 +f3v7s32 f1v13s20 34.000000 +f3v7s32 f2v2s24 33.000000 +f3v7s32 f3v11s36 6.000000 +f3v7s32 f0v5s5 38.000000 +f3v7s32 f1v11s18 34.000000 +f3v7s32 f0v6s6 41.000000 +f3v7s32 f3v4s29 5.000000 +f3v7s32 f0v2s2 38.000000 +f3v7s32 f3v12s37 4.000000 +f3v7s32 f0v3s3 39.000000 +f3v7s32 f3v14s39 2.000000 +f3v7s32 f0v0s0 38.000000 +f3v7s32 f1v12s19 34.000000 +f3v1s26 f3v6s31 4.000000 +f3v1s26 f1v13s20 34.000000 +f3v1s26 f2v2s24 33.000000 +f3v1s26 f3v11s36 6.000000 +f3v1s26 f0v5s5 40.000000 +f3v1s26 f1v11s18 37.000000 +f3v1s26 f0v6s6 44.000000 +f3v1s26 f3v4s29 5.000000 +f3v1s26 f0v2s2 39.000000 +f3v1s26 f3v12s37 4.000000 +f3v1s26 f0v3s3 39.000000 +f3v1s26 f3v14s39 2.000000 +f3v1s26 f0v0s0 40.000000 +f3v1s26 f1v12s19 34.000000 +f3v6s31 f1v13s20 34.000000 +f3v6s31 f2v2s24 33.000000 +f3v6s31 f3v11s36 6.000000 +f3v6s31 f0v5s5 38.000000 +f3v6s31 f1v11s18 34.000000 +f3v6s31 f0v6s6 41.000000 +f3v6s31 f3v4s29 5.000000 +f3v6s31 f0v2s2 38.000000 +f3v6s31 f3v12s37 4.000000 +f3v6s31 f0v3s3 39.000000 +f3v6s31 f3v14s39 2.000000 +f3v6s31 f0v0s0 38.000000 +f3v6s31 f1v12s19 34.000000 +f1v13s20 f2v2s24 31.000000 +f1v13s20 f3v11s36 36.000000 +f1v13s20 f0v5s5 39.000000 +f1v13s20 f1v11s18 3.000000 +f1v13s20 f0v6s6 41.000000 +f1v13s20 f3v4s29 36.000000 +f1v13s20 f0v2s2 40.000000 +f1v13s20 f3v12s37 35.000000 +f1v13s20 f0v3s3 38.000000 +f1v13s20 f3v14s39 35.000000 +f1v13s20 f0v0s0 40.000000 +f1v13s20 f1v12s19 4.000000 +f2v2s24 f3v11s36 33.000000 +f2v2s24 f0v5s5 41.000000 +f2v2s24 f1v11s18 33.000000 +f2v2s24 f0v6s6 38.000000 +f2v2s24 f3v4s29 34.000000 +f2v2s24 f0v2s2 37.000000 +f2v2s24 f3v12s37 33.000000 +f2v2s24 f0v3s3 35.000000 +f2v2s24 f3v14s39 33.000000 +f2v2s24 f0v0s0 37.000000 +f2v2s24 f1v12s19 33.000000 +f3v11s36 f0v5s5 43.000000 +f3v11s36 f1v11s18 36.000000 +f3v11s36 f0v6s6 44.000000 +f3v11s36 f3v4s29 7.000000 +f3v11s36 f0v2s2 40.000000 +f3v11s36 f3v12s37 6.000000 +f3v11s36 f0v3s3 41.000000 +f3v11s36 f3v14s39 4.000000 +f3v11s36 f0v0s0 40.000000 +f3v11s36 f1v12s19 36.000000 +f0v5s5 f1v11s18 40.000000 +f0v5s5 f0v6s6 7.000000 +f0v5s5 f3v4s29 36.000000 +f0v5s5 f0v2s2 4.000000 +f0v5s5 f3v12s37 40.000000 +f0v5s5 f0v3s3 6.000000 +f0v5s5 f3v14s39 39.000000 +f0v5s5 f0v0s0 2.000000 +f0v5s5 f1v12s19 42.000000 +f1v11s18 f0v6s6 39.000000 +f1v11s18 f3v4s29 36.000000 +f1v11s18 f0v2s2 39.000000 +f1v11s18 f3v12s37 35.000000 +f1v11s18 f0v3s3 37.000000 +f1v11s18 f3v14s39 35.000000 +f1v11s18 f0v0s0 39.000000 +f1v11s18 f1v12s19 3.000000 +f0v6s6 f3v4s29 40.000000 +f0v6s6 f0v2s2 7.000000 +f0v6s6 f3v12s37 44.000000 +f0v6s6 f0v3s3 9.000000 +f0v6s6 f3v14s39 43.000000 +f0v6s6 f0v0s0 5.000000 +f0v6s6 f1v12s19 45.000000 +f3v4s29 f0v2s2 35.000000 +f3v4s29 f3v12s37 5.000000 +f3v4s29 f0v3s3 38.000000 +f3v4s29 f3v14s39 3.000000 +f3v4s29 f0v0s0 36.000000 +f3v4s29 f1v12s19 36.000000 +f0v2s2 f3v12s37 38.000000 +f0v2s2 f0v3s3 6.000000 +f0v2s2 f3v14s39 38.000000 +f0v2s2 f0v0s0 2.000000 +f0v2s2 f1v12s19 41.000000 +f3v12s37 f0v3s3 41.000000 +f3v12s37 f3v14s39 2.000000 +f3v12s37 f0v0s0 39.000000 +f3v12s37 f1v12s19 35.000000 +f0v3s3 f3v14s39 40.000000 +f0v3s3 f0v0s0 4.000000 +f0v3s3 f1v12s19 40.000000 +f3v14s39 f0v0s0 39.000000 +f3v14s39 f1v12s19 35.000000 +f0v0s0 f1v12s19 45.000000 diff --git a/pkg/obitools/obialign/testdata/one_allraw.expected b/pkg/obitools/obialign/testdata/one_allraw.expected new file mode 100644 index 0000000..56b344d --- /dev/null +++ b/pkg/obitools/obialign/testdata/one_allraw.expected @@ -0,0 +1,903 @@ +f3v10s35 f3v5s30 58.000000 +f3v10s35 f1v8s15 37.000000 +f3v10s35 f2v1s23 38.000000 +f3v10s35 f4v2s42 38.000000 +f3v10s35 f0v4s4 40.000000 +f3v10s35 f1v6s13 37.000000 +f3v10s35 f3v8s33 61.000000 +f3v10s35 f1v1s8 39.000000 +f3v10s35 f1v14s21 38.000000 +f3v10s35 f0v1s1 40.000000 +f3v10s35 f1v5s12 38.000000 +f3v10s35 f1v4s11 38.000000 +f3v10s35 f1v9s16 39.000000 +f3v10s35 f1v10s17 39.000000 +f3v10s35 f1v0s7 38.000000 +f3v10s35 f3v9s34 61.000000 +f3v10s35 f4v0s40 38.000000 +f3v10s35 f1v7s14 38.000000 +f3v10s35 f3v2s27 61.000000 +f3v10s35 f4v1s41 38.000000 +f3v10s35 f2v0s22 40.000000 +f3v10s35 f1v2s9 40.000000 +f3v10s35 f1v3s10 38.000000 +f3v10s35 f3v0s25 62.000000 +f3v10s35 f3v3s28 61.000000 +f3v10s35 f3v13s38 62.000000 +f3v10s35 f3v7s32 61.000000 +f3v10s35 f3v1s26 61.000000 +f3v10s35 f3v6s31 61.000000 +f3v10s35 f1v13s20 37.000000 +f3v10s35 f2v2s24 35.000000 +f3v10s35 f3v11s36 59.000000 +f3v10s35 f0v5s5 39.000000 +f3v10s35 f1v11s18 38.000000 +f3v10s35 f0v6s6 38.000000 +f3v10s35 f3v4s29 61.000000 +f3v10s35 f0v2s2 40.000000 +f3v10s35 f3v12s37 61.000000 +f3v10s35 f0v3s3 40.000000 +f3v10s35 f3v14s39 62.000000 +f3v10s35 f0v0s0 40.000000 +f3v10s35 f1v12s19 37.000000 +f3v5s30 f1v8s15 38.000000 +f3v5s30 f2v1s23 38.000000 +f3v5s30 f4v2s42 37.000000 +f3v5s30 f0v4s4 39.000000 +f3v5s30 f1v6s13 39.000000 +f3v5s30 f3v8s33 61.000000 +f3v5s30 f1v1s8 39.000000 +f3v5s30 f1v14s21 37.000000 +f3v5s30 f0v1s1 39.000000 +f3v5s30 f1v5s12 38.000000 +f3v5s30 f1v4s11 38.000000 +f3v5s30 f1v9s16 38.000000 +f3v5s30 f1v10s17 39.000000 +f3v5s30 f1v0s7 38.000000 +f3v5s30 f3v9s34 60.000000 +f3v5s30 f4v0s40 37.000000 +f3v5s30 f1v7s14 38.000000 +f3v5s30 f3v2s27 61.000000 +f3v5s30 f4v1s41 37.000000 +f3v5s30 f2v0s22 40.000000 +f3v5s30 f1v2s9 39.000000 +f3v5s30 f1v3s10 38.000000 +f3v5s30 f3v0s25 62.000000 +f3v5s30 f3v3s28 61.000000 +f3v5s30 f3v13s38 62.000000 +f3v5s30 f3v7s32 60.000000 +f3v5s30 f3v1s26 61.000000 +f3v5s30 f3v6s31 60.000000 +f3v5s30 f1v13s20 38.000000 +f3v5s30 f2v2s24 34.000000 +f3v5s30 f3v11s36 59.000000 +f3v5s30 f0v5s5 39.000000 +f3v5s30 f1v11s18 38.000000 +f3v5s30 f0v6s6 41.000000 +f3v5s30 f3v4s29 61.000000 +f3v5s30 f0v2s2 40.000000 +f3v5s30 f3v12s37 61.000000 +f3v5s30 f0v3s3 40.000000 +f3v5s30 f3v14s39 62.000000 +f3v5s30 f0v0s0 40.000000 +f3v5s30 f1v12s19 37.000000 +f1v8s15 f2v1s23 57.000000 +f1v8s15 f4v2s42 34.000000 +f1v8s15 f0v4s4 38.000000 +f1v8s15 f1v6s13 54.000000 +f1v8s15 f3v8s33 39.000000 +f1v8s15 f1v1s8 55.000000 +f1v8s15 f1v14s21 54.000000 +f1v8s15 f0v1s1 38.000000 +f1v8s15 f1v5s12 55.000000 +f1v8s15 f1v4s11 57.000000 +f1v8s15 f1v9s16 56.000000 +f1v8s15 f1v10s17 55.000000 +f1v8s15 f1v0s7 57.000000 +f1v8s15 f3v9s34 40.000000 +f1v8s15 f4v0s40 35.000000 +f1v8s15 f1v7s14 57.000000 +f1v8s15 f3v2s27 39.000000 +f1v8s15 f4v1s41 35.000000 +f1v8s15 f2v0s22 39.000000 +f1v8s15 f1v2s9 38.000000 +f1v8s15 f1v3s10 57.000000 +f1v8s15 f3v0s25 39.000000 +f1v8s15 f3v3s28 39.000000 +f1v8s15 f3v13s38 40.000000 +f1v8s15 f3v7s32 40.000000 +f1v8s15 f3v1s26 39.000000 +f1v8s15 f3v6s31 40.000000 +f1v8s15 f1v13s20 55.000000 +f1v8s15 f2v2s24 34.000000 +f1v8s15 f3v11s36 39.000000 +f1v8s15 f0v5s5 39.000000 +f1v8s15 f1v11s18 56.000000 +f1v8s15 f0v6s6 38.000000 +f1v8s15 f3v4s29 40.000000 +f1v8s15 f0v2s2 39.000000 +f1v8s15 f3v12s37 40.000000 +f1v8s15 f0v3s3 39.000000 +f1v8s15 f3v14s39 39.000000 +f1v8s15 f0v0s0 39.000000 +f1v8s15 f1v12s19 55.000000 +f2v1s23 f4v2s42 34.000000 +f2v1s23 f0v4s4 38.000000 +f2v1s23 f1v6s13 54.000000 +f2v1s23 f3v8s33 39.000000 +f2v1s23 f1v1s8 56.000000 +f2v1s23 f1v14s21 55.000000 +f2v1s23 f0v1s1 38.000000 +f2v1s23 f1v5s12 56.000000 +f2v1s23 f1v4s11 58.000000 +f2v1s23 f1v9s16 57.000000 +f2v1s23 f1v10s17 56.000000 +f2v1s23 f1v0s7 58.000000 +f2v1s23 f3v9s34 40.000000 +f2v1s23 f4v0s40 35.000000 +f2v1s23 f1v7s14 58.000000 +f2v1s23 f3v2s27 39.000000 +f2v1s23 f4v1s41 35.000000 +f2v1s23 f2v0s22 39.000000 +f2v1s23 f1v2s9 38.000000 +f2v1s23 f1v3s10 58.000000 +f2v1s23 f3v0s25 39.000000 +f2v1s23 f3v3s28 39.000000 +f2v1s23 f3v13s38 40.000000 +f2v1s23 f3v7s32 40.000000 +f2v1s23 f3v1s26 39.000000 +f2v1s23 f3v6s31 40.000000 +f2v1s23 f1v13s20 56.000000 +f2v1s23 f2v2s24 34.000000 +f2v1s23 f3v11s36 39.000000 +f2v1s23 f0v5s5 39.000000 +f2v1s23 f1v11s18 57.000000 +f2v1s23 f0v6s6 38.000000 +f2v1s23 f3v4s29 40.000000 +f2v1s23 f0v2s2 39.000000 +f2v1s23 f3v12s37 40.000000 +f2v1s23 f0v3s3 39.000000 +f2v1s23 f3v14s39 39.000000 +f2v1s23 f0v0s0 39.000000 +f2v1s23 f1v12s19 56.000000 +f4v2s42 f0v4s4 39.000000 +f4v2s42 f1v6s13 33.000000 +f4v2s42 f3v8s33 37.000000 +f4v2s42 f1v1s8 36.000000 +f4v2s42 f1v14s21 33.000000 +f4v2s42 f0v1s1 39.000000 +f4v2s42 f1v5s12 34.000000 +f4v2s42 f1v4s11 34.000000 +f4v2s42 f1v9s16 36.000000 +f4v2s42 f1v10s17 35.000000 +f4v2s42 f1v0s7 34.000000 +f4v2s42 f3v9s34 35.000000 +f4v2s42 f4v0s40 59.000000 +f4v2s42 f1v7s14 34.000000 +f4v2s42 f3v2s27 36.000000 +f4v2s42 f4v1s41 59.000000 +f4v2s42 f2v0s22 41.000000 +f4v2s42 f1v2s9 39.000000 +f4v2s42 f1v3s10 34.000000 +f4v2s42 f3v0s25 36.000000 +f4v2s42 f3v3s28 36.000000 +f4v2s42 f3v13s38 37.000000 +f4v2s42 f3v7s32 35.000000 +f4v2s42 f3v1s26 36.000000 +f4v2s42 f3v6s31 35.000000 +f4v2s42 f1v13s20 33.000000 +f4v2s42 f2v2s24 35.000000 +f4v2s42 f3v11s36 36.000000 +f4v2s42 f0v5s5 40.000000 +f4v2s42 f1v11s18 34.000000 +f4v2s42 f0v6s6 41.000000 +f4v2s42 f3v4s29 38.000000 +f4v2s42 f0v2s2 41.000000 +f4v2s42 f3v12s37 37.000000 +f4v2s42 f0v3s3 39.000000 +f4v2s42 f3v14s39 36.000000 +f4v2s42 f0v0s0 40.000000 +f4v2s42 f1v12s19 33.000000 +f0v4s4 f1v6s13 37.000000 +f0v4s4 f3v8s33 41.000000 +f0v4s4 f1v1s8 38.000000 +f0v4s4 f1v14s21 38.000000 +f0v4s4 f0v1s1 69.000000 +f0v4s4 f1v5s12 39.000000 +f0v4s4 f1v4s11 38.000000 +f0v4s4 f1v9s16 39.000000 +f0v4s4 f1v10s17 39.000000 +f0v4s4 f1v0s7 38.000000 +f0v4s4 f3v9s34 40.000000 +f0v4s4 f4v0s40 38.000000 +f0v4s4 f1v7s14 38.000000 +f0v4s4 f3v2s27 40.000000 +f0v4s4 f4v1s41 38.000000 +f0v4s4 f2v0s22 67.000000 +f0v4s4 f1v2s9 69.000000 +f0v4s4 f1v3s10 38.000000 +f0v4s4 f3v0s25 40.000000 +f0v4s4 f3v3s28 40.000000 +f0v4s4 f3v13s38 40.000000 +f0v4s4 f3v7s32 40.000000 +f0v4s4 f3v1s26 39.000000 +f0v4s4 f3v6s31 40.000000 +f0v4s4 f1v13s20 37.000000 +f0v4s4 f2v2s24 35.000000 +f0v4s4 f3v11s36 37.000000 +f0v4s4 f0v5s5 67.000000 +f0v4s4 f1v11s18 38.000000 +f0v4s4 f0v6s6 64.000000 +f0v4s4 f3v4s29 42.000000 +f0v4s4 f0v2s2 67.000000 +f0v4s4 f3v12s37 40.000000 +f0v4s4 f0v3s3 65.000000 +f0v4s4 f3v14s39 40.000000 +f0v4s4 f0v0s0 68.000000 +f0v4s4 f1v12s19 38.000000 +f1v6s13 f3v8s33 39.000000 +f1v6s13 f1v1s8 52.000000 +f1v6s13 f1v14s21 51.000000 +f1v6s13 f0v1s1 37.000000 +f1v6s13 f1v5s12 52.000000 +f1v6s13 f1v4s11 54.000000 +f1v6s13 f1v9s16 53.000000 +f1v6s13 f1v10s17 53.000000 +f1v6s13 f1v0s7 54.000000 +f1v6s13 f3v9s34 39.000000 +f1v6s13 f4v0s40 34.000000 +f1v6s13 f1v7s14 54.000000 +f1v6s13 f3v2s27 39.000000 +f1v6s13 f4v1s41 34.000000 +f1v6s13 f2v0s22 38.000000 +f1v6s13 f1v2s9 37.000000 +f1v6s13 f1v3s10 54.000000 +f1v6s13 f3v0s25 39.000000 +f1v6s13 f3v3s28 39.000000 +f1v6s13 f3v13s38 39.000000 +f1v6s13 f3v7s32 39.000000 +f1v6s13 f3v1s26 39.000000 +f1v6s13 f3v6s31 39.000000 +f1v6s13 f1v13s20 52.000000 +f1v6s13 f2v2s24 33.000000 +f1v6s13 f3v11s36 39.000000 +f1v6s13 f0v5s5 38.000000 +f1v6s13 f1v11s18 53.000000 +f1v6s13 f0v6s6 38.000000 +f1v6s13 f3v4s29 40.000000 +f1v6s13 f0v2s2 38.000000 +f1v6s13 f3v12s37 40.000000 +f1v6s13 f0v3s3 37.000000 +f1v6s13 f3v14s39 39.000000 +f1v6s13 f0v0s0 38.000000 +f1v6s13 f1v12s19 52.000000 +f3v8s33 f1v1s8 40.000000 +f3v8s33 f1v14s21 38.000000 +f3v8s33 f0v1s1 41.000000 +f3v8s33 f1v5s12 39.000000 +f3v8s33 f1v4s11 39.000000 +f3v8s33 f1v9s16 39.000000 +f3v8s33 f1v10s17 40.000000 +f3v8s33 f1v0s7 39.000000 +f3v8s33 f3v9s34 63.000000 +f3v8s33 f4v0s40 37.000000 +f3v8s33 f1v7s14 39.000000 +f3v8s33 f3v2s27 64.000000 +f3v8s33 f4v1s41 37.000000 +f3v8s33 f2v0s22 42.000000 +f3v8s33 f1v2s9 41.000000 +f3v8s33 f1v3s10 39.000000 +f3v8s33 f3v0s25 65.000000 +f3v8s33 f3v3s28 64.000000 +f3v8s33 f3v13s38 65.000000 +f3v8s33 f3v7s32 63.000000 +f3v8s33 f3v1s26 63.000000 +f3v8s33 f3v6s31 63.000000 +f3v8s33 f1v13s20 38.000000 +f3v8s33 f2v2s24 34.000000 +f3v8s33 f3v11s36 62.000000 +f3v8s33 f0v5s5 41.000000 +f3v8s33 f1v11s18 39.000000 +f3v8s33 f0v6s6 41.000000 +f3v8s33 f3v4s29 64.000000 +f3v8s33 f0v2s2 42.000000 +f3v8s33 f3v12s37 64.000000 +f3v8s33 f0v3s3 41.000000 +f3v8s33 f3v14s39 65.000000 +f3v8s33 f0v0s0 42.000000 +f3v8s33 f1v12s19 38.000000 +f1v1s8 f1v14s21 55.000000 +f1v1s8 f0v1s1 38.000000 +f1v1s8 f1v5s12 54.000000 +f1v1s8 f1v4s11 56.000000 +f1v1s8 f1v9s16 55.000000 +f1v1s8 f1v10s17 54.000000 +f1v1s8 f1v0s7 56.000000 +f1v1s8 f3v9s34 41.000000 +f1v1s8 f4v0s40 36.000000 +f1v1s8 f1v7s14 56.000000 +f1v1s8 f3v2s27 40.000000 +f1v1s8 f4v1s41 36.000000 +f1v1s8 f2v0s22 39.000000 +f1v1s8 f1v2s9 38.000000 +f1v1s8 f1v3s10 56.000000 +f1v1s8 f3v0s25 40.000000 +f1v1s8 f3v3s28 40.000000 +f1v1s8 f3v13s38 41.000000 +f1v1s8 f3v7s32 41.000000 +f1v1s8 f3v1s26 40.000000 +f1v1s8 f3v6s31 41.000000 +f1v1s8 f1v13s20 54.000000 +f1v1s8 f2v2s24 33.000000 +f1v1s8 f3v11s36 39.000000 +f1v1s8 f0v5s5 39.000000 +f1v1s8 f1v11s18 55.000000 +f1v1s8 f0v6s6 38.000000 +f1v1s8 f3v4s29 41.000000 +f1v1s8 f0v2s2 39.000000 +f1v1s8 f3v12s37 41.000000 +f1v1s8 f0v3s3 39.000000 +f1v1s8 f3v14s39 40.000000 +f1v1s8 f0v0s0 39.000000 +f1v1s8 f1v12s19 55.000000 +f1v14s21 f0v1s1 38.000000 +f1v14s21 f1v5s12 53.000000 +f1v14s21 f1v4s11 55.000000 +f1v14s21 f1v9s16 54.000000 +f1v14s21 f1v10s17 53.000000 +f1v14s21 f1v0s7 55.000000 +f1v14s21 f3v9s34 39.000000 +f1v14s21 f4v0s40 33.000000 +f1v14s21 f1v7s14 55.000000 +f1v14s21 f3v2s27 38.000000 +f1v14s21 f4v1s41 34.000000 +f1v14s21 f2v0s22 39.000000 +f1v14s21 f1v2s9 38.000000 +f1v14s21 f1v3s10 55.000000 +f1v14s21 f3v0s25 38.000000 +f1v14s21 f3v3s28 38.000000 +f1v14s21 f3v13s38 38.000000 +f1v14s21 f3v7s32 39.000000 +f1v14s21 f3v1s26 38.000000 +f1v14s21 f3v6s31 39.000000 +f1v14s21 f1v13s20 53.000000 +f1v14s21 f2v2s24 34.000000 +f1v14s21 f3v11s36 38.000000 +f1v14s21 f0v5s5 39.000000 +f1v14s21 f1v11s18 54.000000 +f1v14s21 f0v6s6 38.000000 +f1v14s21 f3v4s29 39.000000 +f1v14s21 f0v2s2 39.000000 +f1v14s21 f3v12s37 39.000000 +f1v14s21 f0v3s3 39.000000 +f1v14s21 f3v14s39 38.000000 +f1v14s21 f0v0s0 39.000000 +f1v14s21 f1v12s19 53.000000 +f0v1s1 f1v5s12 39.000000 +f0v1s1 f1v4s11 38.000000 +f0v1s1 f1v9s16 39.000000 +f0v1s1 f1v10s17 39.000000 +f0v1s1 f1v0s7 38.000000 +f0v1s1 f3v9s34 40.000000 +f0v1s1 f4v0s40 38.000000 +f0v1s1 f1v7s14 38.000000 +f0v1s1 f3v2s27 40.000000 +f0v1s1 f4v1s41 38.000000 +f0v1s1 f2v0s22 67.000000 +f0v1s1 f1v2s9 69.000000 +f0v1s1 f1v3s10 38.000000 +f0v1s1 f3v0s25 40.000000 +f0v1s1 f3v3s28 40.000000 +f0v1s1 f3v13s38 40.000000 +f0v1s1 f3v7s32 40.000000 +f0v1s1 f3v1s26 39.000000 +f0v1s1 f3v6s31 40.000000 +f0v1s1 f1v13s20 37.000000 +f0v1s1 f2v2s24 35.000000 +f0v1s1 f3v11s36 37.000000 +f0v1s1 f0v5s5 67.000000 +f0v1s1 f1v11s18 38.000000 +f0v1s1 f0v6s6 64.000000 +f0v1s1 f3v4s29 42.000000 +f0v1s1 f0v2s2 67.000000 +f0v1s1 f3v12s37 40.000000 +f0v1s1 f0v3s3 65.000000 +f0v1s1 f3v14s39 40.000000 +f0v1s1 f0v0s0 68.000000 +f0v1s1 f1v12s19 38.000000 +f1v5s12 f1v4s11 56.000000 +f1v5s12 f1v9s16 55.000000 +f1v5s12 f1v10s17 55.000000 +f1v5s12 f1v0s7 56.000000 +f1v5s12 f3v9s34 40.000000 +f1v5s12 f4v0s40 35.000000 +f1v5s12 f1v7s14 56.000000 +f1v5s12 f3v2s27 39.000000 +f1v5s12 f4v1s41 35.000000 +f1v5s12 f2v0s22 40.000000 +f1v5s12 f1v2s9 39.000000 +f1v5s12 f1v3s10 56.000000 +f1v5s12 f3v0s25 39.000000 +f1v5s12 f3v3s28 38.000000 +f1v5s12 f3v13s38 40.000000 +f1v5s12 f3v7s32 40.000000 +f1v5s12 f3v1s26 38.000000 +f1v5s12 f3v6s31 40.000000 +f1v5s12 f1v13s20 54.000000 +f1v5s12 f2v2s24 32.000000 +f1v5s12 f3v11s36 39.000000 +f1v5s12 f0v5s5 39.000000 +f1v5s12 f1v11s18 55.000000 +f1v5s12 f0v6s6 39.000000 +f1v5s12 f3v4s29 40.000000 +f1v5s12 f0v2s2 40.000000 +f1v5s12 f3v12s37 40.000000 +f1v5s12 f0v3s3 39.000000 +f1v5s12 f3v14s39 39.000000 +f1v5s12 f0v0s0 40.000000 +f1v5s12 f1v12s19 55.000000 +f1v4s11 f1v9s16 57.000000 +f1v4s11 f1v10s17 56.000000 +f1v4s11 f1v0s7 58.000000 +f1v4s11 f3v9s34 40.000000 +f1v4s11 f4v0s40 35.000000 +f1v4s11 f1v7s14 58.000000 +f1v4s11 f3v2s27 39.000000 +f1v4s11 f4v1s41 35.000000 +f1v4s11 f2v0s22 39.000000 +f1v4s11 f1v2s9 38.000000 +f1v4s11 f1v3s10 58.000000 +f1v4s11 f3v0s25 39.000000 +f1v4s11 f3v3s28 39.000000 +f1v4s11 f3v13s38 40.000000 +f1v4s11 f3v7s32 40.000000 +f1v4s11 f3v1s26 39.000000 +f1v4s11 f3v6s31 40.000000 +f1v4s11 f1v13s20 56.000000 +f1v4s11 f2v2s24 34.000000 +f1v4s11 f3v11s36 39.000000 +f1v4s11 f0v5s5 39.000000 +f1v4s11 f1v11s18 57.000000 +f1v4s11 f0v6s6 38.000000 +f1v4s11 f3v4s29 40.000000 +f1v4s11 f0v2s2 39.000000 +f1v4s11 f3v12s37 40.000000 +f1v4s11 f0v3s3 39.000000 +f1v4s11 f3v14s39 39.000000 +f1v4s11 f0v0s0 39.000000 +f1v4s11 f1v12s19 56.000000 +f1v9s16 f1v10s17 55.000000 +f1v9s16 f1v0s7 57.000000 +f1v9s16 f3v9s34 39.000000 +f1v9s16 f4v0s40 36.000000 +f1v9s16 f1v7s14 57.000000 +f1v9s16 f3v2s27 39.000000 +f1v9s16 f4v1s41 36.000000 +f1v9s16 f2v0s22 40.000000 +f1v9s16 f1v2s9 39.000000 +f1v9s16 f1v3s10 57.000000 +f1v9s16 f3v0s25 39.000000 +f1v9s16 f3v3s28 39.000000 +f1v9s16 f3v13s38 40.000000 +f1v9s16 f3v7s32 39.000000 +f1v9s16 f3v1s26 38.000000 +f1v9s16 f3v6s31 39.000000 +f1v9s16 f1v13s20 55.000000 +f1v9s16 f2v2s24 35.000000 +f1v9s16 f3v11s36 39.000000 +f1v9s16 f0v5s5 40.000000 +f1v9s16 f1v11s18 56.000000 +f1v9s16 f0v6s6 39.000000 +f1v9s16 f3v4s29 40.000000 +f1v9s16 f0v2s2 40.000000 +f1v9s16 f3v12s37 40.000000 +f1v9s16 f0v3s3 41.000000 +f1v9s16 f3v14s39 39.000000 +f1v9s16 f0v0s0 40.000000 +f1v9s16 f1v12s19 55.000000 +f1v10s17 f1v0s7 56.000000 +f1v10s17 f3v9s34 41.000000 +f1v10s17 f4v0s40 35.000000 +f1v10s17 f1v7s14 56.000000 +f1v10s17 f3v2s27 40.000000 +f1v10s17 f4v1s41 36.000000 +f1v10s17 f2v0s22 39.000000 +f1v10s17 f1v2s9 39.000000 +f1v10s17 f1v3s10 56.000000 +f1v10s17 f3v0s25 40.000000 +f1v10s17 f3v3s28 40.000000 +f1v10s17 f3v13s38 41.000000 +f1v10s17 f3v7s32 41.000000 +f1v10s17 f3v1s26 39.000000 +f1v10s17 f3v6s31 41.000000 +f1v10s17 f1v13s20 54.000000 +f1v10s17 f2v2s24 33.000000 +f1v10s17 f3v11s36 41.000000 +f1v10s17 f0v5s5 39.000000 +f1v10s17 f1v11s18 55.000000 +f1v10s17 f0v6s6 39.000000 +f1v10s17 f3v4s29 40.000000 +f1v10s17 f0v2s2 39.000000 +f1v10s17 f3v12s37 41.000000 +f1v10s17 f0v3s3 40.000000 +f1v10s17 f3v14s39 40.000000 +f1v10s17 f0v0s0 40.000000 +f1v10s17 f1v12s19 55.000000 +f1v0s7 f3v9s34 40.000000 +f1v0s7 f4v0s40 35.000000 +f1v0s7 f1v7s14 58.000000 +f1v0s7 f3v2s27 39.000000 +f1v0s7 f4v1s41 35.000000 +f1v0s7 f2v0s22 39.000000 +f1v0s7 f1v2s9 38.000000 +f1v0s7 f1v3s10 58.000000 +f1v0s7 f3v0s25 39.000000 +f1v0s7 f3v3s28 39.000000 +f1v0s7 f3v13s38 40.000000 +f1v0s7 f3v7s32 40.000000 +f1v0s7 f3v1s26 39.000000 +f1v0s7 f3v6s31 40.000000 +f1v0s7 f1v13s20 56.000000 +f1v0s7 f2v2s24 34.000000 +f1v0s7 f3v11s36 39.000000 +f1v0s7 f0v5s5 39.000000 +f1v0s7 f1v11s18 57.000000 +f1v0s7 f0v6s6 38.000000 +f1v0s7 f3v4s29 40.000000 +f1v0s7 f0v2s2 39.000000 +f1v0s7 f3v12s37 40.000000 +f1v0s7 f0v3s3 39.000000 +f1v0s7 f3v14s39 39.000000 +f1v0s7 f0v0s0 39.000000 +f1v0s7 f1v12s19 56.000000 +f3v9s34 f4v0s40 35.000000 +f3v9s34 f1v7s14 40.000000 +f3v9s34 f3v2s27 63.000000 +f3v9s34 f4v1s41 35.000000 +f3v9s34 f2v0s22 40.000000 +f3v9s34 f1v2s9 40.000000 +f3v9s34 f1v3s10 40.000000 +f3v9s34 f3v0s25 64.000000 +f3v9s34 f3v3s28 63.000000 +f3v9s34 f3v13s38 64.000000 +f3v9s34 f3v7s32 65.000000 +f3v9s34 f3v1s26 62.000000 +f3v9s34 f3v6s31 65.000000 +f3v9s34 f1v13s20 39.000000 +f3v9s34 f2v2s24 34.000000 +f3v9s34 f3v11s36 61.000000 +f3v9s34 f0v5s5 40.000000 +f3v9s34 f1v11s18 40.000000 +f3v9s34 f0v6s6 39.000000 +f3v9s34 f3v4s29 63.000000 +f3v9s34 f0v2s2 40.000000 +f3v9s34 f3v12s37 63.000000 +f3v9s34 f0v3s3 41.000000 +f3v9s34 f3v14s39 64.000000 +f3v9s34 f0v0s0 41.000000 +f3v9s34 f1v12s19 39.000000 +f4v0s40 f1v7s14 35.000000 +f4v0s40 f3v2s27 36.000000 +f4v0s40 f4v1s41 60.000000 +f4v0s40 f2v0s22 39.000000 +f4v0s40 f1v2s9 38.000000 +f4v0s40 f1v3s10 35.000000 +f4v0s40 f3v0s25 36.000000 +f4v0s40 f3v3s28 36.000000 +f4v0s40 f3v13s38 37.000000 +f4v0s40 f3v7s32 35.000000 +f4v0s40 f3v1s26 36.000000 +f4v0s40 f3v6s31 35.000000 +f4v0s40 f1v13s20 34.000000 +f4v0s40 f2v2s24 34.000000 +f4v0s40 f3v11s36 36.000000 +f4v0s40 f0v5s5 38.000000 +f4v0s40 f1v11s18 35.000000 +f4v0s40 f0v6s6 39.000000 +f4v0s40 f3v4s29 38.000000 +f4v0s40 f0v2s2 39.000000 +f4v0s40 f3v12s37 37.000000 +f4v0s40 f0v3s3 37.000000 +f4v0s40 f3v14s39 36.000000 +f4v0s40 f0v0s0 38.000000 +f4v0s40 f1v12s19 33.000000 +f1v7s14 f3v2s27 39.000000 +f1v7s14 f4v1s41 35.000000 +f1v7s14 f2v0s22 39.000000 +f1v7s14 f1v2s9 38.000000 +f1v7s14 f1v3s10 58.000000 +f1v7s14 f3v0s25 39.000000 +f1v7s14 f3v3s28 39.000000 +f1v7s14 f3v13s38 40.000000 +f1v7s14 f3v7s32 40.000000 +f1v7s14 f3v1s26 39.000000 +f1v7s14 f3v6s31 40.000000 +f1v7s14 f1v13s20 56.000000 +f1v7s14 f2v2s24 34.000000 +f1v7s14 f3v11s36 39.000000 +f1v7s14 f0v5s5 39.000000 +f1v7s14 f1v11s18 57.000000 +f1v7s14 f0v6s6 38.000000 +f1v7s14 f3v4s29 40.000000 +f1v7s14 f0v2s2 39.000000 +f1v7s14 f3v12s37 40.000000 +f1v7s14 f0v3s3 39.000000 +f1v7s14 f3v14s39 39.000000 +f1v7s14 f0v0s0 39.000000 +f1v7s14 f1v12s19 56.000000 +f3v2s27 f4v1s41 36.000000 +f3v2s27 f2v0s22 41.000000 +f3v2s27 f1v2s9 40.000000 +f3v2s27 f1v3s10 39.000000 +f3v2s27 f3v0s25 65.000000 +f3v2s27 f3v3s28 64.000000 +f3v2s27 f3v13s38 65.000000 +f3v2s27 f3v7s32 63.000000 +f3v2s27 f3v1s26 63.000000 +f3v2s27 f3v6s31 63.000000 +f3v2s27 f1v13s20 38.000000 +f3v2s27 f2v2s24 35.000000 +f3v2s27 f3v11s36 62.000000 +f3v2s27 f0v5s5 40.000000 +f3v2s27 f1v11s18 39.000000 +f3v2s27 f0v6s6 40.000000 +f3v2s27 f3v4s29 64.000000 +f3v2s27 f0v2s2 41.000000 +f3v2s27 f3v12s37 64.000000 +f3v2s27 f0v3s3 41.000000 +f3v2s27 f3v14s39 65.000000 +f3v2s27 f0v0s0 41.000000 +f3v2s27 f1v12s19 38.000000 +f4v1s41 f2v0s22 39.000000 +f4v1s41 f1v2s9 38.000000 +f4v1s41 f1v3s10 35.000000 +f4v1s41 f3v0s25 36.000000 +f4v1s41 f3v3s28 36.000000 +f4v1s41 f3v13s38 37.000000 +f4v1s41 f3v7s32 35.000000 +f4v1s41 f3v1s26 36.000000 +f4v1s41 f3v6s31 35.000000 +f4v1s41 f1v13s20 34.000000 +f4v1s41 f2v2s24 34.000000 +f4v1s41 f3v11s36 36.000000 +f4v1s41 f0v5s5 38.000000 +f4v1s41 f1v11s18 35.000000 +f4v1s41 f0v6s6 39.000000 +f4v1s41 f3v4s29 38.000000 +f4v1s41 f0v2s2 39.000000 +f4v1s41 f3v12s37 37.000000 +f4v1s41 f0v3s3 37.000000 +f4v1s41 f3v14s39 36.000000 +f4v1s41 f0v0s0 38.000000 +f4v1s41 f1v12s19 33.000000 +f2v0s22 f1v2s9 67.000000 +f2v0s22 f1v3s10 39.000000 +f2v0s22 f3v0s25 41.000000 +f2v0s22 f3v3s28 41.000000 +f2v0s22 f3v13s38 41.000000 +f2v0s22 f3v7s32 40.000000 +f2v0s22 f3v1s26 40.000000 +f2v0s22 f3v6s31 40.000000 +f2v0s22 f1v13s20 38.000000 +f2v0s22 f2v2s24 35.000000 +f2v0s22 f3v11s36 38.000000 +f2v0s22 f0v5s5 67.000000 +f2v0s22 f1v11s18 39.000000 +f2v0s22 f0v6s6 64.000000 +f2v0s22 f3v4s29 43.000000 +f2v0s22 f0v2s2 69.000000 +f2v0s22 f3v12s37 42.000000 +f2v0s22 f0v3s3 65.000000 +f2v0s22 f3v14s39 41.000000 +f2v0s22 f0v0s0 68.000000 +f2v0s22 f1v12s19 39.000000 +f1v2s9 f1v3s10 38.000000 +f1v2s9 f3v0s25 40.000000 +f1v2s9 f3v3s28 40.000000 +f1v2s9 f3v13s38 40.000000 +f1v2s9 f3v7s32 40.000000 +f1v2s9 f3v1s26 39.000000 +f1v2s9 f3v6s31 40.000000 +f1v2s9 f1v13s20 37.000000 +f1v2s9 f2v2s24 35.000000 +f1v2s9 f3v11s36 37.000000 +f1v2s9 f0v5s5 67.000000 +f1v2s9 f1v11s18 38.000000 +f1v2s9 f0v6s6 64.000000 +f1v2s9 f3v4s29 42.000000 +f1v2s9 f0v2s2 67.000000 +f1v2s9 f3v12s37 40.000000 +f1v2s9 f0v3s3 65.000000 +f1v2s9 f3v14s39 40.000000 +f1v2s9 f0v0s0 68.000000 +f1v2s9 f1v12s19 38.000000 +f1v3s10 f3v0s25 39.000000 +f1v3s10 f3v3s28 39.000000 +f1v3s10 f3v13s38 40.000000 +f1v3s10 f3v7s32 40.000000 +f1v3s10 f3v1s26 39.000000 +f1v3s10 f3v6s31 40.000000 +f1v3s10 f1v13s20 56.000000 +f1v3s10 f2v2s24 34.000000 +f1v3s10 f3v11s36 39.000000 +f1v3s10 f0v5s5 39.000000 +f1v3s10 f1v11s18 57.000000 +f1v3s10 f0v6s6 38.000000 +f1v3s10 f3v4s29 40.000000 +f1v3s10 f0v2s2 39.000000 +f1v3s10 f3v12s37 40.000000 +f1v3s10 f0v3s3 39.000000 +f1v3s10 f3v14s39 39.000000 +f1v3s10 f0v0s0 39.000000 +f1v3s10 f1v12s19 56.000000 +f3v0s25 f3v3s28 65.000000 +f3v0s25 f3v13s38 66.000000 +f3v0s25 f3v7s32 64.000000 +f3v0s25 f3v1s26 64.000000 +f3v0s25 f3v6s31 64.000000 +f3v0s25 f1v13s20 38.000000 +f3v0s25 f2v2s24 35.000000 +f3v0s25 f3v11s36 63.000000 +f3v0s25 f0v5s5 40.000000 +f3v0s25 f1v11s18 39.000000 +f3v0s25 f0v6s6 40.000000 +f3v0s25 f3v4s29 65.000000 +f3v0s25 f0v2s2 41.000000 +f3v0s25 f3v12s37 65.000000 +f3v0s25 f0v3s3 41.000000 +f3v0s25 f3v14s39 66.000000 +f3v0s25 f0v0s0 41.000000 +f3v0s25 f1v12s19 38.000000 +f3v3s28 f3v13s38 65.000000 +f3v3s28 f3v7s32 63.000000 +f3v3s28 f3v1s26 63.000000 +f3v3s28 f3v6s31 63.000000 +f3v3s28 f1v13s20 38.000000 +f3v3s28 f2v2s24 36.000000 +f3v3s28 f3v11s36 62.000000 +f3v3s28 f0v5s5 40.000000 +f3v3s28 f1v11s18 39.000000 +f3v3s28 f0v6s6 40.000000 +f3v3s28 f3v4s29 64.000000 +f3v3s28 f0v2s2 41.000000 +f3v3s28 f3v12s37 64.000000 +f3v3s28 f0v3s3 41.000000 +f3v3s28 f3v14s39 65.000000 +f3v3s28 f0v0s0 41.000000 +f3v3s28 f1v12s19 38.000000 +f3v13s38 f3v7s32 64.000000 +f3v13s38 f3v1s26 64.000000 +f3v13s38 f3v6s31 64.000000 +f3v13s38 f1v13s20 39.000000 +f3v13s38 f2v2s24 36.000000 +f3v13s38 f3v11s36 63.000000 +f3v13s38 f0v5s5 40.000000 +f3v13s38 f1v11s18 40.000000 +f3v13s38 f0v6s6 40.000000 +f3v13s38 f3v4s29 65.000000 +f3v13s38 f0v2s2 41.000000 +f3v13s38 f3v12s37 65.000000 +f3v13s38 f0v3s3 41.000000 +f3v13s38 f3v14s39 66.000000 +f3v13s38 f0v0s0 41.000000 +f3v13s38 f1v12s19 39.000000 +f3v7s32 f3v1s26 62.000000 +f3v7s32 f3v6s31 65.000000 +f3v7s32 f1v13s20 39.000000 +f3v7s32 f2v2s24 34.000000 +f3v7s32 f3v11s36 61.000000 +f3v7s32 f0v5s5 40.000000 +f3v7s32 f1v11s18 40.000000 +f3v7s32 f0v6s6 39.000000 +f3v7s32 f3v4s29 63.000000 +f3v7s32 f0v2s2 40.000000 +f3v7s32 f3v12s37 63.000000 +f3v7s32 f0v3s3 41.000000 +f3v7s32 f3v14s39 64.000000 +f3v7s32 f0v0s0 41.000000 +f3v7s32 f1v12s19 39.000000 +f3v1s26 f3v6s31 62.000000 +f3v1s26 f1v13s20 38.000000 +f3v1s26 f2v2s24 35.000000 +f3v1s26 f3v11s36 61.000000 +f3v1s26 f0v5s5 39.000000 +f3v1s26 f1v11s18 39.000000 +f3v1s26 f0v6s6 39.000000 +f3v1s26 f3v4s29 63.000000 +f3v1s26 f0v2s2 40.000000 +f3v1s26 f3v12s37 63.000000 +f3v1s26 f0v3s3 40.000000 +f3v1s26 f3v14s39 64.000000 +f3v1s26 f0v0s0 40.000000 +f3v1s26 f1v12s19 38.000000 +f3v6s31 f1v13s20 39.000000 +f3v6s31 f2v2s24 34.000000 +f3v6s31 f3v11s36 61.000000 +f3v6s31 f0v5s5 40.000000 +f3v6s31 f1v11s18 40.000000 +f3v6s31 f0v6s6 39.000000 +f3v6s31 f3v4s29 63.000000 +f3v6s31 f0v2s2 40.000000 +f3v6s31 f3v12s37 63.000000 +f3v6s31 f0v3s3 41.000000 +f3v6s31 f3v14s39 64.000000 +f3v6s31 f0v0s0 41.000000 +f3v6s31 f1v12s19 39.000000 +f1v13s20 f2v2s24 34.000000 +f1v13s20 f3v11s36 38.000000 +f1v13s20 f0v5s5 39.000000 +f1v13s20 f1v11s18 55.000000 +f1v13s20 f0v6s6 37.000000 +f1v13s20 f3v4s29 39.000000 +f1v13s20 f0v2s2 38.000000 +f1v13s20 f3v12s37 39.000000 +f1v13s20 f0v3s3 38.000000 +f1v13s20 f3v14s39 38.000000 +f1v13s20 f0v0s0 38.000000 +f1v13s20 f1v12s19 54.000000 +f2v2s24 f3v11s36 34.000000 +f2v2s24 f0v5s5 35.000000 +f2v2s24 f1v11s18 34.000000 +f2v2s24 f0v6s6 35.000000 +f2v2s24 f3v4s29 36.000000 +f2v2s24 f0v2s2 35.000000 +f2v2s24 f3v12s37 36.000000 +f2v2s24 f0v3s3 34.000000 +f2v2s24 f3v14s39 35.000000 +f2v2s24 f0v0s0 35.000000 +f2v2s24 f1v12s19 33.000000 +f3v11s36 f0v5s5 38.000000 +f3v11s36 f1v11s18 39.000000 +f3v11s36 f0v6s6 38.000000 +f3v11s36 f3v4s29 62.000000 +f3v11s36 f0v2s2 38.000000 +f3v11s36 f3v12s37 62.000000 +f3v11s36 f0v3s3 38.000000 +f3v11s36 f3v14s39 63.000000 +f3v11s36 f0v0s0 38.000000 +f3v11s36 f1v12s19 38.000000 +f0v5s5 f1v11s18 40.000000 +f0v5s5 f0v6s6 64.000000 +f0v5s5 f3v4s29 42.000000 +f0v5s5 f0v2s2 67.000000 +f0v5s5 f3v12s37 40.000000 +f0v5s5 f0v3s3 65.000000 +f0v5s5 f3v14s39 40.000000 +f0v5s5 f0v0s0 68.000000 +f0v5s5 f1v12s19 38.000000 +f1v11s18 f0v6s6 38.000000 +f1v11s18 f3v4s29 40.000000 +f1v11s18 f0v2s2 39.000000 +f1v11s18 f3v12s37 40.000000 +f1v11s18 f0v3s3 39.000000 +f1v11s18 f3v14s39 39.000000 +f1v11s18 f0v0s0 39.000000 +f1v11s18 f1v12s19 55.000000 +f0v6s6 f3v4s29 42.000000 +f0v6s6 f0v2s2 64.000000 +f0v6s6 f3v12s37 40.000000 +f0v6s6 f0v3s3 62.000000 +f0v6s6 f3v14s39 40.000000 +f0v6s6 f0v0s0 65.000000 +f0v6s6 f1v12s19 38.000000 +f3v4s29 f0v2s2 43.000000 +f3v4s29 f3v12s37 64.000000 +f3v4s29 f0v3s3 43.000000 +f3v4s29 f3v14s39 65.000000 +f3v4s29 f0v0s0 43.000000 +f3v4s29 f1v12s19 39.000000 +f0v2s2 f3v12s37 42.000000 +f0v2s2 f0v3s3 65.000000 +f0v2s2 f3v14s39 41.000000 +f0v2s2 f0v0s0 68.000000 +f0v2s2 f1v12s19 39.000000 +f3v12s37 f0v3s3 41.000000 +f3v12s37 f3v14s39 65.000000 +f3v12s37 f0v0s0 41.000000 +f3v12s37 f1v12s19 39.000000 +f0v3s3 f3v14s39 41.000000 +f0v3s3 f0v0s0 66.000000 +f0v3s3 f1v12s19 38.000000 +f3v14s39 f0v0s0 41.000000 +f3v14s39 f1v12s19 38.000000 +f0v0s0 f1v12s19 39.000000 diff --git a/pkg/obitools/obialign/testdata/one_allshortest.expected b/pkg/obitools/obialign/testdata/one_allshortest.expected new file mode 100644 index 0000000..11cba2c --- /dev/null +++ b/pkg/obitools/obialign/testdata/one_allshortest.expected @@ -0,0 +1,903 @@ +f3v10s35 f3v5s30 0.920635 +f3v10s35 f1v8s15 0.649123 +f3v10s35 f2v1s23 0.655172 +f3v10s35 f4v2s42 0.622951 +f3v10s35 f0v4s4 0.615385 +f3v10s35 f1v6s13 0.660714 +f3v10s35 f3v8s33 0.938462 +f3v10s35 f1v1s8 0.650000 +f3v10s35 f1v14s21 0.678571 +f3v10s35 f0v1s1 0.615385 +f3v10s35 f1v5s12 0.633333 +f3v10s35 f1v4s11 0.655172 +f3v10s35 f1v9s16 0.650000 +f3v10s35 f1v10s17 0.650000 +f3v10s35 f1v0s7 0.655172 +f3v10s35 f3v9s34 0.938462 +f3v10s35 f4v0s40 0.633333 +f3v10s35 f1v7s14 0.655172 +f3v10s35 f3v2s27 0.938462 +f3v10s35 f4v1s41 0.622951 +f3v10s35 f2v0s22 0.615385 +f3v10s35 f1v2s9 0.615385 +f3v10s35 f1v3s10 0.655172 +f3v10s35 f3v0s25 0.953846 +f3v10s35 f3v3s28 0.938462 +f3v10s35 f3v13s38 0.953846 +f3v10s35 f3v7s32 0.938462 +f3v10s35 f3v1s26 0.938462 +f3v10s35 f3v6s31 0.938462 +f3v10s35 f1v13s20 0.660714 +f3v10s35 f2v2s24 0.714286 +f3v10s35 f3v11s36 0.907692 +f3v10s35 f0v5s5 0.600000 +f3v10s35 f1v11s18 0.655172 +f3v10s35 f0v6s6 0.584615 +f3v10s35 f3v4s29 0.938462 +f3v10s35 f0v2s2 0.615385 +f3v10s35 f3v12s37 0.938462 +f3v10s35 f0v3s3 0.615385 +f3v10s35 f3v14s39 0.953846 +f3v10s35 f0v0s0 0.615385 +f3v10s35 f1v12s19 0.649123 +f3v5s30 f1v8s15 0.666667 +f3v5s30 f2v1s23 0.655172 +f3v5s30 f4v2s42 0.606557 +f3v5s30 f0v4s4 0.619048 +f3v5s30 f1v6s13 0.696429 +f3v5s30 f3v8s33 0.968254 +f3v5s30 f1v1s8 0.650000 +f3v5s30 f1v14s21 0.660714 +f3v5s30 f0v1s1 0.619048 +f3v5s30 f1v5s12 0.633333 +f3v5s30 f1v4s11 0.655172 +f3v5s30 f1v9s16 0.633333 +f3v5s30 f1v10s17 0.650000 +f3v5s30 f1v0s7 0.655172 +f3v5s30 f3v9s34 0.952381 +f3v5s30 f4v0s40 0.616667 +f3v5s30 f1v7s14 0.655172 +f3v5s30 f3v2s27 0.968254 +f3v5s30 f4v1s41 0.606557 +f3v5s30 f2v0s22 0.634921 +f3v5s30 f1v2s9 0.619048 +f3v5s30 f1v3s10 0.655172 +f3v5s30 f3v0s25 0.984127 +f3v5s30 f3v3s28 0.968254 +f3v5s30 f3v13s38 0.984127 +f3v5s30 f3v7s32 0.952381 +f3v5s30 f3v1s26 0.968254 +f3v5s30 f3v6s31 0.952381 +f3v5s30 f1v13s20 0.678571 +f3v5s30 f2v2s24 0.693878 +f3v5s30 f3v11s36 0.936508 +f3v5s30 f0v5s5 0.619048 +f3v5s30 f1v11s18 0.655172 +f3v5s30 f0v6s6 0.650794 +f3v5s30 f3v4s29 0.968254 +f3v5s30 f0v2s2 0.634921 +f3v5s30 f3v12s37 0.968254 +f3v5s30 f0v3s3 0.634921 +f3v5s30 f3v14s39 0.984127 +f3v5s30 f0v0s0 0.634921 +f3v5s30 f1v12s19 0.649123 +f1v8s15 f2v1s23 1.000000 +f1v8s15 f4v2s42 0.596491 +f1v8s15 f0v4s4 0.666667 +f1v8s15 f1v6s13 0.964286 +f1v8s15 f3v8s33 0.684211 +f1v8s15 f1v1s8 0.964912 +f1v8s15 f1v14s21 0.964286 +f1v8s15 f0v1s1 0.666667 +f1v8s15 f1v5s12 0.964912 +f1v8s15 f1v4s11 1.000000 +f1v8s15 f1v9s16 0.982456 +f1v8s15 f1v10s17 0.964912 +f1v8s15 f1v0s7 1.000000 +f1v8s15 f3v9s34 0.701754 +f1v8s15 f4v0s40 0.614035 +f1v8s15 f1v7s14 1.000000 +f1v8s15 f3v2s27 0.684211 +f1v8s15 f4v1s41 0.614035 +f1v8s15 f2v0s22 0.684211 +f1v8s15 f1v2s9 0.666667 +f1v8s15 f1v3s10 1.000000 +f1v8s15 f3v0s25 0.684211 +f1v8s15 f3v3s28 0.684211 +f1v8s15 f3v13s38 0.701754 +f1v8s15 f3v7s32 0.701754 +f1v8s15 f3v1s26 0.684211 +f1v8s15 f3v6s31 0.701754 +f1v8s15 f1v13s20 0.982143 +f1v8s15 f2v2s24 0.693878 +f1v8s15 f3v11s36 0.684211 +f1v8s15 f0v5s5 0.684211 +f1v8s15 f1v11s18 0.982456 +f1v8s15 f0v6s6 0.666667 +f1v8s15 f3v4s29 0.701754 +f1v8s15 f0v2s2 0.684211 +f1v8s15 f3v12s37 0.701754 +f1v8s15 f0v3s3 0.684211 +f1v8s15 f3v14s39 0.684211 +f1v8s15 f0v0s0 0.684211 +f1v8s15 f1v12s19 0.964912 +f2v1s23 f4v2s42 0.586207 +f2v1s23 f0v4s4 0.655172 +f2v1s23 f1v6s13 0.964286 +f2v1s23 f3v8s33 0.672414 +f2v1s23 f1v1s8 0.965517 +f2v1s23 f1v14s21 0.982143 +f2v1s23 f0v1s1 0.655172 +f2v1s23 f1v5s12 0.965517 +f2v1s23 f1v4s11 1.000000 +f2v1s23 f1v9s16 0.982759 +f2v1s23 f1v10s17 0.965517 +f2v1s23 f1v0s7 1.000000 +f2v1s23 f3v9s34 0.689655 +f2v1s23 f4v0s40 0.603448 +f2v1s23 f1v7s14 1.000000 +f2v1s23 f3v2s27 0.672414 +f2v1s23 f4v1s41 0.603448 +f2v1s23 f2v0s22 0.672414 +f2v1s23 f1v2s9 0.655172 +f2v1s23 f1v3s10 1.000000 +f2v1s23 f3v0s25 0.672414 +f2v1s23 f3v3s28 0.672414 +f2v1s23 f3v13s38 0.689655 +f2v1s23 f3v7s32 0.689655 +f2v1s23 f3v1s26 0.672414 +f2v1s23 f3v6s31 0.689655 +f2v1s23 f1v13s20 1.000000 +f2v1s23 f2v2s24 0.693878 +f2v1s23 f3v11s36 0.672414 +f2v1s23 f0v5s5 0.672414 +f2v1s23 f1v11s18 0.982759 +f2v1s23 f0v6s6 0.655172 +f2v1s23 f3v4s29 0.689655 +f2v1s23 f0v2s2 0.672414 +f2v1s23 f3v12s37 0.689655 +f2v1s23 f0v3s3 0.672414 +f2v1s23 f3v14s39 0.672414 +f2v1s23 f0v0s0 0.672414 +f2v1s23 f1v12s19 0.982456 +f4v2s42 f0v4s4 0.639344 +f4v2s42 f1v6s13 0.589286 +f4v2s42 f3v8s33 0.606557 +f4v2s42 f1v1s8 0.600000 +f4v2s42 f1v14s21 0.589286 +f4v2s42 f0v1s1 0.639344 +f4v2s42 f1v5s12 0.566667 +f4v2s42 f1v4s11 0.586207 +f4v2s42 f1v9s16 0.600000 +f4v2s42 f1v10s17 0.583333 +f4v2s42 f1v0s7 0.586207 +f4v2s42 f3v9s34 0.573770 +f4v2s42 f4v0s40 0.983333 +f4v2s42 f1v7s14 0.586207 +f4v2s42 f3v2s27 0.590164 +f4v2s42 f4v1s41 0.967213 +f4v2s42 f2v0s22 0.672131 +f4v2s42 f1v2s9 0.639344 +f4v2s42 f1v3s10 0.586207 +f4v2s42 f3v0s25 0.590164 +f4v2s42 f3v3s28 0.590164 +f4v2s42 f3v13s38 0.606557 +f4v2s42 f3v7s32 0.573770 +f4v2s42 f3v1s26 0.590164 +f4v2s42 f3v6s31 0.573770 +f4v2s42 f1v13s20 0.589286 +f4v2s42 f2v2s24 0.714286 +f4v2s42 f3v11s36 0.590164 +f4v2s42 f0v5s5 0.655738 +f4v2s42 f1v11s18 0.586207 +f4v2s42 f0v6s6 0.672131 +f4v2s42 f3v4s29 0.622951 +f4v2s42 f0v2s2 0.672131 +f4v2s42 f3v12s37 0.606557 +f4v2s42 f0v3s3 0.639344 +f4v2s42 f3v14s39 0.590164 +f4v2s42 f0v0s0 0.655738 +f4v2s42 f1v12s19 0.578947 +f0v4s4 f1v6s13 0.660714 +f0v4s4 f3v8s33 0.621212 +f0v4s4 f1v1s8 0.633333 +f0v4s4 f1v14s21 0.678571 +f0v4s4 f0v1s1 1.000000 +f0v4s4 f1v5s12 0.650000 +f0v4s4 f1v4s11 0.655172 +f0v4s4 f1v9s16 0.650000 +f0v4s4 f1v10s17 0.650000 +f0v4s4 f1v0s7 0.655172 +f0v4s4 f3v9s34 0.615385 +f0v4s4 f4v0s40 0.633333 +f0v4s4 f1v7s14 0.655172 +f0v4s4 f3v2s27 0.615385 +f0v4s4 f4v1s41 0.622951 +f0v4s4 f2v0s22 0.971014 +f0v4s4 f1v2s9 1.000000 +f0v4s4 f1v3s10 0.655172 +f0v4s4 f3v0s25 0.606061 +f0v4s4 f3v3s28 0.606061 +f0v4s4 f3v13s38 0.588235 +f0v4s4 f3v7s32 0.615385 +f0v4s4 f3v1s26 0.600000 +f0v4s4 f3v6s31 0.615385 +f0v4s4 f1v13s20 0.660714 +f0v4s4 f2v2s24 0.714286 +f0v4s4 f3v11s36 0.569231 +f0v4s4 f0v5s5 0.971014 +f0v4s4 f1v11s18 0.655172 +f0v4s4 f0v6s6 0.927536 +f0v4s4 f3v4s29 0.617647 +f0v4s4 f0v2s2 0.971014 +f0v4s4 f3v12s37 0.597015 +f0v4s4 f0v3s3 0.970149 +f0v4s4 f3v14s39 0.606061 +f0v4s4 f0v0s0 0.985507 +f0v4s4 f1v12s19 0.666667 +f1v6s13 f3v8s33 0.696429 +f1v6s13 f1v1s8 0.928571 +f1v6s13 f1v14s21 0.910714 +f1v6s13 f0v1s1 0.660714 +f1v6s13 f1v5s12 0.928571 +f1v6s13 f1v4s11 0.964286 +f1v6s13 f1v9s16 0.946429 +f1v6s13 f1v10s17 0.946429 +f1v6s13 f1v0s7 0.964286 +f1v6s13 f3v9s34 0.696429 +f1v6s13 f4v0s40 0.607143 +f1v6s13 f1v7s14 0.964286 +f1v6s13 f3v2s27 0.696429 +f1v6s13 f4v1s41 0.607143 +f1v6s13 f2v0s22 0.678571 +f1v6s13 f1v2s9 0.660714 +f1v6s13 f1v3s10 0.964286 +f1v6s13 f3v0s25 0.696429 +f1v6s13 f3v3s28 0.696429 +f1v6s13 f3v13s38 0.696429 +f1v6s13 f3v7s32 0.696429 +f1v6s13 f3v1s26 0.696429 +f1v6s13 f3v6s31 0.696429 +f1v6s13 f1v13s20 0.928571 +f1v6s13 f2v2s24 0.673469 +f1v6s13 f3v11s36 0.696429 +f1v6s13 f0v5s5 0.678571 +f1v6s13 f1v11s18 0.946429 +f1v6s13 f0v6s6 0.678571 +f1v6s13 f3v4s29 0.714286 +f1v6s13 f0v2s2 0.678571 +f1v6s13 f3v12s37 0.714286 +f1v6s13 f0v3s3 0.660714 +f1v6s13 f3v14s39 0.696429 +f1v6s13 f0v0s0 0.678571 +f1v6s13 f1v12s19 0.928571 +f3v8s33 f1v1s8 0.666667 +f3v8s33 f1v14s21 0.678571 +f3v8s33 f0v1s1 0.621212 +f3v8s33 f1v5s12 0.650000 +f3v8s33 f1v4s11 0.672414 +f3v8s33 f1v9s16 0.650000 +f3v8s33 f1v10s17 0.666667 +f3v8s33 f1v0s7 0.672414 +f3v8s33 f3v9s34 0.969231 +f3v8s33 f4v0s40 0.616667 +f3v8s33 f1v7s14 0.672414 +f3v8s33 f3v2s27 0.984615 +f3v8s33 f4v1s41 0.606557 +f3v8s33 f2v0s22 0.636364 +f3v8s33 f1v2s9 0.621212 +f3v8s33 f1v3s10 0.672414 +f3v8s33 f3v0s25 0.984848 +f3v8s33 f3v3s28 0.969697 +f3v8s33 f3v13s38 0.984848 +f3v8s33 f3v7s32 0.969231 +f3v8s33 f3v1s26 0.969231 +f3v8s33 f3v6s31 0.969231 +f3v8s33 f1v13s20 0.678571 +f3v8s33 f2v2s24 0.693878 +f3v8s33 f3v11s36 0.953846 +f3v8s33 f0v5s5 0.621212 +f3v8s33 f1v11s18 0.672414 +f3v8s33 f0v6s6 0.621212 +f3v8s33 f3v4s29 0.969697 +f3v8s33 f0v2s2 0.636364 +f3v8s33 f3v12s37 0.969697 +f3v8s33 f0v3s3 0.621212 +f3v8s33 f3v14s39 0.984848 +f3v8s33 f0v0s0 0.636364 +f3v8s33 f1v12s19 0.666667 +f1v1s8 f1v14s21 0.982143 +f1v1s8 f0v1s1 0.633333 +f1v1s8 f1v5s12 0.900000 +f1v1s8 f1v4s11 0.965517 +f1v1s8 f1v9s16 0.916667 +f1v1s8 f1v10s17 0.900000 +f1v1s8 f1v0s7 0.965517 +f1v1s8 f3v9s34 0.683333 +f1v1s8 f4v0s40 0.600000 +f1v1s8 f1v7s14 0.965517 +f1v1s8 f3v2s27 0.666667 +f1v1s8 f4v1s41 0.600000 +f1v1s8 f2v0s22 0.650000 +f1v1s8 f1v2s9 0.633333 +f1v1s8 f1v3s10 0.965517 +f1v1s8 f3v0s25 0.666667 +f1v1s8 f3v3s28 0.666667 +f1v1s8 f3v13s38 0.683333 +f1v1s8 f3v7s32 0.683333 +f1v1s8 f3v1s26 0.666667 +f1v1s8 f3v6s31 0.683333 +f1v1s8 f1v13s20 0.964286 +f1v1s8 f2v2s24 0.673469 +f1v1s8 f3v11s36 0.650000 +f1v1s8 f0v5s5 0.650000 +f1v1s8 f1v11s18 0.948276 +f1v1s8 f0v6s6 0.633333 +f1v1s8 f3v4s29 0.683333 +f1v1s8 f0v2s2 0.650000 +f1v1s8 f3v12s37 0.683333 +f1v1s8 f0v3s3 0.650000 +f1v1s8 f3v14s39 0.666667 +f1v1s8 f0v0s0 0.650000 +f1v1s8 f1v12s19 0.964912 +f1v14s21 f0v1s1 0.678571 +f1v14s21 f1v5s12 0.946429 +f1v14s21 f1v4s11 0.982143 +f1v14s21 f1v9s16 0.964286 +f1v14s21 f1v10s17 0.946429 +f1v14s21 f1v0s7 0.982143 +f1v14s21 f3v9s34 0.696429 +f1v14s21 f4v0s40 0.589286 +f1v14s21 f1v7s14 0.982143 +f1v14s21 f3v2s27 0.678571 +f1v14s21 f4v1s41 0.607143 +f1v14s21 f2v0s22 0.696429 +f1v14s21 f1v2s9 0.678571 +f1v14s21 f1v3s10 0.982143 +f1v14s21 f3v0s25 0.678571 +f1v14s21 f3v3s28 0.678571 +f1v14s21 f3v13s38 0.678571 +f1v14s21 f3v7s32 0.696429 +f1v14s21 f3v1s26 0.678571 +f1v14s21 f3v6s31 0.696429 +f1v14s21 f1v13s20 0.946429 +f1v14s21 f2v2s24 0.693878 +f1v14s21 f3v11s36 0.678571 +f1v14s21 f0v5s5 0.696429 +f1v14s21 f1v11s18 0.964286 +f1v14s21 f0v6s6 0.678571 +f1v14s21 f3v4s29 0.696429 +f1v14s21 f0v2s2 0.696429 +f1v14s21 f3v12s37 0.696429 +f1v14s21 f0v3s3 0.696429 +f1v14s21 f3v14s39 0.678571 +f1v14s21 f0v0s0 0.696429 +f1v14s21 f1v12s19 0.946429 +f0v1s1 f1v5s12 0.650000 +f0v1s1 f1v4s11 0.655172 +f0v1s1 f1v9s16 0.650000 +f0v1s1 f1v10s17 0.650000 +f0v1s1 f1v0s7 0.655172 +f0v1s1 f3v9s34 0.615385 +f0v1s1 f4v0s40 0.633333 +f0v1s1 f1v7s14 0.655172 +f0v1s1 f3v2s27 0.615385 +f0v1s1 f4v1s41 0.622951 +f0v1s1 f2v0s22 0.971014 +f0v1s1 f1v2s9 1.000000 +f0v1s1 f1v3s10 0.655172 +f0v1s1 f3v0s25 0.606061 +f0v1s1 f3v3s28 0.606061 +f0v1s1 f3v13s38 0.588235 +f0v1s1 f3v7s32 0.615385 +f0v1s1 f3v1s26 0.600000 +f0v1s1 f3v6s31 0.615385 +f0v1s1 f1v13s20 0.660714 +f0v1s1 f2v2s24 0.714286 +f0v1s1 f3v11s36 0.569231 +f0v1s1 f0v5s5 0.971014 +f0v1s1 f1v11s18 0.655172 +f0v1s1 f0v6s6 0.927536 +f0v1s1 f3v4s29 0.617647 +f0v1s1 f0v2s2 0.971014 +f0v1s1 f3v12s37 0.597015 +f0v1s1 f0v3s3 0.970149 +f0v1s1 f3v14s39 0.606061 +f0v1s1 f0v0s0 0.985507 +f0v1s1 f1v12s19 0.666667 +f1v5s12 f1v4s11 0.965517 +f1v5s12 f1v9s16 0.916667 +f1v5s12 f1v10s17 0.916667 +f1v5s12 f1v0s7 0.965517 +f1v5s12 f3v9s34 0.666667 +f1v5s12 f4v0s40 0.583333 +f1v5s12 f1v7s14 0.965517 +f1v5s12 f3v2s27 0.650000 +f1v5s12 f4v1s41 0.583333 +f1v5s12 f2v0s22 0.666667 +f1v5s12 f1v2s9 0.650000 +f1v5s12 f1v3s10 0.965517 +f1v5s12 f3v0s25 0.650000 +f1v5s12 f3v3s28 0.633333 +f1v5s12 f3v13s38 0.666667 +f1v5s12 f3v7s32 0.666667 +f1v5s12 f3v1s26 0.633333 +f1v5s12 f3v6s31 0.666667 +f1v5s12 f1v13s20 0.964286 +f1v5s12 f2v2s24 0.653061 +f1v5s12 f3v11s36 0.650000 +f1v5s12 f0v5s5 0.650000 +f1v5s12 f1v11s18 0.948276 +f1v5s12 f0v6s6 0.650000 +f1v5s12 f3v4s29 0.666667 +f1v5s12 f0v2s2 0.666667 +f1v5s12 f3v12s37 0.666667 +f1v5s12 f0v3s3 0.650000 +f1v5s12 f3v14s39 0.650000 +f1v5s12 f0v0s0 0.666667 +f1v5s12 f1v12s19 0.964912 +f1v4s11 f1v9s16 0.982759 +f1v4s11 f1v10s17 0.965517 +f1v4s11 f1v0s7 1.000000 +f1v4s11 f3v9s34 0.689655 +f1v4s11 f4v0s40 0.603448 +f1v4s11 f1v7s14 1.000000 +f1v4s11 f3v2s27 0.672414 +f1v4s11 f4v1s41 0.603448 +f1v4s11 f2v0s22 0.672414 +f1v4s11 f1v2s9 0.655172 +f1v4s11 f1v3s10 1.000000 +f1v4s11 f3v0s25 0.672414 +f1v4s11 f3v3s28 0.672414 +f1v4s11 f3v13s38 0.689655 +f1v4s11 f3v7s32 0.689655 +f1v4s11 f3v1s26 0.672414 +f1v4s11 f3v6s31 0.689655 +f1v4s11 f1v13s20 1.000000 +f1v4s11 f2v2s24 0.693878 +f1v4s11 f3v11s36 0.672414 +f1v4s11 f0v5s5 0.672414 +f1v4s11 f1v11s18 0.982759 +f1v4s11 f0v6s6 0.655172 +f1v4s11 f3v4s29 0.689655 +f1v4s11 f0v2s2 0.672414 +f1v4s11 f3v12s37 0.689655 +f1v4s11 f0v3s3 0.672414 +f1v4s11 f3v14s39 0.672414 +f1v4s11 f0v0s0 0.672414 +f1v4s11 f1v12s19 0.982456 +f1v9s16 f1v10s17 0.916667 +f1v9s16 f1v0s7 0.982759 +f1v9s16 f3v9s34 0.650000 +f1v9s16 f4v0s40 0.600000 +f1v9s16 f1v7s14 0.982759 +f1v9s16 f3v2s27 0.650000 +f1v9s16 f4v1s41 0.600000 +f1v9s16 f2v0s22 0.666667 +f1v9s16 f1v2s9 0.650000 +f1v9s16 f1v3s10 0.982759 +f1v9s16 f3v0s25 0.650000 +f1v9s16 f3v3s28 0.650000 +f1v9s16 f3v13s38 0.666667 +f1v9s16 f3v7s32 0.650000 +f1v9s16 f3v1s26 0.633333 +f1v9s16 f3v6s31 0.650000 +f1v9s16 f1v13s20 0.982143 +f1v9s16 f2v2s24 0.714286 +f1v9s16 f3v11s36 0.650000 +f1v9s16 f0v5s5 0.666667 +f1v9s16 f1v11s18 0.965517 +f1v9s16 f0v6s6 0.650000 +f1v9s16 f3v4s29 0.666667 +f1v9s16 f0v2s2 0.666667 +f1v9s16 f3v12s37 0.666667 +f1v9s16 f0v3s3 0.683333 +f1v9s16 f3v14s39 0.650000 +f1v9s16 f0v0s0 0.666667 +f1v9s16 f1v12s19 0.964912 +f1v10s17 f1v0s7 0.965517 +f1v10s17 f3v9s34 0.683333 +f1v10s17 f4v0s40 0.583333 +f1v10s17 f1v7s14 0.965517 +f1v10s17 f3v2s27 0.666667 +f1v10s17 f4v1s41 0.600000 +f1v10s17 f2v0s22 0.650000 +f1v10s17 f1v2s9 0.650000 +f1v10s17 f1v3s10 0.965517 +f1v10s17 f3v0s25 0.666667 +f1v10s17 f3v3s28 0.666667 +f1v10s17 f3v13s38 0.683333 +f1v10s17 f3v7s32 0.683333 +f1v10s17 f3v1s26 0.650000 +f1v10s17 f3v6s31 0.683333 +f1v10s17 f1v13s20 0.964286 +f1v10s17 f2v2s24 0.673469 +f1v10s17 f3v11s36 0.683333 +f1v10s17 f0v5s5 0.650000 +f1v10s17 f1v11s18 0.948276 +f1v10s17 f0v6s6 0.650000 +f1v10s17 f3v4s29 0.666667 +f1v10s17 f0v2s2 0.650000 +f1v10s17 f3v12s37 0.683333 +f1v10s17 f0v3s3 0.666667 +f1v10s17 f3v14s39 0.666667 +f1v10s17 f0v0s0 0.666667 +f1v10s17 f1v12s19 0.964912 +f1v0s7 f3v9s34 0.689655 +f1v0s7 f4v0s40 0.603448 +f1v0s7 f1v7s14 1.000000 +f1v0s7 f3v2s27 0.672414 +f1v0s7 f4v1s41 0.603448 +f1v0s7 f2v0s22 0.672414 +f1v0s7 f1v2s9 0.655172 +f1v0s7 f1v3s10 1.000000 +f1v0s7 f3v0s25 0.672414 +f1v0s7 f3v3s28 0.672414 +f1v0s7 f3v13s38 0.689655 +f1v0s7 f3v7s32 0.689655 +f1v0s7 f3v1s26 0.672414 +f1v0s7 f3v6s31 0.689655 +f1v0s7 f1v13s20 1.000000 +f1v0s7 f2v2s24 0.693878 +f1v0s7 f3v11s36 0.672414 +f1v0s7 f0v5s5 0.672414 +f1v0s7 f1v11s18 0.982759 +f1v0s7 f0v6s6 0.655172 +f1v0s7 f3v4s29 0.689655 +f1v0s7 f0v2s2 0.672414 +f1v0s7 f3v12s37 0.689655 +f1v0s7 f0v3s3 0.672414 +f1v0s7 f3v14s39 0.672414 +f1v0s7 f0v0s0 0.672414 +f1v0s7 f1v12s19 0.982456 +f3v9s34 f4v0s40 0.583333 +f3v9s34 f1v7s14 0.689655 +f3v9s34 f3v2s27 0.969231 +f3v9s34 f4v1s41 0.573770 +f3v9s34 f2v0s22 0.615385 +f3v9s34 f1v2s9 0.615385 +f3v9s34 f1v3s10 0.689655 +f3v9s34 f3v0s25 0.984615 +f3v9s34 f3v3s28 0.969231 +f3v9s34 f3v13s38 0.984615 +f3v9s34 f3v7s32 1.000000 +f3v9s34 f3v1s26 0.953846 +f3v9s34 f3v6s31 1.000000 +f3v9s34 f1v13s20 0.696429 +f3v9s34 f2v2s24 0.693878 +f3v9s34 f3v11s36 0.938462 +f3v9s34 f0v5s5 0.615385 +f3v9s34 f1v11s18 0.689655 +f3v9s34 f0v6s6 0.600000 +f3v9s34 f3v4s29 0.969231 +f3v9s34 f0v2s2 0.615385 +f3v9s34 f3v12s37 0.969231 +f3v9s34 f0v3s3 0.630769 +f3v9s34 f3v14s39 0.984615 +f3v9s34 f0v0s0 0.630769 +f3v9s34 f1v12s19 0.684211 +f4v0s40 f1v7s14 0.603448 +f4v0s40 f3v2s27 0.600000 +f4v0s40 f4v1s41 1.000000 +f4v0s40 f2v0s22 0.650000 +f4v0s40 f1v2s9 0.633333 +f4v0s40 f1v3s10 0.603448 +f4v0s40 f3v0s25 0.600000 +f4v0s40 f3v3s28 0.600000 +f4v0s40 f3v13s38 0.616667 +f4v0s40 f3v7s32 0.583333 +f4v0s40 f3v1s26 0.600000 +f4v0s40 f3v6s31 0.583333 +f4v0s40 f1v13s20 0.607143 +f4v0s40 f2v2s24 0.693878 +f4v0s40 f3v11s36 0.600000 +f4v0s40 f0v5s5 0.633333 +f4v0s40 f1v11s18 0.603448 +f4v0s40 f0v6s6 0.650000 +f4v0s40 f3v4s29 0.633333 +f4v0s40 f0v2s2 0.650000 +f4v0s40 f3v12s37 0.616667 +f4v0s40 f0v3s3 0.616667 +f4v0s40 f3v14s39 0.600000 +f4v0s40 f0v0s0 0.633333 +f4v0s40 f1v12s19 0.578947 +f1v7s14 f3v2s27 0.672414 +f1v7s14 f4v1s41 0.603448 +f1v7s14 f2v0s22 0.672414 +f1v7s14 f1v2s9 0.655172 +f1v7s14 f1v3s10 1.000000 +f1v7s14 f3v0s25 0.672414 +f1v7s14 f3v3s28 0.672414 +f1v7s14 f3v13s38 0.689655 +f1v7s14 f3v7s32 0.689655 +f1v7s14 f3v1s26 0.672414 +f1v7s14 f3v6s31 0.689655 +f1v7s14 f1v13s20 1.000000 +f1v7s14 f2v2s24 0.693878 +f1v7s14 f3v11s36 0.672414 +f1v7s14 f0v5s5 0.672414 +f1v7s14 f1v11s18 0.982759 +f1v7s14 f0v6s6 0.655172 +f1v7s14 f3v4s29 0.689655 +f1v7s14 f0v2s2 0.672414 +f1v7s14 f3v12s37 0.689655 +f1v7s14 f0v3s3 0.672414 +f1v7s14 f3v14s39 0.672414 +f1v7s14 f0v0s0 0.672414 +f1v7s14 f1v12s19 0.982456 +f3v2s27 f4v1s41 0.590164 +f3v2s27 f2v0s22 0.630769 +f3v2s27 f1v2s9 0.615385 +f3v2s27 f1v3s10 0.672414 +f3v2s27 f3v0s25 1.000000 +f3v2s27 f3v3s28 0.984615 +f3v2s27 f3v13s38 1.000000 +f3v2s27 f3v7s32 0.969231 +f3v2s27 f3v1s26 0.969231 +f3v2s27 f3v6s31 0.969231 +f3v2s27 f1v13s20 0.678571 +f3v2s27 f2v2s24 0.714286 +f3v2s27 f3v11s36 0.953846 +f3v2s27 f0v5s5 0.615385 +f3v2s27 f1v11s18 0.672414 +f3v2s27 f0v6s6 0.615385 +f3v2s27 f3v4s29 0.984615 +f3v2s27 f0v2s2 0.630769 +f3v2s27 f3v12s37 0.984615 +f3v2s27 f0v3s3 0.630769 +f3v2s27 f3v14s39 1.000000 +f3v2s27 f0v0s0 0.630769 +f3v2s27 f1v12s19 0.666667 +f4v1s41 f2v0s22 0.639344 +f4v1s41 f1v2s9 0.622951 +f4v1s41 f1v3s10 0.603448 +f4v1s41 f3v0s25 0.590164 +f4v1s41 f3v3s28 0.590164 +f4v1s41 f3v13s38 0.606557 +f4v1s41 f3v7s32 0.573770 +f4v1s41 f3v1s26 0.590164 +f4v1s41 f3v6s31 0.573770 +f4v1s41 f1v13s20 0.607143 +f4v1s41 f2v2s24 0.693878 +f4v1s41 f3v11s36 0.590164 +f4v1s41 f0v5s5 0.622951 +f4v1s41 f1v11s18 0.603448 +f4v1s41 f0v6s6 0.639344 +f4v1s41 f3v4s29 0.622951 +f4v1s41 f0v2s2 0.639344 +f4v1s41 f3v12s37 0.606557 +f4v1s41 f0v3s3 0.606557 +f4v1s41 f3v14s39 0.590164 +f4v1s41 f0v0s0 0.622951 +f4v1s41 f1v12s19 0.578947 +f2v0s22 f1v2s9 0.971014 +f2v0s22 f1v3s10 0.672414 +f2v0s22 f3v0s25 0.621212 +f2v0s22 f3v3s28 0.621212 +f2v0s22 f3v13s38 0.602941 +f2v0s22 f3v7s32 0.615385 +f2v0s22 f3v1s26 0.615385 +f2v0s22 f3v6s31 0.615385 +f2v0s22 f1v13s20 0.678571 +f2v0s22 f2v2s24 0.714286 +f2v0s22 f3v11s36 0.584615 +f2v0s22 f0v5s5 0.971014 +f2v0s22 f1v11s18 0.672414 +f2v0s22 f0v6s6 0.927536 +f2v0s22 f3v4s29 0.632353 +f2v0s22 f0v2s2 1.000000 +f2v0s22 f3v12s37 0.626866 +f2v0s22 f0v3s3 0.970149 +f2v0s22 f3v14s39 0.621212 +f2v0s22 f0v0s0 0.985507 +f2v0s22 f1v12s19 0.684211 +f1v2s9 f1v3s10 0.655172 +f1v2s9 f3v0s25 0.606061 +f1v2s9 f3v3s28 0.606061 +f1v2s9 f3v13s38 0.588235 +f1v2s9 f3v7s32 0.615385 +f1v2s9 f3v1s26 0.600000 +f1v2s9 f3v6s31 0.615385 +f1v2s9 f1v13s20 0.660714 +f1v2s9 f2v2s24 0.714286 +f1v2s9 f3v11s36 0.569231 +f1v2s9 f0v5s5 0.971014 +f1v2s9 f1v11s18 0.655172 +f1v2s9 f0v6s6 0.927536 +f1v2s9 f3v4s29 0.617647 +f1v2s9 f0v2s2 0.971014 +f1v2s9 f3v12s37 0.597015 +f1v2s9 f0v3s3 0.970149 +f1v2s9 f3v14s39 0.606061 +f1v2s9 f0v0s0 0.985507 +f1v2s9 f1v12s19 0.666667 +f1v3s10 f3v0s25 0.672414 +f1v3s10 f3v3s28 0.672414 +f1v3s10 f3v13s38 0.689655 +f1v3s10 f3v7s32 0.689655 +f1v3s10 f3v1s26 0.672414 +f1v3s10 f3v6s31 0.689655 +f1v3s10 f1v13s20 1.000000 +f1v3s10 f2v2s24 0.693878 +f1v3s10 f3v11s36 0.672414 +f1v3s10 f0v5s5 0.672414 +f1v3s10 f1v11s18 0.982759 +f1v3s10 f0v6s6 0.655172 +f1v3s10 f3v4s29 0.689655 +f1v3s10 f0v2s2 0.672414 +f1v3s10 f3v12s37 0.689655 +f1v3s10 f0v3s3 0.672414 +f1v3s10 f3v14s39 0.672414 +f1v3s10 f0v0s0 0.672414 +f1v3s10 f1v12s19 0.982456 +f3v0s25 f3v3s28 0.984848 +f3v0s25 f3v13s38 1.000000 +f3v0s25 f3v7s32 0.984615 +f3v0s25 f3v1s26 0.984615 +f3v0s25 f3v6s31 0.984615 +f3v0s25 f1v13s20 0.678571 +f3v0s25 f2v2s24 0.714286 +f3v0s25 f3v11s36 0.969231 +f3v0s25 f0v5s5 0.606061 +f3v0s25 f1v11s18 0.672414 +f3v0s25 f0v6s6 0.606061 +f3v0s25 f3v4s29 0.984848 +f3v0s25 f0v2s2 0.621212 +f3v0s25 f3v12s37 0.984848 +f3v0s25 f0v3s3 0.621212 +f3v0s25 f3v14s39 1.000000 +f3v0s25 f0v0s0 0.621212 +f3v0s25 f1v12s19 0.666667 +f3v3s28 f3v13s38 0.984848 +f3v3s28 f3v7s32 0.969231 +f3v3s28 f3v1s26 0.969231 +f3v3s28 f3v6s31 0.969231 +f3v3s28 f1v13s20 0.678571 +f3v3s28 f2v2s24 0.734694 +f3v3s28 f3v11s36 0.953846 +f3v3s28 f0v5s5 0.606061 +f3v3s28 f1v11s18 0.672414 +f3v3s28 f0v6s6 0.606061 +f3v3s28 f3v4s29 0.969697 +f3v3s28 f0v2s2 0.621212 +f3v3s28 f3v12s37 0.969697 +f3v3s28 f0v3s3 0.621212 +f3v3s28 f3v14s39 0.984848 +f3v3s28 f0v0s0 0.621212 +f3v3s28 f1v12s19 0.666667 +f3v13s38 f3v7s32 0.984615 +f3v13s38 f3v1s26 0.984615 +f3v13s38 f3v6s31 0.984615 +f3v13s38 f1v13s20 0.696429 +f3v13s38 f2v2s24 0.734694 +f3v13s38 f3v11s36 0.969231 +f3v13s38 f0v5s5 0.588235 +f3v13s38 f1v11s18 0.689655 +f3v13s38 f0v6s6 0.588235 +f3v13s38 f3v4s29 0.955882 +f3v13s38 f0v2s2 0.602941 +f3v13s38 f3v12s37 0.970149 +f3v13s38 f0v3s3 0.611940 +f3v13s38 f3v14s39 1.000000 +f3v13s38 f0v0s0 0.602941 +f3v13s38 f1v12s19 0.684211 +f3v7s32 f3v1s26 0.953846 +f3v7s32 f3v6s31 1.000000 +f3v7s32 f1v13s20 0.696429 +f3v7s32 f2v2s24 0.693878 +f3v7s32 f3v11s36 0.938462 +f3v7s32 f0v5s5 0.615385 +f3v7s32 f1v11s18 0.689655 +f3v7s32 f0v6s6 0.600000 +f3v7s32 f3v4s29 0.969231 +f3v7s32 f0v2s2 0.615385 +f3v7s32 f3v12s37 0.969231 +f3v7s32 f0v3s3 0.630769 +f3v7s32 f3v14s39 0.984615 +f3v7s32 f0v0s0 0.630769 +f3v7s32 f1v12s19 0.684211 +f3v1s26 f3v6s31 0.953846 +f3v1s26 f1v13s20 0.678571 +f3v1s26 f2v2s24 0.714286 +f3v1s26 f3v11s36 0.938462 +f3v1s26 f0v5s5 0.600000 +f3v1s26 f1v11s18 0.672414 +f3v1s26 f0v6s6 0.600000 +f3v1s26 f3v4s29 0.969231 +f3v1s26 f0v2s2 0.615385 +f3v1s26 f3v12s37 0.969231 +f3v1s26 f0v3s3 0.615385 +f3v1s26 f3v14s39 0.984615 +f3v1s26 f0v0s0 0.615385 +f3v1s26 f1v12s19 0.666667 +f3v6s31 f1v13s20 0.696429 +f3v6s31 f2v2s24 0.693878 +f3v6s31 f3v11s36 0.938462 +f3v6s31 f0v5s5 0.615385 +f3v6s31 f1v11s18 0.689655 +f3v6s31 f0v6s6 0.600000 +f3v6s31 f3v4s29 0.969231 +f3v6s31 f0v2s2 0.615385 +f3v6s31 f3v12s37 0.969231 +f3v6s31 f0v3s3 0.630769 +f3v6s31 f3v14s39 0.984615 +f3v6s31 f0v0s0 0.630769 +f3v6s31 f1v12s19 0.684211 +f1v13s20 f2v2s24 0.693878 +f1v13s20 f3v11s36 0.678571 +f1v13s20 f0v5s5 0.696429 +f1v13s20 f1v11s18 0.982143 +f1v13s20 f0v6s6 0.660714 +f1v13s20 f3v4s29 0.696429 +f1v13s20 f0v2s2 0.678571 +f1v13s20 f3v12s37 0.696429 +f1v13s20 f0v3s3 0.678571 +f1v13s20 f3v14s39 0.678571 +f1v13s20 f0v0s0 0.678571 +f1v13s20 f1v12s19 0.964286 +f2v2s24 f3v11s36 0.693878 +f2v2s24 f0v5s5 0.714286 +f2v2s24 f1v11s18 0.693878 +f2v2s24 f0v6s6 0.714286 +f2v2s24 f3v4s29 0.734694 +f2v2s24 f0v2s2 0.714286 +f2v2s24 f3v12s37 0.734694 +f2v2s24 f0v3s3 0.693878 +f2v2s24 f3v14s39 0.714286 +f2v2s24 f0v0s0 0.714286 +f2v2s24 f1v12s19 0.673469 +f3v11s36 f0v5s5 0.584615 +f3v11s36 f1v11s18 0.672414 +f3v11s36 f0v6s6 0.584615 +f3v11s36 f3v4s29 0.953846 +f3v11s36 f0v2s2 0.584615 +f3v11s36 f3v12s37 0.953846 +f3v11s36 f0v3s3 0.584615 +f3v11s36 f3v14s39 0.969231 +f3v11s36 f0v0s0 0.584615 +f3v11s36 f1v12s19 0.666667 +f0v5s5 f1v11s18 0.689655 +f0v5s5 f0v6s6 0.927536 +f0v5s5 f3v4s29 0.617647 +f0v5s5 f0v2s2 0.971014 +f0v5s5 f3v12s37 0.597015 +f0v5s5 f0v3s3 0.970149 +f0v5s5 f3v14s39 0.606061 +f0v5s5 f0v0s0 0.985507 +f0v5s5 f1v12s19 0.666667 +f1v11s18 f0v6s6 0.655172 +f1v11s18 f3v4s29 0.689655 +f1v11s18 f0v2s2 0.672414 +f1v11s18 f3v12s37 0.689655 +f1v11s18 f0v3s3 0.672414 +f1v11s18 f3v14s39 0.672414 +f1v11s18 f0v0s0 0.672414 +f1v11s18 f1v12s19 0.964912 +f0v6s6 f3v4s29 0.617647 +f0v6s6 f0v2s2 0.927536 +f0v6s6 f3v12s37 0.597015 +f0v6s6 f0v3s3 0.925373 +f0v6s6 f3v14s39 0.606061 +f0v6s6 f0v0s0 0.942029 +f0v6s6 f1v12s19 0.666667 +f3v4s29 f0v2s2 0.632353 +f3v4s29 f3v12s37 0.955224 +f3v4s29 f0v3s3 0.641791 +f3v4s29 f3v14s39 0.984848 +f3v4s29 f0v0s0 0.632353 +f3v4s29 f1v12s19 0.684211 +f0v2s2 f3v12s37 0.626866 +f0v2s2 f0v3s3 0.970149 +f0v2s2 f3v14s39 0.621212 +f0v2s2 f0v0s0 0.985507 +f0v2s2 f1v12s19 0.684211 +f3v12s37 f0v3s3 0.611940 +f3v12s37 f3v14s39 0.984848 +f3v12s37 f0v0s0 0.611940 +f3v12s37 f1v12s19 0.684211 +f0v3s3 f3v14s39 0.621212 +f0v3s3 f0v0s0 0.985075 +f0v3s3 f1v12s19 0.666667 +f3v14s39 f0v0s0 0.621212 +f3v14s39 f1v12s19 0.666667 +f0v0s0 f1v12s19 0.684211 diff --git a/pkg/obitools/obialign/testdata/one_default.expected b/pkg/obitools/obialign/testdata/one_default.expected new file mode 100644 index 0000000..d4e3979 --- /dev/null +++ b/pkg/obitools/obialign/testdata/one_default.expected @@ -0,0 +1,224 @@ +f3v10s35 f3v8s33 0.910448 87579 88635 65 66 +f3v10s35 f3v9s34 0.910448 87579 40677 65 65 +f3v10s35 f3v2s27 0.910448 87579 74030 65 65 +f3v10s35 f3v0s25 0.925373 87579 62755 65 66 +f3v10s35 f3v7s32 0.910448 87579 53710 65 65 +f3v10s35 f3v1s26 0.910448 87579 41283 65 65 +f3v10s35 f3v6s31 0.910448 87579 94783 65 65 +f3v10s35 f3v14s39 0.925373 87579 55015 65 66 +f3v5s30 f3v8s33 0.924242 39802 88635 63 66 +f3v5s30 f3v9s34 0.909091 39802 40677 63 65 +f3v5s30 f3v2s27 0.924242 39802 74030 63 65 +f3v5s30 f3v0s25 0.939394 39802 62755 63 66 +f3v5s30 f3v3s28 0.910448 39802 66465 63 66 +f3v5s30 f3v13s38 0.911765 39802 26948 63 68 +f3v5s30 f3v7s32 0.909091 39802 53710 63 65 +f3v5s30 f3v1s26 0.910448 39802 41283 63 65 +f3v5s30 f3v6s31 0.909091 39802 94783 63 65 +f3v5s30 f3v12s37 0.910448 39802 74202 63 67 +f3v5s30 f3v14s39 0.939394 39802 55015 63 66 +f1v8s15 f2v1s23 0.982759 66109 35696 57 58 +f1v8s15 f1v6s13 0.947368 66109 80246 57 56 +f1v8s15 f1v1s8 0.916667 66109 17651 57 60 +f1v8s15 f1v14s21 0.931034 66109 85030 57 56 +f1v8s15 f1v5s12 0.916667 66109 1410 57 60 +f1v8s15 f1v4s11 0.982759 66109 74527 57 58 +f1v8s15 f1v9s16 0.933333 66109 65777 57 60 +f1v8s15 f1v10s17 0.916667 66109 98659 57 60 +f1v8s15 f1v0s7 0.982759 66109 84420 57 58 +f1v8s15 f1v7s14 0.982759 66109 42277 57 58 +f1v8s15 f1v3s10 0.982759 66109 43230 57 58 +f1v8s15 f1v13s20 0.948276 66109 80913 57 56 +f1v8s15 f1v11s18 0.965517 66109 51814 57 58 +f1v8s15 f1v12s19 0.948276 66109 58178 57 57 +f2v1s23 f1v6s13 0.931034 35696 80246 58 56 +f2v1s23 f1v1s8 0.933333 35696 17651 58 60 +f2v1s23 f1v14s21 0.948276 35696 85030 58 56 +f2v1s23 f1v5s12 0.933333 35696 1410 58 60 +f2v1s23 f1v4s11 1.000000 35696 74527 58 58 +f2v1s23 f1v9s16 0.950000 35696 65777 58 60 +f2v1s23 f1v10s17 0.933333 35696 98659 58 60 +f2v1s23 f1v0s7 1.000000 35696 84420 58 58 +f2v1s23 f1v7s14 1.000000 35696 42277 58 58 +f2v1s23 f1v3s10 1.000000 35696 43230 58 58 +f2v1s23 f1v13s20 0.965517 35696 80913 58 56 +f2v1s23 f1v11s18 0.982759 35696 51814 58 58 +f2v1s23 f1v12s19 0.965517 35696 58178 58 57 +f4v2s42 f4v0s40 0.967213 17302 75843 61 60 +f4v2s42 f4v1s41 0.951613 17302 45686 61 61 +f0v4s4 f0v1s1 1.000000 86916 43883 69 69 +f0v4s4 f2v0s22 0.943662 86916 43219 69 69 +f0v4s4 f1v2s9 1.000000 86916 99108 69 69 +f0v4s4 f0v5s5 0.943662 86916 2668 69 69 +f0v4s4 f0v6s6 0.901408 86916 16273 69 69 +f0v4s4 f0v2s2 0.943662 86916 10896 69 69 +f0v4s4 f0v3s3 0.915493 86916 73040 69 67 +f0v4s4 f0v0s0 0.971429 86916 1332 69 69 +f1v6s13 f1v4s11 0.931034 80246 74527 56 58 +f1v6s13 f1v0s7 0.931034 80246 84420 56 58 +f1v6s13 f1v7s14 0.931034 80246 42277 56 58 +f1v6s13 f1v3s10 0.931034 80246 43230 56 58 +f1v6s13 f1v11s18 0.913793 80246 51814 56 58 +f3v8s33 f3v9s34 0.954545 88635 40677 66 65 +f3v8s33 f3v2s27 0.969697 88635 74030 66 65 +f3v8s33 f3v0s25 0.984848 88635 62755 66 66 +f3v8s33 f3v3s28 0.955224 88635 66465 66 66 +f3v8s33 f3v13s38 0.955882 88635 26948 66 68 +f3v8s33 f3v7s32 0.954545 88635 53710 66 65 +f3v8s33 f3v1s26 0.954545 88635 41283 66 65 +f3v8s33 f3v6s31 0.954545 88635 94783 66 65 +f3v8s33 f3v11s36 0.925373 88635 91927 66 65 +f3v8s33 f3v4s29 0.941176 88635 94356 66 68 +f3v8s33 f3v12s37 0.955224 88635 74202 66 67 +f3v8s33 f3v14s39 0.984848 88635 55015 66 66 +f1v1s8 f1v14s21 0.916667 17651 85030 60 56 +f1v1s8 f1v4s11 0.933333 17651 74527 60 58 +f1v1s8 f1v0s7 0.933333 17651 84420 60 58 +f1v1s8 f1v7s14 0.933333 17651 42277 60 58 +f1v1s8 f1v3s10 0.933333 17651 43230 60 58 +f1v1s8 f1v13s20 0.900000 17651 80913 60 56 +f1v1s8 f1v11s18 0.916667 17651 51814 60 58 +f1v1s8 f1v12s19 0.916667 17651 58178 60 57 +f1v14s21 f1v4s11 0.948276 85030 74527 56 58 +f1v14s21 f1v9s16 0.900000 85030 65777 56 60 +f1v14s21 f1v0s7 0.948276 85030 84420 56 58 +f1v14s21 f1v7s14 0.948276 85030 42277 56 58 +f1v14s21 f1v3s10 0.948276 85030 43230 56 58 +f1v14s21 f1v13s20 0.913793 85030 80913 56 56 +f1v14s21 f1v11s18 0.931034 85030 51814 56 58 +f1v14s21 f1v12s19 0.913793 85030 58178 56 57 +f0v1s1 f2v0s22 0.943662 43883 43219 69 69 +f0v1s1 f1v2s9 1.000000 43883 99108 69 69 +f0v1s1 f0v5s5 0.943662 43883 2668 69 69 +f0v1s1 f0v6s6 0.901408 43883 16273 69 69 +f0v1s1 f0v2s2 0.943662 43883 10896 69 69 +f0v1s1 f0v3s3 0.915493 43883 73040 69 67 +f0v1s1 f0v0s0 0.971429 43883 1332 69 69 +f1v5s12 f1v4s11 0.933333 1410 74527 60 58 +f1v5s12 f1v0s7 0.933333 1410 84420 60 58 +f1v5s12 f1v7s14 0.933333 1410 42277 60 58 +f1v5s12 f1v3s10 0.933333 1410 43230 60 58 +f1v5s12 f1v13s20 0.900000 1410 80913 60 56 +f1v5s12 f1v11s18 0.916667 1410 51814 60 58 +f1v5s12 f1v12s19 0.916667 1410 58178 60 57 +f1v4s11 f1v9s16 0.950000 74527 65777 58 60 +f1v4s11 f1v10s17 0.933333 74527 98659 58 60 +f1v4s11 f1v0s7 1.000000 74527 84420 58 58 +f1v4s11 f1v7s14 1.000000 74527 42277 58 58 +f1v4s11 f1v3s10 1.000000 74527 43230 58 58 +f1v4s11 f1v13s20 0.965517 74527 80913 58 56 +f1v4s11 f1v11s18 0.982759 74527 51814 58 58 +f1v4s11 f1v12s19 0.965517 74527 58178 58 57 +f1v9s16 f1v0s7 0.950000 65777 84420 60 58 +f1v9s16 f1v7s14 0.950000 65777 42277 60 58 +f1v9s16 f1v3s10 0.950000 65777 43230 60 58 +f1v9s16 f1v13s20 0.916667 65777 80913 60 56 +f1v9s16 f1v11s18 0.933333 65777 51814 60 58 +f1v9s16 f1v12s19 0.916667 65777 58178 60 57 +f1v10s17 f1v0s7 0.933333 98659 84420 60 58 +f1v10s17 f1v7s14 0.933333 98659 42277 60 58 +f1v10s17 f1v3s10 0.933333 98659 43230 60 58 +f1v10s17 f1v13s20 0.900000 98659 80913 60 56 +f1v10s17 f1v11s18 0.916667 98659 51814 60 58 +f1v10s17 f1v12s19 0.916667 98659 58178 60 57 +f1v0s7 f1v7s14 1.000000 84420 42277 58 58 +f1v0s7 f1v3s10 1.000000 84420 43230 58 58 +f1v0s7 f1v13s20 0.965517 84420 80913 58 56 +f1v0s7 f1v11s18 0.982759 84420 51814 58 58 +f1v0s7 f1v12s19 0.965517 84420 58178 58 57 +f3v9s34 f3v2s27 0.954545 40677 74030 65 65 +f3v9s34 f3v0s25 0.969697 40677 62755 65 66 +f3v9s34 f3v3s28 0.940299 40677 66465 65 66 +f3v9s34 f3v13s38 0.941176 40677 26948 65 68 +f3v9s34 f3v7s32 1.000000 40677 53710 65 65 +f3v9s34 f3v1s26 0.939394 40677 41283 65 65 +f3v9s34 f3v6s31 1.000000 40677 94783 65 65 +f3v9s34 f3v11s36 0.910448 40677 91927 65 65 +f3v9s34 f3v4s29 0.926471 40677 94356 65 68 +f3v9s34 f3v12s37 0.940299 40677 74202 65 67 +f3v9s34 f3v14s39 0.969697 40677 55015 65 66 +f4v0s40 f4v1s41 0.983607 75843 45686 60 61 +f1v7s14 f1v3s10 1.000000 42277 43230 58 58 +f1v7s14 f1v13s20 0.965517 42277 80913 58 56 +f1v7s14 f1v11s18 0.982759 42277 51814 58 58 +f1v7s14 f1v12s19 0.965517 42277 58178 58 57 +f3v2s27 f3v0s25 0.984848 74030 62755 65 66 +f3v2s27 f3v3s28 0.955224 74030 66465 65 66 +f3v2s27 f3v13s38 0.955882 74030 26948 65 68 +f3v2s27 f3v7s32 0.954545 74030 53710 65 65 +f3v2s27 f3v1s26 0.969231 74030 41283 65 65 +f3v2s27 f3v6s31 0.954545 74030 94783 65 65 +f3v2s27 f3v11s36 0.925373 74030 91927 65 65 +f3v2s27 f3v4s29 0.941176 74030 94356 65 68 +f3v2s27 f3v12s37 0.955224 74030 74202 65 67 +f3v2s27 f3v14s39 0.984848 74030 55015 65 66 +f2v0s22 f1v2s9 0.943662 43219 99108 69 69 +f2v0s22 f0v5s5 0.943662 43219 2668 69 69 +f2v0s22 f0v6s6 0.901408 43219 16273 69 69 +f2v0s22 f0v2s2 1.000000 43219 10896 69 69 +f2v0s22 f0v3s3 0.915493 43219 73040 69 67 +f2v0s22 f0v0s0 0.971429 43219 1332 69 69 +f1v2s9 f0v5s5 0.943662 99108 2668 69 69 +f1v2s9 f0v6s6 0.901408 99108 16273 69 69 +f1v2s9 f0v2s2 0.943662 99108 10896 69 69 +f1v2s9 f0v3s3 0.915493 99108 73040 69 67 +f1v2s9 f0v0s0 0.971429 99108 1332 69 69 +f1v3s10 f1v13s20 0.965517 43230 80913 58 56 +f1v3s10 f1v11s18 0.982759 43230 51814 58 58 +f1v3s10 f1v12s19 0.965517 43230 58178 58 57 +f3v0s25 f3v3s28 0.970149 62755 66465 66 66 +f3v0s25 f3v13s38 0.970588 62755 26948 66 68 +f3v0s25 f3v7s32 0.969697 62755 53710 66 65 +f3v0s25 f3v1s26 0.969697 62755 41283 66 65 +f3v0s25 f3v6s31 0.969697 62755 94783 66 65 +f3v0s25 f3v11s36 0.940299 62755 91927 66 65 +f3v0s25 f3v4s29 0.955882 62755 94356 66 68 +f3v0s25 f3v12s37 0.970149 62755 74202 66 67 +f3v0s25 f3v14s39 1.000000 62755 55015 66 66 +f3v3s28 f3v13s38 0.955882 66465 26948 66 68 +f3v3s28 f3v7s32 0.940299 66465 53710 66 65 +f3v3s28 f3v1s26 0.940299 66465 41283 66 65 +f3v3s28 f3v6s31 0.940299 66465 94783 66 65 +f3v3s28 f3v11s36 0.911765 66465 91927 66 65 +f3v3s28 f3v4s29 0.941176 66465 94356 66 68 +f3v3s28 f3v12s37 0.941176 66465 74202 66 67 +f3v3s28 f3v14s39 0.970149 66465 55015 66 66 +f3v13s38 f3v7s32 0.941176 26948 53710 68 65 +f3v13s38 f3v1s26 0.941176 26948 41283 68 65 +f3v13s38 f3v6s31 0.941176 26948 94783 68 65 +f3v13s38 f3v11s36 0.913043 26948 91927 68 65 +f3v13s38 f3v4s29 0.942029 26948 94356 68 68 +f3v13s38 f3v12s37 0.942029 26948 74202 68 67 +f3v13s38 f3v14s39 0.970588 26948 55015 68 66 +f3v7s32 f3v1s26 0.939394 53710 41283 65 65 +f3v7s32 f3v6s31 1.000000 53710 94783 65 65 +f3v7s32 f3v11s36 0.910448 53710 91927 65 65 +f3v7s32 f3v4s29 0.926471 53710 94356 65 68 +f3v7s32 f3v12s37 0.940299 53710 74202 65 67 +f3v7s32 f3v14s39 0.969697 53710 55015 65 66 +f3v1s26 f3v6s31 0.939394 41283 94783 65 65 +f3v1s26 f3v11s36 0.910448 41283 91927 65 65 +f3v1s26 f3v4s29 0.926471 41283 94356 65 68 +f3v1s26 f3v12s37 0.940299 41283 74202 65 67 +f3v1s26 f3v14s39 0.969697 41283 55015 65 66 +f3v6s31 f3v11s36 0.910448 94783 91927 65 65 +f3v6s31 f3v4s29 0.926471 94783 94356 65 68 +f3v6s31 f3v12s37 0.940299 94783 74202 65 67 +f3v6s31 f3v14s39 0.969697 94783 55015 65 66 +f1v13s20 f1v11s18 0.948276 80913 51814 56 58 +f1v13s20 f1v12s19 0.931034 80913 58178 56 57 +f3v11s36 f3v12s37 0.911765 91927 74202 65 67 +f3v11s36 f3v14s39 0.940299 91927 55015 65 66 +f0v5s5 f0v6s6 0.901408 2668 16273 69 69 +f0v5s5 f0v2s2 0.943662 2668 10896 69 69 +f0v5s5 f0v3s3 0.915493 2668 73040 69 67 +f0v5s5 f0v0s0 0.971429 2668 1332 69 69 +f1v11s18 f1v12s19 0.948276 51814 58178 58 57 +f0v6s6 f0v2s2 0.901408 16273 10896 69 69 +f0v6s6 f0v0s0 0.928571 16273 1332 69 69 +f3v4s29 f3v12s37 0.927536 94356 74202 68 67 +f3v4s29 f3v14s39 0.955882 94356 55015 68 66 +f0v2s2 f0v3s3 0.915493 10896 73040 69 67 +f0v2s2 f0v0s0 0.971429 10896 1332 69 69 +f3v12s37 f3v14s39 0.970149 74202 55015 67 66 +f0v3s3 f0v0s0 0.942857 73040 1332 67 69 diff --git a/pkg/obitools/obialign/testdata/one_distance.expected b/pkg/obitools/obialign/testdata/one_distance.expected new file mode 100644 index 0000000..dc8cc1b --- /dev/null +++ b/pkg/obitools/obialign/testdata/one_distance.expected @@ -0,0 +1,224 @@ +f3v10s35 f3v8s33 0.089552 +f3v10s35 f3v9s34 0.089552 +f3v10s35 f3v2s27 0.089552 +f3v10s35 f3v0s25 0.074627 +f3v10s35 f3v7s32 0.089552 +f3v10s35 f3v1s26 0.089552 +f3v10s35 f3v6s31 0.089552 +f3v10s35 f3v14s39 0.074627 +f3v5s30 f3v8s33 0.075758 +f3v5s30 f3v9s34 0.090909 +f3v5s30 f3v2s27 0.075758 +f3v5s30 f3v0s25 0.060606 +f3v5s30 f3v3s28 0.089552 +f3v5s30 f3v13s38 0.088235 +f3v5s30 f3v7s32 0.090909 +f3v5s30 f3v1s26 0.089552 +f3v5s30 f3v6s31 0.090909 +f3v5s30 f3v12s37 0.089552 +f3v5s30 f3v14s39 0.060606 +f1v8s15 f2v1s23 0.017241 +f1v8s15 f1v6s13 0.052632 +f1v8s15 f1v1s8 0.083333 +f1v8s15 f1v14s21 0.068966 +f1v8s15 f1v5s12 0.083333 +f1v8s15 f1v4s11 0.017241 +f1v8s15 f1v9s16 0.066667 +f1v8s15 f1v10s17 0.083333 +f1v8s15 f1v0s7 0.017241 +f1v8s15 f1v7s14 0.017241 +f1v8s15 f1v3s10 0.017241 +f1v8s15 f1v13s20 0.051724 +f1v8s15 f1v11s18 0.034483 +f1v8s15 f1v12s19 0.051724 +f2v1s23 f1v6s13 0.068966 +f2v1s23 f1v1s8 0.066667 +f2v1s23 f1v14s21 0.051724 +f2v1s23 f1v5s12 0.066667 +f2v1s23 f1v4s11 0.000000 +f2v1s23 f1v9s16 0.050000 +f2v1s23 f1v10s17 0.066667 +f2v1s23 f1v0s7 0.000000 +f2v1s23 f1v7s14 0.000000 +f2v1s23 f1v3s10 0.000000 +f2v1s23 f1v13s20 0.034483 +f2v1s23 f1v11s18 0.017241 +f2v1s23 f1v12s19 0.034483 +f4v2s42 f4v0s40 0.032787 +f4v2s42 f4v1s41 0.048387 +f0v4s4 f0v1s1 0.000000 +f0v4s4 f2v0s22 0.056338 +f0v4s4 f1v2s9 0.000000 +f0v4s4 f0v5s5 0.056338 +f0v4s4 f0v6s6 0.098592 +f0v4s4 f0v2s2 0.056338 +f0v4s4 f0v3s3 0.084507 +f0v4s4 f0v0s0 0.028571 +f1v6s13 f1v4s11 0.068966 +f1v6s13 f1v0s7 0.068966 +f1v6s13 f1v7s14 0.068966 +f1v6s13 f1v3s10 0.068966 +f1v6s13 f1v11s18 0.086207 +f3v8s33 f3v9s34 0.045455 +f3v8s33 f3v2s27 0.030303 +f3v8s33 f3v0s25 0.015152 +f3v8s33 f3v3s28 0.044776 +f3v8s33 f3v13s38 0.044118 +f3v8s33 f3v7s32 0.045455 +f3v8s33 f3v1s26 0.045455 +f3v8s33 f3v6s31 0.045455 +f3v8s33 f3v11s36 0.074627 +f3v8s33 f3v4s29 0.058824 +f3v8s33 f3v12s37 0.044776 +f3v8s33 f3v14s39 0.015152 +f1v1s8 f1v14s21 0.083333 +f1v1s8 f1v4s11 0.066667 +f1v1s8 f1v0s7 0.066667 +f1v1s8 f1v7s14 0.066667 +f1v1s8 f1v3s10 0.066667 +f1v1s8 f1v13s20 0.100000 +f1v1s8 f1v11s18 0.083333 +f1v1s8 f1v12s19 0.083333 +f1v14s21 f1v4s11 0.051724 +f1v14s21 f1v9s16 0.100000 +f1v14s21 f1v0s7 0.051724 +f1v14s21 f1v7s14 0.051724 +f1v14s21 f1v3s10 0.051724 +f1v14s21 f1v13s20 0.086207 +f1v14s21 f1v11s18 0.068966 +f1v14s21 f1v12s19 0.086207 +f0v1s1 f2v0s22 0.056338 +f0v1s1 f1v2s9 0.000000 +f0v1s1 f0v5s5 0.056338 +f0v1s1 f0v6s6 0.098592 +f0v1s1 f0v2s2 0.056338 +f0v1s1 f0v3s3 0.084507 +f0v1s1 f0v0s0 0.028571 +f1v5s12 f1v4s11 0.066667 +f1v5s12 f1v0s7 0.066667 +f1v5s12 f1v7s14 0.066667 +f1v5s12 f1v3s10 0.066667 +f1v5s12 f1v13s20 0.100000 +f1v5s12 f1v11s18 0.083333 +f1v5s12 f1v12s19 0.083333 +f1v4s11 f1v9s16 0.050000 +f1v4s11 f1v10s17 0.066667 +f1v4s11 f1v0s7 0.000000 +f1v4s11 f1v7s14 0.000000 +f1v4s11 f1v3s10 0.000000 +f1v4s11 f1v13s20 0.034483 +f1v4s11 f1v11s18 0.017241 +f1v4s11 f1v12s19 0.034483 +f1v9s16 f1v0s7 0.050000 +f1v9s16 f1v7s14 0.050000 +f1v9s16 f1v3s10 0.050000 +f1v9s16 f1v13s20 0.083333 +f1v9s16 f1v11s18 0.066667 +f1v9s16 f1v12s19 0.083333 +f1v10s17 f1v0s7 0.066667 +f1v10s17 f1v7s14 0.066667 +f1v10s17 f1v3s10 0.066667 +f1v10s17 f1v13s20 0.100000 +f1v10s17 f1v11s18 0.083333 +f1v10s17 f1v12s19 0.083333 +f1v0s7 f1v7s14 0.000000 +f1v0s7 f1v3s10 0.000000 +f1v0s7 f1v13s20 0.034483 +f1v0s7 f1v11s18 0.017241 +f1v0s7 f1v12s19 0.034483 +f3v9s34 f3v2s27 0.045455 +f3v9s34 f3v0s25 0.030303 +f3v9s34 f3v3s28 0.059701 +f3v9s34 f3v13s38 0.058824 +f3v9s34 f3v7s32 0.000000 +f3v9s34 f3v1s26 0.060606 +f3v9s34 f3v6s31 0.000000 +f3v9s34 f3v11s36 0.089552 +f3v9s34 f3v4s29 0.073529 +f3v9s34 f3v12s37 0.059701 +f3v9s34 f3v14s39 0.030303 +f4v0s40 f4v1s41 0.016393 +f1v7s14 f1v3s10 0.000000 +f1v7s14 f1v13s20 0.034483 +f1v7s14 f1v11s18 0.017241 +f1v7s14 f1v12s19 0.034483 +f3v2s27 f3v0s25 0.015152 +f3v2s27 f3v3s28 0.044776 +f3v2s27 f3v13s38 0.044118 +f3v2s27 f3v7s32 0.045455 +f3v2s27 f3v1s26 0.030769 +f3v2s27 f3v6s31 0.045455 +f3v2s27 f3v11s36 0.074627 +f3v2s27 f3v4s29 0.058824 +f3v2s27 f3v12s37 0.044776 +f3v2s27 f3v14s39 0.015152 +f2v0s22 f1v2s9 0.056338 +f2v0s22 f0v5s5 0.056338 +f2v0s22 f0v6s6 0.098592 +f2v0s22 f0v2s2 0.000000 +f2v0s22 f0v3s3 0.084507 +f2v0s22 f0v0s0 0.028571 +f1v2s9 f0v5s5 0.056338 +f1v2s9 f0v6s6 0.098592 +f1v2s9 f0v2s2 0.056338 +f1v2s9 f0v3s3 0.084507 +f1v2s9 f0v0s0 0.028571 +f1v3s10 f1v13s20 0.034483 +f1v3s10 f1v11s18 0.017241 +f1v3s10 f1v12s19 0.034483 +f3v0s25 f3v3s28 0.029851 +f3v0s25 f3v13s38 0.029412 +f3v0s25 f3v7s32 0.030303 +f3v0s25 f3v1s26 0.030303 +f3v0s25 f3v6s31 0.030303 +f3v0s25 f3v11s36 0.059701 +f3v0s25 f3v4s29 0.044118 +f3v0s25 f3v12s37 0.029851 +f3v0s25 f3v14s39 0.000000 +f3v3s28 f3v13s38 0.044118 +f3v3s28 f3v7s32 0.059701 +f3v3s28 f3v1s26 0.059701 +f3v3s28 f3v6s31 0.059701 +f3v3s28 f3v11s36 0.088235 +f3v3s28 f3v4s29 0.058824 +f3v3s28 f3v12s37 0.058824 +f3v3s28 f3v14s39 0.029851 +f3v13s38 f3v7s32 0.058824 +f3v13s38 f3v1s26 0.058824 +f3v13s38 f3v6s31 0.058824 +f3v13s38 f3v11s36 0.086957 +f3v13s38 f3v4s29 0.057971 +f3v13s38 f3v12s37 0.057971 +f3v13s38 f3v14s39 0.029412 +f3v7s32 f3v1s26 0.060606 +f3v7s32 f3v6s31 0.000000 +f3v7s32 f3v11s36 0.089552 +f3v7s32 f3v4s29 0.073529 +f3v7s32 f3v12s37 0.059701 +f3v7s32 f3v14s39 0.030303 +f3v1s26 f3v6s31 0.060606 +f3v1s26 f3v11s36 0.089552 +f3v1s26 f3v4s29 0.073529 +f3v1s26 f3v12s37 0.059701 +f3v1s26 f3v14s39 0.030303 +f3v6s31 f3v11s36 0.089552 +f3v6s31 f3v4s29 0.073529 +f3v6s31 f3v12s37 0.059701 +f3v6s31 f3v14s39 0.030303 +f1v13s20 f1v11s18 0.051724 +f1v13s20 f1v12s19 0.068966 +f3v11s36 f3v12s37 0.088235 +f3v11s36 f3v14s39 0.059701 +f0v5s5 f0v6s6 0.098592 +f0v5s5 f0v2s2 0.056338 +f0v5s5 f0v3s3 0.084507 +f0v5s5 f0v0s0 0.028571 +f1v11s18 f1v12s19 0.051724 +f0v6s6 f0v2s2 0.098592 +f0v6s6 f0v0s0 0.071429 +f3v4s29 f3v12s37 0.072464 +f3v4s29 f3v14s39 0.044118 +f0v2s2 f0v3s3 0.084507 +f0v2s2 f0v0s0 0.028571 +f3v12s37 f3v14s39 0.029851 +f0v3s3 f0v0s0 0.057143 diff --git a/pkg/obitools/obialign/testdata/one_longest.expected b/pkg/obitools/obialign/testdata/one_longest.expected new file mode 100644 index 0000000..50b988e --- /dev/null +++ b/pkg/obitools/obialign/testdata/one_longest.expected @@ -0,0 +1,249 @@ +f3v10s35 f3v5s30 0.892308 +f3v10s35 f3v8s33 0.924242 +f3v10s35 f3v9s34 0.938462 +f3v10s35 f3v2s27 0.938462 +f3v10s35 f3v0s25 0.939394 +f3v10s35 f3v3s28 0.924242 +f3v10s35 f3v13s38 0.911765 +f3v10s35 f3v7s32 0.938462 +f3v10s35 f3v1s26 0.938462 +f3v10s35 f3v6s31 0.938462 +f3v10s35 f3v11s36 0.907692 +f3v10s35 f3v4s29 0.897059 +f3v10s35 f3v12s37 0.910448 +f3v10s35 f3v14s39 0.939394 +f3v5s30 f3v8s33 0.924242 +f3v5s30 f3v9s34 0.923077 +f3v5s30 f3v2s27 0.938462 +f3v5s30 f3v0s25 0.939394 +f3v5s30 f3v3s28 0.924242 +f3v5s30 f3v13s38 0.911765 +f3v5s30 f3v7s32 0.923077 +f3v5s30 f3v1s26 0.938462 +f3v5s30 f3v6s31 0.923077 +f3v5s30 f3v11s36 0.907692 +f3v5s30 f3v4s29 0.897059 +f3v5s30 f3v12s37 0.910448 +f3v5s30 f3v14s39 0.939394 +f1v8s15 f2v1s23 0.982759 +f1v8s15 f1v6s13 0.947368 +f1v8s15 f1v1s8 0.916667 +f1v8s15 f1v14s21 0.947368 +f1v8s15 f1v5s12 0.916667 +f1v8s15 f1v4s11 0.982759 +f1v8s15 f1v9s16 0.933333 +f1v8s15 f1v10s17 0.916667 +f1v8s15 f1v0s7 0.982759 +f1v8s15 f1v7s14 0.982759 +f1v8s15 f1v3s10 0.982759 +f1v8s15 f1v13s20 0.964912 +f1v8s15 f1v11s18 0.965517 +f1v8s15 f1v12s19 0.964912 +f2v1s23 f1v6s13 0.931034 +f2v1s23 f1v1s8 0.933333 +f2v1s23 f1v14s21 0.948276 +f2v1s23 f1v5s12 0.933333 +f2v1s23 f1v4s11 1.000000 +f2v1s23 f1v9s16 0.950000 +f2v1s23 f1v10s17 0.933333 +f2v1s23 f1v0s7 1.000000 +f2v1s23 f1v7s14 1.000000 +f2v1s23 f1v3s10 1.000000 +f2v1s23 f1v13s20 0.965517 +f2v1s23 f1v11s18 0.982759 +f2v1s23 f1v12s19 0.965517 +f4v2s42 f4v0s40 0.967213 +f4v2s42 f4v1s41 0.967213 +f0v4s4 f0v1s1 1.000000 +f0v4s4 f2v0s22 0.971014 +f0v4s4 f1v2s9 1.000000 +f0v4s4 f0v5s5 0.971014 +f0v4s4 f0v6s6 0.927536 +f0v4s4 f0v2s2 0.971014 +f0v4s4 f0v3s3 0.942029 +f0v4s4 f0v0s0 0.985507 +f1v6s13 f1v1s8 0.866667 +f1v6s13 f1v14s21 0.910714 +f1v6s13 f1v5s12 0.866667 +f1v6s13 f1v4s11 0.931034 +f1v6s13 f1v9s16 0.883333 +f1v6s13 f1v10s17 0.883333 +f1v6s13 f1v0s7 0.931034 +f1v6s13 f1v7s14 0.931034 +f1v6s13 f1v3s10 0.931034 +f1v6s13 f1v13s20 0.928571 +f1v6s13 f1v11s18 0.913793 +f1v6s13 f1v12s19 0.912281 +f3v8s33 f3v9s34 0.954545 +f3v8s33 f3v2s27 0.969697 +f3v8s33 f3v0s25 0.984848 +f3v8s33 f3v3s28 0.969697 +f3v8s33 f3v13s38 0.955882 +f3v8s33 f3v7s32 0.954545 +f3v8s33 f3v1s26 0.954545 +f3v8s33 f3v6s31 0.954545 +f3v8s33 f3v11s36 0.939394 +f3v8s33 f3v4s29 0.941176 +f3v8s33 f3v12s37 0.955224 +f3v8s33 f3v14s39 0.984848 +f1v1s8 f1v14s21 0.916667 +f1v1s8 f1v5s12 0.900000 +f1v1s8 f1v4s11 0.933333 +f1v1s8 f1v9s16 0.916667 +f1v1s8 f1v10s17 0.900000 +f1v1s8 f1v0s7 0.933333 +f1v1s8 f1v7s14 0.933333 +f1v1s8 f1v3s10 0.933333 +f1v1s8 f1v13s20 0.900000 +f1v1s8 f1v11s18 0.916667 +f1v1s8 f1v12s19 0.916667 +f1v14s21 f1v5s12 0.883333 +f1v14s21 f1v4s11 0.948276 +f1v14s21 f1v9s16 0.900000 +f1v14s21 f1v10s17 0.883333 +f1v14s21 f1v0s7 0.948276 +f1v14s21 f1v7s14 0.948276 +f1v14s21 f1v3s10 0.948276 +f1v14s21 f1v13s20 0.946429 +f1v14s21 f1v11s18 0.931034 +f1v14s21 f1v12s19 0.929825 +f0v1s1 f2v0s22 0.971014 +f0v1s1 f1v2s9 1.000000 +f0v1s1 f0v5s5 0.971014 +f0v1s1 f0v6s6 0.927536 +f0v1s1 f0v2s2 0.971014 +f0v1s1 f0v3s3 0.942029 +f0v1s1 f0v0s0 0.985507 +f1v5s12 f1v4s11 0.933333 +f1v5s12 f1v9s16 0.916667 +f1v5s12 f1v10s17 0.916667 +f1v5s12 f1v0s7 0.933333 +f1v5s12 f1v7s14 0.933333 +f1v5s12 f1v3s10 0.933333 +f1v5s12 f1v13s20 0.900000 +f1v5s12 f1v11s18 0.916667 +f1v5s12 f1v12s19 0.916667 +f1v4s11 f1v9s16 0.950000 +f1v4s11 f1v10s17 0.933333 +f1v4s11 f1v0s7 1.000000 +f1v4s11 f1v7s14 1.000000 +f1v4s11 f1v3s10 1.000000 +f1v4s11 f1v13s20 0.965517 +f1v4s11 f1v11s18 0.982759 +f1v4s11 f1v12s19 0.965517 +f1v9s16 f1v10s17 0.916667 +f1v9s16 f1v0s7 0.950000 +f1v9s16 f1v7s14 0.950000 +f1v9s16 f1v3s10 0.950000 +f1v9s16 f1v13s20 0.916667 +f1v9s16 f1v11s18 0.933333 +f1v9s16 f1v12s19 0.916667 +f1v10s17 f1v0s7 0.933333 +f1v10s17 f1v7s14 0.933333 +f1v10s17 f1v3s10 0.933333 +f1v10s17 f1v13s20 0.900000 +f1v10s17 f1v11s18 0.916667 +f1v10s17 f1v12s19 0.916667 +f1v0s7 f1v7s14 1.000000 +f1v0s7 f1v3s10 1.000000 +f1v0s7 f1v13s20 0.965517 +f1v0s7 f1v11s18 0.982759 +f1v0s7 f1v12s19 0.965517 +f3v9s34 f3v2s27 0.969231 +f3v9s34 f3v0s25 0.969697 +f3v9s34 f3v3s28 0.954545 +f3v9s34 f3v13s38 0.941176 +f3v9s34 f3v7s32 1.000000 +f3v9s34 f3v1s26 0.953846 +f3v9s34 f3v6s31 1.000000 +f3v9s34 f3v11s36 0.938462 +f3v9s34 f3v4s29 0.926471 +f3v9s34 f3v12s37 0.940299 +f3v9s34 f3v14s39 0.969697 +f4v0s40 f4v1s41 0.983607 +f1v7s14 f1v3s10 1.000000 +f1v7s14 f1v13s20 0.965517 +f1v7s14 f1v11s18 0.982759 +f1v7s14 f1v12s19 0.965517 +f3v2s27 f3v0s25 0.984848 +f3v2s27 f3v3s28 0.969697 +f3v2s27 f3v13s38 0.955882 +f3v2s27 f3v7s32 0.969231 +f3v2s27 f3v1s26 0.969231 +f3v2s27 f3v6s31 0.969231 +f3v2s27 f3v11s36 0.953846 +f3v2s27 f3v4s29 0.941176 +f3v2s27 f3v12s37 0.955224 +f3v2s27 f3v14s39 0.984848 +f2v0s22 f1v2s9 0.971014 +f2v0s22 f0v5s5 0.971014 +f2v0s22 f0v6s6 0.927536 +f2v0s22 f0v2s2 1.000000 +f2v0s22 f0v3s3 0.942029 +f2v0s22 f0v0s0 0.985507 +f1v2s9 f0v5s5 0.971014 +f1v2s9 f0v6s6 0.927536 +f1v2s9 f0v2s2 0.971014 +f1v2s9 f0v3s3 0.942029 +f1v2s9 f0v0s0 0.985507 +f1v3s10 f1v13s20 0.965517 +f1v3s10 f1v11s18 0.982759 +f1v3s10 f1v12s19 0.965517 +f3v0s25 f3v3s28 0.984848 +f3v0s25 f3v13s38 0.970588 +f3v0s25 f3v7s32 0.969697 +f3v0s25 f3v1s26 0.969697 +f3v0s25 f3v6s31 0.969697 +f3v0s25 f3v11s36 0.954545 +f3v0s25 f3v4s29 0.955882 +f3v0s25 f3v12s37 0.970149 +f3v0s25 f3v14s39 1.000000 +f3v3s28 f3v13s38 0.955882 +f3v3s28 f3v7s32 0.954545 +f3v3s28 f3v1s26 0.954545 +f3v3s28 f3v6s31 0.954545 +f3v3s28 f3v11s36 0.939394 +f3v3s28 f3v4s29 0.941176 +f3v3s28 f3v12s37 0.955224 +f3v3s28 f3v14s39 0.984848 +f3v13s38 f3v7s32 0.941176 +f3v13s38 f3v1s26 0.941176 +f3v13s38 f3v6s31 0.941176 +f3v13s38 f3v11s36 0.926471 +f3v13s38 f3v4s29 0.955882 +f3v13s38 f3v12s37 0.955882 +f3v13s38 f3v14s39 0.970588 +f3v7s32 f3v1s26 0.953846 +f3v7s32 f3v6s31 1.000000 +f3v7s32 f3v11s36 0.938462 +f3v7s32 f3v4s29 0.926471 +f3v7s32 f3v12s37 0.940299 +f3v7s32 f3v14s39 0.969697 +f3v1s26 f3v6s31 0.953846 +f3v1s26 f3v11s36 0.938462 +f3v1s26 f3v4s29 0.926471 +f3v1s26 f3v12s37 0.940299 +f3v1s26 f3v14s39 0.969697 +f3v6s31 f3v11s36 0.938462 +f3v6s31 f3v4s29 0.926471 +f3v6s31 f3v12s37 0.940299 +f3v6s31 f3v14s39 0.969697 +f1v13s20 f1v11s18 0.948276 +f1v13s20 f1v12s19 0.947368 +f3v11s36 f3v4s29 0.911765 +f3v11s36 f3v12s37 0.925373 +f3v11s36 f3v14s39 0.954545 +f0v5s5 f0v6s6 0.927536 +f0v5s5 f0v2s2 0.971014 +f0v5s5 f0v3s3 0.942029 +f0v5s5 f0v0s0 0.985507 +f1v11s18 f1v12s19 0.948276 +f0v6s6 f0v2s2 0.927536 +f0v6s6 f0v3s3 0.898551 +f0v6s6 f0v0s0 0.942029 +f3v4s29 f3v12s37 0.941176 +f3v4s29 f3v14s39 0.955882 +f0v2s2 f0v3s3 0.942029 +f0v2s2 f0v0s0 0.985507 +f3v12s37 f3v14s39 0.970149 +f0v3s3 f0v0s0 0.956522 diff --git a/pkg/obitools/obialign/testdata/one_low.expected b/pkg/obitools/obialign/testdata/one_low.expected new file mode 100644 index 0000000..ec98d5c --- /dev/null +++ b/pkg/obitools/obialign/testdata/one_low.expected @@ -0,0 +1,249 @@ +f3v10s35 f3v5s30 0.865672 +f3v10s35 f3v8s33 0.910448 +f3v10s35 f3v9s34 0.910448 +f3v10s35 f3v2s27 0.910448 +f3v10s35 f3v0s25 0.925373 +f3v10s35 f3v3s28 0.897059 +f3v10s35 f3v13s38 0.898551 +f3v10s35 f3v7s32 0.910448 +f3v10s35 f3v1s26 0.910448 +f3v10s35 f3v6s31 0.910448 +f3v10s35 f3v11s36 0.867647 +f3v10s35 f3v4s29 0.884058 +f3v10s35 f3v12s37 0.897059 +f3v10s35 f3v14s39 0.925373 +f3v5s30 f3v8s33 0.924242 +f3v5s30 f3v9s34 0.909091 +f3v5s30 f3v2s27 0.924242 +f3v5s30 f3v0s25 0.939394 +f3v5s30 f3v3s28 0.910448 +f3v5s30 f3v13s38 0.911765 +f3v5s30 f3v7s32 0.909091 +f3v5s30 f3v1s26 0.910448 +f3v5s30 f3v6s31 0.909091 +f3v5s30 f3v11s36 0.893939 +f3v5s30 f3v4s29 0.897059 +f3v5s30 f3v12s37 0.910448 +f3v5s30 f3v14s39 0.939394 +f1v8s15 f2v1s23 0.982759 +f1v8s15 f1v6s13 0.947368 +f1v8s15 f1v1s8 0.916667 +f1v8s15 f1v14s21 0.931034 +f1v8s15 f1v5s12 0.916667 +f1v8s15 f1v4s11 0.982759 +f1v8s15 f1v9s16 0.933333 +f1v8s15 f1v10s17 0.916667 +f1v8s15 f1v0s7 0.982759 +f1v8s15 f1v7s14 0.982759 +f1v8s15 f1v3s10 0.982759 +f1v8s15 f1v13s20 0.948276 +f1v8s15 f1v11s18 0.965517 +f1v8s15 f1v12s19 0.948276 +f2v1s23 f1v6s13 0.931034 +f2v1s23 f1v1s8 0.933333 +f2v1s23 f1v14s21 0.948276 +f2v1s23 f1v5s12 0.933333 +f2v1s23 f1v4s11 1.000000 +f2v1s23 f1v9s16 0.950000 +f2v1s23 f1v10s17 0.933333 +f2v1s23 f1v0s7 1.000000 +f2v1s23 f1v7s14 1.000000 +f2v1s23 f1v3s10 1.000000 +f2v1s23 f1v13s20 0.965517 +f2v1s23 f1v11s18 0.982759 +f2v1s23 f1v12s19 0.965517 +f4v2s42 f4v0s40 0.967213 +f4v2s42 f4v1s41 0.951613 +f0v4s4 f0v1s1 1.000000 +f0v4s4 f2v0s22 0.943662 +f0v4s4 f1v2s9 1.000000 +f0v4s4 f0v5s5 0.943662 +f0v4s4 f0v6s6 0.901408 +f0v4s4 f0v2s2 0.943662 +f0v4s4 f0v3s3 0.915493 +f0v4s4 f0v0s0 0.971429 +f1v6s13 f1v1s8 0.866667 +f1v6s13 f1v14s21 0.879310 +f1v6s13 f1v5s12 0.866667 +f1v6s13 f1v4s11 0.931034 +f1v6s13 f1v9s16 0.883333 +f1v6s13 f1v10s17 0.883333 +f1v6s13 f1v0s7 0.931034 +f1v6s13 f1v7s14 0.931034 +f1v6s13 f1v3s10 0.931034 +f1v6s13 f1v13s20 0.896552 +f1v6s13 f1v11s18 0.913793 +f1v6s13 f1v12s19 0.896552 +f3v8s33 f3v9s34 0.954545 +f3v8s33 f3v2s27 0.969697 +f3v8s33 f3v0s25 0.984848 +f3v8s33 f3v3s28 0.955224 +f3v8s33 f3v13s38 0.955882 +f3v8s33 f3v7s32 0.954545 +f3v8s33 f3v1s26 0.954545 +f3v8s33 f3v6s31 0.954545 +f3v8s33 f3v11s36 0.925373 +f3v8s33 f3v4s29 0.941176 +f3v8s33 f3v12s37 0.955224 +f3v8s33 f3v14s39 0.984848 +f1v1s8 f1v14s21 0.916667 +f1v1s8 f1v5s12 0.870968 +f1v1s8 f1v4s11 0.933333 +f1v1s8 f1v9s16 0.887097 +f1v1s8 f1v10s17 0.870968 +f1v1s8 f1v0s7 0.933333 +f1v1s8 f1v7s14 0.933333 +f1v1s8 f1v3s10 0.933333 +f1v1s8 f1v13s20 0.900000 +f1v1s8 f1v11s18 0.916667 +f1v1s8 f1v12s19 0.916667 +f1v14s21 f1v5s12 0.883333 +f1v14s21 f1v4s11 0.948276 +f1v14s21 f1v9s16 0.900000 +f1v14s21 f1v10s17 0.883333 +f1v14s21 f1v0s7 0.948276 +f1v14s21 f1v7s14 0.948276 +f1v14s21 f1v3s10 0.948276 +f1v14s21 f1v13s20 0.913793 +f1v14s21 f1v11s18 0.931034 +f1v14s21 f1v12s19 0.913793 +f0v1s1 f2v0s22 0.943662 +f0v1s1 f1v2s9 1.000000 +f0v1s1 f0v5s5 0.943662 +f0v1s1 f0v6s6 0.901408 +f0v1s1 f0v2s2 0.943662 +f0v1s1 f0v3s3 0.915493 +f0v1s1 f0v0s0 0.971429 +f1v5s12 f1v4s11 0.933333 +f1v5s12 f1v9s16 0.887097 +f1v5s12 f1v10s17 0.887097 +f1v5s12 f1v0s7 0.933333 +f1v5s12 f1v7s14 0.933333 +f1v5s12 f1v3s10 0.933333 +f1v5s12 f1v13s20 0.900000 +f1v5s12 f1v11s18 0.916667 +f1v5s12 f1v12s19 0.916667 +f1v4s11 f1v9s16 0.950000 +f1v4s11 f1v10s17 0.933333 +f1v4s11 f1v0s7 1.000000 +f1v4s11 f1v7s14 1.000000 +f1v4s11 f1v3s10 1.000000 +f1v4s11 f1v13s20 0.965517 +f1v4s11 f1v11s18 0.982759 +f1v4s11 f1v12s19 0.965517 +f1v9s16 f1v10s17 0.887097 +f1v9s16 f1v0s7 0.950000 +f1v9s16 f1v7s14 0.950000 +f1v9s16 f1v3s10 0.950000 +f1v9s16 f1v13s20 0.916667 +f1v9s16 f1v11s18 0.933333 +f1v9s16 f1v12s19 0.916667 +f1v10s17 f1v0s7 0.933333 +f1v10s17 f1v7s14 0.933333 +f1v10s17 f1v3s10 0.933333 +f1v10s17 f1v13s20 0.900000 +f1v10s17 f1v11s18 0.916667 +f1v10s17 f1v12s19 0.916667 +f1v0s7 f1v7s14 1.000000 +f1v0s7 f1v3s10 1.000000 +f1v0s7 f1v13s20 0.965517 +f1v0s7 f1v11s18 0.982759 +f1v0s7 f1v12s19 0.965517 +f3v9s34 f3v2s27 0.954545 +f3v9s34 f3v0s25 0.969697 +f3v9s34 f3v3s28 0.940299 +f3v9s34 f3v13s38 0.941176 +f3v9s34 f3v7s32 1.000000 +f3v9s34 f3v1s26 0.939394 +f3v9s34 f3v6s31 1.000000 +f3v9s34 f3v11s36 0.910448 +f3v9s34 f3v4s29 0.926471 +f3v9s34 f3v12s37 0.940299 +f3v9s34 f3v14s39 0.969697 +f4v0s40 f4v1s41 0.983607 +f1v7s14 f1v3s10 1.000000 +f1v7s14 f1v13s20 0.965517 +f1v7s14 f1v11s18 0.982759 +f1v7s14 f1v12s19 0.965517 +f3v2s27 f3v0s25 0.984848 +f3v2s27 f3v3s28 0.955224 +f3v2s27 f3v13s38 0.955882 +f3v2s27 f3v7s32 0.954545 +f3v2s27 f3v1s26 0.969231 +f3v2s27 f3v6s31 0.954545 +f3v2s27 f3v11s36 0.925373 +f3v2s27 f3v4s29 0.941176 +f3v2s27 f3v12s37 0.955224 +f3v2s27 f3v14s39 0.984848 +f2v0s22 f1v2s9 0.943662 +f2v0s22 f0v5s5 0.943662 +f2v0s22 f0v6s6 0.901408 +f2v0s22 f0v2s2 1.000000 +f2v0s22 f0v3s3 0.915493 +f2v0s22 f0v0s0 0.971429 +f1v2s9 f0v5s5 0.943662 +f1v2s9 f0v6s6 0.901408 +f1v2s9 f0v2s2 0.943662 +f1v2s9 f0v3s3 0.915493 +f1v2s9 f0v0s0 0.971429 +f1v3s10 f1v13s20 0.965517 +f1v3s10 f1v11s18 0.982759 +f1v3s10 f1v12s19 0.965517 +f3v0s25 f3v3s28 0.970149 +f3v0s25 f3v13s38 0.970588 +f3v0s25 f3v7s32 0.969697 +f3v0s25 f3v1s26 0.969697 +f3v0s25 f3v6s31 0.969697 +f3v0s25 f3v11s36 0.940299 +f3v0s25 f3v4s29 0.955882 +f3v0s25 f3v12s37 0.970149 +f3v0s25 f3v14s39 1.000000 +f3v3s28 f3v13s38 0.955882 +f3v3s28 f3v7s32 0.940299 +f3v3s28 f3v1s26 0.940299 +f3v3s28 f3v6s31 0.940299 +f3v3s28 f3v11s36 0.911765 +f3v3s28 f3v4s29 0.941176 +f3v3s28 f3v12s37 0.941176 +f3v3s28 f3v14s39 0.970149 +f3v13s38 f3v7s32 0.941176 +f3v13s38 f3v1s26 0.941176 +f3v13s38 f3v6s31 0.941176 +f3v13s38 f3v11s36 0.913043 +f3v13s38 f3v4s29 0.942029 +f3v13s38 f3v12s37 0.942029 +f3v13s38 f3v14s39 0.970588 +f3v7s32 f3v1s26 0.939394 +f3v7s32 f3v6s31 1.000000 +f3v7s32 f3v11s36 0.910448 +f3v7s32 f3v4s29 0.926471 +f3v7s32 f3v12s37 0.940299 +f3v7s32 f3v14s39 0.969697 +f3v1s26 f3v6s31 0.939394 +f3v1s26 f3v11s36 0.910448 +f3v1s26 f3v4s29 0.926471 +f3v1s26 f3v12s37 0.940299 +f3v1s26 f3v14s39 0.969697 +f3v6s31 f3v11s36 0.910448 +f3v6s31 f3v4s29 0.926471 +f3v6s31 f3v12s37 0.940299 +f3v6s31 f3v14s39 0.969697 +f1v13s20 f1v11s18 0.948276 +f1v13s20 f1v12s19 0.931034 +f3v11s36 f3v4s29 0.898551 +f3v11s36 f3v12s37 0.911765 +f3v11s36 f3v14s39 0.940299 +f0v5s5 f0v6s6 0.901408 +f0v5s5 f0v2s2 0.943662 +f0v5s5 f0v3s3 0.915493 +f0v5s5 f0v0s0 0.971429 +f1v11s18 f1v12s19 0.948276 +f0v6s6 f0v2s2 0.901408 +f0v6s6 f0v3s3 0.873239 +f0v6s6 f0v0s0 0.928571 +f3v4s29 f3v12s37 0.927536 +f3v4s29 f3v14s39 0.955882 +f0v2s2 f0v3s3 0.915493 +f0v2s2 f0v0s0 0.971429 +f3v12s37 f3v14s39 0.970149 +f0v3s3 f0v0s0 0.942857 diff --git a/pkg/obitools/obialign/testdata/one_rawdist.expected b/pkg/obitools/obialign/testdata/one_rawdist.expected new file mode 100644 index 0000000..982fb6e --- /dev/null +++ b/pkg/obitools/obialign/testdata/one_rawdist.expected @@ -0,0 +1,161 @@ +f3v5s30 f3v0s25 4.000000 +f3v5s30 f3v14s39 4.000000 +f1v8s15 f2v1s23 1.000000 +f1v8s15 f1v6s13 3.000000 +f1v8s15 f1v14s21 4.000000 +f1v8s15 f1v4s11 1.000000 +f1v8s15 f1v9s16 4.000000 +f1v8s15 f1v0s7 1.000000 +f1v8s15 f1v7s14 1.000000 +f1v8s15 f1v3s10 1.000000 +f1v8s15 f1v13s20 3.000000 +f1v8s15 f1v11s18 2.000000 +f1v8s15 f1v12s19 3.000000 +f2v1s23 f1v6s13 4.000000 +f2v1s23 f1v1s8 4.000000 +f2v1s23 f1v14s21 3.000000 +f2v1s23 f1v5s12 4.000000 +f2v1s23 f1v4s11 0.000000 +f2v1s23 f1v9s16 3.000000 +f2v1s23 f1v10s17 4.000000 +f2v1s23 f1v0s7 0.000000 +f2v1s23 f1v7s14 0.000000 +f2v1s23 f1v3s10 0.000000 +f2v1s23 f1v13s20 2.000000 +f2v1s23 f1v11s18 1.000000 +f2v1s23 f1v12s19 2.000000 +f4v2s42 f4v0s40 2.000000 +f4v2s42 f4v1s41 3.000000 +f0v4s4 f0v1s1 0.000000 +f0v4s4 f2v0s22 4.000000 +f0v4s4 f1v2s9 0.000000 +f0v4s4 f0v5s5 4.000000 +f0v4s4 f0v2s2 4.000000 +f0v4s4 f0v0s0 2.000000 +f1v6s13 f1v4s11 4.000000 +f1v6s13 f1v0s7 4.000000 +f1v6s13 f1v7s14 4.000000 +f1v6s13 f1v3s10 4.000000 +f3v8s33 f3v9s34 3.000000 +f3v8s33 f3v2s27 2.000000 +f3v8s33 f3v0s25 1.000000 +f3v8s33 f3v3s28 3.000000 +f3v8s33 f3v13s38 3.000000 +f3v8s33 f3v7s32 3.000000 +f3v8s33 f3v1s26 3.000000 +f3v8s33 f3v6s31 3.000000 +f3v8s33 f3v4s29 4.000000 +f3v8s33 f3v12s37 3.000000 +f3v8s33 f3v14s39 1.000000 +f1v1s8 f1v4s11 4.000000 +f1v1s8 f1v0s7 4.000000 +f1v1s8 f1v7s14 4.000000 +f1v1s8 f1v3s10 4.000000 +f1v14s21 f1v4s11 3.000000 +f1v14s21 f1v0s7 3.000000 +f1v14s21 f1v7s14 3.000000 +f1v14s21 f1v3s10 3.000000 +f1v14s21 f1v11s18 4.000000 +f0v1s1 f2v0s22 4.000000 +f0v1s1 f1v2s9 0.000000 +f0v1s1 f0v5s5 4.000000 +f0v1s1 f0v2s2 4.000000 +f0v1s1 f0v0s0 2.000000 +f1v5s12 f1v4s11 4.000000 +f1v5s12 f1v0s7 4.000000 +f1v5s12 f1v7s14 4.000000 +f1v5s12 f1v3s10 4.000000 +f1v4s11 f1v9s16 3.000000 +f1v4s11 f1v10s17 4.000000 +f1v4s11 f1v0s7 0.000000 +f1v4s11 f1v7s14 0.000000 +f1v4s11 f1v3s10 0.000000 +f1v4s11 f1v13s20 2.000000 +f1v4s11 f1v11s18 1.000000 +f1v4s11 f1v12s19 2.000000 +f1v9s16 f1v0s7 3.000000 +f1v9s16 f1v7s14 3.000000 +f1v9s16 f1v3s10 3.000000 +f1v9s16 f1v11s18 4.000000 +f1v10s17 f1v0s7 4.000000 +f1v10s17 f1v7s14 4.000000 +f1v10s17 f1v3s10 4.000000 +f1v0s7 f1v7s14 0.000000 +f1v0s7 f1v3s10 0.000000 +f1v0s7 f1v13s20 2.000000 +f1v0s7 f1v11s18 1.000000 +f1v0s7 f1v12s19 2.000000 +f3v9s34 f3v2s27 3.000000 +f3v9s34 f3v0s25 2.000000 +f3v9s34 f3v3s28 4.000000 +f3v9s34 f3v13s38 4.000000 +f3v9s34 f3v7s32 0.000000 +f3v9s34 f3v1s26 4.000000 +f3v9s34 f3v6s31 0.000000 +f3v9s34 f3v12s37 4.000000 +f3v9s34 f3v14s39 2.000000 +f4v0s40 f4v1s41 1.000000 +f1v7s14 f1v3s10 0.000000 +f1v7s14 f1v13s20 2.000000 +f1v7s14 f1v11s18 1.000000 +f1v7s14 f1v12s19 2.000000 +f3v2s27 f3v0s25 1.000000 +f3v2s27 f3v3s28 3.000000 +f3v2s27 f3v13s38 3.000000 +f3v2s27 f3v7s32 3.000000 +f3v2s27 f3v1s26 2.000000 +f3v2s27 f3v6s31 3.000000 +f3v2s27 f3v4s29 4.000000 +f3v2s27 f3v12s37 3.000000 +f3v2s27 f3v14s39 1.000000 +f2v0s22 f1v2s9 4.000000 +f2v0s22 f0v5s5 4.000000 +f2v0s22 f0v2s2 0.000000 +f2v0s22 f0v0s0 2.000000 +f1v2s9 f0v5s5 4.000000 +f1v2s9 f0v2s2 4.000000 +f1v2s9 f0v0s0 2.000000 +f1v3s10 f1v13s20 2.000000 +f1v3s10 f1v11s18 1.000000 +f1v3s10 f1v12s19 2.000000 +f3v0s25 f3v3s28 2.000000 +f3v0s25 f3v13s38 2.000000 +f3v0s25 f3v7s32 2.000000 +f3v0s25 f3v1s26 2.000000 +f3v0s25 f3v6s31 2.000000 +f3v0s25 f3v11s36 4.000000 +f3v0s25 f3v4s29 3.000000 +f3v0s25 f3v12s37 2.000000 +f3v0s25 f3v14s39 0.000000 +f3v3s28 f3v13s38 3.000000 +f3v3s28 f3v7s32 4.000000 +f3v3s28 f3v1s26 4.000000 +f3v3s28 f3v6s31 4.000000 +f3v3s28 f3v4s29 4.000000 +f3v3s28 f3v12s37 4.000000 +f3v3s28 f3v14s39 2.000000 +f3v13s38 f3v7s32 4.000000 +f3v13s38 f3v1s26 4.000000 +f3v13s38 f3v6s31 4.000000 +f3v13s38 f3v4s29 4.000000 +f3v13s38 f3v12s37 4.000000 +f3v13s38 f3v14s39 2.000000 +f3v7s32 f3v1s26 4.000000 +f3v7s32 f3v6s31 0.000000 +f3v7s32 f3v12s37 4.000000 +f3v7s32 f3v14s39 2.000000 +f3v1s26 f3v6s31 4.000000 +f3v1s26 f3v12s37 4.000000 +f3v1s26 f3v14s39 2.000000 +f3v6s31 f3v12s37 4.000000 +f3v6s31 f3v14s39 2.000000 +f1v13s20 f1v11s18 3.000000 +f1v13s20 f1v12s19 4.000000 +f3v11s36 f3v14s39 4.000000 +f0v5s5 f0v2s2 4.000000 +f0v5s5 f0v0s0 2.000000 +f1v11s18 f1v12s19 3.000000 +f3v4s29 f3v14s39 3.000000 +f0v2s2 f0v0s0 2.000000 +f3v12s37 f3v14s39 2.000000 +f0v3s3 f0v0s0 4.000000 diff --git a/pkg/obitools/obialign/testdata/one_rawlcs.expected b/pkg/obitools/obialign/testdata/one_rawlcs.expected new file mode 100644 index 0000000..dee5231 --- /dev/null +++ b/pkg/obitools/obialign/testdata/one_rawlcs.expected @@ -0,0 +1,454 @@ +f3v10s35 f3v5s30 58.000000 +f3v10s35 f0v4s4 40.000000 +f3v10s35 f3v8s33 61.000000 +f3v10s35 f0v1s1 40.000000 +f3v10s35 f3v9s34 61.000000 +f3v10s35 f3v2s27 61.000000 +f3v10s35 f2v0s22 40.000000 +f3v10s35 f1v2s9 40.000000 +f3v10s35 f3v0s25 62.000000 +f3v10s35 f3v3s28 61.000000 +f3v10s35 f3v13s38 62.000000 +f3v10s35 f3v7s32 61.000000 +f3v10s35 f3v1s26 61.000000 +f3v10s35 f3v6s31 61.000000 +f3v10s35 f3v11s36 59.000000 +f3v10s35 f3v4s29 61.000000 +f3v10s35 f0v2s2 40.000000 +f3v10s35 f3v12s37 61.000000 +f3v10s35 f0v3s3 40.000000 +f3v10s35 f3v14s39 62.000000 +f3v10s35 f0v0s0 40.000000 +f3v5s30 f3v8s33 61.000000 +f3v5s30 f3v9s34 60.000000 +f3v5s30 f3v2s27 61.000000 +f3v5s30 f2v0s22 40.000000 +f3v5s30 f3v0s25 62.000000 +f3v5s30 f3v3s28 61.000000 +f3v5s30 f3v13s38 62.000000 +f3v5s30 f3v7s32 60.000000 +f3v5s30 f3v1s26 61.000000 +f3v5s30 f3v6s31 60.000000 +f3v5s30 f3v11s36 59.000000 +f3v5s30 f0v6s6 41.000000 +f3v5s30 f3v4s29 61.000000 +f3v5s30 f0v2s2 40.000000 +f3v5s30 f3v12s37 61.000000 +f3v5s30 f0v3s3 40.000000 +f3v5s30 f3v14s39 62.000000 +f3v5s30 f0v0s0 40.000000 +f1v8s15 f2v1s23 57.000000 +f1v8s15 f1v6s13 54.000000 +f1v8s15 f1v1s8 55.000000 +f1v8s15 f1v14s21 54.000000 +f1v8s15 f1v5s12 55.000000 +f1v8s15 f1v4s11 57.000000 +f1v8s15 f1v9s16 56.000000 +f1v8s15 f1v10s17 55.000000 +f1v8s15 f1v0s7 57.000000 +f1v8s15 f3v9s34 40.000000 +f1v8s15 f1v7s14 57.000000 +f1v8s15 f1v3s10 57.000000 +f1v8s15 f3v13s38 40.000000 +f1v8s15 f3v7s32 40.000000 +f1v8s15 f3v6s31 40.000000 +f1v8s15 f1v13s20 55.000000 +f1v8s15 f1v11s18 56.000000 +f1v8s15 f3v4s29 40.000000 +f1v8s15 f3v12s37 40.000000 +f1v8s15 f1v12s19 55.000000 +f2v1s23 f1v6s13 54.000000 +f2v1s23 f1v1s8 56.000000 +f2v1s23 f1v14s21 55.000000 +f2v1s23 f1v5s12 56.000000 +f2v1s23 f1v4s11 58.000000 +f2v1s23 f1v9s16 57.000000 +f2v1s23 f1v10s17 56.000000 +f2v1s23 f1v0s7 58.000000 +f2v1s23 f3v9s34 40.000000 +f2v1s23 f1v7s14 58.000000 +f2v1s23 f1v3s10 58.000000 +f2v1s23 f3v13s38 40.000000 +f2v1s23 f3v7s32 40.000000 +f2v1s23 f3v6s31 40.000000 +f2v1s23 f1v13s20 56.000000 +f2v1s23 f1v11s18 57.000000 +f2v1s23 f3v4s29 40.000000 +f2v1s23 f3v12s37 40.000000 +f2v1s23 f1v12s19 56.000000 +f4v2s42 f4v0s40 59.000000 +f4v2s42 f4v1s41 59.000000 +f4v2s42 f2v0s22 41.000000 +f4v2s42 f0v5s5 40.000000 +f4v2s42 f0v6s6 41.000000 +f4v2s42 f0v2s2 41.000000 +f4v2s42 f0v0s0 40.000000 +f0v4s4 f3v8s33 41.000000 +f0v4s4 f0v1s1 69.000000 +f0v4s4 f3v9s34 40.000000 +f0v4s4 f3v2s27 40.000000 +f0v4s4 f2v0s22 67.000000 +f0v4s4 f1v2s9 69.000000 +f0v4s4 f3v0s25 40.000000 +f0v4s4 f3v3s28 40.000000 +f0v4s4 f3v13s38 40.000000 +f0v4s4 f3v7s32 40.000000 +f0v4s4 f3v6s31 40.000000 +f0v4s4 f0v5s5 67.000000 +f0v4s4 f0v6s6 64.000000 +f0v4s4 f3v4s29 42.000000 +f0v4s4 f0v2s2 67.000000 +f0v4s4 f3v12s37 40.000000 +f0v4s4 f0v3s3 65.000000 +f0v4s4 f3v14s39 40.000000 +f0v4s4 f0v0s0 68.000000 +f1v6s13 f1v1s8 52.000000 +f1v6s13 f1v14s21 51.000000 +f1v6s13 f1v5s12 52.000000 +f1v6s13 f1v4s11 54.000000 +f1v6s13 f1v9s16 53.000000 +f1v6s13 f1v10s17 53.000000 +f1v6s13 f1v0s7 54.000000 +f1v6s13 f1v7s14 54.000000 +f1v6s13 f1v3s10 54.000000 +f1v6s13 f1v13s20 52.000000 +f1v6s13 f1v11s18 53.000000 +f1v6s13 f3v4s29 40.000000 +f1v6s13 f3v12s37 40.000000 +f1v6s13 f1v12s19 52.000000 +f3v8s33 f1v1s8 40.000000 +f3v8s33 f0v1s1 41.000000 +f3v8s33 f1v10s17 40.000000 +f3v8s33 f3v9s34 63.000000 +f3v8s33 f3v2s27 64.000000 +f3v8s33 f2v0s22 42.000000 +f3v8s33 f1v2s9 41.000000 +f3v8s33 f3v0s25 65.000000 +f3v8s33 f3v3s28 64.000000 +f3v8s33 f3v13s38 65.000000 +f3v8s33 f3v7s32 63.000000 +f3v8s33 f3v1s26 63.000000 +f3v8s33 f3v6s31 63.000000 +f3v8s33 f3v11s36 62.000000 +f3v8s33 f0v5s5 41.000000 +f3v8s33 f0v6s6 41.000000 +f3v8s33 f3v4s29 64.000000 +f3v8s33 f0v2s2 42.000000 +f3v8s33 f3v12s37 64.000000 +f3v8s33 f0v3s3 41.000000 +f3v8s33 f3v14s39 65.000000 +f3v8s33 f0v0s0 42.000000 +f1v1s8 f1v14s21 55.000000 +f1v1s8 f1v5s12 54.000000 +f1v1s8 f1v4s11 56.000000 +f1v1s8 f1v9s16 55.000000 +f1v1s8 f1v10s17 54.000000 +f1v1s8 f1v0s7 56.000000 +f1v1s8 f3v9s34 41.000000 +f1v1s8 f1v7s14 56.000000 +f1v1s8 f3v2s27 40.000000 +f1v1s8 f1v3s10 56.000000 +f1v1s8 f3v0s25 40.000000 +f1v1s8 f3v3s28 40.000000 +f1v1s8 f3v13s38 41.000000 +f1v1s8 f3v7s32 41.000000 +f1v1s8 f3v1s26 40.000000 +f1v1s8 f3v6s31 41.000000 +f1v1s8 f1v13s20 54.000000 +f1v1s8 f1v11s18 55.000000 +f1v1s8 f3v4s29 41.000000 +f1v1s8 f3v12s37 41.000000 +f1v1s8 f3v14s39 40.000000 +f1v1s8 f1v12s19 55.000000 +f1v14s21 f1v5s12 53.000000 +f1v14s21 f1v4s11 55.000000 +f1v14s21 f1v9s16 54.000000 +f1v14s21 f1v10s17 53.000000 +f1v14s21 f1v0s7 55.000000 +f1v14s21 f1v7s14 55.000000 +f1v14s21 f1v3s10 55.000000 +f1v14s21 f1v13s20 53.000000 +f1v14s21 f1v11s18 54.000000 +f1v14s21 f1v12s19 53.000000 +f0v1s1 f3v9s34 40.000000 +f0v1s1 f3v2s27 40.000000 +f0v1s1 f2v0s22 67.000000 +f0v1s1 f1v2s9 69.000000 +f0v1s1 f3v0s25 40.000000 +f0v1s1 f3v3s28 40.000000 +f0v1s1 f3v13s38 40.000000 +f0v1s1 f3v7s32 40.000000 +f0v1s1 f3v6s31 40.000000 +f0v1s1 f0v5s5 67.000000 +f0v1s1 f0v6s6 64.000000 +f0v1s1 f3v4s29 42.000000 +f0v1s1 f0v2s2 67.000000 +f0v1s1 f3v12s37 40.000000 +f0v1s1 f0v3s3 65.000000 +f0v1s1 f3v14s39 40.000000 +f0v1s1 f0v0s0 68.000000 +f1v5s12 f1v4s11 56.000000 +f1v5s12 f1v9s16 55.000000 +f1v5s12 f1v10s17 55.000000 +f1v5s12 f1v0s7 56.000000 +f1v5s12 f3v9s34 40.000000 +f1v5s12 f1v7s14 56.000000 +f1v5s12 f2v0s22 40.000000 +f1v5s12 f1v3s10 56.000000 +f1v5s12 f3v13s38 40.000000 +f1v5s12 f3v7s32 40.000000 +f1v5s12 f3v6s31 40.000000 +f1v5s12 f1v13s20 54.000000 +f1v5s12 f1v11s18 55.000000 +f1v5s12 f3v4s29 40.000000 +f1v5s12 f0v2s2 40.000000 +f1v5s12 f3v12s37 40.000000 +f1v5s12 f0v0s0 40.000000 +f1v5s12 f1v12s19 55.000000 +f1v4s11 f1v9s16 57.000000 +f1v4s11 f1v10s17 56.000000 +f1v4s11 f1v0s7 58.000000 +f1v4s11 f3v9s34 40.000000 +f1v4s11 f1v7s14 58.000000 +f1v4s11 f1v3s10 58.000000 +f1v4s11 f3v13s38 40.000000 +f1v4s11 f3v7s32 40.000000 +f1v4s11 f3v6s31 40.000000 +f1v4s11 f1v13s20 56.000000 +f1v4s11 f1v11s18 57.000000 +f1v4s11 f3v4s29 40.000000 +f1v4s11 f3v12s37 40.000000 +f1v4s11 f1v12s19 56.000000 +f1v9s16 f1v10s17 55.000000 +f1v9s16 f1v0s7 57.000000 +f1v9s16 f1v7s14 57.000000 +f1v9s16 f2v0s22 40.000000 +f1v9s16 f1v3s10 57.000000 +f1v9s16 f3v13s38 40.000000 +f1v9s16 f1v13s20 55.000000 +f1v9s16 f0v5s5 40.000000 +f1v9s16 f1v11s18 56.000000 +f1v9s16 f3v4s29 40.000000 +f1v9s16 f0v2s2 40.000000 +f1v9s16 f3v12s37 40.000000 +f1v9s16 f0v3s3 41.000000 +f1v9s16 f0v0s0 40.000000 +f1v9s16 f1v12s19 55.000000 +f1v10s17 f1v0s7 56.000000 +f1v10s17 f3v9s34 41.000000 +f1v10s17 f1v7s14 56.000000 +f1v10s17 f3v2s27 40.000000 +f1v10s17 f1v3s10 56.000000 +f1v10s17 f3v0s25 40.000000 +f1v10s17 f3v3s28 40.000000 +f1v10s17 f3v13s38 41.000000 +f1v10s17 f3v7s32 41.000000 +f1v10s17 f3v6s31 41.000000 +f1v10s17 f1v13s20 54.000000 +f1v10s17 f3v11s36 41.000000 +f1v10s17 f1v11s18 55.000000 +f1v10s17 f3v4s29 40.000000 +f1v10s17 f3v12s37 41.000000 +f1v10s17 f0v3s3 40.000000 +f1v10s17 f3v14s39 40.000000 +f1v10s17 f0v0s0 40.000000 +f1v10s17 f1v12s19 55.000000 +f1v0s7 f3v9s34 40.000000 +f1v0s7 f1v7s14 58.000000 +f1v0s7 f1v3s10 58.000000 +f1v0s7 f3v13s38 40.000000 +f1v0s7 f3v7s32 40.000000 +f1v0s7 f3v6s31 40.000000 +f1v0s7 f1v13s20 56.000000 +f1v0s7 f1v11s18 57.000000 +f1v0s7 f3v4s29 40.000000 +f1v0s7 f3v12s37 40.000000 +f1v0s7 f1v12s19 56.000000 +f3v9s34 f1v7s14 40.000000 +f3v9s34 f3v2s27 63.000000 +f3v9s34 f2v0s22 40.000000 +f3v9s34 f1v2s9 40.000000 +f3v9s34 f1v3s10 40.000000 +f3v9s34 f3v0s25 64.000000 +f3v9s34 f3v3s28 63.000000 +f3v9s34 f3v13s38 64.000000 +f3v9s34 f3v7s32 65.000000 +f3v9s34 f3v1s26 62.000000 +f3v9s34 f3v6s31 65.000000 +f3v9s34 f3v11s36 61.000000 +f3v9s34 f0v5s5 40.000000 +f3v9s34 f1v11s18 40.000000 +f3v9s34 f3v4s29 63.000000 +f3v9s34 f0v2s2 40.000000 +f3v9s34 f3v12s37 63.000000 +f3v9s34 f0v3s3 41.000000 +f3v9s34 f3v14s39 64.000000 +f3v9s34 f0v0s0 41.000000 +f4v0s40 f4v1s41 60.000000 +f1v7s14 f1v3s10 58.000000 +f1v7s14 f3v13s38 40.000000 +f1v7s14 f3v7s32 40.000000 +f1v7s14 f3v6s31 40.000000 +f1v7s14 f1v13s20 56.000000 +f1v7s14 f1v11s18 57.000000 +f1v7s14 f3v4s29 40.000000 +f1v7s14 f3v12s37 40.000000 +f1v7s14 f1v12s19 56.000000 +f3v2s27 f2v0s22 41.000000 +f3v2s27 f1v2s9 40.000000 +f3v2s27 f3v0s25 65.000000 +f3v2s27 f3v3s28 64.000000 +f3v2s27 f3v13s38 65.000000 +f3v2s27 f3v7s32 63.000000 +f3v2s27 f3v1s26 63.000000 +f3v2s27 f3v6s31 63.000000 +f3v2s27 f3v11s36 62.000000 +f3v2s27 f0v5s5 40.000000 +f3v2s27 f0v6s6 40.000000 +f3v2s27 f3v4s29 64.000000 +f3v2s27 f0v2s2 41.000000 +f3v2s27 f3v12s37 64.000000 +f3v2s27 f0v3s3 41.000000 +f3v2s27 f3v14s39 65.000000 +f3v2s27 f0v0s0 41.000000 +f2v0s22 f1v2s9 67.000000 +f2v0s22 f3v0s25 41.000000 +f2v0s22 f3v3s28 41.000000 +f2v0s22 f3v13s38 41.000000 +f2v0s22 f3v7s32 40.000000 +f2v0s22 f3v1s26 40.000000 +f2v0s22 f3v6s31 40.000000 +f2v0s22 f0v5s5 67.000000 +f2v0s22 f0v6s6 64.000000 +f2v0s22 f3v4s29 43.000000 +f2v0s22 f0v2s2 69.000000 +f2v0s22 f3v12s37 42.000000 +f2v0s22 f0v3s3 65.000000 +f2v0s22 f3v14s39 41.000000 +f2v0s22 f0v0s0 68.000000 +f1v2s9 f3v0s25 40.000000 +f1v2s9 f3v3s28 40.000000 +f1v2s9 f3v13s38 40.000000 +f1v2s9 f3v7s32 40.000000 +f1v2s9 f3v6s31 40.000000 +f1v2s9 f0v5s5 67.000000 +f1v2s9 f0v6s6 64.000000 +f1v2s9 f3v4s29 42.000000 +f1v2s9 f0v2s2 67.000000 +f1v2s9 f3v12s37 40.000000 +f1v2s9 f0v3s3 65.000000 +f1v2s9 f3v14s39 40.000000 +f1v2s9 f0v0s0 68.000000 +f1v3s10 f3v13s38 40.000000 +f1v3s10 f3v7s32 40.000000 +f1v3s10 f3v6s31 40.000000 +f1v3s10 f1v13s20 56.000000 +f1v3s10 f1v11s18 57.000000 +f1v3s10 f3v4s29 40.000000 +f1v3s10 f3v12s37 40.000000 +f1v3s10 f1v12s19 56.000000 +f3v0s25 f3v3s28 65.000000 +f3v0s25 f3v13s38 66.000000 +f3v0s25 f3v7s32 64.000000 +f3v0s25 f3v1s26 64.000000 +f3v0s25 f3v6s31 64.000000 +f3v0s25 f3v11s36 63.000000 +f3v0s25 f0v5s5 40.000000 +f3v0s25 f0v6s6 40.000000 +f3v0s25 f3v4s29 65.000000 +f3v0s25 f0v2s2 41.000000 +f3v0s25 f3v12s37 65.000000 +f3v0s25 f0v3s3 41.000000 +f3v0s25 f3v14s39 66.000000 +f3v0s25 f0v0s0 41.000000 +f3v3s28 f3v13s38 65.000000 +f3v3s28 f3v7s32 63.000000 +f3v3s28 f3v1s26 63.000000 +f3v3s28 f3v6s31 63.000000 +f3v3s28 f3v11s36 62.000000 +f3v3s28 f0v5s5 40.000000 +f3v3s28 f0v6s6 40.000000 +f3v3s28 f3v4s29 64.000000 +f3v3s28 f0v2s2 41.000000 +f3v3s28 f3v12s37 64.000000 +f3v3s28 f0v3s3 41.000000 +f3v3s28 f3v14s39 65.000000 +f3v3s28 f0v0s0 41.000000 +f3v13s38 f3v7s32 64.000000 +f3v13s38 f3v1s26 64.000000 +f3v13s38 f3v6s31 64.000000 +f3v13s38 f3v11s36 63.000000 +f3v13s38 f0v5s5 40.000000 +f3v13s38 f1v11s18 40.000000 +f3v13s38 f0v6s6 40.000000 +f3v13s38 f3v4s29 65.000000 +f3v13s38 f0v2s2 41.000000 +f3v13s38 f3v12s37 65.000000 +f3v13s38 f0v3s3 41.000000 +f3v13s38 f3v14s39 66.000000 +f3v13s38 f0v0s0 41.000000 +f3v7s32 f3v1s26 62.000000 +f3v7s32 f3v6s31 65.000000 +f3v7s32 f3v11s36 61.000000 +f3v7s32 f0v5s5 40.000000 +f3v7s32 f1v11s18 40.000000 +f3v7s32 f3v4s29 63.000000 +f3v7s32 f0v2s2 40.000000 +f3v7s32 f3v12s37 63.000000 +f3v7s32 f0v3s3 41.000000 +f3v7s32 f3v14s39 64.000000 +f3v7s32 f0v0s0 41.000000 +f3v1s26 f3v6s31 62.000000 +f3v1s26 f3v11s36 61.000000 +f3v1s26 f3v4s29 63.000000 +f3v1s26 f0v2s2 40.000000 +f3v1s26 f3v12s37 63.000000 +f3v1s26 f0v3s3 40.000000 +f3v1s26 f3v14s39 64.000000 +f3v1s26 f0v0s0 40.000000 +f3v6s31 f3v11s36 61.000000 +f3v6s31 f0v5s5 40.000000 +f3v6s31 f1v11s18 40.000000 +f3v6s31 f3v4s29 63.000000 +f3v6s31 f0v2s2 40.000000 +f3v6s31 f3v12s37 63.000000 +f3v6s31 f0v3s3 41.000000 +f3v6s31 f3v14s39 64.000000 +f3v6s31 f0v0s0 41.000000 +f1v13s20 f1v11s18 55.000000 +f1v13s20 f1v12s19 54.000000 +f3v11s36 f3v4s29 62.000000 +f3v11s36 f3v12s37 62.000000 +f3v11s36 f3v14s39 63.000000 +f0v5s5 f1v11s18 40.000000 +f0v5s5 f0v6s6 64.000000 +f0v5s5 f3v4s29 42.000000 +f0v5s5 f0v2s2 67.000000 +f0v5s5 f3v12s37 40.000000 +f0v5s5 f0v3s3 65.000000 +f0v5s5 f3v14s39 40.000000 +f0v5s5 f0v0s0 68.000000 +f1v11s18 f3v4s29 40.000000 +f1v11s18 f3v12s37 40.000000 +f1v11s18 f1v12s19 55.000000 +f0v6s6 f3v4s29 42.000000 +f0v6s6 f0v2s2 64.000000 +f0v6s6 f3v12s37 40.000000 +f0v6s6 f0v3s3 62.000000 +f0v6s6 f3v14s39 40.000000 +f0v6s6 f0v0s0 65.000000 +f3v4s29 f0v2s2 43.000000 +f3v4s29 f3v12s37 64.000000 +f3v4s29 f0v3s3 43.000000 +f3v4s29 f3v14s39 65.000000 +f3v4s29 f0v0s0 43.000000 +f0v2s2 f3v12s37 42.000000 +f0v2s2 f0v3s3 65.000000 +f0v2s2 f3v14s39 41.000000 +f0v2s2 f0v0s0 68.000000 +f3v12s37 f0v3s3 41.000000 +f3v12s37 f3v14s39 65.000000 +f3v12s37 f0v0s0 41.000000 +f0v3s3 f3v14s39 41.000000 +f0v3s3 f0v0s0 66.000000 +f3v14s39 f0v0s0 41.000000 diff --git a/pkg/obitools/obialign/testdata/one_rawlcs_shortest.expected b/pkg/obitools/obialign/testdata/one_rawlcs_shortest.expected new file mode 100644 index 0000000..5922e93 --- /dev/null +++ b/pkg/obitools/obialign/testdata/one_rawlcs_shortest.expected @@ -0,0 +1,249 @@ +f3v10s35 f3v5s30 58.000000 +f3v10s35 f3v8s33 61.000000 +f3v10s35 f3v9s34 61.000000 +f3v10s35 f3v2s27 61.000000 +f3v10s35 f3v0s25 62.000000 +f3v10s35 f3v3s28 61.000000 +f3v10s35 f3v13s38 62.000000 +f3v10s35 f3v7s32 61.000000 +f3v10s35 f3v1s26 61.000000 +f3v10s35 f3v6s31 61.000000 +f3v10s35 f3v11s36 59.000000 +f3v10s35 f3v4s29 61.000000 +f3v10s35 f3v12s37 61.000000 +f3v10s35 f3v14s39 62.000000 +f3v5s30 f3v8s33 61.000000 +f3v5s30 f3v9s34 60.000000 +f3v5s30 f3v2s27 61.000000 +f3v5s30 f3v0s25 62.000000 +f3v5s30 f3v3s28 61.000000 +f3v5s30 f3v13s38 62.000000 +f3v5s30 f3v7s32 60.000000 +f3v5s30 f3v1s26 61.000000 +f3v5s30 f3v6s31 60.000000 +f3v5s30 f3v11s36 59.000000 +f3v5s30 f3v4s29 61.000000 +f3v5s30 f3v12s37 61.000000 +f3v5s30 f3v14s39 62.000000 +f1v8s15 f2v1s23 57.000000 +f1v8s15 f1v6s13 54.000000 +f1v8s15 f1v1s8 55.000000 +f1v8s15 f1v14s21 54.000000 +f1v8s15 f1v5s12 55.000000 +f1v8s15 f1v4s11 57.000000 +f1v8s15 f1v9s16 56.000000 +f1v8s15 f1v10s17 55.000000 +f1v8s15 f1v0s7 57.000000 +f1v8s15 f1v7s14 57.000000 +f1v8s15 f1v3s10 57.000000 +f1v8s15 f1v13s20 55.000000 +f1v8s15 f1v11s18 56.000000 +f1v8s15 f1v12s19 55.000000 +f2v1s23 f1v6s13 54.000000 +f2v1s23 f1v1s8 56.000000 +f2v1s23 f1v14s21 55.000000 +f2v1s23 f1v5s12 56.000000 +f2v1s23 f1v4s11 58.000000 +f2v1s23 f1v9s16 57.000000 +f2v1s23 f1v10s17 56.000000 +f2v1s23 f1v0s7 58.000000 +f2v1s23 f1v7s14 58.000000 +f2v1s23 f1v3s10 58.000000 +f2v1s23 f1v13s20 56.000000 +f2v1s23 f1v11s18 57.000000 +f2v1s23 f1v12s19 56.000000 +f4v2s42 f4v0s40 59.000000 +f4v2s42 f4v1s41 59.000000 +f0v4s4 f0v1s1 69.000000 +f0v4s4 f2v0s22 67.000000 +f0v4s4 f1v2s9 69.000000 +f0v4s4 f0v5s5 67.000000 +f0v4s4 f0v6s6 64.000000 +f0v4s4 f0v2s2 67.000000 +f0v4s4 f0v3s3 65.000000 +f0v4s4 f0v0s0 68.000000 +f1v6s13 f1v1s8 52.000000 +f1v6s13 f1v14s21 51.000000 +f1v6s13 f1v5s12 52.000000 +f1v6s13 f1v4s11 54.000000 +f1v6s13 f1v9s16 53.000000 +f1v6s13 f1v10s17 53.000000 +f1v6s13 f1v0s7 54.000000 +f1v6s13 f1v7s14 54.000000 +f1v6s13 f1v3s10 54.000000 +f1v6s13 f1v13s20 52.000000 +f1v6s13 f1v11s18 53.000000 +f1v6s13 f1v12s19 52.000000 +f3v8s33 f3v9s34 63.000000 +f3v8s33 f3v2s27 64.000000 +f3v8s33 f3v0s25 65.000000 +f3v8s33 f3v3s28 64.000000 +f3v8s33 f3v13s38 65.000000 +f3v8s33 f3v7s32 63.000000 +f3v8s33 f3v1s26 63.000000 +f3v8s33 f3v6s31 63.000000 +f3v8s33 f3v11s36 62.000000 +f3v8s33 f3v4s29 64.000000 +f3v8s33 f3v12s37 64.000000 +f3v8s33 f3v14s39 65.000000 +f1v1s8 f1v14s21 55.000000 +f1v1s8 f1v5s12 54.000000 +f1v1s8 f1v4s11 56.000000 +f1v1s8 f1v9s16 55.000000 +f1v1s8 f1v10s17 54.000000 +f1v1s8 f1v0s7 56.000000 +f1v1s8 f1v7s14 56.000000 +f1v1s8 f1v3s10 56.000000 +f1v1s8 f1v13s20 54.000000 +f1v1s8 f1v11s18 55.000000 +f1v1s8 f1v12s19 55.000000 +f1v14s21 f1v5s12 53.000000 +f1v14s21 f1v4s11 55.000000 +f1v14s21 f1v9s16 54.000000 +f1v14s21 f1v10s17 53.000000 +f1v14s21 f1v0s7 55.000000 +f1v14s21 f1v7s14 55.000000 +f1v14s21 f1v3s10 55.000000 +f1v14s21 f1v13s20 53.000000 +f1v14s21 f1v11s18 54.000000 +f1v14s21 f1v12s19 53.000000 +f0v1s1 f2v0s22 67.000000 +f0v1s1 f1v2s9 69.000000 +f0v1s1 f0v5s5 67.000000 +f0v1s1 f0v6s6 64.000000 +f0v1s1 f0v2s2 67.000000 +f0v1s1 f0v3s3 65.000000 +f0v1s1 f0v0s0 68.000000 +f1v5s12 f1v4s11 56.000000 +f1v5s12 f1v9s16 55.000000 +f1v5s12 f1v10s17 55.000000 +f1v5s12 f1v0s7 56.000000 +f1v5s12 f1v7s14 56.000000 +f1v5s12 f1v3s10 56.000000 +f1v5s12 f1v13s20 54.000000 +f1v5s12 f1v11s18 55.000000 +f1v5s12 f1v12s19 55.000000 +f1v4s11 f1v9s16 57.000000 +f1v4s11 f1v10s17 56.000000 +f1v4s11 f1v0s7 58.000000 +f1v4s11 f1v7s14 58.000000 +f1v4s11 f1v3s10 58.000000 +f1v4s11 f1v13s20 56.000000 +f1v4s11 f1v11s18 57.000000 +f1v4s11 f1v12s19 56.000000 +f1v9s16 f1v10s17 55.000000 +f1v9s16 f1v0s7 57.000000 +f1v9s16 f1v7s14 57.000000 +f1v9s16 f1v3s10 57.000000 +f1v9s16 f1v13s20 55.000000 +f1v9s16 f1v11s18 56.000000 +f1v9s16 f1v12s19 55.000000 +f1v10s17 f1v0s7 56.000000 +f1v10s17 f1v7s14 56.000000 +f1v10s17 f1v3s10 56.000000 +f1v10s17 f1v13s20 54.000000 +f1v10s17 f1v11s18 55.000000 +f1v10s17 f1v12s19 55.000000 +f1v0s7 f1v7s14 58.000000 +f1v0s7 f1v3s10 58.000000 +f1v0s7 f1v13s20 56.000000 +f1v0s7 f1v11s18 57.000000 +f1v0s7 f1v12s19 56.000000 +f3v9s34 f3v2s27 63.000000 +f3v9s34 f3v0s25 64.000000 +f3v9s34 f3v3s28 63.000000 +f3v9s34 f3v13s38 64.000000 +f3v9s34 f3v7s32 65.000000 +f3v9s34 f3v1s26 62.000000 +f3v9s34 f3v6s31 65.000000 +f3v9s34 f3v11s36 61.000000 +f3v9s34 f3v4s29 63.000000 +f3v9s34 f3v12s37 63.000000 +f3v9s34 f3v14s39 64.000000 +f4v0s40 f4v1s41 60.000000 +f1v7s14 f1v3s10 58.000000 +f1v7s14 f1v13s20 56.000000 +f1v7s14 f1v11s18 57.000000 +f1v7s14 f1v12s19 56.000000 +f3v2s27 f3v0s25 65.000000 +f3v2s27 f3v3s28 64.000000 +f3v2s27 f3v13s38 65.000000 +f3v2s27 f3v7s32 63.000000 +f3v2s27 f3v1s26 63.000000 +f3v2s27 f3v6s31 63.000000 +f3v2s27 f3v11s36 62.000000 +f3v2s27 f3v4s29 64.000000 +f3v2s27 f3v12s37 64.000000 +f3v2s27 f3v14s39 65.000000 +f2v0s22 f1v2s9 67.000000 +f2v0s22 f0v5s5 67.000000 +f2v0s22 f0v6s6 64.000000 +f2v0s22 f0v2s2 69.000000 +f2v0s22 f0v3s3 65.000000 +f2v0s22 f0v0s0 68.000000 +f1v2s9 f0v5s5 67.000000 +f1v2s9 f0v6s6 64.000000 +f1v2s9 f0v2s2 67.000000 +f1v2s9 f0v3s3 65.000000 +f1v2s9 f0v0s0 68.000000 +f1v3s10 f1v13s20 56.000000 +f1v3s10 f1v11s18 57.000000 +f1v3s10 f1v12s19 56.000000 +f3v0s25 f3v3s28 65.000000 +f3v0s25 f3v13s38 66.000000 +f3v0s25 f3v7s32 64.000000 +f3v0s25 f3v1s26 64.000000 +f3v0s25 f3v6s31 64.000000 +f3v0s25 f3v11s36 63.000000 +f3v0s25 f3v4s29 65.000000 +f3v0s25 f3v12s37 65.000000 +f3v0s25 f3v14s39 66.000000 +f3v3s28 f3v13s38 65.000000 +f3v3s28 f3v7s32 63.000000 +f3v3s28 f3v1s26 63.000000 +f3v3s28 f3v6s31 63.000000 +f3v3s28 f3v11s36 62.000000 +f3v3s28 f3v4s29 64.000000 +f3v3s28 f3v12s37 64.000000 +f3v3s28 f3v14s39 65.000000 +f3v13s38 f3v7s32 64.000000 +f3v13s38 f3v1s26 64.000000 +f3v13s38 f3v6s31 64.000000 +f3v13s38 f3v11s36 63.000000 +f3v13s38 f3v4s29 65.000000 +f3v13s38 f3v12s37 65.000000 +f3v13s38 f3v14s39 66.000000 +f3v7s32 f3v1s26 62.000000 +f3v7s32 f3v6s31 65.000000 +f3v7s32 f3v11s36 61.000000 +f3v7s32 f3v4s29 63.000000 +f3v7s32 f3v12s37 63.000000 +f3v7s32 f3v14s39 64.000000 +f3v1s26 f3v6s31 62.000000 +f3v1s26 f3v11s36 61.000000 +f3v1s26 f3v4s29 63.000000 +f3v1s26 f3v12s37 63.000000 +f3v1s26 f3v14s39 64.000000 +f3v6s31 f3v11s36 61.000000 +f3v6s31 f3v4s29 63.000000 +f3v6s31 f3v12s37 63.000000 +f3v6s31 f3v14s39 64.000000 +f1v13s20 f1v11s18 55.000000 +f1v13s20 f1v12s19 54.000000 +f3v11s36 f3v4s29 62.000000 +f3v11s36 f3v12s37 62.000000 +f3v11s36 f3v14s39 63.000000 +f0v5s5 f0v6s6 64.000000 +f0v5s5 f0v2s2 67.000000 +f0v5s5 f0v3s3 65.000000 +f0v5s5 f0v0s0 68.000000 +f1v11s18 f1v12s19 55.000000 +f0v6s6 f0v2s2 64.000000 +f0v6s6 f0v3s3 62.000000 +f0v6s6 f0v0s0 65.000000 +f3v4s29 f3v12s37 64.000000 +f3v4s29 f3v14s39 65.000000 +f0v2s2 f0v3s3 65.000000 +f0v2s2 f0v0s0 68.000000 +f3v12s37 f3v14s39 65.000000 +f0v3s3 f0v0s0 66.000000 diff --git a/pkg/obitools/obialign/testdata/one_shortest.expected b/pkg/obitools/obialign/testdata/one_shortest.expected new file mode 100644 index 0000000..7d22af3 --- /dev/null +++ b/pkg/obitools/obialign/testdata/one_shortest.expected @@ -0,0 +1,249 @@ +f3v10s35 f3v5s30 0.920635 +f3v10s35 f3v8s33 0.938462 +f3v10s35 f3v9s34 0.938462 +f3v10s35 f3v2s27 0.938462 +f3v10s35 f3v0s25 0.953846 +f3v10s35 f3v3s28 0.938462 +f3v10s35 f3v13s38 0.953846 +f3v10s35 f3v7s32 0.938462 +f3v10s35 f3v1s26 0.938462 +f3v10s35 f3v6s31 0.938462 +f3v10s35 f3v11s36 0.907692 +f3v10s35 f3v4s29 0.938462 +f3v10s35 f3v12s37 0.938462 +f3v10s35 f3v14s39 0.953846 +f3v5s30 f3v8s33 0.968254 +f3v5s30 f3v9s34 0.952381 +f3v5s30 f3v2s27 0.968254 +f3v5s30 f3v0s25 0.984127 +f3v5s30 f3v3s28 0.968254 +f3v5s30 f3v13s38 0.984127 +f3v5s30 f3v7s32 0.952381 +f3v5s30 f3v1s26 0.968254 +f3v5s30 f3v6s31 0.952381 +f3v5s30 f3v11s36 0.936508 +f3v5s30 f3v4s29 0.968254 +f3v5s30 f3v12s37 0.968254 +f3v5s30 f3v14s39 0.984127 +f1v8s15 f2v1s23 1.000000 +f1v8s15 f1v6s13 0.964286 +f1v8s15 f1v1s8 0.964912 +f1v8s15 f1v14s21 0.964286 +f1v8s15 f1v5s12 0.964912 +f1v8s15 f1v4s11 1.000000 +f1v8s15 f1v9s16 0.982456 +f1v8s15 f1v10s17 0.964912 +f1v8s15 f1v0s7 1.000000 +f1v8s15 f1v7s14 1.000000 +f1v8s15 f1v3s10 1.000000 +f1v8s15 f1v13s20 0.982143 +f1v8s15 f1v11s18 0.982456 +f1v8s15 f1v12s19 0.964912 +f2v1s23 f1v6s13 0.964286 +f2v1s23 f1v1s8 0.965517 +f2v1s23 f1v14s21 0.982143 +f2v1s23 f1v5s12 0.965517 +f2v1s23 f1v4s11 1.000000 +f2v1s23 f1v9s16 0.982759 +f2v1s23 f1v10s17 0.965517 +f2v1s23 f1v0s7 1.000000 +f2v1s23 f1v7s14 1.000000 +f2v1s23 f1v3s10 1.000000 +f2v1s23 f1v13s20 1.000000 +f2v1s23 f1v11s18 0.982759 +f2v1s23 f1v12s19 0.982456 +f4v2s42 f4v0s40 0.983333 +f4v2s42 f4v1s41 0.967213 +f0v4s4 f0v1s1 1.000000 +f0v4s4 f2v0s22 0.971014 +f0v4s4 f1v2s9 1.000000 +f0v4s4 f0v5s5 0.971014 +f0v4s4 f0v6s6 0.927536 +f0v4s4 f0v2s2 0.971014 +f0v4s4 f0v3s3 0.970149 +f0v4s4 f0v0s0 0.985507 +f1v6s13 f1v1s8 0.928571 +f1v6s13 f1v14s21 0.910714 +f1v6s13 f1v5s12 0.928571 +f1v6s13 f1v4s11 0.964286 +f1v6s13 f1v9s16 0.946429 +f1v6s13 f1v10s17 0.946429 +f1v6s13 f1v0s7 0.964286 +f1v6s13 f1v7s14 0.964286 +f1v6s13 f1v3s10 0.964286 +f1v6s13 f1v13s20 0.928571 +f1v6s13 f1v11s18 0.946429 +f1v6s13 f1v12s19 0.928571 +f3v8s33 f3v9s34 0.969231 +f3v8s33 f3v2s27 0.984615 +f3v8s33 f3v0s25 0.984848 +f3v8s33 f3v3s28 0.969697 +f3v8s33 f3v13s38 0.984848 +f3v8s33 f3v7s32 0.969231 +f3v8s33 f3v1s26 0.969231 +f3v8s33 f3v6s31 0.969231 +f3v8s33 f3v11s36 0.953846 +f3v8s33 f3v4s29 0.969697 +f3v8s33 f3v12s37 0.969697 +f3v8s33 f3v14s39 0.984848 +f1v1s8 f1v14s21 0.982143 +f1v1s8 f1v5s12 0.900000 +f1v1s8 f1v4s11 0.965517 +f1v1s8 f1v9s16 0.916667 +f1v1s8 f1v10s17 0.900000 +f1v1s8 f1v0s7 0.965517 +f1v1s8 f1v7s14 0.965517 +f1v1s8 f1v3s10 0.965517 +f1v1s8 f1v13s20 0.964286 +f1v1s8 f1v11s18 0.948276 +f1v1s8 f1v12s19 0.964912 +f1v14s21 f1v5s12 0.946429 +f1v14s21 f1v4s11 0.982143 +f1v14s21 f1v9s16 0.964286 +f1v14s21 f1v10s17 0.946429 +f1v14s21 f1v0s7 0.982143 +f1v14s21 f1v7s14 0.982143 +f1v14s21 f1v3s10 0.982143 +f1v14s21 f1v13s20 0.946429 +f1v14s21 f1v11s18 0.964286 +f1v14s21 f1v12s19 0.946429 +f0v1s1 f2v0s22 0.971014 +f0v1s1 f1v2s9 1.000000 +f0v1s1 f0v5s5 0.971014 +f0v1s1 f0v6s6 0.927536 +f0v1s1 f0v2s2 0.971014 +f0v1s1 f0v3s3 0.970149 +f0v1s1 f0v0s0 0.985507 +f1v5s12 f1v4s11 0.965517 +f1v5s12 f1v9s16 0.916667 +f1v5s12 f1v10s17 0.916667 +f1v5s12 f1v0s7 0.965517 +f1v5s12 f1v7s14 0.965517 +f1v5s12 f1v3s10 0.965517 +f1v5s12 f1v13s20 0.964286 +f1v5s12 f1v11s18 0.948276 +f1v5s12 f1v12s19 0.964912 +f1v4s11 f1v9s16 0.982759 +f1v4s11 f1v10s17 0.965517 +f1v4s11 f1v0s7 1.000000 +f1v4s11 f1v7s14 1.000000 +f1v4s11 f1v3s10 1.000000 +f1v4s11 f1v13s20 1.000000 +f1v4s11 f1v11s18 0.982759 +f1v4s11 f1v12s19 0.982456 +f1v9s16 f1v10s17 0.916667 +f1v9s16 f1v0s7 0.982759 +f1v9s16 f1v7s14 0.982759 +f1v9s16 f1v3s10 0.982759 +f1v9s16 f1v13s20 0.982143 +f1v9s16 f1v11s18 0.965517 +f1v9s16 f1v12s19 0.964912 +f1v10s17 f1v0s7 0.965517 +f1v10s17 f1v7s14 0.965517 +f1v10s17 f1v3s10 0.965517 +f1v10s17 f1v13s20 0.964286 +f1v10s17 f1v11s18 0.948276 +f1v10s17 f1v12s19 0.964912 +f1v0s7 f1v7s14 1.000000 +f1v0s7 f1v3s10 1.000000 +f1v0s7 f1v13s20 1.000000 +f1v0s7 f1v11s18 0.982759 +f1v0s7 f1v12s19 0.982456 +f3v9s34 f3v2s27 0.969231 +f3v9s34 f3v0s25 0.984615 +f3v9s34 f3v3s28 0.969231 +f3v9s34 f3v13s38 0.984615 +f3v9s34 f3v7s32 1.000000 +f3v9s34 f3v1s26 0.953846 +f3v9s34 f3v6s31 1.000000 +f3v9s34 f3v11s36 0.938462 +f3v9s34 f3v4s29 0.969231 +f3v9s34 f3v12s37 0.969231 +f3v9s34 f3v14s39 0.984615 +f4v0s40 f4v1s41 1.000000 +f1v7s14 f1v3s10 1.000000 +f1v7s14 f1v13s20 1.000000 +f1v7s14 f1v11s18 0.982759 +f1v7s14 f1v12s19 0.982456 +f3v2s27 f3v0s25 1.000000 +f3v2s27 f3v3s28 0.984615 +f3v2s27 f3v13s38 1.000000 +f3v2s27 f3v7s32 0.969231 +f3v2s27 f3v1s26 0.969231 +f3v2s27 f3v6s31 0.969231 +f3v2s27 f3v11s36 0.953846 +f3v2s27 f3v4s29 0.984615 +f3v2s27 f3v12s37 0.984615 +f3v2s27 f3v14s39 1.000000 +f2v0s22 f1v2s9 0.971014 +f2v0s22 f0v5s5 0.971014 +f2v0s22 f0v6s6 0.927536 +f2v0s22 f0v2s2 1.000000 +f2v0s22 f0v3s3 0.970149 +f2v0s22 f0v0s0 0.985507 +f1v2s9 f0v5s5 0.971014 +f1v2s9 f0v6s6 0.927536 +f1v2s9 f0v2s2 0.971014 +f1v2s9 f0v3s3 0.970149 +f1v2s9 f0v0s0 0.985507 +f1v3s10 f1v13s20 1.000000 +f1v3s10 f1v11s18 0.982759 +f1v3s10 f1v12s19 0.982456 +f3v0s25 f3v3s28 0.984848 +f3v0s25 f3v13s38 1.000000 +f3v0s25 f3v7s32 0.984615 +f3v0s25 f3v1s26 0.984615 +f3v0s25 f3v6s31 0.984615 +f3v0s25 f3v11s36 0.969231 +f3v0s25 f3v4s29 0.984848 +f3v0s25 f3v12s37 0.984848 +f3v0s25 f3v14s39 1.000000 +f3v3s28 f3v13s38 0.984848 +f3v3s28 f3v7s32 0.969231 +f3v3s28 f3v1s26 0.969231 +f3v3s28 f3v6s31 0.969231 +f3v3s28 f3v11s36 0.953846 +f3v3s28 f3v4s29 0.969697 +f3v3s28 f3v12s37 0.969697 +f3v3s28 f3v14s39 0.984848 +f3v13s38 f3v7s32 0.984615 +f3v13s38 f3v1s26 0.984615 +f3v13s38 f3v6s31 0.984615 +f3v13s38 f3v11s36 0.969231 +f3v13s38 f3v4s29 0.955882 +f3v13s38 f3v12s37 0.970149 +f3v13s38 f3v14s39 1.000000 +f3v7s32 f3v1s26 0.953846 +f3v7s32 f3v6s31 1.000000 +f3v7s32 f3v11s36 0.938462 +f3v7s32 f3v4s29 0.969231 +f3v7s32 f3v12s37 0.969231 +f3v7s32 f3v14s39 0.984615 +f3v1s26 f3v6s31 0.953846 +f3v1s26 f3v11s36 0.938462 +f3v1s26 f3v4s29 0.969231 +f3v1s26 f3v12s37 0.969231 +f3v1s26 f3v14s39 0.984615 +f3v6s31 f3v11s36 0.938462 +f3v6s31 f3v4s29 0.969231 +f3v6s31 f3v12s37 0.969231 +f3v6s31 f3v14s39 0.984615 +f1v13s20 f1v11s18 0.982143 +f1v13s20 f1v12s19 0.964286 +f3v11s36 f3v4s29 0.953846 +f3v11s36 f3v12s37 0.953846 +f3v11s36 f3v14s39 0.969231 +f0v5s5 f0v6s6 0.927536 +f0v5s5 f0v2s2 0.971014 +f0v5s5 f0v3s3 0.970149 +f0v5s5 f0v0s0 0.985507 +f1v11s18 f1v12s19 0.964912 +f0v6s6 f0v2s2 0.927536 +f0v6s6 f0v3s3 0.925373 +f0v6s6 f0v0s0 0.942029 +f3v4s29 f3v12s37 0.955224 +f3v4s29 f3v14s39 0.984848 +f0v2s2 f0v3s3 0.970149 +f0v2s2 f0v0s0 0.985507 +f3v12s37 f3v14s39 0.984848 +f0v3s3 f0v0s0 0.985075 diff --git a/pkg/obitools/obialign/testdata/two_all.expected b/pkg/obitools/obialign/testdata/two_all.expected new file mode 100644 index 0000000..fec3278 --- /dev/null +++ b/pkg/obitools/obialign/testdata/two_all.expected @@ -0,0 +1,1075 @@ +f3v10s35 f2v9s19 0.487805 87579 6506 65 67 +f3v10s35 f2v12s22 0.465116 87579 93800 65 67 +f3v10s35 f0v0s0 0.469880 87579 63627 65 68 +f3v10s35 f1v6s8 0.469880 87579 97064 65 68 +f3v10s35 f2v13s23 0.487500 87579 70974 65 66 +f3v10s35 f2v4s14 0.487179 87579 13384 65 63 +f3v10s35 f2v3s13 0.475610 87579 15798 65 67 +f3v10s35 f2v2s12 0.493671 87579 95647 65 65 +f3v10s35 f2v6s16 0.493827 87579 64696 65 65 +f3v10s35 f2v7s17 0.487500 87579 14079 65 66 +f3v10s35 f1v3s5 0.426667 87579 42963 65 48 +f3v10s35 f2v10s20 0.487500 87579 31703 65 66 +f3v10s35 f2v1s11 0.506329 87579 2372 65 65 +f3v10s35 f0v1s1 0.469880 87579 77580 65 68 +f3v10s35 f2v8s18 0.458824 87579 3218 65 64 +f3v10s35 f2v0s10 0.487500 87579 77079 65 66 +f3v10s35 f1v2s4 0.424658 87579 24865 65 48 +f3v10s35 f2v5s15 0.493506 87579 34483 65 64 +f3v10s35 f1v7s9 0.438356 87579 84209 65 50 +f3v10s35 f1v5s7 0.438356 87579 34528 65 49 +f3v10s35 f1v0s2 0.432432 87579 16977 65 51 +f3v10s35 f2v11s21 0.493827 87579 82250 65 65 +f3v10s35 f1v4s6 0.452055 87579 15749 65 53 +f3v10s35 f1v1s3 0.432432 87579 53350 65 51 +f3v10s35 f2v14s24 0.500000 87579 27352 65 66 +f3v5s30 f2v9s19 0.469136 39802 6506 63 67 +f3v5s30 f2v12s22 0.452381 39802 93800 63 67 +f3v5s30 f0v0s0 0.441860 39802 63627 63 68 +f3v5s30 f1v6s8 0.441860 39802 97064 63 68 +f3v5s30 f2v13s23 0.468354 39802 70974 63 66 +f3v5s30 f2v4s14 0.473684 39802 13384 63 63 +f3v5s30 f2v3s13 0.462500 39802 15798 63 67 +f3v5s30 f2v2s12 0.474359 39802 95647 63 65 +f3v5s30 f2v6s16 0.487179 39802 64696 63 65 +f3v5s30 f2v7s17 0.468354 39802 14079 63 66 +f3v5s30 f1v3s5 0.424658 39802 42963 63 48 +f3v5s30 f2v10s20 0.468354 39802 31703 63 66 +f3v5s30 f2v1s11 0.468354 39802 2372 63 65 +f3v5s30 f0v1s1 0.441860 39802 77580 63 68 +f3v5s30 f2v8s18 0.444444 39802 3218 63 64 +f3v5s30 f2v0s10 0.468354 39802 77079 63 66 +f3v5s30 f1v2s4 0.450704 39802 24865 63 48 +f3v5s30 f2v5s15 0.493333 39802 34483 63 64 +f3v5s30 f1v7s9 0.463768 39802 84209 63 50 +f3v5s30 f1v5s7 0.455882 39802 34528 63 49 +f3v5s30 f1v0s2 0.438356 39802 16977 63 51 +f3v5s30 f2v11s21 0.487179 39802 82250 63 65 +f3v5s30 f1v4s6 0.470588 39802 15749 63 53 +f3v5s30 f1v1s3 0.438356 39802 53350 63 51 +f3v5s30 f2v14s24 0.480519 39802 27352 63 66 +f1v8s15 f2v9s19 0.513514 66109 6506 57 67 +f1v8s15 f2v12s22 0.506667 66109 93800 57 67 +f1v8s15 f0v0s0 0.541667 66109 63627 57 68 +f1v8s15 f1v6s8 0.541667 66109 97064 57 68 +f1v8s15 f2v13s23 0.520548 66109 70974 57 66 +f1v8s15 f2v4s14 0.513889 66109 13384 57 63 +f1v8s15 f2v3s13 0.500000 66109 15798 57 67 +f1v8s15 f2v2s12 0.527778 66109 95647 57 65 +f1v8s15 f2v6s16 0.541667 66109 64696 57 65 +f1v8s15 f2v7s17 0.520548 66109 14079 57 66 +f1v8s15 f1v3s5 0.515625 66109 42963 57 48 +f1v8s15 f2v10s20 0.520548 66109 31703 57 66 +f1v8s15 f2v1s11 0.520548 66109 2372 57 65 +f1v8s15 f0v1s1 0.541667 66109 77580 57 68 +f1v8s15 f2v8s18 0.520548 66109 3218 57 64 +f1v8s15 f2v0s10 0.520548 66109 77079 57 66 +f1v8s15 f1v2s4 0.515625 66109 24865 57 48 +f1v8s15 f2v5s15 0.541667 66109 34483 57 64 +f1v8s15 f1v7s9 0.538462 66109 84209 57 50 +f1v8s15 f1v5s7 0.515625 66109 34528 57 49 +f1v8s15 f1v0s2 0.523077 66109 16977 57 51 +f1v8s15 f2v11s21 0.541667 66109 82250 57 65 +f1v8s15 f1v4s6 0.507463 66109 15749 57 53 +f1v8s15 f1v1s3 0.523077 66109 53350 57 51 +f1v8s15 f2v14s24 0.520548 66109 27352 57 66 +f2v1s23 f2v9s19 0.520000 35696 6506 58 67 +f2v1s23 f2v12s22 0.513158 35696 93800 58 67 +f2v1s23 f0v0s0 0.547945 35696 63627 58 68 +f2v1s23 f1v6s8 0.547945 35696 97064 58 68 +f2v1s23 f2v13s23 0.527027 35696 70974 58 66 +f2v1s23 f2v4s14 0.520548 35696 13384 58 63 +f2v1s23 f2v3s13 0.506667 35696 15798 58 67 +f2v1s23 f2v2s12 0.534247 35696 95647 58 65 +f2v1s23 f2v6s16 0.547945 35696 64696 58 65 +f2v1s23 f2v7s17 0.527027 35696 14079 58 66 +f2v1s23 f1v3s5 0.523077 35696 42963 58 48 +f2v1s23 f2v10s20 0.527027 35696 31703 58 66 +f2v1s23 f2v1s11 0.520548 35696 2372 58 65 +f2v1s23 f0v1s1 0.547945 35696 77580 58 68 +f2v1s23 f2v8s18 0.520548 35696 3218 58 64 +f2v1s23 f2v0s10 0.527027 35696 77079 58 66 +f2v1s23 f1v2s4 0.523077 35696 24865 58 48 +f2v1s23 f2v5s15 0.547945 35696 34483 58 64 +f2v1s23 f1v7s9 0.545455 35696 84209 58 50 +f2v1s23 f1v5s7 0.523077 35696 34528 58 49 +f2v1s23 f1v0s2 0.530303 35696 16977 58 51 +f2v1s23 f2v11s21 0.547945 35696 82250 58 65 +f2v1s23 f1v4s6 0.514706 35696 15749 58 53 +f2v1s23 f1v1s3 0.530303 35696 53350 58 51 +f2v1s23 f2v14s24 0.527027 35696 27352 58 66 +f4v2s42 f2v9s19 0.506329 17302 6506 61 67 +f4v2s42 f2v12s22 0.506329 17302 93800 61 67 +f4v2s42 f0v0s0 0.518072 17302 63627 61 68 +f4v2s42 f1v6s8 0.518072 17302 97064 61 68 +f4v2s42 f2v13s23 0.512821 17302 70974 61 66 +f4v2s42 f2v4s14 0.513158 17302 13384 61 63 +f4v2s42 f2v3s13 0.487500 17302 15798 61 67 +f4v2s42 f2v2s12 0.506494 17302 95647 61 65 +f4v2s42 f2v6s16 0.500000 17302 64696 61 65 +f4v2s42 f2v7s17 0.512821 17302 14079 61 66 +f4v2s42 f1v3s5 0.522388 17302 42963 61 48 +f4v2s42 f2v10s20 0.512821 17302 31703 61 66 +f4v2s42 f2v1s11 0.506494 17302 2372 61 65 +f4v2s42 f0v1s1 0.518072 17302 77580 61 68 +f4v2s42 f2v8s18 0.500000 17302 3218 61 64 +f4v2s42 f2v0s10 0.512821 17302 77079 61 66 +f4v2s42 f1v2s4 0.485294 17302 24865 61 48 +f4v2s42 f2v5s15 0.500000 17302 34483 61 64 +f4v2s42 f1v7s9 0.478261 17302 84209 61 50 +f4v2s42 f1v5s7 0.522388 17302 34528 61 49 +f4v2s42 f1v0s2 0.514706 17302 16977 61 51 +f4v2s42 f2v11s21 0.500000 17302 82250 61 65 +f4v2s42 f1v4s6 0.521739 17302 15749 61 53 +f4v2s42 f1v1s3 0.514706 17302 53350 61 51 +f4v2s42 f2v14s24 0.518987 17302 27352 61 66 +f0v4s4 f2v9s19 0.481928 86916 6506 69 67 +f0v4s4 f2v12s22 0.481928 86916 93800 69 67 +f0v4s4 f0v0s0 0.505747 86916 63627 69 68 +f0v4s4 f1v6s8 0.505747 86916 97064 69 68 +f0v4s4 f2v13s23 0.487805 86916 70974 69 66 +f0v4s4 f2v4s14 0.469880 86916 13384 69 63 +f0v4s4 f2v3s13 0.487805 86916 15798 69 67 +f0v4s4 f2v2s12 0.476190 86916 95647 69 65 +f0v4s4 f2v6s16 0.470588 86916 64696 69 65 +f0v4s4 f2v7s17 0.487805 86916 14079 69 66 +f0v4s4 f1v3s5 0.465753 86916 42963 69 48 +f0v4s4 f2v10s20 0.487805 86916 31703 69 66 +f0v4s4 f2v1s11 0.482353 86916 2372 69 65 +f0v4s4 f0v1s1 0.505747 86916 77580 69 68 +f0v4s4 f2v8s18 0.481481 86916 3218 69 64 +f0v4s4 f2v0s10 0.487805 86916 77079 69 66 +f0v4s4 f1v2s4 0.441558 86916 24865 69 48 +f0v4s4 f2v5s15 0.458824 86916 34483 69 64 +f0v4s4 f1v7s9 0.459459 86916 84209 69 50 +f0v4s4 f1v5s7 0.486111 86916 34528 69 49 +f0v4s4 f1v0s2 0.472973 86916 16977 69 51 +f0v4s4 f2v11s21 0.470588 86916 82250 69 65 +f0v4s4 f1v4s6 0.466667 86916 15749 69 53 +f0v4s4 f1v1s3 0.472973 86916 53350 69 51 +f0v4s4 f2v14s24 0.482353 86916 27352 69 66 +f1v6s13 f2v9s19 0.486111 80246 6506 56 67 +f1v6s13 f2v12s22 0.479452 80246 93800 56 67 +f1v6s13 f0v0s0 0.534247 80246 63627 56 68 +f1v6s13 f1v6s8 0.534247 80246 97064 56 68 +f1v6s13 f2v13s23 0.492958 80246 70974 56 66 +f1v6s13 f2v4s14 0.492754 80246 13384 56 63 +f1v6s13 f2v3s13 0.478873 80246 15798 56 67 +f1v6s13 f2v2s12 0.500000 80246 95647 56 65 +f1v6s13 f2v6s16 0.521127 80246 64696 56 65 +f1v6s13 f2v7s17 0.492958 80246 14079 56 66 +f1v6s13 f1v3s5 0.548387 80246 42963 56 48 +f1v6s13 f2v10s20 0.492958 80246 31703 56 66 +f1v6s13 f2v1s11 0.492958 80246 2372 56 65 +f1v6s13 f0v1s1 0.534247 80246 77580 56 68 +f1v6s13 f2v8s18 0.500000 80246 3218 56 64 +f1v6s13 f2v0s10 0.492958 80246 77079 56 66 +f1v6s13 f1v2s4 0.548387 80246 24865 56 48 +f1v6s13 f2v5s15 0.507042 80246 34483 56 64 +f1v6s13 f1v7s9 0.571429 80246 84209 56 50 +f1v6s13 f1v5s7 0.540984 80246 34528 56 49 +f1v6s13 f1v0s2 0.555556 80246 16977 56 51 +f1v6s13 f2v11s21 0.521127 80246 82250 56 65 +f1v6s13 f1v4s6 0.538462 80246 15749 56 53 +f1v6s13 f1v1s3 0.555556 80246 53350 56 51 +f1v6s13 f2v14s24 0.493151 80246 27352 56 66 +f3v8s33 f2v9s19 0.458824 88635 6506 66 67 +f3v8s33 f2v12s22 0.438202 88635 93800 66 67 +f3v8s33 f0v0s0 0.481013 88635 63627 66 68 +f3v8s33 f1v6s8 0.481013 88635 97064 66 68 +f3v8s33 f2v13s23 0.457831 88635 70974 66 66 +f3v8s33 f2v4s14 0.456790 88635 13384 66 63 +f3v8s33 f2v3s13 0.452381 88635 15798 66 67 +f3v8s33 f2v2s12 0.457831 88635 95647 66 65 +f3v8s33 f2v6s16 0.469136 88635 64696 66 65 +f3v8s33 f2v7s17 0.457831 88635 14079 66 66 +f3v8s33 f1v3s5 0.445946 88635 42963 66 48 +f3v8s33 f2v10s20 0.457831 88635 31703 66 66 +f3v8s33 f2v1s11 0.475000 88635 2372 66 65 +f3v8s33 f0v1s1 0.481013 88635 77580 66 68 +f3v8s33 f2v8s18 0.445783 88635 3218 66 64 +f3v8s33 f2v0s10 0.457831 88635 77079 66 66 +f3v8s33 f1v2s4 0.452055 88635 24865 66 48 +f3v8s33 f2v5s15 0.475000 88635 34483 66 64 +f3v8s33 f1v7s9 0.459459 88635 84209 66 50 +f3v8s33 f1v5s7 0.465753 88635 34528 66 49 +f3v8s33 f1v0s2 0.459459 88635 16977 66 51 +f3v8s33 f2v11s21 0.469136 88635 82250 66 65 +f3v8s33 f1v4s6 0.447368 88635 15749 66 53 +f3v8s33 f1v1s3 0.459459 88635 53350 66 51 +f3v8s33 f2v14s24 0.481481 88635 27352 66 66 +f1v1s8 f2v9s19 0.526316 17651 6506 60 67 +f1v1s8 f2v12s22 0.519481 17651 93800 60 67 +f1v1s8 f0v0s0 0.526316 17651 63627 60 68 +f1v1s8 f1v6s8 0.526316 17651 97064 60 68 +f1v1s8 f2v13s23 0.533333 17651 70974 60 66 +f1v1s8 f2v4s14 0.527027 17651 13384 60 63 +f1v1s8 f2v3s13 0.506494 17651 15798 60 67 +f1v1s8 f2v2s12 0.540541 17651 95647 60 65 +f1v1s8 f2v6s16 0.554054 17651 64696 60 65 +f1v1s8 f2v7s17 0.533333 17651 14079 60 66 +f1v1s8 f1v3s5 0.523077 17651 42963 60 48 +f1v1s8 f2v10s20 0.533333 17651 31703 60 66 +f1v1s8 f2v1s11 0.506667 17651 2372 60 65 +f1v1s8 f0v1s1 0.526316 17651 77580 60 68 +f1v1s8 f2v8s18 0.520000 17651 3218 60 64 +f1v1s8 f2v0s10 0.533333 17651 77079 60 66 +f1v1s8 f1v2s4 0.523077 17651 24865 60 48 +f1v1s8 f2v5s15 0.561644 17651 34483 60 64 +f1v1s8 f1v7s9 0.529412 17651 84209 60 50 +f1v1s8 f1v5s7 0.531250 17651 34528 60 49 +f1v1s8 f1v0s2 0.522388 17651 16977 60 51 +f1v1s8 f2v11s21 0.554054 17651 82250 60 65 +f1v1s8 f1v4s6 0.522388 17651 15749 60 53 +f1v1s8 f1v1s3 0.522388 17651 53350 60 51 +f1v1s8 f2v14s24 0.533333 17651 27352 60 66 +f1v14s21 f2v9s19 0.493333 85030 6506 56 67 +f1v14s21 f2v12s22 0.486842 85030 93800 56 67 +f1v14s21 f0v0s0 0.555556 85030 63627 56 68 +f1v14s21 f1v6s8 0.555556 85030 97064 56 68 +f1v14s21 f2v13s23 0.500000 85030 70974 56 66 +f1v14s21 f2v4s14 0.506849 85030 13384 56 63 +f1v14s21 f2v3s13 0.480000 85030 15798 56 67 +f1v14s21 f2v2s12 0.506849 85030 95647 56 65 +f1v14s21 f2v6s16 0.520548 85030 64696 56 65 +f1v14s21 f2v7s17 0.500000 85030 14079 56 66 +f1v14s21 f1v3s5 0.500000 85030 42963 56 48 +f1v14s21 f2v10s20 0.500000 85030 31703 56 66 +f1v14s21 f2v1s11 0.493151 85030 2372 56 65 +f1v14s21 f0v1s1 0.555556 85030 77580 56 68 +f1v14s21 f2v8s18 0.500000 85030 3218 56 64 +f1v14s21 f2v0s10 0.500000 85030 77079 56 66 +f1v14s21 f1v2s4 0.515625 85030 24865 56 48 +f1v14s21 f2v5s15 0.527778 85030 34483 56 64 +f1v14s21 f1v7s9 0.523077 85030 84209 56 50 +f1v14s21 f1v5s7 0.507692 85030 34528 56 49 +f1v14s21 f1v0s2 0.507692 85030 16977 56 51 +f1v14s21 f2v11s21 0.520548 85030 82250 56 65 +f1v14s21 f1v4s6 0.507692 85030 15749 56 53 +f1v14s21 f1v1s3 0.507692 85030 53350 56 51 +f1v14s21 f2v14s24 0.500000 85030 27352 56 66 +f0v1s1 f2v9s19 0.481928 43883 6506 69 67 +f0v1s1 f2v12s22 0.481928 43883 93800 69 67 +f0v1s1 f0v0s0 0.505747 43883 63627 69 68 +f0v1s1 f1v6s8 0.505747 43883 97064 69 68 +f0v1s1 f2v13s23 0.487805 43883 70974 69 66 +f0v1s1 f2v4s14 0.469880 43883 13384 69 63 +f0v1s1 f2v3s13 0.487805 43883 15798 69 67 +f0v1s1 f2v2s12 0.476190 43883 95647 69 65 +f0v1s1 f2v6s16 0.470588 43883 64696 69 65 +f0v1s1 f2v7s17 0.487805 43883 14079 69 66 +f0v1s1 f1v3s5 0.465753 43883 42963 69 48 +f0v1s1 f2v10s20 0.487805 43883 31703 69 66 +f0v1s1 f2v1s11 0.482353 43883 2372 69 65 +f0v1s1 f0v1s1 0.505747 43883 77580 69 68 +f0v1s1 f2v8s18 0.481481 43883 3218 69 64 +f0v1s1 f2v0s10 0.487805 43883 77079 69 66 +f0v1s1 f1v2s4 0.441558 43883 24865 69 48 +f0v1s1 f2v5s15 0.458824 43883 34483 69 64 +f0v1s1 f1v7s9 0.459459 43883 84209 69 50 +f0v1s1 f1v5s7 0.486111 43883 34528 69 49 +f0v1s1 f1v0s2 0.472973 43883 16977 69 51 +f0v1s1 f2v11s21 0.470588 43883 82250 69 65 +f0v1s1 f1v4s6 0.466667 43883 15749 69 53 +f0v1s1 f1v1s3 0.472973 43883 53350 69 51 +f0v1s1 f2v14s24 0.482353 43883 27352 69 66 +f1v5s12 f2v9s19 0.487500 1410 6506 60 67 +f1v5s12 f2v12s22 0.513158 1410 93800 60 67 +f1v5s12 f0v0s0 0.533333 1410 63627 60 68 +f1v5s12 f1v6s8 0.533333 1410 97064 60 68 +f1v5s12 f2v13s23 0.493671 1410 70974 60 66 +f1v5s12 f2v4s14 0.520000 1410 13384 60 63 +f1v5s12 f2v3s13 0.493671 1410 15798 60 67 +f1v5s12 f2v2s12 0.493671 1410 95647 60 65 +f1v5s12 f2v6s16 0.520548 1410 64696 60 65 +f1v5s12 f2v7s17 0.493671 1410 14079 60 66 +f1v5s12 f1v3s5 0.530303 1410 42963 60 48 +f1v5s12 f2v10s20 0.493671 1410 31703 60 66 +f1v5s12 f2v1s11 0.481013 1410 2372 60 65 +f1v5s12 f0v1s1 0.533333 1410 77580 60 68 +f1v5s12 f2v8s18 0.500000 1410 3218 60 64 +f1v5s12 f2v0s10 0.493671 1410 77079 60 66 +f1v5s12 f1v2s4 0.522388 1410 24865 60 48 +f1v5s12 f2v5s15 0.527027 1410 34483 60 64 +f1v5s12 f1v7s9 0.537313 1410 84209 60 50 +f1v5s12 f1v5s7 0.515152 1410 34528 60 49 +f1v5s12 f1v0s2 0.529412 1410 16977 60 51 +f1v5s12 f2v11s21 0.520548 1410 82250 60 65 +f1v5s12 f1v4s6 0.545455 1410 15749 60 53 +f1v5s12 f1v1s3 0.529412 1410 53350 60 51 +f1v5s12 f2v14s24 0.513514 1410 27352 60 66 +f1v4s11 f2v9s19 0.520000 74527 6506 58 67 +f1v4s11 f2v12s22 0.513158 74527 93800 58 67 +f1v4s11 f0v0s0 0.547945 74527 63627 58 68 +f1v4s11 f1v6s8 0.547945 74527 97064 58 68 +f1v4s11 f2v13s23 0.527027 74527 70974 58 66 +f1v4s11 f2v4s14 0.520548 74527 13384 58 63 +f1v4s11 f2v3s13 0.506667 74527 15798 58 67 +f1v4s11 f2v2s12 0.534247 74527 95647 58 65 +f1v4s11 f2v6s16 0.547945 74527 64696 58 65 +f1v4s11 f2v7s17 0.527027 74527 14079 58 66 +f1v4s11 f1v3s5 0.523077 74527 42963 58 48 +f1v4s11 f2v10s20 0.527027 74527 31703 58 66 +f1v4s11 f2v1s11 0.520548 74527 2372 58 65 +f1v4s11 f0v1s1 0.547945 74527 77580 58 68 +f1v4s11 f2v8s18 0.520548 74527 3218 58 64 +f1v4s11 f2v0s10 0.527027 74527 77079 58 66 +f1v4s11 f1v2s4 0.523077 74527 24865 58 48 +f1v4s11 f2v5s15 0.547945 74527 34483 58 64 +f1v4s11 f1v7s9 0.545455 74527 84209 58 50 +f1v4s11 f1v5s7 0.523077 74527 34528 58 49 +f1v4s11 f1v0s2 0.530303 74527 16977 58 51 +f1v4s11 f2v11s21 0.547945 74527 82250 58 65 +f1v4s11 f1v4s6 0.514706 74527 15749 58 53 +f1v4s11 f1v1s3 0.530303 74527 53350 58 51 +f1v4s11 f2v14s24 0.527027 74527 27352 58 66 +f1v9s16 f2v9s19 0.519481 65777 6506 60 67 +f1v9s16 f2v12s22 0.519481 65777 93800 60 67 +f1v9s16 f0v0s0 0.560000 65777 63627 60 68 +f1v9s16 f1v6s8 0.560000 65777 97064 60 68 +f1v9s16 f2v13s23 0.526316 65777 70974 60 66 +f1v9s16 f2v4s14 0.520000 65777 13384 60 63 +f1v9s16 f2v3s13 0.506494 65777 15798 60 67 +f1v9s16 f2v2s12 0.533333 65777 95647 60 65 +f1v9s16 f2v6s16 0.533333 65777 64696 60 65 +f1v9s16 f2v7s17 0.526316 65777 14079 60 66 +f1v9s16 f1v3s5 0.522388 65777 42963 60 48 +f1v9s16 f2v10s20 0.526316 65777 31703 60 66 +f1v9s16 f2v1s11 0.520000 65777 2372 60 65 +f1v9s16 f0v1s1 0.560000 65777 77580 60 68 +f1v9s16 f2v8s18 0.506667 65777 3218 60 64 +f1v9s16 f2v0s10 0.526316 65777 77079 60 66 +f1v9s16 f1v2s4 0.522388 65777 24865 60 48 +f1v9s16 f2v5s15 0.546667 65777 34483 60 64 +f1v9s16 f1v7s9 0.536232 65777 84209 60 50 +f1v9s16 f1v5s7 0.514706 65777 34528 60 49 +f1v9s16 f1v0s2 0.529412 65777 16977 60 51 +f1v9s16 f2v11s21 0.533333 65777 82250 60 65 +f1v9s16 f1v4s6 0.514286 65777 15749 60 53 +f1v9s16 f1v1s3 0.529412 65777 53350 60 51 +f1v9s16 f2v14s24 0.526316 65777 27352 60 66 +f1v10s17 f2v9s19 0.480519 98659 6506 60 67 +f1v10s17 f2v12s22 0.474359 98659 93800 60 67 +f1v10s17 f0v0s0 0.560000 98659 63627 60 68 +f1v10s17 f1v6s8 0.560000 98659 97064 60 68 +f1v10s17 f2v13s23 0.486842 98659 70974 60 66 +f1v10s17 f2v4s14 0.480000 98659 13384 60 63 +f1v10s17 f2v3s13 0.430233 98659 15798 60 67 +f1v10s17 f2v2s12 0.493333 98659 95647 60 65 +f1v10s17 f2v6s16 0.506667 98659 64696 60 65 +f1v10s17 f2v7s17 0.486842 98659 14079 60 66 +f1v10s17 f1v3s5 0.545455 98659 42963 60 48 +f1v10s17 f2v10s20 0.486842 98659 31703 60 66 +f1v10s17 f2v1s11 0.480000 98659 2372 60 65 +f1v10s17 f0v1s1 0.560000 98659 77580 60 68 +f1v10s17 f2v8s18 0.480000 98659 3218 60 64 +f1v10s17 f2v0s10 0.486842 98659 77079 60 66 +f1v10s17 f1v2s4 0.530303 98659 24865 60 48 +f1v10s17 f2v5s15 0.506667 98659 34483 60 64 +f1v10s17 f1v7s9 0.560606 98659 84209 60 50 +f1v10s17 f1v5s7 0.529412 98659 34528 60 49 +f1v10s17 f1v0s2 0.552239 98659 16977 60 51 +f1v10s17 f2v11s21 0.506667 98659 82250 60 65 +f1v10s17 f1v4s6 0.536232 98659 15749 60 53 +f1v10s17 f1v1s3 0.552239 98659 53350 60 51 +f1v10s17 f2v14s24 0.486842 98659 27352 60 66 +f1v0s7 f2v9s19 0.520000 84420 6506 58 67 +f1v0s7 f2v12s22 0.513158 84420 93800 58 67 +f1v0s7 f0v0s0 0.547945 84420 63627 58 68 +f1v0s7 f1v6s8 0.547945 84420 97064 58 68 +f1v0s7 f2v13s23 0.527027 84420 70974 58 66 +f1v0s7 f2v4s14 0.520548 84420 13384 58 63 +f1v0s7 f2v3s13 0.506667 84420 15798 58 67 +f1v0s7 f2v2s12 0.534247 84420 95647 58 65 +f1v0s7 f2v6s16 0.547945 84420 64696 58 65 +f1v0s7 f2v7s17 0.527027 84420 14079 58 66 +f1v0s7 f1v3s5 0.523077 84420 42963 58 48 +f1v0s7 f2v10s20 0.527027 84420 31703 58 66 +f1v0s7 f2v1s11 0.520548 84420 2372 58 65 +f1v0s7 f0v1s1 0.547945 84420 77580 58 68 +f1v0s7 f2v8s18 0.520548 84420 3218 58 64 +f1v0s7 f2v0s10 0.527027 84420 77079 58 66 +f1v0s7 f1v2s4 0.523077 84420 24865 58 48 +f1v0s7 f2v5s15 0.547945 84420 34483 58 64 +f1v0s7 f1v7s9 0.545455 84420 84209 58 50 +f1v0s7 f1v5s7 0.523077 84420 34528 58 49 +f1v0s7 f1v0s2 0.530303 84420 16977 58 51 +f1v0s7 f2v11s21 0.547945 84420 82250 58 65 +f1v0s7 f1v4s6 0.514706 84420 15749 58 53 +f1v0s7 f1v1s3 0.530303 84420 53350 58 51 +f1v0s7 f2v14s24 0.527027 84420 27352 58 66 +f3v9s34 f2v9s19 0.464286 40677 6506 65 67 +f3v9s34 f2v12s22 0.469136 40677 93800 65 67 +f3v9s34 f0v0s0 0.500000 40677 63627 65 68 +f3v9s34 f1v6s8 0.500000 40677 97064 65 68 +f3v9s34 f2v13s23 0.463415 40677 70974 65 66 +f3v9s34 f2v4s14 0.469136 40677 13384 65 63 +f3v9s34 f2v3s13 0.457831 40677 15798 65 67 +f3v9s34 f2v2s12 0.463415 40677 95647 65 65 +f3v9s34 f2v6s16 0.462500 40677 64696 65 65 +f3v9s34 f2v7s17 0.463415 40677 14079 65 66 +f3v9s34 f1v3s5 0.452055 40677 42963 65 48 +f3v9s34 f2v10s20 0.463415 40677 31703 65 66 +f3v9s34 f2v1s11 0.469136 40677 2372 65 65 +f3v9s34 f0v1s1 0.500000 40677 77580 65 68 +f3v9s34 f2v8s18 0.445783 40677 3218 65 64 +f3v9s34 f2v0s10 0.463415 40677 77079 65 66 +f3v9s34 f1v2s4 0.428571 40677 24865 65 48 +f3v9s34 f2v5s15 0.469136 40677 34483 65 64 +f3v9s34 f1v7s9 0.441558 40677 84209 65 50 +f3v9s34 f1v5s7 0.447368 40677 34528 65 49 +f3v9s34 f1v0s2 0.435897 40677 16977 65 51 +f3v9s34 f2v11s21 0.462500 40677 82250 65 65 +f3v9s34 f1v4s6 0.447368 40677 15749 65 53 +f3v9s34 f1v1s3 0.435897 40677 53350 65 51 +f3v9s34 f2v14s24 0.475610 40677 27352 65 66 +f4v0s40 f2v9s19 0.506329 75843 6506 60 67 +f4v0s40 f2v12s22 0.513158 75843 93800 60 67 +f4v0s40 f0v0s0 0.512195 75843 63627 60 68 +f4v0s40 f1v6s8 0.512195 75843 97064 60 68 +f4v0s40 f2v13s23 0.512821 75843 70974 60 66 +f4v0s40 f2v4s14 0.513514 75843 13384 60 63 +f4v0s40 f2v3s13 0.487500 75843 15798 60 67 +f4v0s40 f2v2s12 0.506494 75843 95647 60 65 +f4v0s40 f2v6s16 0.513158 75843 64696 60 65 +f4v0s40 f2v7s17 0.512821 75843 14079 60 66 +f4v0s40 f1v3s5 0.530303 75843 42963 60 48 +f4v0s40 f2v10s20 0.512821 75843 31703 60 66 +f4v0s40 f2v1s11 0.500000 75843 2372 60 65 +f4v0s40 f0v1s1 0.512195 75843 77580 60 68 +f4v0s40 f2v8s18 0.500000 75843 3218 60 64 +f4v0s40 f2v0s10 0.512821 75843 77079 60 66 +f4v0s40 f1v2s4 0.492537 75843 24865 60 48 +f4v0s40 f2v5s15 0.513514 75843 34483 60 64 +f4v0s40 f1v7s9 0.485294 75843 84209 60 50 +f4v0s40 f1v5s7 0.530303 75843 34528 60 49 +f4v0s40 f1v0s2 0.522388 75843 16977 60 51 +f4v0s40 f2v11s21 0.513158 75843 82250 60 65 +f4v0s40 f1v4s6 0.529412 75843 15749 60 53 +f4v0s40 f1v1s3 0.522388 75843 53350 60 51 +f4v0s40 f2v14s24 0.513158 75843 27352 60 66 +f1v7s14 f2v9s19 0.520000 42277 6506 58 67 +f1v7s14 f2v12s22 0.513158 42277 93800 58 67 +f1v7s14 f0v0s0 0.547945 42277 63627 58 68 +f1v7s14 f1v6s8 0.547945 42277 97064 58 68 +f1v7s14 f2v13s23 0.527027 42277 70974 58 66 +f1v7s14 f2v4s14 0.520548 42277 13384 58 63 +f1v7s14 f2v3s13 0.506667 42277 15798 58 67 +f1v7s14 f2v2s12 0.534247 42277 95647 58 65 +f1v7s14 f2v6s16 0.547945 42277 64696 58 65 +f1v7s14 f2v7s17 0.527027 42277 14079 58 66 +f1v7s14 f1v3s5 0.523077 42277 42963 58 48 +f1v7s14 f2v10s20 0.527027 42277 31703 58 66 +f1v7s14 f2v1s11 0.520548 42277 2372 58 65 +f1v7s14 f0v1s1 0.547945 42277 77580 58 68 +f1v7s14 f2v8s18 0.520548 42277 3218 58 64 +f1v7s14 f2v0s10 0.527027 42277 77079 58 66 +f1v7s14 f1v2s4 0.523077 42277 24865 58 48 +f1v7s14 f2v5s15 0.547945 42277 34483 58 64 +f1v7s14 f1v7s9 0.545455 42277 84209 58 50 +f1v7s14 f1v5s7 0.523077 42277 34528 58 49 +f1v7s14 f1v0s2 0.530303 42277 16977 58 51 +f1v7s14 f2v11s21 0.547945 42277 82250 58 65 +f1v7s14 f1v4s6 0.514706 42277 15749 58 53 +f1v7s14 f1v1s3 0.530303 42277 53350 58 51 +f1v7s14 f2v14s24 0.527027 42277 27352 58 66 +f3v2s27 f2v9s19 0.458824 74030 6506 65 67 +f3v2s27 f2v12s22 0.443182 74030 93800 65 67 +f3v2s27 f0v0s0 0.487179 74030 63627 65 68 +f3v2s27 f1v6s8 0.487179 74030 97064 65 68 +f3v2s27 f2v13s23 0.457831 74030 70974 65 66 +f3v2s27 f2v4s14 0.469136 74030 13384 65 63 +f3v2s27 f2v3s13 0.452381 74030 15798 65 67 +f3v2s27 f2v2s12 0.463415 74030 95647 65 65 +f3v2s27 f2v6s16 0.475000 74030 64696 65 65 +f3v2s27 f2v7s17 0.457831 74030 14079 65 66 +f3v2s27 f1v3s5 0.444444 74030 42963 65 48 +f3v2s27 f2v10s20 0.457831 74030 31703 65 66 +f3v2s27 f2v1s11 0.475000 74030 2372 65 65 +f3v2s27 f0v1s1 0.487179 74030 77580 65 68 +f3v2s27 f2v8s18 0.452381 74030 3218 65 64 +f3v2s27 f2v0s10 0.457831 74030 77079 65 66 +f3v2s27 f1v2s4 0.444444 74030 24865 65 48 +f3v2s27 f2v5s15 0.487179 74030 34483 65 64 +f3v2s27 f1v7s9 0.465753 74030 84209 65 50 +f3v2s27 f1v5s7 0.458333 74030 34528 65 49 +f3v2s27 f1v0s2 0.452055 74030 16977 65 51 +f3v2s27 f2v11s21 0.475000 74030 82250 65 65 +f3v2s27 f1v4s6 0.458333 74030 15749 65 53 +f3v2s27 f1v1s3 0.452055 74030 53350 65 51 +f3v2s27 f2v14s24 0.487500 74030 27352 65 66 +f4v1s41 f2v9s19 0.506329 45686 6506 61 67 +f4v1s41 f2v12s22 0.513158 45686 93800 61 67 +f4v1s41 f0v0s0 0.512195 45686 63627 61 68 +f4v1s41 f1v6s8 0.512195 45686 97064 61 68 +f4v1s41 f2v13s23 0.512821 45686 70974 61 66 +f4v1s41 f2v4s14 0.527027 45686 13384 61 63 +f4v1s41 f2v3s13 0.487500 45686 15798 61 67 +f4v1s41 f2v2s12 0.506494 45686 95647 61 65 +f4v1s41 f2v6s16 0.512821 45686 64696 61 65 +f4v1s41 f2v7s17 0.512821 45686 14079 61 66 +f4v1s41 f1v3s5 0.530303 45686 42963 61 48 +f4v1s41 f2v10s20 0.512821 45686 31703 61 66 +f4v1s41 f2v1s11 0.500000 45686 2372 61 65 +f4v1s41 f0v1s1 0.512195 45686 77580 61 68 +f4v1s41 f2v8s18 0.506494 45686 3218 61 64 +f4v1s41 f2v0s10 0.512821 45686 77079 61 66 +f4v1s41 f1v2s4 0.492537 45686 24865 61 48 +f4v1s41 f2v5s15 0.520000 45686 34483 61 64 +f4v1s41 f1v7s9 0.485714 45686 84209 61 50 +f4v1s41 f1v5s7 0.530303 45686 34528 61 49 +f4v1s41 f1v0s2 0.522388 45686 16977 61 51 +f4v1s41 f2v11s21 0.512821 45686 82250 61 65 +f4v1s41 f1v4s6 0.529412 45686 15749 61 53 +f4v1s41 f1v1s3 0.522388 45686 53350 61 51 +f4v1s41 f2v14s24 0.513158 45686 27352 61 66 +f2v0s22 f2v9s19 0.506173 43219 6506 69 67 +f2v0s22 f2v12s22 0.512500 43219 93800 69 67 +f2v0s22 f0v0s0 0.500000 43219 63627 69 68 +f2v0s22 f1v6s8 0.500000 43219 97064 69 68 +f2v0s22 f2v13s23 0.512500 43219 70974 69 66 +f2v0s22 f2v4s14 0.487805 43219 13384 69 63 +f2v0s22 f2v3s13 0.506329 43219 15798 69 67 +f2v0s22 f2v2s12 0.512821 43219 95647 69 65 +f2v0s22 f2v6s16 0.506329 43219 64696 69 65 +f2v0s22 f2v7s17 0.512500 43219 14079 69 66 +f2v0s22 f1v3s5 0.464789 43219 42963 69 48 +f2v0s22 f2v10s20 0.512500 43219 31703 69 66 +f2v0s22 f2v1s11 0.518987 43219 2372 69 65 +f2v0s22 f0v1s1 0.500000 43219 77580 69 68 +f2v0s22 f2v8s18 0.500000 43219 3218 69 64 +f2v0s22 f2v0s10 0.512500 43219 77079 69 66 +f2v0s22 f1v2s4 0.428571 43219 24865 69 48 +f2v0s22 f2v5s15 0.476190 43219 34483 69 64 +f2v0s22 f1v7s9 0.417722 43219 84209 69 50 +f2v0s22 f1v5s7 0.435897 43219 34528 69 49 +f2v0s22 f1v0s2 0.430380 43219 16977 69 51 +f2v0s22 f2v11s21 0.506329 43219 82250 69 65 +f2v0s22 f1v4s6 0.419753 43219 15749 69 53 +f2v0s22 f1v1s3 0.430380 43219 53350 69 51 +f2v0s22 f2v14s24 0.471264 43219 27352 69 66 +f1v2s9 f2v9s19 0.481928 99108 6506 69 67 +f1v2s9 f2v12s22 0.481928 99108 93800 69 67 +f1v2s9 f0v0s0 0.505747 99108 63627 69 68 +f1v2s9 f1v6s8 0.505747 99108 97064 69 68 +f1v2s9 f2v13s23 0.487805 99108 70974 69 66 +f1v2s9 f2v4s14 0.469880 99108 13384 69 63 +f1v2s9 f2v3s13 0.487805 99108 15798 69 67 +f1v2s9 f2v2s12 0.476190 99108 95647 69 65 +f1v2s9 f2v6s16 0.470588 99108 64696 69 65 +f1v2s9 f2v7s17 0.487805 99108 14079 69 66 +f1v2s9 f1v3s5 0.465753 99108 42963 69 48 +f1v2s9 f2v10s20 0.487805 99108 31703 69 66 +f1v2s9 f2v1s11 0.482353 99108 2372 69 65 +f1v2s9 f0v1s1 0.505747 99108 77580 69 68 +f1v2s9 f2v8s18 0.481481 99108 3218 69 64 +f1v2s9 f2v0s10 0.487805 99108 77079 69 66 +f1v2s9 f1v2s4 0.441558 99108 24865 69 48 +f1v2s9 f2v5s15 0.458824 99108 34483 69 64 +f1v2s9 f1v7s9 0.459459 99108 84209 69 50 +f1v2s9 f1v5s7 0.486111 99108 34528 69 49 +f1v2s9 f1v0s2 0.472973 99108 16977 69 51 +f1v2s9 f2v11s21 0.470588 99108 82250 69 65 +f1v2s9 f1v4s6 0.466667 99108 15749 69 53 +f1v2s9 f1v1s3 0.472973 99108 53350 69 51 +f1v2s9 f2v14s24 0.482353 99108 27352 69 66 +f1v3s10 f2v9s19 0.520000 43230 6506 58 67 +f1v3s10 f2v12s22 0.513158 43230 93800 58 67 +f1v3s10 f0v0s0 0.547945 43230 63627 58 68 +f1v3s10 f1v6s8 0.547945 43230 97064 58 68 +f1v3s10 f2v13s23 0.527027 43230 70974 58 66 +f1v3s10 f2v4s14 0.520548 43230 13384 58 63 +f1v3s10 f2v3s13 0.506667 43230 15798 58 67 +f1v3s10 f2v2s12 0.534247 43230 95647 58 65 +f1v3s10 f2v6s16 0.547945 43230 64696 58 65 +f1v3s10 f2v7s17 0.527027 43230 14079 58 66 +f1v3s10 f1v3s5 0.523077 43230 42963 58 48 +f1v3s10 f2v10s20 0.527027 43230 31703 58 66 +f1v3s10 f2v1s11 0.520548 43230 2372 58 65 +f1v3s10 f0v1s1 0.547945 43230 77580 58 68 +f1v3s10 f2v8s18 0.520548 43230 3218 58 64 +f1v3s10 f2v0s10 0.527027 43230 77079 58 66 +f1v3s10 f1v2s4 0.523077 43230 24865 58 48 +f1v3s10 f2v5s15 0.547945 43230 34483 58 64 +f1v3s10 f1v7s9 0.545455 43230 84209 58 50 +f1v3s10 f1v5s7 0.523077 43230 34528 58 49 +f1v3s10 f1v0s2 0.530303 43230 16977 58 51 +f1v3s10 f2v11s21 0.547945 43230 82250 58 65 +f1v3s10 f1v4s6 0.514706 43230 15749 58 53 +f1v3s10 f1v1s3 0.530303 43230 53350 58 51 +f1v3s10 f2v14s24 0.527027 43230 27352 58 66 +f3v0s25 f2v9s19 0.458824 62755 6506 66 67 +f3v0s25 f2v12s22 0.438202 62755 93800 66 67 +f3v0s25 f0v0s0 0.493671 62755 63627 66 68 +f3v0s25 f1v6s8 0.493671 62755 97064 66 68 +f3v0s25 f2v13s23 0.457831 62755 70974 66 66 +f3v0s25 f2v4s14 0.463415 62755 13384 66 63 +f3v0s25 f2v3s13 0.452381 62755 15798 66 67 +f3v0s25 f2v2s12 0.457831 62755 95647 66 65 +f3v0s25 f2v6s16 0.469136 62755 64696 66 65 +f3v0s25 f2v7s17 0.457831 62755 14079 66 66 +f3v0s25 f1v3s5 0.445946 62755 42963 66 48 +f3v0s25 f2v10s20 0.457831 62755 31703 66 66 +f3v0s25 f2v1s11 0.475000 62755 2372 66 65 +f3v0s25 f0v1s1 0.493671 62755 77580 66 68 +f3v0s25 f2v8s18 0.447059 62755 3218 66 64 +f3v0s25 f2v0s10 0.457831 62755 77079 66 66 +f3v0s25 f1v2s4 0.452055 62755 24865 66 48 +f3v0s25 f2v5s15 0.481013 62755 34483 66 64 +f3v0s25 f1v7s9 0.459459 62755 84209 66 50 +f3v0s25 f1v5s7 0.465753 62755 34528 66 49 +f3v0s25 f1v0s2 0.459459 62755 16977 66 51 +f3v0s25 f2v11s21 0.469136 62755 82250 66 65 +f3v0s25 f1v4s6 0.465753 62755 15749 66 53 +f3v0s25 f1v1s3 0.459459 62755 53350 66 51 +f3v0s25 f2v14s24 0.481481 62755 27352 66 66 +f3v3s28 f2v9s19 0.469136 66465 6506 66 67 +f3v3s28 f2v12s22 0.447059 66465 93800 66 67 +f3v3s28 f0v0s0 0.493671 66465 63627 66 68 +f3v3s28 f1v6s8 0.493671 66465 97064 66 68 +f3v3s28 f2v13s23 0.468354 66465 70974 66 66 +f3v3s28 f2v4s14 0.445783 66465 13384 66 63 +f3v3s28 f2v3s13 0.456790 66465 15798 66 67 +f3v3s28 f2v2s12 0.474359 66465 95647 66 65 +f3v3s28 f2v6s16 0.475000 66465 64696 66 65 +f3v3s28 f2v7s17 0.468354 66465 14079 66 66 +f3v3s28 f1v3s5 0.445946 66465 42963 66 48 +f3v3s28 f2v10s20 0.468354 66465 31703 66 66 +f3v3s28 f2v1s11 0.475000 66465 2372 66 65 +f3v3s28 f0v1s1 0.493671 66465 77580 66 68 +f3v3s28 f2v8s18 0.435294 66465 3218 66 64 +f3v3s28 f2v0s10 0.468354 66465 77079 66 66 +f3v3s28 f1v2s4 0.459459 66465 24865 66 48 +f3v3s28 f2v5s15 0.452381 66465 34483 66 64 +f3v3s28 f1v7s9 0.478873 66465 84209 66 50 +f3v3s28 f1v5s7 0.472973 66465 34528 66 49 +f3v3s28 f1v0s2 0.466667 66465 16977 66 51 +f3v3s28 f2v11s21 0.475000 66465 82250 66 65 +f3v3s28 f1v4s6 0.479452 66465 15749 66 53 +f3v3s28 f1v1s3 0.466667 66465 53350 66 51 +f3v3s28 f2v14s24 0.487500 66465 27352 66 66 +f3v13s38 f2v9s19 0.449438 26948 6506 68 67 +f3v13s38 f2v12s22 0.453488 26948 93800 68 67 +f3v13s38 f0v0s0 0.493671 26948 63627 68 68 +f3v13s38 f1v6s8 0.493671 26948 97064 68 68 +f3v13s38 f2v13s23 0.448276 26948 70974 68 66 +f3v13s38 f2v4s14 0.452381 26948 13384 68 63 +f3v13s38 f2v3s13 0.448276 26948 15798 68 67 +f3v13s38 f2v2s12 0.448276 26948 95647 68 65 +f3v13s38 f2v6s16 0.475610 26948 64696 68 65 +f3v13s38 f2v7s17 0.448276 26948 14079 68 66 +f3v13s38 f1v3s5 0.453333 26948 42963 68 48 +f3v13s38 f2v10s20 0.448276 26948 31703 68 66 +f3v13s38 f2v1s11 0.469136 26948 2372 68 65 +f3v13s38 f0v1s1 0.493671 26948 77580 68 68 +f3v13s38 f2v8s18 0.436782 26948 3218 68 64 +f3v13s38 f2v0s10 0.448276 26948 77079 68 66 +f3v13s38 f1v2s4 0.453333 26948 24865 68 48 +f3v13s38 f2v5s15 0.475000 26948 34483 68 64 +f3v13s38 f1v7s9 0.466667 26948 84209 68 50 +f3v13s38 f1v5s7 0.466667 26948 34528 68 49 +f3v13s38 f1v0s2 0.460526 26948 16977 68 51 +f3v13s38 f2v11s21 0.475610 26948 82250 68 65 +f3v13s38 f1v4s6 0.480000 26948 15749 68 53 +f3v13s38 f1v1s3 0.460526 26948 53350 68 51 +f3v13s38 f2v14s24 0.487805 26948 27352 68 66 +f3v7s32 f2v9s19 0.464286 53710 6506 65 67 +f3v7s32 f2v12s22 0.469136 53710 93800 65 67 +f3v7s32 f0v0s0 0.500000 53710 63627 65 68 +f3v7s32 f1v6s8 0.500000 53710 97064 65 68 +f3v7s32 f2v13s23 0.463415 53710 70974 65 66 +f3v7s32 f2v4s14 0.469136 53710 13384 65 63 +f3v7s32 f2v3s13 0.457831 53710 15798 65 67 +f3v7s32 f2v2s12 0.463415 53710 95647 65 65 +f3v7s32 f2v6s16 0.462500 53710 64696 65 65 +f3v7s32 f2v7s17 0.463415 53710 14079 65 66 +f3v7s32 f1v3s5 0.452055 53710 42963 65 48 +f3v7s32 f2v10s20 0.463415 53710 31703 65 66 +f3v7s32 f2v1s11 0.469136 53710 2372 65 65 +f3v7s32 f0v1s1 0.500000 53710 77580 65 68 +f3v7s32 f2v8s18 0.445783 53710 3218 65 64 +f3v7s32 f2v0s10 0.463415 53710 77079 65 66 +f3v7s32 f1v2s4 0.428571 53710 24865 65 48 +f3v7s32 f2v5s15 0.469136 53710 34483 65 64 +f3v7s32 f1v7s9 0.441558 53710 84209 65 50 +f3v7s32 f1v5s7 0.447368 53710 34528 65 49 +f3v7s32 f1v0s2 0.435897 53710 16977 65 51 +f3v7s32 f2v11s21 0.462500 53710 82250 65 65 +f3v7s32 f1v4s6 0.447368 53710 15749 65 53 +f3v7s32 f1v1s3 0.435897 53710 53350 65 51 +f3v7s32 f2v14s24 0.475610 53710 27352 65 66 +f3v1s26 f2v9s19 0.469136 41283 6506 65 67 +f3v1s26 f2v12s22 0.441860 41283 93800 65 67 +f3v1s26 f0v0s0 0.481013 41283 63627 65 68 +f3v1s26 f1v6s8 0.481013 41283 97064 65 68 +f3v1s26 f2v13s23 0.468354 41283 70974 65 66 +f3v1s26 f2v4s14 0.456790 41283 13384 65 63 +f3v1s26 f2v3s13 0.451220 41283 15798 65 67 +f3v1s26 f2v2s12 0.474359 41283 95647 65 65 +f3v1s26 f2v6s16 0.475000 41283 64696 65 65 +f3v1s26 f2v7s17 0.468354 41283 14079 65 66 +f3v1s26 f1v3s5 0.436620 41283 42963 65 48 +f3v1s26 f2v10s20 0.468354 41283 31703 65 66 +f3v1s26 f2v1s11 0.481013 41283 2372 65 65 +f3v1s26 f0v1s1 0.481013 41283 77580 65 68 +f3v1s26 f2v8s18 0.440476 41283 3218 65 64 +f3v1s26 f2v0s10 0.468354 41283 77079 65 66 +f3v1s26 f1v2s4 0.442857 41283 24865 65 48 +f3v1s26 f2v5s15 0.474359 41283 34483 65 64 +f3v1s26 f1v7s9 0.450704 41283 84209 65 50 +f3v1s26 f1v5s7 0.450704 41283 34528 65 49 +f3v1s26 f1v0s2 0.444444 41283 16977 65 51 +f3v1s26 f2v11s21 0.475000 41283 82250 65 65 +f3v1s26 f1v4s6 0.458333 41283 15749 65 53 +f3v1s26 f1v1s3 0.444444 41283 53350 65 51 +f3v1s26 f2v14s24 0.487500 41283 27352 65 66 +f3v6s31 f2v9s19 0.464286 94783 6506 65 67 +f3v6s31 f2v12s22 0.469136 94783 93800 65 67 +f3v6s31 f0v0s0 0.500000 94783 63627 65 68 +f3v6s31 f1v6s8 0.500000 94783 97064 65 68 +f3v6s31 f2v13s23 0.463415 94783 70974 65 66 +f3v6s31 f2v4s14 0.469136 94783 13384 65 63 +f3v6s31 f2v3s13 0.457831 94783 15798 65 67 +f3v6s31 f2v2s12 0.463415 94783 95647 65 65 +f3v6s31 f2v6s16 0.462500 94783 64696 65 65 +f3v6s31 f2v7s17 0.463415 94783 14079 65 66 +f3v6s31 f1v3s5 0.452055 94783 42963 65 48 +f3v6s31 f2v10s20 0.463415 94783 31703 65 66 +f3v6s31 f2v1s11 0.469136 94783 2372 65 65 +f3v6s31 f0v1s1 0.500000 94783 77580 65 68 +f3v6s31 f2v8s18 0.445783 94783 3218 65 64 +f3v6s31 f2v0s10 0.463415 94783 77079 65 66 +f3v6s31 f1v2s4 0.428571 94783 24865 65 48 +f3v6s31 f2v5s15 0.469136 94783 34483 65 64 +f3v6s31 f1v7s9 0.441558 94783 84209 65 50 +f3v6s31 f1v5s7 0.447368 94783 34528 65 49 +f3v6s31 f1v0s2 0.435897 94783 16977 65 51 +f3v6s31 f2v11s21 0.462500 94783 82250 65 65 +f3v6s31 f1v4s6 0.447368 94783 15749 65 53 +f3v6s31 f1v1s3 0.435897 94783 53350 65 51 +f3v6s31 f2v14s24 0.475610 94783 27352 65 66 +f1v13s20 f2v9s19 0.535211 80913 6506 56 67 +f1v13s20 f2v12s22 0.527778 80913 93800 56 67 +f1v13s20 f0v0s0 0.534247 80913 63627 56 68 +f1v13s20 f1v6s8 0.534247 80913 97064 56 68 +f1v13s20 f2v13s23 0.542857 80913 70974 56 66 +f1v13s20 f2v4s14 0.536232 80913 13384 56 63 +f1v13s20 f2v3s13 0.513889 80913 15798 56 67 +f1v13s20 f2v2s12 0.550725 80913 95647 56 65 +f1v13s20 f2v6s16 0.563380 80913 64696 56 65 +f1v13s20 f2v7s17 0.542857 80913 14079 56 66 +f1v13s20 f1v3s5 0.531250 80913 42963 56 48 +f1v13s20 f2v10s20 0.542857 80913 31703 56 66 +f1v13s20 f2v1s11 0.528571 80913 2372 56 65 +f1v13s20 f0v1s1 0.534247 80913 77580 56 68 +f1v13s20 f2v8s18 0.528571 80913 3218 56 64 +f1v13s20 f2v0s10 0.542857 80913 77079 56 66 +f1v13s20 f1v2s4 0.531250 80913 24865 56 48 +f1v13s20 f2v5s15 0.557143 80913 34483 56 64 +f1v13s20 f1v7s9 0.553846 80913 84209 56 50 +f1v13s20 f1v5s7 0.531250 80913 34528 56 49 +f1v13s20 f1v0s2 0.538462 80913 16977 56 51 +f1v13s20 f2v11s21 0.563380 80913 82250 56 65 +f1v13s20 f1v4s6 0.522388 80913 15749 56 53 +f1v13s20 f1v1s3 0.538462 80913 53350 56 51 +f1v13s20 f2v14s24 0.535211 80913 27352 56 66 +f2v2s24 f2v9s19 0.507042 68414 6506 49 67 +f2v2s24 f2v12s22 0.521127 68414 93800 49 67 +f2v2s24 f0v0s0 0.500000 68414 63627 49 68 +f2v2s24 f1v6s8 0.500000 68414 97064 49 68 +f2v2s24 f2v13s23 0.514286 68414 70974 49 66 +f2v2s24 f2v4s14 0.530303 68414 13384 49 63 +f2v2s24 f2v3s13 0.513889 68414 15798 49 67 +f2v2s24 f2v2s12 0.521739 68414 95647 49 65 +f2v2s24 f2v6s16 0.536232 68414 64696 49 65 +f2v2s24 f2v7s17 0.514286 68414 14079 49 66 +f2v2s24 f1v3s5 0.500000 68414 42963 49 48 +f2v2s24 f2v10s20 0.514286 68414 31703 49 66 +f2v2s24 f2v1s11 0.528571 68414 2372 49 65 +f2v2s24 f0v1s1 0.500000 68414 77580 49 68 +f2v2s24 f2v8s18 0.514706 68414 3218 49 64 +f2v2s24 f2v0s10 0.514286 68414 77079 49 66 +f2v2s24 f1v2s4 0.491228 68414 24865 49 48 +f2v2s24 f2v5s15 0.544118 68414 34483 49 64 +f2v2s24 f1v7s9 0.491228 68414 84209 49 50 +f2v2s24 f1v5s7 0.526316 68414 34528 49 49 +f2v2s24 f1v0s2 0.517241 68414 16977 49 51 +f2v2s24 f2v11s21 0.536232 68414 82250 49 65 +f2v2s24 f1v4s6 0.491525 68414 15749 49 53 +f2v2s24 f1v1s3 0.517241 68414 53350 49 51 +f2v2s24 f2v14s24 0.521739 68414 27352 49 66 +f3v11s36 f2v9s19 0.453488 91927 6506 65 67 +f3v11s36 f2v12s22 0.452381 91927 93800 65 67 +f3v11s36 f0v0s0 0.500000 91927 63627 65 68 +f3v11s36 f1v6s8 0.500000 91927 97064 65 68 +f3v11s36 f2v13s23 0.452381 91927 70974 65 66 +f3v11s36 f2v4s14 0.463415 91927 13384 65 63 +f3v11s36 f2v3s13 0.447059 91927 15798 65 67 +f3v11s36 f2v2s12 0.457831 91927 95647 65 65 +f3v11s36 f2v6s16 0.474359 91927 64696 65 65 +f3v11s36 f2v7s17 0.452381 91927 14079 65 66 +f3v11s36 f1v3s5 0.442857 91927 42963 65 48 +f3v11s36 f2v10s20 0.452381 91927 31703 65 66 +f3v11s36 f2v1s11 0.447059 91927 2372 65 65 +f3v11s36 f0v1s1 0.500000 91927 77580 65 68 +f3v11s36 f2v8s18 0.451220 91927 3218 65 64 +f3v11s36 f2v0s10 0.452381 91927 77079 65 66 +f3v11s36 f1v2s4 0.444444 91927 24865 65 48 +f3v11s36 f2v5s15 0.481013 91927 34483 65 64 +f3v11s36 f1v7s9 0.464789 91927 84209 65 50 +f3v11s36 f1v5s7 0.450704 91927 34528 65 49 +f3v11s36 f1v0s2 0.444444 91927 16977 65 51 +f3v11s36 f2v11s21 0.474359 91927 82250 65 65 +f3v11s36 f1v4s6 0.465753 91927 15749 65 53 +f3v11s36 f1v1s3 0.444444 91927 53350 65 51 +f3v11s36 f2v14s24 0.462500 91927 27352 65 66 +f0v5s5 f2v9s19 0.493827 2668 6506 69 67 +f0v5s5 f2v12s22 0.500000 2668 93800 69 67 +f0v5s5 f0v0s0 0.494253 2668 63627 69 68 +f0v5s5 f1v6s8 0.494253 2668 97064 69 68 +f0v5s5 f2v13s23 0.500000 2668 70974 69 66 +f0v5s5 f2v4s14 0.481481 2668 13384 69 63 +f0v5s5 f2v3s13 0.493827 2668 15798 69 67 +f0v5s5 f2v2s12 0.500000 2668 95647 69 65 +f0v5s5 f2v6s16 0.493671 2668 64696 69 65 +f0v5s5 f2v7s17 0.500000 2668 14079 69 66 +f0v5s5 f1v3s5 0.452055 2668 42963 69 48 +f0v5s5 f2v10s20 0.500000 2668 31703 69 66 +f0v5s5 f2v1s11 0.506329 2668 2372 69 65 +f0v5s5 f0v1s1 0.494253 2668 77580 69 68 +f0v5s5 f2v8s18 0.487500 2668 3218 69 64 +f0v5s5 f2v0s10 0.500000 2668 77079 69 66 +f0v5s5 f1v2s4 0.441558 2668 24865 69 48 +f0v5s5 f2v5s15 0.469880 2668 34483 69 64 +f0v5s5 f1v7s9 0.430380 2668 84209 69 50 +f0v5s5 f1v5s7 0.448718 2668 34528 69 49 +f0v5s5 f1v0s2 0.443038 2668 16977 69 51 +f0v5s5 f2v11s21 0.493671 2668 82250 69 65 +f0v5s5 f1v4s6 0.432099 2668 15749 69 53 +f0v5s5 f1v1s3 0.443038 2668 53350 69 51 +f0v5s5 f2v14s24 0.455556 2668 27352 69 66 +f1v11s18 f2v9s19 0.520000 51814 6506 58 67 +f1v11s18 f2v12s22 0.513158 51814 93800 58 67 +f1v11s18 f0v0s0 0.554054 51814 63627 58 68 +f1v11s18 f1v6s8 0.554054 51814 97064 58 68 +f1v11s18 f2v13s23 0.527027 51814 70974 58 66 +f1v11s18 f2v4s14 0.520548 51814 13384 58 63 +f1v11s18 f2v3s13 0.506667 51814 15798 58 67 +f1v11s18 f2v2s12 0.534247 51814 95647 58 65 +f1v11s18 f2v6s16 0.540541 51814 64696 58 65 +f1v11s18 f2v7s17 0.527027 51814 14079 58 66 +f1v11s18 f1v3s5 0.531250 51814 42963 58 48 +f1v11s18 f2v10s20 0.527027 51814 31703 58 66 +f1v11s18 f2v1s11 0.520548 51814 2372 58 65 +f1v11s18 f0v1s1 0.554054 51814 77580 58 68 +f1v11s18 f2v8s18 0.520548 51814 3218 58 64 +f1v11s18 f2v0s10 0.527027 51814 77079 58 66 +f1v11s18 f1v2s4 0.523077 51814 24865 58 48 +f1v11s18 f2v5s15 0.547945 51814 34483 58 64 +f1v11s18 f1v7s9 0.553846 51814 84209 58 50 +f1v11s18 f1v5s7 0.531250 51814 34528 58 49 +f1v11s18 f1v0s2 0.538462 51814 16977 58 51 +f1v11s18 f2v11s21 0.540541 51814 82250 58 65 +f1v11s18 f1v4s6 0.522388 51814 15749 58 53 +f1v11s18 f1v1s3 0.538462 51814 53350 58 51 +f1v11s18 f2v14s24 0.520000 51814 27352 58 66 +f0v6s6 f2v9s19 0.512821 16273 6506 69 67 +f0v6s6 f2v12s22 0.487805 16273 93800 69 67 +f0v6s6 f0v0s0 0.524390 16273 63627 69 68 +f0v6s6 f1v6s8 0.524390 16273 97064 69 68 +f0v6s6 f2v13s23 0.519481 16273 70974 69 66 +f0v6s6 f2v4s14 0.481481 16273 13384 69 63 +f0v6s6 f2v3s13 0.459770 16273 15798 69 67 +f0v6s6 f2v2s12 0.470588 16273 95647 69 65 +f0v6s6 f2v6s16 0.487805 16273 64696 69 65 +f0v6s6 f2v7s17 0.519481 16273 14079 69 66 +f0v6s6 f1v3s5 0.452055 16273 42963 69 48 +f0v6s6 f2v10s20 0.519481 16273 31703 69 66 +f0v6s6 f2v1s11 0.532468 16273 2372 69 65 +f0v6s6 f0v1s1 0.524390 16273 77580 69 68 +f0v6s6 f2v8s18 0.506494 16273 3218 69 64 +f0v6s6 f2v0s10 0.519481 16273 77079 69 66 +f0v6s6 f1v2s4 0.438356 16273 24865 69 48 +f0v6s6 f2v5s15 0.469880 16273 34483 69 64 +f0v6s6 f1v7s9 0.417722 16273 84209 69 50 +f0v6s6 f1v5s7 0.452055 16273 34528 69 49 +f0v6s6 f1v0s2 0.445946 16273 16977 69 51 +f0v6s6 f2v11s21 0.487805 16273 82250 69 65 +f0v6s6 f1v4s6 0.459459 16273 15749 69 53 +f0v6s6 f1v1s3 0.445946 16273 53350 69 51 +f0v6s6 f2v14s24 0.482759 16273 27352 69 66 +f3v4s29 f2v9s19 0.453488 94356 6506 68 67 +f3v4s29 f2v12s22 0.434783 94356 93800 68 67 +f3v4s29 f0v0s0 0.506329 94356 63627 68 68 +f3v4s29 f1v6s8 0.506329 94356 97064 68 68 +f3v4s29 f2v13s23 0.458824 94356 70974 68 66 +f3v4s29 f2v4s14 0.464286 94356 13384 68 63 +f3v4s29 f2v3s13 0.458824 94356 15798 68 67 +f3v4s29 f2v2s12 0.458824 94356 95647 68 65 +f3v4s29 f2v6s16 0.464286 94356 64696 68 65 +f3v4s29 f2v7s17 0.458824 94356 14079 68 66 +f3v4s29 f1v3s5 0.434211 94356 42963 68 48 +f3v4s29 f2v10s20 0.458824 94356 31703 68 66 +f3v4s29 f2v1s11 0.469136 94356 2372 68 65 +f3v4s29 f0v1s1 0.506329 94356 77580 68 68 +f3v4s29 f2v8s18 0.453488 94356 3218 68 64 +f3v4s29 f2v0s10 0.458824 94356 77079 68 66 +f3v4s29 f1v2s4 0.452055 94356 24865 68 48 +f3v4s29 f2v5s15 0.475000 94356 34483 68 64 +f3v4s29 f1v7s9 0.447368 94356 84209 68 50 +f3v4s29 f1v5s7 0.465753 94356 34528 68 49 +f3v4s29 f1v0s2 0.465753 94356 16977 68 51 +f3v4s29 f2v11s21 0.464286 94356 82250 68 65 +f3v4s29 f1v4s6 0.453333 94356 15749 68 53 +f3v4s29 f1v1s3 0.465753 94356 53350 68 51 +f3v4s29 f2v14s24 0.475610 94356 27352 68 66 +f0v2s2 f2v9s19 0.506173 10896 6506 69 67 +f0v2s2 f2v12s22 0.512500 10896 93800 69 67 +f0v2s2 f0v0s0 0.500000 10896 63627 69 68 +f0v2s2 f1v6s8 0.500000 10896 97064 69 68 +f0v2s2 f2v13s23 0.512500 10896 70974 69 66 +f0v2s2 f2v4s14 0.487805 10896 13384 69 63 +f0v2s2 f2v3s13 0.506329 10896 15798 69 67 +f0v2s2 f2v2s12 0.512821 10896 95647 69 65 +f0v2s2 f2v6s16 0.506329 10896 64696 69 65 +f0v2s2 f2v7s17 0.512500 10896 14079 69 66 +f0v2s2 f1v3s5 0.464789 10896 42963 69 48 +f0v2s2 f2v10s20 0.512500 10896 31703 69 66 +f0v2s2 f2v1s11 0.518987 10896 2372 69 65 +f0v2s2 f0v1s1 0.500000 10896 77580 69 68 +f0v2s2 f2v8s18 0.500000 10896 3218 69 64 +f0v2s2 f2v0s10 0.512500 10896 77079 69 66 +f0v2s2 f1v2s4 0.428571 10896 24865 69 48 +f0v2s2 f2v5s15 0.476190 10896 34483 69 64 +f0v2s2 f1v7s9 0.417722 10896 84209 69 50 +f0v2s2 f1v5s7 0.435897 10896 34528 69 49 +f0v2s2 f1v0s2 0.430380 10896 16977 69 51 +f0v2s2 f2v11s21 0.506329 10896 82250 69 65 +f0v2s2 f1v4s6 0.419753 10896 15749 69 53 +f0v2s2 f1v1s3 0.430380 10896 53350 69 51 +f0v2s2 f2v14s24 0.471264 10896 27352 69 66 +f3v12s37 f2v9s19 0.471264 74202 6506 67 67 +f3v12s37 f2v12s22 0.470588 74202 93800 67 67 +f3v12s37 f0v0s0 0.500000 74202 63627 67 68 +f3v12s37 f1v6s8 0.500000 74202 97064 67 68 +f3v12s37 f2v13s23 0.470588 74202 70974 67 66 +f3v12s37 f2v4s14 0.481928 74202 13384 67 63 +f3v12s37 f2v3s13 0.465116 74202 15798 67 67 +f3v12s37 f2v2s12 0.476190 74202 95647 67 65 +f3v12s37 f2v6s16 0.469880 74202 64696 67 65 +f3v12s37 f2v7s17 0.470588 74202 14079 67 66 +f3v12s37 f1v3s5 0.440000 74202 42963 67 48 +f3v12s37 f2v10s20 0.470588 74202 31703 67 66 +f3v12s37 f2v1s11 0.458824 74202 2372 67 65 +f3v12s37 f0v1s1 0.500000 74202 77580 67 68 +f3v12s37 f2v8s18 0.465116 74202 3218 67 64 +f3v12s37 f2v0s10 0.470588 74202 77079 67 66 +f3v12s37 f1v2s4 0.430380 74202 24865 67 48 +f3v12s37 f2v5s15 0.464286 74202 34483 67 64 +f3v12s37 f1v7s9 0.443038 74202 84209 67 50 +f3v12s37 f1v5s7 0.448718 74202 34528 67 49 +f3v12s37 f1v0s2 0.437500 74202 16977 67 51 +f3v12s37 f2v11s21 0.469880 74202 82250 67 65 +f3v12s37 f1v4s6 0.426829 74202 15749 67 53 +f3v12s37 f1v1s3 0.437500 74202 53350 67 51 +f3v12s37 f2v14s24 0.470588 74202 27352 67 66 +f0v3s3 f2v9s19 0.506329 73040 6506 67 67 +f0v3s3 f2v12s22 0.512821 73040 93800 67 67 +f0v3s3 f0v0s0 0.494118 73040 63627 67 68 +f0v3s3 f1v6s8 0.494118 73040 97064 67 68 +f0v3s3 f2v13s23 0.512821 73040 70974 67 66 +f0v3s3 f2v4s14 0.448276 73040 13384 67 63 +f0v3s3 f2v3s13 0.506494 73040 15798 67 67 +f0v3s3 f2v2s12 0.506494 73040 95647 67 65 +f0v3s3 f2v6s16 0.506494 73040 64696 67 65 +f0v3s3 f2v7s17 0.512821 73040 14079 67 66 +f0v3s3 f1v3s5 0.478873 73040 42963 67 48 +f0v3s3 f2v10s20 0.512821 73040 31703 67 66 +f0v3s3 f2v1s11 0.512821 73040 2372 67 65 +f0v3s3 f0v1s1 0.494118 73040 77580 67 68 +f0v3s3 f2v8s18 0.500000 73040 3218 67 64 +f0v3s3 f2v0s10 0.512821 73040 77079 67 66 +f0v3s3 f1v2s4 0.453333 73040 24865 67 48 +f0v3s3 f2v5s15 0.469880 73040 34483 67 64 +f0v3s3 f1v7s9 0.435897 73040 84209 67 50 +f0v3s3 f1v5s7 0.441558 73040 34528 67 49 +f0v3s3 f1v0s2 0.441558 73040 16977 67 51 +f0v3s3 f2v11s21 0.506494 73040 82250 67 65 +f0v3s3 f1v4s6 0.430380 73040 15749 67 53 +f0v3s3 f1v1s3 0.441558 73040 53350 67 51 +f0v3s3 f2v14s24 0.470588 73040 27352 67 66 +f3v14s39 f2v9s19 0.458824 55015 6506 66 67 +f3v14s39 f2v12s22 0.438202 55015 93800 66 67 +f3v14s39 f0v0s0 0.493671 55015 63627 66 68 +f3v14s39 f1v6s8 0.493671 55015 97064 66 68 +f3v14s39 f2v13s23 0.457831 55015 70974 66 66 +f3v14s39 f2v4s14 0.463415 55015 13384 66 63 +f3v14s39 f2v3s13 0.452381 55015 15798 66 67 +f3v14s39 f2v2s12 0.457831 55015 95647 66 65 +f3v14s39 f2v6s16 0.469136 55015 64696 66 65 +f3v14s39 f2v7s17 0.457831 55015 14079 66 66 +f3v14s39 f1v3s5 0.445946 55015 42963 66 48 +f3v14s39 f2v10s20 0.457831 55015 31703 66 66 +f3v14s39 f2v1s11 0.475000 55015 2372 66 65 +f3v14s39 f0v1s1 0.493671 55015 77580 66 68 +f3v14s39 f2v8s18 0.447059 55015 3218 66 64 +f3v14s39 f2v0s10 0.457831 55015 77079 66 66 +f3v14s39 f1v2s4 0.452055 55015 24865 66 48 +f3v14s39 f2v5s15 0.481013 55015 34483 66 64 +f3v14s39 f1v7s9 0.459459 55015 84209 66 50 +f3v14s39 f1v5s7 0.465753 55015 34528 66 49 +f3v14s39 f1v0s2 0.459459 55015 16977 66 51 +f3v14s39 f2v11s21 0.469136 55015 82250 66 65 +f3v14s39 f1v4s6 0.465753 55015 15749 66 53 +f3v14s39 f1v1s3 0.459459 55015 53350 66 51 +f3v14s39 f2v14s24 0.481481 55015 27352 66 66 +f0v0s0 f2v9s19 0.493827 1332 6506 69 67 +f0v0s0 f2v12s22 0.500000 1332 93800 69 67 +f0v0s0 f0v0s0 0.505747 1332 63627 69 68 +f0v0s0 f1v6s8 0.505747 1332 97064 69 68 +f0v0s0 f2v13s23 0.500000 1332 70974 69 66 +f0v0s0 f2v4s14 0.481481 1332 13384 69 63 +f0v0s0 f2v3s13 0.500000 1332 15798 69 67 +f0v0s0 f2v2s12 0.454545 1332 95647 69 65 +f0v0s0 f2v6s16 0.459770 1332 64696 69 65 +f0v0s0 f2v7s17 0.500000 1332 14079 69 66 +f0v0s0 f1v3s5 0.465753 1332 42963 69 48 +f0v0s0 f2v10s20 0.500000 1332 31703 69 66 +f0v0s0 f2v1s11 0.465909 1332 2372 69 65 +f0v0s0 f0v1s1 0.505747 1332 77580 69 68 +f0v0s0 f2v8s18 0.487500 1332 3218 69 64 +f0v0s0 f2v0s10 0.500000 1332 77079 69 66 +f0v0s0 f1v2s4 0.441558 1332 24865 69 48 +f0v0s0 f2v5s15 0.458824 1332 34483 69 64 +f0v0s0 f1v7s9 0.430380 1332 84209 69 50 +f0v0s0 f1v5s7 0.448718 1332 34528 69 49 +f0v0s0 f1v0s2 0.443038 1332 16977 69 51 +f0v0s0 f2v11s21 0.459770 1332 82250 69 65 +f0v0s0 f1v4s6 0.432099 1332 15749 69 53 +f0v0s0 f1v1s3 0.443038 1332 53350 69 51 +f0v0s0 f2v14s24 0.476744 1332 27352 69 66 +f1v12s19 f2v9s19 0.513514 58178 6506 57 67 +f1v12s19 f2v12s22 0.506667 58178 93800 57 67 +f1v12s19 f0v0s0 0.547945 58178 63627 57 68 +f1v12s19 f1v6s8 0.547945 58178 97064 57 68 +f1v12s19 f2v13s23 0.520548 58178 70974 57 66 +f1v12s19 f2v4s14 0.513889 58178 13384 57 63 +f1v12s19 f2v3s13 0.500000 58178 15798 57 67 +f1v12s19 f2v2s12 0.527778 58178 95647 57 65 +f1v12s19 f2v6s16 0.541667 58178 64696 57 65 +f1v12s19 f2v7s17 0.520548 58178 14079 57 66 +f1v12s19 f1v3s5 0.507692 58178 42963 57 48 +f1v12s19 f2v10s20 0.520548 58178 31703 57 66 +f1v12s19 f2v1s11 0.513889 58178 2372 57 65 +f1v12s19 f0v1s1 0.547945 58178 77580 57 68 +f1v12s19 f2v8s18 0.513889 58178 3218 57 64 +f1v12s19 f2v0s10 0.520548 58178 77079 57 66 +f1v12s19 f1v2s4 0.515625 58178 24865 57 48 +f1v12s19 f2v5s15 0.541667 58178 34483 57 64 +f1v12s19 f1v7s9 0.530303 58178 84209 57 50 +f1v12s19 f1v5s7 0.500000 58178 34528 57 49 +f1v12s19 f1v0s2 0.515152 58178 16977 57 51 +f1v12s19 f2v11s21 0.541667 58178 82250 57 65 +f1v12s19 f1v4s6 0.500000 58178 15749 57 53 +f1v12s19 f1v1s3 0.515152 58178 53350 57 51 +f1v12s19 f2v14s24 0.520548 58178 27352 57 66 diff --git a/pkg/obitools/obialign/testdata/two_alldist.expected b/pkg/obitools/obialign/testdata/two_alldist.expected new file mode 100644 index 0000000..8ac25bf --- /dev/null +++ b/pkg/obitools/obialign/testdata/two_alldist.expected @@ -0,0 +1,1075 @@ +f3v10s35 f2v9s19 42.000000 +f3v10s35 f2v12s22 46.000000 +f3v10s35 f0v0s0 44.000000 +f3v10s35 f1v6s8 44.000000 +f3v10s35 f2v13s23 41.000000 +f3v10s35 f2v4s14 40.000000 +f3v10s35 f2v3s13 43.000000 +f3v10s35 f2v2s12 40.000000 +f3v10s35 f2v6s16 41.000000 +f3v10s35 f2v7s17 41.000000 +f3v10s35 f1v3s5 43.000000 +f3v10s35 f2v10s20 41.000000 +f3v10s35 f2v1s11 39.000000 +f3v10s35 f0v1s1 44.000000 +f3v10s35 f2v8s18 46.000000 +f3v10s35 f2v0s10 41.000000 +f3v10s35 f1v2s4 42.000000 +f3v10s35 f2v5s15 39.000000 +f3v10s35 f1v7s9 41.000000 +f3v10s35 f1v5s7 41.000000 +f3v10s35 f1v0s2 42.000000 +f3v10s35 f2v11s21 41.000000 +f3v10s35 f1v4s6 40.000000 +f3v10s35 f1v1s3 42.000000 +f3v10s35 f2v14s24 40.000000 +f3v5s30 f2v9s19 43.000000 +f3v5s30 f2v12s22 46.000000 +f3v5s30 f0v0s0 48.000000 +f3v5s30 f1v6s8 48.000000 +f3v5s30 f2v13s23 42.000000 +f3v5s30 f2v4s14 40.000000 +f3v5s30 f2v3s13 43.000000 +f3v5s30 f2v2s12 41.000000 +f3v5s30 f2v6s16 40.000000 +f3v5s30 f2v7s17 42.000000 +f3v5s30 f1v3s5 42.000000 +f3v5s30 f2v10s20 42.000000 +f3v5s30 f2v1s11 42.000000 +f3v5s30 f0v1s1 48.000000 +f3v5s30 f2v8s18 45.000000 +f3v5s30 f2v0s10 42.000000 +f3v5s30 f1v2s4 39.000000 +f3v5s30 f2v5s15 38.000000 +f3v5s30 f1v7s9 37.000000 +f3v5s30 f1v5s7 37.000000 +f3v5s30 f1v0s2 41.000000 +f3v5s30 f2v11s21 40.000000 +f3v5s30 f1v4s6 36.000000 +f3v5s30 f1v1s3 41.000000 +f3v5s30 f2v14s24 40.000000 +f1v8s15 f2v9s19 36.000000 +f1v8s15 f2v12s22 37.000000 +f1v8s15 f0v0s0 33.000000 +f1v8s15 f1v6s8 33.000000 +f1v8s15 f2v13s23 35.000000 +f1v8s15 f2v4s14 35.000000 +f1v8s15 f2v3s13 37.000000 +f1v8s15 f2v2s12 34.000000 +f1v8s15 f2v6s16 33.000000 +f1v8s15 f2v7s17 35.000000 +f1v8s15 f1v3s5 31.000000 +f1v8s15 f2v10s20 35.000000 +f1v8s15 f2v1s11 35.000000 +f1v8s15 f0v1s1 33.000000 +f1v8s15 f2v8s18 35.000000 +f1v8s15 f2v0s10 35.000000 +f1v8s15 f1v2s4 31.000000 +f1v8s15 f2v5s15 33.000000 +f1v8s15 f1v7s9 30.000000 +f1v8s15 f1v5s7 31.000000 +f1v8s15 f1v0s2 31.000000 +f1v8s15 f2v11s21 33.000000 +f1v8s15 f1v4s6 33.000000 +f1v8s15 f1v1s3 31.000000 +f1v8s15 f2v14s24 35.000000 +f2v1s23 f2v9s19 36.000000 +f2v1s23 f2v12s22 37.000000 +f2v1s23 f0v0s0 33.000000 +f2v1s23 f1v6s8 33.000000 +f2v1s23 f2v13s23 35.000000 +f2v1s23 f2v4s14 35.000000 +f2v1s23 f2v3s13 37.000000 +f2v1s23 f2v2s12 34.000000 +f2v1s23 f2v6s16 33.000000 +f2v1s23 f2v7s17 35.000000 +f2v1s23 f1v3s5 31.000000 +f2v1s23 f2v10s20 35.000000 +f2v1s23 f2v1s11 35.000000 +f2v1s23 f0v1s1 33.000000 +f2v1s23 f2v8s18 35.000000 +f2v1s23 f2v0s10 35.000000 +f2v1s23 f1v2s4 31.000000 +f2v1s23 f2v5s15 33.000000 +f2v1s23 f1v7s9 30.000000 +f2v1s23 f1v5s7 31.000000 +f2v1s23 f1v0s2 31.000000 +f2v1s23 f2v11s21 33.000000 +f2v1s23 f1v4s6 33.000000 +f2v1s23 f1v1s3 31.000000 +f2v1s23 f2v14s24 35.000000 +f4v2s42 f2v9s19 39.000000 +f4v2s42 f2v12s22 39.000000 +f4v2s42 f0v0s0 40.000000 +f4v2s42 f1v6s8 40.000000 +f4v2s42 f2v13s23 38.000000 +f4v2s42 f2v4s14 37.000000 +f4v2s42 f2v3s13 41.000000 +f4v2s42 f2v2s12 38.000000 +f4v2s42 f2v6s16 39.000000 +f4v2s42 f2v7s17 38.000000 +f4v2s42 f1v3s5 32.000000 +f4v2s42 f2v10s20 38.000000 +f4v2s42 f2v1s11 38.000000 +f4v2s42 f0v1s1 40.000000 +f4v2s42 f2v8s18 38.000000 +f4v2s42 f2v0s10 38.000000 +f4v2s42 f1v2s4 35.000000 +f4v2s42 f2v5s15 39.000000 +f4v2s42 f1v7s9 36.000000 +f4v2s42 f1v5s7 32.000000 +f4v2s42 f1v0s2 33.000000 +f4v2s42 f2v11s21 39.000000 +f4v2s42 f1v4s6 33.000000 +f4v2s42 f1v1s3 33.000000 +f4v2s42 f2v14s24 38.000000 +f0v4s4 f2v9s19 43.000000 +f0v4s4 f2v12s22 43.000000 +f0v4s4 f0v0s0 43.000000 +f0v4s4 f1v6s8 43.000000 +f0v4s4 f2v13s23 42.000000 +f0v4s4 f2v4s14 44.000000 +f0v4s4 f2v3s13 42.000000 +f0v4s4 f2v2s12 44.000000 +f0v4s4 f2v6s16 45.000000 +f0v4s4 f2v7s17 42.000000 +f0v4s4 f1v3s5 39.000000 +f0v4s4 f2v10s20 42.000000 +f0v4s4 f2v1s11 44.000000 +f0v4s4 f0v1s1 43.000000 +f0v4s4 f2v8s18 42.000000 +f0v4s4 f2v0s10 42.000000 +f0v4s4 f1v2s4 43.000000 +f0v4s4 f2v5s15 46.000000 +f0v4s4 f1v7s9 40.000000 +f0v4s4 f1v5s7 37.000000 +f0v4s4 f1v0s2 39.000000 +f0v4s4 f2v11s21 45.000000 +f0v4s4 f1v4s6 40.000000 +f0v4s4 f1v1s3 39.000000 +f0v4s4 f2v14s24 44.000000 +f1v6s13 f2v9s19 37.000000 +f1v6s13 f2v12s22 38.000000 +f1v6s13 f0v0s0 34.000000 +f1v6s13 f1v6s8 34.000000 +f1v6s13 f2v13s23 36.000000 +f1v6s13 f2v4s14 35.000000 +f1v6s13 f2v3s13 37.000000 +f1v6s13 f2v2s12 35.000000 +f1v6s13 f2v6s16 34.000000 +f1v6s13 f2v7s17 36.000000 +f1v6s13 f1v3s5 28.000000 +f1v6s13 f2v10s20 36.000000 +f1v6s13 f2v1s11 36.000000 +f1v6s13 f0v1s1 34.000000 +f1v6s13 f2v8s18 35.000000 +f1v6s13 f2v0s10 36.000000 +f1v6s13 f1v2s4 28.000000 +f1v6s13 f2v5s15 35.000000 +f1v6s13 f1v7s9 27.000000 +f1v6s13 f1v5s7 28.000000 +f1v6s13 f1v0s2 28.000000 +f1v6s13 f2v11s21 34.000000 +f1v6s13 f1v4s6 30.000000 +f1v6s13 f1v1s3 28.000000 +f1v6s13 f2v14s24 37.000000 +f3v8s33 f2v9s19 46.000000 +f3v8s33 f2v12s22 50.000000 +f3v8s33 f0v0s0 41.000000 +f3v8s33 f1v6s8 41.000000 +f3v8s33 f2v13s23 45.000000 +f3v8s33 f2v4s14 44.000000 +f3v8s33 f2v3s13 46.000000 +f3v8s33 f2v2s12 45.000000 +f3v8s33 f2v6s16 43.000000 +f3v8s33 f2v7s17 45.000000 +f3v8s33 f1v3s5 41.000000 +f3v8s33 f2v10s20 45.000000 +f3v8s33 f2v1s11 42.000000 +f3v8s33 f0v1s1 41.000000 +f3v8s33 f2v8s18 46.000000 +f3v8s33 f2v0s10 45.000000 +f3v8s33 f1v2s4 40.000000 +f3v8s33 f2v5s15 42.000000 +f3v8s33 f1v7s9 40.000000 +f3v8s33 f1v5s7 39.000000 +f3v8s33 f1v0s2 40.000000 +f3v8s33 f2v11s21 43.000000 +f3v8s33 f1v4s6 42.000000 +f3v8s33 f1v1s3 40.000000 +f3v8s33 f2v14s24 42.000000 +f1v1s8 f2v9s19 36.000000 +f1v1s8 f2v12s22 37.000000 +f1v1s8 f0v0s0 36.000000 +f1v1s8 f1v6s8 36.000000 +f1v1s8 f2v13s23 35.000000 +f1v1s8 f2v4s14 35.000000 +f1v1s8 f2v3s13 38.000000 +f1v1s8 f2v2s12 34.000000 +f1v1s8 f2v6s16 33.000000 +f1v1s8 f2v7s17 35.000000 +f1v1s8 f1v3s5 31.000000 +f1v1s8 f2v10s20 35.000000 +f1v1s8 f2v1s11 37.000000 +f1v1s8 f0v1s1 36.000000 +f1v1s8 f2v8s18 36.000000 +f1v1s8 f2v0s10 35.000000 +f1v1s8 f1v2s4 31.000000 +f1v1s8 f2v5s15 32.000000 +f1v1s8 f1v7s9 32.000000 +f1v1s8 f1v5s7 30.000000 +f1v1s8 f1v0s2 32.000000 +f1v1s8 f2v11s21 33.000000 +f1v1s8 f1v4s6 32.000000 +f1v1s8 f1v1s3 32.000000 +f1v1s8 f2v14s24 35.000000 +f1v14s21 f2v9s19 38.000000 +f1v14s21 f2v12s22 39.000000 +f1v14s21 f0v0s0 32.000000 +f1v14s21 f1v6s8 32.000000 +f1v14s21 f2v13s23 37.000000 +f1v14s21 f2v4s14 36.000000 +f1v14s21 f2v3s13 39.000000 +f1v14s21 f2v2s12 36.000000 +f1v14s21 f2v6s16 35.000000 +f1v14s21 f2v7s17 37.000000 +f1v14s21 f1v3s5 32.000000 +f1v14s21 f2v10s20 37.000000 +f1v14s21 f2v1s11 37.000000 +f1v14s21 f0v1s1 32.000000 +f1v14s21 f2v8s18 36.000000 +f1v14s21 f2v0s10 37.000000 +f1v14s21 f1v2s4 31.000000 +f1v14s21 f2v5s15 34.000000 +f1v14s21 f1v7s9 31.000000 +f1v14s21 f1v5s7 32.000000 +f1v14s21 f1v0s2 32.000000 +f1v14s21 f2v11s21 35.000000 +f1v14s21 f1v4s6 32.000000 +f1v14s21 f1v1s3 32.000000 +f1v14s21 f2v14s24 37.000000 +f0v1s1 f2v9s19 43.000000 +f0v1s1 f2v12s22 43.000000 +f0v1s1 f0v0s0 43.000000 +f0v1s1 f1v6s8 43.000000 +f0v1s1 f2v13s23 42.000000 +f0v1s1 f2v4s14 44.000000 +f0v1s1 f2v3s13 42.000000 +f0v1s1 f2v2s12 44.000000 +f0v1s1 f2v6s16 45.000000 +f0v1s1 f2v7s17 42.000000 +f0v1s1 f1v3s5 39.000000 +f0v1s1 f2v10s20 42.000000 +f0v1s1 f2v1s11 44.000000 +f0v1s1 f0v1s1 43.000000 +f0v1s1 f2v8s18 42.000000 +f0v1s1 f2v0s10 42.000000 +f0v1s1 f1v2s4 43.000000 +f0v1s1 f2v5s15 46.000000 +f0v1s1 f1v7s9 40.000000 +f0v1s1 f1v5s7 37.000000 +f0v1s1 f1v0s2 39.000000 +f0v1s1 f2v11s21 45.000000 +f0v1s1 f1v4s6 40.000000 +f0v1s1 f1v1s3 39.000000 +f0v1s1 f2v14s24 44.000000 +f1v5s12 f2v9s19 41.000000 +f1v5s12 f2v12s22 37.000000 +f1v5s12 f0v0s0 35.000000 +f1v5s12 f1v6s8 35.000000 +f1v5s12 f2v13s23 40.000000 +f1v5s12 f2v4s14 36.000000 +f1v5s12 f2v3s13 40.000000 +f1v5s12 f2v2s12 40.000000 +f1v5s12 f2v6s16 35.000000 +f1v5s12 f2v7s17 40.000000 +f1v5s12 f1v3s5 31.000000 +f1v5s12 f2v10s20 40.000000 +f1v5s12 f2v1s11 41.000000 +f1v5s12 f0v1s1 35.000000 +f1v5s12 f2v8s18 37.000000 +f1v5s12 f2v0s10 40.000000 +f1v5s12 f1v2s4 32.000000 +f1v5s12 f2v5s15 35.000000 +f1v5s12 f1v7s9 31.000000 +f1v5s12 f1v5s7 32.000000 +f1v5s12 f1v0s2 32.000000 +f1v5s12 f2v11s21 35.000000 +f1v5s12 f1v4s6 30.000000 +f1v5s12 f1v1s3 32.000000 +f1v5s12 f2v14s24 36.000000 +f1v4s11 f2v9s19 36.000000 +f1v4s11 f2v12s22 37.000000 +f1v4s11 f0v0s0 33.000000 +f1v4s11 f1v6s8 33.000000 +f1v4s11 f2v13s23 35.000000 +f1v4s11 f2v4s14 35.000000 +f1v4s11 f2v3s13 37.000000 +f1v4s11 f2v2s12 34.000000 +f1v4s11 f2v6s16 33.000000 +f1v4s11 f2v7s17 35.000000 +f1v4s11 f1v3s5 31.000000 +f1v4s11 f2v10s20 35.000000 +f1v4s11 f2v1s11 35.000000 +f1v4s11 f0v1s1 33.000000 +f1v4s11 f2v8s18 35.000000 +f1v4s11 f2v0s10 35.000000 +f1v4s11 f1v2s4 31.000000 +f1v4s11 f2v5s15 33.000000 +f1v4s11 f1v7s9 30.000000 +f1v4s11 f1v5s7 31.000000 +f1v4s11 f1v0s2 31.000000 +f1v4s11 f2v11s21 33.000000 +f1v4s11 f1v4s6 33.000000 +f1v4s11 f1v1s3 31.000000 +f1v4s11 f2v14s24 35.000000 +f1v9s16 f2v9s19 37.000000 +f1v9s16 f2v12s22 37.000000 +f1v9s16 f0v0s0 33.000000 +f1v9s16 f1v6s8 33.000000 +f1v9s16 f2v13s23 36.000000 +f1v9s16 f2v4s14 36.000000 +f1v9s16 f2v3s13 38.000000 +f1v9s16 f2v2s12 35.000000 +f1v9s16 f2v6s16 35.000000 +f1v9s16 f2v7s17 36.000000 +f1v9s16 f1v3s5 32.000000 +f1v9s16 f2v10s20 36.000000 +f1v9s16 f2v1s11 36.000000 +f1v9s16 f0v1s1 33.000000 +f1v9s16 f2v8s18 37.000000 +f1v9s16 f2v0s10 36.000000 +f1v9s16 f1v2s4 32.000000 +f1v9s16 f2v5s15 34.000000 +f1v9s16 f1v7s9 32.000000 +f1v9s16 f1v5s7 33.000000 +f1v9s16 f1v0s2 32.000000 +f1v9s16 f2v11s21 35.000000 +f1v9s16 f1v4s6 34.000000 +f1v9s16 f1v1s3 32.000000 +f1v9s16 f2v14s24 36.000000 +f1v10s17 f2v9s19 40.000000 +f1v10s17 f2v12s22 41.000000 +f1v10s17 f0v0s0 33.000000 +f1v10s17 f1v6s8 33.000000 +f1v10s17 f2v13s23 39.000000 +f1v10s17 f2v4s14 39.000000 +f1v10s17 f2v3s13 49.000000 +f1v10s17 f2v2s12 38.000000 +f1v10s17 f2v6s16 37.000000 +f1v10s17 f2v7s17 39.000000 +f1v10s17 f1v3s5 30.000000 +f1v10s17 f2v10s20 39.000000 +f1v10s17 f2v1s11 39.000000 +f1v10s17 f0v1s1 33.000000 +f1v10s17 f2v8s18 39.000000 +f1v10s17 f2v0s10 39.000000 +f1v10s17 f1v2s4 31.000000 +f1v10s17 f2v5s15 37.000000 +f1v10s17 f1v7s9 29.000000 +f1v10s17 f1v5s7 32.000000 +f1v10s17 f1v0s2 30.000000 +f1v10s17 f2v11s21 37.000000 +f1v10s17 f1v4s6 32.000000 +f1v10s17 f1v1s3 30.000000 +f1v10s17 f2v14s24 39.000000 +f1v0s7 f2v9s19 36.000000 +f1v0s7 f2v12s22 37.000000 +f1v0s7 f0v0s0 33.000000 +f1v0s7 f1v6s8 33.000000 +f1v0s7 f2v13s23 35.000000 +f1v0s7 f2v4s14 35.000000 +f1v0s7 f2v3s13 37.000000 +f1v0s7 f2v2s12 34.000000 +f1v0s7 f2v6s16 33.000000 +f1v0s7 f2v7s17 35.000000 +f1v0s7 f1v3s5 31.000000 +f1v0s7 f2v10s20 35.000000 +f1v0s7 f2v1s11 35.000000 +f1v0s7 f0v1s1 33.000000 +f1v0s7 f2v8s18 35.000000 +f1v0s7 f2v0s10 35.000000 +f1v0s7 f1v2s4 31.000000 +f1v0s7 f2v5s15 33.000000 +f1v0s7 f1v7s9 30.000000 +f1v0s7 f1v5s7 31.000000 +f1v0s7 f1v0s2 31.000000 +f1v0s7 f2v11s21 33.000000 +f1v0s7 f1v4s6 33.000000 +f1v0s7 f1v1s3 31.000000 +f1v0s7 f2v14s24 35.000000 +f3v9s34 f2v9s19 45.000000 +f3v9s34 f2v12s22 43.000000 +f3v9s34 f0v0s0 39.000000 +f3v9s34 f1v6s8 39.000000 +f3v9s34 f2v13s23 44.000000 +f3v9s34 f2v4s14 43.000000 +f3v9s34 f2v3s13 45.000000 +f3v9s34 f2v2s12 44.000000 +f3v9s34 f2v6s16 43.000000 +f3v9s34 f2v7s17 44.000000 +f3v9s34 f1v3s5 40.000000 +f3v9s34 f2v10s20 44.000000 +f3v9s34 f2v1s11 43.000000 +f3v9s34 f0v1s1 39.000000 +f3v9s34 f2v8s18 46.000000 +f3v9s34 f2v0s10 44.000000 +f3v9s34 f1v2s4 44.000000 +f3v9s34 f2v5s15 43.000000 +f3v9s34 f1v7s9 43.000000 +f3v9s34 f1v5s7 42.000000 +f3v9s34 f1v0s2 44.000000 +f3v9s34 f2v11s21 43.000000 +f3v9s34 f1v4s6 42.000000 +f3v9s34 f1v1s3 44.000000 +f3v9s34 f2v14s24 43.000000 +f4v0s40 f2v9s19 39.000000 +f4v0s40 f2v12s22 37.000000 +f4v0s40 f0v0s0 40.000000 +f4v0s40 f1v6s8 40.000000 +f4v0s40 f2v13s23 38.000000 +f4v0s40 f2v4s14 36.000000 +f4v0s40 f2v3s13 41.000000 +f4v0s40 f2v2s12 38.000000 +f4v0s40 f2v6s16 37.000000 +f4v0s40 f2v7s17 38.000000 +f4v0s40 f1v3s5 31.000000 +f4v0s40 f2v10s20 38.000000 +f4v0s40 f2v1s11 39.000000 +f4v0s40 f0v1s1 40.000000 +f4v0s40 f2v8s18 38.000000 +f4v0s40 f2v0s10 38.000000 +f4v0s40 f1v2s4 34.000000 +f4v0s40 f2v5s15 36.000000 +f4v0s40 f1v7s9 35.000000 +f4v0s40 f1v5s7 31.000000 +f4v0s40 f1v0s2 32.000000 +f4v0s40 f2v11s21 37.000000 +f4v0s40 f1v4s6 32.000000 +f4v0s40 f1v1s3 32.000000 +f4v0s40 f2v14s24 37.000000 +f1v7s14 f2v9s19 36.000000 +f1v7s14 f2v12s22 37.000000 +f1v7s14 f0v0s0 33.000000 +f1v7s14 f1v6s8 33.000000 +f1v7s14 f2v13s23 35.000000 +f1v7s14 f2v4s14 35.000000 +f1v7s14 f2v3s13 37.000000 +f1v7s14 f2v2s12 34.000000 +f1v7s14 f2v6s16 33.000000 +f1v7s14 f2v7s17 35.000000 +f1v7s14 f1v3s5 31.000000 +f1v7s14 f2v10s20 35.000000 +f1v7s14 f2v1s11 35.000000 +f1v7s14 f0v1s1 33.000000 +f1v7s14 f2v8s18 35.000000 +f1v7s14 f2v0s10 35.000000 +f1v7s14 f1v2s4 31.000000 +f1v7s14 f2v5s15 33.000000 +f1v7s14 f1v7s9 30.000000 +f1v7s14 f1v5s7 31.000000 +f1v7s14 f1v0s2 31.000000 +f1v7s14 f2v11s21 33.000000 +f1v7s14 f1v4s6 33.000000 +f1v7s14 f1v1s3 31.000000 +f1v7s14 f2v14s24 35.000000 +f3v2s27 f2v9s19 46.000000 +f3v2s27 f2v12s22 49.000000 +f3v2s27 f0v0s0 40.000000 +f3v2s27 f1v6s8 40.000000 +f3v2s27 f2v13s23 45.000000 +f3v2s27 f2v4s14 43.000000 +f3v2s27 f2v3s13 46.000000 +f3v2s27 f2v2s12 44.000000 +f3v2s27 f2v6s16 42.000000 +f3v2s27 f2v7s17 45.000000 +f3v2s27 f1v3s5 40.000000 +f3v2s27 f2v10s20 45.000000 +f3v2s27 f2v1s11 42.000000 +f3v2s27 f0v1s1 40.000000 +f3v2s27 f2v8s18 46.000000 +f3v2s27 f2v0s10 45.000000 +f3v2s27 f1v2s4 40.000000 +f3v2s27 f2v5s15 40.000000 +f3v2s27 f1v7s9 39.000000 +f3v2s27 f1v5s7 39.000000 +f3v2s27 f1v0s2 40.000000 +f3v2s27 f2v11s21 42.000000 +f3v2s27 f1v4s6 39.000000 +f3v2s27 f1v1s3 40.000000 +f3v2s27 f2v14s24 41.000000 +f4v1s41 f2v9s19 39.000000 +f4v1s41 f2v12s22 37.000000 +f4v1s41 f0v0s0 40.000000 +f4v1s41 f1v6s8 40.000000 +f4v1s41 f2v13s23 38.000000 +f4v1s41 f2v4s14 35.000000 +f4v1s41 f2v3s13 41.000000 +f4v1s41 f2v2s12 38.000000 +f4v1s41 f2v6s16 38.000000 +f4v1s41 f2v7s17 38.000000 +f4v1s41 f1v3s5 31.000000 +f4v1s41 f2v10s20 38.000000 +f4v1s41 f2v1s11 39.000000 +f4v1s41 f0v1s1 40.000000 +f4v1s41 f2v8s18 38.000000 +f4v1s41 f2v0s10 38.000000 +f4v1s41 f1v2s4 34.000000 +f4v1s41 f2v5s15 36.000000 +f4v1s41 f1v7s9 36.000000 +f4v1s41 f1v5s7 31.000000 +f4v1s41 f1v0s2 32.000000 +f4v1s41 f2v11s21 38.000000 +f4v1s41 f1v4s6 32.000000 +f4v1s41 f1v1s3 32.000000 +f4v1s41 f2v14s24 37.000000 +f2v0s22 f2v9s19 40.000000 +f2v0s22 f2v12s22 39.000000 +f2v0s22 f0v0s0 44.000000 +f2v0s22 f1v6s8 44.000000 +f2v0s22 f2v13s23 39.000000 +f2v0s22 f2v4s14 42.000000 +f2v0s22 f2v3s13 39.000000 +f2v0s22 f2v2s12 38.000000 +f2v0s22 f2v6s16 39.000000 +f2v0s22 f2v7s17 39.000000 +f2v0s22 f1v3s5 38.000000 +f2v0s22 f2v10s20 39.000000 +f2v0s22 f2v1s11 38.000000 +f2v0s22 f0v1s1 44.000000 +f2v0s22 f2v8s18 40.000000 +f2v0s22 f2v0s10 39.000000 +f2v0s22 f1v2s4 44.000000 +f2v0s22 f2v5s15 44.000000 +f2v0s22 f1v7s9 46.000000 +f2v0s22 f1v5s7 44.000000 +f2v0s22 f1v0s2 45.000000 +f2v0s22 f2v11s21 39.000000 +f2v0s22 f1v4s6 47.000000 +f2v0s22 f1v1s3 45.000000 +f2v0s22 f2v14s24 46.000000 +f1v2s9 f2v9s19 43.000000 +f1v2s9 f2v12s22 43.000000 +f1v2s9 f0v0s0 43.000000 +f1v2s9 f1v6s8 43.000000 +f1v2s9 f2v13s23 42.000000 +f1v2s9 f2v4s14 44.000000 +f1v2s9 f2v3s13 42.000000 +f1v2s9 f2v2s12 44.000000 +f1v2s9 f2v6s16 45.000000 +f1v2s9 f2v7s17 42.000000 +f1v2s9 f1v3s5 39.000000 +f1v2s9 f2v10s20 42.000000 +f1v2s9 f2v1s11 44.000000 +f1v2s9 f0v1s1 43.000000 +f1v2s9 f2v8s18 42.000000 +f1v2s9 f2v0s10 42.000000 +f1v2s9 f1v2s4 43.000000 +f1v2s9 f2v5s15 46.000000 +f1v2s9 f1v7s9 40.000000 +f1v2s9 f1v5s7 37.000000 +f1v2s9 f1v0s2 39.000000 +f1v2s9 f2v11s21 45.000000 +f1v2s9 f1v4s6 40.000000 +f1v2s9 f1v1s3 39.000000 +f1v2s9 f2v14s24 44.000000 +f1v3s10 f2v9s19 36.000000 +f1v3s10 f2v12s22 37.000000 +f1v3s10 f0v0s0 33.000000 +f1v3s10 f1v6s8 33.000000 +f1v3s10 f2v13s23 35.000000 +f1v3s10 f2v4s14 35.000000 +f1v3s10 f2v3s13 37.000000 +f1v3s10 f2v2s12 34.000000 +f1v3s10 f2v6s16 33.000000 +f1v3s10 f2v7s17 35.000000 +f1v3s10 f1v3s5 31.000000 +f1v3s10 f2v10s20 35.000000 +f1v3s10 f2v1s11 35.000000 +f1v3s10 f0v1s1 33.000000 +f1v3s10 f2v8s18 35.000000 +f1v3s10 f2v0s10 35.000000 +f1v3s10 f1v2s4 31.000000 +f1v3s10 f2v5s15 33.000000 +f1v3s10 f1v7s9 30.000000 +f1v3s10 f1v5s7 31.000000 +f1v3s10 f1v0s2 31.000000 +f1v3s10 f2v11s21 33.000000 +f1v3s10 f1v4s6 33.000000 +f1v3s10 f1v1s3 31.000000 +f1v3s10 f2v14s24 35.000000 +f3v0s25 f2v9s19 46.000000 +f3v0s25 f2v12s22 50.000000 +f3v0s25 f0v0s0 40.000000 +f3v0s25 f1v6s8 40.000000 +f3v0s25 f2v13s23 45.000000 +f3v0s25 f2v4s14 44.000000 +f3v0s25 f2v3s13 46.000000 +f3v0s25 f2v2s12 45.000000 +f3v0s25 f2v6s16 43.000000 +f3v0s25 f2v7s17 45.000000 +f3v0s25 f1v3s5 41.000000 +f3v0s25 f2v10s20 45.000000 +f3v0s25 f2v1s11 42.000000 +f3v0s25 f0v1s1 40.000000 +f3v0s25 f2v8s18 47.000000 +f3v0s25 f2v0s10 45.000000 +f3v0s25 f1v2s4 40.000000 +f3v0s25 f2v5s15 41.000000 +f3v0s25 f1v7s9 40.000000 +f3v0s25 f1v5s7 39.000000 +f3v0s25 f1v0s2 40.000000 +f3v0s25 f2v11s21 43.000000 +f3v0s25 f1v4s6 39.000000 +f3v0s25 f1v1s3 40.000000 +f3v0s25 f2v14s24 42.000000 +f3v3s28 f2v9s19 43.000000 +f3v3s28 f2v12s22 47.000000 +f3v3s28 f0v0s0 40.000000 +f3v3s28 f1v6s8 40.000000 +f3v3s28 f2v13s23 42.000000 +f3v3s28 f2v4s14 46.000000 +f3v3s28 f2v3s13 44.000000 +f3v3s28 f2v2s12 41.000000 +f3v3s28 f2v6s16 42.000000 +f3v3s28 f2v7s17 42.000000 +f3v3s28 f1v3s5 41.000000 +f3v3s28 f2v10s20 42.000000 +f3v3s28 f2v1s11 42.000000 +f3v3s28 f0v1s1 40.000000 +f3v3s28 f2v8s18 48.000000 +f3v3s28 f2v0s10 42.000000 +f3v3s28 f1v2s4 40.000000 +f3v3s28 f2v5s15 46.000000 +f3v3s28 f1v7s9 37.000000 +f3v3s28 f1v5s7 39.000000 +f3v3s28 f1v0s2 40.000000 +f3v3s28 f2v11s21 42.000000 +f3v3s28 f1v4s6 38.000000 +f3v3s28 f1v1s3 40.000000 +f3v3s28 f2v14s24 41.000000 +f3v13s38 f2v9s19 49.000000 +f3v13s38 f2v12s22 47.000000 +f3v13s38 f0v0s0 40.000000 +f3v13s38 f1v6s8 40.000000 +f3v13s38 f2v13s23 48.000000 +f3v13s38 f2v4s14 46.000000 +f3v13s38 f2v3s13 48.000000 +f3v13s38 f2v2s12 48.000000 +f3v13s38 f2v6s16 43.000000 +f3v13s38 f2v7s17 48.000000 +f3v13s38 f1v3s5 41.000000 +f3v13s38 f2v10s20 48.000000 +f3v13s38 f2v1s11 43.000000 +f3v13s38 f0v1s1 40.000000 +f3v13s38 f2v8s18 49.000000 +f3v13s38 f2v0s10 48.000000 +f3v13s38 f1v2s4 41.000000 +f3v13s38 f2v5s15 42.000000 +f3v13s38 f1v7s9 40.000000 +f3v13s38 f1v5s7 40.000000 +f3v13s38 f1v0s2 41.000000 +f3v13s38 f2v11s21 43.000000 +f3v13s38 f1v4s6 39.000000 +f3v13s38 f1v1s3 41.000000 +f3v13s38 f2v14s24 42.000000 +f3v7s32 f2v9s19 45.000000 +f3v7s32 f2v12s22 43.000000 +f3v7s32 f0v0s0 39.000000 +f3v7s32 f1v6s8 39.000000 +f3v7s32 f2v13s23 44.000000 +f3v7s32 f2v4s14 43.000000 +f3v7s32 f2v3s13 45.000000 +f3v7s32 f2v2s12 44.000000 +f3v7s32 f2v6s16 43.000000 +f3v7s32 f2v7s17 44.000000 +f3v7s32 f1v3s5 40.000000 +f3v7s32 f2v10s20 44.000000 +f3v7s32 f2v1s11 43.000000 +f3v7s32 f0v1s1 39.000000 +f3v7s32 f2v8s18 46.000000 +f3v7s32 f2v0s10 44.000000 +f3v7s32 f1v2s4 44.000000 +f3v7s32 f2v5s15 43.000000 +f3v7s32 f1v7s9 43.000000 +f3v7s32 f1v5s7 42.000000 +f3v7s32 f1v0s2 44.000000 +f3v7s32 f2v11s21 43.000000 +f3v7s32 f1v4s6 42.000000 +f3v7s32 f1v1s3 44.000000 +f3v7s32 f2v14s24 43.000000 +f3v1s26 f2v9s19 43.000000 +f3v1s26 f2v12s22 48.000000 +f3v1s26 f0v0s0 41.000000 +f3v1s26 f1v6s8 41.000000 +f3v1s26 f2v13s23 42.000000 +f3v1s26 f2v4s14 44.000000 +f3v1s26 f2v3s13 45.000000 +f3v1s26 f2v2s12 41.000000 +f3v1s26 f2v6s16 42.000000 +f3v1s26 f2v7s17 42.000000 +f3v1s26 f1v3s5 40.000000 +f3v1s26 f2v10s20 42.000000 +f3v1s26 f2v1s11 41.000000 +f3v1s26 f0v1s1 41.000000 +f3v1s26 f2v8s18 47.000000 +f3v1s26 f2v0s10 42.000000 +f3v1s26 f1v2s4 39.000000 +f3v1s26 f2v5s15 41.000000 +f3v1s26 f1v7s9 39.000000 +f3v1s26 f1v5s7 39.000000 +f3v1s26 f1v0s2 40.000000 +f3v1s26 f2v11s21 42.000000 +f3v1s26 f1v4s6 39.000000 +f3v1s26 f1v1s3 40.000000 +f3v1s26 f2v14s24 41.000000 +f3v6s31 f2v9s19 45.000000 +f3v6s31 f2v12s22 43.000000 +f3v6s31 f0v0s0 39.000000 +f3v6s31 f1v6s8 39.000000 +f3v6s31 f2v13s23 44.000000 +f3v6s31 f2v4s14 43.000000 +f3v6s31 f2v3s13 45.000000 +f3v6s31 f2v2s12 44.000000 +f3v6s31 f2v6s16 43.000000 +f3v6s31 f2v7s17 44.000000 +f3v6s31 f1v3s5 40.000000 +f3v6s31 f2v10s20 44.000000 +f3v6s31 f2v1s11 43.000000 +f3v6s31 f0v1s1 39.000000 +f3v6s31 f2v8s18 46.000000 +f3v6s31 f2v0s10 44.000000 +f3v6s31 f1v2s4 44.000000 +f3v6s31 f2v5s15 43.000000 +f3v6s31 f1v7s9 43.000000 +f3v6s31 f1v5s7 42.000000 +f3v6s31 f1v0s2 44.000000 +f3v6s31 f2v11s21 43.000000 +f3v6s31 f1v4s6 42.000000 +f3v6s31 f1v1s3 44.000000 +f3v6s31 f2v14s24 43.000000 +f1v13s20 f2v9s19 33.000000 +f1v13s20 f2v12s22 34.000000 +f1v13s20 f0v0s0 34.000000 +f1v13s20 f1v6s8 34.000000 +f1v13s20 f2v13s23 32.000000 +f1v13s20 f2v4s14 32.000000 +f1v13s20 f2v3s13 35.000000 +f1v13s20 f2v2s12 31.000000 +f1v13s20 f2v6s16 31.000000 +f1v13s20 f2v7s17 32.000000 +f1v13s20 f1v3s5 30.000000 +f1v13s20 f2v10s20 32.000000 +f1v13s20 f2v1s11 33.000000 +f1v13s20 f0v1s1 34.000000 +f1v13s20 f2v8s18 33.000000 +f1v13s20 f2v0s10 32.000000 +f1v13s20 f1v2s4 30.000000 +f1v13s20 f2v5s15 31.000000 +f1v13s20 f1v7s9 29.000000 +f1v13s20 f1v5s7 30.000000 +f1v13s20 f1v0s2 30.000000 +f1v13s20 f2v11s21 31.000000 +f1v13s20 f1v4s6 32.000000 +f1v13s20 f1v1s3 30.000000 +f1v13s20 f2v14s24 33.000000 +f2v2s24 f2v9s19 35.000000 +f2v2s24 f2v12s22 34.000000 +f2v2s24 f0v0s0 37.000000 +f2v2s24 f1v6s8 37.000000 +f2v2s24 f2v13s23 34.000000 +f2v2s24 f2v4s14 31.000000 +f2v2s24 f2v3s13 35.000000 +f2v2s24 f2v2s12 33.000000 +f2v2s24 f2v6s16 32.000000 +f2v2s24 f2v7s17 34.000000 +f2v2s24 f1v3s5 29.000000 +f2v2s24 f2v10s20 34.000000 +f2v2s24 f2v1s11 33.000000 +f2v2s24 f0v1s1 37.000000 +f2v2s24 f2v8s18 33.000000 +f2v2s24 f2v0s10 34.000000 +f2v2s24 f1v2s4 29.000000 +f2v2s24 f2v5s15 31.000000 +f2v2s24 f1v7s9 29.000000 +f2v2s24 f1v5s7 27.000000 +f2v2s24 f1v0s2 28.000000 +f2v2s24 f2v11s21 32.000000 +f2v2s24 f1v4s6 30.000000 +f2v2s24 f1v1s3 28.000000 +f2v2s24 f2v14s24 33.000000 +f3v11s36 f2v9s19 47.000000 +f3v11s36 f2v12s22 46.000000 +f3v11s36 f0v0s0 39.000000 +f3v11s36 f1v6s8 39.000000 +f3v11s36 f2v13s23 46.000000 +f3v11s36 f2v4s14 44.000000 +f3v11s36 f2v3s13 47.000000 +f3v11s36 f2v2s12 45.000000 +f3v11s36 f2v6s16 41.000000 +f3v11s36 f2v7s17 46.000000 +f3v11s36 f1v3s5 39.000000 +f3v11s36 f2v10s20 46.000000 +f3v11s36 f2v1s11 47.000000 +f3v11s36 f0v1s1 39.000000 +f3v11s36 f2v8s18 45.000000 +f3v11s36 f2v0s10 46.000000 +f3v11s36 f1v2s4 40.000000 +f3v11s36 f2v5s15 41.000000 +f3v11s36 f1v7s9 38.000000 +f3v11s36 f1v5s7 39.000000 +f3v11s36 f1v0s2 40.000000 +f3v11s36 f2v11s21 41.000000 +f3v11s36 f1v4s6 39.000000 +f3v11s36 f1v1s3 40.000000 +f3v11s36 f2v14s24 43.000000 +f0v5s5 f2v9s19 41.000000 +f0v5s5 f2v12s22 40.000000 +f0v5s5 f0v0s0 44.000000 +f0v5s5 f1v6s8 44.000000 +f0v5s5 f2v13s23 40.000000 +f0v5s5 f2v4s14 42.000000 +f0v5s5 f2v3s13 41.000000 +f0v5s5 f2v2s12 39.000000 +f0v5s5 f2v6s16 40.000000 +f0v5s5 f2v7s17 40.000000 +f0v5s5 f1v3s5 40.000000 +f0v5s5 f2v10s20 40.000000 +f0v5s5 f2v1s11 39.000000 +f0v5s5 f0v1s1 44.000000 +f0v5s5 f2v8s18 41.000000 +f0v5s5 f2v0s10 40.000000 +f0v5s5 f1v2s4 43.000000 +f0v5s5 f2v5s15 44.000000 +f0v5s5 f1v7s9 45.000000 +f0v5s5 f1v5s7 43.000000 +f0v5s5 f1v0s2 44.000000 +f0v5s5 f2v11s21 40.000000 +f0v5s5 f1v4s6 46.000000 +f0v5s5 f1v1s3 44.000000 +f0v5s5 f2v14s24 49.000000 +f1v11s18 f2v9s19 36.000000 +f1v11s18 f2v12s22 37.000000 +f1v11s18 f0v0s0 33.000000 +f1v11s18 f1v6s8 33.000000 +f1v11s18 f2v13s23 35.000000 +f1v11s18 f2v4s14 35.000000 +f1v11s18 f2v3s13 37.000000 +f1v11s18 f2v2s12 34.000000 +f1v11s18 f2v6s16 34.000000 +f1v11s18 f2v7s17 35.000000 +f1v11s18 f1v3s5 30.000000 +f1v11s18 f2v10s20 35.000000 +f1v11s18 f2v1s11 35.000000 +f1v11s18 f0v1s1 33.000000 +f1v11s18 f2v8s18 35.000000 +f1v11s18 f2v0s10 35.000000 +f1v11s18 f1v2s4 31.000000 +f1v11s18 f2v5s15 33.000000 +f1v11s18 f1v7s9 29.000000 +f1v11s18 f1v5s7 30.000000 +f1v11s18 f1v0s2 30.000000 +f1v11s18 f2v11s21 34.000000 +f1v11s18 f1v4s6 32.000000 +f1v11s18 f1v1s3 30.000000 +f1v11s18 f2v14s24 36.000000 +f0v6s6 f2v9s19 38.000000 +f0v6s6 f2v12s22 42.000000 +f0v6s6 f0v0s0 39.000000 +f0v6s6 f1v6s8 39.000000 +f0v6s6 f2v13s23 37.000000 +f0v6s6 f2v4s14 42.000000 +f0v6s6 f2v3s13 47.000000 +f0v6s6 f2v2s12 45.000000 +f0v6s6 f2v6s16 42.000000 +f0v6s6 f2v7s17 37.000000 +f0v6s6 f1v3s5 40.000000 +f0v6s6 f2v10s20 37.000000 +f0v6s6 f2v1s11 36.000000 +f0v6s6 f0v1s1 39.000000 +f0v6s6 f2v8s18 38.000000 +f0v6s6 f2v0s10 37.000000 +f0v6s6 f1v2s4 41.000000 +f0v6s6 f2v5s15 44.000000 +f0v6s6 f1v7s9 46.000000 +f0v6s6 f1v5s7 40.000000 +f0v6s6 f1v0s2 41.000000 +f0v6s6 f2v11s21 42.000000 +f0v6s6 f1v4s6 40.000000 +f0v6s6 f1v1s3 41.000000 +f0v6s6 f2v14s24 45.000000 +f3v4s29 f2v9s19 47.000000 +f3v4s29 f2v12s22 52.000000 +f3v4s29 f0v0s0 39.000000 +f3v4s29 f1v6s8 39.000000 +f3v4s29 f2v13s23 46.000000 +f3v4s29 f2v4s14 45.000000 +f3v4s29 f2v3s13 46.000000 +f3v4s29 f2v2s12 46.000000 +f3v4s29 f2v6s16 45.000000 +f3v4s29 f2v7s17 46.000000 +f3v4s29 f1v3s5 43.000000 +f3v4s29 f2v10s20 46.000000 +f3v4s29 f2v1s11 43.000000 +f3v4s29 f0v1s1 39.000000 +f3v4s29 f2v8s18 47.000000 +f3v4s29 f2v0s10 46.000000 +f3v4s29 f1v2s4 40.000000 +f3v4s29 f2v5s15 42.000000 +f3v4s29 f1v7s9 42.000000 +f3v4s29 f1v5s7 39.000000 +f3v4s29 f1v0s2 39.000000 +f3v4s29 f2v11s21 45.000000 +f3v4s29 f1v4s6 41.000000 +f3v4s29 f1v1s3 39.000000 +f3v4s29 f2v14s24 43.000000 +f0v2s2 f2v9s19 40.000000 +f0v2s2 f2v12s22 39.000000 +f0v2s2 f0v0s0 44.000000 +f0v2s2 f1v6s8 44.000000 +f0v2s2 f2v13s23 39.000000 +f0v2s2 f2v4s14 42.000000 +f0v2s2 f2v3s13 39.000000 +f0v2s2 f2v2s12 38.000000 +f0v2s2 f2v6s16 39.000000 +f0v2s2 f2v7s17 39.000000 +f0v2s2 f1v3s5 38.000000 +f0v2s2 f2v10s20 39.000000 +f0v2s2 f2v1s11 38.000000 +f0v2s2 f0v1s1 44.000000 +f0v2s2 f2v8s18 40.000000 +f0v2s2 f2v0s10 39.000000 +f0v2s2 f1v2s4 44.000000 +f0v2s2 f2v5s15 44.000000 +f0v2s2 f1v7s9 46.000000 +f0v2s2 f1v5s7 44.000000 +f0v2s2 f1v0s2 45.000000 +f0v2s2 f2v11s21 39.000000 +f0v2s2 f1v4s6 47.000000 +f0v2s2 f1v1s3 45.000000 +f0v2s2 f2v14s24 46.000000 +f3v12s37 f2v9s19 46.000000 +f3v12s37 f2v12s22 45.000000 +f3v12s37 f0v0s0 40.000000 +f3v12s37 f1v6s8 40.000000 +f3v12s37 f2v13s23 45.000000 +f3v12s37 f2v4s14 43.000000 +f3v12s37 f2v3s13 46.000000 +f3v12s37 f2v2s12 44.000000 +f3v12s37 f2v6s16 44.000000 +f3v12s37 f2v7s17 45.000000 +f3v12s37 f1v3s5 42.000000 +f3v12s37 f2v10s20 45.000000 +f3v12s37 f2v1s11 46.000000 +f3v12s37 f0v1s1 40.000000 +f3v12s37 f2v8s18 46.000000 +f3v12s37 f2v0s10 45.000000 +f3v12s37 f1v2s4 45.000000 +f3v12s37 f2v5s15 45.000000 +f3v12s37 f1v7s9 44.000000 +f3v12s37 f1v5s7 43.000000 +f3v12s37 f1v0s2 45.000000 +f3v12s37 f2v11s21 44.000000 +f3v12s37 f1v4s6 47.000000 +f3v12s37 f1v1s3 45.000000 +f3v12s37 f2v14s24 45.000000 +f0v3s3 f2v9s19 39.000000 +f0v3s3 f2v12s22 38.000000 +f0v3s3 f0v0s0 43.000000 +f0v3s3 f1v6s8 43.000000 +f0v3s3 f2v13s23 38.000000 +f0v3s3 f2v4s14 48.000000 +f0v3s3 f2v3s13 38.000000 +f0v3s3 f2v2s12 38.000000 +f0v3s3 f2v6s16 38.000000 +f0v3s3 f2v7s17 38.000000 +f0v3s3 f1v3s5 37.000000 +f0v3s3 f2v10s20 38.000000 +f0v3s3 f2v1s11 38.000000 +f0v3s3 f0v1s1 43.000000 +f0v3s3 f2v8s18 39.000000 +f0v3s3 f2v0s10 38.000000 +f0v3s3 f1v2s4 41.000000 +f0v3s3 f2v5s15 44.000000 +f0v3s3 f1v7s9 44.000000 +f0v3s3 f1v5s7 43.000000 +f0v3s3 f1v0s2 43.000000 +f0v3s3 f2v11s21 38.000000 +f0v3s3 f1v4s6 45.000000 +f0v3s3 f1v1s3 43.000000 +f0v3s3 f2v14s24 45.000000 +f3v14s39 f2v9s19 46.000000 +f3v14s39 f2v12s22 50.000000 +f3v14s39 f0v0s0 40.000000 +f3v14s39 f1v6s8 40.000000 +f3v14s39 f2v13s23 45.000000 +f3v14s39 f2v4s14 44.000000 +f3v14s39 f2v3s13 46.000000 +f3v14s39 f2v2s12 45.000000 +f3v14s39 f2v6s16 43.000000 +f3v14s39 f2v7s17 45.000000 +f3v14s39 f1v3s5 41.000000 +f3v14s39 f2v10s20 45.000000 +f3v14s39 f2v1s11 42.000000 +f3v14s39 f0v1s1 40.000000 +f3v14s39 f2v8s18 47.000000 +f3v14s39 f2v0s10 45.000000 +f3v14s39 f1v2s4 40.000000 +f3v14s39 f2v5s15 41.000000 +f3v14s39 f1v7s9 40.000000 +f3v14s39 f1v5s7 39.000000 +f3v14s39 f1v0s2 40.000000 +f3v14s39 f2v11s21 43.000000 +f3v14s39 f1v4s6 39.000000 +f3v14s39 f1v1s3 40.000000 +f3v14s39 f2v14s24 42.000000 +f0v0s0 f2v9s19 41.000000 +f0v0s0 f2v12s22 40.000000 +f0v0s0 f0v0s0 43.000000 +f0v0s0 f1v6s8 43.000000 +f0v0s0 f2v13s23 40.000000 +f0v0s0 f2v4s14 42.000000 +f0v0s0 f2v3s13 39.000000 +f0v0s0 f2v2s12 48.000000 +f0v0s0 f2v6s16 47.000000 +f0v0s0 f2v7s17 40.000000 +f0v0s0 f1v3s5 39.000000 +f0v0s0 f2v10s20 40.000000 +f0v0s0 f2v1s11 47.000000 +f0v0s0 f0v1s1 43.000000 +f0v0s0 f2v8s18 41.000000 +f0v0s0 f2v0s10 40.000000 +f0v0s0 f1v2s4 43.000000 +f0v0s0 f2v5s15 46.000000 +f0v0s0 f1v7s9 45.000000 +f0v0s0 f1v5s7 43.000000 +f0v0s0 f1v0s2 44.000000 +f0v0s0 f2v11s21 47.000000 +f0v0s0 f1v4s6 46.000000 +f0v0s0 f1v1s3 44.000000 +f0v0s0 f2v14s24 45.000000 +f1v12s19 f2v9s19 36.000000 +f1v12s19 f2v12s22 37.000000 +f1v12s19 f0v0s0 33.000000 +f1v12s19 f1v6s8 33.000000 +f1v12s19 f2v13s23 35.000000 +f1v12s19 f2v4s14 35.000000 +f1v12s19 f2v3s13 37.000000 +f1v12s19 f2v2s12 34.000000 +f1v12s19 f2v6s16 33.000000 +f1v12s19 f2v7s17 35.000000 +f1v12s19 f1v3s5 32.000000 +f1v12s19 f2v10s20 35.000000 +f1v12s19 f2v1s11 35.000000 +f1v12s19 f0v1s1 33.000000 +f1v12s19 f2v8s18 35.000000 +f1v12s19 f2v0s10 35.000000 +f1v12s19 f1v2s4 31.000000 +f1v12s19 f2v5s15 33.000000 +f1v12s19 f1v7s9 31.000000 +f1v12s19 f1v5s7 33.000000 +f1v12s19 f1v0s2 32.000000 +f1v12s19 f2v11s21 33.000000 +f1v12s19 f1v4s6 34.000000 +f1v12s19 f1v1s3 32.000000 +f1v12s19 f2v14s24 35.000000 diff --git a/pkg/obitools/obialign/testdata/two_allraw.expected b/pkg/obitools/obialign/testdata/two_allraw.expected new file mode 100644 index 0000000..91d197b --- /dev/null +++ b/pkg/obitools/obialign/testdata/two_allraw.expected @@ -0,0 +1,1075 @@ +f3v10s35 f2v9s19 40.000000 +f3v10s35 f2v12s22 40.000000 +f3v10s35 f0v0s0 39.000000 +f3v10s35 f1v6s8 39.000000 +f3v10s35 f2v13s23 39.000000 +f3v10s35 f2v4s14 38.000000 +f3v10s35 f2v3s13 39.000000 +f3v10s35 f2v2s12 39.000000 +f3v10s35 f2v6s16 40.000000 +f3v10s35 f2v7s17 39.000000 +f3v10s35 f1v3s5 32.000000 +f3v10s35 f2v10s20 39.000000 +f3v10s35 f2v1s11 40.000000 +f3v10s35 f0v1s1 39.000000 +f3v10s35 f2v8s18 39.000000 +f3v10s35 f2v0s10 39.000000 +f3v10s35 f1v2s4 31.000000 +f3v10s35 f2v5s15 38.000000 +f3v10s35 f1v7s9 32.000000 +f3v10s35 f1v5s7 32.000000 +f3v10s35 f1v0s2 32.000000 +f3v10s35 f2v11s21 40.000000 +f3v10s35 f1v4s6 33.000000 +f3v10s35 f1v1s3 32.000000 +f3v10s35 f2v14s24 40.000000 +f3v5s30 f2v9s19 38.000000 +f3v5s30 f2v12s22 38.000000 +f3v5s30 f0v0s0 38.000000 +f3v5s30 f1v6s8 38.000000 +f3v5s30 f2v13s23 37.000000 +f3v5s30 f2v4s14 36.000000 +f3v5s30 f2v3s13 37.000000 +f3v5s30 f2v2s12 37.000000 +f3v5s30 f2v6s16 38.000000 +f3v5s30 f2v7s17 37.000000 +f3v5s30 f1v3s5 31.000000 +f3v5s30 f2v10s20 37.000000 +f3v5s30 f2v1s11 37.000000 +f3v5s30 f0v1s1 38.000000 +f3v5s30 f2v8s18 36.000000 +f3v5s30 f2v0s10 37.000000 +f3v5s30 f1v2s4 32.000000 +f3v5s30 f2v5s15 37.000000 +f3v5s30 f1v7s9 32.000000 +f3v5s30 f1v5s7 31.000000 +f3v5s30 f1v0s2 32.000000 +f3v5s30 f2v11s21 38.000000 +f3v5s30 f1v4s6 32.000000 +f3v5s30 f1v1s3 32.000000 +f3v5s30 f2v14s24 37.000000 +f1v8s15 f2v9s19 38.000000 +f1v8s15 f2v12s22 38.000000 +f1v8s15 f0v0s0 39.000000 +f1v8s15 f1v6s8 39.000000 +f1v8s15 f2v13s23 38.000000 +f1v8s15 f2v4s14 37.000000 +f1v8s15 f2v3s13 37.000000 +f1v8s15 f2v2s12 38.000000 +f1v8s15 f2v6s16 39.000000 +f1v8s15 f2v7s17 38.000000 +f1v8s15 f1v3s5 33.000000 +f1v8s15 f2v10s20 38.000000 +f1v8s15 f2v1s11 38.000000 +f1v8s15 f0v1s1 39.000000 +f1v8s15 f2v8s18 38.000000 +f1v8s15 f2v0s10 38.000000 +f1v8s15 f1v2s4 33.000000 +f1v8s15 f2v5s15 39.000000 +f1v8s15 f1v7s9 35.000000 +f1v8s15 f1v5s7 33.000000 +f1v8s15 f1v0s2 34.000000 +f1v8s15 f2v11s21 39.000000 +f1v8s15 f1v4s6 34.000000 +f1v8s15 f1v1s3 34.000000 +f1v8s15 f2v14s24 38.000000 +f2v1s23 f2v9s19 39.000000 +f2v1s23 f2v12s22 39.000000 +f2v1s23 f0v0s0 40.000000 +f2v1s23 f1v6s8 40.000000 +f2v1s23 f2v13s23 39.000000 +f2v1s23 f2v4s14 38.000000 +f2v1s23 f2v3s13 38.000000 +f2v1s23 f2v2s12 39.000000 +f2v1s23 f2v6s16 40.000000 +f2v1s23 f2v7s17 39.000000 +f2v1s23 f1v3s5 34.000000 +f2v1s23 f2v10s20 39.000000 +f2v1s23 f2v1s11 38.000000 +f2v1s23 f0v1s1 40.000000 +f2v1s23 f2v8s18 38.000000 +f2v1s23 f2v0s10 39.000000 +f2v1s23 f1v2s4 34.000000 +f2v1s23 f2v5s15 40.000000 +f2v1s23 f1v7s9 36.000000 +f2v1s23 f1v5s7 34.000000 +f2v1s23 f1v0s2 35.000000 +f2v1s23 f2v11s21 40.000000 +f2v1s23 f1v4s6 35.000000 +f2v1s23 f1v1s3 35.000000 +f2v1s23 f2v14s24 39.000000 +f4v2s42 f2v9s19 40.000000 +f4v2s42 f2v12s22 40.000000 +f4v2s42 f0v0s0 43.000000 +f4v2s42 f1v6s8 43.000000 +f4v2s42 f2v13s23 40.000000 +f4v2s42 f2v4s14 39.000000 +f4v2s42 f2v3s13 39.000000 +f4v2s42 f2v2s12 39.000000 +f4v2s42 f2v6s16 39.000000 +f4v2s42 f2v7s17 40.000000 +f4v2s42 f1v3s5 35.000000 +f4v2s42 f2v10s20 40.000000 +f4v2s42 f2v1s11 39.000000 +f4v2s42 f0v1s1 43.000000 +f4v2s42 f2v8s18 38.000000 +f4v2s42 f2v0s10 40.000000 +f4v2s42 f1v2s4 33.000000 +f4v2s42 f2v5s15 39.000000 +f4v2s42 f1v7s9 33.000000 +f4v2s42 f1v5s7 35.000000 +f4v2s42 f1v0s2 35.000000 +f4v2s42 f2v11s21 39.000000 +f4v2s42 f1v4s6 36.000000 +f4v2s42 f1v1s3 35.000000 +f4v2s42 f2v14s24 41.000000 +f0v4s4 f2v9s19 40.000000 +f0v4s4 f2v12s22 40.000000 +f0v4s4 f0v0s0 44.000000 +f0v4s4 f1v6s8 44.000000 +f0v4s4 f2v13s23 40.000000 +f0v4s4 f2v4s14 39.000000 +f0v4s4 f2v3s13 40.000000 +f0v4s4 f2v2s12 40.000000 +f0v4s4 f2v6s16 40.000000 +f0v4s4 f2v7s17 40.000000 +f0v4s4 f1v3s5 34.000000 +f0v4s4 f2v10s20 40.000000 +f0v4s4 f2v1s11 41.000000 +f0v4s4 f0v1s1 44.000000 +f0v4s4 f2v8s18 39.000000 +f0v4s4 f2v0s10 40.000000 +f0v4s4 f1v2s4 34.000000 +f0v4s4 f2v5s15 39.000000 +f0v4s4 f1v7s9 34.000000 +f0v4s4 f1v5s7 35.000000 +f0v4s4 f1v0s2 35.000000 +f0v4s4 f2v11s21 40.000000 +f0v4s4 f1v4s6 35.000000 +f0v4s4 f1v1s3 35.000000 +f0v4s4 f2v14s24 41.000000 +f1v6s13 f2v9s19 35.000000 +f1v6s13 f2v12s22 35.000000 +f1v6s13 f0v0s0 39.000000 +f1v6s13 f1v6s8 39.000000 +f1v6s13 f2v13s23 35.000000 +f1v6s13 f2v4s14 34.000000 +f1v6s13 f2v3s13 34.000000 +f1v6s13 f2v2s12 35.000000 +f1v6s13 f2v6s16 37.000000 +f1v6s13 f2v7s17 35.000000 +f1v6s13 f1v3s5 34.000000 +f1v6s13 f2v10s20 35.000000 +f1v6s13 f2v1s11 35.000000 +f1v6s13 f0v1s1 39.000000 +f1v6s13 f2v8s18 35.000000 +f1v6s13 f2v0s10 35.000000 +f1v6s13 f1v2s4 34.000000 +f1v6s13 f2v5s15 36.000000 +f1v6s13 f1v7s9 36.000000 +f1v6s13 f1v5s7 33.000000 +f1v6s13 f1v0s2 35.000000 +f1v6s13 f2v11s21 37.000000 +f1v6s13 f1v4s6 35.000000 +f1v6s13 f1v1s3 35.000000 +f1v6s13 f2v14s24 36.000000 +f3v8s33 f2v9s19 39.000000 +f3v8s33 f2v12s22 39.000000 +f3v8s33 f0v0s0 38.000000 +f3v8s33 f1v6s8 38.000000 +f3v8s33 f2v13s23 38.000000 +f3v8s33 f2v4s14 37.000000 +f3v8s33 f2v3s13 38.000000 +f3v8s33 f2v2s12 38.000000 +f3v8s33 f2v6s16 38.000000 +f3v8s33 f2v7s17 38.000000 +f3v8s33 f1v3s5 33.000000 +f3v8s33 f2v10s20 38.000000 +f3v8s33 f2v1s11 38.000000 +f3v8s33 f0v1s1 38.000000 +f3v8s33 f2v8s18 37.000000 +f3v8s33 f2v0s10 38.000000 +f3v8s33 f1v2s4 33.000000 +f3v8s33 f2v5s15 38.000000 +f3v8s33 f1v7s9 34.000000 +f3v8s33 f1v5s7 34.000000 +f3v8s33 f1v0s2 34.000000 +f3v8s33 f2v11s21 38.000000 +f3v8s33 f1v4s6 34.000000 +f3v8s33 f1v1s3 34.000000 +f3v8s33 f2v14s24 39.000000 +f1v1s8 f2v9s19 40.000000 +f1v1s8 f2v12s22 40.000000 +f1v1s8 f0v0s0 40.000000 +f1v1s8 f1v6s8 40.000000 +f1v1s8 f2v13s23 40.000000 +f1v1s8 f2v4s14 39.000000 +f1v1s8 f2v3s13 39.000000 +f1v1s8 f2v2s12 40.000000 +f1v1s8 f2v6s16 41.000000 +f1v1s8 f2v7s17 40.000000 +f1v1s8 f1v3s5 34.000000 +f1v1s8 f2v10s20 40.000000 +f1v1s8 f2v1s11 38.000000 +f1v1s8 f0v1s1 40.000000 +f1v1s8 f2v8s18 39.000000 +f1v1s8 f2v0s10 40.000000 +f1v1s8 f1v2s4 34.000000 +f1v1s8 f2v5s15 41.000000 +f1v1s8 f1v7s9 36.000000 +f1v1s8 f1v5s7 34.000000 +f1v1s8 f1v0s2 35.000000 +f1v1s8 f2v11s21 41.000000 +f1v1s8 f1v4s6 35.000000 +f1v1s8 f1v1s3 35.000000 +f1v1s8 f2v14s24 40.000000 +f1v14s21 f2v9s19 37.000000 +f1v14s21 f2v12s22 37.000000 +f1v14s21 f0v0s0 40.000000 +f1v14s21 f1v6s8 40.000000 +f1v14s21 f2v13s23 37.000000 +f1v14s21 f2v4s14 37.000000 +f1v14s21 f2v3s13 36.000000 +f1v14s21 f2v2s12 37.000000 +f1v14s21 f2v6s16 38.000000 +f1v14s21 f2v7s17 37.000000 +f1v14s21 f1v3s5 32.000000 +f1v14s21 f2v10s20 37.000000 +f1v14s21 f2v1s11 36.000000 +f1v14s21 f0v1s1 40.000000 +f1v14s21 f2v8s18 36.000000 +f1v14s21 f2v0s10 37.000000 +f1v14s21 f1v2s4 33.000000 +f1v14s21 f2v5s15 38.000000 +f1v14s21 f1v7s9 34.000000 +f1v14s21 f1v5s7 33.000000 +f1v14s21 f1v0s2 33.000000 +f1v14s21 f2v11s21 38.000000 +f1v14s21 f1v4s6 33.000000 +f1v14s21 f1v1s3 33.000000 +f1v14s21 f2v14s24 37.000000 +f0v1s1 f2v9s19 40.000000 +f0v1s1 f2v12s22 40.000000 +f0v1s1 f0v0s0 44.000000 +f0v1s1 f1v6s8 44.000000 +f0v1s1 f2v13s23 40.000000 +f0v1s1 f2v4s14 39.000000 +f0v1s1 f2v3s13 40.000000 +f0v1s1 f2v2s12 40.000000 +f0v1s1 f2v6s16 40.000000 +f0v1s1 f2v7s17 40.000000 +f0v1s1 f1v3s5 34.000000 +f0v1s1 f2v10s20 40.000000 +f0v1s1 f2v1s11 41.000000 +f0v1s1 f0v1s1 44.000000 +f0v1s1 f2v8s18 39.000000 +f0v1s1 f2v0s10 40.000000 +f0v1s1 f1v2s4 34.000000 +f0v1s1 f2v5s15 39.000000 +f0v1s1 f1v7s9 34.000000 +f0v1s1 f1v5s7 35.000000 +f0v1s1 f1v0s2 35.000000 +f0v1s1 f2v11s21 40.000000 +f0v1s1 f1v4s6 35.000000 +f0v1s1 f1v1s3 35.000000 +f0v1s1 f2v14s24 41.000000 +f1v5s12 f2v9s19 39.000000 +f1v5s12 f2v12s22 39.000000 +f1v5s12 f0v0s0 40.000000 +f1v5s12 f1v6s8 40.000000 +f1v5s12 f2v13s23 39.000000 +f1v5s12 f2v4s14 39.000000 +f1v5s12 f2v3s13 39.000000 +f1v5s12 f2v2s12 39.000000 +f1v5s12 f2v6s16 38.000000 +f1v5s12 f2v7s17 39.000000 +f1v5s12 f1v3s5 35.000000 +f1v5s12 f2v10s20 39.000000 +f1v5s12 f2v1s11 38.000000 +f1v5s12 f0v1s1 40.000000 +f1v5s12 f2v8s18 37.000000 +f1v5s12 f2v0s10 39.000000 +f1v5s12 f1v2s4 35.000000 +f1v5s12 f2v5s15 39.000000 +f1v5s12 f1v7s9 36.000000 +f1v5s12 f1v5s7 34.000000 +f1v5s12 f1v0s2 36.000000 +f1v5s12 f2v11s21 38.000000 +f1v5s12 f1v4s6 36.000000 +f1v5s12 f1v1s3 36.000000 +f1v5s12 f2v14s24 38.000000 +f1v4s11 f2v9s19 39.000000 +f1v4s11 f2v12s22 39.000000 +f1v4s11 f0v0s0 40.000000 +f1v4s11 f1v6s8 40.000000 +f1v4s11 f2v13s23 39.000000 +f1v4s11 f2v4s14 38.000000 +f1v4s11 f2v3s13 38.000000 +f1v4s11 f2v2s12 39.000000 +f1v4s11 f2v6s16 40.000000 +f1v4s11 f2v7s17 39.000000 +f1v4s11 f1v3s5 34.000000 +f1v4s11 f2v10s20 39.000000 +f1v4s11 f2v1s11 38.000000 +f1v4s11 f0v1s1 40.000000 +f1v4s11 f2v8s18 38.000000 +f1v4s11 f2v0s10 39.000000 +f1v4s11 f1v2s4 34.000000 +f1v4s11 f2v5s15 40.000000 +f1v4s11 f1v7s9 36.000000 +f1v4s11 f1v5s7 34.000000 +f1v4s11 f1v0s2 35.000000 +f1v4s11 f2v11s21 40.000000 +f1v4s11 f1v4s6 35.000000 +f1v4s11 f1v1s3 35.000000 +f1v4s11 f2v14s24 39.000000 +f1v9s16 f2v9s19 40.000000 +f1v9s16 f2v12s22 40.000000 +f1v9s16 f0v0s0 42.000000 +f1v9s16 f1v6s8 42.000000 +f1v9s16 f2v13s23 40.000000 +f1v9s16 f2v4s14 39.000000 +f1v9s16 f2v3s13 39.000000 +f1v9s16 f2v2s12 40.000000 +f1v9s16 f2v6s16 40.000000 +f1v9s16 f2v7s17 40.000000 +f1v9s16 f1v3s5 35.000000 +f1v9s16 f2v10s20 40.000000 +f1v9s16 f2v1s11 39.000000 +f1v9s16 f0v1s1 42.000000 +f1v9s16 f2v8s18 38.000000 +f1v9s16 f2v0s10 40.000000 +f1v9s16 f1v2s4 35.000000 +f1v9s16 f2v5s15 41.000000 +f1v9s16 f1v7s9 37.000000 +f1v9s16 f1v5s7 35.000000 +f1v9s16 f1v0s2 36.000000 +f1v9s16 f2v11s21 40.000000 +f1v9s16 f1v4s6 36.000000 +f1v9s16 f1v1s3 36.000000 +f1v9s16 f2v14s24 40.000000 +f1v10s17 f2v9s19 37.000000 +f1v10s17 f2v12s22 37.000000 +f1v10s17 f0v0s0 42.000000 +f1v10s17 f1v6s8 42.000000 +f1v10s17 f2v13s23 37.000000 +f1v10s17 f2v4s14 36.000000 +f1v10s17 f2v3s13 37.000000 +f1v10s17 f2v2s12 37.000000 +f1v10s17 f2v6s16 38.000000 +f1v10s17 f2v7s17 37.000000 +f1v10s17 f1v3s5 36.000000 +f1v10s17 f2v10s20 37.000000 +f1v10s17 f2v1s11 36.000000 +f1v10s17 f0v1s1 42.000000 +f1v10s17 f2v8s18 36.000000 +f1v10s17 f2v0s10 37.000000 +f1v10s17 f1v2s4 35.000000 +f1v10s17 f2v5s15 38.000000 +f1v10s17 f1v7s9 37.000000 +f1v10s17 f1v5s7 36.000000 +f1v10s17 f1v0s2 37.000000 +f1v10s17 f2v11s21 38.000000 +f1v10s17 f1v4s6 37.000000 +f1v10s17 f1v1s3 37.000000 +f1v10s17 f2v14s24 37.000000 +f1v0s7 f2v9s19 39.000000 +f1v0s7 f2v12s22 39.000000 +f1v0s7 f0v0s0 40.000000 +f1v0s7 f1v6s8 40.000000 +f1v0s7 f2v13s23 39.000000 +f1v0s7 f2v4s14 38.000000 +f1v0s7 f2v3s13 38.000000 +f1v0s7 f2v2s12 39.000000 +f1v0s7 f2v6s16 40.000000 +f1v0s7 f2v7s17 39.000000 +f1v0s7 f1v3s5 34.000000 +f1v0s7 f2v10s20 39.000000 +f1v0s7 f2v1s11 38.000000 +f1v0s7 f0v1s1 40.000000 +f1v0s7 f2v8s18 38.000000 +f1v0s7 f2v0s10 39.000000 +f1v0s7 f1v2s4 34.000000 +f1v0s7 f2v5s15 40.000000 +f1v0s7 f1v7s9 36.000000 +f1v0s7 f1v5s7 34.000000 +f1v0s7 f1v0s2 35.000000 +f1v0s7 f2v11s21 40.000000 +f1v0s7 f1v4s6 35.000000 +f1v0s7 f1v1s3 35.000000 +f1v0s7 f2v14s24 39.000000 +f3v9s34 f2v9s19 39.000000 +f3v9s34 f2v12s22 38.000000 +f3v9s34 f0v0s0 39.000000 +f3v9s34 f1v6s8 39.000000 +f3v9s34 f2v13s23 38.000000 +f3v9s34 f2v4s14 38.000000 +f3v9s34 f2v3s13 38.000000 +f3v9s34 f2v2s12 38.000000 +f3v9s34 f2v6s16 37.000000 +f3v9s34 f2v7s17 38.000000 +f3v9s34 f1v3s5 33.000000 +f3v9s34 f2v10s20 38.000000 +f3v9s34 f2v1s11 38.000000 +f3v9s34 f0v1s1 39.000000 +f3v9s34 f2v8s18 37.000000 +f3v9s34 f2v0s10 38.000000 +f3v9s34 f1v2s4 33.000000 +f3v9s34 f2v5s15 38.000000 +f3v9s34 f1v7s9 34.000000 +f3v9s34 f1v5s7 34.000000 +f3v9s34 f1v0s2 34.000000 +f3v9s34 f2v11s21 37.000000 +f3v9s34 f1v4s6 34.000000 +f3v9s34 f1v1s3 34.000000 +f3v9s34 f2v14s24 39.000000 +f4v0s40 f2v9s19 40.000000 +f4v0s40 f2v12s22 39.000000 +f4v0s40 f0v0s0 42.000000 +f4v0s40 f1v6s8 42.000000 +f4v0s40 f2v13s23 40.000000 +f4v0s40 f2v4s14 38.000000 +f4v0s40 f2v3s13 39.000000 +f4v0s40 f2v2s12 39.000000 +f4v0s40 f2v6s16 39.000000 +f4v0s40 f2v7s17 40.000000 +f4v0s40 f1v3s5 35.000000 +f4v0s40 f2v10s20 40.000000 +f4v0s40 f2v1s11 39.000000 +f4v0s40 f0v1s1 42.000000 +f4v0s40 f2v8s18 38.000000 +f4v0s40 f2v0s10 40.000000 +f4v0s40 f1v2s4 33.000000 +f4v0s40 f2v5s15 38.000000 +f4v0s40 f1v7s9 33.000000 +f4v0s40 f1v5s7 35.000000 +f4v0s40 f1v0s2 35.000000 +f4v0s40 f2v11s21 39.000000 +f4v0s40 f1v4s6 36.000000 +f4v0s40 f1v1s3 35.000000 +f4v0s40 f2v14s24 39.000000 +f1v7s14 f2v9s19 39.000000 +f1v7s14 f2v12s22 39.000000 +f1v7s14 f0v0s0 40.000000 +f1v7s14 f1v6s8 40.000000 +f1v7s14 f2v13s23 39.000000 +f1v7s14 f2v4s14 38.000000 +f1v7s14 f2v3s13 38.000000 +f1v7s14 f2v2s12 39.000000 +f1v7s14 f2v6s16 40.000000 +f1v7s14 f2v7s17 39.000000 +f1v7s14 f1v3s5 34.000000 +f1v7s14 f2v10s20 39.000000 +f1v7s14 f2v1s11 38.000000 +f1v7s14 f0v1s1 40.000000 +f1v7s14 f2v8s18 38.000000 +f1v7s14 f2v0s10 39.000000 +f1v7s14 f1v2s4 34.000000 +f1v7s14 f2v5s15 40.000000 +f1v7s14 f1v7s9 36.000000 +f1v7s14 f1v5s7 34.000000 +f1v7s14 f1v0s2 35.000000 +f1v7s14 f2v11s21 40.000000 +f1v7s14 f1v4s6 35.000000 +f1v7s14 f1v1s3 35.000000 +f1v7s14 f2v14s24 39.000000 +f3v2s27 f2v9s19 39.000000 +f3v2s27 f2v12s22 39.000000 +f3v2s27 f0v0s0 38.000000 +f3v2s27 f1v6s8 38.000000 +f3v2s27 f2v13s23 38.000000 +f3v2s27 f2v4s14 38.000000 +f3v2s27 f2v3s13 38.000000 +f3v2s27 f2v2s12 38.000000 +f3v2s27 f2v6s16 38.000000 +f3v2s27 f2v7s17 38.000000 +f3v2s27 f1v3s5 32.000000 +f3v2s27 f2v10s20 38.000000 +f3v2s27 f2v1s11 38.000000 +f3v2s27 f0v1s1 38.000000 +f3v2s27 f2v8s18 38.000000 +f3v2s27 f2v0s10 38.000000 +f3v2s27 f1v2s4 32.000000 +f3v2s27 f2v5s15 38.000000 +f3v2s27 f1v7s9 34.000000 +f3v2s27 f1v5s7 33.000000 +f3v2s27 f1v0s2 33.000000 +f3v2s27 f2v11s21 38.000000 +f3v2s27 f1v4s6 33.000000 +f3v2s27 f1v1s3 33.000000 +f3v2s27 f2v14s24 39.000000 +f4v1s41 f2v9s19 40.000000 +f4v1s41 f2v12s22 39.000000 +f4v1s41 f0v0s0 42.000000 +f4v1s41 f1v6s8 42.000000 +f4v1s41 f2v13s23 40.000000 +f4v1s41 f2v4s14 39.000000 +f4v1s41 f2v3s13 39.000000 +f4v1s41 f2v2s12 39.000000 +f4v1s41 f2v6s16 40.000000 +f4v1s41 f2v7s17 40.000000 +f4v1s41 f1v3s5 35.000000 +f4v1s41 f2v10s20 40.000000 +f4v1s41 f2v1s11 39.000000 +f4v1s41 f0v1s1 42.000000 +f4v1s41 f2v8s18 39.000000 +f4v1s41 f2v0s10 40.000000 +f4v1s41 f1v2s4 33.000000 +f4v1s41 f2v5s15 39.000000 +f4v1s41 f1v7s9 34.000000 +f4v1s41 f1v5s7 35.000000 +f4v1s41 f1v0s2 35.000000 +f4v1s41 f2v11s21 40.000000 +f4v1s41 f1v4s6 36.000000 +f4v1s41 f1v1s3 35.000000 +f4v1s41 f2v14s24 39.000000 +f2v0s22 f2v9s19 41.000000 +f2v0s22 f2v12s22 41.000000 +f2v0s22 f0v0s0 44.000000 +f2v0s22 f1v6s8 44.000000 +f2v0s22 f2v13s23 41.000000 +f2v0s22 f2v4s14 40.000000 +f2v0s22 f2v3s13 40.000000 +f2v0s22 f2v2s12 40.000000 +f2v0s22 f2v6s16 40.000000 +f2v0s22 f2v7s17 41.000000 +f2v0s22 f1v3s5 33.000000 +f2v0s22 f2v10s20 41.000000 +f2v0s22 f2v1s11 41.000000 +f2v0s22 f0v1s1 44.000000 +f2v0s22 f2v8s18 40.000000 +f2v0s22 f2v0s10 41.000000 +f2v0s22 f1v2s4 33.000000 +f2v0s22 f2v5s15 40.000000 +f2v0s22 f1v7s9 33.000000 +f2v0s22 f1v5s7 34.000000 +f2v0s22 f1v0s2 34.000000 +f2v0s22 f2v11s21 40.000000 +f2v0s22 f1v4s6 34.000000 +f2v0s22 f1v1s3 34.000000 +f2v0s22 f2v14s24 41.000000 +f1v2s9 f2v9s19 40.000000 +f1v2s9 f2v12s22 40.000000 +f1v2s9 f0v0s0 44.000000 +f1v2s9 f1v6s8 44.000000 +f1v2s9 f2v13s23 40.000000 +f1v2s9 f2v4s14 39.000000 +f1v2s9 f2v3s13 40.000000 +f1v2s9 f2v2s12 40.000000 +f1v2s9 f2v6s16 40.000000 +f1v2s9 f2v7s17 40.000000 +f1v2s9 f1v3s5 34.000000 +f1v2s9 f2v10s20 40.000000 +f1v2s9 f2v1s11 41.000000 +f1v2s9 f0v1s1 44.000000 +f1v2s9 f2v8s18 39.000000 +f1v2s9 f2v0s10 40.000000 +f1v2s9 f1v2s4 34.000000 +f1v2s9 f2v5s15 39.000000 +f1v2s9 f1v7s9 34.000000 +f1v2s9 f1v5s7 35.000000 +f1v2s9 f1v0s2 35.000000 +f1v2s9 f2v11s21 40.000000 +f1v2s9 f1v4s6 35.000000 +f1v2s9 f1v1s3 35.000000 +f1v2s9 f2v14s24 41.000000 +f1v3s10 f2v9s19 39.000000 +f1v3s10 f2v12s22 39.000000 +f1v3s10 f0v0s0 40.000000 +f1v3s10 f1v6s8 40.000000 +f1v3s10 f2v13s23 39.000000 +f1v3s10 f2v4s14 38.000000 +f1v3s10 f2v3s13 38.000000 +f1v3s10 f2v2s12 39.000000 +f1v3s10 f2v6s16 40.000000 +f1v3s10 f2v7s17 39.000000 +f1v3s10 f1v3s5 34.000000 +f1v3s10 f2v10s20 39.000000 +f1v3s10 f2v1s11 38.000000 +f1v3s10 f0v1s1 40.000000 +f1v3s10 f2v8s18 38.000000 +f1v3s10 f2v0s10 39.000000 +f1v3s10 f1v2s4 34.000000 +f1v3s10 f2v5s15 40.000000 +f1v3s10 f1v7s9 36.000000 +f1v3s10 f1v5s7 34.000000 +f1v3s10 f1v0s2 35.000000 +f1v3s10 f2v11s21 40.000000 +f1v3s10 f1v4s6 35.000000 +f1v3s10 f1v1s3 35.000000 +f1v3s10 f2v14s24 39.000000 +f3v0s25 f2v9s19 39.000000 +f3v0s25 f2v12s22 39.000000 +f3v0s25 f0v0s0 39.000000 +f3v0s25 f1v6s8 39.000000 +f3v0s25 f2v13s23 38.000000 +f3v0s25 f2v4s14 38.000000 +f3v0s25 f2v3s13 38.000000 +f3v0s25 f2v2s12 38.000000 +f3v0s25 f2v6s16 38.000000 +f3v0s25 f2v7s17 38.000000 +f3v0s25 f1v3s5 33.000000 +f3v0s25 f2v10s20 38.000000 +f3v0s25 f2v1s11 38.000000 +f3v0s25 f0v1s1 39.000000 +f3v0s25 f2v8s18 38.000000 +f3v0s25 f2v0s10 38.000000 +f3v0s25 f1v2s4 33.000000 +f3v0s25 f2v5s15 38.000000 +f3v0s25 f1v7s9 34.000000 +f3v0s25 f1v5s7 34.000000 +f3v0s25 f1v0s2 34.000000 +f3v0s25 f2v11s21 38.000000 +f3v0s25 f1v4s6 34.000000 +f3v0s25 f1v1s3 34.000000 +f3v0s25 f2v14s24 39.000000 +f3v3s28 f2v9s19 38.000000 +f3v3s28 f2v12s22 38.000000 +f3v3s28 f0v0s0 39.000000 +f3v3s28 f1v6s8 39.000000 +f3v3s28 f2v13s23 37.000000 +f3v3s28 f2v4s14 37.000000 +f3v3s28 f2v3s13 37.000000 +f3v3s28 f2v2s12 37.000000 +f3v3s28 f2v6s16 38.000000 +f3v3s28 f2v7s17 37.000000 +f3v3s28 f1v3s5 33.000000 +f3v3s28 f2v10s20 37.000000 +f3v3s28 f2v1s11 38.000000 +f3v3s28 f0v1s1 39.000000 +f3v3s28 f2v8s18 37.000000 +f3v3s28 f2v0s10 37.000000 +f3v3s28 f1v2s4 34.000000 +f3v3s28 f2v5s15 38.000000 +f3v3s28 f1v7s9 34.000000 +f3v3s28 f1v5s7 35.000000 +f3v3s28 f1v0s2 35.000000 +f3v3s28 f2v11s21 38.000000 +f3v3s28 f1v4s6 35.000000 +f3v3s28 f1v1s3 35.000000 +f3v3s28 f2v14s24 39.000000 +f3v13s38 f2v9s19 40.000000 +f3v13s38 f2v12s22 39.000000 +f3v13s38 f0v0s0 39.000000 +f3v13s38 f1v6s8 39.000000 +f3v13s38 f2v13s23 39.000000 +f3v13s38 f2v4s14 38.000000 +f3v13s38 f2v3s13 39.000000 +f3v13s38 f2v2s12 39.000000 +f3v13s38 f2v6s16 39.000000 +f3v13s38 f2v7s17 39.000000 +f3v13s38 f1v3s5 34.000000 +f3v13s38 f2v10s20 39.000000 +f3v13s38 f2v1s11 38.000000 +f3v13s38 f0v1s1 39.000000 +f3v13s38 f2v8s18 38.000000 +f3v13s38 f2v0s10 39.000000 +f3v13s38 f1v2s4 34.000000 +f3v13s38 f2v5s15 38.000000 +f3v13s38 f1v7s9 35.000000 +f3v13s38 f1v5s7 35.000000 +f3v13s38 f1v0s2 35.000000 +f3v13s38 f2v11s21 39.000000 +f3v13s38 f1v4s6 36.000000 +f3v13s38 f1v1s3 35.000000 +f3v13s38 f2v14s24 40.000000 +f3v7s32 f2v9s19 39.000000 +f3v7s32 f2v12s22 38.000000 +f3v7s32 f0v0s0 39.000000 +f3v7s32 f1v6s8 39.000000 +f3v7s32 f2v13s23 38.000000 +f3v7s32 f2v4s14 38.000000 +f3v7s32 f2v3s13 38.000000 +f3v7s32 f2v2s12 38.000000 +f3v7s32 f2v6s16 37.000000 +f3v7s32 f2v7s17 38.000000 +f3v7s32 f1v3s5 33.000000 +f3v7s32 f2v10s20 38.000000 +f3v7s32 f2v1s11 38.000000 +f3v7s32 f0v1s1 39.000000 +f3v7s32 f2v8s18 37.000000 +f3v7s32 f2v0s10 38.000000 +f3v7s32 f1v2s4 33.000000 +f3v7s32 f2v5s15 38.000000 +f3v7s32 f1v7s9 34.000000 +f3v7s32 f1v5s7 34.000000 +f3v7s32 f1v0s2 34.000000 +f3v7s32 f2v11s21 37.000000 +f3v7s32 f1v4s6 34.000000 +f3v7s32 f1v1s3 34.000000 +f3v7s32 f2v14s24 39.000000 +f3v1s26 f2v9s19 38.000000 +f3v1s26 f2v12s22 38.000000 +f3v1s26 f0v0s0 38.000000 +f3v1s26 f1v6s8 38.000000 +f3v1s26 f2v13s23 37.000000 +f3v1s26 f2v4s14 37.000000 +f3v1s26 f2v3s13 37.000000 +f3v1s26 f2v2s12 37.000000 +f3v1s26 f2v6s16 38.000000 +f3v1s26 f2v7s17 37.000000 +f3v1s26 f1v3s5 31.000000 +f3v1s26 f2v10s20 37.000000 +f3v1s26 f2v1s11 38.000000 +f3v1s26 f0v1s1 38.000000 +f3v1s26 f2v8s18 37.000000 +f3v1s26 f2v0s10 37.000000 +f3v1s26 f1v2s4 31.000000 +f3v1s26 f2v5s15 37.000000 +f3v1s26 f1v7s9 32.000000 +f3v1s26 f1v5s7 32.000000 +f3v1s26 f1v0s2 32.000000 +f3v1s26 f2v11s21 38.000000 +f3v1s26 f1v4s6 33.000000 +f3v1s26 f1v1s3 32.000000 +f3v1s26 f2v14s24 39.000000 +f3v6s31 f2v9s19 39.000000 +f3v6s31 f2v12s22 38.000000 +f3v6s31 f0v0s0 39.000000 +f3v6s31 f1v6s8 39.000000 +f3v6s31 f2v13s23 38.000000 +f3v6s31 f2v4s14 38.000000 +f3v6s31 f2v3s13 38.000000 +f3v6s31 f2v2s12 38.000000 +f3v6s31 f2v6s16 37.000000 +f3v6s31 f2v7s17 38.000000 +f3v6s31 f1v3s5 33.000000 +f3v6s31 f2v10s20 38.000000 +f3v6s31 f2v1s11 38.000000 +f3v6s31 f0v1s1 39.000000 +f3v6s31 f2v8s18 37.000000 +f3v6s31 f2v0s10 38.000000 +f3v6s31 f1v2s4 33.000000 +f3v6s31 f2v5s15 38.000000 +f3v6s31 f1v7s9 34.000000 +f3v6s31 f1v5s7 34.000000 +f3v6s31 f1v0s2 34.000000 +f3v6s31 f2v11s21 37.000000 +f3v6s31 f1v4s6 34.000000 +f3v6s31 f1v1s3 34.000000 +f3v6s31 f2v14s24 39.000000 +f1v13s20 f2v9s19 38.000000 +f1v13s20 f2v12s22 38.000000 +f1v13s20 f0v0s0 39.000000 +f1v13s20 f1v6s8 39.000000 +f1v13s20 f2v13s23 38.000000 +f1v13s20 f2v4s14 37.000000 +f1v13s20 f2v3s13 37.000000 +f1v13s20 f2v2s12 38.000000 +f1v13s20 f2v6s16 40.000000 +f1v13s20 f2v7s17 38.000000 +f1v13s20 f1v3s5 34.000000 +f1v13s20 f2v10s20 38.000000 +f1v13s20 f2v1s11 37.000000 +f1v13s20 f0v1s1 39.000000 +f1v13s20 f2v8s18 37.000000 +f1v13s20 f2v0s10 38.000000 +f1v13s20 f1v2s4 34.000000 +f1v13s20 f2v5s15 39.000000 +f1v13s20 f1v7s9 36.000000 +f1v13s20 f1v5s7 34.000000 +f1v13s20 f1v0s2 35.000000 +f1v13s20 f2v11s21 40.000000 +f1v13s20 f1v4s6 35.000000 +f1v13s20 f1v1s3 35.000000 +f1v13s20 f2v14s24 38.000000 +f2v2s24 f2v9s19 36.000000 +f2v2s24 f2v12s22 37.000000 +f2v2s24 f0v0s0 37.000000 +f2v2s24 f1v6s8 37.000000 +f2v2s24 f2v13s23 36.000000 +f2v2s24 f2v4s14 35.000000 +f2v2s24 f2v3s13 37.000000 +f2v2s24 f2v2s12 36.000000 +f2v2s24 f2v6s16 37.000000 +f2v2s24 f2v7s17 36.000000 +f2v2s24 f1v3s5 29.000000 +f2v2s24 f2v10s20 36.000000 +f2v2s24 f2v1s11 37.000000 +f2v2s24 f0v1s1 37.000000 +f2v2s24 f2v8s18 35.000000 +f2v2s24 f2v0s10 36.000000 +f2v2s24 f1v2s4 28.000000 +f2v2s24 f2v5s15 37.000000 +f2v2s24 f1v7s9 28.000000 +f2v2s24 f1v5s7 30.000000 +f2v2s24 f1v0s2 30.000000 +f2v2s24 f2v11s21 37.000000 +f2v2s24 f1v4s6 29.000000 +f2v2s24 f1v1s3 30.000000 +f2v2s24 f2v14s24 36.000000 +f3v11s36 f2v9s19 39.000000 +f3v11s36 f2v12s22 38.000000 +f3v11s36 f0v0s0 39.000000 +f3v11s36 f1v6s8 39.000000 +f3v11s36 f2v13s23 38.000000 +f3v11s36 f2v4s14 38.000000 +f3v11s36 f2v3s13 38.000000 +f3v11s36 f2v2s12 38.000000 +f3v11s36 f2v6s16 37.000000 +f3v11s36 f2v7s17 38.000000 +f3v11s36 f1v3s5 31.000000 +f3v11s36 f2v10s20 38.000000 +f3v11s36 f2v1s11 38.000000 +f3v11s36 f0v1s1 39.000000 +f3v11s36 f2v8s18 37.000000 +f3v11s36 f2v0s10 38.000000 +f3v11s36 f1v2s4 32.000000 +f3v11s36 f2v5s15 38.000000 +f3v11s36 f1v7s9 33.000000 +f3v11s36 f1v5s7 32.000000 +f3v11s36 f1v0s2 32.000000 +f3v11s36 f2v11s21 37.000000 +f3v11s36 f1v4s6 34.000000 +f3v11s36 f1v1s3 32.000000 +f3v11s36 f2v14s24 37.000000 +f0v5s5 f2v9s19 40.000000 +f0v5s5 f2v12s22 40.000000 +f0v5s5 f0v0s0 43.000000 +f0v5s5 f1v6s8 43.000000 +f0v5s5 f2v13s23 40.000000 +f0v5s5 f2v4s14 39.000000 +f0v5s5 f2v3s13 40.000000 +f0v5s5 f2v2s12 39.000000 +f0v5s5 f2v6s16 39.000000 +f0v5s5 f2v7s17 40.000000 +f0v5s5 f1v3s5 33.000000 +f0v5s5 f2v10s20 40.000000 +f0v5s5 f2v1s11 40.000000 +f0v5s5 f0v1s1 43.000000 +f0v5s5 f2v8s18 39.000000 +f0v5s5 f2v0s10 40.000000 +f0v5s5 f1v2s4 34.000000 +f0v5s5 f2v5s15 39.000000 +f0v5s5 f1v7s9 34.000000 +f0v5s5 f1v5s7 35.000000 +f0v5s5 f1v0s2 35.000000 +f0v5s5 f2v11s21 39.000000 +f0v5s5 f1v4s6 35.000000 +f0v5s5 f1v1s3 35.000000 +f0v5s5 f2v14s24 41.000000 +f1v11s18 f2v9s19 39.000000 +f1v11s18 f2v12s22 39.000000 +f1v11s18 f0v0s0 41.000000 +f1v11s18 f1v6s8 41.000000 +f1v11s18 f2v13s23 39.000000 +f1v11s18 f2v4s14 38.000000 +f1v11s18 f2v3s13 38.000000 +f1v11s18 f2v2s12 39.000000 +f1v11s18 f2v6s16 40.000000 +f1v11s18 f2v7s17 39.000000 +f1v11s18 f1v3s5 34.000000 +f1v11s18 f2v10s20 39.000000 +f1v11s18 f2v1s11 38.000000 +f1v11s18 f0v1s1 41.000000 +f1v11s18 f2v8s18 38.000000 +f1v11s18 f2v0s10 39.000000 +f1v11s18 f1v2s4 34.000000 +f1v11s18 f2v5s15 40.000000 +f1v11s18 f1v7s9 36.000000 +f1v11s18 f1v5s7 34.000000 +f1v11s18 f1v0s2 35.000000 +f1v11s18 f2v11s21 40.000000 +f1v11s18 f1v4s6 35.000000 +f1v11s18 f1v1s3 35.000000 +f1v11s18 f2v14s24 39.000000 +f0v6s6 f2v9s19 40.000000 +f0v6s6 f2v12s22 40.000000 +f0v6s6 f0v0s0 43.000000 +f0v6s6 f1v6s8 43.000000 +f0v6s6 f2v13s23 40.000000 +f0v6s6 f2v4s14 39.000000 +f0v6s6 f2v3s13 40.000000 +f0v6s6 f2v2s12 40.000000 +f0v6s6 f2v6s16 40.000000 +f0v6s6 f2v7s17 40.000000 +f0v6s6 f1v3s5 33.000000 +f0v6s6 f2v10s20 40.000000 +f0v6s6 f2v1s11 41.000000 +f0v6s6 f0v1s1 43.000000 +f0v6s6 f2v8s18 39.000000 +f0v6s6 f2v0s10 40.000000 +f0v6s6 f1v2s4 32.000000 +f0v6s6 f2v5s15 39.000000 +f0v6s6 f1v7s9 33.000000 +f0v6s6 f1v5s7 33.000000 +f0v6s6 f1v0s2 33.000000 +f0v6s6 f2v11s21 40.000000 +f0v6s6 f1v4s6 34.000000 +f0v6s6 f1v1s3 33.000000 +f0v6s6 f2v14s24 42.000000 +f3v4s29 f2v9s19 39.000000 +f3v4s29 f2v12s22 40.000000 +f3v4s29 f0v0s0 40.000000 +f3v4s29 f1v6s8 40.000000 +f3v4s29 f2v13s23 39.000000 +f3v4s29 f2v4s14 39.000000 +f3v4s29 f2v3s13 39.000000 +f3v4s29 f2v2s12 39.000000 +f3v4s29 f2v6s16 39.000000 +f3v4s29 f2v7s17 39.000000 +f3v4s29 f1v3s5 33.000000 +f3v4s29 f2v10s20 39.000000 +f3v4s29 f2v1s11 38.000000 +f3v4s29 f0v1s1 40.000000 +f3v4s29 f2v8s18 39.000000 +f3v4s29 f2v0s10 39.000000 +f3v4s29 f1v2s4 33.000000 +f3v4s29 f2v5s15 38.000000 +f3v4s29 f1v7s9 34.000000 +f3v4s29 f1v5s7 34.000000 +f3v4s29 f1v0s2 34.000000 +f3v4s29 f2v11s21 39.000000 +f3v4s29 f1v4s6 34.000000 +f3v4s29 f1v1s3 34.000000 +f3v4s29 f2v14s24 39.000000 +f0v2s2 f2v9s19 41.000000 +f0v2s2 f2v12s22 41.000000 +f0v2s2 f0v0s0 44.000000 +f0v2s2 f1v6s8 44.000000 +f0v2s2 f2v13s23 41.000000 +f0v2s2 f2v4s14 40.000000 +f0v2s2 f2v3s13 40.000000 +f0v2s2 f2v2s12 40.000000 +f0v2s2 f2v6s16 40.000000 +f0v2s2 f2v7s17 41.000000 +f0v2s2 f1v3s5 33.000000 +f0v2s2 f2v10s20 41.000000 +f0v2s2 f2v1s11 41.000000 +f0v2s2 f0v1s1 44.000000 +f0v2s2 f2v8s18 40.000000 +f0v2s2 f2v0s10 41.000000 +f0v2s2 f1v2s4 33.000000 +f0v2s2 f2v5s15 40.000000 +f0v2s2 f1v7s9 33.000000 +f0v2s2 f1v5s7 34.000000 +f0v2s2 f1v0s2 34.000000 +f0v2s2 f2v11s21 40.000000 +f0v2s2 f1v4s6 34.000000 +f0v2s2 f1v1s3 34.000000 +f0v2s2 f2v14s24 41.000000 +f3v12s37 f2v9s19 41.000000 +f3v12s37 f2v12s22 40.000000 +f3v12s37 f0v0s0 40.000000 +f3v12s37 f1v6s8 40.000000 +f3v12s37 f2v13s23 40.000000 +f3v12s37 f2v4s14 40.000000 +f3v12s37 f2v3s13 40.000000 +f3v12s37 f2v2s12 40.000000 +f3v12s37 f2v6s16 39.000000 +f3v12s37 f2v7s17 40.000000 +f3v12s37 f1v3s5 33.000000 +f3v12s37 f2v10s20 40.000000 +f3v12s37 f2v1s11 39.000000 +f3v12s37 f0v1s1 40.000000 +f3v12s37 f2v8s18 40.000000 +f3v12s37 f2v0s10 40.000000 +f3v12s37 f1v2s4 34.000000 +f3v12s37 f2v5s15 39.000000 +f3v12s37 f1v7s9 35.000000 +f3v12s37 f1v5s7 35.000000 +f3v12s37 f1v0s2 35.000000 +f3v12s37 f2v11s21 39.000000 +f3v12s37 f1v4s6 35.000000 +f3v12s37 f1v1s3 35.000000 +f3v12s37 f2v14s24 40.000000 +f0v3s3 f2v9s19 40.000000 +f0v3s3 f2v12s22 40.000000 +f0v3s3 f0v0s0 42.000000 +f0v3s3 f1v6s8 42.000000 +f0v3s3 f2v13s23 40.000000 +f0v3s3 f2v4s14 39.000000 +f0v3s3 f2v3s13 39.000000 +f0v3s3 f2v2s12 39.000000 +f0v3s3 f2v6s16 39.000000 +f0v3s3 f2v7s17 40.000000 +f0v3s3 f1v3s5 34.000000 +f0v3s3 f2v10s20 40.000000 +f0v3s3 f2v1s11 40.000000 +f0v3s3 f0v1s1 42.000000 +f0v3s3 f2v8s18 39.000000 +f0v3s3 f2v0s10 40.000000 +f0v3s3 f1v2s4 34.000000 +f0v3s3 f2v5s15 39.000000 +f0v3s3 f1v7s9 34.000000 +f0v3s3 f1v5s7 34.000000 +f0v3s3 f1v0s2 34.000000 +f0v3s3 f2v11s21 39.000000 +f0v3s3 f1v4s6 34.000000 +f0v3s3 f1v1s3 34.000000 +f0v3s3 f2v14s24 40.000000 +f3v14s39 f2v9s19 39.000000 +f3v14s39 f2v12s22 39.000000 +f3v14s39 f0v0s0 39.000000 +f3v14s39 f1v6s8 39.000000 +f3v14s39 f2v13s23 38.000000 +f3v14s39 f2v4s14 38.000000 +f3v14s39 f2v3s13 38.000000 +f3v14s39 f2v2s12 38.000000 +f3v14s39 f2v6s16 38.000000 +f3v14s39 f2v7s17 38.000000 +f3v14s39 f1v3s5 33.000000 +f3v14s39 f2v10s20 38.000000 +f3v14s39 f2v1s11 38.000000 +f3v14s39 f0v1s1 39.000000 +f3v14s39 f2v8s18 38.000000 +f3v14s39 f2v0s10 38.000000 +f3v14s39 f1v2s4 33.000000 +f3v14s39 f2v5s15 38.000000 +f3v14s39 f1v7s9 34.000000 +f3v14s39 f1v5s7 34.000000 +f3v14s39 f1v0s2 34.000000 +f3v14s39 f2v11s21 38.000000 +f3v14s39 f1v4s6 34.000000 +f3v14s39 f1v1s3 34.000000 +f3v14s39 f2v14s24 39.000000 +f0v0s0 f2v9s19 40.000000 +f0v0s0 f2v12s22 40.000000 +f0v0s0 f0v0s0 44.000000 +f0v0s0 f1v6s8 44.000000 +f0v0s0 f2v13s23 40.000000 +f0v0s0 f2v4s14 39.000000 +f0v0s0 f2v3s13 39.000000 +f0v0s0 f2v2s12 40.000000 +f0v0s0 f2v6s16 40.000000 +f0v0s0 f2v7s17 40.000000 +f0v0s0 f1v3s5 34.000000 +f0v0s0 f2v10s20 40.000000 +f0v0s0 f2v1s11 41.000000 +f0v0s0 f0v1s1 44.000000 +f0v0s0 f2v8s18 39.000000 +f0v0s0 f2v0s10 40.000000 +f0v0s0 f1v2s4 34.000000 +f0v0s0 f2v5s15 39.000000 +f0v0s0 f1v7s9 34.000000 +f0v0s0 f1v5s7 35.000000 +f0v0s0 f1v0s2 35.000000 +f0v0s0 f2v11s21 40.000000 +f0v0s0 f1v4s6 35.000000 +f0v0s0 f1v1s3 35.000000 +f0v0s0 f2v14s24 41.000000 +f1v12s19 f2v9s19 38.000000 +f1v12s19 f2v12s22 38.000000 +f1v12s19 f0v0s0 40.000000 +f1v12s19 f1v6s8 40.000000 +f1v12s19 f2v13s23 38.000000 +f1v12s19 f2v4s14 37.000000 +f1v12s19 f2v3s13 37.000000 +f1v12s19 f2v2s12 38.000000 +f1v12s19 f2v6s16 39.000000 +f1v12s19 f2v7s17 38.000000 +f1v12s19 f1v3s5 33.000000 +f1v12s19 f2v10s20 38.000000 +f1v12s19 f2v1s11 37.000000 +f1v12s19 f0v1s1 40.000000 +f1v12s19 f2v8s18 37.000000 +f1v12s19 f2v0s10 38.000000 +f1v12s19 f1v2s4 33.000000 +f1v12s19 f2v5s15 39.000000 +f1v12s19 f1v7s9 35.000000 +f1v12s19 f1v5s7 33.000000 +f1v12s19 f1v0s2 34.000000 +f1v12s19 f2v11s21 39.000000 +f1v12s19 f1v4s6 34.000000 +f1v12s19 f1v1s3 34.000000 +f1v12s19 f2v14s24 38.000000 diff --git a/pkg/obitools/obialign/testdata/two_allshortest.expected b/pkg/obitools/obialign/testdata/two_allshortest.expected new file mode 100644 index 0000000..e52af63 --- /dev/null +++ b/pkg/obitools/obialign/testdata/two_allshortest.expected @@ -0,0 +1,1075 @@ +f3v10s35 f2v9s19 0.615385 +f3v10s35 f2v12s22 0.615385 +f3v10s35 f0v0s0 0.600000 +f3v10s35 f1v6s8 0.600000 +f3v10s35 f2v13s23 0.600000 +f3v10s35 f2v4s14 0.603175 +f3v10s35 f2v3s13 0.600000 +f3v10s35 f2v2s12 0.600000 +f3v10s35 f2v6s16 0.615385 +f3v10s35 f2v7s17 0.600000 +f3v10s35 f1v3s5 0.666667 +f3v10s35 f2v10s20 0.600000 +f3v10s35 f2v1s11 0.615385 +f3v10s35 f0v1s1 0.600000 +f3v10s35 f2v8s18 0.609375 +f3v10s35 f2v0s10 0.600000 +f3v10s35 f1v2s4 0.645833 +f3v10s35 f2v5s15 0.593750 +f3v10s35 f1v7s9 0.640000 +f3v10s35 f1v5s7 0.653061 +f3v10s35 f1v0s2 0.627451 +f3v10s35 f2v11s21 0.615385 +f3v10s35 f1v4s6 0.622642 +f3v10s35 f1v1s3 0.627451 +f3v10s35 f2v14s24 0.615385 +f3v5s30 f2v9s19 0.603175 +f3v5s30 f2v12s22 0.603175 +f3v5s30 f0v0s0 0.603175 +f3v5s30 f1v6s8 0.603175 +f3v5s30 f2v13s23 0.587302 +f3v5s30 f2v4s14 0.571429 +f3v5s30 f2v3s13 0.587302 +f3v5s30 f2v2s12 0.587302 +f3v5s30 f2v6s16 0.603175 +f3v5s30 f2v7s17 0.587302 +f3v5s30 f1v3s5 0.645833 +f3v5s30 f2v10s20 0.587302 +f3v5s30 f2v1s11 0.587302 +f3v5s30 f0v1s1 0.603175 +f3v5s30 f2v8s18 0.571429 +f3v5s30 f2v0s10 0.587302 +f3v5s30 f1v2s4 0.666667 +f3v5s30 f2v5s15 0.587302 +f3v5s30 f1v7s9 0.640000 +f3v5s30 f1v5s7 0.632653 +f3v5s30 f1v0s2 0.627451 +f3v5s30 f2v11s21 0.603175 +f3v5s30 f1v4s6 0.603774 +f3v5s30 f1v1s3 0.627451 +f3v5s30 f2v14s24 0.587302 +f1v8s15 f2v9s19 0.666667 +f1v8s15 f2v12s22 0.666667 +f1v8s15 f0v0s0 0.684211 +f1v8s15 f1v6s8 0.684211 +f1v8s15 f2v13s23 0.666667 +f1v8s15 f2v4s14 0.649123 +f1v8s15 f2v3s13 0.649123 +f1v8s15 f2v2s12 0.666667 +f1v8s15 f2v6s16 0.684211 +f1v8s15 f2v7s17 0.666667 +f1v8s15 f1v3s5 0.687500 +f1v8s15 f2v10s20 0.666667 +f1v8s15 f2v1s11 0.666667 +f1v8s15 f0v1s1 0.684211 +f1v8s15 f2v8s18 0.666667 +f1v8s15 f2v0s10 0.666667 +f1v8s15 f1v2s4 0.687500 +f1v8s15 f2v5s15 0.684211 +f1v8s15 f1v7s9 0.700000 +f1v8s15 f1v5s7 0.673469 +f1v8s15 f1v0s2 0.666667 +f1v8s15 f2v11s21 0.684211 +f1v8s15 f1v4s6 0.641509 +f1v8s15 f1v1s3 0.666667 +f1v8s15 f2v14s24 0.666667 +f2v1s23 f2v9s19 0.672414 +f2v1s23 f2v12s22 0.672414 +f2v1s23 f0v0s0 0.689655 +f2v1s23 f1v6s8 0.689655 +f2v1s23 f2v13s23 0.672414 +f2v1s23 f2v4s14 0.655172 +f2v1s23 f2v3s13 0.655172 +f2v1s23 f2v2s12 0.672414 +f2v1s23 f2v6s16 0.689655 +f2v1s23 f2v7s17 0.672414 +f2v1s23 f1v3s5 0.708333 +f2v1s23 f2v10s20 0.672414 +f2v1s23 f2v1s11 0.655172 +f2v1s23 f0v1s1 0.689655 +f2v1s23 f2v8s18 0.655172 +f2v1s23 f2v0s10 0.672414 +f2v1s23 f1v2s4 0.708333 +f2v1s23 f2v5s15 0.689655 +f2v1s23 f1v7s9 0.720000 +f2v1s23 f1v5s7 0.693878 +f2v1s23 f1v0s2 0.686275 +f2v1s23 f2v11s21 0.689655 +f2v1s23 f1v4s6 0.660377 +f2v1s23 f1v1s3 0.686275 +f2v1s23 f2v14s24 0.672414 +f4v2s42 f2v9s19 0.655738 +f4v2s42 f2v12s22 0.655738 +f4v2s42 f0v0s0 0.704918 +f4v2s42 f1v6s8 0.704918 +f4v2s42 f2v13s23 0.655738 +f4v2s42 f2v4s14 0.639344 +f4v2s42 f2v3s13 0.639344 +f4v2s42 f2v2s12 0.639344 +f4v2s42 f2v6s16 0.639344 +f4v2s42 f2v7s17 0.655738 +f4v2s42 f1v3s5 0.729167 +f4v2s42 f2v10s20 0.655738 +f4v2s42 f2v1s11 0.639344 +f4v2s42 f0v1s1 0.704918 +f4v2s42 f2v8s18 0.622951 +f4v2s42 f2v0s10 0.655738 +f4v2s42 f1v2s4 0.687500 +f4v2s42 f2v5s15 0.639344 +f4v2s42 f1v7s9 0.660000 +f4v2s42 f1v5s7 0.714286 +f4v2s42 f1v0s2 0.686275 +f4v2s42 f2v11s21 0.639344 +f4v2s42 f1v4s6 0.679245 +f4v2s42 f1v1s3 0.686275 +f4v2s42 f2v14s24 0.672131 +f0v4s4 f2v9s19 0.597015 +f0v4s4 f2v12s22 0.597015 +f0v4s4 f0v0s0 0.647059 +f0v4s4 f1v6s8 0.647059 +f0v4s4 f2v13s23 0.606061 +f0v4s4 f2v4s14 0.619048 +f0v4s4 f2v3s13 0.597015 +f0v4s4 f2v2s12 0.615385 +f0v4s4 f2v6s16 0.615385 +f0v4s4 f2v7s17 0.606061 +f0v4s4 f1v3s5 0.708333 +f0v4s4 f2v10s20 0.606061 +f0v4s4 f2v1s11 0.630769 +f0v4s4 f0v1s1 0.647059 +f0v4s4 f2v8s18 0.609375 +f0v4s4 f2v0s10 0.606061 +f0v4s4 f1v2s4 0.708333 +f0v4s4 f2v5s15 0.609375 +f0v4s4 f1v7s9 0.680000 +f0v4s4 f1v5s7 0.714286 +f0v4s4 f1v0s2 0.686275 +f0v4s4 f2v11s21 0.615385 +f0v4s4 f1v4s6 0.660377 +f0v4s4 f1v1s3 0.686275 +f0v4s4 f2v14s24 0.621212 +f1v6s13 f2v9s19 0.625000 +f1v6s13 f2v12s22 0.625000 +f1v6s13 f0v0s0 0.696429 +f1v6s13 f1v6s8 0.696429 +f1v6s13 f2v13s23 0.625000 +f1v6s13 f2v4s14 0.607143 +f1v6s13 f2v3s13 0.607143 +f1v6s13 f2v2s12 0.625000 +f1v6s13 f2v6s16 0.660714 +f1v6s13 f2v7s17 0.625000 +f1v6s13 f1v3s5 0.708333 +f1v6s13 f2v10s20 0.625000 +f1v6s13 f2v1s11 0.625000 +f1v6s13 f0v1s1 0.696429 +f1v6s13 f2v8s18 0.625000 +f1v6s13 f2v0s10 0.625000 +f1v6s13 f1v2s4 0.708333 +f1v6s13 f2v5s15 0.642857 +f1v6s13 f1v7s9 0.720000 +f1v6s13 f1v5s7 0.673469 +f1v6s13 f1v0s2 0.686275 +f1v6s13 f2v11s21 0.660714 +f1v6s13 f1v4s6 0.660377 +f1v6s13 f1v1s3 0.686275 +f1v6s13 f2v14s24 0.642857 +f3v8s33 f2v9s19 0.590909 +f3v8s33 f2v12s22 0.590909 +f3v8s33 f0v0s0 0.575758 +f3v8s33 f1v6s8 0.575758 +f3v8s33 f2v13s23 0.575758 +f3v8s33 f2v4s14 0.587302 +f3v8s33 f2v3s13 0.575758 +f3v8s33 f2v2s12 0.584615 +f3v8s33 f2v6s16 0.584615 +f3v8s33 f2v7s17 0.575758 +f3v8s33 f1v3s5 0.687500 +f3v8s33 f2v10s20 0.575758 +f3v8s33 f2v1s11 0.584615 +f3v8s33 f0v1s1 0.575758 +f3v8s33 f2v8s18 0.578125 +f3v8s33 f2v0s10 0.575758 +f3v8s33 f1v2s4 0.687500 +f3v8s33 f2v5s15 0.593750 +f3v8s33 f1v7s9 0.680000 +f3v8s33 f1v5s7 0.693878 +f3v8s33 f1v0s2 0.666667 +f3v8s33 f2v11s21 0.584615 +f3v8s33 f1v4s6 0.641509 +f3v8s33 f1v1s3 0.666667 +f3v8s33 f2v14s24 0.590909 +f1v1s8 f2v9s19 0.666667 +f1v1s8 f2v12s22 0.666667 +f1v1s8 f0v0s0 0.666667 +f1v1s8 f1v6s8 0.666667 +f1v1s8 f2v13s23 0.666667 +f1v1s8 f2v4s14 0.650000 +f1v1s8 f2v3s13 0.650000 +f1v1s8 f2v2s12 0.666667 +f1v1s8 f2v6s16 0.683333 +f1v1s8 f2v7s17 0.666667 +f1v1s8 f1v3s5 0.708333 +f1v1s8 f2v10s20 0.666667 +f1v1s8 f2v1s11 0.633333 +f1v1s8 f0v1s1 0.666667 +f1v1s8 f2v8s18 0.650000 +f1v1s8 f2v0s10 0.666667 +f1v1s8 f1v2s4 0.708333 +f1v1s8 f2v5s15 0.683333 +f1v1s8 f1v7s9 0.720000 +f1v1s8 f1v5s7 0.693878 +f1v1s8 f1v0s2 0.686275 +f1v1s8 f2v11s21 0.683333 +f1v1s8 f1v4s6 0.660377 +f1v1s8 f1v1s3 0.686275 +f1v1s8 f2v14s24 0.666667 +f1v14s21 f2v9s19 0.660714 +f1v14s21 f2v12s22 0.660714 +f1v14s21 f0v0s0 0.714286 +f1v14s21 f1v6s8 0.714286 +f1v14s21 f2v13s23 0.660714 +f1v14s21 f2v4s14 0.660714 +f1v14s21 f2v3s13 0.642857 +f1v14s21 f2v2s12 0.660714 +f1v14s21 f2v6s16 0.678571 +f1v14s21 f2v7s17 0.660714 +f1v14s21 f1v3s5 0.666667 +f1v14s21 f2v10s20 0.660714 +f1v14s21 f2v1s11 0.642857 +f1v14s21 f0v1s1 0.714286 +f1v14s21 f2v8s18 0.642857 +f1v14s21 f2v0s10 0.660714 +f1v14s21 f1v2s4 0.687500 +f1v14s21 f2v5s15 0.678571 +f1v14s21 f1v7s9 0.680000 +f1v14s21 f1v5s7 0.673469 +f1v14s21 f1v0s2 0.647059 +f1v14s21 f2v11s21 0.678571 +f1v14s21 f1v4s6 0.622642 +f1v14s21 f1v1s3 0.647059 +f1v14s21 f2v14s24 0.660714 +f0v1s1 f2v9s19 0.597015 +f0v1s1 f2v12s22 0.597015 +f0v1s1 f0v0s0 0.647059 +f0v1s1 f1v6s8 0.647059 +f0v1s1 f2v13s23 0.606061 +f0v1s1 f2v4s14 0.619048 +f0v1s1 f2v3s13 0.597015 +f0v1s1 f2v2s12 0.615385 +f0v1s1 f2v6s16 0.615385 +f0v1s1 f2v7s17 0.606061 +f0v1s1 f1v3s5 0.708333 +f0v1s1 f2v10s20 0.606061 +f0v1s1 f2v1s11 0.630769 +f0v1s1 f0v1s1 0.647059 +f0v1s1 f2v8s18 0.609375 +f0v1s1 f2v0s10 0.606061 +f0v1s1 f1v2s4 0.708333 +f0v1s1 f2v5s15 0.609375 +f0v1s1 f1v7s9 0.680000 +f0v1s1 f1v5s7 0.714286 +f0v1s1 f1v0s2 0.686275 +f0v1s1 f2v11s21 0.615385 +f0v1s1 f1v4s6 0.660377 +f0v1s1 f1v1s3 0.686275 +f0v1s1 f2v14s24 0.621212 +f1v5s12 f2v9s19 0.650000 +f1v5s12 f2v12s22 0.650000 +f1v5s12 f0v0s0 0.666667 +f1v5s12 f1v6s8 0.666667 +f1v5s12 f2v13s23 0.650000 +f1v5s12 f2v4s14 0.650000 +f1v5s12 f2v3s13 0.650000 +f1v5s12 f2v2s12 0.650000 +f1v5s12 f2v6s16 0.633333 +f1v5s12 f2v7s17 0.650000 +f1v5s12 f1v3s5 0.729167 +f1v5s12 f2v10s20 0.650000 +f1v5s12 f2v1s11 0.633333 +f1v5s12 f0v1s1 0.666667 +f1v5s12 f2v8s18 0.616667 +f1v5s12 f2v0s10 0.650000 +f1v5s12 f1v2s4 0.729167 +f1v5s12 f2v5s15 0.650000 +f1v5s12 f1v7s9 0.720000 +f1v5s12 f1v5s7 0.693878 +f1v5s12 f1v0s2 0.705882 +f1v5s12 f2v11s21 0.633333 +f1v5s12 f1v4s6 0.679245 +f1v5s12 f1v1s3 0.705882 +f1v5s12 f2v14s24 0.633333 +f1v4s11 f2v9s19 0.672414 +f1v4s11 f2v12s22 0.672414 +f1v4s11 f0v0s0 0.689655 +f1v4s11 f1v6s8 0.689655 +f1v4s11 f2v13s23 0.672414 +f1v4s11 f2v4s14 0.655172 +f1v4s11 f2v3s13 0.655172 +f1v4s11 f2v2s12 0.672414 +f1v4s11 f2v6s16 0.689655 +f1v4s11 f2v7s17 0.672414 +f1v4s11 f1v3s5 0.708333 +f1v4s11 f2v10s20 0.672414 +f1v4s11 f2v1s11 0.655172 +f1v4s11 f0v1s1 0.689655 +f1v4s11 f2v8s18 0.655172 +f1v4s11 f2v0s10 0.672414 +f1v4s11 f1v2s4 0.708333 +f1v4s11 f2v5s15 0.689655 +f1v4s11 f1v7s9 0.720000 +f1v4s11 f1v5s7 0.693878 +f1v4s11 f1v0s2 0.686275 +f1v4s11 f2v11s21 0.689655 +f1v4s11 f1v4s6 0.660377 +f1v4s11 f1v1s3 0.686275 +f1v4s11 f2v14s24 0.672414 +f1v9s16 f2v9s19 0.666667 +f1v9s16 f2v12s22 0.666667 +f1v9s16 f0v0s0 0.700000 +f1v9s16 f1v6s8 0.700000 +f1v9s16 f2v13s23 0.666667 +f1v9s16 f2v4s14 0.650000 +f1v9s16 f2v3s13 0.650000 +f1v9s16 f2v2s12 0.666667 +f1v9s16 f2v6s16 0.666667 +f1v9s16 f2v7s17 0.666667 +f1v9s16 f1v3s5 0.729167 +f1v9s16 f2v10s20 0.666667 +f1v9s16 f2v1s11 0.650000 +f1v9s16 f0v1s1 0.700000 +f1v9s16 f2v8s18 0.633333 +f1v9s16 f2v0s10 0.666667 +f1v9s16 f1v2s4 0.729167 +f1v9s16 f2v5s15 0.683333 +f1v9s16 f1v7s9 0.740000 +f1v9s16 f1v5s7 0.714286 +f1v9s16 f1v0s2 0.705882 +f1v9s16 f2v11s21 0.666667 +f1v9s16 f1v4s6 0.679245 +f1v9s16 f1v1s3 0.705882 +f1v9s16 f2v14s24 0.666667 +f1v10s17 f2v9s19 0.616667 +f1v10s17 f2v12s22 0.616667 +f1v10s17 f0v0s0 0.700000 +f1v10s17 f1v6s8 0.700000 +f1v10s17 f2v13s23 0.616667 +f1v10s17 f2v4s14 0.600000 +f1v10s17 f2v3s13 0.616667 +f1v10s17 f2v2s12 0.616667 +f1v10s17 f2v6s16 0.633333 +f1v10s17 f2v7s17 0.616667 +f1v10s17 f1v3s5 0.750000 +f1v10s17 f2v10s20 0.616667 +f1v10s17 f2v1s11 0.600000 +f1v10s17 f0v1s1 0.700000 +f1v10s17 f2v8s18 0.600000 +f1v10s17 f2v0s10 0.616667 +f1v10s17 f1v2s4 0.729167 +f1v10s17 f2v5s15 0.633333 +f1v10s17 f1v7s9 0.740000 +f1v10s17 f1v5s7 0.734694 +f1v10s17 f1v0s2 0.725490 +f1v10s17 f2v11s21 0.633333 +f1v10s17 f1v4s6 0.698113 +f1v10s17 f1v1s3 0.725490 +f1v10s17 f2v14s24 0.616667 +f1v0s7 f2v9s19 0.672414 +f1v0s7 f2v12s22 0.672414 +f1v0s7 f0v0s0 0.689655 +f1v0s7 f1v6s8 0.689655 +f1v0s7 f2v13s23 0.672414 +f1v0s7 f2v4s14 0.655172 +f1v0s7 f2v3s13 0.655172 +f1v0s7 f2v2s12 0.672414 +f1v0s7 f2v6s16 0.689655 +f1v0s7 f2v7s17 0.672414 +f1v0s7 f1v3s5 0.708333 +f1v0s7 f2v10s20 0.672414 +f1v0s7 f2v1s11 0.655172 +f1v0s7 f0v1s1 0.689655 +f1v0s7 f2v8s18 0.655172 +f1v0s7 f2v0s10 0.672414 +f1v0s7 f1v2s4 0.708333 +f1v0s7 f2v5s15 0.689655 +f1v0s7 f1v7s9 0.720000 +f1v0s7 f1v5s7 0.693878 +f1v0s7 f1v0s2 0.686275 +f1v0s7 f2v11s21 0.689655 +f1v0s7 f1v4s6 0.660377 +f1v0s7 f1v1s3 0.686275 +f1v0s7 f2v14s24 0.672414 +f3v9s34 f2v9s19 0.600000 +f3v9s34 f2v12s22 0.584615 +f3v9s34 f0v0s0 0.600000 +f3v9s34 f1v6s8 0.600000 +f3v9s34 f2v13s23 0.584615 +f3v9s34 f2v4s14 0.603175 +f3v9s34 f2v3s13 0.584615 +f3v9s34 f2v2s12 0.584615 +f3v9s34 f2v6s16 0.569231 +f3v9s34 f2v7s17 0.584615 +f3v9s34 f1v3s5 0.687500 +f3v9s34 f2v10s20 0.584615 +f3v9s34 f2v1s11 0.584615 +f3v9s34 f0v1s1 0.600000 +f3v9s34 f2v8s18 0.578125 +f3v9s34 f2v0s10 0.584615 +f3v9s34 f1v2s4 0.687500 +f3v9s34 f2v5s15 0.593750 +f3v9s34 f1v7s9 0.680000 +f3v9s34 f1v5s7 0.693878 +f3v9s34 f1v0s2 0.666667 +f3v9s34 f2v11s21 0.569231 +f3v9s34 f1v4s6 0.641509 +f3v9s34 f1v1s3 0.666667 +f3v9s34 f2v14s24 0.600000 +f4v0s40 f2v9s19 0.666667 +f4v0s40 f2v12s22 0.650000 +f4v0s40 f0v0s0 0.700000 +f4v0s40 f1v6s8 0.700000 +f4v0s40 f2v13s23 0.666667 +f4v0s40 f2v4s14 0.633333 +f4v0s40 f2v3s13 0.650000 +f4v0s40 f2v2s12 0.650000 +f4v0s40 f2v6s16 0.650000 +f4v0s40 f2v7s17 0.666667 +f4v0s40 f1v3s5 0.729167 +f4v0s40 f2v10s20 0.666667 +f4v0s40 f2v1s11 0.650000 +f4v0s40 f0v1s1 0.700000 +f4v0s40 f2v8s18 0.633333 +f4v0s40 f2v0s10 0.666667 +f4v0s40 f1v2s4 0.687500 +f4v0s40 f2v5s15 0.633333 +f4v0s40 f1v7s9 0.660000 +f4v0s40 f1v5s7 0.714286 +f4v0s40 f1v0s2 0.686275 +f4v0s40 f2v11s21 0.650000 +f4v0s40 f1v4s6 0.679245 +f4v0s40 f1v1s3 0.686275 +f4v0s40 f2v14s24 0.650000 +f1v7s14 f2v9s19 0.672414 +f1v7s14 f2v12s22 0.672414 +f1v7s14 f0v0s0 0.689655 +f1v7s14 f1v6s8 0.689655 +f1v7s14 f2v13s23 0.672414 +f1v7s14 f2v4s14 0.655172 +f1v7s14 f2v3s13 0.655172 +f1v7s14 f2v2s12 0.672414 +f1v7s14 f2v6s16 0.689655 +f1v7s14 f2v7s17 0.672414 +f1v7s14 f1v3s5 0.708333 +f1v7s14 f2v10s20 0.672414 +f1v7s14 f2v1s11 0.655172 +f1v7s14 f0v1s1 0.689655 +f1v7s14 f2v8s18 0.655172 +f1v7s14 f2v0s10 0.672414 +f1v7s14 f1v2s4 0.708333 +f1v7s14 f2v5s15 0.689655 +f1v7s14 f1v7s9 0.720000 +f1v7s14 f1v5s7 0.693878 +f1v7s14 f1v0s2 0.686275 +f1v7s14 f2v11s21 0.689655 +f1v7s14 f1v4s6 0.660377 +f1v7s14 f1v1s3 0.686275 +f1v7s14 f2v14s24 0.672414 +f3v2s27 f2v9s19 0.600000 +f3v2s27 f2v12s22 0.600000 +f3v2s27 f0v0s0 0.584615 +f3v2s27 f1v6s8 0.584615 +f3v2s27 f2v13s23 0.584615 +f3v2s27 f2v4s14 0.603175 +f3v2s27 f2v3s13 0.584615 +f3v2s27 f2v2s12 0.584615 +f3v2s27 f2v6s16 0.584615 +f3v2s27 f2v7s17 0.584615 +f3v2s27 f1v3s5 0.666667 +f3v2s27 f2v10s20 0.584615 +f3v2s27 f2v1s11 0.584615 +f3v2s27 f0v1s1 0.584615 +f3v2s27 f2v8s18 0.593750 +f3v2s27 f2v0s10 0.584615 +f3v2s27 f1v2s4 0.666667 +f3v2s27 f2v5s15 0.593750 +f3v2s27 f1v7s9 0.680000 +f3v2s27 f1v5s7 0.673469 +f3v2s27 f1v0s2 0.647059 +f3v2s27 f2v11s21 0.584615 +f3v2s27 f1v4s6 0.622642 +f3v2s27 f1v1s3 0.647059 +f3v2s27 f2v14s24 0.600000 +f4v1s41 f2v9s19 0.655738 +f4v1s41 f2v12s22 0.639344 +f4v1s41 f0v0s0 0.688525 +f4v1s41 f1v6s8 0.688525 +f4v1s41 f2v13s23 0.655738 +f4v1s41 f2v4s14 0.639344 +f4v1s41 f2v3s13 0.639344 +f4v1s41 f2v2s12 0.639344 +f4v1s41 f2v6s16 0.655738 +f4v1s41 f2v7s17 0.655738 +f4v1s41 f1v3s5 0.729167 +f4v1s41 f2v10s20 0.655738 +f4v1s41 f2v1s11 0.639344 +f4v1s41 f0v1s1 0.688525 +f4v1s41 f2v8s18 0.639344 +f4v1s41 f2v0s10 0.655738 +f4v1s41 f1v2s4 0.687500 +f4v1s41 f2v5s15 0.639344 +f4v1s41 f1v7s9 0.680000 +f4v1s41 f1v5s7 0.714286 +f4v1s41 f1v0s2 0.686275 +f4v1s41 f2v11s21 0.655738 +f4v1s41 f1v4s6 0.679245 +f4v1s41 f1v1s3 0.686275 +f4v1s41 f2v14s24 0.639344 +f2v0s22 f2v9s19 0.611940 +f2v0s22 f2v12s22 0.611940 +f2v0s22 f0v0s0 0.647059 +f2v0s22 f1v6s8 0.647059 +f2v0s22 f2v13s23 0.621212 +f2v0s22 f2v4s14 0.634921 +f2v0s22 f2v3s13 0.597015 +f2v0s22 f2v2s12 0.615385 +f2v0s22 f2v6s16 0.615385 +f2v0s22 f2v7s17 0.621212 +f2v0s22 f1v3s5 0.687500 +f2v0s22 f2v10s20 0.621212 +f2v0s22 f2v1s11 0.630769 +f2v0s22 f0v1s1 0.647059 +f2v0s22 f2v8s18 0.625000 +f2v0s22 f2v0s10 0.621212 +f2v0s22 f1v2s4 0.687500 +f2v0s22 f2v5s15 0.625000 +f2v0s22 f1v7s9 0.660000 +f2v0s22 f1v5s7 0.693878 +f2v0s22 f1v0s2 0.666667 +f2v0s22 f2v11s21 0.615385 +f2v0s22 f1v4s6 0.641509 +f2v0s22 f1v1s3 0.666667 +f2v0s22 f2v14s24 0.621212 +f1v2s9 f2v9s19 0.597015 +f1v2s9 f2v12s22 0.597015 +f1v2s9 f0v0s0 0.647059 +f1v2s9 f1v6s8 0.647059 +f1v2s9 f2v13s23 0.606061 +f1v2s9 f2v4s14 0.619048 +f1v2s9 f2v3s13 0.597015 +f1v2s9 f2v2s12 0.615385 +f1v2s9 f2v6s16 0.615385 +f1v2s9 f2v7s17 0.606061 +f1v2s9 f1v3s5 0.708333 +f1v2s9 f2v10s20 0.606061 +f1v2s9 f2v1s11 0.630769 +f1v2s9 f0v1s1 0.647059 +f1v2s9 f2v8s18 0.609375 +f1v2s9 f2v0s10 0.606061 +f1v2s9 f1v2s4 0.708333 +f1v2s9 f2v5s15 0.609375 +f1v2s9 f1v7s9 0.680000 +f1v2s9 f1v5s7 0.714286 +f1v2s9 f1v0s2 0.686275 +f1v2s9 f2v11s21 0.615385 +f1v2s9 f1v4s6 0.660377 +f1v2s9 f1v1s3 0.686275 +f1v2s9 f2v14s24 0.621212 +f1v3s10 f2v9s19 0.672414 +f1v3s10 f2v12s22 0.672414 +f1v3s10 f0v0s0 0.689655 +f1v3s10 f1v6s8 0.689655 +f1v3s10 f2v13s23 0.672414 +f1v3s10 f2v4s14 0.655172 +f1v3s10 f2v3s13 0.655172 +f1v3s10 f2v2s12 0.672414 +f1v3s10 f2v6s16 0.689655 +f1v3s10 f2v7s17 0.672414 +f1v3s10 f1v3s5 0.708333 +f1v3s10 f2v10s20 0.672414 +f1v3s10 f2v1s11 0.655172 +f1v3s10 f0v1s1 0.689655 +f1v3s10 f2v8s18 0.655172 +f1v3s10 f2v0s10 0.672414 +f1v3s10 f1v2s4 0.708333 +f1v3s10 f2v5s15 0.689655 +f1v3s10 f1v7s9 0.720000 +f1v3s10 f1v5s7 0.693878 +f1v3s10 f1v0s2 0.686275 +f1v3s10 f2v11s21 0.689655 +f1v3s10 f1v4s6 0.660377 +f1v3s10 f1v1s3 0.686275 +f1v3s10 f2v14s24 0.672414 +f3v0s25 f2v9s19 0.590909 +f3v0s25 f2v12s22 0.590909 +f3v0s25 f0v0s0 0.590909 +f3v0s25 f1v6s8 0.590909 +f3v0s25 f2v13s23 0.575758 +f3v0s25 f2v4s14 0.603175 +f3v0s25 f2v3s13 0.575758 +f3v0s25 f2v2s12 0.584615 +f3v0s25 f2v6s16 0.584615 +f3v0s25 f2v7s17 0.575758 +f3v0s25 f1v3s5 0.687500 +f3v0s25 f2v10s20 0.575758 +f3v0s25 f2v1s11 0.584615 +f3v0s25 f0v1s1 0.590909 +f3v0s25 f2v8s18 0.593750 +f3v0s25 f2v0s10 0.575758 +f3v0s25 f1v2s4 0.687500 +f3v0s25 f2v5s15 0.593750 +f3v0s25 f1v7s9 0.680000 +f3v0s25 f1v5s7 0.693878 +f3v0s25 f1v0s2 0.666667 +f3v0s25 f2v11s21 0.584615 +f3v0s25 f1v4s6 0.641509 +f3v0s25 f1v1s3 0.666667 +f3v0s25 f2v14s24 0.590909 +f3v3s28 f2v9s19 0.575758 +f3v3s28 f2v12s22 0.575758 +f3v3s28 f0v0s0 0.590909 +f3v3s28 f1v6s8 0.590909 +f3v3s28 f2v13s23 0.560606 +f3v3s28 f2v4s14 0.587302 +f3v3s28 f2v3s13 0.560606 +f3v3s28 f2v2s12 0.569231 +f3v3s28 f2v6s16 0.584615 +f3v3s28 f2v7s17 0.560606 +f3v3s28 f1v3s5 0.687500 +f3v3s28 f2v10s20 0.560606 +f3v3s28 f2v1s11 0.584615 +f3v3s28 f0v1s1 0.590909 +f3v3s28 f2v8s18 0.578125 +f3v3s28 f2v0s10 0.560606 +f3v3s28 f1v2s4 0.708333 +f3v3s28 f2v5s15 0.593750 +f3v3s28 f1v7s9 0.680000 +f3v3s28 f1v5s7 0.714286 +f3v3s28 f1v0s2 0.686275 +f3v3s28 f2v11s21 0.584615 +f3v3s28 f1v4s6 0.660377 +f3v3s28 f1v1s3 0.686275 +f3v3s28 f2v14s24 0.590909 +f3v13s38 f2v9s19 0.597015 +f3v13s38 f2v12s22 0.582090 +f3v13s38 f0v0s0 0.573529 +f3v13s38 f1v6s8 0.573529 +f3v13s38 f2v13s23 0.590909 +f3v13s38 f2v4s14 0.603175 +f3v13s38 f2v3s13 0.582090 +f3v13s38 f2v2s12 0.600000 +f3v13s38 f2v6s16 0.600000 +f3v13s38 f2v7s17 0.590909 +f3v13s38 f1v3s5 0.708333 +f3v13s38 f2v10s20 0.590909 +f3v13s38 f2v1s11 0.584615 +f3v13s38 f0v1s1 0.573529 +f3v13s38 f2v8s18 0.593750 +f3v13s38 f2v0s10 0.590909 +f3v13s38 f1v2s4 0.708333 +f3v13s38 f2v5s15 0.593750 +f3v13s38 f1v7s9 0.700000 +f3v13s38 f1v5s7 0.714286 +f3v13s38 f1v0s2 0.686275 +f3v13s38 f2v11s21 0.600000 +f3v13s38 f1v4s6 0.679245 +f3v13s38 f1v1s3 0.686275 +f3v13s38 f2v14s24 0.606061 +f3v7s32 f2v9s19 0.600000 +f3v7s32 f2v12s22 0.584615 +f3v7s32 f0v0s0 0.600000 +f3v7s32 f1v6s8 0.600000 +f3v7s32 f2v13s23 0.584615 +f3v7s32 f2v4s14 0.603175 +f3v7s32 f2v3s13 0.584615 +f3v7s32 f2v2s12 0.584615 +f3v7s32 f2v6s16 0.569231 +f3v7s32 f2v7s17 0.584615 +f3v7s32 f1v3s5 0.687500 +f3v7s32 f2v10s20 0.584615 +f3v7s32 f2v1s11 0.584615 +f3v7s32 f0v1s1 0.600000 +f3v7s32 f2v8s18 0.578125 +f3v7s32 f2v0s10 0.584615 +f3v7s32 f1v2s4 0.687500 +f3v7s32 f2v5s15 0.593750 +f3v7s32 f1v7s9 0.680000 +f3v7s32 f1v5s7 0.693878 +f3v7s32 f1v0s2 0.666667 +f3v7s32 f2v11s21 0.569231 +f3v7s32 f1v4s6 0.641509 +f3v7s32 f1v1s3 0.666667 +f3v7s32 f2v14s24 0.600000 +f3v1s26 f2v9s19 0.584615 +f3v1s26 f2v12s22 0.584615 +f3v1s26 f0v0s0 0.584615 +f3v1s26 f1v6s8 0.584615 +f3v1s26 f2v13s23 0.569231 +f3v1s26 f2v4s14 0.587302 +f3v1s26 f2v3s13 0.569231 +f3v1s26 f2v2s12 0.569231 +f3v1s26 f2v6s16 0.584615 +f3v1s26 f2v7s17 0.569231 +f3v1s26 f1v3s5 0.645833 +f3v1s26 f2v10s20 0.569231 +f3v1s26 f2v1s11 0.584615 +f3v1s26 f0v1s1 0.584615 +f3v1s26 f2v8s18 0.578125 +f3v1s26 f2v0s10 0.569231 +f3v1s26 f1v2s4 0.645833 +f3v1s26 f2v5s15 0.578125 +f3v1s26 f1v7s9 0.640000 +f3v1s26 f1v5s7 0.653061 +f3v1s26 f1v0s2 0.627451 +f3v1s26 f2v11s21 0.584615 +f3v1s26 f1v4s6 0.622642 +f3v1s26 f1v1s3 0.627451 +f3v1s26 f2v14s24 0.600000 +f3v6s31 f2v9s19 0.600000 +f3v6s31 f2v12s22 0.584615 +f3v6s31 f0v0s0 0.600000 +f3v6s31 f1v6s8 0.600000 +f3v6s31 f2v13s23 0.584615 +f3v6s31 f2v4s14 0.603175 +f3v6s31 f2v3s13 0.584615 +f3v6s31 f2v2s12 0.584615 +f3v6s31 f2v6s16 0.569231 +f3v6s31 f2v7s17 0.584615 +f3v6s31 f1v3s5 0.687500 +f3v6s31 f2v10s20 0.584615 +f3v6s31 f2v1s11 0.584615 +f3v6s31 f0v1s1 0.600000 +f3v6s31 f2v8s18 0.578125 +f3v6s31 f2v0s10 0.584615 +f3v6s31 f1v2s4 0.687500 +f3v6s31 f2v5s15 0.593750 +f3v6s31 f1v7s9 0.680000 +f3v6s31 f1v5s7 0.693878 +f3v6s31 f1v0s2 0.666667 +f3v6s31 f2v11s21 0.569231 +f3v6s31 f1v4s6 0.641509 +f3v6s31 f1v1s3 0.666667 +f3v6s31 f2v14s24 0.600000 +f1v13s20 f2v9s19 0.678571 +f1v13s20 f2v12s22 0.678571 +f1v13s20 f0v0s0 0.696429 +f1v13s20 f1v6s8 0.696429 +f1v13s20 f2v13s23 0.678571 +f1v13s20 f2v4s14 0.660714 +f1v13s20 f2v3s13 0.660714 +f1v13s20 f2v2s12 0.678571 +f1v13s20 f2v6s16 0.714286 +f1v13s20 f2v7s17 0.678571 +f1v13s20 f1v3s5 0.708333 +f1v13s20 f2v10s20 0.678571 +f1v13s20 f2v1s11 0.660714 +f1v13s20 f0v1s1 0.696429 +f1v13s20 f2v8s18 0.660714 +f1v13s20 f2v0s10 0.678571 +f1v13s20 f1v2s4 0.708333 +f1v13s20 f2v5s15 0.696429 +f1v13s20 f1v7s9 0.720000 +f1v13s20 f1v5s7 0.693878 +f1v13s20 f1v0s2 0.686275 +f1v13s20 f2v11s21 0.714286 +f1v13s20 f1v4s6 0.660377 +f1v13s20 f1v1s3 0.686275 +f1v13s20 f2v14s24 0.678571 +f2v2s24 f2v9s19 0.734694 +f2v2s24 f2v12s22 0.755102 +f2v2s24 f0v0s0 0.755102 +f2v2s24 f1v6s8 0.755102 +f2v2s24 f2v13s23 0.734694 +f2v2s24 f2v4s14 0.714286 +f2v2s24 f2v3s13 0.755102 +f2v2s24 f2v2s12 0.734694 +f2v2s24 f2v6s16 0.755102 +f2v2s24 f2v7s17 0.734694 +f2v2s24 f1v3s5 0.604167 +f2v2s24 f2v10s20 0.734694 +f2v2s24 f2v1s11 0.755102 +f2v2s24 f0v1s1 0.755102 +f2v2s24 f2v8s18 0.714286 +f2v2s24 f2v0s10 0.734694 +f2v2s24 f1v2s4 0.583333 +f2v2s24 f2v5s15 0.755102 +f2v2s24 f1v7s9 0.571429 +f2v2s24 f1v5s7 0.612245 +f2v2s24 f1v0s2 0.612245 +f2v2s24 f2v11s21 0.755102 +f2v2s24 f1v4s6 0.591837 +f2v2s24 f1v1s3 0.612245 +f2v2s24 f2v14s24 0.734694 +f3v11s36 f2v9s19 0.600000 +f3v11s36 f2v12s22 0.584615 +f3v11s36 f0v0s0 0.600000 +f3v11s36 f1v6s8 0.600000 +f3v11s36 f2v13s23 0.584615 +f3v11s36 f2v4s14 0.603175 +f3v11s36 f2v3s13 0.584615 +f3v11s36 f2v2s12 0.584615 +f3v11s36 f2v6s16 0.569231 +f3v11s36 f2v7s17 0.584615 +f3v11s36 f1v3s5 0.645833 +f3v11s36 f2v10s20 0.584615 +f3v11s36 f2v1s11 0.584615 +f3v11s36 f0v1s1 0.600000 +f3v11s36 f2v8s18 0.578125 +f3v11s36 f2v0s10 0.584615 +f3v11s36 f1v2s4 0.666667 +f3v11s36 f2v5s15 0.593750 +f3v11s36 f1v7s9 0.660000 +f3v11s36 f1v5s7 0.653061 +f3v11s36 f1v0s2 0.627451 +f3v11s36 f2v11s21 0.569231 +f3v11s36 f1v4s6 0.641509 +f3v11s36 f1v1s3 0.627451 +f3v11s36 f2v14s24 0.569231 +f0v5s5 f2v9s19 0.597015 +f0v5s5 f2v12s22 0.597015 +f0v5s5 f0v0s0 0.632353 +f0v5s5 f1v6s8 0.632353 +f0v5s5 f2v13s23 0.606061 +f0v5s5 f2v4s14 0.619048 +f0v5s5 f2v3s13 0.597015 +f0v5s5 f2v2s12 0.600000 +f0v5s5 f2v6s16 0.600000 +f0v5s5 f2v7s17 0.606061 +f0v5s5 f1v3s5 0.687500 +f0v5s5 f2v10s20 0.606061 +f0v5s5 f2v1s11 0.615385 +f0v5s5 f0v1s1 0.632353 +f0v5s5 f2v8s18 0.609375 +f0v5s5 f2v0s10 0.606061 +f0v5s5 f1v2s4 0.708333 +f0v5s5 f2v5s15 0.609375 +f0v5s5 f1v7s9 0.680000 +f0v5s5 f1v5s7 0.714286 +f0v5s5 f1v0s2 0.686275 +f0v5s5 f2v11s21 0.600000 +f0v5s5 f1v4s6 0.660377 +f0v5s5 f1v1s3 0.686275 +f0v5s5 f2v14s24 0.621212 +f1v11s18 f2v9s19 0.672414 +f1v11s18 f2v12s22 0.672414 +f1v11s18 f0v0s0 0.706897 +f1v11s18 f1v6s8 0.706897 +f1v11s18 f2v13s23 0.672414 +f1v11s18 f2v4s14 0.655172 +f1v11s18 f2v3s13 0.655172 +f1v11s18 f2v2s12 0.672414 +f1v11s18 f2v6s16 0.689655 +f1v11s18 f2v7s17 0.672414 +f1v11s18 f1v3s5 0.708333 +f1v11s18 f2v10s20 0.672414 +f1v11s18 f2v1s11 0.655172 +f1v11s18 f0v1s1 0.706897 +f1v11s18 f2v8s18 0.655172 +f1v11s18 f2v0s10 0.672414 +f1v11s18 f1v2s4 0.708333 +f1v11s18 f2v5s15 0.689655 +f1v11s18 f1v7s9 0.720000 +f1v11s18 f1v5s7 0.693878 +f1v11s18 f1v0s2 0.686275 +f1v11s18 f2v11s21 0.689655 +f1v11s18 f1v4s6 0.660377 +f1v11s18 f1v1s3 0.686275 +f1v11s18 f2v14s24 0.672414 +f0v6s6 f2v9s19 0.597015 +f0v6s6 f2v12s22 0.597015 +f0v6s6 f0v0s0 0.632353 +f0v6s6 f1v6s8 0.632353 +f0v6s6 f2v13s23 0.606061 +f0v6s6 f2v4s14 0.619048 +f0v6s6 f2v3s13 0.597015 +f0v6s6 f2v2s12 0.615385 +f0v6s6 f2v6s16 0.615385 +f0v6s6 f2v7s17 0.606061 +f0v6s6 f1v3s5 0.687500 +f0v6s6 f2v10s20 0.606061 +f0v6s6 f2v1s11 0.630769 +f0v6s6 f0v1s1 0.632353 +f0v6s6 f2v8s18 0.609375 +f0v6s6 f2v0s10 0.606061 +f0v6s6 f1v2s4 0.666667 +f0v6s6 f2v5s15 0.609375 +f0v6s6 f1v7s9 0.660000 +f0v6s6 f1v5s7 0.673469 +f0v6s6 f1v0s2 0.647059 +f0v6s6 f2v11s21 0.615385 +f0v6s6 f1v4s6 0.641509 +f0v6s6 f1v1s3 0.647059 +f0v6s6 f2v14s24 0.636364 +f3v4s29 f2v9s19 0.582090 +f3v4s29 f2v12s22 0.597015 +f3v4s29 f0v0s0 0.588235 +f3v4s29 f1v6s8 0.588235 +f3v4s29 f2v13s23 0.590909 +f3v4s29 f2v4s14 0.619048 +f3v4s29 f2v3s13 0.582090 +f3v4s29 f2v2s12 0.600000 +f3v4s29 f2v6s16 0.600000 +f3v4s29 f2v7s17 0.590909 +f3v4s29 f1v3s5 0.687500 +f3v4s29 f2v10s20 0.590909 +f3v4s29 f2v1s11 0.584615 +f3v4s29 f0v1s1 0.588235 +f3v4s29 f2v8s18 0.609375 +f3v4s29 f2v0s10 0.590909 +f3v4s29 f1v2s4 0.687500 +f3v4s29 f2v5s15 0.593750 +f3v4s29 f1v7s9 0.680000 +f3v4s29 f1v5s7 0.693878 +f3v4s29 f1v0s2 0.666667 +f3v4s29 f2v11s21 0.600000 +f3v4s29 f1v4s6 0.641509 +f3v4s29 f1v1s3 0.666667 +f3v4s29 f2v14s24 0.590909 +f0v2s2 f2v9s19 0.611940 +f0v2s2 f2v12s22 0.611940 +f0v2s2 f0v0s0 0.647059 +f0v2s2 f1v6s8 0.647059 +f0v2s2 f2v13s23 0.621212 +f0v2s2 f2v4s14 0.634921 +f0v2s2 f2v3s13 0.597015 +f0v2s2 f2v2s12 0.615385 +f0v2s2 f2v6s16 0.615385 +f0v2s2 f2v7s17 0.621212 +f0v2s2 f1v3s5 0.687500 +f0v2s2 f2v10s20 0.621212 +f0v2s2 f2v1s11 0.630769 +f0v2s2 f0v1s1 0.647059 +f0v2s2 f2v8s18 0.625000 +f0v2s2 f2v0s10 0.621212 +f0v2s2 f1v2s4 0.687500 +f0v2s2 f2v5s15 0.625000 +f0v2s2 f1v7s9 0.660000 +f0v2s2 f1v5s7 0.693878 +f0v2s2 f1v0s2 0.666667 +f0v2s2 f2v11s21 0.615385 +f0v2s2 f1v4s6 0.641509 +f0v2s2 f1v1s3 0.666667 +f0v2s2 f2v14s24 0.621212 +f3v12s37 f2v9s19 0.611940 +f3v12s37 f2v12s22 0.597015 +f3v12s37 f0v0s0 0.597015 +f3v12s37 f1v6s8 0.597015 +f3v12s37 f2v13s23 0.606061 +f3v12s37 f2v4s14 0.634921 +f3v12s37 f2v3s13 0.597015 +f3v12s37 f2v2s12 0.615385 +f3v12s37 f2v6s16 0.600000 +f3v12s37 f2v7s17 0.606061 +f3v12s37 f1v3s5 0.687500 +f3v12s37 f2v10s20 0.606061 +f3v12s37 f2v1s11 0.600000 +f3v12s37 f0v1s1 0.597015 +f3v12s37 f2v8s18 0.625000 +f3v12s37 f2v0s10 0.606061 +f3v12s37 f1v2s4 0.708333 +f3v12s37 f2v5s15 0.609375 +f3v12s37 f1v7s9 0.700000 +f3v12s37 f1v5s7 0.714286 +f3v12s37 f1v0s2 0.686275 +f3v12s37 f2v11s21 0.600000 +f3v12s37 f1v4s6 0.660377 +f3v12s37 f1v1s3 0.686275 +f3v12s37 f2v14s24 0.606061 +f0v3s3 f2v9s19 0.597015 +f0v3s3 f2v12s22 0.597015 +f0v3s3 f0v0s0 0.626866 +f0v3s3 f1v6s8 0.626866 +f0v3s3 f2v13s23 0.606061 +f0v3s3 f2v4s14 0.619048 +f0v3s3 f2v3s13 0.582090 +f0v3s3 f2v2s12 0.600000 +f0v3s3 f2v6s16 0.600000 +f0v3s3 f2v7s17 0.606061 +f0v3s3 f1v3s5 0.708333 +f0v3s3 f2v10s20 0.606061 +f0v3s3 f2v1s11 0.615385 +f0v3s3 f0v1s1 0.626866 +f0v3s3 f2v8s18 0.609375 +f0v3s3 f2v0s10 0.606061 +f0v3s3 f1v2s4 0.708333 +f0v3s3 f2v5s15 0.609375 +f0v3s3 f1v7s9 0.680000 +f0v3s3 f1v5s7 0.693878 +f0v3s3 f1v0s2 0.666667 +f0v3s3 f2v11s21 0.600000 +f0v3s3 f1v4s6 0.641509 +f0v3s3 f1v1s3 0.666667 +f0v3s3 f2v14s24 0.606061 +f3v14s39 f2v9s19 0.590909 +f3v14s39 f2v12s22 0.590909 +f3v14s39 f0v0s0 0.590909 +f3v14s39 f1v6s8 0.590909 +f3v14s39 f2v13s23 0.575758 +f3v14s39 f2v4s14 0.603175 +f3v14s39 f2v3s13 0.575758 +f3v14s39 f2v2s12 0.584615 +f3v14s39 f2v6s16 0.584615 +f3v14s39 f2v7s17 0.575758 +f3v14s39 f1v3s5 0.687500 +f3v14s39 f2v10s20 0.575758 +f3v14s39 f2v1s11 0.584615 +f3v14s39 f0v1s1 0.590909 +f3v14s39 f2v8s18 0.593750 +f3v14s39 f2v0s10 0.575758 +f3v14s39 f1v2s4 0.687500 +f3v14s39 f2v5s15 0.593750 +f3v14s39 f1v7s9 0.680000 +f3v14s39 f1v5s7 0.693878 +f3v14s39 f1v0s2 0.666667 +f3v14s39 f2v11s21 0.584615 +f3v14s39 f1v4s6 0.641509 +f3v14s39 f1v1s3 0.666667 +f3v14s39 f2v14s24 0.590909 +f0v0s0 f2v9s19 0.597015 +f0v0s0 f2v12s22 0.597015 +f0v0s0 f0v0s0 0.647059 +f0v0s0 f1v6s8 0.647059 +f0v0s0 f2v13s23 0.606061 +f0v0s0 f2v4s14 0.619048 +f0v0s0 f2v3s13 0.582090 +f0v0s0 f2v2s12 0.615385 +f0v0s0 f2v6s16 0.615385 +f0v0s0 f2v7s17 0.606061 +f0v0s0 f1v3s5 0.708333 +f0v0s0 f2v10s20 0.606061 +f0v0s0 f2v1s11 0.630769 +f0v0s0 f0v1s1 0.647059 +f0v0s0 f2v8s18 0.609375 +f0v0s0 f2v0s10 0.606061 +f0v0s0 f1v2s4 0.708333 +f0v0s0 f2v5s15 0.609375 +f0v0s0 f1v7s9 0.680000 +f0v0s0 f1v5s7 0.714286 +f0v0s0 f1v0s2 0.686275 +f0v0s0 f2v11s21 0.615385 +f0v0s0 f1v4s6 0.660377 +f0v0s0 f1v1s3 0.686275 +f0v0s0 f2v14s24 0.621212 +f1v12s19 f2v9s19 0.666667 +f1v12s19 f2v12s22 0.666667 +f1v12s19 f0v0s0 0.701754 +f1v12s19 f1v6s8 0.701754 +f1v12s19 f2v13s23 0.666667 +f1v12s19 f2v4s14 0.649123 +f1v12s19 f2v3s13 0.649123 +f1v12s19 f2v2s12 0.666667 +f1v12s19 f2v6s16 0.684211 +f1v12s19 f2v7s17 0.666667 +f1v12s19 f1v3s5 0.687500 +f1v12s19 f2v10s20 0.666667 +f1v12s19 f2v1s11 0.649123 +f1v12s19 f0v1s1 0.701754 +f1v12s19 f2v8s18 0.649123 +f1v12s19 f2v0s10 0.666667 +f1v12s19 f1v2s4 0.687500 +f1v12s19 f2v5s15 0.684211 +f1v12s19 f1v7s9 0.700000 +f1v12s19 f1v5s7 0.673469 +f1v12s19 f1v0s2 0.666667 +f1v12s19 f2v11s21 0.684211 +f1v12s19 f1v4s6 0.641509 +f1v12s19 f1v1s3 0.666667 +f1v12s19 f2v14s24 0.666667 diff --git a/pkg/obitools/obialign/testdata/two_default.expected b/pkg/obitools/obialign/testdata/two_default.expected new file mode 100644 index 0000000..e69de29 diff --git a/pkg/obitools/obialign/testdata/two_distance.expected b/pkg/obitools/obialign/testdata/two_distance.expected new file mode 100644 index 0000000..e69de29 diff --git a/pkg/obitools/obialign/testdata/two_longest.expected b/pkg/obitools/obialign/testdata/two_longest.expected new file mode 100644 index 0000000..e69de29 diff --git a/pkg/obitools/obialign/testdata/two_low.expected b/pkg/obitools/obialign/testdata/two_low.expected new file mode 100644 index 0000000..e69de29 diff --git a/pkg/obitools/obialign/testdata/two_rawdist.expected b/pkg/obitools/obialign/testdata/two_rawdist.expected new file mode 100644 index 0000000..e69de29 diff --git a/pkg/obitools/obialign/testdata/two_rawlcs.expected b/pkg/obitools/obialign/testdata/two_rawlcs.expected new file mode 100644 index 0000000..76ec3d5 --- /dev/null +++ b/pkg/obitools/obialign/testdata/two_rawlcs.expected @@ -0,0 +1,265 @@ +f3v10s35 f2v9s19 40.000000 +f3v10s35 f2v12s22 40.000000 +f3v10s35 f2v6s16 40.000000 +f3v10s35 f2v1s11 40.000000 +f3v10s35 f2v11s21 40.000000 +f3v10s35 f2v14s24 40.000000 +f2v1s23 f0v0s0 40.000000 +f2v1s23 f1v6s8 40.000000 +f2v1s23 f2v6s16 40.000000 +f2v1s23 f0v1s1 40.000000 +f2v1s23 f2v5s15 40.000000 +f2v1s23 f2v11s21 40.000000 +f4v2s42 f2v9s19 40.000000 +f4v2s42 f2v12s22 40.000000 +f4v2s42 f0v0s0 43.000000 +f4v2s42 f1v6s8 43.000000 +f4v2s42 f2v13s23 40.000000 +f4v2s42 f2v7s17 40.000000 +f4v2s42 f2v10s20 40.000000 +f4v2s42 f0v1s1 43.000000 +f4v2s42 f2v0s10 40.000000 +f4v2s42 f2v14s24 41.000000 +f0v4s4 f2v9s19 40.000000 +f0v4s4 f2v12s22 40.000000 +f0v4s4 f0v0s0 44.000000 +f0v4s4 f1v6s8 44.000000 +f0v4s4 f2v13s23 40.000000 +f0v4s4 f2v3s13 40.000000 +f0v4s4 f2v2s12 40.000000 +f0v4s4 f2v6s16 40.000000 +f0v4s4 f2v7s17 40.000000 +f0v4s4 f2v10s20 40.000000 +f0v4s4 f2v1s11 41.000000 +f0v4s4 f0v1s1 44.000000 +f0v4s4 f2v0s10 40.000000 +f0v4s4 f2v11s21 40.000000 +f0v4s4 f2v14s24 41.000000 +f1v1s8 f2v9s19 40.000000 +f1v1s8 f2v12s22 40.000000 +f1v1s8 f0v0s0 40.000000 +f1v1s8 f1v6s8 40.000000 +f1v1s8 f2v13s23 40.000000 +f1v1s8 f2v2s12 40.000000 +f1v1s8 f2v6s16 41.000000 +f1v1s8 f2v7s17 40.000000 +f1v1s8 f2v10s20 40.000000 +f1v1s8 f0v1s1 40.000000 +f1v1s8 f2v0s10 40.000000 +f1v1s8 f2v5s15 41.000000 +f1v1s8 f2v11s21 41.000000 +f1v1s8 f2v14s24 40.000000 +f1v14s21 f0v0s0 40.000000 +f1v14s21 f1v6s8 40.000000 +f1v14s21 f0v1s1 40.000000 +f0v1s1 f2v9s19 40.000000 +f0v1s1 f2v12s22 40.000000 +f0v1s1 f0v0s0 44.000000 +f0v1s1 f1v6s8 44.000000 +f0v1s1 f2v13s23 40.000000 +f0v1s1 f2v3s13 40.000000 +f0v1s1 f2v2s12 40.000000 +f0v1s1 f2v6s16 40.000000 +f0v1s1 f2v7s17 40.000000 +f0v1s1 f2v10s20 40.000000 +f0v1s1 f2v1s11 41.000000 +f0v1s1 f0v1s1 44.000000 +f0v1s1 f2v0s10 40.000000 +f0v1s1 f2v11s21 40.000000 +f0v1s1 f2v14s24 41.000000 +f1v5s12 f0v0s0 40.000000 +f1v5s12 f1v6s8 40.000000 +f1v5s12 f0v1s1 40.000000 +f1v4s11 f0v0s0 40.000000 +f1v4s11 f1v6s8 40.000000 +f1v4s11 f2v6s16 40.000000 +f1v4s11 f0v1s1 40.000000 +f1v4s11 f2v5s15 40.000000 +f1v4s11 f2v11s21 40.000000 +f1v9s16 f2v9s19 40.000000 +f1v9s16 f2v12s22 40.000000 +f1v9s16 f0v0s0 42.000000 +f1v9s16 f1v6s8 42.000000 +f1v9s16 f2v13s23 40.000000 +f1v9s16 f2v2s12 40.000000 +f1v9s16 f2v6s16 40.000000 +f1v9s16 f2v7s17 40.000000 +f1v9s16 f2v10s20 40.000000 +f1v9s16 f0v1s1 42.000000 +f1v9s16 f2v0s10 40.000000 +f1v9s16 f2v5s15 41.000000 +f1v9s16 f2v11s21 40.000000 +f1v9s16 f2v14s24 40.000000 +f1v10s17 f0v0s0 42.000000 +f1v10s17 f1v6s8 42.000000 +f1v10s17 f0v1s1 42.000000 +f1v0s7 f0v0s0 40.000000 +f1v0s7 f1v6s8 40.000000 +f1v0s7 f2v6s16 40.000000 +f1v0s7 f0v1s1 40.000000 +f1v0s7 f2v5s15 40.000000 +f1v0s7 f2v11s21 40.000000 +f4v0s40 f2v9s19 40.000000 +f4v0s40 f0v0s0 42.000000 +f4v0s40 f1v6s8 42.000000 +f4v0s40 f2v13s23 40.000000 +f4v0s40 f2v7s17 40.000000 +f4v0s40 f2v10s20 40.000000 +f4v0s40 f0v1s1 42.000000 +f4v0s40 f2v0s10 40.000000 +f1v7s14 f0v0s0 40.000000 +f1v7s14 f1v6s8 40.000000 +f1v7s14 f2v6s16 40.000000 +f1v7s14 f0v1s1 40.000000 +f1v7s14 f2v5s15 40.000000 +f1v7s14 f2v11s21 40.000000 +f4v1s41 f2v9s19 40.000000 +f4v1s41 f0v0s0 42.000000 +f4v1s41 f1v6s8 42.000000 +f4v1s41 f2v13s23 40.000000 +f4v1s41 f2v6s16 40.000000 +f4v1s41 f2v7s17 40.000000 +f4v1s41 f2v10s20 40.000000 +f4v1s41 f0v1s1 42.000000 +f4v1s41 f2v0s10 40.000000 +f4v1s41 f2v11s21 40.000000 +f2v0s22 f2v9s19 41.000000 +f2v0s22 f2v12s22 41.000000 +f2v0s22 f0v0s0 44.000000 +f2v0s22 f1v6s8 44.000000 +f2v0s22 f2v13s23 41.000000 +f2v0s22 f2v4s14 40.000000 +f2v0s22 f2v3s13 40.000000 +f2v0s22 f2v2s12 40.000000 +f2v0s22 f2v6s16 40.000000 +f2v0s22 f2v7s17 41.000000 +f2v0s22 f2v10s20 41.000000 +f2v0s22 f2v1s11 41.000000 +f2v0s22 f0v1s1 44.000000 +f2v0s22 f2v8s18 40.000000 +f2v0s22 f2v0s10 41.000000 +f2v0s22 f2v5s15 40.000000 +f2v0s22 f2v11s21 40.000000 +f2v0s22 f2v14s24 41.000000 +f1v2s9 f2v9s19 40.000000 +f1v2s9 f2v12s22 40.000000 +f1v2s9 f0v0s0 44.000000 +f1v2s9 f1v6s8 44.000000 +f1v2s9 f2v13s23 40.000000 +f1v2s9 f2v3s13 40.000000 +f1v2s9 f2v2s12 40.000000 +f1v2s9 f2v6s16 40.000000 +f1v2s9 f2v7s17 40.000000 +f1v2s9 f2v10s20 40.000000 +f1v2s9 f2v1s11 41.000000 +f1v2s9 f0v1s1 44.000000 +f1v2s9 f2v0s10 40.000000 +f1v2s9 f2v11s21 40.000000 +f1v2s9 f2v14s24 41.000000 +f1v3s10 f0v0s0 40.000000 +f1v3s10 f1v6s8 40.000000 +f1v3s10 f2v6s16 40.000000 +f1v3s10 f0v1s1 40.000000 +f1v3s10 f2v5s15 40.000000 +f1v3s10 f2v11s21 40.000000 +f3v13s38 f2v9s19 40.000000 +f3v13s38 f2v14s24 40.000000 +f1v13s20 f2v6s16 40.000000 +f1v13s20 f2v11s21 40.000000 +f0v5s5 f2v9s19 40.000000 +f0v5s5 f2v12s22 40.000000 +f0v5s5 f0v0s0 43.000000 +f0v5s5 f1v6s8 43.000000 +f0v5s5 f2v13s23 40.000000 +f0v5s5 f2v3s13 40.000000 +f0v5s5 f2v7s17 40.000000 +f0v5s5 f2v10s20 40.000000 +f0v5s5 f2v1s11 40.000000 +f0v5s5 f0v1s1 43.000000 +f0v5s5 f2v0s10 40.000000 +f0v5s5 f2v14s24 41.000000 +f1v11s18 f0v0s0 41.000000 +f1v11s18 f1v6s8 41.000000 +f1v11s18 f2v6s16 40.000000 +f1v11s18 f0v1s1 41.000000 +f1v11s18 f2v5s15 40.000000 +f1v11s18 f2v11s21 40.000000 +f0v6s6 f2v9s19 40.000000 +f0v6s6 f2v12s22 40.000000 +f0v6s6 f0v0s0 43.000000 +f0v6s6 f1v6s8 43.000000 +f0v6s6 f2v13s23 40.000000 +f0v6s6 f2v3s13 40.000000 +f0v6s6 f2v2s12 40.000000 +f0v6s6 f2v6s16 40.000000 +f0v6s6 f2v7s17 40.000000 +f0v6s6 f2v10s20 40.000000 +f0v6s6 f2v1s11 41.000000 +f0v6s6 f0v1s1 43.000000 +f0v6s6 f2v0s10 40.000000 +f0v6s6 f2v11s21 40.000000 +f0v6s6 f2v14s24 42.000000 +f3v4s29 f2v12s22 40.000000 +f3v4s29 f0v0s0 40.000000 +f3v4s29 f1v6s8 40.000000 +f3v4s29 f0v1s1 40.000000 +f0v2s2 f2v9s19 41.000000 +f0v2s2 f2v12s22 41.000000 +f0v2s2 f0v0s0 44.000000 +f0v2s2 f1v6s8 44.000000 +f0v2s2 f2v13s23 41.000000 +f0v2s2 f2v4s14 40.000000 +f0v2s2 f2v3s13 40.000000 +f0v2s2 f2v2s12 40.000000 +f0v2s2 f2v6s16 40.000000 +f0v2s2 f2v7s17 41.000000 +f0v2s2 f2v10s20 41.000000 +f0v2s2 f2v1s11 41.000000 +f0v2s2 f0v1s1 44.000000 +f0v2s2 f2v8s18 40.000000 +f0v2s2 f2v0s10 41.000000 +f0v2s2 f2v5s15 40.000000 +f0v2s2 f2v11s21 40.000000 +f0v2s2 f2v14s24 41.000000 +f3v12s37 f2v9s19 41.000000 +f3v12s37 f2v12s22 40.000000 +f3v12s37 f0v0s0 40.000000 +f3v12s37 f1v6s8 40.000000 +f3v12s37 f2v13s23 40.000000 +f3v12s37 f2v4s14 40.000000 +f3v12s37 f2v3s13 40.000000 +f3v12s37 f2v2s12 40.000000 +f3v12s37 f2v7s17 40.000000 +f3v12s37 f2v10s20 40.000000 +f3v12s37 f0v1s1 40.000000 +f3v12s37 f2v8s18 40.000000 +f3v12s37 f2v0s10 40.000000 +f3v12s37 f2v14s24 40.000000 +f0v3s3 f2v9s19 40.000000 +f0v3s3 f2v12s22 40.000000 +f0v3s3 f0v0s0 42.000000 +f0v3s3 f1v6s8 42.000000 +f0v3s3 f2v13s23 40.000000 +f0v3s3 f2v7s17 40.000000 +f0v3s3 f2v10s20 40.000000 +f0v3s3 f2v1s11 40.000000 +f0v3s3 f0v1s1 42.000000 +f0v3s3 f2v0s10 40.000000 +f0v3s3 f2v14s24 40.000000 +f0v0s0 f2v9s19 40.000000 +f0v0s0 f2v12s22 40.000000 +f0v0s0 f0v0s0 44.000000 +f0v0s0 f1v6s8 44.000000 +f0v0s0 f2v13s23 40.000000 +f0v0s0 f2v2s12 40.000000 +f0v0s0 f2v6s16 40.000000 +f0v0s0 f2v7s17 40.000000 +f0v0s0 f2v10s20 40.000000 +f0v0s0 f2v1s11 41.000000 +f0v0s0 f0v1s1 44.000000 +f0v0s0 f2v0s10 40.000000 +f0v0s0 f2v11s21 40.000000 +f0v0s0 f2v14s24 41.000000 +f1v12s19 f0v0s0 40.000000 +f1v12s19 f1v6s8 40.000000 +f1v12s19 f0v1s1 40.000000 diff --git a/pkg/obitools/obialign/testdata/two_rawlcs_shortest.expected b/pkg/obitools/obialign/testdata/two_rawlcs_shortest.expected new file mode 100644 index 0000000..e69de29 diff --git a/pkg/obitools/obialign/testdata/two_shortest.expected b/pkg/obitools/obialign/testdata/two_shortest.expected new file mode 100644 index 0000000..e69de29 diff --git a/pkg/obitools/obiclust/centers.go b/pkg/obitools/obiclust/centers.go deleted file mode 100644 index c88cf83..0000000 --- a/pkg/obitools/obiclust/centers.go +++ /dev/null @@ -1,136 +0,0 @@ -package obiclust - -import ( - "math" - "sort" - - "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obialign" -) - -// center is a copy of the entry of a center, with its own table: scanning -// the centers of a list then reads contiguous memory. -type center struct { - entry - idx int // index of the entry in the dataset - rank int // rank of the center in the order of creation -} - -const tabChunk = 1024 - -// A bucket holds the centers of one length, in the order of creation, and -// their tables by blocks (see tableBlock). -type bucket struct { - cents []center - blocks []tableBlock -} - -// centerSet stores the centers by length. A sequence can only be attached to a -// center whose length is compatible with its own (see lengthCompatible), so -// it only visits the buckets of these lengths, instead of all the centers. -type centerSet struct { - width int // centers per block of transposed tables, 0 if unused - byLen map[int]*bucket - lens []int // lengths that have a bucket, increasing - tabs [][]table4 // chunks holding the tables of the centers - n int -} - -func newCenterSet(width int) *centerSet { - return ¢erSet{width: width, byLen: make(map[int]*bucket)} -} - -// add stores a copy of the entry e, which is the entry idx of the dataset, as -// the next center, and returns it. -func (s *centerSet) add(e entry, idx int) center { - if s.n%tabChunk == 0 { - s.tabs = append(s.tabs, make([]table4, tabChunk)) - } - tab := &s.tabs[len(s.tabs)-1][s.n%tabChunk] - if e.table != nil { - *tab = *e.table - } - e.table = tab - - b := s.byLen[e.length] - if b == nil { - b = new(bucket) - s.byLen[e.length] = b - s.lens = append(s.lens, e.length) - sort.Ints(s.lens) - } - if w := s.width; w > 0 && e.table != nil { - if len(b.cents)%w == 0 { - b.blocks = append(b.blocks, newTableBlock(w)) - } - b.blocks[len(b.blocks)-1].set(w, len(b.cents)%w, tab) - } - - ctr := center{entry: e, idx: idx, rank: s.n} - b.cents = append(b.cents, ctr) - s.n++ - return ctr -} - -// scan compares entry i with all the centers of the set, and returns the -// first one it matches (in fast mode) or the best one (in exact mode, the -// first of them in case of tie), exactly as compare would on the list of the -// centers in the order of creation. -// -// Only the buckets of compatible lengths are visited: the other centers would -// give the worst score. In each bucket the first match is the one of lowest -// rank, so the first match of the whole set is the one of lowest rank among -// the buckets. In exact mode, the best of each bucket is compared, ties going -// to the lowest rank. -func (c *clustering) scan(i int, buf *obialign.BandedLCSBuffer) attachment { - q := &c.entries[i] - set := c.set - width := set.width - useBlocks := width > 0 && q.sparse != nil - - best := attachment{-1, c.sc.worst} - bestRank := math.MaxInt - var common [64]uint8 - - for _, length := range set.lens { - if !c.sc.lengthCompatible(q.length, length) { - continue - } - b := set.byLen[length] - local := attachment{-1, c.sc.worst} - localRank := 0 - - for k := range b.cents { - ctr := &b.cents[k] - if c.fast && ctr.rank > bestRank { - break // a center of lower rank has been found in another bucket - } - cw := -1 - if useBlocks { - if k%width == 0 { - b.blocks[k/width].common(width, q.sparse, common[:]) - } - cw = int(common[k%width]) - } - s := c.sc.pairScore(q, &ctr.entry, cw, buf) - if c.sc.selects(s, local.score) { - local, localRank = attachment{ctr.idx, s}, ctr.rank - if c.fast { - break - } - } - } - - if local.center < 0 { - continue - } - if c.fast { - if localRank < bestRank { - best, bestRank = local, localRank - } - } else if best.center < 0 || c.sc.beats(local.score, best.score) || - (local.score == best.score && localRank < bestRank) { - best, bestRank = local, localRank - } - } - return best -} diff --git a/pkg/obitools/obiclust/cluster.go b/pkg/obitools/obiclust/cluster.go index 2e8835e..7f3c7ae 100644 --- a/pkg/obitools/obiclust/cluster.go +++ b/pkg/obitools/obiclust/cluster.go @@ -6,6 +6,7 @@ import ( "sync/atomic" "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obialign" + sim "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obisimilarity" ) // clustering is the state of the star clustering of the distinct sequences, @@ -32,16 +33,16 @@ import ( // The result does not depend on the size of the blocks, which only trades the // sequential work of phase B against the synchronizations. type clustering struct { - entries []entry - sc *scoring + entries []sim.Entry + sc *sim.Scoring fast bool workers int // bounds of the size of the blocks, 0 for the defaults (tests only) blockMin, blockMax int centers []int // indices of the centers, in the order of creation - set *centerSet - fresh []center // the centers created in the current block + index *sim.Index + fresh []sim.Member // the centers created in the current block center []int // for each entry, the index of its center, -1 if it has none score []float64 // for each entry, the score with its center @@ -54,7 +55,7 @@ type attachment struct { score float64 } -func newClustering(entries []entry, sc *scoring, fast bool, workers int) *clustering { +func newClustering(entries []sim.Entry, sc *sim.Scoring, fast bool, workers int) *clustering { c := &clustering{ entries: entries, sc: sc, @@ -67,26 +68,32 @@ func newClustering(entries []entry, sc *scoring, fast bool, workers int) *cluste for i := range c.center { c.center[i] = -1 } - c.set = newCenterSet(simdBlockWidth()) + c.index = sim.NewIndex() return c } func (c *clustering) addCenter(i int) { - c.fresh = append(c.fresh, c.set.add(c.entries[i], i)) + c.fresh = append(c.fresh, c.index.Add(c.entries[i], i)) c.centers = append(c.centers, i) c.center[i] = i - c.score[i] = c.sc.centerScore(c.entries[i].length) + c.score[i] = c.sc.SelfScore(c.entries[i].Length) +} + +// selects tells if a pair of score s, ok telling that it was scored, must +// replace the best attachment found so far. +func (c *clustering) selects(s float64, ok bool, best attachment) bool { + return ok && c.sc.Passes(s) && (best.center < 0 || c.sc.Beats(s, best.score)) } // compare looks for the best center of entry i among the given ones. The // attachment `from` is the best one already known. The search stops at the // first match in fast mode. -func (c *clustering) compare(i int, cents []center, from attachment, buf *obialign.BandedLCSBuffer) attachment { +func (c *clustering) compare(i int, cents []sim.Member, from attachment, buf *obialign.BandedLCSBuffer) attachment { best := from for k := range cents { - s := c.sc.pairScore(&c.entries[i], ¢s[k].entry, -1, buf) - if c.sc.selects(s, best.score) { - best = attachment{cents[k].idx, s} + s, ok := c.sc.PairScore(&c.entries[i], ¢s[k].Entry, -1, buf) + if c.selects(s, ok, best) { + best = attachment{cents[k].ID, s} if c.fast { break } @@ -162,7 +169,7 @@ func (c *clustering) run(progress func(done int)) { } unmatched++ *a = c.compare(i, c.fresh, *a, &c.bufs[0]) - if a.center < 0 && !c.entries[i].ambiguous { + if a.center < 0 && !c.entries[i].Ambiguous { newCenters = append(newCenters, i) c.addCenter(i) } @@ -181,7 +188,7 @@ func (c *clustering) run(progress func(done int)) { for i := pos; i < end; i++ { if a := att[i-pos]; a.center >= 0 { c.center[i] = a.center - c.score[i] = c.sc.memberScore(a.score) + c.score[i] = c.sc.Reported(a.score) } } diff --git a/pkg/obitools/obiclust/kmer4.go b/pkg/obitools/obiclust/kmer4.go deleted file mode 100644 index 71db5bd..0000000 --- a/pkg/obitools/obiclust/kmer4.go +++ /dev/null @@ -1,47 +0,0 @@ -package obiclust - -// table4 is the 4-mer occurrence table of a sequence, as sumaclust builds it: -// one saturating counter per word, and the number of occurrences that did -// not fit in the counters. -type table4 struct { - count [256]uint8 - overflow int -} - -// buildTable4 counts the 4-mers of a sequence made of the letters a, c, g -// and t. The 2-bit code of a base is (letter>>1)&3, i.e. a=0, c=1, t=2, g=3, -// which is the encoding of sumaclust. -func buildTable4(seq []byte) *table4 { - t := new(table4) - t.fill(seq) - return t -} - -// fill counts the 4-mers of seq in a zeroed table. -func (t *table4) fill(seq []byte) { - if len(seq) < 4 { - return - } - w := uint(0) - for i, c := range seq { - w = (w<<2 | uint(c>>1)&3) & 0xFF - if i < 3 { - continue - } - if t.count[w] < 255 { - t.count[w]++ - } else { - t.overflow++ - } - } -} - -// minCommonWords is the minimum number of 4-mers that two sequences must -// share when the longest has length reflen and they have a common -// subsequence of length lcs (integer arithmetic of sumaclust, thresholdLCS4). -func minCommonWords(reflen, lcs int) int { - nbfrag := (reflen-lcs)*2 + 1 - smin := lcs / nbfrag - r := lcs - smin*nbfrag - return max(smin-2, 0)*r + max(smin-3, 0)*(nbfrag-r) -} diff --git a/pkg/obitools/obiclust/kmer4_nosimd.go b/pkg/obitools/obiclust/kmer4_nosimd.go deleted file mode 100644 index 01e32a4..0000000 --- a/pkg/obitools/obiclust/kmer4_nosimd.go +++ /dev/null @@ -1,14 +0,0 @@ -//go:build !goexperiment.simd - -package obiclust - -// commonWords is the upper bound sumaclust uses for the number of 4-mers -// shared by two sequences: the sum of the minima of the counters, plus the -// minimum of the two overflows. -func commonWords(t1, t2 *table4) int { - total := 0 - for i := range t1.count { - total += int(min(t1.count[i], t2.count[i])) - } - return total + min(t1.overflow, t2.overflow) -} diff --git a/pkg/obitools/obiclust/kmer4_test.go b/pkg/obitools/obiclust/kmer4_test.go deleted file mode 100644 index 66e8f34..0000000 --- a/pkg/obitools/obiclust/kmer4_test.go +++ /dev/null @@ -1,71 +0,0 @@ -package obiclust - -import ( - "math/rand" - "testing" -) - -// commonWordsRef is the definition of commonWords, spelled out. -func commonWordsRef(t1, t2 *table4) int { - total := 0 - for i := range t1.count { - total += int(min(t1.count[i], t2.count[i])) - } - return total + min(t1.overflow, t2.overflow) -} - -func TestCommonWords(t *testing.T) { - rng := rand.New(rand.NewSource(3)) - seq := func(n int, alpha string) []byte { - s := make([]byte, n) - for i := range s { - s[i] = alpha[rng.Intn(len(alpha))] - } - return s - } - for it := 0; it < 3000; it++ { - alpha := []string{"acgt", "ac", "a"}[it%3] - t1 := buildTable4(seq(rng.Intn(1200), alpha)) - t2 := buildTable4(seq(rng.Intn(1200), alpha)) - if got, want := commonWords(t1, t2), commonWordsRef(t1, t2); got != want { - t.Fatalf("commonWords = %d, want %d", got, want) - } - } -} - -// TestBuildTable4 checks the table against the 4-mers counted the long way, -// including the saturation of the counters. -func TestBuildTable4(t *testing.T) { - code := map[byte]int{'a': 0, 'c': 1, 't': 2, 'g': 3} - rng := rand.New(rand.NewSource(4)) - for it := 0; it < 500; it++ { - alpha := []string{"acgt", "a", "ac"}[it%3] - n := rng.Intn(900) - s := make([]byte, n) - for i := range s { - s[i] = alpha[rng.Intn(len(alpha))] - } - var counts [256]int - for i := 0; i+4 <= n; i++ { - w := 0 - for _, c := range s[i : i+4] { - w = w<<2 | code[c] - } - counts[w]++ - } - tab := buildTable4(s) - over := 0 - for w, c := range counts { - if c > 255 { - over += c - 255 - c = 255 - } - if int(tab.count[w]) != c { - t.Fatalf("word %d: got %d, want %d", w, tab.count[w], c) - } - } - if tab.overflow != over { - t.Fatalf("overflow = %d, want %d", tab.overflow, over) - } - } -} diff --git a/pkg/obitools/obiclust/obiclust.go b/pkg/obitools/obiclust/obiclust.go index 1342db5..298454c 100644 --- a/pkg/obitools/obiclust/obiclust.go +++ b/pkg/obitools/obiclust/obiclust.go @@ -2,57 +2,18 @@ package obiclust import ( "os" - "sync" - "sync/atomic" "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obialign" "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obidefault" "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiiter" "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq" + sim "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obisimilarity" "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obitools/obiconvert" "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obitools/obicsv" "github.com/schollz/progressbar/v3" log "github.com/sirupsen/logrus" ) -// buildEntries describes the distinct sequences of the groups, the 4-mer -// tables being computed in parallel. -func buildEntries(recs []record, groups []group, workers int) []entry { - entries := make([]entry, len(groups)) - tables := make([]table4, len(groups)) // contiguous, in the order the centers are scanned - - var next atomic.Int64 - var wg sync.WaitGroup - for w := 0; w < max(workers, 1); w++ { - wg.Add(1) - go func() { - defer wg.Done() - for { - g := int(next.Add(1)) - 1 - if g >= len(groups) { - return - } - head := &recs[groups[g].start] - var sparse []uint16 - if !head.ambiguous { - tables[g].fill(head.clean) - sparse = sparseWords(&tables[g]) - } - entries[g] = entry{ - seq: head.clean, - length: len(head.clean), - count: groups[g].count, - table: &tables[g], - sparse: sparse, - ambiguous: head.ambiguous, - } - } - }() - } - wg.Wait() - return entries -} - // Params are the parameters of the clustering. type Params struct { Threshold float64 @@ -60,7 +21,7 @@ type Params struct { Exact bool // attach a sequence to the best center, not to the first matching one Distance bool // the score is a distance, not a similarity Normalized bool // the score is normalized by the reference length - Reference reference + Reference sim.Reference SortKey string // "count", "None", or an attribute Ascending bool AcceptAmbiguous bool // keep the sequences with IUPAC letters other than acgt (they cannot be centers) @@ -84,34 +45,20 @@ func ParamsFromCLI() Params { } // newScoring builds the scoring from the parameters, checking them as sumaclust does. -func newScoring(p Params, entries []entry) *scoring { - threshold := p.Threshold - - if threshold <= 0 || (p.Normalized && threshold > 1.0) { +func newScoring(p Params, entries []sim.Entry) *sim.Scoring { + if p.Threshold <= 0 { log.Fatal("Please specify a threshold > 0, and < 1 when scores are normalized.") } - - sc := &scoring{ - normalize: p.Normalized, - lcsmode: !p.Distance, - ref: p.Reference, - ratio: p.Ratio, + sc, err := sim.NewScoring(sim.Params{ + Threshold: p.Threshold, + Normalized: p.Normalized, + Distance: p.Distance, + Reference: p.Reference, + Ratio: p.Ratio, + }, entries) + if err != nil { + log.Fatalf("Please specify a threshold > 0, and < 1 when scores are normalized: %v", err) } - if !sc.lcsmode && sc.normalize { - threshold = 1.0 - threshold - } - sc.threshold = threshold - - lmax := 0 - for i := range entries { - lmax = max(lmax, entries[i].length) - } - if sc.similarityLike() { - sc.worst = 0.0 - } else { - sc.worst = float64(lmax) - } - sc.prepareLengths(lmax) return sc } @@ -153,19 +100,22 @@ func Cluster(db obiseq.BioSequenceSlice, p Params, progress func(done, total int } log.Infof("%d distinct sequences", len(groups)) - entries := buildEntries(recs, groups, p.Workers) + entries := sim.BuildEntries(len(groups), func(g int) ([]byte, int, bool) { + head := &recs[groups[g].start] + return head.clean, groups[g].count, head.ambiguous + }, p.Workers) for i := range entries { - if entries[i].length > obialign.MaxBandedLCSLength { + if entries[i].Length > obialign.MaxBandedLCSLength { log.Fatalf("The sequence %s is too long (%d bp, maximum %d bp)", - recs[groups[i].start].seq.Id(), entries[i].length, obialign.MaxBandedLCSLength) + recs[groups[i].start].seq.Id(), entries[i].Length, obialign.MaxBandedLCSLength) } } sc := newScoring(p, entries) - if sc.similarityLike() { - log.Infof("Clustering sequences when similarity >= %f", sc.threshold) + if sc.SimilarityLike() { + log.Infof("Clustering sequences when similarity >= %f", sc.Threshold()) } else { - log.Infof("Clustering sequences when distance <= %f", sc.threshold) + log.Infof("Clustering sequences when distance <= %f", sc.Threshold()) } cl := newClustering(entries, sc, !p.Exact, p.Workers) @@ -185,10 +135,11 @@ func CLIOBIClust(iterator obiiter.IBioSequence) obiiter.IBioSequence { log.Infof("Sequence dataset of %d sequences loaded", len(db)) var bar *progressbar.ProgressBar + barChecked := false // the bar is created once, even when it is disabled last := 0 res := Cluster(db, ParamsFromCLI(), func(done, total int) { - if bar == nil { - bar = progressBar(total) + if !barChecked { + bar, barChecked = progressBar(total), true } if bar != nil { bar.Add(done - last) diff --git a/pkg/obitools/obiclust/obiclust_test.go b/pkg/obitools/obiclust/obiclust_test.go index fff004e..d5a61ff 100644 --- a/pkg/obitools/obiclust/obiclust_test.go +++ b/pkg/obitools/obiclust/obiclust_test.go @@ -12,6 +12,7 @@ import ( "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obialign" "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq" + sim "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obisimilarity" ) // readTestFasta reads a fasta file whose headers are "id count=N; key=V;". @@ -75,11 +76,11 @@ func paramsFromSumaclust(t testing.TB, args []string) Params { case "-e": p.Exact = true case "-l": - p.Reference = refShortest + p.Reference = sim.RefShortest case "-L": - p.Reference = refLongest + p.Reference = sim.RefLongest case "-a": - p.Reference = refAlignment + p.Reference = sim.RefAlignment case "-d": p.Distance = true case "-r": @@ -138,37 +139,37 @@ func TestSumaclustResults(t *testing.T) { } // referenceCluster transcribes the sequential algorithm of sumaclust. -func referenceCluster(entries []entry, sc *scoring, fast bool) (center []int, score []float64) { +func referenceCluster(entries []sim.Entry, sc *sim.Scoring, fast bool) (center []int, score []float64) { n := len(entries) center, score = make([]int, n), make([]float64, n) var centers []int var buf obialign.BandedLCSBuffer for i := range entries { - scoremax, best := sc.worst, -1 + best, bestScore := -1, 0.0 for _, c := range centers { - s := sc.pairScore(&entries[i], &entries[c], -1, &buf) - if sc.selects(s, scoremax) { - scoremax, best = s, c + s, ok := sc.PairScore(&entries[i], &entries[c], -1, &buf) + if ok && sc.Passes(s) && (best < 0 || sc.Beats(s, bestScore)) { + best, bestScore = c, s if fast { break } } } if best >= 0 { - center[i], score[i] = best, sc.memberScore(scoremax) - } else if entries[i].ambiguous { + center[i], score[i] = best, sc.Reported(bestScore) + } else if entries[i].Ambiguous { center[i] = -1 } else { - center[i], score[i] = i, sc.centerScore(entries[i].length) + center[i], score[i] = i, sc.SelfScore(entries[i].Length) centers = append(centers, i) } } return center, score } -func randomEntries(rng *rand.Rand, n int) []entry { +func randomEntries(rng *rand.Rand, n int) []sim.Entry { var roots [][]byte - entries := make([]entry, 0, n) + entries := make([]sim.Entry, 0, n) for len(entries) < n { var s []byte if len(roots) == 0 || rng.Intn(5) == 0 { @@ -191,8 +192,8 @@ func randomEntries(rng *rand.Rand, n int) []entry { } } } - tab := buildTable4(s) - entries = append(entries, entry{seq: s, length: len(s), count: 1 + rng.Intn(20), table: tab, sparse: sparseWords(tab)}) + tab := sim.NewTable4(s) + entries = append(entries, sim.Entry{Seq: s, Length: len(s), Count: 1 + rng.Intn(20), Table: tab, Sparse: sim.SparseWords(tab)}) } return entries } @@ -202,22 +203,25 @@ func randomEntries(rng *rand.Rand, n int) []entry { // result of the sequential algorithm. func TestParallelClusteringIsSequential(t *testing.T) { rng := rand.New(rand.NewSource(21)) - scorings := []scoring{ - {threshold: 0.9, normalize: true, lcsmode: true, ref: refAlignment, ratio: 1, worst: 0}, - {threshold: 0.85, normalize: true, lcsmode: true, ref: refShortest, ratio: 2, worst: 0}, - {threshold: 0.9, normalize: true, lcsmode: false, ref: refLongest, ratio: 1, worst: 0}, - {threshold: 35, normalize: false, lcsmode: true, ref: refAlignment, ratio: 1, worst: 0}, - {threshold: 5, normalize: false, lcsmode: false, ref: refAlignment, ratio: 1, worst: 80}, + scorings := []sim.Params{ + {Threshold: 0.9, Normalized: true, Reference: sim.RefAlignment, Ratio: 1}, + {Threshold: 0.85, Normalized: true, Reference: sim.RefShortest, Ratio: 2}, + {Threshold: 0.9, Normalized: true, Distance: true, Reference: sim.RefLongest, Ratio: 1}, + {Threshold: 35, Reference: sim.RefAlignment, Ratio: 1}, + {Threshold: 5, Distance: true, Reference: sim.RefAlignment, Ratio: 1}, } for round := 0; round < 4; round++ { entries := randomEntries(rng, 300) for si := range scorings { - sc := scorings[si] + sc, err := sim.NewScoring(scorings[si], entries) + if err != nil { + t.Fatal(err) + } for _, fast := range []bool{true, false} { - wantCenter, wantScore := referenceCluster(entries, &sc, fast) + wantCenter, wantScore := referenceCluster(entries, sc, fast) for _, workers := range []int{1, 3, 8} { for _, blocks := range [][2]int{{0, 0}, {1, 1}, {2, 5}, {7, 7}, {50, 300}} { - cl := newClustering(entries, &sc, fast, workers) + cl := newClustering(entries, sc, fast, workers) cl.blockMin, cl.blockMax = blocks[0], blocks[1] cl.run(func(int) {}) for i := range entries { @@ -233,28 +237,6 @@ func TestParallelClusteringIsSequential(t *testing.T) { } } -func TestClassify(t *testing.T) { - cases := []struct { - in string - accept bool - kept bool - ambiguous bool - }{ - {"acgt", false, true, false}, {"acgt", true, true, false}, - {"acgtn", false, false, false}, {"acgtn", true, true, true}, - {"acgtx", true, false, false}, {"nacgt", false, false, false}, - {"acgt-", true, false, false}, {"acgu", true, true, true}, - {"ACGT", true, false, false}, {"", true, false, false}, - {"rykmswbdhvn", true, true, true}, {"rykmswbdhvn", false, false, false}, - } - for _, c := range cases { - got, amb := classify([]byte(c.in), c.accept) - if (got != nil) != c.kept || amb != c.ambiguous { - t.Errorf("classify(%q, %v) = (%q, %v), want kept=%v ambiguous=%v", c.in, c.accept, got, amb, c.kept, c.ambiguous) - } - } -} - func makeSeq(id, seq string, count int, key float64) *obiseq.BioSequence { s := obiseq.NewBioSequence(id, []byte(seq), "") s.SetAttribute("count", count) @@ -353,82 +335,3 @@ func TestAmbiguousSequences(t *testing.T) { } } } - -// TestLengthTable checks that the values precomputed by length give the -// scores of the direct computation, in every mode. -func TestLengthTable(t *testing.T) { - rng := rand.New(rand.NewSource(33)) - entries := randomEntries(rng, 120) - for i := 0; i < 20; i++ { // some very short sequences, which skip the filter - s := entries[i].seq[:8+rng.Intn(10)] - entries[i] = entry{seq: s, length: len(s), count: 1, table: buildTable4(s)} - } - lmax := 0 - for i := range entries { - lmax = max(lmax, entries[i].length) - } - var buf obialign.BandedLCSBuffer - for _, ref := range []reference{refAlignment, refLongest, refShortest} { - for _, mode := range []scoring{ - {threshold: 0.9, normalize: true, lcsmode: true}, - {threshold: 0.8, normalize: true, lcsmode: false}, - {threshold: 30, normalize: false, lcsmode: true}, - {threshold: 6, normalize: false, lcsmode: false}, - } { - direct := mode - direct.ref, direct.ratio = ref, 3 - table := direct - table.prepareLengths(lmax) - if (ref == refShortest) != (table.byLength == nil) { - t.Fatalf("ref %v: table presence is wrong", ref) - } - for i := range entries { - for j := range entries { - a, b := direct.pairScore(&entries[i], &entries[j], -1, &buf), table.pairScore(&entries[i], &entries[j], -1, &buf) - if a != b { - t.Fatalf("ref %v mode %+v pair %d,%d: %v with the table, %v directly", ref, mode, i, j, b, a) - } - } - } - } - } -} - -// TestBlockCommon checks the sums of a block of transposed tables against -// commonWords. -func TestBlockCommon(t *testing.T) { - width := simdBlockWidth() - if width == 0 { - t.Skip("blocks of tables are used with GOEXPERIMENT=simd only") - } - rng := rand.New(rand.NewSource(8)) - seq := func(n int, alpha string) []byte { - s := make([]byte, n) - for i := range s { - s[i] = alpha[rng.Intn(len(alpha))] - } - return s - } - for it := 0; it < 200; it++ { - alpha := []string{"acgt", "ac", "a"}[it%3] - block := newTableBlock(width) - tabs := make([]*table4, width) - for j := range tabs { - tabs[j] = buildTable4(seq(rng.Intn(600), alpha)) - block.set(width, j, tabs[j]) - } - q := buildTable4(seq(rng.Intn(260), alpha)) - words := sparseWords(q) - if words == nil { - continue - } - out := make([]uint8, 64) - block.common(width, words, out) - for j, tab := range tabs { - want := commonWordsRef(q, tab) - min(q.overflow, tab.overflow) - if int(out[j]) != want { - t.Fatalf("lane %d: %d, want %d", j, out[j], want) - } - } - } -} diff --git a/pkg/obitools/obiclust/options.go b/pkg/obitools/obiclust/options.go index 00a4217..5cf7ae2 100644 --- a/pkg/obitools/obiclust/options.go +++ b/pkg/obitools/obiclust/options.go @@ -1,21 +1,12 @@ package obiclust import ( + sim "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obisimilarity" "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obitools/obiconvert" "github.com/DavidGamba/go-getoptions" log "github.com/sirupsen/logrus" ) -// reference is the length used to normalize a score (or to express a raw -// distance): the alignment length, the longest or the shortest sequence. -type reference int - -const ( - refAlignment reference = iota - refLongest - refShortest -) - var _threshold = 0.97 var _ratio = 1.0 var _exact = false @@ -76,7 +67,7 @@ func ObiclustOptionSet(options *getoptions.GetOpt) { options.Alias("R"), options.ArgName("RATIO"), options.Description("Maximum ratio between the counts of two sequences so that the less abundant one "+ - "can be considered as a variant of the more abundant one (default: %3.2f).")) + "can be considered as a variant of the more abundant one.")) options.StringVar(&_sortKey, "sort", _sortKey, options.Alias("s"), @@ -133,16 +124,16 @@ func CLINormalized() bool { } // CLIReference returns the length used as reference to normalize the score. -func CLIReference() reference { +func CLIReference() sim.Reference { n := 0 - ref := refAlignment + ref := sim.RefAlignment if _refShortest { n++ - ref = refShortest + ref = sim.RefShortest } if _refLongest { n++ - ref = refLongest + ref = sim.RefLongest } if _refAlignment { n++ diff --git a/pkg/obitools/obiclust/prepare.go b/pkg/obitools/obiclust/prepare.go index 73a0f7a..c40f9a8 100644 --- a/pkg/obitools/obiclust/prepare.go +++ b/pkg/obitools/obiclust/prepare.go @@ -7,6 +7,7 @@ import ( "strings" "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq" + sim "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obisimilarity" "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiutils" log "github.com/sirupsen/logrus" ) @@ -30,45 +31,6 @@ type group struct { key float64 } -// nucleotide classes of a byte -const ( - notNucleotide = iota - plainNucleotide - ambiguousNucleotide -) - -var nucleotideClass = func() (t [256]uint8) { - for _, c := range "acgt" { - t[c] = plainNucleotide - } - for _, c := range "umrwsykvhdbn" { - t[c] = ambiguousNucleotide - } - return t -}() - -// classify returns the nucleotides of a sequence and tells if it contains -// IUPAC letters other than a, c, g and t. nil is returned if the sequence is -// empty, or contains anything else than IUPAC letters (and also IUPAC letters -// other than a, c, g and t unless ambiguous ones are accepted). -func classify(s []byte, acceptAmbiguous bool) (clean []byte, ambiguous bool) { - if len(s) == 0 { - return nil, false - } - for _, c := range s { - switch nucleotideClass[c] { - case notNucleotide: - return nil, false - case ambiguousNucleotide: - if !acceptAmbiguous { - return nil, false - } - ambiguous = true - } - } - return s, ambiguous -} - // atof reads the leading number of a string as C atof does. func atof(v interface{}) float64 { if s, ok := v.(string); ok { @@ -120,12 +82,12 @@ func prepare(db obiseq.BioSequenceSlice, key string, ascending, acceptAmbiguous discarded := 0 for pos, s := range db { - clean, ambiguous := classify(s.Sequence(), acceptAmbiguous) - if clean == nil { + ok, ambiguous := sim.Classify(s.Sequence(), acceptAmbiguous) + if !ok { discarded++ continue } - r := record{seq: s, pos: pos, clean: clean, count: s.Count(), ambiguous: ambiguous} + r := record{seq: s, pos: pos, clean: s.Sequence(), count: s.Count(), ambiguous: ambiguous} if key != "None" { r.key = sortKeyOf(s, key) } diff --git a/pkg/obitools/obiclust/scan.go b/pkg/obitools/obiclust/scan.go new file mode 100644 index 0000000..e92c450 --- /dev/null +++ b/pkg/obitools/obiclust/scan.go @@ -0,0 +1,63 @@ +package obiclust + +import ( + "math" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obialign" + sim "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obisimilarity" +) + +// scan compares entry i with all the centers of the index, and returns the +// first one it matches (in fast mode) or the best one (in exact mode, the +// first of them in case of tie), exactly as compare would on the list of the +// centers in the order of creation. +// +// Only the buckets of compatible lengths are visited: the other centers would +// not match. In each bucket the first match is the one of lowest rank, so the +// first match of the whole index is the one of lowest rank among the buckets. +// In exact mode, the best of each bucket is compared, ties going to the +// lowest rank. +func (c *clustering) scan(i int, buf *obialign.BandedLCSBuffer) attachment { + q := &c.entries[i] + + best := attachment{-1, 0} + bestRank := math.MaxInt + var commons sim.Commons + + for _, length := range c.index.Lengths() { + if !c.sc.LengthCompatible(q.Length, length) { + continue + } + b := c.index.Bucket(length) + commons.Reset() + local := attachment{-1, 0} + localRank := 0 + + for k := range b.Members { + m := &b.Members[k] + if c.fast && m.Rank > bestRank { + break // a center of lower rank has been found in another bucket + } + s, ok := c.sc.PairScore(q, &m.Entry, b.Common(k, q.Sparse, &commons), buf) + if c.selects(s, ok, local) { + local, localRank = attachment{m.ID, s}, m.Rank + if c.fast { + break + } + } + } + + if local.center < 0 { + continue + } + if c.fast { + if localRank < bestRank { + best, bestRank = local, localRank + } + } else if best.center < 0 || c.sc.Beats(local.score, best.score) || + (local.score == best.score && localRank < bestRank) { + best, bestRank = local, localRank + } + } + return best +} diff --git a/pkg/obitools/obiclust/score.go b/pkg/obitools/obiclust/score.go deleted file mode 100644 index 5d13a08..0000000 --- a/pkg/obitools/obiclust/score.go +++ /dev/null @@ -1,233 +0,0 @@ -package obiclust - -import ( - "math" - - "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obialign" - log "github.com/sirupsen/logrus" -) - -// entry is a distinct sequence of the dataset with what the clustering needs. -type entry struct { - seq []byte - length int - count int // cumulated count of the identical sequences - table *table4 - sparse []uint16 // words of the table (see sparseWords), nil if unusable - // ambiguous sequences contain IUPAC letters other than a, c, g and t: - // they have no 4-mer table, and cannot be the center of a cluster - ambiguous bool -} - -// scoring holds the way sumaclust scores a pair of sequences and decides -// whether it passes the threshold. It reproduces the arithmetic of the -// reference implementation (integer truncations, floating point -// comparisons) so that the same pairs are accepted with the same scores. -type scoring struct { - threshold float64 // threshold as used for the comparison (1 - t for a normalized distance) - normalize bool - lcsmode bool // true: score is a similarity, false: it is a distance - ref reference - ratio float64 - worst float64 // score of a pair that cannot be attached - - // byLength gives, for the pairs whose longest sequence has a length l, - // the values that depend on l only. It is built when the reference is not - // the shortest sequence, otherwise those values depend on both lengths. - byLength []lengthInfo -} - -type lengthInfo struct { - lcsMin int32 // see lcsMin - minCommon int32 // see minCommon -} - -// prepareLengths fills byLength for the lengths up to lmax. -func (sc *scoring) prepareLengths(lmax int) { - if sc.ref == refShortest { - return - } - sc.byLength = make([]lengthInfo, lmax+1) - for l := range sc.byLength { - sc.byLength[l] = lengthInfo{int32(sc.lcsMin(l, l)), int32(sc.minCommon(l, l))} - } -} - -// similarityLike tells if the comparison with the threshold is "score >= threshold". -func (sc *scoring) similarityLike() bool { return sc.lcsmode || sc.normalize } - -// lcsMin is the smallest LCS score that a pair of lengths l1 >= l2 can have -// and still pass the threshold (calculateLCSmin of sumaclust). -func (sc *scoring) lcsMin(l1, l2 int) int { - switch { - case sc.threshold <= 0: - return 0 - case sc.normalize: - if sc.ref == refShortest { - return int(sc.threshold * float64(l2)) - } - return int(sc.threshold * float64(l1)) - case sc.lcsmode: - return int(sc.threshold) - case sc.ref == refShortest: - return int(float64(l2) - sc.threshold) - default: - return int(float64(l1) - sc.threshold) - } -} - -// minCommon is the number of 4-mers that two sequences of lengths l1 >= l2 -// must share to be possibly similar enough (isPossibleSumathings). -func (sc *scoring) minCommon(l1, l2 int) int { - reflen := l2 - if sc.ref == refAlignment || sc.ref == refLongest { - reflen = l1 - } - var lcs int - if sc.normalize { - lcs = int(math.Ceil(float64(reflen) * sc.threshold)) - } else { - th := sc.threshold - if !sc.lcsmode { - th = float64(reflen) - th - } - lcs = int(th) - } - return minCommonWords(l1, lcs) -} - -// pairScore returns the score of a query sequence against a center, or the -// worst score when the pair cannot be attached. -// -// common, if not negative, is the sum of the minima of the 4-mer counters of -// the two sequences, already computed by a block of tables. -func (sc *scoring) pairScore(q, c *entry, common int, buf *obialign.BandedLCSBuffer) float64 { - // The quotient of two counts is not above 1 when the first is not above - // the second, which spares the division in the usual case. - if !(sc.ratio >= 1 && q.count <= c.count) && float64(q.count)/float64(c.count) > sc.ratio { - return sc.worst - } - - l1, l2 := q.length, c.length - long, short := q, c - if l1 < l2 { - l1, l2 = l2, l1 - long, short = c, q - } - var lcsMin, minCommon int - if sc.byLength != nil { - info := sc.byLength[l1] - lcsMin, minCommon = int(info.lcsMin), int(info.minCommon) - } else { - lcsMin, minCommon = sc.lcsMin(l1, l2), sc.minCommon(l1, l2) - } - if l2 < lcsMin { - return sc.worst - } - // The 4-mer filter counts identical words: it does not hold when IUPAC - // codes can match different letters. It lets short sequences pass. - if l1 >= 12 && l2 >= 12 && !long.ambiguous && !short.ambiguous { - if common >= 0 { - common += min(long.table.overflow, short.table.overflow) - } else { - common = commonWords(long.table, short.table) - } - if common < minCommon { - return sc.worst - } - } - - // The length of the alignment is needed only to normalize by it, or to - // express a distance with it; the score alone is much cheaper. - var lcs, alilen int - var err error - if sc.ref == refAlignment && (sc.normalize || !sc.lcsmode) { - lcs, alilen, err = buf.AtLeast(q.seq, c.seq, lcsMin) - } else { - lcs, err = buf.ScoreAtLeast(q.seq, c.seq, lcsMin) - } - if err != nil { - log.Fatalf("Cannot align two sequences: %v", err) - } - if lcs < 0 { - // The best alignment has a score under lcsMin: the pair cannot pass. - return sc.worst - } - - id := float64(lcs) - if !sc.lcsmode && !sc.normalize { - switch sc.ref { - case refAlignment: - id = float64(alilen) - id - case refLongest: - id = float64(l1) - id - case refShortest: - id = float64(l2) - id - } - } - if sc.normalize { - switch sc.ref { - case refAlignment: - id /= float64(alilen) - case refLongest: - id /= float64(l1) - case refShortest: - id /= float64(l2) - } - } - return id -} - -// beats tells if the score s is better than the score best (without any -// threshold). -func (sc *scoring) beats(s, best float64) bool { - if sc.similarityLike() { - return s > best - } - return s < best -} - -// lcsMinFor is lcsMin for sequences of lengths l1 >= l2, from the table when it exists. -func (sc *scoring) lcsMinFor(l1, l2 int) int { - if sc.byLength != nil { - return int(sc.byLength[l1].lcsMin) - } - return sc.lcsMin(l1, l2) -} - -// lengthCompatible tells if two sequences of these lengths may be similar enough: -// the shortest cannot be shorter than the minimum LCS score of the pair. -func (sc *scoring) lengthCompatible(la, lb int) bool { - l1, l2 := max(la, lb), min(la, lb) - return l2 >= sc.lcsMinFor(l1, l2) -} - -// selects tells if a pair of score s must replace the best pair found so far, -// of score best. -func (sc *scoring) selects(s, best float64) bool { - if sc.similarityLike() { - return s >= sc.threshold && s > best - } - return s <= sc.threshold && s < best -} - -// centerScore is the score of a sequence with itself. -func (sc *scoring) centerScore(length int) float64 { - switch { - case sc.normalize && sc.lcsmode: - return 1.0 - case !sc.lcsmode: - return 0.0 - default: - return float64(length) - } -} - -// memberScore is the score reported for a sequence attached to a center -// with the score s. -func (sc *scoring) memberScore(s float64) float64 { - if !sc.lcsmode && sc.normalize { - return 1.0 - s - } - return s -} From 3f0ca993d28e08382833d99c3b63bc9347ae1f07 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Tue, 3 Nov 2026 14:10:48 +1100 Subject: [PATCH 11/24] Implement k-mer counting and sequence handling Introduces the SeqKmerCounter structure for parallel k-mer counting, refactors sequence reading and writing logic, and adds comprehensive test coverage for various sequence scenarios. --- OBIPRIMERS_NOTES.md | 36 ++++- pkg/obikmer/seqquorum.go | 190 +++++++++++++++++++++++ pkg/obikmer/seqquorum_count.go | 155 +++++++++++++++++++ pkg/obikmer/seqquorum_test.go | 272 +++++++++++++++++++++++++++++++++ pkg/obikmer/skm_reader.go | 18 ++- pkg/obikmer/skm_writer.go | 45 +++--- 6 files changed, 684 insertions(+), 32 deletions(-) create mode 100644 pkg/obikmer/seqquorum.go create mode 100644 pkg/obikmer/seqquorum_count.go create mode 100644 pkg/obikmer/seqquorum_test.go diff --git a/OBIPRIMERS_NOTES.md b/OBIPRIMERS_NOTES.md index 196f647..a1d88a0 100644 --- a/OBIPRIMERS_NOTES.md +++ b/OBIPRIMERS_NOTES.md @@ -168,9 +168,7 @@ Validation : - Les 13 Tm imprimés par ecoPrimers sont retrouvés à +/- 0,05 °C près. Écarts volontaires avec ecoPrimers (à confirmer) : -1. **273,15 au lieu de 273,0** : ecoPrimers calcule `Tm(K) - 273.0`. Les Tm de `obithermo` sont donc - 0,15 °C plus bas que ceux d'ecoPrimers (le test tient compte du décalage). Une constante, - facile à inverser si la compatibilité exacte des colonnes Tm prime. +1. **273,15 au lieu de 273,0** : ecoPrimers calcule `Tm(K) - 273.0`, `obithermo` utilise la valeur exacte 273,15 (Tm 0,15 °C plus bas ; le test tient compte du décalage). **Validé par l'utilisateur le 2026-11-03 : on garde 273,15.** 2. **Owczarzy corrigé** : dans ecoPrimers, `gcContent` n'est jamais affecté (grep sur tout `src/`), donc `-m 2` lit de la mémoire non initialisée (`CNNParams` est sur la pile dans `main`). `obithermo` utilise la fraction GC de l'amorce. Non comparable au C ; testé par invariants @@ -181,3 +179,35 @@ Validation : Pour plus tard : les k-mers d'obikmer sont codés A,C,G,T = 0..3, comme les tables de `obithermo` ; une variante `TmCode(uint64, k)` évitera le décodage quand le cÅ“ur appellera Tm par paire. + +## 9. Mode A, étape 2 : `obikmer.SeqKmerCounter` (2026-11-03) + +**Constat qui change le plan** : `KmerSetGroupBuilder` crée n x P écrivains `.skm` simultanés (un set par +séquence, P >= 64). Pour 3 909 génomes cela fait > 250 000 fichiers ouverts : `KmerSetGroup` + +`QuorumAtLeast` ne convient pas pour « dans combien de séquences ce k-mer apparaît ». + +**Solution** (enrichissement d'obikmer, pas de code local à obiprimers) : `pkg/obikmer/seqquorum.go` et +`seqquorum_count.go`. +- Phase 1, `AddSequence(id, seq, circular)` (concurrent) : super-kmers groupés par minimiseur, écrits avec + l'id de séquence dans **un seul** jeu de P fichiers (format `[uvarint id][uvarint len][2 bits/base]`). + Les bases non ACGTU découpent la séquence en segments : aucun k-mer ne contient d'ambiguïté (comme + le masque d'erreur d'ecoPrimers). Génome circulaire : on ajoute les k-1 premières bases. +- Phase 2, `CountAtLeast(q)` : partitions traitées en parallèle (`obidefault.ParallelWorkers()`), + paires (k-mer canonique, id) triées, séquences distinctes comptées. Résultat en mémoire, trié + (il est filtré par le quorum : 255 mots pour Teleostei, 1 208 pour Cyprinidae chez ecoPrimers). +- `SeqKmerCount.Repeated` reproduit le drapeau MULTIWORD d'ecoPrimers (k-mer présent plusieurs fois + dans une même séquence, les deux brins confondus), utile pour `-U` (pas de multi-match). +- Mémoire de la phase 2 : ~16 octets par occurrence de k-mer des partitions en cours de traitement ; + P doit croître avec la taille totale des données. +- Refactor neutre dans `skm_writer.go` / `skm_reader.go` : `appendPacked` et `unpackBases` extraits et + partagés avec le nouveau format (tests skm inchangés et verts). + +Tests (`seqquorum_test.go`) : oracle force brute indépendant (`EncodeCanonicalKmer` sur chaque fenêtre), +6 configurations (k = 4, 7, 18, 31 ; linéaire/circulaire ; avec N/R ; m et P variés ; insertion +concurrente ; motifs partagés sur les deux brins), plus cas des deux brins dans une même séquence, +ambiguïtés + jonction circulaire, paramètres invalides. `go test ./pkg/obikmer/ -race` vert. +Un bug trouvé par ces tests dans ma boucle de réduction (compteur de séquences) a été corrigé. + +Contrôle de bout en bout prévu (étape suivante) : `Strict primer count` d'ecoPrimers = **1 208** +(Cyprinidae) et **255** (Teleostei) avec `-e 3 -3 2 -l 30 -L 150 -c`, k = 18, quorum 0,7 ; le calcul +d'ecoPrimers est `floor(|E| x 0,7)` séquences (> longueur d'amorce). diff --git a/pkg/obikmer/seqquorum.go b/pkg/obikmer/seqquorum.go new file mode 100644 index 0000000..3471eaa --- /dev/null +++ b/pkg/obikmer/seqquorum.go @@ -0,0 +1,190 @@ +package obikmer + +import ( + "bufio" + "encoding/binary" + "fmt" + "os" + "path/filepath" + "sync" +) + +// SeqKmerCounter counts, for every canonical k-mer, in how many distinct input +// sequences it occurs. It answers "which k-mers are present in at least q of +// the N sequences" without one k-mer set per sequence: a KmerSetGroup needs +// N*P partition files, which is out of reach for thousands of genomes, whereas +// a SeqKmerCounter uses a single set of P files whatever N is. +// +// Construction is two-phase, like KmerSetGroupBuilder: +// +// 1. AddSequence extracts super-kmers (grouped by minimizer, so a k-mer and its +// reverse complement always land in the same partition) and appends them, +// tagged with the sequence id, to one of P temporary files. It is safe to +// call concurrently. +// 2. CountAtLeast processes the partitions in parallel: each one is loaded, +// expanded to (canonical k-mer, sequence id) pairs, sorted, and reduced. +// +// Memory during phase 2 is about 16 bytes per k-mer occurrence of the largest +// partitions being processed at the same time, so P must grow with the total +// input size (total bases / P is the occurrence count of an average partition). +// +// Bases other than A, C, G, T, U break a sequence into independent runs: no +// k-mer ever contains an ambiguous base. +type SeqKmerCounter struct { + dir string + k, m, p int + + parts []*seqPartWriter + + mu sync.Mutex + firstErr error + sealed bool +} + +type seqPartWriter struct { + mu sync.Mutex + w *bufio.Writer + file *os.File + path string +} + +// SeqKmerCount is the result for one k-mer. +type SeqKmerCount struct { + Kmer uint64 // canonical k-mer + Sequences uint32 // number of distinct sequences containing it + Repeated bool // occurs more than once (either strand) in at least one sequence +} + +// NewSeqKmerCounter creates a counter whose temporary files live in a new +// directory created under parentDir (use "" for the system temporary +// directory). k is the k-mer size (2..31), m the minimizer size (<=0 for the +// default) and partitions the number of partition files P. +func NewSeqKmerCounter(parentDir string, k, m, partitions int) (*SeqKmerCounter, error) { + if k < 2 || k > 31 { + return nil, fmt.Errorf("obikmer: k must be between 2 and 31, got %d", k) + } + if partitions < 1 { + return nil, fmt.Errorf("obikmer: partitions must be >= 1, got %d", partitions) + } + if m <= 0 { + m = DefaultMinimizerSize(k) + } + if m >= k { + return nil, fmt.Errorf("obikmer: minimizer size %d must be smaller than k=%d", m, k) + } + + dir, err := os.MkdirTemp(parentDir, "obikmer-seqcount-") + if err != nil { + return nil, err + } + + c := &SeqKmerCounter{dir: dir, k: k, m: m, p: partitions} + c.parts = make([]*seqPartWriter, partitions) + for i := range c.parts { + path := filepath.Join(dir, fmt.Sprintf("part_%04d.sqk", i)) + f, err := os.Create(path) + if err != nil { + c.Close() + return nil, err + } + c.parts[i] = &seqPartWriter{w: bufio.NewWriterSize(f, 32*1024), file: f, path: path} + } + return c, nil +} + +// K returns the k-mer size. +func (c *SeqKmerCounter) K() int { return c.k } + +var unambiguousBase = func() (t [256]bool) { + for _, b := range "ACGTUacgtu" { + t[b] = true + } + return +}() + +// AddSequence registers sequence id. The same id must be used for every call +// concerning the same sequence; ids need not be contiguous. If circular is +// true the k-1 first bases are appended so that k-mers spanning the junction +// are counted. AddSequence is safe for concurrent use and must not be called +// after CountAtLeast. +func (c *SeqKmerCounter) AddSequence(id uint32, seq []byte, circular bool) { + if len(seq) < c.k { + return + } + if circular { + ext := make([]byte, 0, len(seq)+c.k-1) + ext = append(ext, seq...) + seq = append(ext, seq[:c.k-1]...) + } + + var scratch []byte + start := -1 + for i, b := range seq { + switch { + case unambiguousBase[b]: + if start < 0 { + start = i + } + case start >= 0: + scratch = c.addRun(id, seq[start:i], scratch) + start = -1 + } + } + if start >= 0 { + c.addRun(id, seq[start:], scratch) + } +} + +// addRun writes the super-kmers of one ambiguity-free run. +func (c *SeqKmerCounter) addRun(id uint32, run []byte, scratch []byte) []byte { + if len(run) < c.k { + return scratch + } + for sk := range IterSuperKmers(run, c.k, c.m) { + scratch = binary.AppendUvarint(scratch[:0], uint64(id)) + scratch = binary.AppendUvarint(scratch, uint64(len(sk.Sequence))) + scratch = appendPacked(scratch, sk.Sequence) + + pw := c.parts[int(sk.Minimizer%uint64(c.p))] + pw.mu.Lock() + _, err := pw.w.Write(scratch) + pw.mu.Unlock() + if err != nil { + c.setErr(err) + } + } + return scratch +} + +func (c *SeqKmerCounter) setErr(err error) { + c.mu.Lock() + if c.firstErr == nil { + c.firstErr = err + } + c.mu.Unlock() +} + +// seal flushes and closes the partition files; no more sequences can be added. +func (c *SeqKmerCounter) seal() error { + c.mu.Lock() + defer c.mu.Unlock() + if c.sealed { + return c.firstErr + } + c.sealed = true + for _, pw := range c.parts { + if err := pw.w.Flush(); err != nil && c.firstErr == nil { + c.firstErr = err + } + if err := pw.file.Close(); err != nil && c.firstErr == nil { + c.firstErr = err + } + } + return c.firstErr +} + +// Close removes the temporary files. It is safe to call more than once. +func (c *SeqKmerCounter) Close() error { + _ = c.seal() + return os.RemoveAll(c.dir) +} diff --git a/pkg/obikmer/seqquorum_count.go b/pkg/obikmer/seqquorum_count.go new file mode 100644 index 0000000..9af0cd6 --- /dev/null +++ b/pkg/obikmer/seqquorum_count.go @@ -0,0 +1,155 @@ +package obikmer + +import ( + "bufio" + "cmp" + "encoding/binary" + "io" + "os" + "slices" + "sync" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obidefault" +) + +// kmerOcc is one occurrence of a canonical k-mer in a sequence. +type kmerOcc struct { + kmer uint64 + seq uint32 +} + +func cmpOcc(a, b kmerOcc) int { + if c := cmp.Compare(a.kmer, b.kmer); c != 0 { + return c + } + return cmp.Compare(a.seq, b.seq) +} + +// CountAtLeast returns, sorted by k-mer, every canonical k-mer found in at +// least minSeq distinct sequences, with its exact sequence count. +// +// The result is held in memory: it is meant for quorum-filtered sets (the +// conserved k-mers), not for the complete k-mer spectrum. The first call +// seals the counter; AddSequence must not be called afterwards. The count +// phase uses obidefault.ParallelWorkers() goroutines over the partitions. +func (c *SeqKmerCounter) CountAtLeast(minSeq int) ([]SeqKmerCount, error) { + if err := c.seal(); err != nil { + return nil, err + } + if minSeq < 1 { + minSeq = 1 + } + + workers := min(obidefault.ParallelWorkers(), c.p) + jobs := make(chan int) + results := make([][]SeqKmerCount, c.p) + + var wg sync.WaitGroup + var errMu sync.Mutex + var firstErr error + + for range workers { + wg.Add(1) + go func() { + defer wg.Done() + var occ []kmerOcc // reused between partitions + for p := range jobs { + var err error + occ, err = c.loadPartition(p, occ[:0]) + if err == nil { + results[p] = reduceOccurrences(occ, minSeq) + } + if err != nil { + errMu.Lock() + if firstErr == nil { + firstErr = err + } + errMu.Unlock() + } + } + }() + } + for p := range c.p { + jobs <- p + } + close(jobs) + wg.Wait() + if firstErr != nil { + return nil, firstErr + } + + total := 0 + for _, r := range results { + total += len(r) + } + all := make([]SeqKmerCount, 0, total) + for _, r := range results { + all = append(all, r...) + } + slices.SortFunc(all, func(a, b SeqKmerCount) int { return cmp.Compare(a.Kmer, b.Kmer) }) + return all, nil +} + +// loadPartition appends to occ every (canonical k-mer, sequence id) of the +// partition file. +func (c *SeqKmerCounter) loadPartition(p int, occ []kmerOcc) ([]kmerOcc, error) { + f, err := os.Open(c.parts[p].path) + if err != nil { + return occ, err + } + defer f.Close() + r := bufio.NewReaderSize(f, 64*1024) + + var packed, bases []byte + for { + id, err := binary.ReadUvarint(r) + if err == io.EOF { + return occ, nil + } + if err != nil { + return occ, err + } + n, err := binary.ReadUvarint(r) + if err != nil { + return occ, err + } + + nb := (int(n) + 3) / 4 + packed = slices.Grow(packed[:0], nb)[:nb] + if _, err := io.ReadFull(r, packed); err != nil { + return occ, err + } + bases = slices.Grow(bases[:0], int(n))[:n] + unpackBases(bases, packed) + + for kmer := range IterCanonicalKmers(bases, c.k) { + occ = append(occ, kmerOcc{kmer, uint32(id)}) + } + } +} + +// reduceOccurrences sorts occ and returns the k-mers present in at least +// minSeq distinct sequences. occ is reordered in place. +func reduceOccurrences(occ []kmerOcc, minSeq int) []SeqKmerCount { + slices.SortFunc(occ, cmpOcc) + + var out []SeqKmerCount + for i := 0; i < len(occ); { + kmer := occ[i].kmer + var seqs uint32 + repeated := false + j := i + for ; j < len(occ) && occ[j].kmer == kmer; j++ { + if j == i || occ[j].seq != occ[j-1].seq { + seqs++ + } else { + repeated = true + } + } + if int(seqs) >= minSeq { + out = append(out, SeqKmerCount{Kmer: kmer, Sequences: seqs, Repeated: repeated}) + } + i = j + } + return out +} diff --git a/pkg/obikmer/seqquorum_test.go b/pkg/obikmer/seqquorum_test.go new file mode 100644 index 0000000..9d781b6 --- /dev/null +++ b/pkg/obikmer/seqquorum_test.go @@ -0,0 +1,272 @@ +package obikmer + +import ( + "math/rand" + "slices" + "strings" + "sync" + "testing" +) + +type bruteInfo struct { + seqs map[uint32]int // sequence id -> occurrences + repeated bool +} + +// bruteForce is the oracle: every window of k unambiguous bases, circular +// junction included, encoded with EncodeCanonicalKmer. +func bruteForce(seqs [][]byte, circular []bool, k int) map[uint64]*bruteInfo { + res := map[uint64]*bruteInfo{} + for id, s := range seqs { + if len(s) < k { + continue + } + if circular[id] { + s = append(slices.Clone(s), s[:k-1]...) + } + window: + for i := 0; i+k <= len(s); i++ { + w := s[i : i+k] + for _, b := range w { + if !unambiguousBase[b] { + continue window + } + } + kmer := EncodeCanonicalKmer(w, k) + info := res[kmer] + if info == nil { + info = &bruteInfo{seqs: map[uint32]int{}} + res[kmer] = info + } + info.seqs[uint32(id)]++ + if info.seqs[uint32(id)] > 1 { + info.repeated = true + } + } + } + return res +} + +func randomSeqs(rng *rand.Rand, n, minLen, maxLen int, withN bool) [][]byte { + alphabet := "acgtACGT" + if withN { + alphabet = "acgtACGTacgtACGTNnR" + } + // A shared motif and its reverse complement make k-mers common to several + // sequences on both strands. + motif := make([]byte, 40) + for i := range motif { + motif[i] = "ACGT"[rng.Intn(4)] + } + rc := make([]byte, len(motif)) + for i, b := range motif { + rc[len(motif)-1-i] = map[byte]byte{'A': 'T', 'C': 'G', 'G': 'C', 'T': 'A'}[b] + } + + out := make([][]byte, n) + for i := range out { + l := minLen + rng.Intn(maxLen-minLen+1) + s := make([]byte, l) + for j := range s { + s[j] = alphabet[rng.Intn(len(alphabet))] + } + mode := rng.Intn(4) + if l < 60 { // too short to host the motifs + mode = 3 + } + switch mode { + case 0: + copy(s[l/4:], motif) + case 1: + copy(s[l/4:], rc) + case 2: // internal repeat, both strands + copy(s[2:], motif[:20]) + copy(s[l-25:], rc[:20]) + } + out[i] = s + } + return out +} + +func TestSeqKmerCounterAgainstBruteForce(t *testing.T) { + cases := []struct { + name string + k, m, p int + withN bool + circ bool + }{ + {"k7-linear", 7, 3, 8, false, false}, + {"k7-circular-N", 7, 3, 5, true, true}, + {"k18-linear-N", 18, 8, 16, true, false}, + {"k18-circular", 18, 8, 7, false, true}, + {"k31-p1", 31, 10, 1, false, true}, + {"k4-small-m", 4, 1, 3, true, false}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + rng := rand.New(rand.NewSource(int64(tc.k*100 + tc.p))) + seqs := randomSeqs(rng, 30, 5, 400, tc.withN) + circ := make([]bool, len(seqs)) + for i := range circ { + circ[i] = tc.circ && i%3 != 0 + } + + c, err := NewSeqKmerCounter(t.TempDir(), tc.k, tc.m, tc.p) + if err != nil { + t.Fatal(err) + } + defer c.Close() + + // Concurrent insertion, shuffled order, ids not contiguous. + var wg sync.WaitGroup + for i := range seqs { + wg.Add(1) + go func() { + defer wg.Done() + c.AddSequence(uint32(i), seqs[i], circ[i]) + }() + } + wg.Wait() + + want := bruteForce(seqs, circ, tc.k) + for _, q := range []int{1, 2, 3, 10} { + got, err := c.CountAtLeast(q) + if err != nil { + t.Fatal(err) + } + if !slices.IsSortedFunc(got, func(a, b SeqKmerCount) int { + switch { + case a.Kmer < b.Kmer: + return -1 + case a.Kmer > b.Kmer: + return 1 + } + return 0 + }) { + t.Fatal("result is not sorted by k-mer") + } + + n := 0 + for kmer, info := range want { + if len(info.seqs) >= q { + n++ + _ = kmer + } + } + if len(got) != n { + t.Fatalf("q=%d: %d k-mers, want %d", q, len(got), n) + } + for _, g := range got { + info := want[g.Kmer] + if info == nil { + t.Fatalf("q=%d: unexpected k-mer %d", q, g.Kmer) + } + if int(g.Sequences) != len(info.seqs) || g.Repeated != info.repeated { + t.Fatalf("q=%d kmer %d: got (%d,%v) want (%d,%v)", + q, g.Kmer, g.Sequences, g.Repeated, len(info.seqs), info.repeated) + } + } + } + }) + } +} + +// A k-mer appearing on both strands of one sequence counts once but is Repeated. +func TestSeqKmerCounterBothStrandsOneSequence(t *testing.T) { + const k = 8 + fwd := "ACGGTCAT" + rc := "ATGACCGT" + seq := []byte("TTTT" + fwd + "GGGGGGGG" + rc + "CCCC") + + c, err := NewSeqKmerCounter(t.TempDir(), k, 3, 4) + if err != nil { + t.Fatal(err) + } + defer c.Close() + c.AddSequence(7, seq, false) + + got, err := c.CountAtLeast(1) + if err != nil { + t.Fatal(err) + } + target := EncodeCanonicalKmer([]byte(fwd), k) + found := false + for _, g := range got { + if g.Kmer == target { + found = true + if g.Sequences != 1 || !g.Repeated { + t.Errorf("got %+v, want 1 sequence and Repeated", g) + } + } + } + if !found { + t.Fatal("k-mer not found") + } +} + +// Ambiguous bases must never be part of a k-mer, and the circular junction +// must produce the wrap-around k-mers. +func TestSeqKmerCounterAmbiguityAndCircular(t *testing.T) { + const k = 5 + c, err := NewSeqKmerCounter(t.TempDir(), k, 2, 3) + if err != nil { + t.Fatal(err) + } + defer c.Close() + + c.AddSequence(0, []byte("ACGTNACGT"), false) // runs ACGT (=k must fail") + } + if _, err := NewSeqKmerCounter(t.TempDir(), 10, 4, 0); err == nil { + t.Error("zero partitions must fail") + } + c, err := NewSeqKmerCounter(t.TempDir(), 10, 0, 4) // default minimizer + if err != nil { + t.Fatal(err) + } + if err := c.Close(); err != nil { + t.Fatal(err) + } + if err := c.Close(); err != nil { + t.Fatalf("second Close must be harmless: %v", err) + } +} diff --git a/pkg/obikmer/skm_reader.go b/pkg/obikmer/skm_reader.go index 64ef6f9..3246dd4 100644 --- a/pkg/obikmer/skm_reader.go +++ b/pkg/obikmer/skm_reader.go @@ -10,6 +10,16 @@ import ( // decode2bit maps 2-bit codes back to nucleotide bytes. var decode2bit = [4]byte{'a', 'c', 'g', 't'} +// unpackBases decodes the 2-bit packed bases in packed into dst (one lowercase +// nucleotide per byte, len(dst) bases) and returns dst. +func unpackBases(dst, packed []byte) []byte { + for i := range dst { + code := (packed[i/4] >> uint(6-(i%4)*2)) & 0x03 + dst[i] = decode2bit[code] + } + return dst +} + // SkmReader reads super-kmers from a binary .skm file. type SkmReader struct { r *bufio.Reader @@ -46,13 +56,7 @@ func (sr *SkmReader) Next() (SuperKmer, bool) { } // Decode to nucleotide bytes - seq := make([]byte, seqLen) - for i := 0; i < seqLen; i++ { - byteIdx := i / 4 - bitPos := uint(6 - (i%4)*2) - code := (packed[byteIdx] >> bitPos) & 0x03 - seq[i] = decode2bit[code] - } + seq := unpackBases(make([]byte, seqLen), packed) return SuperKmer{ Sequence: seq, diff --git a/pkg/obikmer/skm_writer.go b/pkg/obikmer/skm_writer.go index 1123d2e..cb6dee4 100644 --- a/pkg/obikmer/skm_writer.go +++ b/pkg/obikmer/skm_writer.go @@ -16,8 +16,9 @@ import ( // Nucleotide encoding: A=00, C=01, G=10, T=11. // The last byte is zero-padded on the low bits if len%4 != 0. type SkmWriter struct { - w *bufio.Writer - file *os.File + w *bufio.Writer + file *os.File + scratch []byte } // NewSkmWriter creates a new SkmWriter writing to the given file path. @@ -32,36 +33,36 @@ func NewSkmWriter(path string) (*SkmWriter, error) { }, nil } +// appendPacked appends seq to dst packed 2 bits per base (A=00, C=01, G=10, +// T=11), four bases per byte, the last byte zero-padded on the low bits. +func appendPacked(dst, seq []byte) []byte { + for i := 0; i < len(seq); i += 4 { + var packed byte + for j := 0; j < 4; j++ { + packed <<= 2 + if i+j < len(seq) { + packed |= __single_base_code__[seq[i+j]&31] + } + } + dst = append(dst, packed) + } + return dst +} + // Write encodes a SuperKmer to the .skm file. // The sequence bytes are packed 2 bits per base. func (sw *SkmWriter) Write(sk SuperKmer) error { - seq := sk.Sequence - seqLen := uint16(len(seq)) - // Write length var lenbuf [2]byte - binary.LittleEndian.PutUint16(lenbuf[:], seqLen) + binary.LittleEndian.PutUint16(lenbuf[:], uint16(len(sk.Sequence))) if _, err := sw.w.Write(lenbuf[:]); err != nil { return err } // Encode and write packed sequence (2 bits/base) - nBytes := (int(seqLen) + 3) / 4 - for i := 0; i < nBytes; i++ { - var packed byte - for j := 0; j < 4; j++ { - pos := i*4 + j - packed <<= 2 - if pos < int(seqLen) { - packed |= __single_base_code__[seq[pos]&31] - } - } - if err := sw.w.WriteByte(packed); err != nil { - return err - } - } - - return nil + sw.scratch = appendPacked(sw.scratch[:0], sk.Sequence) + _, err := sw.w.Write(sw.scratch) + return err } // Close flushes buffered data and closes the underlying file. From 56f5fe33d726b493a6e0bcfdbe095068421db5f1 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Tue, 3 Nov 2026 14:18:39 +1100 Subject: [PATCH 12/24] Implement strict word finding and sequence filtering This introduces new structures for defining sequence roles and constraints, core logic for strict word identification, and comprehensive unit tests to ensure accuracy and correct behavior. --- OBIPRIMERS_NOTES.md | 38 ++++++ pkg/obiprimer/roles.go | 76 ++++++++++++ pkg/obiprimer/words.go | 118 +++++++++++++++++++ pkg/obiprimer/words_test.go | 223 ++++++++++++++++++++++++++++++++++++ 4 files changed, 455 insertions(+) create mode 100644 pkg/obiprimer/roles.go create mode 100644 pkg/obiprimer/words.go create mode 100644 pkg/obiprimer/words_test.go diff --git a/OBIPRIMERS_NOTES.md b/OBIPRIMERS_NOTES.md index a1d88a0..450a692 100644 --- a/OBIPRIMERS_NOTES.md +++ b/OBIPRIMERS_NOTES.md @@ -211,3 +211,41 @@ Un bug trouvé par ces tests dans ma boucle de réduction (compteur de séquence Contrôle de bout en bout prévu (étape suivante) : `Strict primer count` d'ecoPrimers = **1 208** (Cyprinidae) et **255** (Teleostei) avec `-e 3 -3 2 -l 30 -L 150 -c`, k = 18, quorum 0,7 ; le calcul d'ecoPrimers est `floor(|E| x 0,7)` séquences (> longueur d'amorce). + +## 10. Mode A, étape 2b : mots stricts de bout en bout (2026-11-03) + +Code : `pkg/obiprimer/roles.go` (`Role`, `Selection`, `BelongingTo`) et `words.go` (`WordSearch`, +`StrictWords`, `FindStrictWords`, `MinSequences`). Tests : `words_test.go`. + +- `Selection` reproduit `-r` (restrict), `-i` (ignore) et `-E` (exception) d'ecoPrimers, vérifié sur les + quatre cas du tableau de la section 4 : exemple = dans Restrict et hors Except ; sinon contre-exemple si + dans Ignore ou Except ; sinon ignoré. Prédicats construits avec `obiseq.IsSubCladeOf` (existant). +- `FindStrictWords` lit les séquences une fois (consommateurs parallèles via `Split()`), indexe les + exemples dans un `SeqKmerCounter`, puis `CountAtLeast(floor(exemples x quorum))`. Quorum calculé avec + une petite tolérance (10 x 0,7 = 7) au lieu de la multiplication en float32 du C. + +**Contrôle contre ecoPrimers** (test opt-in : `OBIPRIMERS_TUTORIAL= go test ./pkg/obiprimer/`) : + +| Jeu | Exemples | Quorum | Mots obiprimers | Mots ecoPrimers | +|---|---|---|---|---| +| Teleostei | 3 909 | 2 736 | **255** | 255 (identique) | +| Cyprinidae | 325 | 227 | **1 207** | 1 208 | + +Écart de Cyprinidae : un mot en plus chez ecoPrimers, `TTTATGAAAACAAATAAA`. Comparaison des deux listes +complètes (le journal d'ecoPrimers imprime ses 1 208 mots) : tous les autres mots sont identiques. Un +comptage par force brute (deux brins, jonction circulaire) trouve ce mot dans **225** séquences sur 325, +sous le quorum de 227 : ecoPrimers le garde à tort. Le mécanisme exact n'est pas établi (`merge.c` élague +avec `remainingSeq + count >= quorum` pendant une fusion en place avec file d'attente ; non investigué +plus loin). Impact : ce mot n'apparaît dans **aucune** des 3 448 paires finales de +`Cyprinidae.ecoprimers`. Le test fige 1 207 avec cette explication. + +Mesure : étape « mots stricts » seule, 0,5 s (Cyprinidae) et 1,1 s (Teleostei) sur cette machine, lecture +de la taxonomie et du FASTA non comprise ; ecoPrimers complet : 134 s et 350 s (non décomposé par étape). + +**Bug existant repéré hors périmètre** : `go test -race` signale une course dans +`obiutils/pipe.go` (`globalLockerCounter` est un `int` incrémenté et décrémenté depuis plusieurs +goroutines, utilisé seulement pour des `log.Debugln`). Sans rapport avec obiprimers ; un `atomic.Int64` +suffirait. Non modifié (code partagé) : à décider. + +Suite : étape 3 (recherche approchée des mots stricts, <= e substitutions, deux brins, dans exemples +et contre-exemples), puis paires et indices. diff --git a/pkg/obiprimer/roles.go b/pkg/obiprimer/roles.go new file mode 100644 index 0000000..607399b --- /dev/null +++ b/pkg/obiprimer/roles.go @@ -0,0 +1,76 @@ +// Package obiprimer implements the core of obiprimers, the OBITools4 +// re-implementation of ecoPrimers: finding conserved words usable as primers +// in a set of example sequences, and the primer pairs they form. +package obiprimer + +import ( + "fmt" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obitax" +) + +// Role tells how a sequence takes part in the primer search. +type Role uint8 + +const ( + // Ignored sequences are not used at all. + Ignored Role = iota + // Example sequences are those the primers must amplify. + Example + // CounterExample sequences are those the primers must not amplify. + CounterExample +) + +// Selection defines the roles from the taxonomy, with the ecoPrimers +// semantics of -r (restrict), -i (ignore) and -E (exception): +// +// - a sequence is an Example when it belongs to Restrict (all sequences when +// Restrict is nil) and not to Except; +// - otherwise it is a CounterExample when it belongs to Ignore or to Except; +// - otherwise it is Ignored. +// +// Nil predicates mean "no such constraint". +type Selection struct { + Restrict obiseq.SequencePredicate + Ignore obiseq.SequencePredicate + Except obiseq.SequencePredicate +} + +// Role returns the role of seq. +func (s Selection) Role(seq *obiseq.BioSequence) Role { + inRestrict := s.Restrict == nil || s.Restrict(seq) + inExcept := s.Except != nil && s.Except(seq) + + switch { + case inRestrict && !inExcept: + return Example + case inExcept, s.Ignore != nil && s.Ignore(seq): + return CounterExample + default: + return Ignored + } +} + +// BelongingTo returns a predicate true for the sequences whose taxon is a +// sub-clade of at least one of the given taxids. It returns nil (no +// constraint) when taxids is empty. +func BelongingTo(taxonomy *obitax.Taxonomy, taxids []string) (obiseq.SequencePredicate, error) { + var pred obiseq.SequencePredicate + for _, id := range taxids { + taxon, _, err := taxonomy.Taxon(id) + if err != nil { + return nil, fmt.Errorf("obiprimer: taxid %q: %w", id, err) + } + if taxon == nil { + return nil, fmt.Errorf("obiprimer: taxid %q is unknown from the taxonomy", id) + } + p := obiseq.IsSubCladeOf(taxonomy, taxon) + if pred == nil { + pred = p + } else { + pred = pred.Or(p) + } + } + return pred, nil +} diff --git a/pkg/obiprimer/words.go b/pkg/obiprimer/words.go new file mode 100644 index 0000000..c154e4d --- /dev/null +++ b/pkg/obiprimer/words.go @@ -0,0 +1,118 @@ +package obiprimer + +import ( + "fmt" + "math" + "sync" + "sync/atomic" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obidefault" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiiter" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obikmer" +) + +// WordSearch holds the parameters of the strict word search. +type WordSearch struct { + PrimerLength int // word size (ecoPrimers -O, 18 by default; 2..31) + Quorum float64 // fraction of example sequences that must contain the word (ecoPrimers -q, 0.7) + Circular bool // sequences are circular (ecoPrimers -c) + Partitions int // partition files of the counter (default 256) + TmpDir string // parent of the temporary directory ("" = system default) +} + +// Counts gives the number of sequences by role. +type Counts struct { + Examples int + CounterExamples int + Ignored int +} + +// StrictWords is the result of the strict word search: the canonical words of +// PrimerLength bases present, without any error, in at least MinSequences +// example sequences. +type StrictWords struct { + PrimerLength int + MinSequences int // quorum in number of example sequences + Words []obikmer.SeqKmerCount + Counts Counts +} + +// MinSequences returns the quorum in sequences: floor(examples * quorum). +// The tiny epsilon makes exact products (10 * 0.7 = 7) robust to the binary +// representation of the fraction. +func MinSequences(examples int, quorum float64) int { + return int(math.Floor(float64(examples)*quorum + 1e-9)) +} + +// FindStrictWords reads every sequence of seqs once, indexes the example +// sequences, and returns the words reaching the quorum. Sequences no longer +// than the word size are counted as examples but cannot contain any word, as +// in ecoPrimers. Words are canonical (the smaller of the word and its reverse +// complement), so both strands are searched. +func FindStrictWords(seqs obiiter.IBioSequence, sel Selection, cfg WordSearch) (*StrictWords, error) { + if cfg.PrimerLength < 2 || cfg.PrimerLength > 31 { + return nil, fmt.Errorf("obiprimer: primer length must be between 2 and 31, got %d", cfg.PrimerLength) + } + if cfg.Quorum <= 0 || cfg.Quorum > 1 { + return nil, fmt.Errorf("obiprimer: quorum must be in ]0,1], got %g", cfg.Quorum) + } + if cfg.Partitions <= 0 { + cfg.Partitions = 256 + } + + counter, err := obikmer.NewSeqKmerCounter(cfg.TmpDir, cfg.PrimerLength, 0, cfg.Partitions) + if err != nil { + return nil, err + } + defer counter.Close() + + var nextID atomic.Uint32 + var examples, counters, ignored atomic.Int64 + + consumer := func(it obiiter.IBioSequence, wg *sync.WaitGroup) { + defer wg.Done() + for it.Next() { + for _, seq := range it.Get().Slice() { + switch sel.Role(seq) { + case Example: + examples.Add(1) + if seq.Len() > cfg.PrimerLength { + counter.AddSequence(nextID.Add(1)-1, seq.Sequence(), cfg.Circular) + } + case CounterExample: + counters.Add(1) + default: + ignored.Add(1) + } + } + } + } + + var wg sync.WaitGroup + for i := 1; i < obidefault.ParallelWorkers(); i++ { + wg.Add(1) + go consumer(seqs.Split(), &wg) + } + wg.Add(1) + go consumer(seqs, &wg) + wg.Wait() + + res := &StrictWords{ + PrimerLength: cfg.PrimerLength, + Counts: Counts{ + Examples: int(examples.Load()), + CounterExamples: int(counters.Load()), + Ignored: int(ignored.Load()), + }, + } + res.MinSequences = MinSequences(res.Counts.Examples, cfg.Quorum) + if res.Counts.Examples == 0 { + return nil, fmt.Errorf("obiprimer: no example sequence selected") + } + + res.Words, err = counter.CountAtLeast(max(res.MinSequences, 1)) + if err != nil { + return nil, err + } + return res, nil +} diff --git a/pkg/obiprimer/words_test.go b/pkg/obiprimer/words_test.go new file mode 100644 index 0000000..1fbdb0d --- /dev/null +++ b/pkg/obiprimer/words_test.go @@ -0,0 +1,223 @@ +package obiprimer + +import ( + "math/rand" + "os" + "path/filepath" + "strings" + "testing" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiformats" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiiter" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obikmer" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq" +) + +func idPrefix(prefix string) obiseq.SequencePredicate { + return func(s *obiseq.BioSequence) bool { return strings.HasPrefix(s.Id(), prefix) } +} + +func TestSelectionRoles(t *testing.T) { + seq := func(id string) *obiseq.BioSequence { return obiseq.NewBioSequence(id, []byte("acgt"), "") } + // ids: "r" restricted clade, "x" exception inside it, "i" ignored clade, "o" other + cases := []struct { + name string + sel Selection + want map[string]Role + }{ + {"nothing set: everything is an example", + Selection{}, map[string]Role{"r1": Example, "x1": Example, "i1": Example, "o1": Example}}, + {"restrict only: the rest is dropped", + Selection{Restrict: idPrefix("r")}, + map[string]Role{"r1": Example, "x1": Ignored, "i1": Ignored, "o1": Ignored}}, + {"restrict + ignore", + Selection{Restrict: idPrefix("r"), Ignore: idPrefix("i")}, + map[string]Role{"r1": Example, "i1": CounterExample, "o1": Ignored}}, + {"exception inside restrict becomes a counterexample", + Selection{Restrict: idPrefix("r").Or(idPrefix("x")), Except: idPrefix("x")}, + map[string]Role{"r1": Example, "x1": CounterExample, "o1": Ignored}}, + {"example wins over ignore when both match", + Selection{Restrict: idPrefix("r"), Ignore: idPrefix("r").Or(idPrefix("i"))}, + map[string]Role{"r1": Example, "i1": CounterExample, "o1": Ignored}}, + } + for _, tc := range cases { + for id, want := range tc.want { + if got := tc.sel.Role(seq(id)); got != want { + t.Errorf("%s: %s: role %d, want %d", tc.name, id, got, want) + } + } + } +} + +func TestMinSequences(t *testing.T) { + for _, tc := range []struct { + n int + q float64 + want int + }{{10, 0.7, 7}, {3909, 0.7, 2736}, {325, 0.7, 227}, {100, 0.9, 90}, {7, 0.9, 6}, {1, 1, 1}} { + if got := MinSequences(tc.n, tc.q); got != tc.want { + t.Errorf("MinSequences(%d,%g) = %d, want %d", tc.n, tc.q, got, tc.want) + } + } +} + +func revcomp(s string) string { + c := map[byte]byte{'A': 'T', 'C': 'G', 'G': 'C', 'T': 'A'} + out := make([]byte, len(s)) + for i := range s { + out[len(s)-1-i] = c[s[i]] + } + return string(out) +} + +func randDNA(rng *rand.Rand, n int) string { + b := make([]byte, n) + for i := range b { + b[i] = "ACGT"[rng.Intn(4)] + } + return string(b) +} + +func TestFindStrictWords(t *testing.T) { + const k = 18 + rng := rand.New(rand.NewSource(1)) + motif := randDNA(rng, 30) + + var data obiseq.BioSequenceSlice + add := func(id, s string) { data = append(data, obiseq.NewBioSequence(id, []byte(s), "")) } + + // 10 examples; the motif is in 8 of them, 3 on the reverse strand. One + // example carries it twice. Another has an ambiguity breaking it. + for i := 0; i < 8; i++ { + m := motif + if i%3 == 0 { + m = revcomp(motif) + } + s := randDNA(rng, 120) + m + randDNA(rng, 120) + if i == 5 { + s += randDNA(rng, 50) + motif + } + add("ex"+string(rune('a'+i)), s) + } + broken := motif[:12] + "N" + motif[13:] + add("exh", randDNA(rng, 100)+broken+randDNA(rng, 100)) + add("exi", randDNA(rng, 300)) + // counterexamples and ignored sequences must not count + add("cx1", randDNA(rng, 50)+motif) + add("zz1", randDNA(rng, 50)+motif) + + sel := Selection{Restrict: idPrefix("ex").Or(idPrefix("cx")), Ignore: idPrefix("cx"), Except: idPrefix("cx")} + res, err := FindStrictWords(obiiter.IBatchOver("t", data, 3), sel, + WordSearch{PrimerLength: k, Quorum: 0.7, Partitions: 8, TmpDir: t.TempDir()}) + if err != nil { + t.Fatal(err) + } + + if res.Counts != (Counts{Examples: 10, CounterExamples: 1, Ignored: 1}) { + t.Fatalf("counts = %+v", res.Counts) + } + if res.MinSequences != 7 { + t.Fatalf("quorum = %d sequences, want 7", res.MinSequences) + } + + want := map[uint64]bool{} + for i := 0; i+k <= len(motif); i++ { + want[obikmer.EncodeCanonicalKmer([]byte(motif[i:i+k]), k)] = true + } + // the N breaks the windows that cover it, but 8 sequences still carry all of them + if len(res.Words) != len(want) { + t.Fatalf("%d words, want %d (the motif windows)", len(res.Words), len(want)) + } + for _, w := range res.Words { + if !want[w.Kmer] { + t.Errorf("unexpected word %d", w.Kmer) + } + if w.Sequences != 8 { + t.Errorf("word %d in %d sequences, want 8", w.Kmer, w.Sequences) + } + } + repeated := 0 + for _, w := range res.Words { + if w.Repeated { + repeated++ + } + } + if repeated != len(want) { + t.Errorf("%d words flagged Repeated, want all %d (motif twice in one sequence)", repeated, len(want)) + } +} + +func TestFindStrictWordsParameters(t *testing.T) { + it := func() obiiter.IBioSequence { + return obiiter.IBatchOver("t", obiseq.BioSequenceSlice{obiseq.NewBioSequence("a", []byte("acgtacgtacgtacgtacgtacgt"), "")}, 1) + } + if _, err := FindStrictWords(it(), Selection{}, WordSearch{PrimerLength: 1, Quorum: 0.7}); err == nil { + t.Error("primer length 1 must fail") + } + if _, err := FindStrictWords(it(), Selection{}, WordSearch{PrimerLength: 18, Quorum: 0}); err == nil { + t.Error("quorum 0 must fail") + } + if _, err := FindStrictWords(it(), Selection{Restrict: idPrefix("zz")}, + WordSearch{PrimerLength: 18, Quorum: 0.7}); err == nil { + t.Error("no example must fail") + } +} + +// Opt-in end-to-end check against ecoPrimers on the "Designing fish primers" +// tutorial data. Set OBIPRIMERS_TUTORIAL to the directory holding +// ncbitaxo_20250211.tgz and mito.vert.fasta.gz. +// +// ecoPrimers (-e 3 -3 2 -l 30 -L 150 -c, k=18, strict quorum 0.7) reports +// "Strict primer count": 1208 for Cyprinidae (-r 7953, 325 examples) and 255 +// for Teleostei (-r 32443, 3909 examples). +// +// Teleostei matches exactly. For Cyprinidae we find 1207: ecoPrimers keeps +// TTTATGAAAACAAATAAA, which a brute-force scan (both strands, circular +// junction) finds in only 225 of the 325 examples, below its own quorum of +// 227. That word is in none of the 3448 pairs ecoPrimers finally retains, so +// the divergence has no effect on the reference results. +func TestStrictWordsAgainstEcoPrimers(t *testing.T) { + dir := os.Getenv("OBIPRIMERS_TUTORIAL") + if dir == "" { + t.Skip("OBIPRIMERS_TUTORIAL not set") + } + + taxonomy, err := obiformats.LoadTaxonomy(filepath.Join(dir, "ncbitaxo_20250211.tgz"), false, false) + if err != nil { + t.Fatal(err) + } + + for _, tc := range []struct { + name string + taxid string + examples int + words int + }{ + {"Cyprinidae", "7953", 325, 1207}, // ecoPrimers: 1208, see above + {"Teleostei", "32443", 3909, 255}, + } { + t.Run(tc.name, func(t *testing.T) { + restrict, err := BelongingTo(taxonomy, []string{tc.taxid}) + if err != nil { + t.Fatal(err) + } + // The predicates read the taxon through the taxid annotation. + seqs, err := obiformats.ReadSequencesFromFile(filepath.Join(dir, "mito.vert.fasta.gz")) + if err != nil { + t.Fatal(err) + } + res, err := FindStrictWords(seqs, Selection{Restrict: restrict}, + WordSearch{PrimerLength: 18, Quorum: 0.7, Circular: true, TmpDir: t.TempDir()}) + if err != nil { + t.Fatal(err) + } + t.Logf("%+v min=%d words=%d", res.Counts, res.MinSequences, len(res.Words)) + if res.Counts.Examples != tc.examples { + t.Errorf("%d examples, ecoPrimers reports %d", res.Counts.Examples, tc.examples) + } + if len(res.Words) != tc.words { + t.Errorf("%d strict words, ecoPrimers reports %d", len(res.Words), tc.words) + } + }) + } +} From f75e774bd529cf8446e265a091a164c63f1b4ced Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Tue, 3 Nov 2026 14:21:55 +1100 Subject: [PATCH 13/24] Fix: Prevent race condition during pipe registration This change introduces thread-safe pipe registration and unregistration functions using `sync.WaitGroup` and `atomic.Int64` to manage the count of registered pipes, addressing a race condition identified by the race detector. --- OBIPRIMERS_NOTES.md | 3 ++- pkg/obiutils/pipe.go | 11 ++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/OBIPRIMERS_NOTES.md b/OBIPRIMERS_NOTES.md index 450a692..77e1862 100644 --- a/OBIPRIMERS_NOTES.md +++ b/OBIPRIMERS_NOTES.md @@ -245,7 +245,8 @@ de la taxonomie et du FASTA non comprise ; ecoPrimers complet : 134 s et 350 s ( **Bug existant repéré hors périmètre** : `go test -race` signale une course dans `obiutils/pipe.go` (`globalLockerCounter` est un `int` incrémenté et décrémenté depuis plusieurs goroutines, utilisé seulement pour des `log.Debugln`). Sans rapport avec obiprimers ; un `atomic.Int64` -suffirait. Non modifié (code partagé) : à décider. +suffisait. **Corrigé** (accord de l'utilisateur) : `globalLockerCounter` est un `atomic.Int64` dans +`pkg/obiutils/pipe.go` ; le test de bout en bout passe désormais sous `-race`. Suite : étape 3 (recherche approchée des mots stricts, <= e substitutions, deux brins, dans exemples et contre-exemples), puis paires et indices. diff --git a/pkg/obiutils/pipe.go b/pkg/obiutils/pipe.go index fef4447..344e161 100644 --- a/pkg/obiutils/pipe.go +++ b/pkg/obiutils/pipe.go @@ -2,12 +2,13 @@ package obiutils import ( "sync" + "sync/atomic" log "github.com/sirupsen/logrus" ) var globalLocker sync.WaitGroup -var globalLockerCounter = 0 +var globalLockerCounter atomic.Int64 // RegisterAPipe increments the global lock counter and adds a new pipe to the global wait group. // @@ -15,8 +16,8 @@ var globalLockerCounter = 0 // No return values. func RegisterAPipe() { globalLocker.Add(1) - globalLockerCounter++ - log.Debugln(globalLockerCounter, " Pipes are registered now") + n := globalLockerCounter.Add(1) + log.Debugln(n, " Pipes are registered now") } // UnregisterPipe decrements the global lock counter and signals that a pipe has finished. @@ -25,8 +26,8 @@ func RegisterAPipe() { // No return values. func UnregisterPipe() { globalLocker.Done() - globalLockerCounter-- - log.Debugln(globalLockerCounter, "are still registered") + n := globalLockerCounter.Add(-1) + log.Debugln(n, "are still registered") } // WaitForLastPipe waits until all registered pipes have finished. From ad065f68532432a9f104adaefac42194ce4b8355 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Tue, 3 Nov 2026 14:41:30 +1100 Subject: [PATCH 14/24] Introduce new sequence matching logic and corpus handling This commit introduces the core matching functionality, including parallel corpus loading, the `Corpus` data structure, and implementations for `matchNaive` and `matchApat`. It also defines new matching rules, variant handling, and comprehensive unit and integration tests to validate the new logic and parameter handling. --- OBIPRIMERS_NOTES.md | 51 + pkg/obiprimer/corpus.go | 108 ++ pkg/obiprimer/match.go | 156 +++ pkg/obiprimer/match_apat.go | 87 ++ pkg/obiprimer/match_naive.go | 78 ++ pkg/obiprimer/match_test.go | 285 ++++ .../testdata/ecoprimers_Cyprinidae.words.tsv | 1208 +++++++++++++++++ .../testdata/ecoprimers_Teleostei.words.tsv | 255 ++++ pkg/obiprimer/words.go | 72 +- pkg/obiprimer/words_test.go | 24 +- 10 files changed, 2265 insertions(+), 59 deletions(-) create mode 100644 pkg/obiprimer/corpus.go create mode 100644 pkg/obiprimer/match.go create mode 100644 pkg/obiprimer/match_apat.go create mode 100644 pkg/obiprimer/match_naive.go create mode 100644 pkg/obiprimer/match_test.go create mode 100644 pkg/obiprimer/testdata/ecoprimers_Cyprinidae.words.tsv create mode 100644 pkg/obiprimer/testdata/ecoprimers_Teleostei.words.tsv diff --git a/OBIPRIMERS_NOTES.md b/OBIPRIMERS_NOTES.md index 77e1862..e335cc3 100644 --- a/OBIPRIMERS_NOTES.md +++ b/OBIPRIMERS_NOTES.md @@ -250,3 +250,54 @@ suffisait. **Corrigé** (accord de l'utilisateur) : `globalLockerCounter` est un Suite : étape 3 (recherche approchée des mots stricts, <= e substitutions, deux brins, dans exemples et contre-exemples), puis paires et indices. + +## 11. Mode A, étape 3 : recherche approchée (2026-11-03) + +Code : `pkg/obiprimer/corpus.go` (`Corpus`, `LoadCorpus` : exemples + contre-exemples en mémoire, ordre +d'entrée conservé, ignorés comptés seulement, comme ecoPrimers), `match.go` (types `Match`, +`WordMatches`, `MatchParams`, harnais parallèle `matchCorpus` indépendant du nombre de workers), +`match_naive.go` (oracle en Go pur), `match_apat.go` (variante `obiapat`). `FindStrictWords` travaille +maintenant sur un `Corpus`. + +**Règle d'appariement** (celle d'ecoPrimers, figée dans `MatchParams`) : substitutions seules ; toute base +hors A, C, G, T (U lu comme T) dans la séquence compte comme une erreur ; toutes les fenêtres à <= e +erreurs sont rapportées, chevauchantes comprises ; fenêtres à cheval sur la jonction si circulaire. +**`obiapat` ne suit pas cette règle de lui-même** : il lit les codes IUPAC dans la séquence (un N +correspond à toutes les bases). La variante `obiapat` remplace donc les symboles ambigus par `z` (rien +ne lui correspond), sur une copie, seulement pour les séquences qui en contiennent. + +Vérification de justesse : +- oracle validé à la main sur 12 cas (brins, N, IUPAC, U, jonction, chevauchement, séquence trop courte) ; +- `obiapat` == oracle position par position et nombre d'erreurs compris, 6 configurations (k = 12, 18, + 25 ; e = 0 à 4 ; linéaire/circulaire ; séquences avec n, r, y, k, m ; mots plantés sur les deux brins + avec 0 à 3 substitutions et à la jonction) ; +- **bout en bout** : pour chaque mot strict, nombre de séquences exemples touchées == celui que + ecoPrimers a journalisé (`e = 3`) : **1 207 mots Cyprinidae et 255 mots Teleostei, 0 différence**. + Les nombres d'ecoPrimers sont dans `testdata/ecoprimers_*.words.tsv` ; test opt-in + `TestMatchApatAgainstEcoPrimers` (`OBIPRIMERS_TUTORIAL`, `OBIPRIMERS_WORKERS=n` pour fixer les cÅ“urs). + (Cyprinidae : 1 207 des 1 208 mots d'ecoPrimers, le mot en trop de la section 10 est exclu.) + +Mesures, `e = 3`, génomes circulaires, M3 Max : + +| Jeu | Mots x séquences | Matches | 16 workers | 1 worker | Mémoire ajoutée | +|---|---|---|---|---|---| +| Cyprinidae | 1 207 x 325 (5,4 Mb) | 416 386 | 3,8 s | non mesuré | 53 Mo | +| Teleostei | 255 x 3 909 (65 Mb) | 999 407 | 10,7 s | **113 s** (293 Mb/s par passe mot-brin) | 151 Mo | + +ecoPrimers complet (non décomposé par étape) : 134 s et 350 s, 1,9 et 2,1 Go. + +Profil monothread (Teleostei, `go tool pprof`) : 74 % dans `runtime.cgocall` (le balayage C), 11 % en +surcoût de transition cgo (`pthread_cond_signal`), tri/GC marginaux. Il y a 2 millions d'appels cgo +(255 mots x 2 brins x 3 909 séquences). + +Variantes du protocole : +1. `obiapat` mot par mot, parallélisé par séquence : **retenue comme référence de production** pour l'instant. +2. Arrêt anticipé sous le quorum de 90 % : **écartée, mesuré inutile** (Teleostei : 255/255 mots + conservés ; Cyprinidae : 1 201/1 208). Le parallélisme par séquence est déjà dans la variante 1. +3. Bit-parallèle multi-mots en Go (SIMD) : **non écrite**. Cible : les ~74 % de balayage C. +4. Index de voisinage / Aho-Corasick : écartée pour e >= 2 par le calcul (graines de 4 à 5 bases, + ~250 000 candidats par graine et par mot sur 65 Mb) ; `obicorazick` reste pertinent pour e = 0. + Non mesurée. + +Décision en suspens : écrire la variante 3 maintenant, ou d'abord finir le pipeline (paires et indices) +et profiler l'ensemble pour savoir si l'appariement domine encore. diff --git a/pkg/obiprimer/corpus.go b/pkg/obiprimer/corpus.go new file mode 100644 index 0000000..0796dd2 --- /dev/null +++ b/pkg/obiprimer/corpus.go @@ -0,0 +1,108 @@ +package obiprimer + +import ( + "cmp" + "fmt" + "slices" + "sync" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obidefault" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiiter" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq" +) + +// Counts gives the number of sequences by role. +type Counts struct { + Examples int + CounterExamples int + Ignored int +} + +// Corpus is the set of sequences used by the primer search: the examples and +// the counterexamples, in input order. Sequences with the Ignored role are not +// kept (as in ecoPrimers), only counted. +// +// The primer search needs several passes over the sequences (words are +// searched approximately in every one of them), so the corpus lives in memory. +type Corpus struct { + Seqs []*obiseq.BioSequence + Roles []Role // Roles[i] is Example or CounterExample + Ignored int // number of sequences dropped by the selection +} + +// Counts returns the number of sequences by role. +func (c *Corpus) Counts() Counts { + n := Counts{Ignored: c.Ignored} + for _, r := range c.Roles { + if r == Example { + n.Examples++ + } else { + n.CounterExamples++ + } + } + return n +} + +type corpusEntry struct { + batch, index int + seq *obiseq.BioSequence + role Role +} + +// LoadCorpus reads every sequence of seqs, once, and keeps those selected as +// examples or counterexamples. The order of the input is preserved whatever +// the number of reader goroutines. +func LoadCorpus(seqs obiiter.IBioSequence, sel Selection) (*Corpus, error) { + var mu sync.Mutex + var entries []corpusEntry + ignored := 0 + + consumer := func(it obiiter.IBioSequence, wg *sync.WaitGroup) { + defer wg.Done() + var local []corpusEntry + localIgnored := 0 + for it.Next() { + batch := it.Get() + for i, seq := range batch.Slice() { + if role := sel.Role(seq); role == Ignored { + localIgnored++ + } else { + local = append(local, corpusEntry{batch.Order(), i, seq, role}) + } + } + } + mu.Lock() + entries = append(entries, local...) + ignored += localIgnored + mu.Unlock() + } + + var wg sync.WaitGroup + for i := 1; i < obidefault.ParallelWorkers(); i++ { + wg.Add(1) + go consumer(seqs.Split(), &wg) + } + wg.Add(1) + go consumer(seqs, &wg) + wg.Wait() + + slices.SortFunc(entries, func(a, b corpusEntry) int { + if c := cmp.Compare(a.batch, b.batch); c != 0 { + return c + } + return cmp.Compare(a.index, b.index) + }) + + c := &Corpus{ + Seqs: make([]*obiseq.BioSequence, len(entries)), + Roles: make([]Role, len(entries)), + Ignored: ignored, + } + for i, e := range entries { + c.Seqs[i], c.Roles[i] = e.seq, e.role + } + if c.Counts().Examples == 0 { + return nil, fmt.Errorf("obiprimer: no example sequence selected") + } + return c, nil +} diff --git a/pkg/obiprimer/match.go b/pkg/obiprimer/match.go new file mode 100644 index 0000000..9a14f72 --- /dev/null +++ b/pkg/obiprimer/match.go @@ -0,0 +1,156 @@ +package obiprimer + +import ( + "cmp" + "fmt" + "slices" + "sync" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obidefault" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obikmer" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq" +) + +// Match is one approximate occurrence of a word in a corpus sequence. +// +// Pos is the 0-based start of the window on the sequence as given (forward +// coordinates). When Reverse is false the window reads like the word; when it +// is true the window reads like the reverse complement of the word, i.e. the +// word occurs on the other strand. Errors is the number of substitutions. +type Match struct { + Seq uint32 // index in the Corpus + Pos uint32 + Errors uint8 + Reverse bool +} + +// WordMatches gathers every occurrence of one canonical word, sorted by +// (Seq, Pos, Reverse). +type WordMatches struct { + Word uint64 + Matches []Match +} + +// MatchParams parameterizes the approximate search. +// +// Rules, those of ecoPrimers: only substitutions are allowed; a base other +// than A, C, G, T (or U, read as T) in the sequence never matches, it counts as +// one error; every window with at most MaxErrors errors is reported, windows +// overlapping each other included; on circular sequences windows may span the +// junction. +type MatchParams struct { + K int + MaxErrors int + Circular bool +} + +func (p MatchParams) validate() error { + if p.K < 2 || p.K > 31 { + return fmt.Errorf("obiprimer: word size must be between 2 and 31, got %d", p.K) + } + if p.MaxErrors < 0 || p.MaxErrors >= p.K { + return fmt.Errorf("obiprimer: error count must be in [0,%d[, got %d", p.K, p.MaxErrors) + } + return nil +} + +// hit is a Match before its sequence index is known to be final. +type hit struct { + word int + m Match +} + +// scanFunc finds every occurrence of every word, on both strands, in one +// sequence and reports them through emit. +type scanFunc func(seq *obiseq.BioSequence, emit func(word, pos, errors int, reverse bool)) + +// scannerMaker returns an independent scanFunc; each worker gets its own so +// that scanners can keep private buffers. +type scannerMaker func() scanFunc + +// wordStrings returns, for each canonical word, the word and its reverse +// complement as lowercase strings. +func wordStrings(words []uint64, k int) (fwd, rev []string) { + fwd = make([]string, len(words)) + rev = make([]string, len(words)) + for i, w := range words { + fwd[i] = string(obikmer.DecodeKmer(w, k, nil)) + rev[i] = string(obikmer.DecodeKmer(obikmer.ReverseComplement(w, k), k, nil)) + } + return +} + +// matchCorpus runs the scanners over every sequence of the corpus in parallel +// and assembles the per-word results. The output does not depend on the number +// of workers. +func matchCorpus(c *Corpus, words []uint64, mk scannerMaker) []WordMatches { + perSeq := make([][]hit, len(c.Seqs)) + + jobs := make(chan int) + var wg sync.WaitGroup + for range obidefault.ParallelWorkers() { + wg.Add(1) + go func() { + defer wg.Done() + scan := mk() + for i := range jobs { + var hits []hit + scan(c.Seqs[i], func(word, pos, errors int, reverse bool) { + hits = append(hits, hit{word, Match{uint32(i), uint32(pos), uint8(errors), reverse}}) + }) + perSeq[i] = hits + } + }() + } + for i := range c.Seqs { + jobs <- i + } + close(jobs) + wg.Wait() + + res := make([]WordMatches, len(words)) + for i, w := range words { + res[i].Word = w + } + for _, hits := range perSeq { + for _, h := range hits { + res[h.word].Matches = append(res[h.word].Matches, h.m) + } + } + for i := range res { + slices.SortFunc(res[i].Matches, func(a, b Match) int { + if c := cmp.Compare(a.Seq, b.Seq); c != 0 { + return c + } + if c := cmp.Compare(a.Pos, b.Pos); c != 0 { + return c + } + if a.Reverse != b.Reverse { + if !a.Reverse { + return -1 + } + return 1 + } + return 0 + }) + } + return res +} + +// SequencesMatched returns, for the matches of one word, the number of +// distinct example and counterexample sequences it occurs in. +func (w WordMatches) SequencesMatched(c *Corpus) (examples, counterExamples int) { + last := ^uint32(0) + for _, m := range w.Matches { + if m.Seq == last { + continue + } + last = m.Seq + if c.Roles[m.Seq] == Example { + examples++ + } else { + counterExamples++ + } + } + return +} diff --git a/pkg/obiprimer/match_apat.go b/pkg/obiprimer/match_apat.go new file mode 100644 index 0000000..d96e1dc --- /dev/null +++ b/pkg/obiprimer/match_apat.go @@ -0,0 +1,87 @@ +package obiprimer + +import ( + "fmt" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiapat" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq" +) + +// apatSafe returns seq when it only holds a, c, g, t; otherwise a copy where u +// becomes t and every other symbol becomes z. obiapat reads IUPAC codes in the +// sequence (an n matches every base); z is the symbol it matches with nothing, +// which gives back the ecoPrimers rule "an ambiguous base is an error". +func apatSafe(seq *obiseq.BioSequence) *obiseq.BioSequence { + raw := seq.Sequence() + clean := true + for _, b := range raw { + if b != 'a' && b != 'c' && b != 'g' && b != 't' { + clean = false + break + } + } + if clean { + return seq + } + out := make([]byte, len(raw)) + for i, b := range raw { + switch b { + case 'a', 'c', 'g', 't': + out[i] = b + case 'A', 'C', 'G', 'T': + out[i] = b + 'a' - 'A' + case 'u', 'U': + out[i] = 't' + default: + out[i] = 'z' + } + } + return obiseq.NewBioSequence(seq.Id(), out, "") +} + +// matchApat searches the words with obiapat, the bit-parallel Baeza-Yates / +// Manber matcher of OBITools (the engine of ecoPrimers and obipcr). +func matchApat(c *Corpus, words []uint64, p MatchParams) ([]WordMatches, error) { + if err := p.validate(); err != nil { + return nil, err + } + fwd, rev := wordStrings(words, p.K) + pats := make([][2]obiapat.ApatPattern, len(words)) + for i := range words { + var err error + if pats[i][0], err = obiapat.MakeApatPattern(fwd[i], p.MaxErrors, false); err != nil { + return nil, fmt.Errorf("obiprimer: pattern %s: %w", fwd[i], err) + } + if pats[i][1], err = obiapat.MakeApatPattern(rev[i], p.MaxErrors, false); err != nil { + return nil, fmt.Errorf("obiprimer: pattern %s: %w", rev[i], err) + } + } + + mk := func() scanFunc { + var recycle []obiapat.ApatSequence + return func(seq *obiseq.BioSequence, emit func(word, pos, errors int, reverse bool)) { + n := seq.Len() + if n < p.K { + return + } + aseq, err := obiapat.MakeApatSequence(apatSafe(seq), p.Circular, recycle...) + if err != nil { + panic(fmt.Sprintf("obiprimer: cannot encode sequence %s: %v", seq.Id(), err)) + } + recycle = append(recycle[:0], aseq) + + for w := range pats { + for strand := range 2 { + for _, m := range pats[w][strand].FindAllIndex(aseq, 0, -1) { + // On circular sequences obiapat also scans a copy of the + // start: those hits repeat the ones at start-n. + if m[0] >= 0 && m[0] < n { + emit(w, m[0], m[2], strand == 1) + } + } + } + } + } + } + return matchCorpus(c, words, mk), nil +} diff --git a/pkg/obiprimer/match_naive.go b/pkg/obiprimer/match_naive.go new file mode 100644 index 0000000..54e9567 --- /dev/null +++ b/pkg/obiprimer/match_naive.go @@ -0,0 +1,78 @@ +package obiprimer + +import "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq" + +// baseCode maps a nucleotide to 0..3 (A,C,G,T; U is T) and everything else to 4, +// which never equals a word base. +var baseCode = func() (t [256]byte) { + for i := range t { + t[i] = 4 + } + for i, c := range "ACGT" { + t[c], t[c+'a'-'A'] = byte(i), byte(i) + } + t['U'], t['u'] = 3, 3 + return +}() + +func codesOf(s string) []byte { + out := make([]byte, len(s)) + for i := range out { + out[i] = baseCode[s[i]] + } + return out +} + +// matchNaive is the reference implementation: every window of every sequence +// is compared with every word, base by base. It is slow and exists to define +// the exact semantics (see MatchParams) that faster implementations must +// reproduce. +func matchNaive(c *Corpus, words []uint64, p MatchParams) ([]WordMatches, error) { + if err := p.validate(); err != nil { + return nil, err + } + fwd, rev := wordStrings(words, p.K) + fc, rc := make([][]byte, len(words)), make([][]byte, len(words)) + for i := range words { + fc[i], rc[i] = codesOf(fwd[i]), codesOf(rev[i]) + } + + mk := func() scanFunc { + var codes []byte + return func(seq *obiseq.BioSequence, emit func(word, pos, errors int, reverse bool)) { + raw := seq.Sequence() + n := len(raw) + if n < p.K { + return + } + codes = codes[:0] + for _, b := range raw { + codes = append(codes, baseCode[b]) + } + windows := n - p.K + 1 + if p.Circular { + codes = append(codes, codes[:p.K-1]...) + windows = n + } + for pos := range windows { + win := codes[pos : pos+p.K] + for w := range words { + for strand, pat := range [2][]byte{fc[w], rc[w]} { + errs := 0 + for j, b := range pat { + if win[j] != b { + if errs++; errs > p.MaxErrors { + break + } + } + } + if errs <= p.MaxErrors { + emit(w, pos, errs, strand == 1) + } + } + } + } + } + } + return matchCorpus(c, words, mk), nil +} diff --git a/pkg/obiprimer/match_test.go b/pkg/obiprimer/match_test.go new file mode 100644 index 0000000..65ccd82 --- /dev/null +++ b/pkg/obiprimer/match_test.go @@ -0,0 +1,285 @@ +package obiprimer + +import ( + "bufio" + "fmt" + "math/rand" + "os" + "path/filepath" + "reflect" + "runtime" + "strconv" + "strings" + "testing" + "time" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obidefault" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiformats" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obikmer" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq" +) + +func corpusOf(roles []Role, seqs ...string) *Corpus { + c := &Corpus{Roles: roles} + for i, s := range seqs { + c.Seqs = append(c.Seqs, obiseq.NewBioSequence(fmt.Sprintf("s%d", i), []byte(s), "")) + } + return c +} + +func canon(word string) uint64 { return obikmer.EncodeCanonicalKmer([]byte(word), len(word)) } + +// The reference implementation is checked by hand first: everything else is +// compared with it. +func TestNaiveMatchSemantics(t *testing.T) { + const word = "AACCGGTA" // not a palindrome; reverse complement TACCGGTT + cases := []struct { + name string + seq string + circular bool + e int + want []Match + }{ + {"exact forward", "ggaaccggtacc", false, 0, []Match{{0, 2, 0, false}}}, + {"exact reverse strand", "ggtaccggttcc", false, 0, []Match{{0, 2, 0, true}}}, + {"one error is not found at e=0", "ggaaccgctacc", false, 0, nil}, + {"one error found at e=1 with its count", "ggaaccgctacc", false, 1, []Match{{0, 2, 1, false}}}, + {"an N is an error", "ggaaccgntacc", false, 1, []Match{{0, 2, 1, false}}}, + {"an N is never a match at e=0", "ggaaccgntacc", false, 0, nil}, + {"an IUPAC code is an error, not a wildcard", "ggaaccgrtacc", false, 0, nil}, + {"U reads as T", "ggaaccgguacc", false, 0, []Match{{0, 2, 0, false}}}, + {"junction ignored on a linear sequence", "ccggtagggaa", false, 0, nil}, + {"junction found on a circular sequence", "ccggtagggaa", true, 0, []Match{{0, 9, 0, false}}}, + {"overlapping windows are all reported", "aaccggtaaccggta", false, 0, []Match{{0, 0, 0, false}, {0, 7, 0, false}}}, + {"too short", "aaccgg", true, 3, nil}, + } + for _, tc := range cases { + c := corpusOf([]Role{Example}, tc.seq) + got, err := matchNaive(c, []uint64{canon(word)}, MatchParams{K: len(word), MaxErrors: tc.e, Circular: tc.circular}) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got[0].Matches, tc.want) { + t.Errorf("%s: got %v, want %v", tc.name, got[0].Matches, tc.want) + } + } +} + +func TestMatchParamsValidation(t *testing.T) { + c := corpusOf([]Role{Example}, "acgtacgtacgtacgt") + for _, p := range []MatchParams{{K: 1}, {K: 32}, {K: 8, MaxErrors: -1}, {K: 8, MaxErrors: 8}} { + if _, err := matchNaive(c, nil, p); err == nil { + t.Errorf("%+v must be rejected", p) + } + if _, err := matchApat(c, nil, p); err == nil { + t.Errorf("%+v must be rejected by obiapat variant", p) + } + } +} + +func randSeq(rng *rand.Rand, n int, alphabet string) []byte { + b := make([]byte, n) + for i := range b { + b[i] = alphabet[rng.Intn(len(alphabet))] + } + return b +} + +func mutate(rng *rand.Rand, s []byte, n int) []byte { + out := append([]byte(nil), s...) + for _, i := range rng.Perm(len(out))[:n] { + out[i] = "acgt"[rng.Intn(4)] + } + return out +} + +// randomCorpus builds sequences with words planted on both strands, with 0..3 +// substitutions, across the circular junction, and with ambiguity codes. +func randomCorpus(rng *rand.Rand, words []string, nseq int) *Corpus { + c := &Corpus{} + for i := 0; i < nseq; i++ { + alphabet := "acgt" + if i%3 == 0 { + alphabet = "acgtacgtacgtacgtacgtnnrykm" + } + s := randSeq(rng, 30+rng.Intn(500), alphabet) + for range 1 + rng.Intn(4) { + w := []byte(words[rng.Intn(len(words))]) + if rng.Intn(2) == 0 { + w = []byte(revcomp(string(w))) + } + w = mutate(rng, []byte(string(w)), rng.Intn(4)) + for j := range w { + w[j] += 'a' - 'A' + } + at := rng.Intn(len(s) - len(w) + 1) + if rng.Intn(5) == 0 { // across the junction + s = append(s[:0:0], append(append([]byte(nil), w[len(w)/2:]...), append(s[:len(s)-len(w)/2], w[:len(w)/2]...)...)...) + continue + } + copy(s[at:], w) + } + c.Seqs = append(c.Seqs, obiseq.NewBioSequence(fmt.Sprintf("r%d", i), s, "")) + c.Roles = append(c.Roles, Example) + } + return c +} + +// Every implementation must return exactly what the reference returns. +func TestMatchersAgreeWithReference(t *testing.T) { + for _, tc := range []struct { + k, e int + circular bool + }{{18, 0, false}, {18, 1, true}, {18, 3, false}, {18, 3, true}, {12, 2, true}, {25, 4, false}} { + t.Run(fmt.Sprintf("k%d-e%d-circ%v", tc.k, tc.e, tc.circular), func(t *testing.T) { + rng := rand.New(rand.NewSource(int64(tc.k*100 + tc.e))) + var wordStr []string + var words []uint64 + for range 7 { + w := string(randSeq(rng, tc.k, "ACGT")) + wordStr = append(wordStr, w) + words = append(words, canon(w)) + } + c := randomCorpus(rng, wordStr, 40) + p := MatchParams{K: tc.k, MaxErrors: tc.e, Circular: tc.circular} + + want, err := matchNaive(c, words, p) + if err != nil { + t.Fatal(err) + } + total := 0 + for _, w := range want { + total += len(w.Matches) + } + if total < 10 { + t.Fatalf("test data too poor: only %d reference matches", total) + } + + got, err := matchApat(c, words, p) + if err != nil { + t.Fatal(err) + } + for i := range want { + if !reflect.DeepEqual(got[i].Matches, want[i].Matches) { + t.Fatalf("word %d: obiapat gives %v\nreference gives %v", i, got[i].Matches, want[i].Matches) + } + } + }) + } +} + +func TestSequencesMatched(t *testing.T) { + c := corpusOf([]Role{Example, Example, CounterExample, CounterExample}, "a", "a", "a", "a") + w := WordMatches{Matches: []Match{{0, 1, 0, false}, {0, 9, 0, true}, {1, 3, 0, false}, {3, 0, 0, false}}} + if e, x := w.SequencesMatched(c); e != 2 || x != 1 { + t.Errorf("got %d examples, %d counterexamples", e, x) + } +} + +func readEcoCounts(t *testing.T, path string) map[string]int { + t.Helper() + f, err := os.Open(path) + if err != nil { + t.Fatal(err) + } + defer f.Close() + counts := map[string]int{} + sc := bufio.NewScanner(f) + for sc.Scan() { + var w string + var in, out int + if _, err := fmt.Sscanf(sc.Text(), "%s\t%d\t%d", &w, &in, &out); err != nil { + t.Fatal(err) + } + counts[w] = in + } + return counts +} + +// Opt-in comparison with ecoPrimers on the tutorial data (same variable as the +// strict word test). For every strict word, ecoPrimers logs the number of +// example sequences in which the fuzzy search (-e 3, both strands, circular) +// finds it; testdata/ecoprimers_*.words.tsv holds those numbers. +func TestMatchApatAgainstEcoPrimers(t *testing.T) { + dir := os.Getenv("OBIPRIMERS_TUTORIAL") + if dir == "" { + t.Skip("OBIPRIMERS_TUTORIAL not set") + } + if v := os.Getenv("OBIPRIMERS_WORKERS"); v != "" { // e.g. 1, to time a single core + n, err := strconv.Atoi(v) + if err != nil { + t.Fatal(err) + } + obidefault.SetMaxCPU(n) + } + taxonomy, err := obiformats.LoadTaxonomy(filepath.Join(dir, "ncbitaxo_20250211.tgz"), false, false) + if err != nil { + t.Fatal(err) + } + + for _, tc := range []struct{ name, taxid string }{{"Cyprinidae", "7953"}, {"Teleostei", "32443"}} { + t.Run(tc.name, func(t *testing.T) { + restrict, err := BelongingTo(taxonomy, []string{tc.taxid}) + if err != nil { + t.Fatal(err) + } + seqs, err := obiformats.ReadSequencesFromFile(filepath.Join(dir, "mito.vert.fasta.gz")) + if err != nil { + t.Fatal(err) + } + corpus, err := LoadCorpus(seqs, Selection{Restrict: restrict}) + if err != nil { + t.Fatal(err) + } + sw, err := FindStrictWords(corpus, WordSearch{PrimerLength: 18, Quorum: 0.7, Circular: true, TmpDir: t.TempDir()}) + if err != nil { + t.Fatal(err) + } + words := make([]uint64, len(sw.Words)) + for i, w := range sw.Words { + words[i] = w.Kmer + } + + var before, after runtime.MemStats + runtime.GC() + runtime.ReadMemStats(&before) + start := time.Now() + res, err := matchApat(corpus, words, MatchParams{K: 18, MaxErrors: 3, Circular: true}) + elapsed := time.Since(start) + runtime.ReadMemStats(&after) + if err != nil { + t.Fatal(err) + } + nm := 0 + for _, r := range res { + nm += len(r.Matches) + } + bases := 0 + for _, s := range corpus.Seqs { + bases += s.Len() + } + t.Logf("obiapat: %d words x %d sequences (%.1f Mb) e=3 in %s (%.0f Mb/s per word-strand pass); %d matches; heap +%.0f MB, allocated %.0f MB", + len(words), len(corpus.Seqs), float64(bases)/1e6, elapsed.Round(time.Millisecond), + float64(bases)*float64(len(words))*2/1e6/elapsed.Seconds(), nm, + float64(after.HeapAlloc-before.HeapAlloc)/1e6, float64(after.TotalAlloc-before.TotalAlloc)/1e6) + + eco := readEcoCounts(t, filepath.Join("testdata", "ecoprimers_"+tc.name+".words.tsv")) + compared, differ := 0, 0 + for i, r := range res { + w := strings.ToUpper(string(obikmer.DecodeKmer(words[i], 18, nil))) + want, ok := eco[w] + if !ok { + t.Errorf("word %s is not in the ecoPrimers log", w) + continue + } + got, _ := r.SequencesMatched(corpus) + compared++ + if got != want { + differ++ + t.Errorf("word %s: %d example sequences, ecoPrimers %d", w, got, want) + } + } + t.Logf("%d words compared with ecoPrimers, %d differ", compared, differ) + }) + } +} diff --git a/pkg/obiprimer/testdata/ecoprimers_Cyprinidae.words.tsv b/pkg/obiprimer/testdata/ecoprimers_Cyprinidae.words.tsv new file mode 100644 index 0000000..49f6354 --- /dev/null +++ b/pkg/obiprimer/testdata/ecoprimers_Cyprinidae.words.tsv @@ -0,0 +1,1208 @@ +AAAAACATCGCCTCCTGC 324 0 +AAAAACTCTTGGTGCAAA 324 0 +AAAACATCGCCTCCTGCA 324 0 +AAAACCCAAAGGACCTGA 324 0 +AAAACTCGTGCCAGCCAC 325 0 +AAAACTCTTGGTGCAAAT 324 0 +AAAAGACAAGTGATTGCG 324 0 +AAAAGAGTGGGAAGAGCT 324 0 +AAAAGCATCGGTCTTGTA 324 0 +AAAAGGGCTTAGGCCTTT 325 0 +AAAATGAGTAACAAGAAG 325 0 +AAAATGTAGCCCATTTCT 325 0 +AAACAAACTGGGATTAGA 325 0 +AAACACAAGCCTCGCCTG 320 0 +AAACAGAAAGCGTTAAAG 325 0 +AAACAGGCGAGGCTTGTG 323 0 +AAACATCGCCTCCTGCAA 324 0 +AAACCCAAAGGACCTGAC 324 0 +AAACCCCAAGGAAAGATA 324 0 +AAACCCCCCTACCCCCCT 320 0 +AAACCTCTGTCTTCGGGG 313 0 +AAACGAACCCTTAATAGC 323 0 +AAACGAGGGCTTAACTGT 325 0 +AAACTAAAGGTCTCTTTG 325 0 +AAACTCGTGCCAGCCACC 325 0 +AAACTCTTAGTTAACAGC 325 0 +AAACTCTTGGTGCAAATC 324 0 +AAACTGACCTGGATTGCT 325 0 +AAACTGGGATTAGATACC 325 0 +AAAGACAAGTGATTGCGC 324 0 +AAAGAGACCTTTAGTTTG 325 0 +AAAGAGTGGGAAGAGCTC 323 0 +AAAGATTATGAGCCTCAT 300 0 +AAAGCATAGCACTGAAGA 324 0 +AAAGCATCGGTCTTGTAA 325 0 +AAAGCGTTAAAGCTCAGA 325 0 +AAAGCTGAAAGAGAAATG 325 0 +AAAGGAAAGACTAAAAGA 321 0 +AAAGGACCTGACGGTGTC 324 0 +AAAGGCATGGTCCCGACC 323 0 +AAAGGGAGGAATTGAACC 325 0 +AAAGGGCTTAGGCCTTTC 325 0 +AAAGGGGCTGTACCCCCT 325 0 +AAAGGGTCTTCTCGTCTT 325 0 +AAAGGTAGCGCAATCACT 324 0 +AAAGTAGATGGATGCTCG 324 0 +AAAGTCCTACGTGATCTG 325 0 +AAATAAGGACATACTGGG 311 0 +AAATAGGAACCAGATGCC 322 0 +AAATCATGATGCAAAAGG 325 0 +AAATGAGTAACAAGAAGG 325 0 +AAATGGGCTACATTTTCT 325 0 +AAATGTAGCCCATTTCTT 325 0 +AAATTGCGAAAGGCCTAA 320 0 +AACAAACGAACCCTTAAT 323 0 +AACAAACTGGGATTAGAT 325 0 +AACAACGGTGGTTCTTCA 325 0 +AACACAAGCCTCGCCTGT 320 0 +AACACGGTAAGTGTACCG 324 0 +AACAGAAAGCGTTAAAGC 325 0 +AACAGCGCAATCCTCTCC 325 0 +AACAGGCGAGGCTTGTGT 320 0 +AACAGGCTCCTCTAGGGG 325 0 +AACATCGAGGTCGTAAAC 325 0 +AACATCGCCTCCTGCAAC 324 0 +AACCAAAAACTCTTGGTG 325 0 +AACCAAGTTACCCTAGGG 325 0 +AACCACCCTTTACGCCGT 324 0 +AACCACCGTTGTTATTCA 316 0 +AACCAGATGCCAGTAATA 324 0 +AACCAGCTATCACCAGGT 325 0 +AACCCAAAGGACCTGACG 324 0 +AACCCCAAGGAAAGATAA 324 0 +AACCCCCCTACCCCCCTA 320 0 +AACCCGGCTCTGAGACGC 324 0 +AACCCGTCTCTGTGGCAA 325 0 +AACCCTTAATAGCGGCTG 325 0 +AACCGAGACCAATGACTT 320 0 +AACCGCGGTGGCTGGCAC 325 0 +AACCGTCATGTTCGGGGT 324 0 +AACCTCCGATCTTCGGAT 321 0 +AACCTCTGTCTTCGGGGC 314 0 +AACCTGGTGATAGCTGGT 325 0 +AACGAACCAAGTTACCCT 325 0 +AACGAACCCTTAATAGCG 323 0 +AACGAGAAAGGGAGGAAT 324 0 +AACGAGGGCTTAACTGTC 325 0 +AACGATTAAAGTCCTACG 320 0 +AACGGCCGCGGTATTTTG 323 0 +AACGGCGTAAAGGGTGGT 324 0 +AACGTCAGGTCGAGGTGT 325 0 +AACTAAAGGTCTCTTTGC 325 0 +AACTAAGAGTTTGTAGGA 325 0 +AACTATTACTGGCATCTG 322 0 +AACTCAGATCACGTAGGA 325 0 +AACTCGGCAAACACAAGC 322 0 +AACTCGTGCCAGCCACCG 325 0 +AACTCTTAGTTAACAGCT 325 0 +AACTCTTGGTGCAAATCC 324 0 +AACTGACCTGGATTGCTC 325 0 +AACTGGGATTAGATACCC 325 0 +AACTTACACATGCAAGTC 325 0 +AACTTGGTTCGTTGATCG 325 0 +AAGAACCACCGTTGTTAT 321 0 +AAGACAAGTGATTGCGCT 324 0 +AAGACCCTTTGGAGCTTA 325 0 +AAGACCTCTGATTTCGGC 324 0 +AAGACTGTGTGATTGGAA 322 0 +AAGAGACCTTTAGTTTGA 325 0 +AAGAGTTTGTAGGATCGA 325 0 +AAGAGTTTTTGGTTCCTA 323 0 +AAGATTATGAGCCTCATC 317 0 +AAGATTTTCACTTCTACT 322 0 +AAGCAAAGAGACCTTTAG 325 0 +AAGCATAGCACTGAAGAT 324 0 +AAGCATCGGTCTTGTAAT 325 0 +AAGCATCTCACTTACACC 323 0 +AAGCCACAGAACATCTGA 321 0 +AAGCCAGCCACATAACCA 322 0 +AAGCCATAAGTGAAAACT 325 0 +AAGCCTCGCCTGTTTACC 325 0 +AAGCGTTAAAGCTCAGAC 325 0 +AAGCTCCAAAGGGTCTTC 325 0 +AAGCTGAAAGAGAAATGA 325 0 +AAGCTGACGACGGCGGTA 325 0 +AAGCTTTCGGGCCCATAC 325 0 +AAGGAAAGACTAAAAGAA 323 0 +AAGGAACTCGGCAAACAC 323 0 +AAGGAAGGAACTCGGCAA 324 0 +AAGGACATACTGGGGTGG 312 0 +AAGGACCACTTTGATAGA 325 0 +AAGGACCTGACGGTGTCT 325 0 +AAGGAGGATTTAGTAGTA 325 0 +AAGGCATGGTCCCGACCT 323 0 +AAGGCCTAAGCCCTTTTA 324 0 +AAGGCCTCGATCCTACAA 325 0 +AAGGGAAAGCTGAAAGAG 325 0 +AAGGGAATTCAGCAGTGA 325 0 +AAGGGAGGAATTGAACCC 325 0 +AAGGGCTTAGGCCTTTCG 325 0 +AAGGGGAGGCAAGTCGTA 325 0 +AAGGGGCTGTACCCCCTT 325 0 +AAGGGTCTTCTCGTCTTG 325 0 +AAGGGTTCGTTTGTTCAA 324 0 +AAGGTAGCGCAATCACTT 324 0 +AAGGTCGGGACCATGCCT 322 0 +AAGTAGAAGTGAAAATCT 323 0 +AAGTCCTACGTGATCTGA 325 0 +AAGTCGTAACACGGTAAG 325 0 +AAGTGACAGACCTACCGA 322 0 +AAGTGACAGAGTGGTTAT 322 0 +AAGTGACTTCCAATCACA 320 0 +AAGTGCACCTTCCGGTAC 323 0 +AAGTGTACCGGAAGGTGC 324 0 +AAGTTACCCTAGGGATAA 325 0 +AAGTTTTCACTTATGGCT 325 0 +AATAACAACGGTGGTTCT 319 0 +AATACAAGACGAGAAGAC 325 0 +AATACCGCGGCCGTTGAA 323 0 +AATAGCGGCTGCACCATT 324 0 +AATAGGAACCAGATGCCA 322 0 +AATAGGCTGTCTCGGGGT 322 0 +AATCACTTGTCTTTTAAA 324 0 +AATCATGATGCAAAAGGT 325 0 +AATCCAGGTCAGTTTCTA 325 0 +AATCCGAAGATCGGAGGT 321 0 +AATCCTCTCCCAGAGTCC 325 0 +AATCGTTGAACAAACGAA 325 0 +AATCTTTGGCTTAAAAGG 323 0 +AATGAGTAACAAGAAGGC 325 0 +AATGGCCCACCAAGCACA 325 0 +AATGGGCTACATTTTCTA 325 0 +AATTCCCTTGGGGGTGTG 325 0 +AATTCCTCCCTTTCTCGT 324 0 +AATTGCGAAAGGCCTAAG 322 0 +AATTGTGGCATTTCACTA 317 0 +AATTTACCATGCTCTGCC 324 0 +ACAAACGAACCCTTAATA 322 0 +ACAAACTCTTAGTTAACA 325 0 +ACAAACTGGGATTAGATA 325 0 +ACAAAGGCATGGTCCCGA 322 0 +ACAACGGTGGTTCTTCAA 325 0 +ACAAGACCGATGCTTTTA 324 0 +ACAAGACGAGAAGACCCT 325 0 +ACAAGCCTCGCCTGTTTA 325 0 +ACAAGGGGAGGCAAGTCG 321 0 +ACAAGTGACTTCCAATCA 320 0 +ACAAGTGATTGCGCTACC 324 0 +ACACACCGCCCGTCACTC 325 0 +ACACATGCAAGTCTCCGC 324 0 +ACACCCCCAAGGGAATTC 324 0 +ACACCGAGAAGACATCCA 324 0 +ACACCGCCCGTCACTCTC 325 0 +ACACCGTCAGGTCCTTTG 324 0 +ACACGGTAAGTGTACCGG 324 0 +ACACTTACCGTGTTACGA 325 0 +ACAGAAAGCGTTAAAGCT 325 0 +ACAGACCTACCGAACCTG 323 0 +ACAGAGACGGGTTGGCCC 325 0 +ACAGAGTGGTTATGTGGC 319 0 +ACAGATAGAAACTGACCT 323 0 +ACAGCCCCTTTAACAAAG 324 0 +ACAGCGCAATCCTCTCCC 325 0 +ACAGGCTCCTCTAGGGGG 325 0 +ACAGGGTAAGCTGACGAC 325 0 +ACATAACCACTCTGTCAC 320 0 +ACATACTGGGGTGGCAGA 320 0 +ACATCCTAATGGTGCAGC 325 0 +ACATCGAGGTCGTAAACC 325 0 +ACCAAAAACATCGCCTCC 324 0 +ACCAAAAACTCTTGGTGC 325 0 +ACCAAAAGCCTTCAAAGC 321 0 +ACCAAGACTGTGTGATTG 322 0 +ACCAAGAGTTTTTGGTTC 325 0 +ACCAAGCACATGCCTATC 323 0 +ACCAAGTTACCCTAGGGA 325 0 +ACCAATCTTTGGCTTAAA 321 0 +ACCAATGACTTGAAGAAC 325 0 +ACCACCGTTGTTATTCAA 316 0 +ACCACTTTGATAGAGTGA 325 0 +ACCAGATGCCAGTAATAG 322 0 +ACCAGGTTCGGTAGGTCT 323 0 +ACCATGCTCTGCCACCCC 324 0 +ACCATTAGGATGTCCTGA 325 0 +ACCCAAAGGACCTGACGG 324 0 +ACCCCAAGGAAAGATAAT 324 0 +ACCCCCAAGGGAATTCAG 324 0 +ACCCCCCTACCCCCCTAC 298 0 +ACCCGGCTCTGAGACGCG 324 0 +ACCCGTCTCTGTGGCAAA 325 0 +ACCCTACCTGTGGCAATC 324 0 +ACCCTAGGGATAACAGCG 325 0 +ACCCTTAATAGCGGCTGC 325 0 +ACCCTTTGGAGCTTAAGG 325 0 +ACCGAACCTGGTGATAGC 325 0 +ACCGAGAAGACATCCATG 323 0 +ACCGAGACCAATGACTTG 320 0 +ACCGCAAGGGAAAGCTGA 324 0 +ACCGCCCGTCACTCTCCC 325 0 +ACCGCCGTCGTCAGCTTA 325 0 +ACCGCGGTGGCTGGCACG 325 0 +ACCGCGGTTAGACGAGAG 324 0 +ACCGGAAGGTGCACTTGG 322 0 +ACCGGAGCAATCCAGGTC 324 0 +ACCGTCAGGTCCTTTGGG 324 0 +ACCGTCATGTTCGGGGTA 324 0 +ACCGTGCAAAGGTAGCGC 324 0 +ACCGTTGTTATTCAACTA 317 0 +ACCTACCGAACCTGGTGA 325 0 +ACCTCGATGTTGGATCAG 325 0 +ACCTCTGATTTCGGCTCA 324 0 +ACCTCTGTCTTCGGGGCT 314 0 +ACCTGACGGTGTCTCAGA 325 0 +ACCTGGATTGCTCCGGTC 324 0 +ACCTGTATGAATGGCTAA 325 0 +ACCTTAAGCTCCAAAGGG 325 0 +ACCTTCCGGTACACTTAC 324 0 +ACCTTCGGTTGGGGCGAC 325 0 +ACCTTTAGTTTGAAACCC 324 0 +ACCTTTGCACGGTCAAAA 324 0 +ACGAACCAAGTTACCCTA 325 0 +ACGAACCCTTAATAGCGG 323 0 +ACGACCTCGATGTTGGAT 325 0 +ACGACGGCGGTATATAGG 324 0 +ACGACTTGCCTCCCCTTG 323 0 +ACGAGAAGACCCTTTGGA 325 0 +ACGAGGAGCGGGCATCAG 316 0 +ACGAGGGCTTAACTGTCT 325 0 +ACGAGTTTTACCGGCCCT 324 0 +ACGATTAAAGTCCTACGT 321 0 +ACGCGTACACACCGCCCG 325 0 +ACGCGTCTCAGAGCCGGG 323 0 +ACGGCCGCGGTATTTTGA 323 0 +ACGGCGGTATATAGGCTG 324 0 +ACGGGCGGTGTGTACGCG 325 0 +ACGGTAAGTGTACCGGAA 324 0 +ACGGTCAAAATACCGCGG 323 0 +ACGGTGTCTCAGACCCCC 323 0 +ACGTCAGGTCGAGGTGTA 325 0 +ACGTGATCTGAGTTCAGA 325 0 +ACTAAAGGTCTCTTTGCT 325 0 +ACTAAGAGTTTGTAGGAT 325 0 +ACTACTAAATCCTCCTTC 324 0 +ACTAGAAAGATTATGAGC 284 0 +ACTATTACTGGCATCTGG 322 0 +ACTATTCCGGCTCAGGCA 323 0 +ACTCAAGAGATCAAAACT 320 0 +ACTCAGATCACGTAGGAC 325 0 +ACTCGGCAAACACAAGCC 322 0 +ACTCGTGCCAGCCACCGC 325 0 +ACTCTATCAAAGTGGTCC 325 0 +ACTCTGGGAGAGGATTGC 325 0 +ACTCTTAGTTAACAGCTA 325 0 +ACTCTTGGTGCAAATCCA 324 0 +ACTCTTTTGCCACAGAGA 325 0 +ACTGAAGATGCTAAGATG 325 0 +ACTGACCTGGATTGCTCC 324 0 +ACTGAGTCAAGTTTTCAC 325 0 +ACTGCTGAATTCCCTTGG 325 0 +ACTGGCATCTGGTTCCTA 322 0 +ACTGGCTAAATCATGATG 325 0 +ACTGGGATTAGATACCCC 325 0 +ACTGGGCAGGCTGGACCT 323 0 +ACTGGGGTGGCAGAGCAT 323 0 +ACTTACACATGCAAGTCT 325 0 +ACTTACACCGAGAAGACA 323 0 +ACTTACCGTGTTACGACT 325 0 +ACTTGCATGTGTAAGTTG 325 0 +ACTTGGATTTGCACCAAG 324 0 +ACTTGGTTCGTTGATCGG 325 0 +ACTTTAATCGTTGAACAA 325 0 +ACTTTTCCTAGTACGAAA 323 0 +AGAAACAAACTGGGATTA 325 0 +AGAAACTGACCTGGATTG 325 0 +AGAAAGATTATGAGCCTC 271 0 +AGAAAGCGTTAAAGCTCA 325 0 +AGAAAGGGAGGAATTGAA 325 0 +AGAACAGGCTCCTCTAGG 325 0 +AGAAGACCCTTTGGAGCT 325 0 +AGAAGATTTTCACTTCTA 323 0 +AGAAGTGAAAATCTTCTA 323 0 +AGAAGTGACAGACCTACC 321 0 +AGAATCACAATCTAATAT 321 0 +AGAATTCTGGCTTTGGGA 321 0 +AGACAAGTGATTGCGCTA 324 0 +AGACCAATGACTTGAAGA 325 0 +AGACCCTTTGGAGCTTAA 325 0 +AGACCGGAGCAATCCAGG 324 0 +AGACCTCTGATTTCGGCT 324 0 +AGACCTTTAGTTTGAAAC 324 0 +AGACGAGAAGACCCTTTG 325 0 +AGACGCGTACACACCGCC 324 0 +AGACTGTGTGATTGGAAG 318 0 +AGAGACCTTTAGTTTGAA 325 0 +AGAGCATGGTAAATTGCG 307 0 +AGAGGAGCCTGTTCTAGA 325 0 +AGAGGATTGCGCTGTTAT 325 0 +AGAGGATTTGAACCTCTG 325 0 +AGAGTCCATATCGACGAG 324 0 +AGAGTGACGGGCGGTGTG 325 0 +AGAGTGGTTATGTGGCTG 321 0 +AGAGTTTGTAGGATCGAG 325 0 +AGAGTTTTGATCTCTTGA 316 0 +AGAGTTTTTGGTTCCTAA 323 0 +AGATACCCCACTATGCTC 325 0 +AGATAGAAACTGACCTGG 323 0 +AGATCAATTTCACTGACT 325 0 +AGATCACGTAGGACTTTA 325 0 +AGATGCCAGTAATAGTTC 322 0 +AGATTATGAGCCTCATCA 317 0 +AGATTGTGATTCTAAAGA 322 0 +AGCACTGAAGATGCTAAG 324 0 +AGCATAGCACTGAAGATG 324 0 +AGCATAGTGGGGTATCTA 325 0 +AGCATCGGTCTTGTAATC 325 0 +AGCATCTCACTTACACCG 323 0 +AGCATCTTCAGTGCTATG 324 0 +AGCATGGTAAATTGCGAA 306 0 +AGCCACACCCCCAAGGGA 324 0 +AGCCACAGAACATCTGAC 321 0 +AGCCACATAACCACTCTG 321 0 +AGCCACCGCGGTTAGACG 325 0 +AGCCAGCCACATAACCAC 322 0 +AGCCCCTTTAACAAAGGA 324 0 +AGCCGCTATTAAGGGTTC 324 0 +AGCCTATATACCGCCGTC 324 0 +AGCCTCGCCTGTTTACCA 325 0 +AGCCTGTTCTAGAACCGA 325 0 +AGCGCAATCCTCTCCCAG 325 0 +AGCGCTTAGCTGTTAACT 325 0 +AGCGGCTGCACCATTAGG 324 0 +AGCGGGCATCAGGCACAA 323 0 +AGCGTAGGGGGGTAGGGG 305 0 +AGCGTTAAAGCTCAGACA 325 0 +AGCTACCCCGAGACAGCC 324 0 +AGCTAGCTGTTTGGCTCA 324 0 +AGCTATCACCAGGTTCGG 325 0 +AGCTGAAAGAGAAATGAA 325 0 +AGCTGACGACGGCGGTAT 325 0 +AGCTTAAAACCCAAAGGA 325 0 +AGCTTTCCCTTGCGGTAC 324 0 +AGCTTTCGGGCCCATACC 323 0 +AGCTTTGAAGGCTTTTGG 315 0 +AGGAAAGACTAAAAGAAA 322 0 +AGGAACCAAAAACTCTTG 323 0 +AGGAACCAGATGCCAGTA 322 0 +AGGAACTCGGCAAACACA 323 0 +AGGAAGGAACTCGGCAAA 324 0 +AGGAATTGAACCCCCATA 325 0 +AGGACATACTGGGGTGGC 311 0 +AGGACATCCTAATGGTGC 325 0 +AGGACCACTTTGATAGAG 325 0 +AGGACCTGACGGTGTCTC 325 0 +AGGACTTTAATCGTTGAA 320 0 +AGGAGCCTGTTCTAGAAC 325 0 +AGGAGCGGGCATCAGGCA 321 0 +AGGAGGATAAAGATCATA 325 0 +AGGAGGATTTAGTAGTAA 325 0 +AGGAGGCGATGTTTTTGG 324 0 +AGGAGGTCCAGCCTGCCC 324 0 +AGGATAAAGATCATAATA 324 0 +AGGATCGAGGCCTTCCCA 325 0 +AGGATGTCCTGATCCAAC 325 0 +AGGATTGCGCTGTTATCC 325 0 +AGGCAACCAGCTATCACC 325 0 +AGGCATGTGCTTGGTGGG 323 0 +AGGCCTCGATCCTACAAA 325 0 +AGGCCTTTCGCAATTTAC 316 0 +AGGCGAGGCTTGTGTTTG 320 0 +AGGCGATGTTTTTGGTAA 325 0 +AGGCGGGAAAAGCCCCGG 314 0 +AGGCGGTGGGTTGTAGCC 323 0 +AGGCTCATAATCTTTCTA 274 0 +AGGCTGGACCTCCTATAC 320 0 +AGGCTGTCTCGGGGTAGC 324 0 +AGGCTTGTGTTTGCCGAG 320 0 +AGGGAAAGCTGAAAGAGA 325 0 +AGGGAATTCAGCAGTGAT 325 0 +AGGGAGGAATTGAACCCC 325 0 +AGGGATAACAGCGCAATC 325 0 +AGGGCCAACCCGTCTCTG 325 0 +AGGGCTTAACTGTCTCCC 325 0 +AGGGCTTAGGCCTTTCGC 325 0 +AGGGGAGGCAAGTCGTAA 325 0 +AGGGGTTAAAATCCCCTC 325 0 +AGGGTAAGCTGACGACGG 325 0 +AGGGTTCGTTTGTTCAAC 324 0 +AGGTAGCGCAATCACTTG 324 0 +AGGTCCTTTGGGTTTTAA 325 0 +AGGTCGAGGTGTAGCGCA 325 0 +AGGTCTGTCACTTCTACC 323 0 +AGGTGAGCTACCCCGAGA 325 0 +AGGTTCGGTAGGTCTGTC 322 0 +AGTACAAGTGACTTCCAA 325 0 +AGTACCGCAAGGGAAAGC 324 0 +AGTACGAAAGGATCGGAA 325 0 +AGTCAAGTTTTCACTTAT 325 0 +AGTCATTGGTCTCGGTTA 320 0 +AGTCCTACGTGATCTGAG 325 0 +AGTGAAAACTTGACTCAG 325 0 +AGTGACAGACCTACCGAA 323 0 +AGTGACAGAGTGGTTATG 322 0 +AGTGACGGGCGGTGTGTA 325 0 +AGTGACTTCCAATCACAC 316 0 +AGTGCACCTTCCGGTACA 323 0 +AGTGGGCCTAAAAGCAGC 324 0 +AGTGGGGTATCTAATCCC 325 0 +AGTGGTTATGTGGCTGGC 321 0 +AGTGTACCGGAAGGTGCA 323 0 +AGTTACCCTAGGGATAAC 325 0 +AGTTCAGACCGGAGCAAT 325 0 +AGTTGAATAACAACGGTG 317 0 +AGTTTCTATCTGTAACGC 323 0 +AGTTTGAAACCCCGAAAC 324 0 +AGTTTGTAGGATCGAGGC 325 0 +AGTTTTTGGTTCCTAAGA 325 0 +ATACAAGACGAGAAGACC 325 0 +ATACCCCACTATGCTCAG 325 0 +ATACCCCGAACATGACGG 324 0 +ATACCGCGGCCGTTGAAC 323 0 +ATACTAGAAAGATTATGA 271 0 +ATACTGGGGTGGCAGAGC 323 0 +ATAGAAACTGACCTGGAT 324 0 +ATAGCACTGAAGATGCTA 325 0 +ATAGCGGCTGCACCATTA 324 0 +ATAGCTGGTTGCCTAAGA 324 0 +ATAGGAACCAGATGCCAG 322 0 +ATAGGAGGTCCAGCCTGC 324 0 +ATAGGCATGTGCTTGGTG 322 0 +ATAGGCTGTCTCGGGGTA 322 0 +ATAGTGGGGTATCTAATC 325 0 +ATATACCGCCGTCGTCAG 325 0 +ATATATATATATATATAA 315 0 +ATATATATATATATATAT 307 0 +ATATGGACTCTGGGAGAG 325 0 +ATATGGGGGTTCAATTCC 325 0 +ATCAACGAACCAAGTTAC 325 0 +ATCACCAGGTTCGGTAGG 325 0 +ATCACCCGTTATAGAAGA 323 0 +ATCACGTAGGACTTTAAT 325 0 +ATCACTTGTCTTTTAAAT 324 0 +ATCAGGACATCCTAATGG 325 0 +ATCATGATGCAAAAGGTA 325 0 +ATCATGATTTAGCCAGTA 325 0 +ATCCCTAGGGTAACTTGG 325 0 +ATCCTAATGGTGCAGCCG 325 0 +ATCCTCTCCCAGAGTCCA 325 0 +ATCCTTTCGTACTAGGAA 324 0 +ATCGGTCTTGTAATCCGA 325 0 +ATCGGTTCTAGAACAGGC 325 0 +ATCGTTGAACAAACGAAC 325 0 +ATCTCACTTACACCGAGA 323 0 +ATCTGAGTTCAGACCGGA 325 0 +ATCTGGTTCCTATTTCAG 321 0 +ATCTTAGCATCTTCAGTG 325 0 +ATCTTCGGATTACAAGAC 323 0 +ATCTTTCCTTGGGGTTTA 323 0 +ATCTTTGGCTTAAAAGGC 323 0 +ATGAGTAACAAGAAGGCC 325 0 +ATGATCTTTATCCTCCTG 325 0 +ATGCCCGCTCCTCGTCCC 306 0 +ATGCGCTACACCTCGACC 325 0 +ATGCTAAAATGAGTAACA 325 0 +ATGCTGGTTTCAAGCCAG 323 0 +ATGGACTCTGGGAGAGGA 325 0 +ATGGATGTCTTCTCGGTG 324 0 +ATGGCAAGCCTACGAAAA 324 0 +ATGGCCCACCAAGCACAT 325 0 +ATGGGGGTTCAATTCCTC 325 0 +ATGGTAAATTGCGAAAGG 304 0 +ATGGTGCAGCCGCTATTA 324 0 +ATGTCCTGATCCAACATC 325 0 +ATGTCTTCTCGGTGTAAG 322 0 +ATGTGGCTGGCTTGAAAC 323 0 +ATGTTCGGGGTATGGGCC 325 0 +ATGTTGGATCAGGACATC 325 0 +ATGTTTTTGGTAAACAGG 324 0 +ATTAAGCCATAAGTGAAA 323 0 +ATTACTGGCATCTGGTTC 323 0 +ATTAGATACCCCACTATG 325 0 +ATTAGATTGTGATTCTAA 321 0 +ATTAGGATGTCCTGATCC 325 0 +ATTATGATCTTTATCCTC 325 0 +ATTCCCTTCTTTCTAGGA 323 0 +ATTCCCTTGGGGGTGTGG 325 0 +ATTCCGGCTCAGGCACCA 323 0 +ATTGCGAAAGGCCTAAGC 323 0 +ATTGCGCTACCTTTGCAC 324 0 +ATTGCGCTGTTATCCCTA 325 0 +ATTGGAAGTCACTTGTAC 325 0 +ATTTACCATGCTCTGCCA 325 0 +ATTTATTTGTTTTCATAA 325 0 +ATTTCTCTTTCAGCTTTC 325 0 +ATTTGGTGCCTGAGCCGG 324 0 +ATTTTGACCGTGCAAAGG 324 0 +CAAAAACATCGCCTCCTG 324 0 +CAAAAACTCTTGGTGCAA 325 0 +CAAAAGAGTGGGAAGAGC 323 0 +CAAACCCCCCTACCCCCC 320 0 +CAAACGAACCCTTAATAG 323 0 +CAAACTCTTAGTTAACAG 325 0 +CAAACTGGGATTAGATAC 325 0 +CAAAGGCATGGTCCCGAC 321 0 +CAAAGGTAGCGCAATCAC 324 0 +CAACATCGAGGTCGTAAA 325 0 +CAACCAGCTATCACCAGG 325 0 +CAACCCGTCTCTGTGGCA 325 0 +CAACGAACCAAGTTACCC 325 0 +CAACGATTAAAGTCCTAC 320 0 +CAAGACTGTGTGATTGGA 322 0 +CAAGCAAAGAGACCTTTA 324 0 +CAAGCCAGCCACATAACC 322 0 +CAAGCCTCGCCTGTTTAC 325 0 +CAAGGGAAAGCTGAAAGA 323 0 +CAAGGGAATTCAGCAGTG 325 0 +CAAGTCGTAACACGGTAA 325 0 +CAAGTGACTTCCAATCAC 320 0 +CAAGTGCACCTTCCGGTA 323 0 +CAAGTTACCCTAGGGATA 325 0 +CAAGTTTTCACTTATGGC 325 0 +CAATCACTTGTCTTTTAA 324 0 +CAATCCTCTCCCAGAGTC 325 0 +CAATCTTTGGCTTAAAAG 324 0 +CAATGACTTGAAGAACCA 325 0 +CAATTCCTCCCTTTCTCG 324 0 +CAATTTACCATGCTCTGC 323 0 +CACAAAGGCATGGTCCCG 324 0 +CACAGAACATCTGACCAA 320 0 +CACAGAGACGGGTTGGCC 325 0 +CACAGGGTAAGCTGACGA 325 0 +CACATAACCACTCTGTCA 320 0 +CACATGCAAGTCTCCGCA 324 0 +CACCAATCTTTGGCTTAA 321 0 +CACCAGGTTCGGTAGGTC 323 0 +CACCATTAGGATGTCCTG 325 0 +CACCCCCAAGGGAATTCA 324 0 +CACCGCCCGTCACTCTCC 325 0 +CACCGCGGTTAGACGAGA 325 0 +CACCGTCAGGTCCTTTGG 324 0 +CACCTTCCGGTACACTTA 324 0 +CACGAGTTTTACCGGCCC 324 0 +CACGGTAAGTGTACCGGA 324 0 +CACGGTCAAAATACCGCG 323 0 +CACGTAGGACTTTAATCG 320 0 +CACTCTATCAAAGTGGTC 325 0 +CACTCTTTTGCCACAGAG 325 0 +CACTGGGCAGGCTGGACC 323 0 +CACTTACACCGAGAAGAC 323 0 +CACTTACCGTGTTACGAC 325 0 +CAGAAAGCGTTAAAGCTC 325 0 +CAGACCGGAGCAATCCAG 324 0 +CAGACCTACCGAACCTGG 323 0 +CAGAGCATGGTAAATTGC 310 0 +CAGAGTCCATATCGACGA 324 0 +CAGATAGAAACTGACCTG 323 0 +CAGCCACCGCGGTTAGAC 325 0 +CAGCCCCTTTAACAAAGG 324 0 +CAGCGAGCATCCATCTAC 324 0 +CAGCGCAATCCTCTCCCA 325 0 +CAGCTATCACCAGGTTCG 325 0 +CAGCTTACCCTGTGAAGG 325 0 +CAGCTTTCCCTTGCGGTA 324 0 +CAGCTTTCGTGGGGTCAG 323 0 +CAGGCTGGACCTCCTATA 324 0 +CAGGGTAAGCTGACGACG 325 0 +CAGGTCCTTTGGGTTTTA 325 0 +CAGGTCGAGGTGTAGCGC 325 0 +CAGGTGAGCTACCCCGAG 325 0 +CAGTTAAGCCCTCGTTTA 325 0 +CAGTTTCTATCTGTAACG 323 0 +CATAAGTGAAAACTTGAC 325 0 +CATACCCCGAACATGACG 324 0 +CATACTGGGGTGGCAGAG 323 0 +CATATATATATATATATA 309 0 +CATCACCCGTTATAGAAG 323 0 +CATCCTAATGGTGCAGCC 325 0 +CATCGAGGTCGTAAACCC 325 0 +CATCGGTCTTGTAATCCG 325 0 +CATCTCACTTACACCGAG 324 0 +CATCTGGTTCCTATTTCA 321 0 +CATGATGCAAAAGGTACA 325 0 +CATGCGCTACACCTCGAC 325 0 +CATGCTCTGCCACCCCAG 325 0 +CATGGTAAATTGCGAAAG 306 0 +CATGTGCTTGGTGGGCCA 324 0 +CATGTTCGGGGTATGGGC 325 0 +CATTAGGATGTCCTGATC 325 0 +CATTATCTTTCCTTGGGG 324 0 +CCAAAAACTCTTGGTGCA 325 0 +CCAAAGCCAGAATTCTAA 319 0 +CCAAAGGGTCTTCTCGTC 325 0 +CCAACATCGAGGTCGTAA 325 0 +CCAACCCGTCTCTGTGGC 325 0 +CCAAGACTGTGTGATTGG 322 0 +CCAAGAGTTTTTGGTTCC 323 0 +CCAAGCACATGCCTATCA 323 0 +CCAAGGAAAGATAATGAA 325 0 +CCAATCTTTGGCTTAAAA 324 0 +CCAATGACTTGAAGAACC 325 0 +CCACAGAACATCTGACCA 321 0 +CCACAGAGACGGGTTGGC 325 0 +CCACATAACCACTCTGTC 319 0 +CCACCAAGCACATGCCTA 323 0 +CCACCGCGGTTAGACGAG 325 0 +CCACCGTTGTTATTCAAC 317 0 +CCACTCTTTTGCCACAGA 325 0 +CCAGAGGTTCAAATCCTC 323 0 +CCAGAGTCCATATCGACG 324 0 +CCAGCCACATAACCACTC 321 0 +CCAGCCACCGCGGTTAGA 325 0 +CCAGCCTATATACCGCCG 323 0 +CCAGCGAGCATCCATCTA 324 0 +CCAGCTATCACCAGGTTC 325 0 +CCAGGTGAGCTACCCCGA 325 0 +CCATAAGTGAAAACTTGA 325 0 +CCATACCCCGAACATGAC 325 0 +CCATGCTCTGCCACCCCA 325 0 +CCCAAAGCCAGAATTCTA 320 0 +CCCAAGGAAAGATAATGA 325 0 +CCCAAGGGAATTCAGCAG 325 0 +CCCACTCTTTTGCCACAG 324 0 +CCCAGAGTCCATATCGAC 324 0 +CCCAGCCTATATACCGCC 323 0 +CCCATACCCCGAACATGA 325 0 +CCCCAACCGAAGGTAAAA 320 0 +CCCCAAGGGAATTCAGCA 325 0 +CCCCCAAGGGAATTCAGC 325 0 +CCCCCCTACCCCCCTACG 295 0 +CCCCCTACCCCCCTACGC 314 0 +CCCCGAACATGACGGTTA 324 0 +CCCCGAGACAGCCTATTA 322 0 +CCCCTCAGTTCCTAGAAA 319 0 +CCCGAACATGACGGTTAA 322 0 +CCCGCTCCTCGTCCCCGG 305 0 +CCCGGGGACGAGGAGCGG 305 0 +CCCGTCTCTGTGGCAAAA 325 0 +CCCTACCTGTGGCAATCA 324 0 +CCCTAGAGGAGCCTGTTC 325 0 +CCCTAGGGATAACAGCGC 325 0 +CCCTCCTCCGCCAATGAA 311 0 +CCCTGAGCCAAACAGCTA 324 0 +CCCTTAATAGCGGCTGCA 324 0 +CCGAACATGACGGTTAAA 323 0 +CCGAAGATCGGAGGTTAA 322 0 +CCGAGAAGACATCCATGC 324 0 +CCGAGACCAATGACTTGA 320 0 +CCGATCCTTTCGTACTAG 323 0 +CCGATCTTCGGATTACAA 321 0 +CCGCAAGGGAAAGCTGAA 324 0 +CCGCCAGGGTACTACGAG 325 0 +CCGCCCGTCACTCTCCCC 325 0 +CCGCCGTCGTCAGCTTAC 325 0 +CCGCGGTGGCTGGCACGA 325 0 +CCGCGGTTAGACGAGAGG 325 0 +CCGGAAGGTGCACTTGGA 323 0 +CCGGAGCAATCCAGGTCA 324 0 +CCGGCTCTGAGACGCGTA 324 0 +CCGGGTAGAAGTGACAGA 323 0 +CCGGTAAAACTCGTGCCA 324 0 +CCGGTCTGAACTCAGATC 325 0 +CCGTCTCTGTGGCAAAAG 325 0 +CCGTGCAAAGGTAGCGCA 324 0 +CCGTGGTCGCCCCAACCG 325 0 +CCGTTGTTATTCAACTAC 316 0 +CCTACAAACTCTTAGTTA 325 0 +CCTACCTGTGGCAATCAC 324 0 +CCTACGTGATCTGAGTTC 325 0 +CCTAGGGATAACAGCGCA 325 0 +CCTAGGGTAACTTGGTTC 325 0 +CCTAGTACGAAAGGATCG 323 0 +CCTCCGATCTTCGGATTA 320 0 +CCTCCGTGGTCGCCCCAA 325 0 +CCTCGATCCTACAAACTC 325 0 +CCTCGATGTTGGATCAGG 325 0 +CCTCGCCTGTTTACCAAA 325 0 +CCTCTCCCAGAGTCCATA 325 0 +CCTCTGATTTCGGCTCAG 324 0 +CCTCTGTCTTCGGGGCTA 312 0 +CCTGAAATAGGAACCAGA 321 0 +CCTGAGCCAAACAGCTAG 324 0 +CCTGATGCCCGCTCCTCG 321 0 +CCTGTATGAATGGCTAAA 325 0 +CCTGTTCTAGAACCGATA 325 0 +CCTTAACCACCCTTTACG 324 0 +CCTTAATAGCGGCTGCAC 324 0 +CCTTCCGGTACACTTACC 324 0 +CCTTCGGTTGGGGCGACC 325 0 +CCTTGGGGTTTAACCAAG 323 0 +CCTTTAGTTTGAAACCCC 324 0 +CCTTTCGTACTAGGAAAA 324 0 +CCTTTGGAGCTTAAGGTA 325 0 +CGAACCAAGTTACCCTAG 325 0 +CGAACCCTTAATAGCGGC 323 0 +CGAAGATCGGAGGTTAAA 322 0 +CGACCTCGATGTTGGATC 325 0 +CGACGGCGGTATATAGGC 324 0 +CGAGAAGACATCCATGCA 323 0 +CGAGAAGACCCTTTGGAG 325 0 +CGAGACCAATGACTTGAA 320 0 +CGAGGCTTGTGTTTGCCG 320 0 +CGAGGGCTTAACTGTCTC 325 0 +CGATATGGACTCTGGGAG 324 0 +CGATCCTACAAACTCTTA 325 0 +CGATCTTCGGATTACAAG 321 0 +CGATGTTGGATCAGGACA 325 0 +CGATGTTTTTGGTAAACA 324 0 +CGCAAGGGAAAGCTGAAA 324 0 +CGCAATCCTCTCCCAGAG 325 0 +CGCCAGGGTACTACGAGC 325 0 +CGCCCCAACCGAAGGTAA 325 0 +CGCCGTCGTCAGCTTACC 325 0 +CGCCTGTTTACCAAAAAC 325 0 +CGCGGTGGCTGGCACGAG 325 0 +CGCGGTTAGACGAGAGGC 322 0 +CGCTACACCTCGACCTGA 325 0 +CGCTACCTTTGCACGGTC 324 0 +CGCTTAGCTGTTAACTAA 325 0 +CGGAGACTTGCATGTGTA 324 0 +CGGAGCAATCCAGGTCAG 324 0 +CGGCCGCGGTATTTTGAC 323 0 +CGGCGTAAAGGGTGGTTA 324 0 +CGGCTCAGGCACCAAATA 324 0 +CGGCTCTGAGACGCGTAC 324 0 +CGGGAAAAGCCCCGGCAG 321 0 +CGGGCCCATACCCCGAAC 323 0 +CGGGGACGAGGAGCGGGC 304 0 +CGGGGCTACAACCCACCG 322 0 +CGGGGTATGGGCCCGAAA 323 0 +CGGGGTTTCAAACTAAAG 325 0 +CGGGTAGAAGTGACAGAC 322 0 +CGGTAAAACTCGTGCCAG 324 0 +CGGTAAGTGTACCGGAAG 324 0 +CGGTAGGTCTGTCACTTC 321 0 +CGGTATTTTGACCGTGCA 324 0 +CGGTCAAAATACCGCGGC 323 0 +CGGTCTGAACTCAGATCA 325 0 +CGGTCTTGTAATCCGAAG 325 0 +CGGTGTGTACGCGTCTCA 324 0 +CGGTTAGACGAGAGGCCC 322 0 +CGGTTCTAGAACAGGCTC 325 0 +CGTAACACGGTAAGTGTA 325 0 +CGTACACACCGCCCGTCA 325 0 +CGTCAGCTTACCCTGTGA 325 0 +CGTCAGGTCGAGGTGTAG 325 0 +CGTCTCAGAGCCGGGTTC 324 0 +CGTCTCTGTGGCAAAAGA 325 0 +CGTGATCTGAGTTCAGAC 325 0 +CGTGCAAAGGTAGCGCAA 324 0 +CGTGGTCGCCCCAACCGA 325 0 +CGTTAAAGCTCAGACAGA 325 0 +CGTTGAACAAACGAACCC 325 0 +CGTTGTTATTCAACTACA 317 0 +CGTTTAGCCATTCATACA 325 0 +CGTTTGTTCAACGATTAA 325 0 +CTAAAATGAGTAACAAGA 325 0 +CTAAATCATGATGCAAAA 325 0 +CTAAATCCTCCTTCAAGC 324 0 +CTAACCGCGGTGGCTGGC 325 0 +CTAAGAGTTTGTAGGATC 325 0 +CTAATCCCAGTTTGTTTC 325 0 +CTAATGGTGCAGCCGCTA 324 0 +CTACAAACTCTTAGTTAA 325 0 +CTACAACCCACCGCCTAA 320 0 +CTACCCCGAGACAGCCTA 323 0 +CTACCGAACCTGGTGATA 325 0 +CTACCTTTGCACGGTCAA 324 0 +CTACGTGATCTGAGTTCA 325 0 +CTACTAAATCCTCCTTCA 324 0 +CTAGAAAGATTATGAGCC 275 0 +CTAGAACAGGCTCCTCTA 325 0 +CTAGAGGAGCCTGTTCTA 325 0 +CTAGGGATAACAGCGCAA 325 0 +CTAGTGGGCCTAAAAGCA 324 0 +CTATATACCGCCGTCGTC 324 0 +CTATCACCAGGTTCGGTA 325 0 +CTATTCCGGCTCAGGCAC 324 0 +CTCAAGAGATCAAAACTC 320 0 +CTCACTTACACCGAGAAG 323 0 +CTCAGAGCCGGGTTCAAA 324 0 +CTCATAATCTTTCTAGTA 275 0 +CTCCCAAAGCCAGAATTC 320 0 +CTCCGATCTTCGGATTAC 321 0 +CTCCGGGTAGAAGTGACA 323 0 +CTCCGGTCTGAACTCAGA 325 0 +CTCCGTGGTCGCCCCAAC 325 0 +CTCGATGTTGGATCAGGA 325 0 +CTCGCCTGTTTACCAAAA 325 0 +CTCTGAGACGCGTACACA 324 0 +CTCTGATTTCGGCTCAGA 324 0 +CTCTGTCTTCGGGGCTAC 312 0 +CTCTTAGTTAACAGCTAA 325 0 +CTCTTCCCACTCTTTTGC 323 0 +CTCTTGGTGCAAATCCAA 324 0 +CTCTTTTGCCACAGAGAC 325 0 +CTGAACTCAGATCACGTA 325 0 +CTGAGACACCGTCAGGTC 325 0 +CTGAGACGCGTACACACC 324 0 +CTGAGCCAAACAGCTAGC 324 0 +CTGAGCTTTAACGCTTTC 325 0 +CTGAGTTCAGACCGGAGC 325 0 +CTGCACCATTAGGATGTC 325 0 +CTGCCACCCCAGTATGTC 316 0 +CTGCTTTTAGGCCCACTA 324 0 +CTGGCTAAATCATGATGC 325 0 +CTGGCTCCCAAAGCCAGA 307 0 +CTGGGATTAGATACCCCA 325 0 +CTGGGCAGGCTGGACCTC 324 0 +CTGGTGATAGCTGGTTGC 325 0 +CTGGTTTCAAGCCAGCCA 322 0 +CTGTATGAATGGCTAAAC 325 0 +CTGTCACTTCTACCCGGA 323 0 +CTGTCTCGGGGTAGCTCA 324 0 +CTGTCTGAGCTTTAACGC 325 0 +CTGTCTTCGGGGCTACAA 315 0 +CTGTGTGATTGGAAGTCA 319 0 +CTGTTATCCCTAGGGTAA 325 0 +CTGTTCTAGAACCGATAA 325 0 +CTGTTCTAGTGGGCCTAA 317 0 +CTGTTTACCAAAAACATC 324 0 +CTTAAAACCCAAAGGACC 325 0 +CTTAACCACCCTTTACGC 324 0 +CTTAAGCTCCAAAGGGTC 325 0 +CTTAATAGCGGCTGCACC 324 0 +CTTACACATGCAAGTCTC 325 0 +CTTAGCTGTTAACTAAGA 325 0 +CTTAGGAACCAAAAACTC 323 0 +CTTAGGCAACCAGCTATC 324 0 +CTTAGTTAACAGCTAAGC 325 0 +CTTCAAGTCATTGGTCTC 325 0 +CTTCACAGGGTAAGCTGA 325 0 +CTTCCCACTCTTTTGCCA 324 0 +CTTCGGGGCTACAACCCA 319 0 +CTTCGGTTGGGGCGACCA 325 0 +CTTGGATTTGCACCAAGA 324 0 +CTTGGGGTTTAACCAAGA 323 0 +CTTGTAGTTGAATAACAA 318 0 +CTTGTGTTTGCCGAGTTC 323 0 +CTTGTTACTCATTTTAGC 325 0 +CTTTAATCGTTGAACAAA 325 0 +CTTTAGAATCACAATCTA 322 0 +CTTTCAGCTTTCCCTTGC 323 0 +CTTTCCTTGGGGTTTAAC 323 0 +CTTTCGGGCCCATACCCC 323 0 +CTTTCGTACTAGGAAAAG 323 0 +CTTTGCACGGTCAAAATA 324 0 +CTTTGGAGCTTAAGGTAC 325 0 +CTTTGGCTTAAAAGGCCA 324 0 +CTTTTAGGCCCACTAGAA 323 0 +CTTTTCTTTTAGTCTTTC 323 0 +CTTTTGCATCATGATTTA 325 0 +GAAAATGTAGCCCATTTC 325 0 +GAAACTGACCTGGATTGC 325 0 +GAAAGATTATGAGCCTCA 283 0 +GAAAGGGAGGAATTGAAC 325 0 +GAAATAGGAACCAGATGC 322 0 +GAACAAACGAACCCTTAA 324 0 +GAACCACCGTTGTTATTC 317 0 +GAAGAACCACCGTTGTTA 324 0 +GAAGATTTTCACTTCTAC 322 0 +GAAGCTAAATATTGGACA 296 0 +GAAGGAACTCGGCAAACA 323 0 +GAAGGCCTCGATCCTACA 325 0 +GAATAGAAGTTCAGCCTC 324 0 +GAATTCAGCAGTGATAAA 325 0 +GACAAGTGATTGCGCTAC 324 0 +GACCAATGACTTGAAGAA 325 0 +GACCTCGATGTTGGATCA 325 0 +GACCTCTGATTTCGGCTC 324 0 +GACCTGTATGAATGGCTA 325 0 +GACCTTTAGTTTGAAACC 324 0 +GACGAGGAGCGGGCATCA 312 0 +GACGCGTACACACCGCCC 325 0 +GACGGGCGGTGTGTACGC 325 0 +GACGGTGTCTCAGACCCC 323 0 +GACTTTAATCGTTGAACA 320 0 +GAGAAAGGGAGGAATTGA 325 0 +GAGAAGACATCCATGCAA 324 0 +GAGAAGACCCTTTGGAGC 325 0 +GAGACCTGTATGAATGGC 325 0 +GAGACCTTTAGTTTGAAA 324 0 +GAGACGCGTACACACCGC 324 0 +GAGAGGATTGCGCTGTTA 325 0 +GAGCATGGTAAATTGCGA 306 0 +GAGCGCTTAGCTGTTAAC 325 0 +GAGCGGGCATCAGGCACA 322 0 +GAGCTACCCCGAGACAGC 324 0 +GAGGAGCCTGTTCTAGAA 325 0 +GAGGAGCGGGCATCAGGC 321 0 +GAGGATTGCGCTGTTATC 325 0 +GAGGATTTAGTAGTAAAA 325 0 +GAGGCGATGTTTTTGGTA 325 0 +GAGGCTTGTGTTTGCCGA 320 0 +GAGGGCTTAACTGTCTCC 325 0 +GAGTTCAGACCGGAGCAA 325 0 +GATACCCCACTATGCTCA 325 0 +GATAGAAACTGACCTGGA 323 0 +GATATGGACTCTGGGAGA 325 0 +GATCAACGAACCAAGTTA 325 0 +GATCACGTAGGACTTTAA 325 0 +GATCCTTTCGTACTAGGA 323 0 +GATCTTCGGATTACAAGA 321 0 +GATGCCAGTAATAGTTCA 322 0 +GATGCCCGCTCCTCGTCC 308 0 +GATGTCTTCTCGGTGTAA 323 0 +GATTAAAGTCCTACGTGA 320 0 +GATTGCGCTACCTTTGCA 324 0 +GATTGCTCCGGTCTGAAC 325 0 +GATTGGAAGTCACTTGTA 325 0 +GATTTTCACTTCTACTTA 324 0 +GCAAACACAAGCCTCGCC 320 0 +GCAAAGGTAGCGCAATCA 324 0 +GCAATCACTTGTCTTTTA 324 0 +GCACAAAGGCATGGTCCC 324 0 +GCACAAGTGTAAGCCAAA 324 0 +GCACGAGTTTTACCGGCC 324 0 +GCACGGTCAAAATACCGC 324 0 +GCACTGAAGATGCTAAGA 325 0 +GCATAGCACTGAAGATGC 324 0 +GCATAGTGGGGTATCTAA 325 0 +GCATCACCCGTTATAGAA 323 0 +GCATCGGTCTTGTAATCC 325 0 +GCATCTCACTTACACCGA 324 0 +GCATGGTAAATTGCGAAA 307 0 +GCATGTGCTTGGTGGGCC 324 0 +GCCACACCCCCAAGGGAA 324 0 +GCCACAGAACATCTGACC 321 0 +GCCACCGCGGTTAGACGA 325 0 +GCCAGGGTACTACGAGCA 325 0 +GCCCACCAAGCACATGCC 323 0 +GCCCCAACCGAAGGTAAA 325 0 +GCCGGGGCTTTTCCCGCC 310 0 +GCCGGTAAAACTCGTGCC 324 0 +GCCGTCGTCAGCTTACCC 325 0 +GCCTCGCCTGTTTACCAA 325 0 +GCCTGTTTACCAAAAACA 325 0 +GCCTTTCGCAATTTACCA 303 0 +GCGAGGCTTGTGTTTGCC 320 0 +GCGATGTTTTTGGTAAAC 324 0 +GCGCAATCCTCTCCCAGA 325 0 +GCGCTTAGCTGTTAACTA 325 0 +GCGGCTGCACCATTAGGA 324 0 +GCGGGAAAAGCCCCGGCA 319 0 +GCGGTGGGTTGTAGCCCC 322 0 +GCGGTTAGACGAGAGGCC 322 0 +GCTAAATCATGATGCAAA 325 0 +GCTACAACCCACCGCCTA 322 0 +GCTACACCTCGACCTGAC 325 0 +GCTACCTTTGCACGGTCA 324 0 +GCTCTGAGACGCGTACAC 324 0 +GCTGAAAGAGAAATGAAA 325 0 +GCTGACGACGGCGGTATA 325 0 +GCTGAGCATAGTGGGGTA 325 0 +GCTGGCACGAGTTTTACC 324 0 +GCTGGCTTGAAACCAGCA 323 0 +GCTGGTTTCAAGCCAGCC 323 0 +GCTGTTATCCCTAGGGTA 325 0 +GCTTAAAACCCAAAGGAC 325 0 +GCTTTCGGGCCCATACCC 323 0 +GCTTTTAGGCCCACTAGA 323 0 +GGAAAGACTAAAAGAAAA 323 0 +GGAAAGCTGAAAGAGAAA 325 0 +GGAACCAGATGCCAGTAA 322 0 +GGAACTCGGCAAACACAA 323 0 +GGAAGGAACTCGGCAAAC 324 0 +GGAAGGCCTCGATCCTAC 325 0 +GGAATTCAGCAGTGATAA 325 0 +GGACATACTGGGGTGGCA 312 0 +GGACATCCTAATGGTGCA 325 0 +GGACCATGCCTTTGTGCA 323 0 +GGACCTGACGGTGTCTCA 325 0 +GGACTTTAATCGTTGAAC 320 0 +GGAGACTTGCATGTGTAA 325 0 +GGAGCCTGTTCTAGAACC 325 0 +GGAGCGGGCATCAGGCAC 321 0 +GGAGCTTAAGGTACAAAA 325 0 +GGAGGAATTGAACCCCCA 325 0 +GGAGGATAAAGATCATAA 325 0 +GGAGGATTTAGTAGTAAA 325 0 +GGAGGTCCAGCCTGCCCA 324 0 +GGATGTCCTGATCCAACA 325 0 +GGATGTCTTCTCGGTGTA 323 0 +GGATTAGATACCCCACTA 325 0 +GGATTGCGCTGTTATCCC 325 0 +GGATTGCTCCGGTCTGAA 324 0 +GGCAAAAGAGTGGGAAGA 323 0 +GGCAACCAGCTATCACCA 325 0 +GGCAAGCCTACGAAAAAC 324 0 +GGCAGGCTGGACCTCCTA 324 0 +GGCATGGTCCCGACCTTA 321 0 +GGCCGCGGTATTTTGACC 323 0 +GGCCTCGATCCTACAAAC 325 0 +GGCCTTTCGCAATTTACC 310 0 +GGCCTTTTAAGCCAAAGA 324 0 +GGCGATGTTTTTGGTAAA 324 0 +GGCGGTGGGTTGTAGCCC 323 0 +GGCGTAAAGGGTGGTTAA 324 0 +GGCTAAATCATGATGCAA 325 0 +GGCTCAGGCACCAAATAC 324 0 +GGCTCTGAGACGCGTACA 324 0 +GGCTGCTTTTAGGCCCAC 324 0 +GGCTGGCACGAGTTTTAC 325 0 +GGCTTAGGCCTTTCGCAA 325 0 +GGGAAAGCTGAAAGAGAA 325 0 +GGGAAGGCCTCGATCCTA 325 0 +GGGAATTCAGCAGTGATA 325 0 +GGGAGGAATTGAACCCCC 325 0 +GGGAGGCAAGTCGTAACA 325 0 +GGGCCCATACCCCGAACA 325 0 +GGGCGACCACGGAGGAAA 323 0 +GGGCTTAGGCCTTTCGCA 325 0 +GGGGACGAGGAGCGGGCA 305 0 +GGGGAGGCAAGTCGTAAC 325 0 +GGGGCGACCACGGAGGAA 324 0 +GGGGGTACAGCCCCTTTA 325 0 +GGGGTACAGCCCCTTTAA 325 0 +GGGGTTAAAATCCCCTCA 325 0 +GGGTACAGCCCCTTTAAC 325 0 +GGGTAGAAGTGACAGACC 322 0 +GGGTCTGAGACACCGTCA 324 0 +GGGTCTTCTCGTCTTGTA 325 0 +GGGTGGCAGAGCATGGTA 325 0 +GGGTTATCGGTTCTAGAA 325 0 +GGGTTGTAGCCCCGAAGA 319 0 +GGTAACTTGGTTCGTTGA 325 0 +GGTACACTTACCGTGTTA 324 0 +GGTACAGCCCCTTTAACA 325 0 +GGTATTTTGACCGTGCAA 324 0 +GGTCAGTTTCTATCTGTA 323 0 +GGTCTGAACTCAGATCAC 325 0 +GGTCTTAGGAACCAAAAA 325 0 +GGTCTTGTAATCCGAAGA 325 0 +GGTGAGCTACCCCGAGAC 325 0 +GGTGCCTGAGCCGGAATA 324 0 +GGTGGCAGAGCATGGTAA 325 0 +GGTGGGTTGTAGCCCCGA 323 0 +GGTTAAACCCCAAGGAAA 323 0 +GGTTATCGGTTCTAGAAC 325 0 +GGTTCGGTAGGTCTGTCA 322 0 +GGTTCGTTTGTTCAACGA 325 0 +GGTTCTTGTAGTTGAATA 319 0 +GGTTGGGGCGACCACGGA 325 0 +GGTTGTAGCCCCGAAGAC 319 0 +GGTTTCAAGCCAGCCACA 324 0 +GGTTTCGGGGTTTCAAAC 324 0 +GTAACACGGTAAGTGTAC 325 0 +GTACACACCGCCCGTCAC 325 0 +GTACAGCCCCTTTAACAA 325 0 +GTACCTTTTGCATCATGA 325 0 +GTACGAAAGGATCGGAAA 325 0 +GTAGAAGTGACAGACCTA 323 0 +GTAGGTCTGTCACTTCTA 321 0 +GTAGTACCCTGGCGGACA 325 0 +GTATGAATGGCTAAACGA 325 0 +GTATTTTGACCGTGCAAA 324 0 +GTCACTGGGCAGGCTGGA 323 0 +GTCAGCTTACCCTGTGAA 325 0 +GTCAGTTTCTATCTGTAA 323 0 +GTCATTGGTCTCGGTTAA 318 0 +GTCCAGCCTGCCCAGTGA 323 0 +GTCCTGATCCAACATCGA 325 0 +GTCTCAGAGCCGGGTTCA 324 0 +GTCTTAGGAACCAAAAAC 325 0 +GTCTTGGTTAAACCCCAA 323 0 +GTGACAGACCTACCGAAC 323 0 +GTGACTTCCAATCACACA 317 0 +GTGAGCTACCCCGAGACA 325 0 +GTGATAGCTGGTTGCCTA 325 0 +GTGCACCTTCCGGTACAC 323 0 +GTGCTTCCTTTACACCAC 323 0 +GTGCTTGGTGGGCCATTA 325 0 +GTGGCAAAAGAGTGGGAA 324 0 +GTGGCAGAGCATGGTAAA 324 0 +GTGGCTGGCTTGAAACCA 323 0 +GTGGGGTATCTAATCCCA 325 0 +GTGGGTTGTAGCCCCGAA 322 0 +GTGGTCGCCCCAACCGAA 325 0 +GTGTGTACGCGTCTCAGA 324 0 +GTTAACTAAGAGTTTGTA 325 0 +GTTACAGATAGAAACTGA 323 0 +GTTACCCTAGGGATAACA 325 0 +GTTACTCATTTTAGCATA 325 0 +GTTATCGGTTCTAGAACA 325 0 +GTTATGTGGCTGGCTTGA 322 0 +GTTATTCAACTACAAGAA 321 0 +GTTCCTAGAAAGAAGGGA 317 0 +GTTCTAGTGGGCCTAAAA 319 0 +GTTCTTGTAGTTGAATAA 320 0 +GTTGTAGCCCCGAAGACA 315 0 +GTTGTTATTCAACTACAA 318 0 +GTTTGTTCAACGATTAAA 325 0 +GTTTTCACTTATGGCTTA 323 0 +TAAAACTCGTGCCAGCCA 325 0 +TAAAATGAGTAACAAGAA 325 0 +TAAATCCTCCTTCAAGCA 323 0 +TAAATTGCGAAAGGCCTA 317 0 +TAACAGCTAAGCGCTCAA 325 0 +TAACCGCGGTGGCTGGCA 325 0 +TAACCTCCGATCTTCGGA 321 0 +TAACGAGAAAGGGAGGAA 324 0 +TAAGCCAAAGATTGGTGA 322 0 +TAAGGGTTCGTTTGTTCA 324 0 +TAATCGTTGAACAAACGA 325 0 +TACAGCCCCTTTAACAAA 325 0 +TACCTTCGGTTGGGGCGA 325 0 +TACCTTTGCACGGTCAAA 324 0 +TACGAAAGGATCGGAAAA 325 0 +TACGACCTCGATGTTGGA 325 0 +TACTGGGGTGGCAGAGCA 323 0 +TAGAATCACAATCTAATA 320 0 +TAGCACTGAAGATGCTAA 325 0 +TAGCTGGTTGCCTAAGAA 324 0 +TAGGACTTTAATCGTTGA 320 0 +TAGGATGTCCTGATCCAA 325 0 +TAGTACAAGTGACTTCCA 324 0 +TAGTACGAAAGGATCGGA 324 0 +TAGTTTGAAACCCCGAAA 324 0 +TATATACCGCCGTCGTCA 324 0 +TATATATATATATATATA 312 0 +TATGAAAACAAATAAATA 325 0 +TATGCTGGTTTCAAGCCA 324 0 +TATGTGGCTGGCTTGAAA 323 0 +TATTTAAAAGACAAGTGA 323 0 +TCACTTACACCGAGAAGA 323 0 +TCAGACCGGAGCAATCCA 324 0 +TCATTGGCGGAGGAGGGA 306 0 +TCATTGGTCTCGGTTAAA 322 0 +TCCCACTCTTTTGCCACA 324 0 +TCCCAGAGTCCATATCGA 324 0 +TCCGATCTTCGGATTACA 321 0 +TCCGGCTCAGGCACCAAA 323 0 +TCCTCCGTGGTCGCCCCA 324 0 +TCCTTGGGGTTTAACCAA 323 0 +TCCTTTCGTACTAGGAAA 324 0 +TCGCCTGTTTACCAAAAA 325 0 +TCGGGCCCATACCCCGAA 323 0 +TCGGGGTATGGGCCCGAA 322 0 +TCGGGGTTTCAAACTAAA 325 0 +TCGGTCTTGTAATCCGAA 324 0 +TCTCACTTACACCGAGAA 323 0 +TCTCAGAGCCGGGTTCAA 324 0 +TCTGATTTCGGCTCAGAA 324 0 +TCTGTCTTCGGGGCTACA 313 0 +TCTTGTAGTTGAATAACA 321 0 +TCTTTCCTTGGGGTTTAA 323 0 +TGAGCGCTTAGCTGTTAA 325 0 +TGAGTTCAGACCGGAGCA 325 0 +TGATAGCTGGTTGCCTAA 324 0 +TGCATCATGATTTAGCCA 325 0 +TGCTAAAATGAGTAACAA 325 0 +TGGAGCTTAAGGTACAAA 325 0 +TGGATCAGGACATCCTAA 325 0 +TGGCAAGCCTACGAAAAA 324 0 +TGGCTGCTTTTAGGCCCA 325 0 +TGGTCTTAGGAACCAAAA 325 0 +TGGTGCAGCCGCTATTAA 324 0 +TGGTTAAACCCCAAGGAA 323 0 +TGGTTTCGGGGTTTCAAA 324 0 +TGTACCTTAAGCTCCAAA 325 0 +TTAAGCCATAAGTGAAAA 323 0 +TTAGATTGTGATTCTAAA 321 0 +TTAGTTTGAAACCCCGAA 325 0 +TTATGTGGCTGGCTTGAA 322 0 +TTCCCTTCTTTCTAGGAA 323 0 +TTCCGGCTCAGGCACCAA 323 0 +TTGGAGCTTAAGGTACAA 325 0 +TTGGCCTTTTAAGCCAAA 324 0 +TTGGTCTTAGGAACCAAA 325 0 +TTTATGAAAACAAATAAA 325 0 diff --git a/pkg/obiprimer/testdata/ecoprimers_Teleostei.words.tsv b/pkg/obiprimer/testdata/ecoprimers_Teleostei.words.tsv new file mode 100644 index 0000000..89319e3 --- /dev/null +++ b/pkg/obiprimer/testdata/ecoprimers_Teleostei.words.tsv @@ -0,0 +1,255 @@ +AAAACTCGTGCCAGCCAC 3891 0 +AAACCCAAAGGACTTGGC 3890 0 +AAACGAACCCTTAATAGC 3875 0 +AAACTCGTGCCAGCCACC 3894 0 +AAACTGGGATTAGATACC 3896 0 +AAAGTCCTACGTGATCTG 3901 0 +AAATCATGATGCAAAAGG 3894 0 +AACAAACGAACCCTTAAT 3888 0 +AACATCGAGGTCGTAAAC 3893 0 +AACATGGTAAGTGTACCG 3894 0 +AACCAAAAACTCTTGGTG 3854 0 +AACCCAAAGGACTTGGCG 3888 0 +AACCCTTAATAGCGGCTG 3858 0 +AACCGCGGTGGCTGGCAC 3896 0 +AACGAACCCTTAATAGCG 3860 0 +AACGATTAAAGTCCTACG 3700 0 +AACGTCAGGTCGAGGTGT 3894 0 +AACTCAGATCACGTAGGA 3899 0 +AACTCGTGCCAGCCACCG 3893 0 +AACTGGGATTAGATACCC 3896 0 +AAGAGTTTTTGGTTCCTA 3848 0 +AAGCCTCGCCTGTTTACC 3876 0 +AAGGGTTCGTTTGTTCAA 3889 0 +AAGGTAGCGCAATCACTT 3889 0 +AAGTCCTACGTGATCTGA 3900 0 +AAGTCGTAACATGGTAAG 3893 0 +AAGTGCACCTTCCGGTAC 3888 0 +AAGTGTACCGGAAGGTGC 3889 0 +AATAGCGGCTGCACCATT 3839 0 +AATCACTTGTCTTTTAAA 3856 0 +AATCATGATGCAAAAGGT 3893 0 +AATCCAGGTCAGTTTCTA 3903 0 +AATCGTTGAACAAACGAA 3876 0 +AATGTAGCCCATTTCTTC 3871 0 +ACAAACGAACCCTTAATA 3885 0 +ACAAACTGGGATTAGATA 3893 0 +ACAAGTGATTGCGCTACC 3889 0 +ACACACCGCCCGTCACTC 3891 0 +ACACCGCCCGTCACTCTC 3890 0 +ACACTTACCATGTTACGA 3893 0 +ACATCCTAATGGTGCAGC 3858 0 +ACATCGAGGTCGTAAACC 3894 0 +ACATGGTAAGTGTACCGG 3894 0 +ACCAAAAACTCTTGGTGC 3875 0 +ACCAAGAGTTTTTGGTTC 3857 0 +ACCATGTTACGACTTGCC 3891 0 +ACCATTAGGATGTCCTGA 3891 0 +ACCCAAAGGACTTGGCGG 3886 0 +ACCCGTCTCTGTGGCAAA 3892 0 +ACCCTAGGGATAACAGCG 3895 0 +ACCCTTAATAGCGGCTGC 3861 0 +ACCGCCCGTCACTCTCCC 3893 0 +ACCGCGGTGGCTGGCACG 3895 0 +ACCGGAAGGTGCACTTGG 3887 0 +ACCTCGATGTTGGATCAG 3891 0 +ACCTTCCGGTACACTTAC 3893 0 +ACGAACCCTTAATAGCGG 3859 0 +ACGACCTCGATGTTGGAT 3889 0 +ACGATTAAAGTCCTACGT 3701 0 +ACGTCAGGTCGAGGTGTA 3894 0 +ACGTGATCTGAGTTCAGA 3901 0 +ACTCAGATCACGTAGGAC 3899 0 +ACTCGTGCCAGCCACCGC 3894 0 +ACTCTTTTGCCACAGAGA 3867 0 +ACTGGGATTAGATACCCC 3896 0 +ACTTACCATGTTACGACT 3894 0 +ACTTTAATCGTTGAACAA 3876 0 +AGAACAGGCTCCTCTAGG 3893 0 +AGACAAGTGATTGCGCTA 3883 0 +AGAGGAGCCTGTTCTAGA 3876 0 +AGAGTGACGGGCGGTGTG 3890 0 +AGAGTTTTTGGTTCCTAA 3851 0 +AGATAGAAACTGACCTGG 3900 0 +AGATCACGTAGGACTTTA 3901 0 +AGCCGCTATTAAGGGTTC 3856 0 +AGCCTCGCCTGTTTACCA 3889 0 +AGCCTGTTCTAGAACCGA 3871 0 +AGCGGCTGCACCATTAGG 3846 0 +AGGAACCAAAAACTCTTG 3842 0 +AGGACATCCTAATGGTGC 3866 0 +AGGACTTTAATCGTTGAA 3688 0 +AGGAGCCTGTTCTAGAAC 3874 0 +AGGATGTCCTGATCCAAC 3897 0 +AGGCAAGTCGTAACATGG 3868 0 +AGGCGATGTTTTTGGTAA 3894 0 +AGGGATAACAGCGCAATC 3896 0 +AGGGGAGGCAAGTCGTAA 3890 0 +AGGGGGTTTACGACCTCG 3894 0 +AGGGTTCGTTTGTTCAAC 3888 0 +AGGTAGCGCAATCACTTG 3888 0 +AGTCCTACGTGATCTGAG 3900 0 +AGTGGGGTATCTAATCCC 3896 0 +AGTTACCCTAGGGATAAC 3895 0 +AGTTTTTGGTTCCTAAGA 3848 0 +ATAGAAACTGACCTGGAT 3901 0 +ATAGCGGCTGCACCATTA 3837 0 +ATAGTGGGGTATCTAATC 3895 0 +ATCACGTAGGACTTTAAT 3880 0 +ATCACTTGTCTTTTAAAT 3856 0 +ATCAGGACATCCTAATGG 3892 0 +ATCATGATGCAAAAGGTA 3894 0 +ATCCTAATGGTGCAGCCG 3852 0 +ATCGAGGTCGTAAACCCC 3894 0 +ATCGGTTCTAGAACAGGC 3862 0 +ATCGTTGAACAAACGAAC 3877 0 +ATCTGAGTTCAGACCGGA 3902 0 +ATGATGCAAAAGGTACGA 3891 0 +ATGGTAAGTGTACCGGAA 3895 0 +ATGGTGCAGCCGCTATTA 3840 0 +ATGTAGCCCATTTCTTCC 3871 0 +ATGTCCTGATCCAACATC 3892 0 +ATGTTACGACTTGCCTCC 3864 0 +ATGTTGGATCAGGACATC 3893 0 +ATGTTTTTGGTAAACAGG 3887 0 +ATTAGATACCCCACTATG 3896 0 +ATTAGGATGTCCTGATCC 3893 0 +ATTGCGCTGTTATCCCTA 3896 0 +CAAAAACTCTTGGTGCAA 3886 0 +CAAACGAACCCTTAATAG 3882 0 +CAAACTGGGATTAGATAC 3895 0 +CAACATCGAGGTCGTAAA 3894 0 +CAACGATTAAAGTCCTAC 3691 0 +CAAGTCGTAACATGGTAA 3893 0 +CAAGTGCACCTTCCGGTA 3888 0 +CACCATTAGGATGTCCTG 3890 0 +CACCGCCCGTCACTCTCC 3888 0 +CACCTTCCGGTACACTTA 3891 0 +CACGTAGGACTTTAATCG 3702 0 +CACTCTTTTGCCACAGAG 3856 0 +CACTTACCATGTTACGAC 3894 0 +CATCCTAATGGTGCAGCC 3852 0 +CATCGAGGTCGTAAACCC 3894 0 +CATGATGCAAAAGGTACG 3890 0 +CATGGTAAGTGTACCGGA 3894 0 +CATGTTACGACTTGCCTC 3866 0 +CATTAGGATGTCCTGATC 3893 0 +CCAAAAACTCTTGGTGCA 3883 0 +CCAACATCGAGGTCGTAA 3894 0 +CCAAGAGTTTTTGGTTCC 3844 0 +CCACTCTTTTGCCACAGA 3879 0 +CCCACTCTTTTGCCACAG 3879 0 +CCCGTCTCTGTGGCAAAA 3894 0 +CCCTAGGGATAACAGCGC 3898 0 +CCCTTAATAGCGGCTGCA 3853 0 +CCGCCCGTCACTCTCCCC 3890 0 +CCGCGGTGGCTGGCACGA 3895 0 +CCGGTAAAACTCGTGCCA 3847 0 +CCGGTCTGAACTCAGATC 3904 0 +CCGTCTCTGTGGCAAAAG 3894 0 +CCTACGTGATCTGAGTTC 3899 0 +CCTAGGGATAACAGCGCA 3896 0 +CCTCGATGTTGGATCAGG 3892 0 +CCTCGCCTGTTTACCAAA 3892 0 +CCTGTTCTAGAACCGATA 3862 0 +CCTTAATAGCGGCTGCAC 3850 0 +CCTTCCGGTACACTTACC 3893 0 +CGAACCCTTAATAGCGGC 3855 0 +CGACCTCGATGTTGGATC 3890 0 +CGACGGCGGTATATAGGC 3890 0 +CGATGTTGGATCAGGACA 3892 0 +CGATGTTTTTGGTAAACA 3886 0 +CGCCTGTTTACCAAAAAC 3889 0 +CGCGGTGGCTGGCACGAG 3894 0 +CGCTTAGCTGTTAACTAA 3864 0 +CGGTAAAACTCGTGCCAG 3856 0 +CGGTCTGAACTCAGATCA 3904 0 +CGGTTCTAGAACAGGCTC 3873 0 +CGTAACATGGTAAGTGTA 3892 0 +CGTCAGGTCGAGGTGTAG 3893 0 +CGTCTCTGTGGCAAAAGA 3891 0 +CGTGATCTGAGTTCAGAC 3902 0 +CGTTGAACAAACGAACCC 3887 0 +CGTTTGTTCAACGATTAA 3877 0 +CTAAATCATGATGCAAAA 3896 0 +CTAATGGTGCAGCCGCTA 3840 0 +CTACGTGATCTGAGTTCA 3899 0 +CTAGAACAGGCTCCTCTA 3877 0 +CTAGAGGAGCCTGTTCTA 3891 0 +CTAGGGATAACAGCGCAA 3897 0 +CTCCGGTCTGAACTCAGA 3903 0 +CTCGATGTTGGATCAGGA 3892 0 +CTCGCCTGTTTACCAAAA 3892 0 +CTCGTACCTTTTGCATCA 3889 0 +CTCTTTTGCCACAGAGAC 3869 0 +CTGAACTCAGATCACGTA 3900 0 +CTGCACCATTAGGATGTC 3861 0 +CTGGGATTAGATACCCCA 3895 0 +CTGTTATCCCTAGGGTAA 3897 0 +CTGTTTACCAAAAACATC 3886 0 +CTTAATAGCGGCTGCACC 3848 0 +CTTAGCTGTTAACTAAGA 3878 0 +CTTAGGAACCAAAAACTC 3847 0 +CTTAGTTAACAGCTAAGC 3875 0 +CTTCCGGTACACTTACCA 3891 0 +CTTGGATTTGCACCAAGA 3899 0 +CTTTAATCGTTGAACAAA 3876 0 +CTTTTGCATCATGATTTA 3896 0 +GAACAAACGAACCCTTAA 3890 0 +GAAGGAACTCGGCAAACA 3889 0 +GACAAGTGATTGCGCTAC 3889 0 +GACCTCGATGTTGGATCA 3890 0 +GACTTTAATCGTTGAACA 3686 0 +GAGGAGCCTGTTCTAGAA 3876 0 +GAGGCGATGTTTTTGGTA 3893 0 +GATAGAAACTGACCTGGA 3900 0 +GATCACGTAGGACTTTAA 3900 0 +GATTAAAGTCCTACGTGA 3704 0 +GCAAGTCGTAACATGGTA 3891 0 +GCATAGTGGGGTATCTAA 3896 0 +GCCGGTAAAACTCGTGCC 3827 0 +GCCTCGCCTGTTTACCAA 3890 0 +GCCTGTTTACCAAAAACA 3889 0 +GCGATGTTTTTGGTAAAC 3888 0 +GCGCTTAGCTGTTAACTA 3899 0 +GCGGCTGCACCATTAGGA 3850 0 +GCTAAATCATGATGCAAA 3893 0 +GCTACACCTCGACCTGAC 3893 0 +GCTGGCACGAGTTTTACC 3881 0 +GCTGTTATCCCTAGGGTA 3896 0 +GGACATCCTAATGGTGCA 3865 0 +GGACTTTAATCGTTGAAC 3687 0 +GGAGCCTGTTCTAGAACC 3874 0 +GGATGTCCTGATCCAACA 3895 0 +GGATTAGATACCCCACTA 3896 0 +GGATTGCGCTGTTATCCC 3895 0 +GGCGATGTTTTTGGTAAA 3890 0 +GGCTGGCACGAGTTTTAC 3880 0 +GGGAAGAAATGGGCTACA 3872 0 +GGGAGGCAAGTCGTAACA 3891 0 +GGGGAGGCAAGTCGTAAC 3890 0 +GGGGGTTTACGACCTCGA 3894 0 +GGTACACTTACCATGTTA 3893 0 +GGTCTGAACTCAGATCAC 3902 0 +GGTCTTAGGAACCAAAAA 3862 0 +GGTTCGTTTGTTCAACGA 3877 0 +GTAACATGGTAAGTGTAC 3893 0 +GTACCTTTTGCATCATGA 3892 0 +GTCCTGATCCAACATCGA 3892 0 +GTCTTAGGAACCAAAAAC 3853 0 +GTGGGGTATCTAATCCCA 3895 0 +GTTACCCTAGGGATAACA 3896 0 +GTTTGTTCAACGATTAAA 3877 0 +TAAAACTCGTGCCAGCCA 3887 0 +TAACCGCGGTGGCTGGCA 3895 0 +TAAGGGTTCGTTTGTTCA 3889 0 +TAATCGTTGAACAAACGA 3876 0 +TACGACCTCGATGTTGGA 3892 0 +TAGGACTTTAATCGTTGA 3689 0 +TAGGATGTCCTGATCCAA 3897 0 +TCCCACTCTTTTGCCACA 3866 0 +TCGCCTGTTTACCAAAAA 3891 0 +TGGATCAGGACATCCTAA 3894 0 +TGGTCTTAGGAACCAAAA 3862 0 +TGGTGCAGCCGCTATTAA 3847 0 +TTGGTCTTAGGAACCAAA 3862 0 diff --git a/pkg/obiprimer/words.go b/pkg/obiprimer/words.go index c154e4d..d492039 100644 --- a/pkg/obiprimer/words.go +++ b/pkg/obiprimer/words.go @@ -4,10 +4,8 @@ import ( "fmt" "math" "sync" - "sync/atomic" "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obidefault" - "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiiter" "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obikmer" ) @@ -20,13 +18,6 @@ type WordSearch struct { TmpDir string // parent of the temporary directory ("" = system default) } -// Counts gives the number of sequences by role. -type Counts struct { - Examples int - CounterExamples int - Ignored int -} - // StrictWords is the result of the strict word search: the canonical words of // PrimerLength bases present, without any error, in at least MinSequences // example sequences. @@ -44,12 +35,12 @@ func MinSequences(examples int, quorum float64) int { return int(math.Floor(float64(examples)*quorum + 1e-9)) } -// FindStrictWords reads every sequence of seqs once, indexes the example -// sequences, and returns the words reaching the quorum. Sequences no longer -// than the word size are counted as examples but cannot contain any word, as -// in ecoPrimers. Words are canonical (the smaller of the word and its reverse -// complement), so both strands are searched. -func FindStrictWords(seqs obiiter.IBioSequence, sel Selection, cfg WordSearch) (*StrictWords, error) { +// FindStrictWords indexes the example sequences of the corpus and returns the +// words reaching the quorum. Sequences no longer than the word size are +// counted as examples but cannot contain any word, as in ecoPrimers. Words are +// canonical (the smaller of the word and its reverse complement), so both +// strands are searched. +func FindStrictWords(c *Corpus, cfg WordSearch) (*StrictWords, error) { if cfg.PrimerLength < 2 || cfg.PrimerLength > 31 { return nil, fmt.Errorf("obiprimer: primer length must be between 2 and 31, got %d", cfg.PrimerLength) } @@ -66,50 +57,27 @@ func FindStrictWords(seqs obiiter.IBioSequence, sel Selection, cfg WordSearch) ( } defer counter.Close() - var nextID atomic.Uint32 - var examples, counters, ignored atomic.Int64 - - consumer := func(it obiiter.IBioSequence, wg *sync.WaitGroup) { - defer wg.Done() - for it.Next() { - for _, seq := range it.Get().Slice() { - switch sel.Role(seq) { - case Example: - examples.Add(1) - if seq.Len() > cfg.PrimerLength { - counter.AddSequence(nextID.Add(1)-1, seq.Sequence(), cfg.Circular) - } - case CounterExample: - counters.Add(1) - default: - ignored.Add(1) + jobs := make(chan int) + var wg sync.WaitGroup + for range obidefault.ParallelWorkers() { + wg.Add(1) + go func() { + defer wg.Done() + for i := range jobs { + if seq := c.Seqs[i]; c.Roles[i] == Example && seq.Len() > cfg.PrimerLength { + counter.AddSequence(uint32(i), seq.Sequence(), cfg.Circular) } } - } + }() } - - var wg sync.WaitGroup - for i := 1; i < obidefault.ParallelWorkers(); i++ { - wg.Add(1) - go consumer(seqs.Split(), &wg) + for i := range c.Seqs { + jobs <- i } - wg.Add(1) - go consumer(seqs, &wg) + close(jobs) wg.Wait() - res := &StrictWords{ - PrimerLength: cfg.PrimerLength, - Counts: Counts{ - Examples: int(examples.Load()), - CounterExamples: int(counters.Load()), - Ignored: int(ignored.Load()), - }, - } + res := &StrictWords{PrimerLength: cfg.PrimerLength, Counts: c.Counts()} res.MinSequences = MinSequences(res.Counts.Examples, cfg.Quorum) - if res.Counts.Examples == 0 { - return nil, fmt.Errorf("obiprimer: no example sequence selected") - } - res.Words, err = counter.CountAtLeast(max(res.MinSequences, 1)) if err != nil { return nil, err diff --git a/pkg/obiprimer/words_test.go b/pkg/obiprimer/words_test.go index 1fbdb0d..8abaa18 100644 --- a/pkg/obiprimer/words_test.go +++ b/pkg/obiprimer/words_test.go @@ -107,8 +107,11 @@ func TestFindStrictWords(t *testing.T) { add("zz1", randDNA(rng, 50)+motif) sel := Selection{Restrict: idPrefix("ex").Or(idPrefix("cx")), Ignore: idPrefix("cx"), Except: idPrefix("cx")} - res, err := FindStrictWords(obiiter.IBatchOver("t", data, 3), sel, - WordSearch{PrimerLength: k, Quorum: 0.7, Partitions: 8, TmpDir: t.TempDir()}) + corpus, err := LoadCorpus(obiiter.IBatchOver("t", data, 3), sel) + if err != nil { + t.Fatal(err) + } + res, err := FindStrictWords(corpus, WordSearch{PrimerLength: k, Quorum: 0.7, Partitions: 8, TmpDir: t.TempDir()}) if err != nil { t.Fatal(err) } @@ -151,14 +154,17 @@ func TestFindStrictWordsParameters(t *testing.T) { it := func() obiiter.IBioSequence { return obiiter.IBatchOver("t", obiseq.BioSequenceSlice{obiseq.NewBioSequence("a", []byte("acgtacgtacgtacgtacgtacgt"), "")}, 1) } - if _, err := FindStrictWords(it(), Selection{}, WordSearch{PrimerLength: 1, Quorum: 0.7}); err == nil { + corpus, err := LoadCorpus(it(), Selection{}) + if err != nil { + t.Fatal(err) + } + if _, err := FindStrictWords(corpus, WordSearch{PrimerLength: 1, Quorum: 0.7}); err == nil { t.Error("primer length 1 must fail") } - if _, err := FindStrictWords(it(), Selection{}, WordSearch{PrimerLength: 18, Quorum: 0}); err == nil { + if _, err := FindStrictWords(corpus, WordSearch{PrimerLength: 18, Quorum: 0}); err == nil { t.Error("quorum 0 must fail") } - if _, err := FindStrictWords(it(), Selection{Restrict: idPrefix("zz")}, - WordSearch{PrimerLength: 18, Quorum: 0.7}); err == nil { + if _, err := LoadCorpus(it(), Selection{Restrict: idPrefix("zz")}); err == nil { t.Error("no example must fail") } } @@ -206,7 +212,11 @@ func TestStrictWordsAgainstEcoPrimers(t *testing.T) { if err != nil { t.Fatal(err) } - res, err := FindStrictWords(seqs, Selection{Restrict: restrict}, + corpus, err := LoadCorpus(seqs, Selection{Restrict: restrict}) + if err != nil { + t.Fatal(err) + } + res, err := FindStrictWords(corpus, WordSearch{PrimerLength: 18, Quorum: 0.7, Circular: true, TmpDir: t.TempDir()}) if err != nil { t.Fatal(err) From 02c8ded683d676ec394d90bd04f97d76d0561d80 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Tue, 3 Nov 2026 14:52:40 +1100 Subject: [PATCH 15/24] Implement primer pair finding and evaluation logic This change introduces the core functionality for identifying, validating, and statistically evaluating primer pairs within the corpus. It includes new data structures for primer parameters, amplicon statistics, and sequence handling, along with methods for assigning taxa to sequences. Comprehensive tests are added to ensure the accuracy of the pair evaluation against expected outputs. --- OBIPRIMERS_NOTES.md | 56 ++++++ pkg/obiprimer/pairs.go | 281 ++++++++++++++++++++++++++++ pkg/obiprimer/pairs_eval.go | 360 ++++++++++++++++++++++++++++++++++++ pkg/obiprimer/pairs_test.go | 237 ++++++++++++++++++++++++ pkg/obiprimer/taxa.go | 69 +++++++ 5 files changed, 1003 insertions(+) create mode 100644 pkg/obiprimer/pairs.go create mode 100644 pkg/obiprimer/pairs_eval.go create mode 100644 pkg/obiprimer/pairs_test.go create mode 100644 pkg/obiprimer/taxa.go diff --git a/OBIPRIMERS_NOTES.md b/OBIPRIMERS_NOTES.md index e335cc3..d664ced 100644 --- a/OBIPRIMERS_NOTES.md +++ b/OBIPRIMERS_NOTES.md @@ -301,3 +301,59 @@ Variantes du protocole : Décision en suspens : écrire la variante 3 maintenant, ou d'abord finir le pipeline (paires et indices) et profiler l'ensemble pour savoir si l'appariement domine encore. + +## 12. Mode A, étape 4 : paires et indices (2026-11-03) + +Code : `taxa.go` (`Taxa`, `Corpus.AssignTaxa`), `pairs.go` (paramètres, amorces conservées, énumération +des amplicons, contrainte du 3', comptage), `pairs_eval.go` (`FindPairs`, `PairStats`, Bc, Bs, Yule, Tm), +test opt-in `TestPairsAgainstEcoPrimers`. + +Fidèle à ecoPrimers : amorce conservée = trouvée dans >= floor(|E| x 0,9) exemples ; « good » si en plus +<= floor(|C| x 0,1) contre-exemples ; une paire a au moins une amorce good ; longueur d'amplicon +strictement entre `-l` et `-L` ; clé de paire (amorce 1, sens, amorce 2, sens) ordonnée par mot +canonique ; paire retenue si q > 0,9 et qfp < 0,1 (comparaisons en float32 comme le C) et Bs >= 0,6 ; +Bc = taxons amplifiés / taxons exemples ; **Bs = taxons identifiés / taxons amplifiés par la paire** +(et non / tous les taxons : c'est ce que fait le code, l'article est ambigu) ; un taxon est identifié si +au moins un de ses amplicons n'est partagé avec aucun autre taxon ; tri par Yule x Bs. + +Choix d'implémentation (mémoire) : deux phases. Phase 1 ne garde que des compteurs par paire (séquences +exemples/contre-exemples, longueurs) ; phase 2 ne revisite que les paires retenues (407 / 3 448) et +n'enregistre qu'un hash 64 bits d'amplicon par occurrence au lieu de stocker les amplicons. ecoPrimers +conserve un amplicon par occurrence de chaque paire candidate (42 682 paires pour Cyprinidae). + +**Contrôle contre les tables complètes d'ecoPrimers** (mêmes paramètres, `-e 3 -3 2 -l 30 -L 150 -c`) : + +| Jeu | Paires | Résultat | +|---|---|---| +| Cyprinidae | 3 448 / 3 448 | **identique sur toutes les colonnes** : amorces, good, séquences, taxons, Bc, Bs, longueurs, GC, Tm, Tm minimale | +| Teleostei | 407 / 407 | mêmes paires ; **6 comptent 1 séquence de moins** (NC_033548) | + +Cause de l'écart Teleostei **établie par expérience** : l'`extractSite` d'ecoPrimers lit un `N` (code 4) +dans un registre à décalage ; le 4 déborde sur la base voisine et `~4 & 3` vaut 3 (T), donc un `N` en 3' +peut être accepté par accident. En émulant exactement ce registre dans le test du 3' (modification +temporaire, annulée), les **deux tables sont reproduites à 100 %** sur toutes les colonnes. Non conservé : +une base ambiguë ne peut pas être une correspondance 3' vérifiée. Le test fige donc l'écart (6 paires, +-1 séquence, rien d'autre ne diffère). Une première émulation plus grossière (N lu comme A) était +fausse : elle créait un écart dans Cyprinidae. + +Autres écarts connus, non testés sur ces données : fenêtres à cheval sur la jonction circulaire, que +le C lit au-delà du tableau (comportement indéfini) alors que nous reprenons au début ; Tm minimale +ignorant les sites contenant une base ambiguë (le C en tire des valeurs arbitraires). + +**Profil du pipeline complet (16 workers, M3 Max)**, le critère de l'option A : + +| Étape | Cyprinidae | Teleostei | +|---|---|---| +| lecture + sélection | 0,45 s | 0,44 s | +| taxons | 0,00 s | 0,00 s | +| mots stricts | 0,14 s | 1,04 s | +| **recherche approchée** | **4,14 s** | **10,18 s** | +| paires + indices + Tm | 1,04 s | 1,12 s | +| total | ~5,8 s | ~12,8 s | + +ecoPrimers complet : 134 s et 350 s. **L'appariement approché reste 72 % à 80 % du total : la variante +SIMD (variante 3) est justifiée par la mesure.** Mémoire : non isolée (le tas de 2,3 à 2,8 Go du test +contient la taxonomie NCBI complète). + +Non fait : option `-U` (pas de multi-match), séquence de référence `-R`, sets d'amorces, sortie CSV, +CLI. Prochaine étape : variante 3 (bit-parallèle multi-mots, SIMD). diff --git a/pkg/obiprimer/pairs.go b/pkg/obiprimer/pairs.go new file mode 100644 index 0000000..2582bf0 --- /dev/null +++ b/pkg/obiprimer/pairs.go @@ -0,0 +1,281 @@ +package obiprimer + +import ( + "cmp" + "fmt" + "slices" + "strings" + "sync" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obidefault" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obikmer" +) + +// PairParams holds the parameters of the primer pair search, named after the +// ecoPrimers options. +type PairParams struct { + PrimerLength int // -O, word size + MinLength int // -l: the amplicon (primers excluded) must be longer than this + MaxLength int // -L: and shorter than this + ThreePrime int // -3: number of 3' bases that must match exactly (0 = no constraint) + Sensitivity float64 // -s: a primer must hit more than this fraction of the examples (0.9) + FalsePositive float64 // -x: and less than this fraction of the counterexamples (0.1) + MinSpecificity float64 // -T: pairs with a lower Bs are dropped (0.6) + Circular bool // -c +} + +func (p PairParams) validate() error { + switch { + case p.PrimerLength < 2 || p.PrimerLength > 31: + return fmt.Errorf("obiprimer: primer length must be between 2 and 31, got %d", p.PrimerLength) + case p.MaxLength <= p.MinLength+1: + return fmt.Errorf("obiprimer: empty amplicon length range ]%d,%d[", p.MinLength, p.MaxLength) + case p.ThreePrime < 0 || p.ThreePrime > p.PrimerLength: + return fmt.Errorf("obiprimer: 3' strict bases must be in [0,%d], got %d", p.PrimerLength, p.ThreePrime) + case p.Sensitivity <= 0 || p.Sensitivity > 1 || p.FalsePositive < 0 || p.FalsePositive > 1: + return fmt.Errorf("obiprimer: quorums must be in ]0,1] and [0,1]") + } + return nil +} + +// Primer is a conserved word: one found, with at most the allowed errors, in +// enough example sequences to be a candidate primer. +type Primer struct { + Word uint64 // canonical word + Examples int // example sequences where it is found (either strand) + CounterExamples int + // Good means conserved in the examples and rare enough in the + // counterexamples; a pair needs at least one good primer. + Good bool +} + +// seqMatch is one occurrence of a primer in one sequence. +type seqMatch struct { + primer int32 + pos uint32 + reverse bool +} + +// primerSet is the conserved words and, for each corpus sequence, their +// occurrences sorted by position. +type primerSet struct { + primers []Primer + perSeq [][]seqMatch +} + +// quorumCount is floor(n*fraction), robust to the binary form of the fraction. +func quorumCount(n int, fraction float64) int { return MinSequences(n, fraction) } + +// selectPrimers keeps the words found in at least floor(examples*Sensitivity) +// example sequences and marks as good those found in at most +// floor(counterexamples*FalsePositive) counterexamples. +func selectPrimers(c *Corpus, wm []WordMatches, p PairParams) *primerSet { + counts := c.Counts() + inQ := quorumCount(counts.Examples, p.Sensitivity) + outQ := quorumCount(counts.CounterExamples, p.FalsePositive) + + ps := &primerSet{perSeq: make([][]seqMatch, len(c.Seqs))} + for _, w := range wm { + ex, co := w.SequencesMatched(c) + if ex < inQ { + continue + } + id := int32(len(ps.primers)) + ps.primers = append(ps.primers, Primer{w.Word, ex, co, co <= outQ}) + for _, m := range w.Matches { + ps.perSeq[m.Seq] = append(ps.perSeq[m.Seq], seqMatch{id, m.Pos, m.Reverse}) + } + } + for _, ms := range ps.perSeq { + slices.SortStableFunc(ms, func(a, b seqMatch) int { return cmp.Compare(a.pos, b.pos) }) + } + return ps +} + +// pairKey identifies a pair: the two primers, ordered by canonical word, and +// for each whether the primer is the canonical word as is (true) or its reverse +// complement (false). It is the key of ecoPrimers' pair tree. +type pairKey struct { + p1, p2 int32 + a1, a2 bool +} + +// occurrence is one amplicon: a pair of matches in a sequence. +type occurrence struct { + key pairKey + swapped bool // true when the upstream match is the second primer of the key + up, down seqMatch + length int // amplicon length, primers excluded +} + +// forEachOccurrence calls fn for every amplicon of sequence s, applying the +// length, good-primer and 3' constraints of ecoPrimers. +func (ps *primerSet) forEachOccurrence(c *Corpus, s int, p PairParams, fn func(occurrence)) { + ms := ps.perSeq[s] + lp := p.PrimerLength + var raw []byte + if p.ThreePrime > 0 { + raw = c.Seqs[s].Sequence() + } + + for i := range ms { + for j := i + 1; j < len(ms); j++ { + dpos := int(ms[j].pos) - int(ms[i].pos) + if dpos <= lp { + continue + } + length := dpos - lp + if length >= p.MaxLength { + break + } + up, down := ms[i], ms[j] + if length <= p.MinLength || !(ps.primers[up.primer].Good || ps.primers[down.primer].Good) { + continue + } + + // The upstream match is read as is (primer 1 of the pair when its word + // is smaller), the downstream one as the reverse primer. + key := pairKey{up.primer, down.primer, !up.reverse, down.reverse} + swapped := !(ps.primers[key.p2].Word > ps.primers[key.p1].Word) + if swapped { + key = pairKey{key.p2, key.p1, key.a2, key.a1} + } + + if p.ThreePrime > 0 && !ps.threePrimeMatches(raw, up, down, p) { + continue + } + fn(occurrence{key, swapped, up, down, length}) + } + } +} + +// wordBase returns base t (0 = 5') of a 2-bit packed word of size lp. +func wordBase(word uint64, lp, t int) byte { return byte(word>>(2*uint(lp-1-t))) & 3 } + +// threePrimeMatches reports whether the last p.ThreePrime bases of the forward +// primer (as found at the upstream match) and of the reverse primer (reverse +// complement of the downstream window) equal the sequence, i.e. have no +// mismatch at their 3' end. +func (ps *primerSet) threePrimeMatches(raw []byte, up, down seqMatch, p PairParams) bool { + lp, n := p.PrimerLength, len(raw) + at := func(pos int) byte { + if p.Circular { + pos %= n + } + return baseCode[raw[pos]] + } + + fw := ps.primers[up.primer].Word + rv := ps.primers[down.primer].Word + for t := lp - p.ThreePrime; t < lp; t++ { + // forward primer: the word when found direct, its reverse complement otherwise + var f byte + if !up.reverse { + f = wordBase(fw, lp, t) + } else { + f = 3 - wordBase(fw, lp, lp-1-t) + } + if s := at(int(up.pos) + t); s > 3 || s != f { + return false + } + + // reverse primer: the word when found reverse, its reverse complement otherwise + var r byte + if down.reverse { + r = wordBase(rv, lp, t) + } else { + r = 3 - wordBase(rv, lp, lp-1-t) + } + if s := at(int(down.pos) + lp - 1 - t); s > 3 || 3-s != r { + return false + } + } + return true +} + +// pairAcc accumulates, over the sequences, the counts ecoPrimers keeps while it +// builds the pairs. +type pairAcc struct { + examples, counters uint32 + last int32 // 1 + last sequence counted, to count each sequence once + amplicons uint32 + sumLen uint64 + minLen, maxLen uint32 +} + +// countPairs enumerates the amplicons of every sequence, in parallel, and +// returns for each pair the sequences it amplifies and the length statistics +// of its example amplicons. +func (ps *primerSet) countPairs(c *Corpus, p PairParams) map[pairKey]*pairAcc { + workers := obidefault.ParallelWorkers() + partial := make([]map[pairKey]*pairAcc, workers) + + jobs := make(chan int) + var wg sync.WaitGroup + for w := range workers { + wg.Add(1) + partial[w] = map[pairKey]*pairAcc{} + go func() { + defer wg.Done() + acc := partial[w] + for s := range jobs { + isExample := c.Roles[s] == Example + ps.forEachOccurrence(c, s, p, func(o occurrence) { + a := acc[o.key] + if a == nil { + a = &pairAcc{minLen: ^uint32(0)} + acc[o.key] = a + } + if a.last != int32(s)+1 { + a.last = int32(s) + 1 + if isExample { + a.examples++ + } else { + a.counters++ + } + } + if isExample { + l := uint32(o.length) + a.amplicons++ + a.sumLen += uint64(l) + a.minLen = min(a.minLen, l) + a.maxLen = max(a.maxLen, l) + } + }) + } + }() + } + for s := range c.Seqs { + jobs <- s + } + close(jobs) + wg.Wait() + + // A sequence is handled by one worker only, so the sequence counts add up. + total := partial[0] + for _, m := range partial[1:] { + for k, a := range m { + t := total[k] + if t == nil { + total[k] = a + continue + } + t.examples += a.examples + t.counters += a.counters + t.amplicons += a.amplicons + t.sumLen += a.sumLen + t.minLen = min(t.minLen, a.minLen) + t.maxLen = max(t.maxLen, a.maxLen) + } + } + return total +} + +// orientedWord returns the upper-case primer string 5'->3': the canonical word +// when direct, its reverse complement otherwise. +func orientedWord(word uint64, lp int, direct bool) string { + if !direct { + word = obikmer.ReverseComplement(word, lp) + } + return strings.ToUpper(string(obikmer.DecodeKmer(word, lp, nil))) +} diff --git a/pkg/obiprimer/pairs_eval.go b/pkg/obiprimer/pairs_eval.go new file mode 100644 index 0000000..ece6e0a --- /dev/null +++ b/pkg/obiprimer/pairs_eval.go @@ -0,0 +1,360 @@ +package obiprimer + +import ( + "cmp" + "fmt" + "math" + "slices" + "sync" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obidefault" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obithermo" +) + +// PairStats describes one primer pair and its quality indices. Primers are +// written 5'->3' and ordered so that Primer1 < Primer2 (as in ecoPrimers). +type PairStats struct { + Primer1, Primer2 string + Good1, Good2 bool // primer conserved in the examples and rare in the counterexamples + + Examples int // example sequences amplified + CounterExamples int + Yule float32 // Examples/|E| - CounterExamples/|C| + + ExampleTaxa int // taxa amplified among the examples + CounterTaxa int // taxa amplified among the counterexamples + Bc float32 // coverage: ExampleTaxa over all the example taxa + Identified int // example taxa with an amplicon no other taxon shares + Bs float32 // specificity: Identified over ExampleTaxa + MinLen, MaxLen int + MeanLen float64 + Tm1, Tm2 float64 // melting temperature of the primers against their perfect target + MinTm1, MinTm2 float64 // lowest melting temperature over the example amplicons + GC1, GC2 int +} + +// Score is the ranking criterion of ecoPrimers. +func (p PairStats) Score() float32 { return p.Yule * p.Bs } + +// ampRecord is an amplicon of a retained pair, in one sequence. +type ampRecord struct { + pair int32 + seq int32 + hash uint64 + tm1, tm2 float32 // NaN when unavailable +} + +// ampHash hashes the amplicon between the two primers read from the side of +// the first primer of the pair (reverse complemented when the upstream match is +// the second primer), so that identical amplicons give the same value +// whatever their strand. Non ACGT bases hash as a fifth symbol. +func ampHash(raw []byte, from, to int, reverse bool) uint64 { + const prime = 1099511628211 + h := uint64(14695981039346656037) ^ uint64(to-from) + h *= prime + if !reverse { + for _, b := range raw[from:to] { + h = (h ^ uint64(baseCode[b])) * prime + } + return h + } + for i := to - 1; i >= from; i-- { + c := baseCode[raw[i]] + if c < 4 { + c = 3 - c + } + h = (h ^ uint64(c)) * prime + } + return h +} + +// FindPairs searches the primer pairs from the approximate occurrences of the +// strict words (see MatchApat) and evaluates them. +// +// A pair is a pair of conserved primers, at least one of them good, that +// delimit an amplicon of the allowed length. It is retained when it amplifies +// more than Sensitivity of the examples and less than FalsePositive of the +// counterexamples, and its specificity Bs reaches MinSpecificity. The result +// is sorted by decreasing Yule*Bs. +// +// model computes the melting temperatures; nil leaves them NaN. +func FindPairs(c *Corpus, taxa *Taxa, wm []WordMatches, p PairParams, model *obithermo.Model) ([]PairStats, error) { + if err := p.validate(); err != nil { + return nil, err + } + if taxa == nil || len(taxa.Of) != len(c.Seqs) { + return nil, fmt.Errorf("obiprimer: taxa do not match the corpus") + } + ps := selectPrimers(c, wm, p) + if len(ps.primers) == 0 { + return nil, nil + } + + // Phase 1: which pairs amplify enough examples and few enough counterexamples. + counts := c.Counts() + acc := ps.countPairs(c, p) + sens, fpq := float32(p.Sensitivity), float32(p.FalsePositive) + var kept []pairKey + for k, a := range acc { + q := float32(a.examples) / float32(counts.Examples) + qfp := float32(0) + if counts.CounterExamples > 0 { + qfp = float32(a.counters) / float32(counts.CounterExamples) + } + if q > sens && qfp < fpq { + kept = append(kept, k) + } + } + slices.SortFunc(kept, func(a, b pairKey) int { + return cmp.Or(cmp.Compare(a.p1, b.p1), cmp.Compare(a.p2, b.p2), + boolCmp(a.a1, b.a1), boolCmp(a.a2, b.a2)) + }) + index := make(map[pairKey]int32, len(kept)) + for i, k := range kept { + index[k] = int32(i) + } + + // Phase 2: the amplicons of the retained pairs only. + records := ps.collectAmplicons(c, p, index, model) + byPair := make([][]ampRecord, len(kept)) + for _, r := range records { + byPair[r.pair] = append(byPair[r.pair], r) + } + + var out []PairStats + for i, k := range kept { + st, ok := ps.evaluate(c, taxa, p, k, acc[k], byPair[i], model) + if ok { + out = append(out, st) + } + } + slices.SortFunc(out, func(a, b PairStats) int { + return cmp.Or(-cmp.Compare(a.Score(), b.Score()), cmp.Compare(a.Primer1, b.Primer1), cmp.Compare(a.Primer2, b.Primer2)) + }) + return out, nil +} + +func boolCmp(a, b bool) int { + switch { + case a == b: + return 0 + case !a: + return -1 + } + return 1 +} + +// collectAmplicons gathers, in parallel, one record per amplicon of a retained pair. +func (ps *primerSet) collectAmplicons(c *Corpus, p PairParams, index map[pairKey]int32, model *obithermo.Model) []ampRecord { + lp := p.PrimerLength + workers := obidefault.ParallelWorkers() + parts := make([][]ampRecord, workers) + + // oriented[primer][0] is the reverse complement of the word, [1] the word itself. + oriented := make([][2][]byte, len(ps.primers)) + for i, pr := range ps.primers { + oriented[i][0] = []byte(orientedWord(pr.Word, lp, false)) + oriented[i][1] = []byte(orientedWord(pr.Word, lp, true)) + } + nan := float32(math.NaN()) + + jobs := make(chan int) + var wg sync.WaitGroup + for w := range workers { + wg.Add(1) + go func() { + defer wg.Done() + var site1, site2 [32]byte + for s := range jobs { + raw := c.Seqs[s].Sequence() + n := len(raw) + isExample := c.Roles[s] == Example + ps.forEachOccurrence(c, s, p, func(o occurrence) { + pi, ok := index[o.key] + if !ok { + return + } + rec := ampRecord{pair: pi, seq: int32(s), tm1: nan, tm2: nan} + if isExample { + rec.hash = ampHash(raw, int(o.up.pos)+lp, int(o.down.pos), o.swapped) + if model != nil { + fp := oriented[o.up.primer][b2i(!o.up.reverse)] + rp := oriented[o.down.primer][b2i(o.down.reverse)] + fsite, rsite := site1[:lp], site2[:lp] + okF := windowAsIs(fsite, raw, int(o.up.pos), n, p.Circular) + okR := windowRevComp(rsite, raw, int(o.down.pos), n, p.Circular) + tmF, tmR := nan, nan + if okF { + if t, err := model.Tm(fp, fsite); err == nil { + tmF = float32(t) + } + } + if okR { + if t, err := model.Tm(rp, rsite); err == nil { + tmR = float32(t) + } + } + if o.swapped { + rec.tm1, rec.tm2 = tmR, tmF + } else { + rec.tm1, rec.tm2 = tmF, tmR + } + } + } + parts[w] = append(parts[w], rec) + }) + } + }() + } + for s := range c.Seqs { + jobs <- s + } + close(jobs) + wg.Wait() + + var all []ampRecord + for _, part := range parts { + all = append(all, part...) + } + return all +} + +func b2i(b bool) int { + if b { + return 1 + } + return 0 +} + +const upper = "ACGT" + +// windowAsIs copies the len(dst) bases at pos as upper-case ACGT; it reports +// false if one of them is not A, C, G or T. +func windowAsIs(dst, raw []byte, pos, n int, circular bool) bool { + for t := range dst { + i := pos + t + if circular { + i %= n + } + c := baseCode[raw[i]] + if c > 3 { + return false + } + dst[t] = upper[c] + } + return true +} + +// windowRevComp writes the reverse complement of the len(dst) bases at pos. +func windowRevComp(dst, raw []byte, pos, n int, circular bool) bool { + l := len(dst) + for t := range dst { + i := pos + l - 1 - t + if circular { + i %= n + } + c := baseCode[raw[i]] + if c > 3 { + return false + } + dst[t] = upper[3-c] + } + return true +} + +// evaluate computes the indices of one retained pair. It reports false when the +// pair must be dropped (no taxon amplified, or specificity under the threshold). +func (ps *primerSet) evaluate(c *Corpus, taxa *Taxa, p PairParams, k pairKey, a *pairAcc, + recs []ampRecord, model *obithermo.Model) (PairStats, bool) { + + exTaxa := map[int32]struct{}{} + coTaxa := map[int32]struct{}{} + ampTaxon := map[uint64]int32{} // amplicon -> its taxon, or -2 when shared + for _, r := range recs { + t := taxa.Of[r.seq] + if t < 0 { + continue + } + if c.Roles[r.seq] != Example { + coTaxa[t] = struct{}{} + continue + } + exTaxa[t] = struct{}{} + if prev, seen := ampTaxon[r.hash]; !seen { + ampTaxon[r.hash] = t + } else if prev != t { + ampTaxon[r.hash] = -2 + } + } + if len(exTaxa) == 0 { + return PairStats{}, false + } + identified := map[int32]struct{}{} + for _, t := range ampTaxon { + if t >= 0 { + identified[t] = struct{}{} + } + } + + counts := c.Counts() + st := PairStats{ + Examples: int(a.examples), + CounterExamples: int(a.counters), + ExampleTaxa: len(exTaxa), + CounterTaxa: len(coTaxa), + Identified: len(identified), + MinLen: int(a.minLen), + MaxLen: int(a.maxLen), + MeanLen: float64(a.sumLen) / float64(a.amplicons), + } + q := float32(a.examples) / float32(counts.Examples) + qfp := float32(0) + if counts.CounterExamples > 0 { + qfp = float32(a.counters) / float32(counts.CounterExamples) + } + st.Yule = q - qfp + st.Bc = float32(st.ExampleTaxa) / float32(taxa.ExampleTaxa) + st.Bs = float32(st.Identified) / float32(st.ExampleTaxa) + if st.Bs < float32(p.MinSpecificity) { + return PairStats{}, false + } + + lp := p.PrimerLength + st.Primer1 = orientedWord(ps.primers[k.p1].Word, lp, k.a1) + st.Primer2 = orientedWord(ps.primers[k.p2].Word, lp, k.a2) + st.Good1, st.Good2 = ps.primers[k.p1].Good, ps.primers[k.p2].Good + st.GC1, st.GC2 = obithermo.GCCount([]byte(st.Primer1)), obithermo.GCCount([]byte(st.Primer2)) + + st.Tm1, st.Tm2, st.MinTm1, st.MinTm2 = math.NaN(), math.NaN(), math.NaN(), math.NaN() + if model != nil { + st.Tm1, _ = model.SelfTm([]byte(st.Primer1)) + st.Tm2, _ = model.SelfTm([]byte(st.Primer2)) + m1, m2 := float32(math.Inf(1)), float32(math.Inf(1)) + for _, r := range recs { + if c.Roles[r.seq] != Example { + continue + } + if r.tm1 == r.tm1 { // not NaN + m1 = min(m1, r.tm1) + } + if r.tm2 == r.tm2 { + m2 = min(m2, r.tm2) + } + } + if !math.IsInf(float64(m1), 1) { + st.MinTm1 = float64(m1) + } + if !math.IsInf(float64(m2), 1) { + st.MinTm2 = float64(m2) + } + } + + if st.Primer2 < st.Primer1 { + st.Primer1, st.Primer2 = st.Primer2, st.Primer1 + st.Good1, st.Good2 = st.Good2, st.Good1 + st.GC1, st.GC2 = st.GC2, st.GC1 + st.Tm1, st.Tm2 = st.Tm2, st.Tm1 + st.MinTm1, st.MinTm2 = st.MinTm2, st.MinTm1 + } + return st, true +} diff --git a/pkg/obiprimer/pairs_test.go b/pkg/obiprimer/pairs_test.go new file mode 100644 index 0000000..8d673d1 --- /dev/null +++ b/pkg/obiprimer/pairs_test.go @@ -0,0 +1,237 @@ +package obiprimer + +import ( + "bufio" + "fmt" + "math" + "os" + "path/filepath" + "runtime" + "strconv" + "strings" + "testing" + "time" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiformats" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obikmer" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obithermo" +) + +// ecoRow is one result line of ecoPrimers. +type ecoRow struct { + p1, p2 string + tm1, mtm1 float64 + tm2, mtm2 float64 + gc1, gc2 int + good string + in, out int + yule float64 + intaxa, outtax int + bc float64 + well int + bs float64 + mind, maxd int + avg float64 +} + +func readEcoRows(t *testing.T, path string) []ecoRow { + t.Helper() + f, err := os.Open(path) + if err != nil { + t.Fatal(err) + } + defer f.Close() + var rows []ecoRow + sc := bufio.NewScanner(f) + sc.Buffer(make([]byte, 1<<20), 1<<20) + for sc.Scan() { + line := sc.Text() + if strings.HasPrefix(line, "#") || strings.TrimSpace(line) == "" { + continue + } + f := strings.Split(line, "\t") + fl := func(i int) float64 { + v, err := strconv.ParseFloat(strings.TrimSpace(f[i]), 64) + if err != nil { + t.Fatalf("field %d of %q: %v", i, line, err) + } + return v + } + in := func(i int) int { return int(fl(i)) } + rows = append(rows, ecoRow{ + p1: f[1], p2: f[2], tm1: fl(3), mtm1: fl(4), tm2: fl(5), mtm2: fl(6), + gc1: in(7), gc2: in(8), good: f[9], in: in(10), out: in(11), yule: fl(12), + intaxa: in(13), outtax: in(14), bc: fl(15), well: in(16), bs: fl(17), + mind: in(18), maxd: in(19), avg: fl(20), + }) + } + if err := sc.Err(); err != nil { + t.Fatal(err) + } + return rows +} + +// Opt-in end-to-end comparison with the complete ecoPrimers result tables of +// the tutorial: same parameters (-e 3 -3 2 -l 30 -L 150 -c, -r taxid). +// +// Cyprinidae is reproduced exactly: its 3448 pairs agree on every column +// (primers, good flags, sequence and taxon counts, Bc, Bs, lengths, GC, Tm and +// lowest Tm). Teleostei finds the same 407 pairs, and 6 of them count one +// sequence less than ecoPrimers (NC_033548, whose downstream site is +// NGGGGAGGCAAGTCGTAA). The 3' check of ecoPrimers reads an N through the +// shift register of extractSite, where the code 4 of N spills on the next base +// and ~4&3 reads as T, so an N can be accepted by accident. Emulating that +// register exactly in the 3' check was verified to reproduce both tables on +// every column; it is not kept: an ambiguous base cannot be a verified 3' match. +func TestPairsAgainstEcoPrimers(t *testing.T) { + dir := os.Getenv("OBIPRIMERS_TUTORIAL") + if dir == "" { + t.Skip("OBIPRIMERS_TUTORIAL not set") + } + taxonomy, err := obiformats.LoadTaxonomy(filepath.Join(dir, "ncbitaxo_20250211.tgz"), false, false) + if err != nil { + t.Fatal(err) + } + + for _, tc := range []struct { + name, taxid string + species int + divergent int + }{{"Cyprinidae", "7953", 316, 0}, {"Teleostei", "32443", 3876, 6}} { + t.Run(tc.name, func(t *testing.T) { + var stages []string + tick := time.Now() + lap := func(name string) { + stages = append(stages, fmt.Sprintf("%s %s", name, time.Since(tick).Round(time.Millisecond))) + tick = time.Now() + } + restrict, err := BelongingTo(taxonomy, []string{tc.taxid}) + if err != nil { + t.Fatal(err) + } + seqs, err := obiformats.ReadSequencesFromFile(filepath.Join(dir, "mito.vert.fasta.gz")) + if err != nil { + t.Fatal(err) + } + corpus, err := LoadCorpus(seqs, Selection{Restrict: restrict}) + if err != nil { + t.Fatal(err) + } + lap("read+select") + taxa, err := corpus.AssignTaxa(taxonomy, "species") + if err != nil { + t.Fatal(err) + } + lap("taxa") + if taxa.ExampleTaxa != tc.species { + t.Errorf("%d example species, ecoPrimers reports %d", taxa.ExampleTaxa, tc.species) + } + + sw, err := FindStrictWords(corpus, WordSearch{PrimerLength: 18, Quorum: 0.7, Circular: true, TmpDir: t.TempDir()}) + if err != nil { + t.Fatal(err) + } + lap("strict words") + words := make([]uint64, len(sw.Words)) + for i, w := range sw.Words { + words[i] = w.Kmer + } + wm, err := matchApat(corpus, words, MatchParams{K: 18, MaxErrors: 3, Circular: true}) + if err != nil { + t.Fatal(err) + } + lap("approximate search") + got, err := FindPairs(corpus, taxa, wm, PairParams{ + PrimerLength: 18, MinLength: 30, MaxLength: 150, ThreePrime: 2, + Sensitivity: 0.9, FalsePositive: 0.1, MinSpecificity: 0.6, Circular: true, + }, obithermo.DefaultModel()) + if err != nil { + t.Fatal(err) + } + + lap("pairs+indices+Tm") + var ms runtime.MemStats + runtime.ReadMemStats(&ms) + t.Logf("stages: %s; Go heap in use at the end %.0f MB (includes the taxonomy)", strings.Join(stages, ", "), float64(ms.HeapAlloc)/1e6) + + want := readEcoRows(t, filepath.Join(dir, tc.name+".ecoprimers")) + t.Logf("%d pairs, ecoPrimers %d", len(got), len(want)) + + byKey := map[string]PairStats{} + for _, g := range got { + byKey[g.Primer1+"/"+g.Primer2] = g + } + bad := map[string]int{} + missing, divergent := 0, 0 + note := func(field string, w ecoRow, format string, args ...any) { + if bad[field]++; bad[field] <= 3 { + t.Errorf("%s/%s %s: "+format, append([]any{w.p1, w.p2, field}, args...)...) + } + } + near := func(a, b, tol float64) bool { return math.Abs(a-b) <= tol } + for _, w := range want { + g, ok := byKey[w.p1+"/"+w.p2] + if !ok { + if missing++; missing <= 5 { + t.Errorf("ecoPrimers pair %s/%s not found", w.p1, w.p2) + } + continue + } + delete(byKey, w.p1+"/"+w.p2) + good := string("bG"[b2i(g.Good1)]) + string("bG"[b2i(g.Good2)]) + if good != w.good { + note("good", w, "got %s want %s", good, w.good) + } + // Known divergence, see the comment of the test: ecoPrimers accepts one + // amplicon (NC_033548) whose 3' primer site ends with an N. We reject it, + // so the counts can be exactly one lower and nothing else may differ. + lower := func(got, want int) bool { return got == want || got == want-1 } + diverges := g.Examples != w.in || g.ExampleTaxa != w.intaxa || g.Identified != w.well + if diverges { + divergent++ + } + if !lower(g.Examples, w.in) || g.CounterExamples != w.out { + note("sequences", w, "got %d/%d want %d/%d", g.Examples, g.CounterExamples, w.in, w.out) + } + if !lower(g.ExampleTaxa, w.intaxa) || g.CounterTaxa != w.outtax { + note("taxa", w, "got %d/%d want %d/%d", g.ExampleTaxa, g.CounterTaxa, w.intaxa, w.outtax) + } + if !lower(g.Identified, w.well) { + note("identified", w, "got %d want %d", g.Identified, w.well) + } + tol := 0.0006 + if diverges { + tol = 0.0021 + } + if !near(float64(g.Yule), w.yule, tol) || !near(float64(g.Bc), w.bc, tol) || !near(float64(g.Bs), w.bs, tol) { + note("indices", w, "yule/bc/bs got %.4f/%.4f/%.4f want %.3f/%.3f/%.3f", g.Yule, g.Bc, g.Bs, w.yule, w.bc, w.bs) + } + if g.MinLen != w.mind || g.MaxLen != w.maxd || !near(g.MeanLen, w.avg, 0.006) { + note("lengths", w, "got %d/%d/%.2f want %d/%d/%.2f", g.MinLen, g.MaxLen, g.MeanLen, w.mind, w.maxd, w.avg) + } + if g.GC1 != w.gc1 || g.GC2 != w.gc2 { + note("gc", w, "got %d/%d want %d/%d", g.GC1, g.GC2, w.gc1, w.gc2) + } + // ecoPrimers prints Kelvin-273.0; obithermo uses 273.15 + if !near(g.Tm1+0.15, w.tm1, 0.051) || !near(g.Tm2+0.15, w.tm2, 0.051) { + note("tm", w, "got %.2f/%.2f want %.1f/%.1f", g.Tm1+0.15, g.Tm2+0.15, w.tm1, w.tm2) + } + if !near(g.MinTm1+0.15, w.mtm1, 0.051) || !near(g.MinTm2+0.15, w.mtm2, 0.051) { + note("mintm", w, "got %.2f/%.2f want %.1f/%.1f", g.MinTm1+0.15, g.MinTm2+0.15, w.mtm1, w.mtm2) + } + } + t.Logf("missing from ours: %d; extra in ours: %d; mismatching fields: %v; pairs one sequence below ecoPrimers: %d", + missing, len(byKey), bad, divergent) + if divergent != tc.divergent { + t.Errorf("%d pairs differ from ecoPrimers by one sequence, expected %d", divergent, tc.divergent) + } + for k := range byKey { + if len(byKey) > 0 { + t.Errorf("extra pair %s", k) + } + break + } + }) + } + _ = obikmer.DecodeKmer +} diff --git a/pkg/obiprimer/taxa.go b/pkg/obiprimer/taxa.go new file mode 100644 index 0000000..bd3417c --- /dev/null +++ b/pkg/obiprimer/taxa.go @@ -0,0 +1,69 @@ +package obiprimer + +import ( + "fmt" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obitax" +) + +// Taxa gives every corpus sequence its taxon at one rank (the species by +// default in ecoPrimers), as a dense index, and the number of distinct taxa +// among the examples and among the counterexamples. +// +// A sequence whose taxon has no ancestor at that rank (or no taxon at all) has +// the index -1; it is never counted as a taxon. +type Taxa struct { + Rank string + Of []int32 // Of[i] is the taxon index of Corpus.Seqs[i], or -1 + ExampleTaxa int + CounterExamples int // distinct taxa among the counterexamples + nodes []*obitax.Taxon +} + +// Name returns the taxon with the given index, for reporting. +func (t *Taxa) Name(i int32) *obitax.Taxon { return t.nodes[i] } + +// AssignTaxa determines the taxon of every sequence of the corpus at rank. +func (c *Corpus) AssignTaxa(taxonomy *obitax.Taxonomy, rank string) (*Taxa, error) { + if taxonomy == nil { + return nil, fmt.Errorf("obiprimer: a taxonomy is required to compute taxon statistics") + } + ranks := taxonomy.RankList() + known := false + for _, r := range ranks { + if r == rank { + known = true + break + } + } + if !known { + return nil, fmt.Errorf("obiprimer: unknown taxonomic rank %q", rank) + } + + t := &Taxa{Rank: rank, Of: make([]int32, len(c.Seqs))} + index := map[*obitax.TaxNode]int32{} + exSeen := map[int32]struct{}{} + coSeen := map[int32]struct{}{} + + for i, seq := range c.Seqs { + t.Of[i] = -1 + taxon := seq.Taxon(taxonomy).TaxonAtRank(rank) + if taxon == nil { + continue + } + id, ok := index[taxon.Node] + if !ok { + id = int32(len(t.nodes)) + index[taxon.Node] = id + t.nodes = append(t.nodes, taxon) + } + t.Of[i] = id + if c.Roles[i] == Example { + exSeen[id] = struct{}{} + } else { + coSeen[id] = struct{}{} + } + } + t.ExampleTaxa, t.CounterExamples = len(exSeen), len(coSeen) + return t, nil +} From 1ab512ab5c36364730ed973d24a117f159a900ef Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Tue, 3 Nov 2026 15:06:01 +1100 Subject: [PATCH 16/24] Refactor MatchWords indexing and introduce seed indexing Corrects the indexing strategy for seed searching, improving performance and introducing a new seed index structure for efficient pattern lookup. This commit updates the core matching logic by shifting the indexing strategy to use two halves of k/2 bases, resulting in significant speed improvements for sequence matching. It also introduces a new `seedIndex` structure for fast pattern retrieval and implements comprehensive performance testing logic for matching functions. Future features, such as bit-parallel search, are deferred. --- OBIPRIMERS_NOTES.md | 59 +++++++++- pkg/obiprimer/match_apat.go | 87 --------------- pkg/obiprimer/match_seed.go | 214 ++++++++++++++++++++++++++++++++++++ pkg/obiprimer/match_test.go | 41 +++++-- pkg/obiprimer/pairs_test.go | 2 +- 5 files changed, 301 insertions(+), 102 deletions(-) delete mode 100644 pkg/obiprimer/match_apat.go create mode 100644 pkg/obiprimer/match_seed.go diff --git a/OBIPRIMERS_NOTES.md b/OBIPRIMERS_NOTES.md index d664ced..76fc038 100644 --- a/OBIPRIMERS_NOTES.md +++ b/OBIPRIMERS_NOTES.md @@ -295,9 +295,9 @@ Variantes du protocole : 2. Arrêt anticipé sous le quorum de 90 % : **écartée, mesuré inutile** (Teleostei : 255/255 mots conservés ; Cyprinidae : 1 201/1 208). Le parallélisme par séquence est déjà dans la variante 1. 3. Bit-parallèle multi-mots en Go (SIMD) : **non écrite**. Cible : les ~74 % de balayage C. -4. Index de voisinage / Aho-Corasick : écartée pour e >= 2 par le calcul (graines de 4 à 5 bases, - ~250 000 candidats par graine et par mot sur 65 Mb) ; `obicorazick` reste pertinent pour e = 0. - Non mesurée. +4. Index de voisinage / Aho-Corasick : **j'avais écarté cette piste à tort** (voir section 13) : mon calcul + ne considérait que le découpage en e+1 morceaux (graines de 4 bases). Le découpage en deux moitiés avec + voisinage à distance floor(e/2) est très sélectif et a gagné. Décision en suspens : écrire la variante 3 maintenant, ou d'abord finir le pipeline (paires et indices) et profiler l'ensemble pour savoir si l'appariement domine encore. @@ -357,3 +357,56 @@ contient la taxonomie NCBI complète). Non fait : option `-U` (pas de multi-match), séquence de référence `-R`, sets d'amorces, sortie CSV, CLI. Prochaine étape : variante 3 (bit-parallèle multi-mots, SIMD). + +## 13. Mode A, étape 5 : recherche approchée par index de graines (2026-11-03) + +**Correction d'une erreur de ma part.** À la section 11 j'écartais l'index de graines « par le calcul » : +je n'avais considéré que le principe des tiroirs avec e+1 morceaux (graines de 4 à 5 bases, des +centaines de milliers de candidats). C'était la mauvaise configuration. Avec **deux moitiés** de +k/2 bases : une fenêtre à <= e = 2d+1 erreurs a au plus d = floor(e/2) erreurs dans l'une des deux +moitiés. On indexe donc, pour chaque moitié de chaque motif (mot et son complément inverse), la moitié +et ses voisins à distance d : 1 + 3L valeurs pour d = 1 (28 pour L = 9, dans 4^9 = 262 144). Sur du +texte aléatoire cela fait ~0,0002 x W candidats par position et par moitié au lieu de 2W comparaisons. + +Code : `pkg/obiprimer/match_seed.go`, fonction exportée **`MatchWords`** (remplace la variante `obiapat`, +supprimée). Détails : fenêtre glissante 2 bits/base ; bases ambiguës suivies par un masque (erreur +sûre, aucune correspondance) ; fenêtres circulaires par extension de k-1 bases ; index direct pour les +moitiés de <= 11 bases, haché sur 22 bits au-delà (k >= 24) ; doublons évités par recherche binaire +dans la liste de l'index gauche ; vérification exacte par popcount sur le code complet. + +Justesse : égal à l'oracle `matchNaive` sur 11 configurations (k = 9, 12, 18, 24, 25, 31 ; e = 0 à 5 ; +linéaire/circulaire ; séquences avec n, r, y, k, m ; cas hachés) ; égal à l'oracle sur les 40 +premières séquences réelles de chaque jeu ; égal à `obiapat` sur l'intégralité des 999 407 +occurrences de Teleostei ; et mêmes nombres de séquences que le journal d'ecoPrimers, mot par mot +(1 207 et 255 mots, 0 différence). + +Mesures `e = 3`, M3 Max : + +| Jeu | obiapat 1 worker | MatchWords 1 worker | obiapat 16 workers | MatchWords 16 workers | +|---|---|---|---|---| +| Teleostei (255 mots, 65 Mb) | 113 s | **0,42 s** (x270) | 9,4 s | **43 ms** (x220) | +| Cyprinidae (1 207 mots, 5,4 Mb) | non mesuré | non mesuré | 3,8 s | **13 ms** | + +Mémoire allouée par l'appariement : 120 Mo (Teleostei), 51 Mo (Cyprinidae), essentiellement le +stockage des occurrences. + +**Profil du pipeline complet, 16 workers** (lecture du FASTA gz comprise, taxonomie exclue) : + +| Étape | Cyprinidae | Teleostei | +|---|---|---| +| lecture + sélection | 0,46 s | 0,42 s | +| mots stricts | 0,10 s | 1,02 s | +| recherche approchée | 0,014 s | 0,041 s | +| paires + indices + Tm | 1,09 s | 1,20 s | +| total | ~1,7 s | ~2,7 s | + +ecoPrimers complet : 134 s et 350 s, soit environ 80 et 130 fois plus. Les résultats complets restent +ceux de la section 12 (Cyprinidae identique sur toutes les colonnes ; Teleostei : mêmes 407 paires, +6 à -1 séquence). + +**Décision SIMD** : l'appariement ne représente plus que 1 % du temps, donc un noyau SIMD pour la +recherche n'a plus d'objet. Les étapes restantes (lecture 0,4 s, mots stricts 1 s sur Teleostei, +paires 1,1 s) sont celles à profiler si l'on veut aller plus loin ; le tri des paires (kmer, id) +dans `SeqKmerCounter` et la phase 2 de `FindPairs` en sont les candidats. + +Non fait : option `-U`, `-R`, sets d'amorces, mode B, sortie CSV, CLI `obiprimers`. diff --git a/pkg/obiprimer/match_apat.go b/pkg/obiprimer/match_apat.go deleted file mode 100644 index d96e1dc..0000000 --- a/pkg/obiprimer/match_apat.go +++ /dev/null @@ -1,87 +0,0 @@ -package obiprimer - -import ( - "fmt" - - "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiapat" - "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq" -) - -// apatSafe returns seq when it only holds a, c, g, t; otherwise a copy where u -// becomes t and every other symbol becomes z. obiapat reads IUPAC codes in the -// sequence (an n matches every base); z is the symbol it matches with nothing, -// which gives back the ecoPrimers rule "an ambiguous base is an error". -func apatSafe(seq *obiseq.BioSequence) *obiseq.BioSequence { - raw := seq.Sequence() - clean := true - for _, b := range raw { - if b != 'a' && b != 'c' && b != 'g' && b != 't' { - clean = false - break - } - } - if clean { - return seq - } - out := make([]byte, len(raw)) - for i, b := range raw { - switch b { - case 'a', 'c', 'g', 't': - out[i] = b - case 'A', 'C', 'G', 'T': - out[i] = b + 'a' - 'A' - case 'u', 'U': - out[i] = 't' - default: - out[i] = 'z' - } - } - return obiseq.NewBioSequence(seq.Id(), out, "") -} - -// matchApat searches the words with obiapat, the bit-parallel Baeza-Yates / -// Manber matcher of OBITools (the engine of ecoPrimers and obipcr). -func matchApat(c *Corpus, words []uint64, p MatchParams) ([]WordMatches, error) { - if err := p.validate(); err != nil { - return nil, err - } - fwd, rev := wordStrings(words, p.K) - pats := make([][2]obiapat.ApatPattern, len(words)) - for i := range words { - var err error - if pats[i][0], err = obiapat.MakeApatPattern(fwd[i], p.MaxErrors, false); err != nil { - return nil, fmt.Errorf("obiprimer: pattern %s: %w", fwd[i], err) - } - if pats[i][1], err = obiapat.MakeApatPattern(rev[i], p.MaxErrors, false); err != nil { - return nil, fmt.Errorf("obiprimer: pattern %s: %w", rev[i], err) - } - } - - mk := func() scanFunc { - var recycle []obiapat.ApatSequence - return func(seq *obiseq.BioSequence, emit func(word, pos, errors int, reverse bool)) { - n := seq.Len() - if n < p.K { - return - } - aseq, err := obiapat.MakeApatSequence(apatSafe(seq), p.Circular, recycle...) - if err != nil { - panic(fmt.Sprintf("obiprimer: cannot encode sequence %s: %v", seq.Id(), err)) - } - recycle = append(recycle[:0], aseq) - - for w := range pats { - for strand := range 2 { - for _, m := range pats[w][strand].FindAllIndex(aseq, 0, -1) { - // On circular sequences obiapat also scans a copy of the - // start: those hits repeat the ones at start-n. - if m[0] >= 0 && m[0] < n { - emit(w, m[0], m[2], strand == 1) - } - } - } - } - } - } - return matchCorpus(c, words, mk), nil -} diff --git a/pkg/obiprimer/match_seed.go b/pkg/obiprimer/match_seed.go new file mode 100644 index 0000000..d720088 --- /dev/null +++ b/pkg/obiprimer/match_seed.go @@ -0,0 +1,214 @@ +package obiprimer + +import ( + "math/bits" + "slices" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq" +) + +// seedIndex maps the value of one half of a window to the patterns that are +// candidates for it: those whose same half is within d substitutions of it. +// +// Pigeonhole: a window with at most e = 2d+1 errors against a pattern has at +// most d errors in at least one of the two halves of the pattern, so the +// windows to verify are exactly those found through one of the two indexes. +// Indexing the half of every pattern together with its neighbours at distance +// d (1 + 3L values for d = 1, 1 + 3L + 9L(L-1)/2 for d = 2, L = 9 bases) costs +// a few tens of entries per pattern, and a window of a random text finds about +// patterns x neighbours / 4^L candidates, far fewer than the patterns. +type seedIndex struct { + shift uint // slot = seed >> shift when hashed, else seed + hashed bool // the seed space is too large for a direct table + offsets []uint32 // CSR: ids[offsets[s]:offsets[s+1]] are the candidates of slot s + ids []int32 // pattern ids (2*word + strand) +} + +const maxDirectSeedBits = 22 + +func (x *seedIndex) slot(seed uint64) uint32 { + if !x.hashed { + return uint32(seed) + } + return uint32((seed * 0x9E3779B97F4A7C15) >> x.shift) +} + +func (x *seedIndex) candidates(seed uint64) []int32 { + s := x.slot(seed) + return x.ids[x.offsets[s]:x.offsets[s+1]] +} + +// neighbours calls fn for v and every value within d substitutions of it (each +// once), v being L bases packed on 2 bits. +func neighbours(v uint64, L, d int, fn func(uint64)) { + var rec func(v uint64, from, left int) + rec = func(v uint64, from, left int) { + fn(v) + if left == 0 { + return + } + for pos := from; pos < L; pos++ { + for alt := uint64(1); alt <= 3; alt++ { + rec(v^(alt<<(2*uint(pos))), pos+1, left-1) + } + } + } + rec(v, 0, d) +} + +// newSeedIndex indexes, for every pattern, the half of the given width (in +// bases) selected by (code >> shiftBases*2) & mask, with its neighbours at +// distance d. +func newSeedIndex(patterns []uint64, shiftBases, width, d int) *seedIndex { + x := &seedIndex{} + bitsNeeded := 2 * width + if bitsNeeded > maxDirectSeedBits { + x.hashed, bitsNeeded = true, maxDirectSeedBits + x.shift = uint(64 - maxDirectSeedBits) + } + nslots := 1 << uint(bitsNeeded) + halfMask := uint64(1)<<(2*uint(width)) - 1 + + type entry struct { + slot uint32 + id int32 + } + var entries []entry + var scratch []uint32 + for id, pat := range patterns { + half := (pat >> (2 * uint(shiftBases))) & halfMask + scratch = scratch[:0] + neighbours(half, width, d, func(v uint64) { scratch = append(scratch, x.slot(v)) }) + slices.Sort(scratch) + scratch = slices.Compact(scratch) // hashing may merge neighbours + for _, s := range scratch { + entries = append(entries, entry{s, int32(id)}) + } + } + + x.offsets = make([]uint32, nslots+1) + for _, e := range entries { + x.offsets[e.slot+1]++ + } + for i := 1; i <= nslots; i++ { + x.offsets[i] += x.offsets[i-1] + } + x.ids = make([]int32, len(entries)) + fill := slices.Clone(x.offsets[:nslots]) + for _, e := range entries { + x.ids[fill[e.slot]] = e.id + fill[e.slot]++ + } + return x +} + +// MatchWords finds every approximate occurrence, on both strands, of the given +// canonical words in the sequences of the corpus, with the rules of MatchParams +// (substitutions only, ambiguous bases count as errors, circular junction). +// +// It uses the pigeonhole seed indexes above. It returns exactly what the +// reference matchNaive returns, and what ecoPrimers' fuzzy search finds, in a +// small fraction of the time: on the Teleostei tutorial data (255 words, 3909 +// sequences, 65 Mb, e = 3) 0.42 s on one core, against 113 s for the +// obiapat (agrep) search it replaces. +func MatchWords(c *Corpus, words []uint64, p MatchParams) ([]WordMatches, error) { + if err := p.validate(); err != nil { + return nil, err + } + k := p.K + d := p.MaxErrors / 2 + + // pattern 2w is the word, 2w+1 its reverse complement + patterns := make([]uint64, 2*len(words)) + for i, w := range words { + patterns[2*i] = codeOf(wordCodes(w, k, false), k) + patterns[2*i+1] = codeOf(wordCodes(w, k, true), k) + } + + right := k / 2 // the right half holds the low bits + left := k - right + leftIdx := newSeedIndex(patterns, right, left, d) + rightIdx := newSeedIndex(patterns, 0, right, d) + + rightMask := uint64(1)<<(2*uint(right)) - 1 + windowMask := uint64(1)<<(2*uint(k)) - 1 + lowBits := uint64(0x5555555555555555) & windowMask + maxErr := p.MaxErrors + + mk := func() scanFunc { + return func(seq *obiseq.BioSequence, emit func(word, pos, errors int, reverse bool)) { + raw := seq.Sequence() + n := len(raw) + if n < k { + return + } + total := n // number of bases fed to the rolling window + windows := n - k + 1 + if p.Circular { + total, windows = n+k-1, n + } + + var code, bad uint64 // window, and its non-ACGT positions (bit 2j) + verify := func(pos int, id int32) { + pat := patterns[id] + x := code ^ pat + if errs := bits.OnesCount64(((x | x>>1) & lowBits) | bad); errs <= maxErr { + emit(int(id)>>1, pos, errs, id&1 == 1) + } + } + + for i := 0; i < total; i++ { + j := i + if j >= n { + j -= n + } + v := baseCode[raw[j]] + isBad := uint64(0) + if v > 3 { + v, isBad = 0, 1 + } + code = (code<<2 | uint64(v)) & windowMask + bad = (bad<<2 | isBad) & windowMask + + if i < k-1 { + continue + } + pos := i - k + 1 + if pos >= windows { + break + } + leftCands := leftIdx.candidates(code >> (2 * uint(right))) + for _, id := range leftCands { + verify(pos, id) + } + for _, id := range rightIdx.candidates(code & rightMask) { + // ids are sorted: skip the ones the left index has just verified + if _, done := slices.BinarySearch(leftCands, id); !done { + verify(pos, id) + } + } + } + } + } + return matchCorpus(c, words, mk), nil +} + +// wordCodes returns the 2-bit codes (A=0..T=3) of the word or of its reverse +// complement, 5' to 3'. +func wordCodes(word uint64, k int, reverse bool) []byte { + fwd, rev := wordStrings([]uint64{word}, k) + s := fwd[0] + if reverse { + s = rev[0] + } + return codesOf(s) +} + +// codeOf packs codes (each 0..3) on 2 bits, first base in the high bits. +func codeOf(codes []byte, k int) uint64 { + var v uint64 + for _, c := range codes[:k] { + v = v<<2 | uint64(c) + } + return v +} diff --git a/pkg/obiprimer/match_test.go b/pkg/obiprimer/match_test.go index 65ccd82..5ba8f77 100644 --- a/pkg/obiprimer/match_test.go +++ b/pkg/obiprimer/match_test.go @@ -71,8 +71,8 @@ func TestMatchParamsValidation(t *testing.T) { if _, err := matchNaive(c, nil, p); err == nil { t.Errorf("%+v must be rejected", p) } - if _, err := matchApat(c, nil, p); err == nil { - t.Errorf("%+v must be rejected by obiapat variant", p) + if _, err := MatchWords(c, nil, p); err == nil { + t.Errorf("%+v must be rejected by MatchWords", p) } } } @@ -130,7 +130,7 @@ func TestMatchersAgreeWithReference(t *testing.T) { for _, tc := range []struct { k, e int circular bool - }{{18, 0, false}, {18, 1, true}, {18, 3, false}, {18, 3, true}, {12, 2, true}, {25, 4, false}} { + }{{18, 0, false}, {18, 1, true}, {18, 3, false}, {18, 3, true}, {12, 2, true}, {25, 4, false}, {18, 2, true}, {18, 5, false}, {31, 3, true}, {24, 3, false}, {9, 2, true}, {18, 6, true}, {18, 7, false}, {20, 8, true}, {12, 11, false}} { t.Run(fmt.Sprintf("k%d-e%d-circ%v", tc.k, tc.e, tc.circular), func(t *testing.T) { rng := rand.New(rand.NewSource(int64(tc.k*100 + tc.e))) var wordStr []string @@ -155,13 +155,13 @@ func TestMatchersAgreeWithReference(t *testing.T) { t.Fatalf("test data too poor: only %d reference matches", total) } - got, err := matchApat(c, words, p) + got, err := MatchWords(c, words, p) if err != nil { t.Fatal(err) } for i := range want { if !reflect.DeepEqual(got[i].Matches, want[i].Matches) { - t.Fatalf("word %d: obiapat gives %v\nreference gives %v", i, got[i].Matches, want[i].Matches) + t.Fatalf("word %d: MatchWords gives %v\nreference gives %v", i, got[i].Matches, want[i].Matches) } } }) @@ -200,7 +200,7 @@ func readEcoCounts(t *testing.T, path string) map[string]int { // strict word test). For every strict word, ecoPrimers logs the number of // example sequences in which the fuzzy search (-e 3, both strands, circular) // finds it; testdata/ecoprimers_*.words.tsv holds those numbers. -func TestMatchApatAgainstEcoPrimers(t *testing.T) { +func TestMatchWordsAgainstEcoPrimers(t *testing.T) { dir := os.Getenv("OBIPRIMERS_TUTORIAL") if dir == "" { t.Skip("OBIPRIMERS_TUTORIAL not set") @@ -244,7 +244,7 @@ func TestMatchApatAgainstEcoPrimers(t *testing.T) { runtime.GC() runtime.ReadMemStats(&before) start := time.Now() - res, err := matchApat(corpus, words, MatchParams{K: 18, MaxErrors: 3, Circular: true}) + res, err := MatchWords(corpus, words, MatchParams{K: 18, MaxErrors: 3, Circular: true}) elapsed := time.Since(start) runtime.ReadMemStats(&after) if err != nil { @@ -258,10 +258,29 @@ func TestMatchApatAgainstEcoPrimers(t *testing.T) { for _, s := range corpus.Seqs { bases += s.Len() } - t.Logf("obiapat: %d words x %d sequences (%.1f Mb) e=3 in %s (%.0f Mb/s per word-strand pass); %d matches; heap +%.0f MB, allocated %.0f MB", - len(words), len(corpus.Seqs), float64(bases)/1e6, elapsed.Round(time.Millisecond), - float64(bases)*float64(len(words))*2/1e6/elapsed.Seconds(), nm, - float64(after.HeapAlloc-before.HeapAlloc)/1e6, float64(after.TotalAlloc-before.TotalAlloc)/1e6) + t.Logf("%d words x %d sequences (%.1f Mb) e=3 in %s; %d matches; allocated %.0f MB", + len(words), len(corpus.Seqs), float64(bases)/1e6, elapsed.Round(time.Millisecond), nm, + float64(after.TotalAlloc-before.TotalAlloc)/1e6) + + // On real sequences (with their ambiguity codes), the first ones must give + // exactly what the reference implementation gives. + sub := &Corpus{Seqs: corpus.Seqs[:40], Roles: corpus.Roles[:40]} + wantSub, err := matchNaive(sub, words, MatchParams{K: 18, MaxErrors: 3, Circular: true}) + if err != nil { + t.Fatal(err) + } + gotSub, err := MatchWords(sub, words, MatchParams{K: 18, MaxErrors: 3, Circular: true}) + if err != nil { + t.Fatal(err) + } + subMatches := 0 + for i := range wantSub { + subMatches += len(wantSub[i].Matches) + if !reflect.DeepEqual(gotSub[i].Matches, wantSub[i].Matches) { + t.Fatalf("word %d differs from the reference on the first 40 sequences", i) + } + } + t.Logf("first 40 sequences identical to the reference (%d matches)", subMatches) eco := readEcoCounts(t, filepath.Join("testdata", "ecoprimers_"+tc.name+".words.tsv")) compared, differ := 0, 0 diff --git a/pkg/obiprimer/pairs_test.go b/pkg/obiprimer/pairs_test.go index 8d673d1..e167772 100644 --- a/pkg/obiprimer/pairs_test.go +++ b/pkg/obiprimer/pairs_test.go @@ -136,7 +136,7 @@ func TestPairsAgainstEcoPrimers(t *testing.T) { for i, w := range sw.Words { words[i] = w.Kmer } - wm, err := matchApat(corpus, words, MatchParams{K: 18, MaxErrors: 3, Circular: true}) + wm, err := MatchWords(corpus, words, MatchParams{K: 18, MaxErrors: 3, Circular: true}) if err != nil { t.Fatal(err) } From c156f119473ce4bf6b2c85557089f9ac39d6db09 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Tue, 3 Nov 2026 15:14:04 +1100 Subject: [PATCH 17/24] Introduce primer pair searching functionality This change introduces the `obiprimers` command-line tool and a complete search pipeline for identifying primer pairs. It includes new data structures for pair evaluation, comprehensive search configuration options, and detailed CSV output handling for results. --- OBIPRIMERS_NOTES.md | 63 +++++++ cmd/obitools/obiprimers/main.go | 41 +++++ pkg/obiprimer/pairs_eval.go | 27 ++- pkg/obiprimer/pairs_test.go | 2 +- pkg/obiprimer/search.go | 91 +++++++++++ pkg/obitools/obiprimers/obiprimers.go | 62 +++++++ pkg/obitools/obiprimers/obiprimers_test.go | 115 +++++++++++++ pkg/obitools/obiprimers/options.go | 182 +++++++++++++++++++++ pkg/obitools/obiprimers/output.go | 78 +++++++++ 9 files changed, 655 insertions(+), 6 deletions(-) create mode 100644 cmd/obitools/obiprimers/main.go create mode 100644 pkg/obiprimer/search.go create mode 100644 pkg/obitools/obiprimers/obiprimers.go create mode 100644 pkg/obitools/obiprimers/obiprimers_test.go create mode 100644 pkg/obitools/obiprimers/options.go create mode 100644 pkg/obitools/obiprimers/output.go diff --git a/OBIPRIMERS_NOTES.md b/OBIPRIMERS_NOTES.md index 76fc038..20b1712 100644 --- a/OBIPRIMERS_NOTES.md +++ b/OBIPRIMERS_NOTES.md @@ -410,3 +410,66 @@ paires 1,1 s) sont celles à profiler si l'on veut aller plus loin ; le tri des dans `SeqKmerCounter` et la phase 2 de `FindPairs` en sont les candidats. Non fait : option `-U`, `-R`, sets d'amorces, mode B, sortie CSV, CLI `obiprimers`. + +## 14. Commande `obiprimers`, mode A (2026-11-03) + +Code : `pkg/obiprimer/search.go` (`SearchParams`, `DefaultSearchParams`, `Search`, `SearchInfo` ; +`FindPairs` renvoie maintenant aussi un `PairInfo`), `pkg/obitools/obiprimers/{options,obiprimers,output}.go`, +`cmd/obitools/obiprimers/main.go`, test `obiprimers_test.go`. Le `Makefile` découvre la commande tout seul +(`wildcard cmd/obitools/*/*.go`). Pattern : `InputOptionSet` + `OutputModeOptionSet` + `-t` obligatoire, +sortie par `obiitercsv` + `obicsv.CLICSVWriter` (donc `-o`, `--compress`), comme `obitaxonomy`. + +Options (noms alignés sur `obipcr` et `obigrep` quand ils existent) : + +| Option | ecoPrimers | Défaut | +|---|---|---| +| `-t/--taxonomy` (obligatoire) | `-d` (base) | | +| `-r/--restrict-to-taxon` | `-r` | tous les exemples | +| `-i/--ignore-taxon` | `-i` | | +| `-E/--exception-taxon` | `-E` | | +| `--rank` | `-t` | species | +| `-c/--circular` | `-c` | non | +| `--primer-length` | `-O` | 18 | +| `-e/--allowed-mismatches` | `-e` | 3 | +| `-l/--min-length`, `-L/--max-length` | `-l`, `-L` | 0, 1000 | +| `-3/--three-prime` | `-3` | 0 | +| `-q/--strict-quorum` | `-q` | 0,7 | +| `-s/--sensitivity` | `-s` | 0,9 | +| `-x/--false-positive` | `-x` | 0,1 | +| `-T/--specificity` | `-T` | 0,6 | +| `--salt-correction`, `--salt`, `--primer-concentration` | `-m`, `-a` | santalucia, 0,05 M, 8e-7 M | +| `--partitions` | (nouveau) | 256 | + +`-O` n'a pas d'alias court : dans obitools4 il désigne l'en-tête OBI en sortie. Non reprises : `-U` +(pas de multi-match), `-R` (séquence de référence), `-p/-b/-M/-k` (sets et liens), `-f`, `-v`, `-A`. + +Sortie CSV (en-têtes nommés, ordre des colonnes d'ecoPrimers) : `index, primer1, primer2, tm1, tm1_min, +tm2, tm2_min, gc1, gc2, good1, good2, examples, counterexamples, yule, example_taxa, +counterexample_taxa, bc, identified_taxa, bs, min_length, max_length, mean_length`. Tm arrondies à +0,1 °C, indices à 4 décimales, `NA` quand une Tm est indisponible. Le résumé (nombres de mots, de paires, +d'exemples...) va dans le journal (stderr), pas dans le CSV : les lignes `#` d'ecoPrimers sont donc +remplacées par des messages `INFO`. + +**Contrôle** : `obiprimers -t ncbitaxo_20250211.tgz -r 7953 -e 3 --three-prime 2 -l 30 -L 150 -c +mito.vert.fasta.gz` : 3 448 lignes, comparées à `Cyprinidae.ecoprimers` champ par champ (script jetable) : +**0 différence**. Teleostei : 407 lignes (les 6 écarts d'une séquence de la section 12 s'appliquent). + +Mesures du binaire complet (M3 Max, 16 workers, taxonomie NCBI complète comprise) : + +| Jeu | Temps | Pic mémoire | ecoPrimers (temps, pic) | +|---|---|---|---| +| Cyprinidae | 7,4 s | 2,82 Go | 134 s, 1,86 Go | +| Teleostei | 8,4 s | 3,42 Go | 350 s, 2,11 Go | + +**Le pic de mémoire est PLUS ÉLEVÉ qu'ecoPrimers**, malgré un temps 18 à 40 fois meilleur. Éléments mesurés : +le chargeur de taxonomie NCBI existant coûte à lui seul 1,07 Go (`obitaxonomy`, mesuré) ; avec +`GOGC=25` le pic de Cyprinidae passe de 2,82 à 1,98 Go (donc ~0,85 Go de ramasse-miettes en retard, au +prix de 2x de temps CPU) ; le reste (~0,9 Go au-dessus de la taxonomie pour 5,4 Mb d'exemples) n'est pas +analysé. Pistes non explorées : ne pas parser les 131 Mb du FASTA des séquences ignorées, libérer la +taxonomie après `AssignTaxa`, régler le GC de la commande. + +Avertissements `Taxid ... has to be updated` : viennent du mécanisme existant d'obitools (taxids +fusionnés dans le FASTA) ; `--update-taxid` les corrige à la lecture. + +Reste à faire : mode B, `-U`, `-R`, tests shell `obitests/obitools/obiprimers` (données dans le dépôt), +documentation, réduction de la mémoire, comparaison systématique `--rank` autre que species. diff --git a/cmd/obitools/obiprimers/main.go b/cmd/obitools/obiprimers/main.go new file mode 100644 index 0000000..29f095e --- /dev/null +++ b/cmd/obitools/obiprimers/main.go @@ -0,0 +1,41 @@ +package main + +import ( + "os" + + log "github.com/sirupsen/logrus" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obidefault" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obioptions" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obitools/obiconvert" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obitools/obicsv" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obitools/obiprimers" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiutils" +) + +func main() { + optionParser := obioptions.GenerateOptionParser( + "obiprimers", + "designs the primer pairs amplifying a set of example sequences and evaluates the DNA barcode they delimit", + obiprimers.OptionSet) + + _, args := optionParser(os.Args) + + if !obidefault.HasSelectedTaxonomy() { + log.Fatal("you must indicate a taxonomy using the -t or --taxonomy option") + } + + sequences, err := obiconvert.CLIReadBioSequences(args...) + if err != nil { + log.Fatalf("Cannot open the sequence files: %v", err) + } + + pairs, err := obiprimers.CLIPrimerPairs(sequences) + if err != nil { + log.Fatalf("%v", err) + } + + obicsv.CLICSVWriter(obiprimers.PairsToCSVIterator(pairs), true) + + obiutils.WaitForLastPipe() +} diff --git a/pkg/obiprimer/pairs_eval.go b/pkg/obiprimer/pairs_eval.go index ece6e0a..cd37864 100644 --- a/pkg/obiprimer/pairs_eval.go +++ b/pkg/obiprimer/pairs_eval.go @@ -36,6 +36,14 @@ type PairStats struct { // Score is the ranking criterion of ecoPrimers. func (p PairStats) Score() float32 { return p.Yule * p.Bs } +// PairInfo summarizes the pair search, for reporting. +type PairInfo struct { + Primers int // conserved words kept as candidate primers + GoodPrimers int // of which good ones + CandidatePairs int // pairs delimiting an amplicon in at least one sequence + RetainedPairs int // pairs passing the sensitivity and false positive quorums, before the Bs filter +} + // ampRecord is an amplicon of a retained pair, in one sequence. type ampRecord struct { pair int32 @@ -78,21 +86,29 @@ func ampHash(raw []byte, from, to int, reverse bool) uint64 { // is sorted by decreasing Yule*Bs. // // model computes the melting temperatures; nil leaves them NaN. -func FindPairs(c *Corpus, taxa *Taxa, wm []WordMatches, p PairParams, model *obithermo.Model) ([]PairStats, error) { +func FindPairs(c *Corpus, taxa *Taxa, wm []WordMatches, p PairParams, model *obithermo.Model) ([]PairStats, PairInfo, error) { + var info PairInfo if err := p.validate(); err != nil { - return nil, err + return nil, info, err } if taxa == nil || len(taxa.Of) != len(c.Seqs) { - return nil, fmt.Errorf("obiprimer: taxa do not match the corpus") + return nil, info, fmt.Errorf("obiprimer: taxa do not match the corpus") } ps := selectPrimers(c, wm, p) + info.Primers = len(ps.primers) + for _, pr := range ps.primers { + if pr.Good { + info.GoodPrimers++ + } + } if len(ps.primers) == 0 { - return nil, nil + return nil, info, nil } // Phase 1: which pairs amplify enough examples and few enough counterexamples. counts := c.Counts() acc := ps.countPairs(c, p) + info.CandidatePairs = len(acc) sens, fpq := float32(p.Sensitivity), float32(p.FalsePositive) var kept []pairKey for k, a := range acc { @@ -105,6 +121,7 @@ func FindPairs(c *Corpus, taxa *Taxa, wm []WordMatches, p PairParams, model *obi kept = append(kept, k) } } + info.RetainedPairs = len(kept) slices.SortFunc(kept, func(a, b pairKey) int { return cmp.Or(cmp.Compare(a.p1, b.p1), cmp.Compare(a.p2, b.p2), boolCmp(a.a1, b.a1), boolCmp(a.a2, b.a2)) @@ -131,7 +148,7 @@ func FindPairs(c *Corpus, taxa *Taxa, wm []WordMatches, p PairParams, model *obi slices.SortFunc(out, func(a, b PairStats) int { return cmp.Or(-cmp.Compare(a.Score(), b.Score()), cmp.Compare(a.Primer1, b.Primer1), cmp.Compare(a.Primer2, b.Primer2)) }) - return out, nil + return out, info, nil } func boolCmp(a, b bool) int { diff --git a/pkg/obiprimer/pairs_test.go b/pkg/obiprimer/pairs_test.go index e167772..9160cd7 100644 --- a/pkg/obiprimer/pairs_test.go +++ b/pkg/obiprimer/pairs_test.go @@ -141,7 +141,7 @@ func TestPairsAgainstEcoPrimers(t *testing.T) { t.Fatal(err) } lap("approximate search") - got, err := FindPairs(corpus, taxa, wm, PairParams{ + got, _, err := FindPairs(corpus, taxa, wm, PairParams{ PrimerLength: 18, MinLength: 30, MaxLength: 150, ThreePrime: 2, Sensitivity: 0.9, FalsePositive: 0.1, MinSpecificity: 0.6, Circular: true, }, obithermo.DefaultModel()) diff --git a/pkg/obiprimer/search.go b/pkg/obiprimer/search.go new file mode 100644 index 0000000..3a48567 --- /dev/null +++ b/pkg/obiprimer/search.go @@ -0,0 +1,91 @@ +package obiprimer + +import ( + "fmt" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obithermo" +) + +// SearchParams gathers every parameter of the primer pair search, named after +// the ecoPrimers options they come from. +type SearchParams struct { + PrimerLength int // -O: size of the primers (default 18) + MaxErrors int // -e: substitutions allowed between a primer and its sites (default 3) + MinLength int // -l: the amplicon (primers excluded) must be longer than this + MaxLength int // -L: and shorter than this + ThreePrime int // -3: number of 3' bases that must match exactly + StrictQuorum float64 // -q: fraction of the examples containing the exact word (0.7) + Sensitivity float64 // -s: fraction of the examples a primer must match (0.9) + FalsePositive float64 // -x: fraction of the counterexamples it may match (0.1) + MinSpecificity float64 // -T: pairs with a lower Bs are dropped (0.6) + Circular bool // -c: sequences are circular + + Partitions int // partitions of the k-mer counter (default 256) + TmpDir string + + Model *obithermo.Model // melting temperatures; nil leaves them NaN +} + +// DefaultSearchParams returns the ecoPrimers defaults. +func DefaultSearchParams() SearchParams { + return SearchParams{ + PrimerLength: 18, MaxErrors: 3, MinLength: 0, MaxLength: 1000, + StrictQuorum: 0.7, Sensitivity: 0.9, FalsePositive: 0.1, MinSpecificity: 0.6, + Partitions: 256, Model: obithermo.DefaultModel(), + } +} + +// SearchInfo reports the size of every stage of a search. +type SearchInfo struct { + Counts Counts + StrictWords int // words found without error in at least StrictQuorum of the examples + MinStrict int // that quorum, in sequences + Matches int // approximate occurrences of those words + Pair PairInfo +} + +// Search runs the whole ecoPrimers pipeline on a corpus: strict words, +// approximate occurrences, pairs and their indices. The pairs are sorted by +// decreasing Yule*Bs. +func Search(c *Corpus, taxa *Taxa, p SearchParams) ([]PairStats, *SearchInfo, error) { + if p.MaxErrors < 0 { + return nil, nil, fmt.Errorf("obiprimer: the allowed mismatch count cannot be negative") + } + + sw, err := FindStrictWords(c, WordSearch{ + PrimerLength: p.PrimerLength, + Quorum: p.StrictQuorum, + Circular: p.Circular, + Partitions: p.Partitions, + TmpDir: p.TmpDir, + }) + if err != nil { + return nil, nil, err + } + info := &SearchInfo{Counts: sw.Counts, StrictWords: len(sw.Words), MinStrict: sw.MinSequences} + + words := make([]uint64, len(sw.Words)) + for i, w := range sw.Words { + words[i] = w.Kmer + } + wm, err := MatchWords(c, words, MatchParams{K: p.PrimerLength, MaxErrors: p.MaxErrors, Circular: p.Circular}) + if err != nil { + return nil, info, err + } + for _, w := range wm { + info.Matches += len(w.Matches) + } + + pairs, pinfo, err := FindPairs(c, taxa, wm, PairParams{ + PrimerLength: p.PrimerLength, + MinLength: p.MinLength, + MaxLength: p.MaxLength, + ThreePrime: p.ThreePrime, + Sensitivity: p.Sensitivity, + FalsePositive: p.FalsePositive, + MinSpecificity: p.MinSpecificity, + Circular: p.Circular, + }, p.Model) + info.Pair = pinfo + return pairs, info, err +} diff --git a/pkg/obitools/obiprimers/obiprimers.go b/pkg/obitools/obiprimers/obiprimers.go new file mode 100644 index 0000000..0362434 --- /dev/null +++ b/pkg/obitools/obiprimers/obiprimers.go @@ -0,0 +1,62 @@ +package obiprimers + +import ( + "fmt" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiiter" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiprimer" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obitax" + log "github.com/sirupsen/logrus" +) + +// CLIPrimerPairs reads the sequences, selects the examples and the +// counterexamples from the taxonomy options, and searches the primer pairs. +// The default taxonomy must be loaded. +func CLIPrimerPairs(sequences obiiter.IBioSequence) ([]obiprimer.PairStats, error) { + taxonomy := obitax.DefaultTaxonomy() + if taxonomy == nil { + return nil, fmt.Errorf("a taxonomy is required: use the -t option") + } + + sel := obiprimer.Selection{} + var err error + if sel.Restrict, err = obiprimer.BelongingTo(taxonomy, CLIRestrictTaxa()); err != nil { + return nil, err + } + if sel.Ignore, err = obiprimer.BelongingTo(taxonomy, CLIIgnoreTaxa()); err != nil { + return nil, err + } + if sel.Except, err = obiprimer.BelongingTo(taxonomy, CLIExceptionTaxa()); err != nil { + return nil, err + } + + params, err := CLISearchParams() + if err != nil { + return nil, err + } + + log.Infof("Reading the sequences") + corpus, err := obiprimer.LoadCorpus(sequences, sel) + if err != nil { + return nil, err + } + taxa, err := corpus.AssignTaxa(taxonomy, CLIRank()) + if err != nil { + return nil, err + } + + counts := corpus.Counts() + log.Infof("%d example sequences (%d %s), %d counterexamples (%d %s), %d ignored", + counts.Examples, taxa.ExampleTaxa, CLIRank(), + counts.CounterExamples, taxa.CounterExamples, CLIRank(), counts.Ignored) + + pairs, info, err := obiprimer.Search(corpus, taxa, params) + if info != nil { + log.Infof("%d strict words (found without error in at least %d examples)", info.StrictWords, info.MinStrict) + log.Infof("%d approximate occurrences; %d conserved primers, %d good", + info.Matches, info.Pair.Primers, info.Pair.GoodPrimers) + log.Infof("%d candidate pairs, %d amplifying enough examples, %d retained", + info.Pair.CandidatePairs, info.Pair.RetainedPairs, len(pairs)) + } + return pairs, err +} diff --git a/pkg/obitools/obiprimers/obiprimers_test.go b/pkg/obitools/obiprimers/obiprimers_test.go new file mode 100644 index 0000000..9c33a27 --- /dev/null +++ b/pkg/obitools/obiprimers/obiprimers_test.go @@ -0,0 +1,115 @@ +package obiprimers + +import ( + "math" + "testing" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiprimer" +) + +func samplePairs() []obiprimer.PairStats { + return []obiprimer.PairStats{ + { + Primer1: "ACACCGCCCGTCACTCTC", Primer2: "CCAAGTGCACCTTCCGGT", Good1: true, Good2: false, + Examples: 3837, CounterExamples: 2, Yule: 0.98249996, ExampleTaxa: 3805, CounterTaxa: 1, + Bc: 0.98169, Identified: 2650, Bs: 0.69647, MinLen: 54, MaxLen: 140, MeanLen: 74.75123, + Tm1: 62.46, Tm2: 60.74, MinTm1: 36.81, MinTm2: math.NaN(), GC1: 12, GC2: 11, + }, + {Primer1: "AAAA", Primer2: "TTTT", Tm1: math.NaN(), Tm2: math.NaN(), MinTm1: math.NaN(), MinTm2: math.NaN()}, + } +} + +func TestPairsToCSVIterator(t *testing.T) { + it := PairsToCSVIterator(samplePairs()) + + header := it.Header() + if len(header) != len(csvColumns) || header[0] != "index" || header[1] != "primer1" { + t.Fatalf("unexpected header %v", header) + } + + var records []map[string]interface{} + for it.Next() { + batch := it.Get() + for _, r := range batch.Slice() { + records = append(records, r) + } + } + if len(records) != 2 { + t.Fatalf("%d records, want 2", len(records)) + } + + r := records[0] + if r["index"] != 1 || r["primer1"] != "ACACCGCCCGTCACTCTC" || r["good1"] != true || r["good2"] != false { + t.Errorf("unexpected record %v", r) + } + if r["yule"] != 0.9825 || r["bc"] != 0.9817 || r["bs"] != 0.6965 || r["mean_length"] != 74.75 { + t.Errorf("indices are not rounded as expected: %v %v %v %v", r["yule"], r["bc"], r["bs"], r["mean_length"]) + } + if r["tm1"] != 62.5 || r["tm2"] != 60.7 || r["tm1_min"] != 36.8 { + t.Errorf("temperatures: %v %v %v", r["tm1"], r["tm2"], r["tm1_min"]) + } + if _, present := r["tm2_min"]; present { + t.Error("an unavailable temperature must be left out, to be written as NA") + } + if _, present := records[1]["tm1"]; present { + t.Error("NaN temperatures must be left out") + } + if records[1]["index"] != 2 { + t.Errorf("second pair index %v", records[1]["index"]) + } +} + +func TestEmptyResultStillHasAHeader(t *testing.T) { + it := PairsToCSVIterator(nil) + if len(it.Header()) != len(csvColumns) { + t.Fatalf("header %v", it.Header()) + } + n := 0 + for it.Next() { + batch := it.Get() + n += len(batch.Slice()) + } + if n != 0 { + t.Errorf("%d records for no pairs", n) + } +} + +func TestEveryColumnIsProduced(t *testing.T) { + full := pairRecord(1, samplePairs()[0]) + full["tm2_min"] = 1.0 // the only column left out for this pair + for _, c := range csvColumns { + if _, ok := full[c]; !ok { + t.Errorf("column %q is never filled", c) + } + } + if len(full) != len(csvColumns) { + t.Errorf("record has %d fields for %d columns", len(full), len(csvColumns)) + } +} + +func TestCLISearchParams(t *testing.T) { + p, err := CLISearchParams() + if err != nil { + t.Fatal(err) + } + d := obiprimer.DefaultSearchParams() + if p.PrimerLength != d.PrimerLength || p.MaxErrors != d.MaxErrors || p.MaxLength != d.MaxLength || + p.StrictQuorum != d.StrictQuorum || p.Sensitivity != d.Sensitivity || + p.FalsePositive != d.FalsePositive || p.MinSpecificity != d.MinSpecificity || p.Model == nil { + t.Errorf("option defaults %+v differ from the ecoPrimers defaults %+v", p, d) + } + + _saltMethod = "owczarzy" + if p, err = CLISearchParams(); err != nil || p.Model == nil { + t.Errorf("owczarzy must be accepted: %v", err) + } + _saltMethod = "nope" + if _, err = CLISearchParams(); err == nil { + t.Error("an unknown salt correction must be refused") + } + _saltMethod = "santalucia" + _salt = -1 + if _, err = CLISearchParams(); err == nil { + t.Error("a negative salt concentration must be refused") + } +} diff --git a/pkg/obitools/obiprimers/options.go b/pkg/obitools/obiprimers/options.go new file mode 100644 index 0000000..84cb611 --- /dev/null +++ b/pkg/obitools/obiprimers/options.go @@ -0,0 +1,182 @@ +// obiprimers function utility package. +// +// The obitools/obiprimers package contains every function specific to the +// obiprimers utility, the OBITools4 version of ecoPrimers: it designs the +// primer pairs that best amplify a set of example sequences, optionally +// avoiding a set of counterexample sequences, and measures the quality of the +// DNA marker each pair delimits. +package obiprimers + +import ( + "fmt" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obioptions" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiprimer" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obithermo" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obitools/obiconvert" + "github.com/DavidGamba/go-getoptions" +) + +var _restrict = []string{} +var _ignore = []string{} +var _except = []string{} +var _rank = "species" + +var _primerLength = 18 +var _allowedMismatch = 3 +var _minLength = 0 +var _maxLength = 1000 +var _threePrime = 0 +var _circular = false + +var _strictQuorum = 0.7 +var _sensitivity = 0.9 +var _falsePositive = 0.1 +var _specificity = 0.6 + +var _saltMethod = "santalucia" +var _salt = obithermo.DefaultSalt +var _primerConc = obithermo.DefaultPrimerConc + +var _partitions = 256 + +// OptionSet registers the options of obiprimers. +func OptionSet(options *getoptions.GetOpt) { + obiconvert.InputOptionSet(options) + obiconvert.OutputModeOptionSet(options, true) + obioptions.LoadTaxonomyOptionSet(options, true, false) + + options.StringSliceVar(&_restrict, "restrict-to-taxon", 1, 1, + options.Alias("r"), + options.ArgName("TAXID"), + options.Description("Restricts the examples (the sequences the primers must amplify) to the "+ + "sequences belonging to this taxon. Other sequences are ignored, unless they are "+ + "counterexamples. Can be repeated. Without this option every sequence is an example.")) + + options.StringSliceVar(&_ignore, "ignore-taxon", 1, 1, + options.Alias("i"), + options.ArgName("TAXID"), + options.Description("Declares the sequences belonging to this taxon as counterexamples "+ + "(sequences the primers must not amplify). Can be repeated.")) + + options.StringSliceVar(&_except, "exception-taxon", 1, 1, + options.Alias("E"), + options.ArgName("TAXID"), + options.Description("Declares a sub-clade of the examples as counterexamples. Can be repeated.")) + + options.StringVar(&_rank, "rank", _rank, + options.ArgName("RANK"), + options.Description("Taxonomic rank at which coverage (Bc) and specificity (Bs) are computed.")) + + options.BoolVar(&_circular, "circular", _circular, + options.Alias("c"), + options.Description("Considers the sequences as circular (whole organelle genomes).")) + + options.IntVar(&_primerLength, "primer-length", _primerLength, + options.ArgName("LENGTH"), + options.Description("Size of the primers, from 2 to 31.")) + + options.IntVar(&_allowedMismatch, "allowed-mismatches", _allowedMismatch, + options.Alias("e"), + options.ArgName("ERRORS"), + options.Description("Maximum number of substitutions between a primer and a site it matches. "+ + "The search is fast up to about 5; beyond, the number of matches explodes.")) + + options.IntVar(&_minLength, "min-length", _minLength, + options.Alias("l"), + options.ArgName("LENGTH"), + options.Description("The amplified region, primers excluded, must be longer than this.")) + + options.IntVar(&_maxLength, "max-length", _maxLength, + options.Alias("L"), + options.ArgName("LENGTH"), + options.Description("The amplified region, primers excluded, must be shorter than this.")) + + options.IntVar(&_threePrime, "three-prime", _threePrime, + options.Alias("3"), + options.ArgName("BASES"), + options.Description("Number of bases at the 3' end of each primer that must match the "+ + "amplified sequence exactly (an ambiguous base is not a match).")) + + options.Float64Var(&_strictQuorum, "strict-quorum", _strictQuorum, + options.Alias("q"), + options.ArgName("FRACTION"), + options.Description("Fraction of the examples in which a primer word must be found without any error.")) + + options.Float64Var(&_sensitivity, "sensitivity", _sensitivity, + options.Alias("s"), + options.ArgName("FRACTION"), + options.Description("Fraction of the examples a primer must match (with allowed mismatches), "+ + "and a pair must amplify.")) + + options.Float64Var(&_falsePositive, "false-positive", _falsePositive, + options.Alias("x"), + options.ArgName("FRACTION"), + options.Description("Fraction of the counterexamples a good primer may match, and a pair may amplify.")) + + options.Float64Var(&_specificity, "specificity", _specificity, + options.Alias("T"), + options.ArgName("FRACTION"), + options.Description("Pairs whose specificity Bs is lower than this are dropped.")) + + options.StringVar(&_saltMethod, "salt-correction", _saltMethod, + options.ArgName("METHOD"), + options.Description("Salt correction of the melting temperatures: 'santalucia' or 'owczarzy'.")) + + options.Float64Var(&_salt, "salt", _salt, + options.ArgName("MOLAR"), + options.Description("Monovalent salt concentration (M) used for the melting temperatures.")) + + options.Float64Var(&_primerConc, "primer-concentration", _primerConc, + options.ArgName("MOLAR"), + options.Description("Primer concentration (M) used for the melting temperatures.")) + + options.IntVar(&_partitions, "partitions", _partitions, + options.ArgName("COUNT"), + options.Description("Number of temporary partition files used to count the conserved words. "+ + "Increase it for very large inputs (memory per partition grows with data size / COUNT).")) +} + +// CLIRestrictTaxa returns the taxids restricting the examples. +func CLIRestrictTaxa() []string { return _restrict } + +// CLIIgnoreTaxa returns the taxids of the counterexample clades. +func CLIIgnoreTaxa() []string { return _ignore } + +// CLIExceptionTaxa returns the taxids of the sub-clades turned into counterexamples. +func CLIExceptionTaxa() []string { return _except } + +// CLIRank returns the taxonomic rank of the coverage and specificity indices. +func CLIRank() string { return _rank } + +// CLISearchParams builds the search parameters from the command line. +func CLISearchParams() (obiprimer.SearchParams, error) { + var method obithermo.SaltMethod + switch _saltMethod { + case "santalucia": + method = obithermo.SantaLucia + case "owczarzy": + method = obithermo.Owczarzy + default: + return obiprimer.SearchParams{}, fmt.Errorf("unknown salt correction %q: use 'santalucia' or 'owczarzy'", _saltMethod) + } + model, err := obithermo.NewModel(_primerConc, obithermo.DefaultTargetConc, _salt, method) + if err != nil { + return obiprimer.SearchParams{}, err + } + + return obiprimer.SearchParams{ + PrimerLength: _primerLength, + MaxErrors: _allowedMismatch, + MinLength: _minLength, + MaxLength: _maxLength, + ThreePrime: _threePrime, + StrictQuorum: _strictQuorum, + Sensitivity: _sensitivity, + FalsePositive: _falsePositive, + MinSpecificity: _specificity, + Circular: _circular, + Partitions: _partitions, + Model: model, + }, nil +} diff --git a/pkg/obitools/obiprimers/output.go b/pkg/obitools/obiprimers/output.go new file mode 100644 index 0000000..49c3dc6 --- /dev/null +++ b/pkg/obitools/obiprimers/output.go @@ -0,0 +1,78 @@ +package obiprimers + +import ( + "math" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obidefault" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiitercsv" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiprimer" +) + +// Columns of the output, in the order of the ecoPrimers result table. +var csvColumns = []string{ + "index", // 1 serial number + "primer1", "primer2", // 2-3 + "tm1", "tm1_min", "tm2", "tm2_min", // 4-7 melting temperatures: perfect match, lowest over the examples + "gc1", "gc2", // 8-9 + "good1", "good2", // 10 primer conserved in the examples and rare in the counterexamples + "examples", "counterexamples", // 11-12 sequences amplified + "yule", // 13 + "example_taxa", "counterexample_taxa", // 14-15 taxa amplified + "bc", // 16 coverage + "identified_taxa", // 17 + "bs", // 18 specificity + "min_length", "max_length", "mean_length", // 19-21 +} + +// round returns x rounded to the given number of decimals; the CSV writer +// prints it with its shortest representation. +func round(x float64, decimals int) float64 { + p := math.Pow10(decimals) + return math.Round(x*p) / p +} + +// pairRecord converts a pair to a CSV record. Unavailable values (NaN) are +// left out, which the writer prints as the NA value. +func pairRecord(index int, p obiprimer.PairStats) obiitercsv.CSVRecord { + r := obiitercsv.CSVRecord{ + "index": index, "primer1": p.Primer1, "primer2": p.Primer2, + "gc1": p.GC1, "gc2": p.GC2, "good1": p.Good1, "good2": p.Good2, + "examples": p.Examples, "counterexamples": p.CounterExamples, + "yule": round(float64(p.Yule), 4), + "example_taxa": p.ExampleTaxa, "counterexample_taxa": p.CounterTaxa, + "bc": round(float64(p.Bc), 4), "identified_taxa": p.Identified, "bs": round(float64(p.Bs), 4), + "min_length": p.MinLen, "max_length": p.MaxLen, "mean_length": round(p.MeanLen, 2), + } + for key, v := range map[string]float64{"tm1": p.Tm1, "tm1_min": p.MinTm1, "tm2": p.Tm2, "tm2_min": p.MinTm2} { + if !math.IsNaN(v) { + r[key] = round(v, 1) + } + } + return r +} + +// PairsToCSVIterator streams the pairs, in order, as CSV records. +func PairsToCSVIterator(pairs []obiprimer.PairStats) *obiitercsv.ICSVRecord { + iterator := obiitercsv.NewICSVRecord() + iterator.Add(1) + for _, column := range csvColumns { + iterator.AppendField(column) + } + + go func() { + defer iterator.Done() + batchSize := obidefault.BatchSize() + order := 0 + for start := 0; start < len(pairs) || order == 0; start += batchSize { + end := min(start+batchSize, len(pairs)) + data := make([]obiitercsv.CSVRecord, 0, end-start) + for i := start; i < end; i++ { + data = append(data, pairRecord(i+1, pairs[i])) + } + iterator.Push(obiitercsv.MakeCSVRecordBatch("obiprimers", order, data)) + order++ + } + iterator.Close() + }() + return iterator +} From 57c2517b24eaf9c16820c648ca949bccd5993898 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Tue, 3 Nov 2026 15:24:52 +1100 Subject: [PATCH 18/24] Implement advanced primer search filtering and reference handling This update introduces new configuration options for pair selection, including multi-match filtering, circularity settings, and reference site validation. It also enhances the search functionality to correctly identify and track repeated sequences and reference locations, improving the accuracy of primer pair identification. --- OBIPRIMERS_NOTES.md | 61 +- cmd/obitools/obiprimers/main.go | 2 +- pkg/obiprimer/corpus.go | 15 + pkg/obiprimer/multimatch.go | 41 + pkg/obiprimer/pairs.go | 2 + pkg/obiprimer/pairs_eval.go | 35 +- pkg/obiprimer/reference.go | 44 + pkg/obiprimer/search.go | 19 + pkg/obiprimer/search_real_test.go | 175 +++ pkg/obiprimer/search_test.go | 199 +++ .../ecoprimers_Cyprinidae_U.pairs.tsv | 1366 +++++++++++++++++ pkg/obitools/obiprimers/obiprimers.go | 4 + pkg/obitools/obiprimers/obiprimers_test.go | 51 +- pkg/obitools/obiprimers/options.go | 18 + pkg/obitools/obiprimers/output.go | 18 +- 15 files changed, 2041 insertions(+), 9 deletions(-) create mode 100644 pkg/obiprimer/multimatch.go create mode 100644 pkg/obiprimer/reference.go create mode 100644 pkg/obiprimer/search_real_test.go create mode 100644 pkg/obiprimer/search_test.go create mode 100644 pkg/obiprimer/testdata/ecoprimers_Cyprinidae_U.pairs.tsv diff --git a/OBIPRIMERS_NOTES.md b/OBIPRIMERS_NOTES.md index 20b1712..551b60d 100644 --- a/OBIPRIMERS_NOTES.md +++ b/OBIPRIMERS_NOTES.md @@ -471,5 +471,64 @@ taxonomie après `AssignTaxa`, régler le GC de la commande. Avertissements `Taxid ... has to be updated` : viennent du mécanisme existant d'obitools (taxids fusionnés dans le FASTA) ; `--update-taxid` les corrige à la lecture. -Reste à faire : mode B, `-U`, `-R`, tests shell `obitests/obitools/obiprimers` (données dans le dépôt), +Reste à faire : mode B, tests shell `obitests/obitools/obiprimers` (données dans le dépôt), documentation, réduction de la mémoire, comparaison systématique `--rank` autre que species. +(`-U` et `-R` : faits, section 15.) + +## 15. Options `-U` (no multi-match) et `-R` (référence) (2026-11-03) + +Code : `pkg/obiprimer/multimatch.go` (`DropRepeatedWords`, `DropMultiMatch`), `reference.go` +(`referenceSites`, `span`), `Corpus.IndexOfExample`, champs `SearchParams.NoMultiMatch/Reference`, +`PairStats.RefLocation/RefAmplicon`, options CLI `-U/--no-multi-match` et `-R/--reference`, colonnes CSV +`reference_location` et `reference_amplicon` (présentes seulement avec `-R`, `NA` pour une paire qui +n'amplifie pas la référence). Tests : `search_test.go` (synthétiques), `search_real_test.go` (opt-in). + +### -U + +Sémantique lue dans le C (`aproxpattern.c` et `ahocorasick.c`, qui donnent la même règle) : un mot est +écarté s'il a >= 2 occurrences approchées, **deux brins confondus**, dans **une** séquence quelconque du +corpus (exemple ou contre-exemple). Le drapeau « multi-mot » du comptage strict (`Repeated`, mot présent +plusieurs fois dans un exemple) n'en est qu'un cas particulier ; ecoPrimers l'applique d'abord pour +alléger la recherche, nous aussi (`DropRepeatedWords` puis `DropMultiMatch`). + +**Contrôle contre ecoPrimers `-U`** (Cyprinidae, 98,7 s, 1,18 Go) : **1 366 paires, identiques sur tous les +champs** (primers, good, séquences, taxons, Bc, Bs, Yule, longueurs, GC, Tm). Notre journal : +34 mots stricts écartés comme répétés, 469 écartés pour multi-match, 703 amorces conservées. +`testdata/ecoprimers_Cyprinidae_U.pairs.tsv` garde les valeurs de référence (test opt-in +`TestNoMultiMatchAgainstEcoPrimers`). + +Détail sans conséquence sur les résultats : avec `-e 0`, ecoPrimers (chemin Aho-Corasick) ne filtre pas les +amorces par le quorum de sensibilité et définit « good » par les seuls contre-exemples ; nous appliquons le +même filtre qu'avec `-e > 0`. Une amorce sous le quorum ne peut pas figurer dans une paire retenue +(une paire amplifie au plus autant d'exemples que ses amorces en touchent), donc les tables finales +sont les mêmes ; seuls les décomptes du journal peuvent différer. Non vérifié par une exécution `-e 0`. + +### -R + +Contrairement à `-U`, **le `-R` d'ecoPrimers est faux** : `refsequence` reçoit le numéro de la séquence +(`pairs.c`) puis sert d'indice dans la liste des amplicons de la paire (`ecoprimer.c`, `printapair`). +Mesuré sur Cyprinidae : avec la référence `NC_022183` ecoPrimers coïncide avec nous sur les coordonnées +des 3 448 paires (coïncidence : l'indice tombe juste) ; avec `NC_083213`, **seuls 382 de ses 3 405 +emplacements sont de vrais sites d'amorces dans la séquence, contre 3 405 sur 3 405 pour obiprimers** +(vérifié par un script indépendant et par `TestReferenceSitesAreRealSites`). Nous implémentons donc +l'intention, pas le C : + +- `reference_location` : emplacement GenBank 1-based des deux sites, de gauche à droite, + `join(a..b,c..d)`, entouré de `complement()` quand `primer1` (tel qu'écrit dans le CSV) se fixe sur le brin + moins. Fenêtre à cheval sur la jonction circulaire : coupée en deux (`a..n,1..m`). +- `reference_amplicon` : région entre les sites, amorces exclues, en minuscules, lue depuis `primer1`. +- **Orientation** : ecoPrimers oriente l'amplicon par rapport à l'ordre canonique des mots, nous par rapport + à `primer1` du CSV (plus lisible). Sur NC_022183 : coordonnées identiques pour les 3 448 paires, + amplicon identique pour 2 572 et complémenté-inversé pour 836, tout le reste égal. +- Plusieurs amplicons de la paire sur la référence : on garde le plus à gauche (le C garde le dernier, + par écrasement, mais ses valeurs sont de toute façon indéfinies). +- Référence introuvable ou contre-exemple : **erreur** (le C ignore silencieusement). +- Vérifications : 3 408 paires sur NC_022183 et 3 405 sur NC_083213, sites à <= 3 erreurs, amplicon égal à + la séquence (ou son complément inverse), orientation cohérente (2 184 paires `primer1` sur le brin +, + 1 224 sur le brin -). + +### Autres constats + +- `obiutils` : `TestSetString` est **instable** (échoue ~4 fois sur 6) : `Set.String()` parcourt une map Go + et le test attend `[1 2 3]`. Sans rapport avec obiprimers, non corrigé. +- Un défaut de mon test précédent (`TestCLISearchParams` laissait `_salt = -1`) a été corrigé. diff --git a/cmd/obitools/obiprimers/main.go b/cmd/obitools/obiprimers/main.go index 29f095e..14cb7ba 100644 --- a/cmd/obitools/obiprimers/main.go +++ b/cmd/obitools/obiprimers/main.go @@ -35,7 +35,7 @@ func main() { log.Fatalf("%v", err) } - obicsv.CLICSVWriter(obiprimers.PairsToCSVIterator(pairs), true) + obicsv.CLICSVWriter(obiprimers.PairsToCSVIterator(pairs, obiprimers.CLIReference() != ""), true) obiutils.WaitForLastPipe() } diff --git a/pkg/obiprimer/corpus.go b/pkg/obiprimer/corpus.go index 0796dd2..489bf36 100644 --- a/pkg/obiprimer/corpus.go +++ b/pkg/obiprimer/corpus.go @@ -106,3 +106,18 @@ func LoadCorpus(seqs obiiter.IBioSequence, sel Selection) (*Corpus, error) { } return c, nil } + +// IndexOfExample returns the index of the example sequence with the given +// identifier. It is an error if there is none, or if the sequence is a +// counterexample. +func (c *Corpus) IndexOfExample(id string) (int, error) { + for i, s := range c.Seqs { + if s.Id() == id { + if c.Roles[i] != Example { + return -1, fmt.Errorf("obiprimer: reference sequence %q is a counterexample, it must be an example", id) + } + return i, nil + } + } + return -1, fmt.Errorf("obiprimer: reference sequence %q is not among the selected example sequences", id) +} diff --git a/pkg/obiprimer/multimatch.go b/pkg/obiprimer/multimatch.go new file mode 100644 index 0000000..c06713f --- /dev/null +++ b/pkg/obiprimer/multimatch.go @@ -0,0 +1,41 @@ +package obiprimer + +import "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obikmer" + +// DropRepeatedWords removes the strict words found more than once (on either +// strand) in at least one example sequence. It is the first half of the +// ecoPrimers -U option (no multi-match). +func DropRepeatedWords(words []obikmer.SeqKmerCount) (kept []obikmer.SeqKmerCount, dropped int) { + kept = make([]obikmer.SeqKmerCount, 0, len(words)) + for _, w := range words { + if w.Repeated { + dropped++ + continue + } + kept = append(kept, w) + } + return kept, dropped +} + +// DropMultiMatch removes the words having two or more approximate occurrences, +// both strands counted, in at least one sequence of the corpus, whatever its +// role. It is the second half of the ecoPrimers -U option: the primers that +// can anneal at several places of a sequence are discarded. +func DropMultiMatch(wm []WordMatches) (kept []WordMatches, dropped int) { + kept = make([]WordMatches, 0, len(wm)) + for _, w := range wm { + multi := false + for i := 1; i < len(w.Matches); i++ { + if w.Matches[i].Seq == w.Matches[i-1].Seq { + multi = true + break + } + } + if multi { + dropped++ + continue + } + kept = append(kept, w) + } + return kept, dropped +} diff --git a/pkg/obiprimer/pairs.go b/pkg/obiprimer/pairs.go index 2582bf0..eaa2142 100644 --- a/pkg/obiprimer/pairs.go +++ b/pkg/obiprimer/pairs.go @@ -22,6 +22,8 @@ type PairParams struct { FalsePositive float64 // -x: and less than this fraction of the counterexamples (0.1) MinSpecificity float64 // -T: pairs with a lower Bs are dropped (0.6) Circular bool // -c + Reference int // -R: corpus index of the reference sequence (used when HasReference) + HasReference bool } func (p PairParams) validate() error { diff --git a/pkg/obiprimer/pairs_eval.go b/pkg/obiprimer/pairs_eval.go index cd37864..51c5189 100644 --- a/pkg/obiprimer/pairs_eval.go +++ b/pkg/obiprimer/pairs_eval.go @@ -31,6 +31,14 @@ type PairStats struct { Tm1, Tm2 float64 // melting temperature of the primers against their perfect target MinTm1, MinTm2 float64 // lowest melting temperature over the example amplicons GC1, GC2 int + + // With a reference sequence (-R): where the pair amplifies it. RefLocation + // is the GenBank location of the two primer sites (1-based, left to right), + // wrapped in complement() when Primer1 binds the other strand; RefAmplicon + // is the amplified region, primers excluded, read from Primer1 to Primer2. + // Both are empty when the pair does not amplify the reference. + RefLocation string + RefAmplicon string } // Score is the ranking criterion of ecoPrimers. @@ -52,6 +60,12 @@ type ampRecord struct { tm1, tm2 float32 // NaN when unavailable } +// refHit is an amplicon of a retained pair on the reference sequence. +type refHit struct { + up, down seqMatch + swapped bool +} + // ampHash hashes the amplicon between the two primers read from the side of // the first primer of the pair (reverse complemented when the upstream match is // the second primer), so that identical amplicons give the same value @@ -132,7 +146,7 @@ func FindPairs(c *Corpus, taxa *Taxa, wm []WordMatches, p PairParams, model *obi } // Phase 2: the amplicons of the retained pairs only. - records := ps.collectAmplicons(c, p, index, model) + records, refs := ps.collectAmplicons(c, p, index, model) byPair := make([][]ampRecord, len(kept)) for _, r := range records { byPair[r.pair] = append(byPair[r.pair], r) @@ -142,6 +156,12 @@ func FindPairs(c *Corpus, taxa *Taxa, wm []WordMatches, p PairParams, model *obi for i, k := range kept { st, ok := ps.evaluate(c, taxa, p, k, acc[k], byPair[i], model) if ok { + if h, found := refs[int32(i)]; found { + // evaluate orders the primers by string; the amplicon is read from the first one + s1 := orientedWord(ps.primers[k.p1].Word, p.PrimerLength, k.a1) + s2 := orientedWord(ps.primers[k.p2].Word, p.PrimerLength, k.a2) + st.RefLocation, st.RefAmplicon = referenceSites(c.Seqs[p.Reference].Sequence(), h, p, s2 < s1) + } out = append(out, st) } } @@ -162,7 +182,7 @@ func boolCmp(a, b bool) int { } // collectAmplicons gathers, in parallel, one record per amplicon of a retained pair. -func (ps *primerSet) collectAmplicons(c *Corpus, p PairParams, index map[pairKey]int32, model *obithermo.Model) []ampRecord { +func (ps *primerSet) collectAmplicons(c *Corpus, p PairParams, index map[pairKey]int32, model *obithermo.Model) ([]ampRecord, map[int32]refHit) { lp := p.PrimerLength workers := obidefault.ParallelWorkers() parts := make([][]ampRecord, workers) @@ -175,6 +195,10 @@ func (ps *primerSet) collectAmplicons(c *Corpus, p PairParams, index map[pairKey } nan := float32(math.NaN()) + // The reference sequence is handled by a single worker, which is the only + // writer of refs; the first (leftmost) amplicon of each pair is kept. + refs := map[int32]refHit{} + jobs := make(chan int) var wg sync.WaitGroup for w := range workers { @@ -191,6 +215,11 @@ func (ps *primerSet) collectAmplicons(c *Corpus, p PairParams, index map[pairKey if !ok { return } + if p.HasReference && s == p.Reference { + if old, seen := refs[pi]; !seen || o.up.pos < old.up.pos { + refs[pi] = refHit{o.up, o.down, o.swapped} + } + } rec := ampRecord{pair: pi, seq: int32(s), tm1: nan, tm2: nan} if isExample { rec.hash = ampHash(raw, int(o.up.pos)+lp, int(o.down.pos), o.swapped) @@ -233,7 +262,7 @@ func (ps *primerSet) collectAmplicons(c *Corpus, p PairParams, index map[pairKey for _, part := range parts { all = append(all, part...) } - return all + return all, refs } func b2i(b bool) int { diff --git a/pkg/obiprimer/reference.go b/pkg/obiprimer/reference.go new file mode 100644 index 0000000..a2f6b31 --- /dev/null +++ b/pkg/obiprimer/reference.go @@ -0,0 +1,44 @@ +package obiprimer + +import ( + "fmt" + "strings" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq" +) + +// span writes the 1-based GenBank range of the lp bases starting at the 0-based +// position pos. On a circular sequence a range running over the junction is +// split in two. +func span(pos, lp, n int, circular bool) string { + end := pos + lp + if end > n && circular { + return fmt.Sprintf("%d..%d,1..%d", pos+1, n, end-n) + } + return fmt.Sprintf("%d..%d", pos+1, end) +} + +// referenceSites describes an amplicon of a pair on the reference sequence raw. +// +// The location lists the two primer sites left to right as join(a..b,c..d), +// inside complement() when the first primer of the pair (Primer1 once the pair +// is in string order) binds the reverse strand. The amplicon is the region +// between the sites, read 5'->3' from the first primer, in lower case. +// +// swappedByOrder says whether putting the pair in string order exchanged the +// two primers of the key. +func referenceSites(raw []byte, h refHit, p PairParams, swappedByOrder bool) (location, amplicon string) { + lp, n := p.PrimerLength, len(raw) + + // The key slot 1 is the upstream primer unless the match order was swapped; + // the exchange due to the string order flips it again. + firstIsUpstream := h.swapped == swappedByOrder + + location = "join(" + span(int(h.up.pos), lp, n, p.Circular) + "," + span(int(h.down.pos), lp, n, p.Circular) + ")" + region := []byte(strings.ToLower(string(raw[int(h.up.pos)+lp : int(h.down.pos)]))) + if firstIsUpstream { + return location, string(region) + } + rc := obiseq.NewBioSequence("", region, "").ReverseComplement(true) + return "complement(" + location + ")", string(rc.Sequence()) +} diff --git a/pkg/obiprimer/search.go b/pkg/obiprimer/search.go index 3a48567..61d14fc 100644 --- a/pkg/obiprimer/search.go +++ b/pkg/obiprimer/search.go @@ -19,6 +19,8 @@ type SearchParams struct { FalsePositive float64 // -x: fraction of the counterexamples it may match (0.1) MinSpecificity float64 // -T: pairs with a lower Bs are dropped (0.6) Circular bool // -c: sequences are circular + NoMultiMatch bool // -U: discard the primers matching several times in a sequence + Reference string // -R: identifier of an example sequence on which the amplicons are located ("" = none) Partitions int // partitions of the k-mer counter (default 256) TmpDir string @@ -41,6 +43,8 @@ type SearchInfo struct { StrictWords int // words found without error in at least StrictQuorum of the examples MinStrict int // that quorum, in sequences Matches int // approximate occurrences of those words + Repeated int // strict words dropped because repeated in an example (-U) + MultiMatch int // words dropped because matching several times in a sequence (-U) Pair PairInfo } @@ -63,6 +67,16 @@ func Search(c *Corpus, taxa *Taxa, p SearchParams) ([]PairStats, *SearchInfo, er return nil, nil, err } info := &SearchInfo{Counts: sw.Counts, StrictWords: len(sw.Words), MinStrict: sw.MinSequences} + if p.NoMultiMatch { + sw.Words, info.Repeated = DropRepeatedWords(sw.Words) + } + + refIndex := -1 + if p.Reference != "" { + if refIndex, err = c.IndexOfExample(p.Reference); err != nil { + return nil, info, err + } + } words := make([]uint64, len(sw.Words)) for i, w := range sw.Words { @@ -72,6 +86,9 @@ func Search(c *Corpus, taxa *Taxa, p SearchParams) ([]PairStats, *SearchInfo, er if err != nil { return nil, info, err } + if p.NoMultiMatch { + wm, info.MultiMatch = DropMultiMatch(wm) + } for _, w := range wm { info.Matches += len(w.Matches) } @@ -85,6 +102,8 @@ func Search(c *Corpus, taxa *Taxa, p SearchParams) ([]PairStats, *SearchInfo, er FalsePositive: p.FalsePositive, MinSpecificity: p.MinSpecificity, Circular: p.Circular, + Reference: refIndex, + HasReference: refIndex >= 0, }, p.Model) info.Pair = pinfo return pairs, info, err diff --git a/pkg/obiprimer/search_real_test.go b/pkg/obiprimer/search_real_test.go new file mode 100644 index 0000000..babb400 --- /dev/null +++ b/pkg/obiprimer/search_real_test.go @@ -0,0 +1,175 @@ +package obiprimer + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + "testing" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiformats" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obitax" +) + +// loadTutorial reads the Cyprinidae examples of the tutorial data (the opt-in +// tests need OBIPRIMERS_TUTORIAL, see TestStrictWordsAgainstEcoPrimers). +func loadTutorial(t *testing.T) (*Corpus, *Taxa, *obitax.Taxonomy) { + t.Helper() + dir := os.Getenv("OBIPRIMERS_TUTORIAL") + if dir == "" { + t.Skip("OBIPRIMERS_TUTORIAL not set") + } + taxonomy, err := obiformats.LoadTaxonomy(filepath.Join(dir, "ncbitaxo_20250211.tgz"), false, false) + if err != nil { + t.Fatal(err) + } + restrict, err := BelongingTo(taxonomy, []string{"7953"}) + if err != nil { + t.Fatal(err) + } + seqs, err := obiformats.ReadSequencesFromFile(filepath.Join(dir, "mito.vert.fasta.gz")) + if err != nil { + t.Fatal(err) + } + corpus, err := LoadCorpus(seqs, Selection{Restrict: restrict}) + if err != nil { + t.Fatal(err) + } + taxa, err := corpus.AssignTaxa(taxonomy, "species") + if err != nil { + t.Fatal(err) + } + return corpus, taxa, taxonomy +} + +func tutorialParams(t *testing.T) SearchParams { + p := DefaultSearchParams() + p.MinLength, p.MaxLength, p.ThreePrime, p.Circular = 30, 150, 2, true + p.TmpDir = t.TempDir() + return p +} + +// ecoPrimers -U on the Cyprinidae tutorial case (-e 3 -3 2 -l 30 -L 150 -c -r 7953 -U) +// keeps 1366 pairs. testdata/ecoprimers_Cyprinidae_U.pairs.tsv holds, for each, +// the primers, the amplified example and counterexample sequences, the amplified +// example and counterexample taxa, and the identified taxa. +func TestNoMultiMatchAgainstEcoPrimers(t *testing.T) { + corpus, taxa, _ := loadTutorial(t) + p := tutorialParams(t) + p.NoMultiMatch = true + pairs, info, err := Search(corpus, taxa, p) + if err != nil { + t.Fatal(err) + } + t.Logf("%+v", *info) + + f, err := os.Open(filepath.Join("testdata", "ecoprimers_Cyprinidae_U.pairs.tsv")) + if err != nil { + t.Fatal(err) + } + defer f.Close() + want := map[string][5]int{} + sc := bufio.NewScanner(f) + for sc.Scan() { + var p1, p2 string + var v [5]int + if _, err := fmt.Sscanf(sc.Text(), "%s\t%s\t%d\t%d\t%d\t%d\t%d", &p1, &p2, &v[0], &v[1], &v[2], &v[3], &v[4]); err != nil { + t.Fatal(err) + } + want[p1+"/"+p2] = v + } + + if len(pairs) != len(want) { + t.Errorf("%d pairs, ecoPrimers -U reports %d", len(pairs), len(want)) + } + bad := 0 + for _, g := range pairs { + v, ok := want[g.Primer1+"/"+g.Primer2] + if !ok { + t.Errorf("pair %s/%s is not in the ecoPrimers -U result", g.Primer1, g.Primer2) + continue + } + got := [5]int{g.Examples, g.CounterExamples, g.ExampleTaxa, g.CounterTaxa, g.Identified} + if got != v { + if bad++; bad <= 3 { + t.Errorf("pair %s/%s: got %v, ecoPrimers %v", g.Primer1, g.Primer2, got, v) + } + } + } +} + +var locRE = regexp.MustCompile(`(\d+)\.\.(\d+)`) + +// ecoPrimers -R is only right for the references that happen to come first: it +// indexes the amplicons of a pair by sequence number. On NC_083213 only 382 of +// its 3405 locations are real primer sites, against all 3405 for obiprimers. +// Here the locations are checked against the sequence itself, so no +// ecoPrimers output is needed: each location must give two real primer sites +// (at most 3 substitutions) and the amplicon must be the region between them, +// read from the first primer. +func TestReferenceSitesAreRealSites(t *testing.T) { + corpus, taxa, _ := loadTutorial(t) + + for _, ref := range []string{"NC_022183", "NC_083213"} { + t.Run(ref, func(t *testing.T) { + p := tutorialParams(t) + p.Reference = ref + pairs, _, err := Search(corpus, taxa, p) + if err != nil { + t.Fatal(err) + } + idx, err := corpus.IndexOfExample(ref) + if err != nil { + t.Fatal(err) + } + seq := strings.ToLower(string(corpus.Seqs[idx].Sequence())) + + hamming := func(a, b string) int { + d := 0 + for i := range a { + if a[i] != b[i] { + d++ + } + } + return d + } + located, bad := 0, 0 + for _, pr := range pairs { + if pr.RefLocation == "" { + continue + } + located++ + m := locRE.FindAllStringSubmatch(pr.RefLocation, -1) + if len(m) != 2 { + t.Fatalf("location %q", pr.RefLocation) + } + var a1, b1, a2, b2 int + fmt.Sscanf(m[0][0], "%d..%d", &a1, &b1) + fmt.Sscanf(m[1][0], "%d..%d", &a2, &b2) + left, right := seq[a1-1:b1], seq[a2-1:b2] + between := seq[b1 : a2-1] + + p1, p2 := strings.ToLower(pr.Primer1), strings.ToLower(pr.Primer2) + var ok bool + if strings.HasPrefix(pr.RefLocation, "complement(") { + ok = hamming(strings.ToLower(revcomp(strings.ToUpper(right))), p1) <= 3 && hamming(left, p2) <= 3 && + pr.RefAmplicon == strings.ToLower(revcomp(strings.ToUpper(between))) + } else { + ok = hamming(left, p1) <= 3 && hamming(strings.ToLower(revcomp(strings.ToUpper(right))), p2) <= 3 && + pr.RefAmplicon == between + } + if !ok { + if bad++; bad <= 3 { + t.Errorf("pair %s/%s: %s is not a real location", pr.Primer1, pr.Primer2, pr.RefLocation) + } + } + } + t.Logf("%d of %d pairs amplify %s; %d wrong locations", located, len(pairs), ref, bad) + if located == 0 { + t.Error("no pair amplifies the reference") + } + }) + } +} diff --git a/pkg/obiprimer/search_test.go b/pkg/obiprimer/search_test.go new file mode 100644 index 0000000..9bcc92e --- /dev/null +++ b/pkg/obiprimer/search_test.go @@ -0,0 +1,199 @@ +package obiprimer + +import ( + "fmt" + "math/rand" + "strings" + "testing" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obikmer" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq" +) + +// synthetic builds nseq example sequences sharing the block F + insert + rc(R), +// each with its own flanks and its own substitution inside the insert (so that +// every sequence has a distinct amplicon, hence Bs = 1, with one taxon each). +// extraF, if not empty, is appended to the flank of sequence extraIn: that +// sequence then holds the forward site twice. +type synthetic struct { + corpus *Corpus + taxa *Taxa + f, r, i string // forward primer, reverse primer (5'->3'), insert of sequence 0 + pos int // 0-based start of the block in every sequence +} + +func newSynthetic(nseq int, repeatForwardIn int, seed int64) synthetic { + rng := rand.New(rand.NewSource(seed)) + f := randDNA(rng, 18) + r := randDNA(rng, 18) + ins := randDNA(rng, 40) + const pos = 60 + + c := &Corpus{} + taxa := &Taxa{Rank: "species", ExampleTaxa: nseq} + for k := 0; k < nseq; k++ { + in := []byte(ins) + in[3+k] = "ACGT"[(strings.IndexByte("ACGT", in[3+k])+1)%4] // one private substitution + s := randDNA(rng, pos) + f + string(in) + revcomp(r) + randDNA(rng, 60) + if k == repeatForwardIn { + s += randDNA(rng, 20) + f // the forward site a second time + } + c.Seqs = append(c.Seqs, obiseq.NewBioSequence(fmt.Sprintf("s%d", k), []byte(strings.ToLower(s)), "")) + c.Roles = append(c.Roles, Example) + taxa.Of = append(taxa.Of, int32(k)) + } + return synthetic{c, taxa, f, r, ins, pos} +} + +func (s synthetic) params() SearchParams { + p := DefaultSearchParams() + p.MaxErrors = 1 + p.MinLength, p.MaxLength = 20, 100 + p.Partitions = 8 + p.TmpDir = "" + return p +} + +// find returns the pair made of the primers f and r, whatever their order. +func find(pairs []PairStats, f, r string) *PairStats { + for i := range pairs { + if (pairs[i].Primer1 == f && pairs[i].Primer2 == r) || (pairs[i].Primer1 == r && pairs[i].Primer2 == f) { + return &pairs[i] + } + } + return nil +} + +func TestSearchFindsThePlantedPair(t *testing.T) { + s := newSynthetic(8, -1, 11) + pairs, info, err := Search(s.corpus, s.taxa, s.params()) + if err != nil { + t.Fatal(err) + } + p := find(pairs, s.f, s.r) + if p == nil { + t.Fatalf("the planted pair was not found among %d pairs (%+v)", len(pairs), info) + } + if p.Examples != 8 || p.MinLen != 40 || p.MaxLen != 40 || p.Bs != 1 || p.Bc != 1 { + t.Errorf("unexpected indices %+v", *p) + } + if p.RefLocation != "" { + t.Error("no reference was asked: no location expected") + } +} + +func TestReferenceLocation(t *testing.T) { + forwardFirst, reverseFirst := 0, 0 + for seed := int64(1); seed <= 24; seed++ { + s := newSynthetic(8, -1, seed) + p := s.params() + p.Reference = "s3" + pairs, _, err := Search(s.corpus, s.taxa, p) + if err != nil { + t.Fatal(err) + } + pair := find(pairs, s.f, s.r) + if pair == nil { + t.Fatalf("seed %d: planted pair not found", seed) + } + + // 1-based sites: forward primer at 61..78, then 40 bases of insert, then the reverse site + wantLoc := "join(61..78,119..136)" + insert := strings.ToLower(string(s.corpus.Seqs[3].Sequence()[s.pos+18 : s.pos+58])) + if pair.Primer1 == s.f { // the amplicon is read from the first primer + forwardFirst++ + if pair.RefLocation != wantLoc || pair.RefAmplicon != insert { + t.Errorf("seed %d, forward first: got %s %s", seed, pair.RefLocation, pair.RefAmplicon) + } + } else { + reverseFirst++ + rc := strings.ToLower(revcomp(strings.ToUpper(insert))) + if pair.RefLocation != "complement("+wantLoc+")" || pair.RefAmplicon != rc { + t.Errorf("seed %d, reverse first: got %s %s", seed, pair.RefLocation, pair.RefAmplicon) + } + } + if len(pair.RefAmplicon) != pair.MinLen { + t.Errorf("seed %d: amplicon length %d, pair length %d", seed, len(pair.RefAmplicon), pair.MinLen) + } + } + if forwardFirst == 0 || reverseFirst == 0 { + t.Errorf("both orientations must be exercised: %d forward first, %d reverse first", forwardFirst, reverseFirst) + } +} + +func TestReferenceErrors(t *testing.T) { + s := newSynthetic(8, -1, 11) + p := s.params() + p.Reference = "nope" + if _, _, err := Search(s.corpus, s.taxa, p); err == nil { + t.Error("an unknown reference must be refused") + } + s.corpus.Roles[2] = CounterExample + p.Reference = "s2" + if _, _, err := Search(s.corpus, s.taxa, p); err == nil { + t.Error("a counterexample cannot be the reference") + } +} + +func TestReferenceOnACircularJunction(t *testing.T) { + // span writes a range running over the end of a circular sequence in two parts + if got := span(95, 10, 100, true); got != "96..100,1..5" { + t.Errorf("circular span %q", got) + } + if got := span(95, 10, 100, false); got != "96..105" { + t.Errorf("linear span %q", got) + } + if got := span(10, 18, 100, true); got != "11..28" { + t.Errorf("inner span %q", got) + } +} + +func TestNoMultiMatchDropsRepeatedPrimers(t *testing.T) { + // the forward primer is found twice in sequence 2 + s := newSynthetic(8, 2, 11) + + with, infoWith, err := Search(s.corpus, s.taxa, s.params()) + if err != nil { + t.Fatal(err) + } + if find(with, s.f, s.r) == nil { + t.Fatalf("without -U the pair must be found (%+v)", infoWith) + } + + p := s.params() + p.NoMultiMatch = true + without, info, err := Search(s.corpus, s.taxa, p) + if err != nil { + t.Fatal(err) + } + if find(without, s.f, s.r) != nil { + t.Error("with -U the repeated forward primer must be discarded, and its pair with it") + } + if info.Repeated == 0 { + t.Errorf("the repeated word was not counted: %+v", info) + } + if len(without) >= len(with) { + t.Errorf("-U must not add pairs: %d with, %d without", len(with), len(without)) + } +} + +func TestDropMultiMatch(t *testing.T) { + wm := []WordMatches{ + {Word: 1, Matches: []Match{{0, 5, 0, false}, {1, 9, 0, true}}}, // one per sequence: kept + {Word: 2, Matches: []Match{{0, 5, 0, false}, {0, 50, 1, false}}}, // two in sequence 0 + {Word: 3, Matches: []Match{{2, 5, 0, false}, {2, 5, 0, true}}}, // both strands of one sequence + {Word: 4}, // no match at all: kept + } + kept, dropped := DropMultiMatch(wm) + if dropped != 2 || len(kept) != 2 || kept[0].Word != 1 || kept[1].Word != 4 { + t.Errorf("kept %v, dropped %d", kept, dropped) + } +} + +func TestDropRepeatedWords(t *testing.T) { + words := []obikmer.SeqKmerCount{{Kmer: 1}, {Kmer: 2, Repeated: true}, {Kmer: 3}} + kept, dropped := DropRepeatedWords(words) + if dropped != 1 || len(kept) != 2 || kept[0].Kmer != 1 || kept[1].Kmer != 3 { + t.Errorf("kept %v, dropped %d", kept, dropped) + } +} diff --git a/pkg/obiprimer/testdata/ecoprimers_Cyprinidae_U.pairs.tsv b/pkg/obiprimer/testdata/ecoprimers_Cyprinidae_U.pairs.tsv new file mode 100644 index 0000000..678b908 --- /dev/null +++ b/pkg/obiprimer/testdata/ecoprimers_Cyprinidae_U.pairs.tsv @@ -0,0 +1,1366 @@ +ACATAACCACTCTGTCAC TTCTATAACGGGTGATGC 317 0 309 0 235 +CACATAACCACTCTGTCA TTCTATAACGGGTGATGC 317 0 309 0 235 +AAAAGAGTGGGAAGAGCT TGTTAAAGGGGCTGTACC 321 0 312 0 234 +AAAGAGTGGGAAGAGCTC TGTTAAAGGGGCTGTACC 320 0 311 0 233 +ACCTACCGAACCTGGTGA TGTTAAAGGGGCTGTACC 323 0 314 0 233 +CTACCGAACCTGGTGATA TGTTAAAGGGGCTGTACC 325 0 316 0 233 +AACCTGGTGATAGCTGGT TGTTAAAGGGGCTGTACC 325 0 316 0 233 +CCTACCGAACCTGGTGAT TGTTAAAGGGGCTGTACC 325 0 316 0 233 +CAGCCACATAACCACTCT TTCTATAACGGGTGATGC 315 0 307 0 233 +AGCCACATAACCACTCTG TTCTATAACGGGTGATGC 315 0 307 0 233 +GTAGAAGTGACAGACCTA TGTTAAAGGGGCTGTACC 322 0 313 0 232 +AGACCTACCGAACCTGGT TGTTAAAGGGGCTGTACC 322 0 313 0 232 +ACCTACCGAACCTGGTGA CCTTTGTTAAAGGGGCTG 322 0 313 0 232 +AACCTGGTGATAGCTGGT CCTTTGTTAAAGGGGCTG 324 0 315 0 232 +GAACCTGGTGATAGCTGG TGTTAAAGGGGCTGTACC 324 0 315 0 232 +CCGAACCTGGTGATAGCT TGTTAAAGGGGCTGTACC 324 0 315 0 232 +CGAACCTGGTGATAGCTG TGTTAAAGGGGCTGTACC 324 0 315 0 232 +CCTACCGAACCTGGTGAT CCTTTGTTAAAGGGGCTG 324 0 315 0 232 +CCTTTGTTAAAGGGGCTG CTACCGAACCTGGTGATA 324 0 315 0 232 +AACGGCGTAAAGGGTGGT AGCATAGTGGGGTATCTA 323 0 315 0 232 +AACGGCGTAAAGGGTGGT GGGGTATCTAATCCCAGT 323 0 315 0 232 +AACGGCGTAAAGGGTGGT CATAGTGGGGTATCTAAT 323 0 315 0 232 +AACGGCGTAAAGGGTGGT GCATAGTGGGGTATCTAA 323 0 315 0 232 +TGGTTTCAAGCCAGCCAC TTCTATAACGGGTGATGC 297 0 291 0 233 +AGTGACAGACCTACCGAA TGTTAAAGGGGCTGTACC 317 0 308 0 231 +AGACCTACCGAACCTGGT CCTTTGTTAAAGGGGCTG 321 0 312 0 231 +CCTTTGTTAAAGGGGCTG GTAGAAGTGACAGACCTA 321 0 312 0 231 +ACCTGGTGATAGCTGGTT TGTTAAAGGGGCTGTACC 323 0 314 0 231 +CCGAACCTGGTGATAGCT CCTTTGTTAAAGGGGCTG 323 0 314 0 231 +CCTTTGTTAAAGGGGCTG GAACCTGGTGATAGCTGG 323 0 314 0 231 +CCTTTGTTAAAGGGGCTG CGAACCTGGTGATAGCTG 323 0 314 0 231 +AACGGCGTAAAGGGTGGT GAGCATAGTGGGGTATCT 322 0 314 0 231 +AACGGCGTAAAGGGTGGT ATAGTGGGGTATCTAATC 322 0 314 0 231 +AACGGCGTAAAGGGTGGT TGGGGTATCTAATCCCAG 322 0 314 0 231 +AGTGACAGACCTACCGAA CCTTTGTTAAAGGGGCTG 316 0 307 0 230 +AAAGAGTGGGAAGAGCTC CCTTTGTTAAAGGGGCTG 317 0 308 0 230 +AAAAGAGTGGGAAGAGCT CCTTTGTTAAAGGGGCTG 317 0 308 0 230 +GACCTACCGAACCTGGTG TGTTAAAGGGGCTGTACC 320 0 311 0 230 +ACAGACCTACCGAACCTG TGTTAAAGGGGCTGTACC 320 0 311 0 230 +GAAGTGACAGACCTACCG TGTTAAAGGGGCTGTACC 320 0 311 0 230 +ACCTGGTGATAGCTGGTT CCTTTGTTAAAGGGGCTG 322 0 313 0 230 +CCTTTGTTAAAGGGGCTG GAAGTGACAGACCTACCG 319 0 310 0 229 +CCTTTGTTAAAGGGGCTG GACCTACCGAACCTGGTG 319 0 310 0 229 +GTGACAGACCTACCGAAC TGTTAAAGGGGCTGTACC 314 0 305 0 228 +AAAAGAGTGGGAAGAGCT AAGGGGCTGTACCCCCTT 315 0 306 0 228 +AAAAGAGTGGGAAGAGCT GTTAAAGGGGCTGTACCC 315 0 306 0 228 +GGTAGAAGTGACAGACCT TGTTAAAGGGGCTGTACC 318 0 309 0 228 +ACAGACCTACCGAACCTG CCTTTGTTAAAGGGGCTG 318 0 309 0 228 +AAAGAGTGGGAAGAGCTC AAGGGGCTGTACCCCCTT 314 0 305 0 227 +AAAGAGTGGGAAGAGCTC GTTAAAGGGGCTGTACCC 314 0 305 0 227 +CCTTTGTTAAAGGGGCTG GGTAGAAGTGACAGACCT 317 0 308 0 227 +ACCTACCGAACCTGGTGA GTTAAAGGGGCTGTACCC 317 0 308 0 227 +CTACCGAACCTGGTGATA GTTAAAGGGGCTGTACCC 319 0 310 0 227 +AACCTGGTGATAGCTGGT GTTAAAGGGGCTGTACCC 319 0 310 0 227 +CCTACCGAACCTGGTGAT GTTAAAGGGGCTGTACCC 319 0 310 0 227 +CGGGTAGAAGTGACAGAC TGTTAAAGGGGCTGTACC 314 0 306 0 227 +AGTGACAGACCTACCGAA GTTAAAGGGGCTGTACCC 312 0 303 0 226 +CCTTTGTTAAAGGGGCTG GTGACAGACCTACCGAAC 312 0 303 0 226 +AGACCTACCGAACCTGGT GTTAAAGGGGCTGTACCC 316 0 307 0 226 +AAGGGGCTGTACCCCCTT ACCTACCGAACCTGGTGA 316 0 307 0 226 +GTAGAAGTGACAGACCTA GTTAAAGGGGCTGTACCC 316 0 307 0 226 +AAGGGGCTGTACCCCCTT CCTACCGAACCTGGTGAT 318 0 309 0 226 +AACCTGGTGATAGCTGGT AAGGGGCTGTACCCCCTT 318 0 309 0 226 +GAACCTGGTGATAGCTGG GTTAAAGGGGCTGTACCC 318 0 309 0 226 +CGAACCTGGTGATAGCTG GTTAAAGGGGCTGTACCC 318 0 309 0 226 +CCGAACCTGGTGATAGCT GTTAAAGGGGCTGTACCC 318 0 309 0 226 +AAGGGGCTGTACCCCCTT CTACCGAACCTGGTGATA 318 0 309 0 226 +CCTTTGTTAAAGGGGCTG CGGGTAGAAGTGACAGAC 313 0 305 0 226 +AAGGGGCTGTACCCCCTT AGTGACAGACCTACCGAA 311 0 302 0 225 +AAGGGGCTGTACCCCCTT TCTCTGTGGCAAAAGAGT 312 0 303 0 225 +AAGGGGCTGTACCCCCTT CTCTGTGGCAAAAGAGTG 312 0 303 0 225 +AAGGGGCTGTACCCCCTT GTAGAAGTGACAGACCTA 315 0 306 0 225 +AAGGGGCTGTACCCCCTT AGACCTACCGAACCTGGT 315 0 306 0 225 +AAGGGGCTGTACCCCCTT CCGAACCTGGTGATAGCT 317 0 308 0 225 +ACCTGGTGATAGCTGGTT GTTAAAGGGGCTGTACCC 317 0 308 0 225 +AAGGGGCTGTACCCCCTT GAACCTGGTGATAGCTGG 317 0 308 0 225 +AAGGGGCTGTACCCCCTT CGAACCTGGTGATAGCTG 317 0 308 0 225 +AGAAGTGACAGACCTACC TGTTAAAGGGGCTGTACC 314 0 305 0 224 +GAAGTGACAGACCTACCG GTTAAAGGGGCTGTACCC 314 0 305 0 224 +AAGGGGCTGTACCCCCTT GAAGTGACAGACCTACCG 314 0 305 0 224 +TAGAAGTGACAGACCTAC TGTTAAAGGGGCTGTACC 314 0 305 0 224 +ACAGACCTACCGAACCTG GTTAAAGGGGCTGTACCC 314 0 305 0 224 +GACCTACCGAACCTGGTG GTTAAAGGGGCTGTACCC 314 0 305 0 224 +TGAGCCAAACAGCTAGCT TTCAGCTTTCCCTTGCGG 315 0 306 0 224 +TGAGCCAAACAGCTAGCT TTCTCTTTCAGCTTTCCC 315 0 306 0 224 +CAGCTTTCCCTTGCGGTA TGAGCCAAACAGCTAGCT 315 0 306 0 224 +TCAGCTTTCCCTTGCGGT TGAGCCAAACAGCTAGCT 315 0 306 0 224 +AAGGGGCTGTACCCCCTT ACCTGGTGATAGCTGGTT 316 0 307 0 224 +AAGGGGCTGTACCCCCTT GTCTCTGTGGCAAAAGAG 308 0 299 0 223 +TGTGGCAAAAGAGTGGGA TGTTAAAGGGGCTGTACC 308 0 299 0 223 +AGCCCCGAAGACAGAGGT TTAACAGCTAAGCGCTCA 313 0 304 0 223 +AGCCCCGAAGACAGAGGT TAACAGCTAAGCGCTCAA 313 0 304 0 223 +AGAAGTGACAGACCTACC CCTTTGTTAAAGGGGCTG 313 0 304 0 223 +CCTTTGTTAAAGGGGCTG TAGAAGTGACAGACCTAC 313 0 304 0 223 +AAGGGGCTGTACCCCCTT ACAGACCTACCGAACCTG 313 0 304 0 223 +AAGGGGCTGTACCCCCTT GACCTACCGAACCTGGTG 313 0 304 0 223 +TAACAGCTAAGCGCTCAA TAGCCCCGAAGACAGAGG 311 0 303 0 223 +GTAGCCCCGAAGACAGAG TAACAGCTAAGCGCTCAA 311 0 303 0 223 +TAGCCCCGAAGACAGAGG TTAACAGCTAAGCGCTCA 311 0 303 0 223 +GTAGCCCCGAAGACAGAG TTAACAGCTAAGCGCTCA 311 0 303 0 223 +GAGAAGACATCCATGCAA GCTTTCCCTTGCGGTACT 300 0 291 0 222 +GTGACAGACCTACCGAAC GTTAAAGGGGCTGTACCC 308 0 299 0 222 +AAGGGGCTGTACCCCCTT TCTGTGGCAAAAGAGTGG 309 0 300 0 222 +GCCCCGAAGACAGAGGTT TTAACAGCTAAGCGCTCA 312 0 303 0 222 +GCCCCGAAGACAGAGGTT TAACAGCTAAGCGCTCAA 312 0 303 0 222 +GGTAGAAGTGACAGACCT GTTAAAGGGGCTGTACCC 312 0 303 0 222 +GCTTTCCCTTGCGGTACT TGAGCCAAACAGCTAGCT 313 0 304 0 222 +AGCTTTCCCTTGCGGTAC TGAGCCAAACAGCTAGCT 313 0 304 0 222 +GAATAGAAGTTCAGCCTC TGTTAAAGGGGCTGTACC 316 0 307 0 222 +AAGGGGCTGTACCCCCTT GTGACAGACCTACCGAAC 307 0 298 0 221 +AAGGGGCTGTACCCCCTT GGTAGAAGTGACAGACCT 311 0 302 0 221 +CCTTTGTTAAAGGGGCTG GAATAGAAGTTCAGCCTC 315 0 306 0 221 +CGGGTAGAAGTGACAGAC GTTAAAGGGGCTGTACCC 308 0 300 0 221 +AACGGCGTAAAGGGTGGT GTGGGGTATCTAATCCCA 310 0 302 0 221 +TGAGCCAAACAGCTAGCT TTTCATTTCTCTTTCAGC 309 0 300 0 220 +AGCCCCGAAGACAGAGGT TAGTTAACAGCTAAGCGC 310 0 301 0 220 +ACCTTCGGTTGGGGCGAC GACTCTGGGAGAGGATTG 324 0 315 0 220 +ACCTTCGGTTGGGGCGAC ACTCTGGGAGAGGATTGC 324 0 315 0 220 +AAGGGGCTGTACCCCCTT CGGGTAGAAGTGACAGAC 307 0 299 0 220 +TAGCCCCGAAGACAGAGG TAGTTAACAGCTAAGCGC 308 0 300 0 220 +GTAGCCCCGAAGACAGAG TAGTTAACAGCTAAGCGC 308 0 300 0 220 +ACTCTTAGTTAACAGCTA GCCCCGAAGACAGAGGTT 308 0 299 0 219 +GCCCCGAAGACAGAGGTT TAGTTAACAGCTAAGCGC 309 0 300 0 219 +ACCTTCGGTTGGGGCGAC TCTGGGAGAGGATTGCGC 323 0 314 0 219 +ACCTTCGGTTGGGGCGAC CTCTGGGAGAGGATTGCG 323 0 314 0 219 +ACCTTCGGTTGGGGCGAC GGGAGAGGATTGCGCTGT 324 0 315 0 219 +ACCTTCGGTTGGGGCGAC AGAGGATTGCGCTGTTAT 324 0 315 0 219 +ACCTTCGGTTGGGGCGAC GAGGATTGCGCTGTTATC 324 0 315 0 219 +ACCTTCGGTTGGGGCGAC AGGATTGCGCTGTTATCC 324 0 315 0 219 +ACCTTCGGTTGGGGCGAC GGATTGCGCTGTTATCCC 324 0 315 0 219 +ACCTTCGGTTGGGGCGAC GATTGCGCTGTTATCCCT 324 0 315 0 219 +ACCTTCGGTTGGGGCGAC TAACTTGGTTCGTTGATC 324 0 315 0 219 +ACCTTCGGTTGGGGCGAC CTGGGAGAGGATTGCGCT 324 0 315 0 219 +ACCTTCGGTTGGGGCGAC TGGGAGAGGATTGCGCTG 324 0 315 0 219 +AAGGGGCTGTACCCCCTT CGTCTCTGTGGCAAAAGA 303 0 294 0 218 +TCTGTGGCAAAAGAGTGG TGTTAAAGGGGCTGTACC 303 0 294 0 218 +CTTTTAGGCCCACTAGAA GAATAGAAGTTCAGCCTC 304 0 295 0 218 +AAGGGGCTGTACCCCCTT TGTGGCAAAAGAGTGGGA 305 0 296 0 218 +GTTAAAGGGGCTGTACCC TGTGGCAAAAGAGTGGGA 305 0 296 0 218 +GTTAAAGGGGCTGTACCC TAGAAGTGACAGACCTAC 308 0 299 0 218 +GTGATAGCTGGTTGCCTA TGTTAAAGGGGCTGTACC 308 0 299 0 218 +AGAAGTGACAGACCTACC GTTAAAGGGGCTGTACCC 308 0 299 0 218 +ACTCTGGGAGAGGATTGC TACCTTCGGTTGGGGCGA 322 0 313 0 218 +AACTTGGTTCGTTGATCG ACCTTCGGTTGGGGCGAC 323 0 314 0 218 +AAGGACCACTTTGATAGA CCGTCATGTTCGGGGTAT 324 0 315 0 218 +AAGGACCACTTTGATAGA CGTCATGTTCGGGGTATG 324 0 315 0 218 +AGGACCACTTTGATAGAG CGTCATGTTCGGGGTATG 324 0 315 0 218 +AAGGACCACTTTGATAGA ACCGTCATGTTCGGGGTA 324 0 315 0 218 +ACCGTCATGTTCGGGGTA AGGACCACTTTGATAGAG 324 0 315 0 218 +AGGACCACTTTGATAGAG CCGTCATGTTCGGGGTAT 324 0 315 0 218 +GCCAGCCACATAACCACT TTCTATAACGGGTGATGC 297 0 289 0 218 +AAGGGGCTGTACCCCCTT CCGTCTCTGTGGCAAAAG 302 0 293 0 217 +GCACAAAGGCATGGTCCC TATCACTGCTGAATTCCC 321 0 313 0 218 +TATCACTGCTGAATTCCC TGCACAAAGGCATGGTCC 321 0 313 0 218 +AAGGGGCTGTACCCCCTT TAGAAGTGACAGACCTAC 307 0 298 0 217 +AAGGGGCTGTACCCCCTT AGAAGTGACAGACCTACC 307 0 298 0 217 +CCTTTGTTAAAGGGGCTG GTGATAGCTGGTTGCCTA 307 0 298 0 217 +GCACAAAGGCATGGTCCC TTATCACTGCTGAATTCC 321 0 312 0 217 +CCTTCGGTTGGGGCGACC GACTCTGGGAGAGGATTG 321 0 312 0 217 +TCTGGGAGAGGATTGCGC TTTACCTTCGGTTGGGGC 321 0 312 0 217 +ACTCTGGGAGAGGATTGC CCTTCGGTTGGGGCGACC 321 0 312 0 217 +ACCTTCGGTTGGGGCGAC TGGACTCTGGGAGAGGAT 321 0 312 0 217 +ACCTTCGGTTGGGGCGAC GGACTCTGGGAGAGGATT 321 0 312 0 217 +CTCTGGGAGAGGATTGCG TACCTTCGGTTGGGGCGA 321 0 312 0 217 +TACCTTCGGTTGGGGCGA TCTGGGAGAGGATTGCGC 321 0 312 0 217 +CCTTCGGTTGGGGCGACC GGACTCTGGGAGAGGATT 321 0 312 0 217 +AATTCCCTTGGGGGTGTG GCACAAAGGCATGGTCCC 322 0 313 0 217 +TAACTTGGTTCGTTGATC TACCTTCGGTTGGGGCGA 322 0 313 0 217 +GATTGCGCTGTTATCCCT TACCTTCGGTTGGGGCGA 322 0 313 0 217 +GGATTGCGCTGTTATCCC TACCTTCGGTTGGGGCGA 322 0 313 0 217 +AGGATTGCGCTGTTATCC TACCTTCGGTTGGGGCGA 322 0 313 0 217 +GAGGATTGCGCTGTTATC TACCTTCGGTTGGGGCGA 322 0 313 0 217 +AGAGGATTGCGCTGTTAT TACCTTCGGTTGGGGCGA 322 0 313 0 217 +CTGGGAGAGGATTGCGCT TTTACCTTCGGTTGGGGC 322 0 313 0 217 +TGGGAGAGGATTGCGCTG TTTACCTTCGGTTGGGGC 322 0 313 0 217 +GGGAGAGGATTGCGCTGT TTTACCTTCGGTTGGGGC 322 0 313 0 217 +AGAGGATTGCGCTGTTAT TTTACCTTCGGTTGGGGC 322 0 313 0 217 +GAGGATTGCGCTGTTATC TTTACCTTCGGTTGGGGC 322 0 313 0 217 +AGGATTGCGCTGTTATCC TTTACCTTCGGTTGGGGC 322 0 313 0 217 +GGATTGCGCTGTTATCCC TTTACCTTCGGTTGGGGC 322 0 313 0 217 +GATTGCGCTGTTATCCCT TTTACCTTCGGTTGGGGC 322 0 313 0 217 +GGGAGAGGATTGCGCTGT TACCTTCGGTTGGGGCGA 322 0 313 0 217 +TAACTTGGTTCGTTGATC TTTACCTTCGGTTGGGGC 322 0 313 0 217 +TACCTTCGGTTGGGGCGA TGGGAGAGGATTGCGCTG 322 0 313 0 217 +CTGGGAGAGGATTGCGCT TACCTTCGGTTGGGGCGA 322 0 313 0 217 +AATTCCCTTGGGGGTGTG TGCACAAAGGCATGGTCC 322 0 313 0 217 +CTCTGTGGCAAAAGAGTG GTTAAAGGGGCTGTACCC 301 0 292 0 216 +CTGTGGCAAAAGAGTGGG TGTTAAAGGGGCTGTACC 301 0 292 0 216 +GTTAAAGGGGCTGTACCC TCTGTGGCAAAAGAGTGG 301 0 292 0 216 +AAGGGGCTGTACCCCCTT CTGTGGCAAAAGAGTGGG 303 0 294 0 216 +GCACAAAGGCATGGTCCC GCTGAATTCCCTTGGGGG 321 0 313 0 217 +GAATTCCCTTGGGGGTGT TGCACAAAGGCATGGTCC 321 0 313 0 217 +GCTGAATTCCCTTGGGGG TGCACAAAGGCATGGTCC 321 0 313 0 217 +GAATTCCCTTGGGGGTGT GCACAAAGGCATGGTCCC 321 0 313 0 217 +GAATAGAAGTTCAGCCTC GTTAAAGGGGCTGTACCC 310 0 301 0 216 +CCTTCGGTTGGGGCGACC TCTGGGAGAGGATTGCGC 320 0 311 0 216 +TGCACAAAGGCATGGTCC TTATCACTGCTGAATTCC 320 0 311 0 216 +CCTTCGGTTGGGGCGACC CTCTGGGAGAGGATTGCG 320 0 311 0 216 +CCTTCGGTTGGGGCGACC CTGGGAGAGGATTGCGCT 321 0 312 0 216 +CCTTCGGTTGGGGCGACC TGGGAGAGGATTGCGCTG 321 0 312 0 216 +AACTTGGTTCGTTGATCG TTTACCTTCGGTTGGGGC 321 0 312 0 216 +AACTTGGTTCGTTGATCG TACCTTCGGTTGGGGCGA 321 0 312 0 216 +CCTTCGGTTGGGGCGACC GGGAGAGGATTGCGCTGT 321 0 312 0 216 +AACTTGGTTCGTTGATCG CCTTCGGTTGGGGCGACC 321 0 312 0 216 +CCTTCGGTTGGGGCGACC TAACTTGGTTCGTTGATC 321 0 312 0 216 +CTGCTGAATTCCCTTGGG GCACAAAGGCATGGTCCC 321 0 312 0 216 +CCTTCGGTTGGGGCGACC GATTGCGCTGTTATCCCT 321 0 312 0 216 +CCTTCGGTTGGGGCGACC GGATTGCGCTGTTATCCC 321 0 312 0 216 +CTGCTGAATTCCCTTGGG TGCACAAAGGCATGGTCC 321 0 312 0 216 +AGGATTGCGCTGTTATCC CCTTCGGTTGGGGCGACC 321 0 312 0 216 +CCTTCGGTTGGGGCGACC GAGGATTGCGCTGTTATC 321 0 312 0 216 +AGAGGATTGCGCTGTTAT CCTTCGGTTGGGGCGACC 321 0 312 0 216 +ATGGACTCTGGGAGAGGA CGGTTGGGGCGACCACGG 325 0 316 0 216 +CGGTTGGGGCGACCACGG TATGGACTCTGGGAGAGG 325 0 316 0 216 +ACTCTGGGAGAGGATTGC CGGTTGGGGCGACCACGG 325 0 316 0 216 +CGGTTGGGGCGACCACGG GACTCTGGGAGAGGATTG 325 0 316 0 216 +CGGTTGGGGCGACCACGG GGACTCTGGGAGAGGATT 325 0 316 0 216 +CGGTTGGGGCGACCACGG TGGACTCTGGGAGAGGAT 325 0 316 0 216 +CTGTGGCAAAAGAGTGGG GTTAAAGGGGCTGTACCC 300 0 291 0 215 +CTGGTGATAGCTGGTTGC TGTTAAAGGGGCTGTACC 303 0 294 0 215 +GTGATAGCTGGTTGCCTA GTTAAAGGGGCTGTACCC 305 0 296 0 215 +AAGGGGCTGTACCCCCTT GAATAGAAGTTCAGCCTC 309 0 300 0 215 +CTCTGGGAGAGGATTGCG TTACCTTCGGTTGGGGCG 319 0 310 0 215 +GGACTCTGGGAGAGGATT TACCTTCGGTTGGGGCGA 319 0 310 0 215 +ACTCTGGGAGAGGATTGC TTTACCTTCGGTTGGGGC 319 0 310 0 215 +TCTGGGAGAGGATTGCGC TTACCTTCGGTTGGGGCG 319 0 310 0 215 +GACTCTGGGAGAGGATTG TACCTTCGGTTGGGGCGA 319 0 310 0 215 +GGGAGAGGATTGCGCTGT TTTTACCTTCGGTTGGGG 320 0 311 0 215 +TGGGAGAGGATTGCGCTG TTTTACCTTCGGTTGGGG 320 0 311 0 215 +CTGGGAGAGGATTGCGCT TTTTACCTTCGGTTGGGG 320 0 311 0 215 +TAACTTGGTTCGTTGATC TTACCTTCGGTTGGGGCG 320 0 311 0 215 +GAGGATTGCGCTGTTATC TTTTACCTTCGGTTGGGG 320 0 311 0 215 +TAACTTGGTTCGTTGATC TTTTACCTTCGGTTGGGG 320 0 311 0 215 +AGGATTGCGCTGTTATCC TTTTACCTTCGGTTGGGG 320 0 311 0 215 +CTGGGAGAGGATTGCGCT TTACCTTCGGTTGGGGCG 320 0 311 0 215 +TGGGAGAGGATTGCGCTG TTACCTTCGGTTGGGGCG 320 0 311 0 215 +GGATTGCGCTGTTATCCC TTTTACCTTCGGTTGGGG 320 0 311 0 215 +GGGAGAGGATTGCGCTGT TTACCTTCGGTTGGGGCG 320 0 311 0 215 +AGAGGATTGCGCTGTTAT TTACCTTCGGTTGGGGCG 320 0 311 0 215 +GGATTGCGCTGTTATCCC TTACCTTCGGTTGGGGCG 320 0 311 0 215 +GATTGCGCTGTTATCCCT TTTTACCTTCGGTTGGGG 320 0 311 0 215 +GAGGATTGCGCTGTTATC TTACCTTCGGTTGGGGCG 320 0 311 0 215 +AGGATTGCGCTGTTATCC TTACCTTCGGTTGGGGCG 320 0 311 0 215 +GATTGCGCTGTTATCCCT TTACCTTCGGTTGGGGCG 320 0 311 0 215 +AGAGGATTGCGCTGTTAT TTTTACCTTCGGTTGGGG 320 0 311 0 215 +CGTCATGTTCGGGGTATG GACCACTTTGATAGAGTG 321 0 312 0 215 +CCGTCATGTTCGGGGTAT GACCACTTTGATAGAGTG 321 0 312 0 215 +ACCGTCATGTTCGGGGTA GACCACTTTGATAGAGTG 321 0 312 0 215 +GGTTGGGGCGACCACGGA TATGGACTCTGGGAGAGG 324 0 315 0 215 +CGCCGTCGTCAGCTTACC TTACTACTAAATCCTCCT 324 0 315 0 215 +CGGTTGGGGCGACCACGG TCTGGGAGAGGATTGCGC 324 0 315 0 215 +CGGTTGGGGCGACCACGG CTCTGGGAGAGGATTGCG 324 0 315 0 215 +ACTCTGGGAGAGGATTGC GGTTGGGGCGACCACGGA 324 0 315 0 215 +GACTCTGGGAGAGGATTG GGTTGGGGCGACCACGGA 324 0 315 0 215 +GGACTCTGGGAGAGGATT GGTTGGGGCGACCACGGA 324 0 315 0 215 +GGTTGGGGCGACCACGGA TGGACTCTGGGAGAGGAT 324 0 315 0 215 +ATGGACTCTGGGAGAGGA GGTTGGGGCGACCACGGA 324 0 315 0 215 +CGGTTGGGGCGACCACGG GATTGCGCTGTTATCCCT 325 0 316 0 215 +CGGTTGGGGCGACCACGG TAACTTGGTTCGTTGATC 325 0 316 0 215 +AGGATTGCGCTGTTATCC CGGTTGGGGCGACCACGG 325 0 316 0 215 +CGGTTGGGGCGACCACGG GAGGATTGCGCTGTTATC 325 0 316 0 215 +AGAGGATTGCGCTGTTAT CGGTTGGGGCGACCACGG 325 0 316 0 215 +CGGTTGGGGCGACCACGG GGGAGAGGATTGCGCTGT 325 0 316 0 215 +CGGTTGGGGCGACCACGG TGGGAGAGGATTGCGCTG 325 0 316 0 215 +CGGTTGGGGCGACCACGG CTGGGAGAGGATTGCGCT 325 0 316 0 215 +CGGTTGGGGCGACCACGG GGATTGCGCTGTTATCCC 325 0 316 0 215 +CTCTGTGGCAAAAGAGTG TGTTAAAGGGGCTGTACC 297 0 288 0 214 +CCTTTGTTAAAGGGGCTG CTGGTGATAGCTGGTTGC 302 0 293 0 214 +CCTGGTGATAGCTGGTTG TGTTAAAGGGGCTGTACC 302 0 293 0 214 +AAGGGGCTGTACCCCCTT GTGATAGCTGGTTGCCTA 304 0 295 0 214 +ATTCCCTTGGGGGTGTGG TGCACAAAGGCATGGTCC 320 0 312 0 215 +ATTCCCTTGGGGGTGTGG GCACAAAGGCATGGTCCC 320 0 312 0 215 +AGCCAGCCACATAACCAC TTCTATAACGGGTGATGC 293 0 286 0 215 +CTCTGGGAGAGGATTGCG TTTACCTTCGGTTGGGGC 318 0 309 0 214 +CCTTCGGTTGGGGCGACC TGGACTCTGGGAGAGGAT 318 0 309 0 214 +ATGGACTCTGGGAGAGGA CCTTCGGTTGGGGCGACC 318 0 309 0 214 +ACTGCTGAATTCCCTTGG TGCACAAAGGCATGGTCC 318 0 309 0 214 +AACTTGGTTCGTTGATCG TTTTACCTTCGGTTGGGG 319 0 310 0 214 +CGATATGGACTCTGGGAG GTTGGGGCGACCACGGAG 319 0 310 0 214 +ACCTTCGGTTGGGGCGAC CCTAGGGTAACTTGGTTC 319 0 310 0 214 +AACTTGGTTCGTTGATCG TTACCTTCGGTTGGGGCG 319 0 310 0 214 +CGATATGGACTCTGGGAG CGGTTGGGGCGACCACGG 321 0 312 0 214 +GGTTGGGGCGACCACGGA TCTGGGAGAGGATTGCGC 323 0 314 0 214 +CTCTGGGAGAGGATTGCG GGTTGGGGCGACCACGGA 323 0 314 0 214 +GGTTGGGGCGACCACGGA TGGGAGAGGATTGCGCTG 324 0 315 0 214 +GGTTGGGGCGACCACGGA TAACTTGGTTCGTTGATC 324 0 315 0 214 +AACTTGGTTCGTTGATCG CGGTTGGGGCGACCACGG 324 0 315 0 214 +GGGAGAGGATTGCGCTGT GGTTGGGGCGACCACGGA 324 0 315 0 214 +CTGGGAGAGGATTGCGCT GGTTGGGGCGACCACGGA 324 0 315 0 214 +AGAGGATTGCGCTGTTAT GGTTGGGGCGACCACGGA 324 0 315 0 214 +GAGGATTGCGCTGTTATC GGTTGGGGCGACCACGGA 324 0 315 0 214 +AGGATTGCGCTGTTATCC GGTTGGGGCGACCACGGA 324 0 315 0 214 +GGATTGCGCTGTTATCCC GGTTGGGGCGACCACGGA 324 0 315 0 214 +GATTGCGCTGTTATCCCT GGTTGGGGCGACCACGGA 324 0 315 0 214 +CCTGGTGATAGCTGGTTG CCTTTGTTAAAGGGGCTG 301 0 292 0 213 +CACTGCTGAATTCCCTTG TGCACAAAGGCATGGTCC 317 0 309 0 214 +ACCTTCGGTTGGGGCGAC GTAACTTGGTTCGTTGAT 314 0 305 0 213 +ACTGCTGAATTCCCTTGG GCACAAAGGCATGGTCCC 317 0 308 0 213 +ACTCTGGGAGAGGATTGC TTACCTTCGGTTGGGGCG 317 0 308 0 213 +GACTCTGGGAGAGGATTG TTACCTTCGGTTGGGGCG 317 0 308 0 213 +AAGGACCACTTTGATAGA TCATGTTCGGGGTATGGG 319 0 310 0 213 +AGGACCACTTTGATAGAG TCATGTTCGGGGTATGGG 319 0 310 0 213 +GTTGGGGCGACCACGGAG TGGACTCTGGGAGAGGAT 320 0 311 0 213 +GTTGGGGCGACCACGGAG TATGGACTCTGGGAGAGG 320 0 311 0 213 +ATGGACTCTGGGAGAGGA GTTGGGGCGACCACGGAG 320 0 311 0 213 +CGATATGGACTCTGGGAG GGTTGGGGCGACCACGGA 320 0 311 0 213 +GACTCTGGGAGAGGATTG GTTGGGGCGACCACGGAG 320 0 311 0 213 +ACTCTGGGAGAGGATTGC GTTGGGGCGACCACGGAG 320 0 311 0 213 +GGACTCTGGGAGAGGATT GTTGGGGCGACCACGGAG 320 0 311 0 213 +AAGGACCACTTTGATAGA GGGGTATGGGCCCGAAAG 321 0 312 0 213 +AGGACCACTTTGATAGAG GGGGTATGGGCCCGAAAG 321 0 312 0 213 +AAGGACCACTTTGATAGA GGGTATGGGCCCGAAAGC 322 0 313 0 213 +AGGACCACTTTGATAGAG GGGTATGGGCCCGAAAGC 322 0 313 0 213 +AGGACCACTTTGATAGAG GTATGGGCCCGAAAGCTT 322 0 313 0 213 +AAGGACCACTTTGATAGA GTATGGGCCCGAAAGCTT 322 0 313 0 213 +TCGTCAGCTTACCCTGTG TTACTACTAAATCCTCCT 323 0 314 0 213 +AACTTGGTTCGTTGATCG GGTTGGGGCGACCACGGA 323 0 314 0 213 +CGTCAGCTTACCCTGTGA TTACTACTAAATCCTCCT 323 0 314 0 213 +ATCACTGCTGAATTCCCT GCACAAAGGCATGGTCCC 317 0 310 0 214 +ATCACTGCTGAATTCCCT TGCACAAAGGCATGGTCC 317 0 310 0 214 +GCACAAAGGCATGGTCCC TCACTGCTGAATTCCCTT 317 0 310 0 214 +TCACTGCTGAATTCCCTT TGCACAAAGGCATGGTCC 317 0 310 0 214 +CACTGCTGAATTCCCTTG GCACAAAGGCATGGTCCC 316 0 308 0 213 +TGCACAAAGGCATGGTCC TTCCCTTGGGGGTGTGGC 318 0 310 0 213 +GCACAAAGGCATGGTCCC TTCCCTTGGGGGTGTGGC 318 0 310 0 213 +TCCGGGTAGAAGTGACAG TGTTAAAGGGGCTGTACC 294 0 288 0 214 +CCGGGTAGAAGTGACAGA TGTTAAAGGGGCTGTACC 294 0 288 0 214 +AGGCATGGTCCCGACCTT TATCACTGCTGAATTCCC 321 0 313 0 213 +AAAGGCATGGTCCCGACC TATCACTGCTGAATTCCC 321 0 313 0 213 +AAGGCATGGTCCCGACCT TATCACTGCTGAATTCCC 321 0 313 0 213 +GTAACTTGGTTCGTTGAT TACCTTCGGTTGGGGCGA 313 0 304 0 212 +TCTGGGAGAGGATTGCGC TTTTACCTTCGGTTGGGG 316 0 307 0 212 +CTCTGGGAGAGGATTGCG TTTTACCTTCGGTTGGGG 316 0 307 0 212 +CCTAGGGTAACTTGGTTC TACCTTCGGTTGGGGCGA 317 0 308 0 212 +CCTAGGGTAACTTGGTTC TTTACCTTCGGTTGGGGC 317 0 308 0 212 +GTTGGGGCGACCACGGAG TCTGGGAGAGGATTGCGC 319 0 310 0 212 +CTCTGGGAGAGGATTGCG GTTGGGGCGACCACGGAG 319 0 310 0 212 +GGATTGCGCTGTTATCCC GTTGGGGCGACCACGGAG 320 0 311 0 212 +GGGAGAGGATTGCGCTGT GTTGGGGCGACCACGGAG 320 0 311 0 212 +GTTGGGGCGACCACGGAG TGGGAGAGGATTGCGCTG 320 0 311 0 212 +GTTGGGGCGACCACGGAG TAACTTGGTTCGTTGATC 320 0 311 0 212 +CTGGGAGAGGATTGCGCT GTTGGGGCGACCACGGAG 320 0 311 0 212 +AGAGGATTGCGCTGTTAT GTTGGGGCGACCACGGAG 320 0 311 0 212 +GAGGATTGCGCTGTTATC GTTGGGGCGACCACGGAG 320 0 311 0 212 +AGGATTGCGCTGTTATCC GTTGGGGCGACCACGGAG 320 0 311 0 212 +GATTGCGCTGTTATCCCT GTTGGGGCGACCACGGAG 320 0 311 0 212 +AGCCTATATACCGCCGTC TTACTACTAAATCCTCCT 321 0 312 0 212 +AGGCATGGTCCCGACCTT TTATCACTGCTGAATTCC 321 0 312 0 212 +GCCTATATACCGCCGTCG TTACTACTAAATCCTCCT 321 0 312 0 212 +AAAGGCATGGTCCCGACC TTATCACTGCTGAATTCC 321 0 312 0 212 +AAGGCATGGTCCCGACCT TTATCACTGCTGAATTCC 321 0 312 0 212 +AAAGGCATGGTCCCGACC AATTCCCTTGGGGGTGTG 322 0 313 0 212 +AAGGCATGGTCCCGACCT AATTCCCTTGGGGGTGTG 322 0 313 0 212 +AATTCCCTTGGGGGTGTG AGGCATGGTCCCGACCTT 322 0 313 0 212 +ACAAGACGAGAAGACCCT GTGGTCGCCCCAACCGAA 325 0 316 0 212 +CCTCCGTGGTCGCCCCAA CGAGAAGACCCTTTGGAG 325 0 316 0 212 +CGAGAAGACCCTTTGGAG CTCCGTGGTCGCCCCAAC 325 0 316 0 212 +CGAGAAGACCCTTTGGAG CGTGGTCGCCCCAACCGA 325 0 316 0 212 +CGAGAAGACCCTTTGGAG GTGGTCGCCCCAACCGAA 325 0 316 0 212 +ACGAGAAGACCCTTTGGA CCTCCGTGGTCGCCCCAA 325 0 316 0 212 +ACGAGAAGACCCTTTGGA CTCCGTGGTCGCCCCAAC 325 0 316 0 212 +ACGAGAAGACCCTTTGGA CGTGGTCGCCCCAACCGA 325 0 316 0 212 +ACGAGAAGACCCTTTGGA GTGGTCGCCCCAACCGAA 325 0 316 0 212 +ACAAGACGAGAAGACCCT CCTCCGTGGTCGCCCCAA 325 0 316 0 212 +ACAAGACGAGAAGACCCT CTCCGTGGTCGCCCCAAC 325 0 316 0 212 +ACAAGACGAGAAGACCCT CGTGGTCGCCCCAACCGA 325 0 316 0 212 +CCTCCGTGGTCGCCCCAA TACAAGACGAGAAGACCC 325 0 316 0 212 +CTCCGTGGTCGCCCCAAC TACAAGACGAGAAGACCC 325 0 316 0 212 +CGTGGTCGCCCCAACCGA TACAAGACGAGAAGACCC 325 0 316 0 212 +GTGGTCGCCCCAACCGAA TACAAGACGAGAAGACCC 325 0 316 0 212 +CCTCCGTGGTCGCCCCAA GAGAAGACCCTTTGGAGC 325 0 316 0 212 +ATACAAGACGAGAAGACC CCTCCGTGGTCGCCCCAA 325 0 316 0 212 +ATACAAGACGAGAAGACC CTCCGTGGTCGCCCCAAC 325 0 316 0 212 +ATACAAGACGAGAAGACC CGTGGTCGCCCCAACCGA 325 0 316 0 212 +ATACAAGACGAGAAGACC GTGGTCGCCCCAACCGAA 325 0 316 0 212 +AGAAGACCCTTTGGAGCT GTGGTCGCCCCAACCGAA 325 0 316 0 212 +AGAAGACCCTTTGGAGCT CGTGGTCGCCCCAACCGA 325 0 316 0 212 +AGAAGACCCTTTGGAGCT CTCCGTGGTCGCCCCAAC 325 0 316 0 212 +AGAAGACCCTTTGGAGCT CCTCCGTGGTCGCCCCAA 325 0 316 0 212 +GAAGACCCTTTGGAGCTT GTGGTCGCCCCAACCGAA 325 0 316 0 212 +CGTGGTCGCCCCAACCGA GAAGACCCTTTGGAGCTT 325 0 316 0 212 +CTCCGTGGTCGCCCCAAC GAAGACCCTTTGGAGCTT 325 0 316 0 212 +CCTCCGTGGTCGCCCCAA GAAGACCCTTTGGAGCTT 325 0 316 0 212 +CTCCGTGGTCGCCCCAAC GAGAAGACCCTTTGGAGC 325 0 316 0 212 +CGTGGTCGCCCCAACCGA GAGAAGACCCTTTGGAGC 325 0 316 0 212 +GAGAAGACCCTTTGGAGC GTGGTCGCCCCAACCGAA 325 0 316 0 212 +ACTCTTAGTTAACAGCTA AGCCCCGAAGACAGAGGT 300 0 291 0 211 +CCTTTGTTAAAGGGGCTG TCCGGGTAGAAGTGACAG 293 0 287 0 213 +CCGGGTAGAAGTGACAGA CCTTTGTTAAAGGGGCTG 293 0 287 0 213 +AAGGCATGGTCCCGACCT GAATTCCCTTGGGGGTGT 321 0 313 0 212 +AGGCATGGTCCCGACCTT GCTGAATTCCCTTGGGGG 321 0 313 0 212 +AGGCATGGTCCCGACCTT GAATTCCCTTGGGGGTGT 321 0 313 0 212 +AAAGGCATGGTCCCGACC GCTGAATTCCCTTGGGGG 321 0 313 0 212 +AAAGGCATGGTCCCGACC GAATTCCCTTGGGGGTGT 321 0 313 0 212 +AAGGCATGGTCCCGACCT GCTGAATTCCCTTGGGGG 321 0 313 0 212 +GTAACTTGGTTCGTTGAT TTTACCTTCGGTTGGGGC 312 0 303 0 211 +CCTAGGGTAACTTGGTTC CCTTCGGTTGGGGCGACC 316 0 307 0 211 +GACCACTTTGATAGAGTG TCATGTTCGGGGTATGGG 317 0 308 0 211 +GGTTGGGGCGACCACGGA TCGATATGGACTCTGGGA 318 0 309 0 211 +AACTTGGTTCGTTGATCG GTTGGGGCGACCACGGAG 319 0 310 0 211 +AGGACCACTTTGATAGAG GGTATGGGCCCGAAAGCT 320 0 311 0 211 +AAGGACCACTTTGATAGA GGTATGGGCCCGAAAGCT 320 0 311 0 211 +AAGGCATGGTCCCGACCT CTGCTGAATTCCCTTGGG 321 0 312 0 211 +AAAGGCATGGTCCCGACC CTGCTGAATTCCCTTGGG 321 0 312 0 211 +AGGCATGGTCCCGACCTT CTGCTGAATTCCCTTGGG 321 0 312 0 211 +TACAAGACGAGAAGACCC TCCTCCGTGGTCGCCCCA 324 0 315 0 211 +TACAAGACGAGAAGACCC TTCCTCCGTGGTCGCCCC 324 0 315 0 211 +ACCCTTTGGAGCTTAAGG CCTCCGTGGTCGCCCCAA 324 0 315 0 211 +ACCCTTTGGAGCTTAAGG CTCCGTGGTCGCCCCAAC 324 0 315 0 211 +ACCCTTTGGAGCTTAAGG CGTGGTCGCCCCAACCGA 324 0 315 0 211 +ACCCTTTGGAGCTTAAGG GTGGTCGCCCCAACCGAA 324 0 315 0 211 +CCTCCGTGGTCGCCCCAA GACCCTTTGGAGCTTAAG 324 0 315 0 211 +CTCCGTGGTCGCCCCAAC GACCCTTTGGAGCTTAAG 324 0 315 0 211 +CGTGGTCGCCCCAACCGA GACCCTTTGGAGCTTAAG 324 0 315 0 211 +GACCCTTTGGAGCTTAAG GTGGTCGCCCCAACCGAA 324 0 315 0 211 +GAAGACCCTTTGGAGCTT TTCCTCCGTGGTCGCCCC 324 0 315 0 211 +GAAGACCCTTTGGAGCTT TCCTCCGTGGTCGCCCCA 324 0 315 0 211 +AGGGCTTAGGCCTTTCGC TTTTCCTAGTACGAAAGG 324 0 315 0 211 +GGGCTTAGGCCTTTCGCA TTTTCCTAGTACGAAAGG 324 0 315 0 211 +AGAAGACCCTTTGGAGCT TTCCTCCGTGGTCGCCCC 324 0 315 0 211 +AGAAGACCCTTTGGAGCT TCCTCCGTGGTCGCCCCA 324 0 315 0 211 +GAGAAGACCCTTTGGAGC TTCCTCCGTGGTCGCCCC 324 0 315 0 211 +GAGAAGACCCTTTGGAGC TCCTCCGTGGTCGCCCCA 324 0 315 0 211 +ATACAAGACGAGAAGACC TCCTCCGTGGTCGCCCCA 324 0 315 0 211 +ATACAAGACGAGAAGACC TTCCTCCGTGGTCGCCCC 324 0 315 0 211 +CGAGAAGACCCTTTGGAG TCCTCCGTGGTCGCCCCA 324 0 315 0 211 +ACAAGACGAGAAGACCCT TCCTCCGTGGTCGCCCCA 324 0 315 0 211 +ACAAGACGAGAAGACCCT TTCCTCCGTGGTCGCCCC 324 0 315 0 211 +ACGAGAAGACCCTTTGGA TCCTCCGTGGTCGCCCCA 324 0 315 0 211 +CGAGAAGACCCTTTGGAG TTCCTCCGTGGTCGCCCC 324 0 315 0 211 +ACGAGAAGACCCTTTGGA TTCCTCCGTGGTCGCCCC 324 0 315 0 211 +TAACAGCTAAGCGCTCAA TGGGTTGTAGCCCCGAAG 299 0 291 0 211 +ACCTTCGGTTGGGGCGAC GGGTAACTTGGTTCGTTG 309 0 300 0 210 +CCTTCGGTTGGGGCGACC GTAACTTGGTTCGTTGAT 311 0 302 0 210 +GTAACTTGGTTCGTTGAT TTACCTTCGGTTGGGGCG 311 0 302 0 210 +CCTAGGGTAACTTGGTTC TTTTACCTTCGGTTGGGG 315 0 306 0 210 +CCTAGGGTAACTTGGTTC TTACCTTCGGTTGGGGCG 315 0 306 0 210 +CCTAGGGTAACTTGGTTC CGGTTGGGGCGACCACGG 320 0 311 0 210 +ATACAAGACGAGAAGACC TTTCCTCCGTGGTCGCCC 323 0 314 0 210 +ATACAAGACGAGAAGACC GGTCGCCCCAACCGAAGG 323 0 314 0 210 +ATACAAGACGAGAAGACC TGGTCGCCCCAACCGAAG 323 0 314 0 210 +GGTCGCCCCAACCGAAGG TACAAGACGAGAAGACCC 323 0 314 0 210 +TACAAGACGAGAAGACCC TGGTCGCCCCAACCGAAG 323 0 314 0 210 +TACAAGACGAGAAGACCC TTTCCTCCGTGGTCGCCC 323 0 314 0 210 +ACAAGACGAGAAGACCCT GGTCGCCCCAACCGAAGG 323 0 314 0 210 +ACAAGACGAGAAGACCCT TGGTCGCCCCAACCGAAG 323 0 314 0 210 +ACAAGACGAGAAGACCCT TTTCCTCCGTGGTCGCCC 323 0 314 0 210 +GACGAGAAGACCCTTTGG GTGGTCGCCCCAACCGAA 323 0 314 0 210 +CGTGGTCGCCCCAACCGA GACGAGAAGACCCTTTGG 323 0 314 0 210 +CTCCGTGGTCGCCCCAAC GACGAGAAGACCCTTTGG 323 0 314 0 210 +CCTCCGTGGTCGCCCCAA GACGAGAAGACCCTTTGG 323 0 314 0 210 +ACGAGAAGACCCTTTGGA GGTCGCCCCAACCGAAGG 323 0 314 0 210 +ACGAGAAGACCCTTTGGA TGGTCGCCCCAACCGAAG 323 0 314 0 210 +ACGAGAAGACCCTTTGGA TTTCCTCCGTGGTCGCCC 323 0 314 0 210 +CGAGAAGACCCTTTGGAG GGTCGCCCCAACCGAAGG 323 0 314 0 210 +CGAGAAGACCCTTTGGAG TGGTCGCCCCAACCGAAG 323 0 314 0 210 +CGAGAAGACCCTTTGGAG TTTCCTCCGTGGTCGCCC 323 0 314 0 210 +GAGAAGACCCTTTGGAGC GGTCGCCCCAACCGAAGG 323 0 314 0 210 +GAGAAGACCCTTTGGAGC TGGTCGCCCCAACCGAAG 323 0 314 0 210 +GAGAAGACCCTTTGGAGC TTTCCTCCGTGGTCGCCC 323 0 314 0 210 +AGAAGACCCTTTGGAGCT GGTCGCCCCAACCGAAGG 323 0 314 0 210 +AGAAGACCCTTTGGAGCT TGGTCGCCCCAACCGAAG 323 0 314 0 210 +AGAAGACCCTTTGGAGCT TTTCCTCCGTGGTCGCCC 323 0 314 0 210 +GAAGACCCTTTGGAGCTT GGTCGCCCCAACCGAAGG 323 0 314 0 210 +GAAGACCCTTTGGAGCTT TGGTCGCCCCAACCGAAG 323 0 314 0 210 +GAAGACCCTTTGGAGCTT TTTCCTCCGTGGTCGCCC 323 0 314 0 210 +GACCCTTTGGAGCTTAAG GGTCGCCCCAACCGAAGG 323 0 314 0 210 +GACCCTTTGGAGCTTAAG TGGTCGCCCCAACCGAAG 323 0 314 0 210 +GACCCTTTGGAGCTTAAG TCCTCCGTGGTCGCCCCA 323 0 314 0 210 +GACCCTTTGGAGCTTAAG TTCCTCCGTGGTCGCCCC 323 0 314 0 210 +ACCCTTTGGAGCTTAAGG GGTCGCCCCAACCGAAGG 323 0 314 0 210 +ACCCTTTGGAGCTTAAGG TGGTCGCCCCAACCGAAG 323 0 314 0 210 +ACCCTTTGGAGCTTAAGG TCCTCCGTGGTCGCCCCA 323 0 314 0 210 +ACCCTTTGGAGCTTAAGG TTCCTCCGTGGTCGCCCC 323 0 314 0 210 +CTGGTGATAGCTGGTTGC GTTAAAGGGGCTGTACCC 297 0 288 0 209 +TGAATTCCCTTGGGGGTG TGCACAAAGGCATGGTCC 314 0 306 0 210 +CTGAATTCCCTTGGGGGT TGCACAAAGGCATGGTCC 314 0 306 0 210 +AAAGGCATGGTCCCGACC ATTCCCTTGGGGGTGTGG 320 0 312 0 210 +AGGCATGGTCCCGACCTT ATTCCCTTGGGGGTGTGG 320 0 312 0 210 +AAGGCATGGTCCCGACCT ATTCCCTTGGGGGTGTGG 320 0 312 0 210 +GGGTAACTTGGTTCGTTG TACCTTCGGTTGGGGCGA 308 0 299 0 209 +GTAACTTGGTTCGTTGAT TTTTACCTTCGGTTGGGG 310 0 301 0 209 +GTTGGGGCGACCACGGAG TCGATATGGACTCTGGGA 314 0 305 0 209 +ACCACTTTGATAGAGTGA CCGTCATGTTCGGGGTAT 315 0 306 0 209 +CGGTTGGGGCGACCACGG GTAACTTGGTTCGTTGAT 315 0 306 0 209 +ACCACTTTGATAGAGTGA CGTCATGTTCGGGGTATG 315 0 306 0 209 +ACCACTTTGATAGAGTGA ACCGTCATGTTCGGGGTA 315 0 306 0 209 +GACCACTTTGATAGAGTG GGGGTATGGGCCCGAAAG 317 0 308 0 209 +GACCACTTTGATAGAGTG GGGTATGGGCCCGAAAGC 318 0 309 0 209 +GACCACTTTGATAGAGTG GTATGGGCCCGAAAGCTT 318 0 309 0 209 +CCTAGGGTAACTTGGTTC GGTTGGGGCGACCACGGA 319 0 310 0 209 +ATACAAGACGAGAAGACC TCCGTGGTCGCCCCAACC 322 0 313 0 209 +GACGAGAAGACCCTTTGG GGTCGCCCCAACCGAAGG 322 0 313 0 209 +GACCCTTTGGAGCTTAAG TTTCCTCCGTGGTCGCCC 322 0 313 0 209 +TACAAGACGAGAAGACCC TCCGTGGTCGCCCCAACC 322 0 313 0 209 +AGACCCTTTGGAGCTTAA CCTCCGTGGTCGCCCCAA 322 0 313 0 209 +AGACCCTTTGGAGCTTAA CTCCGTGGTCGCCCCAAC 322 0 313 0 209 +AGACCCTTTGGAGCTTAA CGTGGTCGCCCCAACCGA 322 0 313 0 209 +AGACCCTTTGGAGCTTAA GTGGTCGCCCCAACCGAA 322 0 313 0 209 +AAGACCCTTTGGAGCTTA CCTCCGTGGTCGCCCCAA 322 0 313 0 209 +AAGACCCTTTGGAGCTTA CTCCGTGGTCGCCCCAAC 322 0 313 0 209 +AAGACCCTTTGGAGCTTA CGTGGTCGCCCCAACCGA 322 0 313 0 209 +AAGACCCTTTGGAGCTTA GTGGTCGCCCCAACCGAA 322 0 313 0 209 +GAAGACCCTTTGGAGCTT TCCGTGGTCGCCCCAACC 322 0 313 0 209 +CCGTGGTCGCCCCAACCG GAAGACCCTTTGGAGCTT 322 0 313 0 209 +TAAAAGGGCTTAGGCCTT TTTTCCTAGTACGAAAGG 322 0 313 0 209 +ACCCTTTGGAGCTTAAGG TTTCCTCCGTGGTCGCCC 322 0 313 0 209 +AGAAGACCCTTTGGAGCT TCCGTGGTCGCCCCAACC 322 0 313 0 209 +ACAAGACGAGAAGACCCT CCGTGGTCGCCCCAACCG 322 0 313 0 209 +ACAAGACGAGAAGACCCT TCCGTGGTCGCCCCAACC 322 0 313 0 209 +AGAAGACCCTTTGGAGCT CCGTGGTCGCCCCAACCG 322 0 313 0 209 +GAGAAGACCCTTTGGAGC TCCGTGGTCGCCCCAACC 322 0 313 0 209 +CCGTGGTCGCCCCAACCG GAGAAGACCCTTTGGAGC 322 0 313 0 209 +CGAGAAGACCCTTTGGAG TCCGTGGTCGCCCCAACC 322 0 313 0 209 +CCGTGGTCGCCCCAACCG CGAGAAGACCCTTTGGAG 322 0 313 0 209 +GACGAGAAGACCCTTTGG TGGTCGCCCCAACCGAAG 322 0 313 0 209 +ATACAAGACGAGAAGACC CCGTGGTCGCCCCAACCG 322 0 313 0 209 +CCGTGGTCGCCCCAACCG TACAAGACGAGAAGACCC 322 0 313 0 209 +ACGAGAAGACCCTTTGGA TCCGTGGTCGCCCCAACC 322 0 313 0 209 +ACGAGAAGACCCTTTGGA CCGTGGTCGCCCCAACCG 322 0 313 0 209 +GACGAGAAGACCCTTTGG TTCCTCCGTGGTCGCCCC 322 0 313 0 209 +GACGAGAAGACCCTTTGG TCCTCCGTGGTCGCCCCA 322 0 313 0 209 +AAGGGGCTGTACCCCCTT CTGGTGATAGCTGGTTGC 296 0 287 0 208 +CCTGGTGATAGCTGGTTG GTTAAAGGGGCTGTACCC 296 0 287 0 208 +CTGAATTCCCTTGGGGGT GCACAAAGGCATGGTCCC 313 0 305 0 209 +GCACAAAGGCATGGTCCC TGAATTCCCTTGGGGGTG 313 0 305 0 209 +GGGTAACTTGGTTCGTTG TTTACCTTCGGTTGGGGC 307 0 298 0 208 +GGTTGGGGCGACCACGGA GTAACTTGGTTCGTTGAT 314 0 305 0 208 +AAGGCATGGTCCCGACCT ACTGCTGAATTCCCTTGG 317 0 308 0 208 +ACTGCTGAATTCCCTTGG AGGCATGGTCCCGACCTT 317 0 308 0 208 +AAAGGCATGGTCCCGACC ACTGCTGAATTCCCTTGG 317 0 308 0 208 +TTAGATTGTGATTCTAAA TTTTTGGTTCCTAAGACC 318 0 309 0 208 +TTAGATTGTGATTCTAAA TTTTGGTTCCTAAGACCA 318 0 309 0 208 +AAGACCCTTTGGAGCTTA TCCTCCGTGGTCGCCCCA 321 0 312 0 208 +GACCCTTTGGAGCTTAAG TCCGTGGTCGCCCCAACC 321 0 312 0 208 +CAAGACGAGAAGACCCTT GTGGTCGCCCCAACCGAA 321 0 312 0 208 +CAAGACGAGAAGACCCTT CCTCCGTGGTCGCCCCAA 321 0 312 0 208 +GACGAGAAGACCCTTTGG TTTCCTCCGTGGTCGCCC 321 0 312 0 208 +AGACCCTTTGGAGCTTAA TCCTCCGTGGTCGCCCCA 321 0 312 0 208 +CAAGACGAGAAGACCCTT CGTGGTCGCCCCAACCGA 321 0 312 0 208 +AGACCCTTTGGAGCTTAA TTCCTCCGTGGTCGCCCC 321 0 312 0 208 +ACCCTTTGGAGCTTAAGG TCCGTGGTCGCCCCAACC 321 0 312 0 208 +CCGTGGTCGCCCCAACCG GACCCTTTGGAGCTTAAG 321 0 312 0 208 +ACCCTTTGGAGCTTAAGG CCGTGGTCGCCCCAACCG 321 0 312 0 208 +AAGACCCTTTGGAGCTTA TTCCTCCGTGGTCGCCCC 321 0 312 0 208 +CAAGACGAGAAGACCCTT CTCCGTGGTCGCCCCAAC 321 0 312 0 208 +AGGTCAGTTTCTATCTGT ATTTACCATGCTCTGCCA 323 0 314 0 208 +ATTTACCATGCTCTGCCA GGTCAGTTTCTATCTGTA 323 0 314 0 208 +GCGTACACACCGCCCGTC TTGCATGGATGTCTTCTC 324 0 315 0 208 +ACACCGCCCGTCACTCTC TTGCATGGATGTCTTCTC 324 0 315 0 208 +CACACCGCCCGTCACTCT TTGCATGGATGTCTTCTC 324 0 315 0 208 +AAAGGCATGGTCCCGACC TCACTGCTGAATTCCCTT 317 0 310 0 209 +AAGGCATGGTCCCGACCT ATCACTGCTGAATTCCCT 317 0 310 0 209 +AAGGCATGGTCCCGACCT TCACTGCTGAATTCCCTT 317 0 310 0 209 +AGGCATGGTCCCGACCTT TCACTGCTGAATTCCCTT 317 0 310 0 209 +AAAGGCATGGTCCCGACC ATCACTGCTGAATTCCCT 317 0 310 0 209 +AGGCATGGTCCCGACCTT ATCACTGCTGAATTCCCT 317 0 310 0 209 +AAGGGGCTGTACCCCCTT CCTGGTGATAGCTGGTTG 295 0 286 0 207 +AAGGCATGGTCCCGACCT CACTGCTGAATTCCCTTG 316 0 308 0 208 +AGGCATGGTCCCGACCTT CACTGCTGAATTCCCTTG 316 0 308 0 208 +AAAGGCATGGTCCCGACC CACTGCTGAATTCCCTTG 316 0 308 0 208 +AGGCATGGTCCCGACCTT TTCCCTTGGGGGTGTGGC 318 0 310 0 208 +AAAGGCATGGTCCCGACC TTCCCTTGGGGGTGTGGC 318 0 310 0 208 +AAGGCATGGTCCCGACCT TTCCCTTGGGGGTGTGGC 318 0 310 0 208 +CCTTCGGTTGGGGCGACC GGGTAACTTGGTTCGTTG 306 0 297 0 207 +GGGTAACTTGGTTCGTTG TTACCTTCGGTTGGGGCG 306 0 297 0 207 +CCTAGGGTAACTTGGTTC GTTGGGGCGACCACGGAG 315 0 306 0 207 +GACCACTTTGATAGAGTG GGTATGGGCCCGAAAGCT 316 0 307 0 207 +TGCACCAAGAGTTTTTGG TTAGATTGTGATTCTAAA 317 0 308 0 207 +AGAGTTTTTGGTTCCTAA TTAGATTGTGATTCTAAA 317 0 308 0 207 +GAGTTTTTGGTTCCTAAG TTAGATTGTGATTCTAAA 317 0 308 0 207 +AAGACCCTTTGGAGCTTA TGGTCGCCCCAACCGAAG 320 0 311 0 207 +CAAGACGAGAAGACCCTT GGTCGCCCCAACCGAAGG 320 0 311 0 207 +CAAGACGAGAAGACCCTT TGGTCGCCCCAACCGAAG 320 0 311 0 207 +CAAGACGAGAAGACCCTT TCCTCCGTGGTCGCCCCA 320 0 311 0 207 +CAAGACGAGAAGACCCTT TTCCTCCGTGGTCGCCCC 320 0 311 0 207 +CCGTGGTCGCCCCAACCG GACGAGAAGACCCTTTGG 320 0 311 0 207 +GACGAGAAGACCCTTTGG TCCGTGGTCGCCCCAACC 320 0 311 0 207 +AAGACCCTTTGGAGCTTA GGTCGCCCCAACCGAAGG 320 0 311 0 207 +AAGACCCTTTGGAGCTTA TTTCCTCCGTGGTCGCCC 320 0 311 0 207 +AGACCCTTTGGAGCTTAA GGTCGCCCCAACCGAAGG 320 0 311 0 207 +AGACCCTTTGGAGCTTAA TGGTCGCCCCAACCGAAG 320 0 311 0 207 +AGACCCTTTGGAGCTTAA TTTCCTCCGTGGTCGCCC 320 0 311 0 207 +AATTTACCATGCTCTGCC GGTCAGTTTCTATCTGTA 322 0 313 0 207 +CGTACACACCGCCCGTCA TTGCATGGATGTCTTCTC 323 0 314 0 207 +CTTTCAGCTTTCCCTTGC TGAGCCAAACAGCTAGCT 297 0 288 0 206 +ACCTTCGGTTGGGGCGAC GGTAACTTGGTTCGTTGA 305 0 296 0 206 +GGGTAACTTGGTTCGTTG TTTTACCTTCGGTTGGGG 305 0 296 0 206 +GTAACTTGGTTCGTTGAT GTTGGGGCGACCACGGAG 310 0 301 0 206 +CGGTTGGGGCGACCACGG GGGTAACTTGGTTCGTTG 310 0 301 0 206 +AAGACCCTTTGGAGCTTA TCCGTGGTCGCCCCAACC 319 0 310 0 206 +AAGACCCTTTGGAGCTTA CCGTGGTCGCCCCAACCG 319 0 310 0 206 +AGACCCTTTGGAGCTTAA CCGTGGTCGCCCCAACCG 319 0 310 0 206 +CAAGACGAGAAGACCCTT TTTCCTCCGTGGTCGCCC 319 0 310 0 206 +AGACCCTTTGGAGCTTAA TCCGTGGTCGCCCCAACC 319 0 310 0 206 +GGGCTTAGGCCTTTCGCA TACGAAAGGATCGGAAAA 320 0 311 0 206 +AGGGCTTAGGCCTTTCGC TACGAAAGGATCGGAAAA 320 0 311 0 206 +ACACCGCCCGTCACTCTC CTTCTCGGTGTAAGTGAG 322 0 313 0 206 +ACACCGCCCGTCACTCTC TCTTCTCGGTGTAAGTGA 322 0 313 0 206 +ACGCGTACACACCGCCCG GGATGTCTTCTCGGTGTA 322 0 313 0 206 +CACACCGCCCGTCACTCT TCTTCTCGGTGTAAGTGA 322 0 313 0 206 +GTACACACCGCCCGTCAC TTGCATGGATGTCTTCTC 322 0 313 0 206 +CACACCGCCCGTCACTCT CTTCTCGGTGTAAGTGAG 322 0 313 0 206 +CACACCGCCCGTCACTCT GGATGTCTTCTCGGTGTA 322 0 313 0 206 +CTTCTCGGTGTAAGTGAG GACGCGTACACACCGCCC 322 0 313 0 206 +CGCGTACACACCGCCCGT TCTTCTCGGTGTAAGTGA 322 0 313 0 206 +CGCGTACACACCGCCCGT CTTCTCGGTGTAAGTGAG 322 0 313 0 206 +GACGCGTACACACCGCCC TCTTCTCGGTGTAAGTGA 322 0 313 0 206 +GACGCGTACACACCGCCC GGATGTCTTCTCGGTGTA 322 0 313 0 206 +GCGTACACACCGCCCGTC GGATGTCTTCTCGGTGTA 322 0 313 0 206 +GCGTACACACCGCCCGTC TCTTCTCGGTGTAAGTGA 322 0 313 0 206 +CTTCTCGGTGTAAGTGAG GCGTACACACCGCCCGTC 322 0 313 0 206 +CGCGTACACACCGCCCGT GGATGTCTTCTCGGTGTA 322 0 313 0 206 +ACGCGTACACACCGCCCG CTTCTCGGTGTAAGTGAG 322 0 313 0 206 +ACGCGTACACACCGCCCG TCTTCTCGGTGTAAGTGA 322 0 313 0 206 +ACACCGCCCGTCACTCTC GGATGTCTTCTCGGTGTA 322 0 313 0 206 +GGCATGGTCCCGACCTTA TATCACTGCTGAATTCCC 315 0 308 0 207 +GGCATGGTCCCGACCTTA TTATCACTGCTGAATTCC 315 0 307 0 206 +AATTCCCTTGGGGGTGTG GGCATGGTCCCGACCTTA 316 0 308 0 206 +GGTAACTTGGTTCGTTGA TACCTTCGGTTGGGGCGA 304 0 295 0 205 +GGGTAACTTGGTTCGTTG GGTTGGGGCGACCACGGA 309 0 300 0 205 +GTCGATATGGACTCTGGG GTTGGGGCGACCACGGAG 310 0 301 0 205 +ACCACTTTGATAGAGTGA TCATGTTCGGGGTATGGG 311 0 302 0 205 +ACCACTTTGATAGAGTGA GTATGGGCCCGAAAGCTT 314 0 305 0 205 +CAAGACGAGAAGACCCTT TCCGTGGTCGCCCCAACC 318 0 309 0 205 +CAAGACGAGAAGACCCTT CCGTGGTCGCCCCAACCG 318 0 309 0 205 +CTTCTCGGTGTAAGTGAG TGAGACGCGTACACACCG 321 0 312 0 205 +CTGAGACGCGTACACACC TCTTCTCGGTGTAAGTGA 321 0 312 0 205 +ACACCGCCCGTCACTCTC GCATGGATGTCTTCTCGG 321 0 312 0 205 +CGTACACACCGCCCGTCA GGATGTCTTCTCGGTGTA 321 0 312 0 205 +CGCGTACACACCGCCCGT GCATGGATGTCTTCTCGG 321 0 312 0 205 +ACACCGCCCGTCACTCTC TGCATGGATGTCTTCTCG 321 0 312 0 205 +CTGAGACGCGTACACACC CTTCTCGGTGTAAGTGAG 321 0 312 0 205 +AGACGCGTACACACCGCC GGATGTCTTCTCGGTGTA 321 0 312 0 205 +ACACCGCCCGTCACTCTC GTCTTCTCGGTGTAAGTG 321 0 312 0 205 +ACGCGTACACACCGCCCG GCATGGATGTCTTCTCGG 321 0 312 0 205 +GGATGTCTTCTCGGTGTA TGAGACGCGTACACACCG 321 0 312 0 205 +ACGCGTACACACCGCCCG GTCTTCTCGGTGTAAGTG 321 0 312 0 205 +TCTGAGACGCGTACACAC TCTTCTCGGTGTAAGTGA 321 0 312 0 205 +AGACGCGTACACACCGCC CTTCTCGGTGTAAGTGAG 321 0 312 0 205 +TCTTCTCGGTGTAAGTGA TGAGACGCGTACACACCG 321 0 312 0 205 +CTTCTCGGTGTAAGTGAG GAGACGCGTACACACCGC 321 0 312 0 205 +GCGTACACACCGCCCGTC TGCATGGATGTCTTCTCG 321 0 312 0 205 +CGGCTCTGAGACGCGTAC CTTCTCGGTGTAAGTGAG 321 0 312 0 205 +GCGTACACACCGCCCGTC GTCTTCTCGGTGTAAGTG 321 0 312 0 205 +CGCGTACACACCGCCCGT TTGCATGGATGTCTTCTC 321 0 312 0 205 +CTTCTCGGTGTAAGTGAG GGCTCTGAGACGCGTACA 321 0 312 0 205 +GGCTCTGAGACGCGTACA TCTTCTCGGTGTAAGTGA 321 0 312 0 205 +GACGCGTACACACCGCCC GTCTTCTCGGTGTAAGTG 321 0 312 0 205 +GCATGGATGTCTTCTCGG GCGTACACACCGCCCGTC 321 0 312 0 205 +GAGACGCGTACACACCGC TCTTCTCGGTGTAAGTGA 321 0 312 0 205 +CGCGTACACACCGCCCGT TGCATGGATGTCTTCTCG 321 0 312 0 205 +CGCGTACACACCGCCCGT GTCTTCTCGGTGTAAGTG 321 0 312 0 205 +CACACCGCCCGTCACTCT TGCATGGATGTCTTCTCG 321 0 312 0 205 +CACACCGCCCGTCACTCT GCATGGATGTCTTCTCGG 321 0 312 0 205 +AGACGCGTACACACCGCC TCTTCTCGGTGTAAGTGA 321 0 312 0 205 +GAGACGCGTACACACCGC GGATGTCTTCTCGGTGTA 321 0 312 0 205 +CACACCGCCCGTCACTCT GTCTTCTCGGTGTAAGTG 321 0 312 0 205 +CTTCTCGGTGTAAGTGAG TCTGAGACGCGTACACAC 321 0 312 0 205 +ATTTACCATGCTCTGCCA TTTTCCTAGTACGAAAGG 324 0 315 0 205 +GCTGAATTCCCTTGGGGG GGCATGGTCCCGACCTTA 315 0 308 0 206 +GAATTCCCTTGGGGGTGT GGCATGGTCCCGACCTTA 315 0 308 0 206 +AAGGCATGGTCCCGACCT CTGAATTCCCTTGGGGGT 314 0 306 0 205 +AAAGGCATGGTCCCGACC CTGAATTCCCTTGGGGGT 314 0 306 0 205 +AAAGGCATGGTCCCGACC TGAATTCCCTTGGGGGTG 314 0 306 0 205 +AGGCATGGTCCCGACCTT CTGAATTCCCTTGGGGGT 314 0 306 0 205 +AGGCATGGTCCCGACCTT TGAATTCCCTTGGGGGTG 314 0 306 0 205 +AAGGCATGGTCCCGACCT TGAATTCCCTTGGGGGTG 314 0 306 0 205 +ATTCCCTTGGGGGTGTGG GGCATGGTCCCGACCTTA 315 0 307 0 205 +CTGCTGAATTCCCTTGGG GGCATGGTCCCGACCTTA 315 0 307 0 205 +AGGGCTTAACTGTCTCCC GGTCGCCCCAACCGAAGG 300 0 291 0 204 +AGGGCTTAACTGTCTCCC GTCGCCCCAACCGAAGGT 300 0 291 0 204 +GGTAACTTGGTTCGTTGA TTTACCTTCGGTTGGGGC 303 0 294 0 204 +AGGAGGATTTAGTAGTAA GCACCTTCCGGTACACTT 324 0 316 0 205 +ACCTTCCGGTACACTTAC AGGAGGATTTAGTAGTAA 324 0 316 0 205 +AGGAGGATTTAGTAGTAA CACCTTCCGGTACACTTA 324 0 316 0 205 +AGGAGGATTTAGTAGTAA CCTTCCGGTACACTTACC 324 0 316 0 205 +CGATATGGACTCTGGGAG GGGGCGACCACGGAGGAA 311 0 302 0 204 +CAAAGGCATGGTCCCGAC TTATCACTGCTGAATTCC 311 0 302 0 204 +CGATATGGACTCTGGGAG GGGCGACCACGGAGGAAA 311 0 302 0 204 +GTCGTCAGCTTACCCTGT TTACTACTAAATCCTCCT 313 0 304 0 204 +TATTAGATTGTGATTCTA TTTTGGTTCCTAAGACCA 314 0 305 0 204 +TATTAGATTGTGATTCTA TTTTTGGTTCCTAAGACC 314 0 305 0 204 +TAAAAGGGCTTAGGCCTT TACGAAAGGATCGGAAAA 318 0 309 0 204 +ATTTACCATGCTCTGCCA CAGGTCAGTTTCTATCTG 319 0 310 0 204 +CTCTGAGACGCGTACACA CTTCTCGGTGTAAGTGAG 320 0 311 0 204 +GTCTTCTCGGTGTAAGTG TCTGAGACGCGTACACAC 320 0 311 0 204 +CTCTGAGACGCGTACACA TCTTCTCGGTGTAAGTGA 320 0 311 0 204 +GGATGTCTTCTCGGTGTA GTACACACCGCCCGTCAC 320 0 311 0 204 +GCTCTGAGACGCGTACAC TCTTCTCGGTGTAAGTGA 320 0 311 0 204 +CTTCTCGGTGTAAGTGAG GCTCTGAGACGCGTACAC 320 0 311 0 204 +GAGACGCGTACACACCGC GTCTTCTCGGTGTAAGTG 320 0 311 0 204 +CGTACACACCGCCCGTCA TGCATGGATGTCTTCTCG 320 0 311 0 204 +CGTACACACCGCCCGTCA GCATGGATGTCTTCTCGG 320 0 311 0 204 +CTGAGACGCGTACACACC GTCTTCTCGGTGTAAGTG 320 0 311 0 204 +CGTACACACCGCCCGTCA TCTTCTCGGTGTAAGTGA 320 0 311 0 204 +CGTACACACCGCCCGTCA CTTCTCGGTGTAAGTGAG 320 0 311 0 204 +GTCTTCTCGGTGTAAGTG TGAGACGCGTACACACCG 320 0 311 0 204 +AGACGCGTACACACCGCC GTCTTCTCGGTGTAAGTG 320 0 311 0 204 +AATTTACCATGCTCTGCC TTTTCCTAGTACGAAAGG 323 0 314 0 204 +CAAAGGCATGGTCCCGAC TATCACTGCTGAATTCCC 310 0 302 0 204 +GATTTGCACCAAGAGTTT TTAGATTGTGATTCTAAA 312 0 304 0 204 +ACCTTCGGTTGGGGCGAC CTAGGGTAACTTGGTTCG 300 0 291 0 203 +CAGGTCAGTTTCTATCTG CCATGCTCTGCCACCCCA 318 0 310 0 204 +ATCCAGGTCAGTTTCTAT CCATGCTCTGCCACCCCA 318 0 310 0 204 +CCATGCTCTGCCACCCCA TCCAGGTCAGTTTCTATC 318 0 310 0 204 +CCATGCTCTGCCACCCCA GGTCAGTTTCTATCTGTA 318 0 310 0 204 +AATCCAGGTCAGTTTCTA CCATGCTCTGCCACCCCA 318 0 310 0 204 +CCAGGTCAGTTTCTATCT CCATGCTCTGCCACCCCA 318 0 310 0 204 +AGGTCAGTTTCTATCTGT CCATGCTCTGCCACCCCA 318 0 310 0 204 +CCTTCGGTTGGGGCGACC GGTAACTTGGTTCGTTGA 302 0 293 0 203 +GGTAACTTGGTTCGTTGA TTACCTTCGGTTGGGGCG 302 0 293 0 203 +AGGAGGATTTAGTAGTAA TGCACCTTCCGGTACACT 323 0 315 0 204 +GGGTAACTTGGTTCGTTG GTTGGGGCGACCACGGAG 305 0 296 0 203 +ACCTTCGGTTGGGGCGAC ATGGACTCTGGGAGAGGA 307 0 298 0 203 +ACCACTTTGATAGAGTGA GGGGTATGGGCCCGAAAG 311 0 302 0 203 +AATTCCCTTGGGGGTGTG CAAAGGCATGGTCCCGAC 311 0 302 0 203 +ACCACTTTGATAGAGTGA GGTATGGGCCCGAAAGCT 312 0 303 0 203 +ACTCTGGGAGAGGATTGC GGGCGACCACGGAGGAAA 312 0 303 0 203 +GACTCTGGGAGAGGATTG GGGCGACCACGGAGGAAA 312 0 303 0 203 +GGACTCTGGGAGAGGATT GGGCGACCACGGAGGAAA 312 0 303 0 203 +GGGCGACCACGGAGGAAA TGGACTCTGGGAGAGGAT 312 0 303 0 203 +ATGGACTCTGGGAGAGGA GGGCGACCACGGAGGAAA 312 0 303 0 203 +GGGCGACCACGGAGGAAA TATGGACTCTGGGAGAGG 312 0 303 0 203 +ACCACTTTGATAGAGTGA GGGTATGGGCCCGAAAGC 312 0 303 0 203 +ACTCTGGGAGAGGATTGC GGGGCGACCACGGAGGAA 312 0 303 0 203 +GACTCTGGGAGAGGATTG GGGGCGACCACGGAGGAA 312 0 303 0 203 +GGACTCTGGGAGAGGATT GGGGCGACCACGGAGGAA 312 0 303 0 203 +GGGGCGACCACGGAGGAA TGGACTCTGGGAGAGGAT 312 0 303 0 203 +ATGGACTCTGGGAGAGGA GGGGCGACCACGGAGGAA 312 0 303 0 203 +GGGGCGACCACGGAGGAA TATGGACTCTGGGAGAGG 312 0 303 0 203 +TATTAGATTGTGATTCTA TGCACCAAGAGTTTTTGG 313 0 304 0 203 +GAGTTTTTGGTTCCTAAG TATTAGATTGTGATTCTA 313 0 304 0 203 +AGAGTTTTTGGTTCCTAA TATTAGATTGTGATTCTA 313 0 304 0 203 +CTTAGGCCTTTCGCAATT TTTTCCTAGTACGAAAGG 316 0 307 0 203 +GGCTTAGGCCTTTCGCAA TTTTCCTAGTACGAAAGG 316 0 307 0 203 +ACGCGTACACACCGCCCG TTGCATGGATGTCTTCTC 317 0 308 0 203 +AATTTACCATGCTCTGCC AGGTCAGTTTCTATCTGT 318 0 309 0 203 +CTGAGACGCGTACACACC GGATGTCTTCTCGGTGTA 319 0 310 0 203 +GTACACACCGCCCGTCAC TGCATGGATGTCTTCTCG 319 0 310 0 203 +GCATGGATGTCTTCTCGG GTACACACCGCCCGTCAC 319 0 310 0 203 +GTACACACCGCCCGTCAC TCTTCTCGGTGTAAGTGA 319 0 310 0 203 +CTTCTCGGTGTAAGTGAG GTACACACCGCCCGTCAC 319 0 310 0 203 +CGTACACACCGCCCGTCA GTCTTCTCGGTGTAAGTG 319 0 310 0 203 +GCTCTGAGACGCGTACAC GTCTTCTCGGTGTAAGTG 319 0 310 0 203 +CTCTGAGACGCGTACACA GTCTTCTCGGTGTAAGTG 319 0 310 0 203 +CGCGTACACACCGCCCGT CGGTGTAAGTGAGATGCT 321 0 312 0 203 +ACGCGTACACACCGCCCG CGGTGTAAGTGAGATGCT 321 0 312 0 203 +ACACCGCCCGTCACTCTC CGGTGTAAGTGAGATGCT 321 0 312 0 203 +CGGTGTAAGTGAGATGCT GACGCGTACACACCGCCC 321 0 312 0 203 +CACACCGCCCGTCACTCT CGGTGTAAGTGAGATGCT 321 0 312 0 203 +CGGTGTAAGTGAGATGCT GCGTACACACCGCCCGTC 321 0 312 0 203 +CAATTTACCATGCTCTGC TTTTCCTAGTACGAAAGG 322 0 313 0 203 +AGGAGGATTTAGTAGTAA CACTTACCGTGTTACGAC 325 0 316 0 203 +AGGAGGATTTAGTAGTAA CTTACCGTGTTACGACTT 325 0 316 0 203 +ACTTACCGTGTTACGACT AGGAGGATTTAGTAGTAA 325 0 316 0 203 +ACACTTACCGTGTTACGA AGGAGGATTTAGTAGTAA 325 0 316 0 203 +AGGAGGATTTAGTAGTAA TACACTTACCGTGTTACG 325 0 316 0 203 +AGGAGGATTTAGTAGTAA GTACACTTACCGTGTTAC 325 0 316 0 203 +ATCACTGCTGAATTCCCT GGCATGGTCCCGACCTTA 312 0 305 0 204 +GGCATGGTCCCGACCTTA TCACTGCTGAATTCCCTT 312 0 305 0 204 +CAAAGGCATGGTCCCGAC GAATTCCCTTGGGGGTGT 310 0 302 0 203 +CAAAGGCATGGTCCCGAC GCTGAATTCCCTTGGGGG 310 0 302 0 203 +AGTCAGTGAAATTGATCT CCTCCGTGGTCGCCCCAA 295 0 286 0 202 +AGTCAGTGAAATTGATCT CGTGGTCGCCCCAACCGA 295 0 286 0 202 +AGTCAGTGAAATTGATCT GTGGTCGCCCCAACCGAA 295 0 286 0 202 +AGTCAGTGAAATTGATCT CTCCGTGGTCGCCCCAAC 295 0 286 0 202 +CGAGGGCTTAACTGTCTC GTCGCCCCAACCGAAGGT 296 0 287 0 202 +GGCATGGTCCCGACCTTA TTCCCTTGGGGGTGTGGC 313 0 305 0 203 +AGGGCTTAACTGTCTCCC TCGCCCCAACCGAAGGTA 298 0 289 0 202 +GGTAACTTGGTTCGTTGA TTTTACCTTCGGTTGGGG 301 0 292 0 202 +AAGTGCACCTTCCGGTAC AGGAGGATTTAGTAGTAA 322 0 314 0 203 +AGGAGGATTTAGTAGTAA GTGCACCTTCCGGTACAC 322 0 314 0 203 +AGGAGGATTTAGTAGTAA AGTGCACCTTCCGGTACA 322 0 314 0 203 +AGGAGGATTTAGTAGTAA CGGTACACTTACCGTGTT 324 0 316 0 203 +AGGAGGATTTAGTAGTAA GGTACACTTACCGTGTTA 324 0 316 0 203 +AGGAGGATTTAGTAGTAA CCGGTACACTTACCGTGT 324 0 316 0 203 +AGGAGGATTTAGTAGTAA TCCGGTACACTTACCGTG 324 0 316 0 203 +TACCTTCGGTTGGGGCGA TGGACTCTGGGAGAGGAT 306 0 297 0 202 +CGGTTGGGGCGACCACGG GGTAACTTGGTTCGTTGA 306 0 297 0 202 +GGGCGACCACGGAGGAAA TCGATATGGACTCTGGGA 309 0 300 0 202 +GGGGCGACCACGGAGGAA TCGATATGGACTCTGGGA 309 0 300 0 202 +AGGACCACTTTGATAGAG TTCGGGGTATGGGCCCGA 310 0 301 0 202 +CAAAGGCATGGTCCCGAC CTGCTGAATTCCCTTGGG 310 0 301 0 202 +AAGGACCACTTTGATAGA TTCGGGGTATGGGCCCGA 310 0 301 0 202 +CTCTGGGAGAGGATTGCG GGGGCGACCACGGAGGAA 311 0 302 0 202 +ATTTGCACCAAGAGTTTT TTAGATTGTGATTCTAAA 311 0 302 0 202 +TATATACCGCCGTCGTCA TTACTACTAAATCCTCCT 311 0 302 0 202 +GGGGCGACCACGGAGGAA TCTGGGAGAGGATTGCGC 311 0 302 0 202 +CTCTGGGAGAGGATTGCG GGGCGACCACGGAGGAAA 311 0 302 0 202 +GGGCGACCACGGAGGAAA TCTGGGAGAGGATTGCGC 311 0 302 0 202 +CTGGGAGAGGATTGCGCT GGGGCGACCACGGAGGAA 312 0 303 0 202 +GATTGCGCTGTTATCCCT GGGCGACCACGGAGGAAA 312 0 303 0 202 +AGAGGATTGCGCTGTTAT GGGCGACCACGGAGGAAA 312 0 303 0 202 +GGGGCGACCACGGAGGAA TGGGAGAGGATTGCGCTG 312 0 303 0 202 +GGGAGAGGATTGCGCTGT GGGGCGACCACGGAGGAA 312 0 303 0 202 +AGAGGATTGCGCTGTTAT GGGGCGACCACGGAGGAA 312 0 303 0 202 +GAGGATTGCGCTGTTATC GGGCGACCACGGAGGAAA 312 0 303 0 202 +AGGATTGCGCTGTTATCC GGGCGACCACGGAGGAAA 312 0 303 0 202 +GAGGATTGCGCTGTTATC GGGGCGACCACGGAGGAA 312 0 303 0 202 +AGGATTGCGCTGTTATCC GGGGCGACCACGGAGGAA 312 0 303 0 202 +GGATTGCGCTGTTATCCC GGGGCGACCACGGAGGAA 312 0 303 0 202 +GGATTGCGCTGTTATCCC GGGCGACCACGGAGGAAA 312 0 303 0 202 +GATTGCGCTGTTATCCCT GGGGCGACCACGGAGGAA 312 0 303 0 202 +GGGGCGACCACGGAGGAA TAACTTGGTTCGTTGATC 312 0 303 0 202 +CTGGGAGAGGATTGCGCT GGGCGACCACGGAGGAAA 312 0 303 0 202 +GGGAGAGGATTGCGCTGT GGGCGACCACGGAGGAAA 312 0 303 0 202 +GGGCGACCACGGAGGAAA TGGGAGAGGATTGCGCTG 312 0 303 0 202 +GGGCGACCACGGAGGAAA TAACTTGGTTCGTTGATC 312 0 303 0 202 +CCCTTTGGAGCTTAAGGT GTGGTCGCCCCAACCGAA 315 0 306 0 202 +CCCTTTGGAGCTTAAGGT CTCCGTGGTCGCCCCAAC 315 0 306 0 202 +CCCTTTGGAGCTTAAGGT CCTCCGTGGTCGCCCCAA 315 0 306 0 202 +CCCTTTGGAGCTTAAGGT CGTGGTCGCCCCAACCGA 315 0 306 0 202 +CAATTTACCATGCTCTGC GGTCAGTTTCTATCTGTA 317 0 308 0 202 +ATTTACCATGCTCTGCCA CCAGGTCAGTTTCTATCT 317 0 308 0 202 +ACGCGTACACACCGCCCG TGCATGGATGTCTTCTCG 318 0 309 0 202 +GTACACACCGCCCGTCAC GTCTTCTCGGTGTAAGTG 318 0 309 0 202 +GACGCGTACACACCGCCC GCATGGATGTCTTCTCGG 318 0 309 0 202 +CGGCTCTGAGACGCGTAC TCTTCTCGGTGTAAGTGA 318 0 309 0 202 +CGGCTCTGAGACGCGTAC CGGTGTAAGTGAGATGCT 320 0 311 0 202 +CGGTGTAAGTGAGATGCT TCTGAGACGCGTACACAC 320 0 311 0 202 +CGGTGTAAGTGAGATGCT GAGACGCGTACACACCGC 320 0 311 0 202 +CGGTGTAAGTGAGATGCT TGAGACGCGTACACACCG 320 0 311 0 202 +CGGTGTAAGTGAGATGCT CTGAGACGCGTACACACC 320 0 311 0 202 +CGGTGTAAGTGAGATGCT GGCTCTGAGACGCGTACA 320 0 311 0 202 +AGACGCGTACACACCGCC CGGTGTAAGTGAGATGCT 320 0 311 0 202 +GCACAAAGGCATGGTCCC TCCCTTGGGGGTGTGGCT 301 0 293 0 202 +ATTCCCTTGGGGGTGTGG CAAAGGCATGGTCCCGAC 310 0 302 0 202 +AGTCAGTGAAATTGATCT TCCTCCGTGGTCGCCCCA 294 0 285 0 201 +AGTCAGTGAAATTGATCT TTCCTCCGTGGTCGCCCC 294 0 285 0 201 +ACTGCTGAATTCCCTTGG GGCATGGTCCCGACCTTA 311 0 303 0 202 +CGAGGGCTTAACTGTCTC TCGCCCCAACCGAAGGTA 297 0 288 0 201 +CTAGGGTAACTTGGTTCG TTTACCTTCGGTTGGGGC 298 0 289 0 201 +CTAGGGTAACTTGGTTCG TACCTTCGGTTGGGGCGA 298 0 289 0 201 +AGGAGGATTTAGTAGTAA CCAAGTGCACCTTCCGGT 321 0 313 0 202 +GACTCTGGGAGAGGATTG TTTACCTTCGGTTGGGGC 305 0 296 0 201 +GGTAACTTGGTTCGTTGA GGTTGGGGCGACCACGGA 305 0 296 0 201 +CCTTCGGTTGGGGCGACC TATGGACTCTGGGAGAGG 305 0 296 0 201 +AAGGACCACTTTGATAGA CGGGGTATGGGCCCGAAA 309 0 300 0 201 +AGGACCACTTTGATAGAG CGGGGTATGGGCCCGAAA 309 0 300 0 201 +AACTTGGTTCGTTGATCG GGGCGACCACGGAGGAAA 311 0 302 0 201 +AACTTGGTTCGTTGATCG GGGGCGACCACGGAGGAA 311 0 302 0 201 +CCCTTTGGAGCTTAAGGT TTCCTCCGTGGTCGCCCC 314 0 305 0 201 +ATTTACCATGCTCTGCCA GTCAGTTTCTATCTGTAA 314 0 305 0 201 +ACGAGAAGACCCTTTGGA GTCGCCCCAACCGAAGGT 314 0 305 0 201 +ACAAGACGAGAAGACCCT GTCGCCCCAACCGAAGGT 314 0 305 0 201 +GTCGCCCCAACCGAAGGT TACAAGACGAGAAGACCC 314 0 305 0 201 +GAGAAGACCCTTTGGAGC GTCGCCCCAACCGAAGGT 314 0 305 0 201 +AGAAGACCCTTTGGAGCT GTCGCCCCAACCGAAGGT 314 0 305 0 201 +ATACAAGACGAGAAGACC GTCGCCCCAACCGAAGGT 314 0 305 0 201 +CCTCCGTGGTCGCCCCAA CCTTTGGAGCTTAAGGTA 314 0 305 0 201 +CCTTTGGAGCTTAAGGTA CTCCGTGGTCGCCCCAAC 314 0 305 0 201 +GCTTAGGCCTTTCGCAAT TTTTCCTAGTACGAAAGG 314 0 305 0 201 +CCTTTGGAGCTTAAGGTA CGTGGTCGCCCCAACCGA 314 0 305 0 201 +CCTTTGGAGCTTAAGGTA GTGGTCGCCCCAACCGAA 314 0 305 0 201 +CCCTTTGGAGCTTAAGGT TCCTCCGTGGTCGCCCCA 314 0 305 0 201 +CGAGAAGACCCTTTGGAG GTCGCCCCAACCGAAGGT 314 0 305 0 201 +GAAGACCCTTTGGAGCTT GTCGCCCCAACCGAAGGT 314 0 305 0 201 +GGATGTCTTCTCGGTGTA TCTGAGACGCGTACACAC 315 0 306 0 201 +AATTTACCATGCTCTGCC CAGGTCAGTTTCTATCTG 316 0 307 0 201 +ACCGCCCGTCACTCTCCC TTGCATGGATGTCTTCTC 317 0 308 0 201 +CACACCGCCCGTCACTCT TGTCTTCTCGGTGTAAGT 317 0 308 0 201 +ACACCGCCCGTCACTCTC TGTCTTCTCGGTGTAAGT 317 0 308 0 201 +ACGCGTACACACCGCCCG TGTCTTCTCGGTGTAAGT 317 0 308 0 201 +CACCGCCCGTCACTCTCC TTGCATGGATGTCTTCTC 317 0 308 0 201 +GGCTCTGAGACGCGTACA GTCTTCTCGGTGTAAGTG 317 0 308 0 201 +GACGCGTACACACCGCCC TGTCTTCTCGGTGTAAGT 317 0 308 0 201 +GCGTACACACCGCCCGTC TGTCTTCTCGGTGTAAGT 317 0 308 0 201 +CGCGTACACACCGCCCGT TGTCTTCTCGGTGTAAGT 317 0 308 0 201 +CGGTGTAAGTGAGATGCT GCTCTGAGACGCGTACAC 319 0 310 0 201 +CGGTGTAAGTGAGATGCT CTCTGAGACGCGTACACA 319 0 310 0 201 +CGGTGTAAGTGAGATGCT CGTACACACCGCCCGTCA 319 0 310 0 201 +ACCCGGCTCTGAGACGCG CGGTGTAAGTGAGATGCT 319 0 310 0 201 +CACTGCTGAATTCCCTTG GGCATGGTCCCGACCTTA 310 0 303 0 202 +TCCCTTGGGGGTGTGGCT TGCACAAAGGCATGGTCC 300 0 292 0 201 +AGTCAGTGAAATTGATCT TTTCCTCCGTGGTCGCCC 293 0 284 0 200 +AGTCAGTGAAATTGATCT GGTCGCCCCAACCGAAGG 293 0 284 0 200 +AGTCAGTGAAATTGATCT TGGTCGCCCCAACCGAAG 293 0 284 0 200 +AGGGCTTAACTGTCTCCC TGGTCGCCCCAACCGAAG 294 0 285 0 200 +CCTTCGGTTGGGGCGACC CTAGGGTAACTTGGTTCG 297 0 288 0 200 +CCATGCTCTGCCACCCCA TTTTCCTAGTACGAAAGG 319 0 311 0 201 +CTTCGGTTGGGGCGACCA GGACTCTGGGAGAGGATT 301 0 292 0 200 +CTTCGGTTGGGGCGACCA GACTCTGGGAGAGGATTG 301 0 292 0 200 +ACTCTGGGAGAGGATTGC CTTCGGTTGGGGCGACCA 301 0 292 0 200 +CTTCGGTTGGGGCGACCA TGGACTCTGGGAGAGGAT 301 0 292 0 200 +GGACTCTGGGAGAGGATT TTACCTTCGGTTGGGGCG 304 0 295 0 200 +ACTGCTGAATTCCCTTGG CAAAGGCATGGTCCCGAC 307 0 298 0 200 +CTCGTCGATATGGACTCT GGGCGACCACGGAGGAAA 307 0 298 0 200 +GGGCGACCACGGAGGAAA GTCGATATGGACTCTGGG 307 0 298 0 200 +GGGGCGACCACGGAGGAA GTCGATATGGACTCTGGG 307 0 298 0 200 +CTCGTCGATATGGACTCT GGGGCGACCACGGAGGAA 307 0 298 0 200 +AGGACCACTTTGATAGAG TCGGGGTATGGGCCCGAA 308 0 299 0 200 +AAGGACCACTTTGATAGA TCGGGGTATGGGCCCGAA 308 0 299 0 200 +TTAGATTGTGATTCTAAA TTTGCACCAAGAGTTTTT 309 0 300 0 200 +CCCTTTGGAGCTTAAGGT TGGTCGCCCCAACCGAAG 313 0 304 0 200 +CCCTTTGGAGCTTAAGGT TTTCCTCCGTGGTCGCCC 313 0 304 0 200 +ACCCTTTGGAGCTTAAGG GTCGCCCCAACCGAAGGT 313 0 304 0 200 +CCTTTGGAGCTTAAGGTA TCCTCCGTGGTCGCCCCA 313 0 304 0 200 +CCTTTGGAGCTTAAGGTA TTCCTCCGTGGTCGCCCC 313 0 304 0 200 +GACCCTTTGGAGCTTAAG GTCGCCCCAACCGAAGGT 313 0 304 0 200 +CCCTTTGGAGCTTAAGGT GTCGCCCCAACCGAAGGT 313 0 304 0 200 +AATTTACCATGCTCTGCC GTCAGTTTCTATCTGTAA 313 0 304 0 200 +CCCTTTGGAGCTTAAGGT GGTCGCCCCAACCGAAGG 313 0 304 0 200 +GACGCGTACACACCGCCC TGCATGGATGTCTTCTCG 314 0 305 0 200 +GGTCAGTTTCTATCTGTA TTACCATGCTCTGCCACC 315 0 306 0 200 +ATTTACCATGCTCTGCCA TCCAGGTCAGTTTCTATC 315 0 306 0 200 +CCAGGTCAGTTTCTATCT TTACCATGCTCTGCCACC 315 0 306 0 200 +CAGGTCAGTTTCTATCTG TTACCATGCTCTGCCACC 315 0 306 0 200 +CAGGTCAGTTTCTATCTG TTTACCATGCTCTGCCAC 315 0 306 0 200 +AGGTCAGTTTCTATCTGT TTACCATGCTCTGCCACC 315 0 306 0 200 +AGGTCAGTTTCTATCTGT TTTACCATGCTCTGCCAC 315 0 306 0 200 +AGGTCAGTTTCTATCTGT CAATTTACCATGCTCTGC 315 0 306 0 200 +GGTCAGTTTCTATCTGTA TTTACCATGCTCTGCCAC 315 0 306 0 200 +ACCGCCCGTCACTCTCCC GCATGGATGTCTTCTCGG 316 0 307 0 200 +ACACCGCCCGTCACTCTC ATGTCTTCTCGGTGTAAG 316 0 307 0 200 +CTGAGACGCGTACACACC TGTCTTCTCGGTGTAAGT 316 0 307 0 200 +ATGTCTTCTCGGTGTAAG GCGTACACACCGCCCGTC 316 0 307 0 200 +TGAGACGCGTACACACCG TGTCTTCTCGGTGTAAGT 316 0 307 0 200 +ATGTCTTCTCGGTGTAAG CACACCGCCCGTCACTCT 316 0 307 0 200 +CACCGCCCGTCACTCTCC GCATGGATGTCTTCTCGG 316 0 307 0 200 +ATGTCTTCTCGGTGTAAG CGCGTACACACCGCCCGT 316 0 307 0 200 +ATGTCTTCTCGGTGTAAG GACGCGTACACACCGCCC 316 0 307 0 200 +CACCGCCCGTCACTCTCC TGCATGGATGTCTTCTCG 316 0 307 0 200 +GAGACGCGTACACACCGC TGTCTTCTCGGTGTAAGT 316 0 307 0 200 +ACGCGTACACACCGCCCG ATGTCTTCTCGGTGTAAG 316 0 307 0 200 +AGACGCGTACACACCGCC TGTCTTCTCGGTGTAAGT 316 0 307 0 200 +TCTGAGACGCGTACACAC TGTCTTCTCGGTGTAAGT 316 0 307 0 200 +ACCGCCCGTCACTCTCCC TGCATGGATGTCTTCTCG 316 0 307 0 200 +CGCGTACACACCGCCCGT CTCGGTGTAAGTGAGATG 318 0 309 0 200 +CGGTGTAAGTGAGATGCT GTACACACCGCCCGTCAC 318 0 309 0 200 +CACACCGCCCGTCACTCT CTCGGTGTAAGTGAGATG 318 0 309 0 200 +CTCGGTGTAAGTGAGATG GACGCGTACACACCGCCC 318 0 309 0 200 +ACACCGCCCGTCACTCTC CTCGGTGTAAGTGAGATG 318 0 309 0 200 +CTCGGTGTAAGTGAGATG GCGTACACACCGCCCGTC 318 0 309 0 200 +ACGCGTACACACCGCCCG CTCGGTGTAAGTGAGATG 318 0 309 0 200 +ATTTACCATGCTCTGCCA TACGAAAGGATCGGAAAA 320 0 311 0 200 +CAAAGGCATGGTCCCGAC CACTGCTGAATTCCCTTG 306 0 298 0 200 +GATTTGCACCAAGAGTTT TATTAGATTGTGATTCTA 308 0 300 0 200 +CAAAGGCATGGTCCCGAC TTCCCTTGGGGGTGTGGC 308 0 300 0 200 +AGGGCTTAACTGTCTCCC GTGGTCGCCCCAACCGAA 293 0 284 0 199 +CGAGGGCTTAACTGTCTC GGTCGCCCCAACCGAAGG 293 0 284 0 199 +CTAGGGTAACTTGGTTCG TTACCTTCGGTTGGGGCG 296 0 287 0 199 +CTAGGGTAACTTGGTTCG TTTTACCTTCGGTTGGGG 296 0 287 0 199 +CAATCCAGGTCAGTTTCT CCATGCTCTGCCACCCCA 314 0 306 0 200 +CTCTGGGAGAGGATTGCG CTTCGGTTGGGGCGACCA 300 0 291 0 199 +CTTCGGTTGGGGCGACCA TCTGGGAGAGGATTGCGC 300 0 291 0 199 +AGGAGGATTTAGTAGTAA TCCAAGTGCACCTTCCGG 319 0 311 0 200 +AGGATTGCGCTGTTATCC CTTCGGTTGGGGCGACCA 301 0 292 0 199 +CTGGGAGAGGATTGCGCT CTTCGGTTGGGGCGACCA 301 0 292 0 199 +AACTTGGTTCGTTGATCG CTTCGGTTGGGGCGACCA 301 0 292 0 199 +CTTCGGTTGGGGCGACCA TAACTTGGTTCGTTGATC 301 0 292 0 199 +CTTCGGTTGGGGCGACCA TGGGAGAGGATTGCGCTG 301 0 292 0 199 +CTTCGGTTGGGGCGACCA GGGAGAGGATTGCGCTGT 301 0 292 0 199 +CGGTTGGGGCGACCACGG CTAGGGTAACTTGGTTCG 301 0 292 0 199 +AGAGGATTGCGCTGTTAT CTTCGGTTGGGGCGACCA 301 0 292 0 199 +CTTCGGTTGGGGCGACCA GAGGATTGCGCTGTTATC 301 0 292 0 199 +CTTCGGTTGGGGCGACCA GATTGCGCTGTTATCCCT 301 0 292 0 199 +GGTAACTTGGTTCGTTGA GTTGGGGCGACCACGGAG 301 0 292 0 199 +CTTCGGTTGGGGCGACCA GGATTGCGCTGTTATCCC 301 0 292 0 199 +ACTCTGGGAGAGGATTGC TTTTACCTTCGGTTGGGG 303 0 294 0 199 +ATTTGCACCAAGAGTTTT TATTAGATTGTGATTCTA 308 0 299 0 199 +CCCTTTGGAGCTTAAGGT CCGTGGTCGCCCCAACCG 312 0 303 0 199 +CCCTTTGGAGCTTAAGGT TCCGTGGTCGCCCCAACC 312 0 303 0 199 +CCTTTGGAGCTTAAGGTA GTCGCCCCAACCGAAGGT 312 0 303 0 199 +CCTTTGGAGCTTAAGGTA GGTCGCCCCAACCGAAGG 312 0 303 0 199 +CCTTTGGAGCTTAAGGTA TGGTCGCCCCAACCGAAG 312 0 303 0 199 +CCGTGGTCGCCCCAACCG CCTTTGGAGCTTAAGGTA 312 0 303 0 199 +GACGAGAAGACCCTTTGG GTCGCCCCAACCGAAGGT 312 0 303 0 199 +CCTTTGGAGCTTAAGGTA TTTCCTCCGTGGTCGCCC 312 0 303 0 199 +CCTTTGGAGCTTAAGGTA TCCGTGGTCGCCCCAACC 312 0 303 0 199 +CAATTTACCATGCTCTGC GTCAGTTTCTATCTGTAA 312 0 303 0 199 +AGACGCGTACACACCGCC GCATGGATGTCTTCTCGG 313 0 304 0 199 +CGGCTCTGAGACGCGTAC GTCTTCTCGGTGTAAGTG 313 0 304 0 199 +AATTTACCATGCTCTGCC CCAGGTCAGTTTCTATCT 314 0 305 0 199 +CACCGCCCGTCACTCTCC CTTCTCGGTGTAAGTGAG 315 0 306 0 199 +CGTACACACCGCCCGTCA TGTCTTCTCGGTGTAAGT 315 0 306 0 199 +ATGTCTTCTCGGTGTAAG CGTACACACCGCCCGTCA 315 0 306 0 199 +ACCGCCCGTCACTCTCCC GGATGTCTTCTCGGTGTA 315 0 306 0 199 +ACCGCCCGTCACTCTCCC TCTTCTCGGTGTAAGTGA 315 0 306 0 199 +ACCGCCCGTCACTCTCCC CTTCTCGGTGTAAGTGAG 315 0 306 0 199 +CTCTGAGACGCGTACACA TGTCTTCTCGGTGTAAGT 315 0 306 0 199 +ATGTCTTCTCGGTGTAAG TCTGAGACGCGTACACAC 315 0 306 0 199 +CACCGCCCGTCACTCTCC GGATGTCTTCTCGGTGTA 315 0 306 0 199 +ATGTCTTCTCGGTGTAAG TGAGACGCGTACACACCG 315 0 306 0 199 +CACCGCCCGTCACTCTCC TCTTCTCGGTGTAAGTGA 315 0 306 0 199 +AGACGCGTACACACCGCC ATGTCTTCTCGGTGTAAG 315 0 306 0 199 +ATGTCTTCTCGGTGTAAG CTGAGACGCGTACACACC 315 0 306 0 199 +ATGTCTTCTCGGTGTAAG GAGACGCGTACACACCGC 315 0 306 0 199 +CTCGGTGTAAGTGAGATG TGAGACGCGTACACACCG 317 0 308 0 199 +CTCGGTGTAAGTGAGATG TCTGAGACGCGTACACAC 317 0 308 0 199 +CTCGGTGTAAGTGAGATG CTGAGACGCGTACACACC 317 0 308 0 199 +AGACGCGTACACACCGCC CTCGGTGTAAGTGAGATG 317 0 308 0 199 +AACCCGGCTCTGAGACGC CGGTGTAAGTGAGATGCT 317 0 308 0 199 +CTCGGTGTAAGTGAGATG GGCTCTGAGACGCGTACA 317 0 308 0 199 +CTCGGTGTAAGTGAGATG GAGACGCGTACACACCGC 317 0 308 0 199 +ACTGAGTCAAGTTTTCAC CAACTTACACATGCAAGT 317 0 308 0 199 +CGGCTCTGAGACGCGTAC CTCGGTGTAAGTGAGATG 317 0 308 0 199 +AATTTACCATGCTCTGCC TACGAAAGGATCGGAAAA 319 0 310 0 199 +ATCACTGCTGAATTCCCT CAAAGGCATGGTCCCGAC 306 0 299 0 200 +CAAAGGCATGGTCCCGAC TCACTGCTGAATTCCCTT 306 0 299 0 200 +CAACTTACACATGCAAGT CTGAGTCAAGTTTTCACT 317 0 309 0 199 +CTAGGGTAACTTGGTTCG GGTTGGGGCGACCACGGA 300 0 291 0 198 +CGGTTGGGGCGACCACGG TCGATATGGACTCTGGGA 305 0 296 0 198 +GACCACTTTGATAGAGTG TTCGGGGTATGGGCCCGA 306 0 297 0 198 +AAGACCCTTTGGAGCTTA GTCGCCCCAACCGAAGGT 311 0 302 0 198 +AGACCCTTTGGAGCTTAA GTCGCCCCAACCGAAGGT 311 0 302 0 198 +GGCTTAGGCCTTTCGCAA TACGAAAGGATCGGAAAA 312 0 303 0 198 +CTTAGGCCTTTCGCAATT TACGAAAGGATCGGAAAA 312 0 303 0 198 +CAATTTACCATGCTCTGC CAGGTCAGTTTCTATCTG 313 0 304 0 198 +ATCCAGGTCAGTTTCTAT ATTTACCATGCTCTGCCA 313 0 304 0 198 +CACCGCCCGTCACTCTCC GTCTTCTCGGTGTAAGTG 314 0 305 0 198 +ACCGCCCGTCACTCTCCC GTCTTCTCGGTGTAAGTG 314 0 305 0 198 +ATGTCTTCTCGGTGTAAG GTACACACCGCCCGTCAC 314 0 305 0 198 +GTACACACCGCCCGTCAC TGTCTTCTCGGTGTAAGT 314 0 305 0 198 +ACCCGGCTCTGAGACGCG CTCGGTGTAAGTGAGATG 316 0 307 0 198 +CGTACACACCGCCCGTCA CTCGGTGTAAGTGAGATG 316 0 307 0 198 +CTCGGTGTAAGTGAGATG CTCTGAGACGCGTACACA 316 0 307 0 198 +CTCGGTGTAAGTGAGATG GCTCTGAGACGCGTACAC 316 0 307 0 198 +TTACCATGCTCTGCCACC TTTTCCTAGTACGAAAGG 317 0 308 0 198 +TTTACCATGCTCTGCCAC TTTTCCTAGTACGAAAGG 317 0 308 0 198 +CAATTTACCATGCTCTGC TACGAAAGGATCGGAAAA 318 0 309 0 198 +CTGAATTCCCTTGGGGGT GGCATGGTCCCGACCTTA 308 0 301 0 199 +GGCATGGTCCCGACCTTA TGAATTCCCTTGGGGGTG 308 0 301 0 199 +AGCAATCCAGGTCAGTTT CCATGCTCTGCCACCCCA 312 0 304 0 198 +ATGGACTCTGGGAGAGGA CTTCGGTTGGGGCGACCA 298 0 289 0 197 +CTTCGGTTGGGGCGACCA TATGGACTCTGGGAGAGG 298 0 289 0 197 +CGGGGTATGGGCCCGAAA GACCACTTTGATAGAGTG 305 0 296 0 197 +GGACTCTGGGAGAGGATT TCGGTTGGGGCGACCACG 306 0 297 0 197 +TATTAGATTGTGATTCTA TTTGCACCAAGAGTTTTT 306 0 297 0 197 +ACTCTGGGAGAGGATTGC TCGGTTGGGGCGACCACG 306 0 297 0 197 +GACTCTGGGAGAGGATTG TCGGTTGGGGCGACCACG 306 0 297 0 197 +TATGGACTCTGGGAGAGG TCGGTTGGGGCGACCACG 306 0 297 0 197 +ATGGACTCTGGGAGAGGA TCGGTTGGGGCGACCACG 306 0 297 0 197 +TCGGTTGGGGCGACCACG TGGACTCTGGGAGAGGAT 306 0 297 0 197 +CCTAGGGTAACTTGGTTC GGGGCGACCACGGAGGAA 307 0 298 0 197 +CCTAGGGTAACTTGGTTC GGGCGACCACGGAGGAAA 307 0 298 0 197 +TACAAGACGAGAAGACCC TCGCCCCAACCGAAGGTA 310 0 301 0 197 +GAGAAGACCCTTTGGAGC TCGCCCCAACCGAAGGTA 310 0 301 0 197 +GAAGACCCTTTGGAGCTT TCGCCCCAACCGAAGGTA 310 0 301 0 197 +AGAAGACCCTTTGGAGCT TCGCCCCAACCGAAGGTA 310 0 301 0 197 +ACAAGACGAGAAGACCCT TCGCCCCAACCGAAGGTA 310 0 301 0 197 +CAAGACGAGAAGACCCTT GTCGCCCCAACCGAAGGT 310 0 301 0 197 +ACGAGAAGACCCTTTGGA TCGCCCCAACCGAAGGTA 310 0 301 0 197 +ATACAAGACGAGAAGACC TCGCCCCAACCGAAGGTA 310 0 301 0 197 +CGAGAAGACCCTTTGGAG TCGCCCCAACCGAAGGTA 310 0 301 0 197 +AATTTACCATGCTCTGCC TCCAGGTCAGTTTCTATC 312 0 303 0 197 +GCTCTGAGACGCGTACAC TGTCTTCTCGGTGTAAGT 313 0 304 0 197 +CTCGGTGTAAGTGAGATG GTACACACCGCCCGTCAC 315 0 306 0 197 +CAAAGGCATGGTCCCGAC TGAATTCCCTTGGGGGTG 304 0 296 0 197 +CAAAGGCATGGTCCCGAC CTGAATTCCCTTGGGGGT 304 0 296 0 197 +AGCAATCCAGGTCAGTTT TGCCACCCCAGTATGTCC 309 0 301 0 197 +CCATGCTCTGCCACCCCA GTCAGTTTCTATCTGTAA 309 0 301 0 197 +ACCTTCGGTTGGGGCGAC TAGGGTAACTTGGTTCGT 293 0 284 0 196 +CTTCGGTTGGGGCGACCA GTAACTTGGTTCGTTGAT 294 0 285 0 196 +CTAGGGTAACTTGGTTCG GTTGGGGCGACCACGGAG 296 0 287 0 196 +GGGCGACCACGGAGGAAA GTAACTTGGTTCGTTGAT 302 0 293 0 196 +GGGGCGACCACGGAGGAA GTAACTTGGTTCGTTGAT 302 0 293 0 196 +GGGCGACCACGGAGGAAA TCGTCGATATGGACTCTG 303 0 294 0 196 +GGTTGGGGCGACCACGGA GTCGATATGGACTCTGGG 303 0 294 0 196 +GACCACTTTGATAGAGTG TCGGGGTATGGGCCCGAA 304 0 295 0 196 +TCGGTTGGGGCGACCACG TCTGGGAGAGGATTGCGC 305 0 296 0 196 +CTCTGGGAGAGGATTGCG TCGGTTGGGGCGACCACG 305 0 296 0 196 +GAGGATTGCGCTGTTATC TCGGTTGGGGCGACCACG 306 0 297 0 196 +GGGAGAGGATTGCGCTGT TCGGTTGGGGCGACCACG 306 0 297 0 196 +TCGGTTGGGGCGACCACG TGGGAGAGGATTGCGCTG 306 0 297 0 196 +TAACTTGGTTCGTTGATC TCGGTTGGGGCGACCACG 306 0 297 0 196 +GATTGCGCTGTTATCCCT TCGGTTGGGGCGACCACG 306 0 297 0 196 +GGATTGCGCTGTTATCCC TCGGTTGGGGCGACCACG 306 0 297 0 196 +AGGATTGCGCTGTTATCC TCGGTTGGGGCGACCACG 306 0 297 0 196 +AACTTGGTTCGTTGATCG TCGGTTGGGGCGACCACG 306 0 297 0 196 +CTGGGAGAGGATTGCGCT TCGGTTGGGGCGACCACG 306 0 297 0 196 +AGAGGATTGCGCTGTTAT TCGGTTGGGGCGACCACG 306 0 297 0 196 +CCTTTGGAGCTTAAGGTA TCGCCCCAACCGAAGGTA 309 0 300 0 196 +ACCCTTTGGAGCTTAAGG TCGCCCCAACCGAAGGTA 309 0 300 0 196 +CCCTTTGGAGCTTAAGGT TCGCCCCAACCGAAGGTA 309 0 300 0 196 +GACCCTTTGGAGCTTAAG TCGCCCCAACCGAAGGTA 309 0 300 0 196 +GGCTCTGAGACGCGTACA TGTCTTCTCGGTGTAAGT 310 0 301 0 196 +GCTTAGGCCTTTCGCAAT TACGAAAGGATCGGAAAA 310 0 301 0 196 +TCCAGGTCAGTTTCTATC TTACCATGCTCTGCCACC 311 0 302 0 196 +CAATTTACCATGCTCTGC CCAGGTCAGTTTCTATCT 311 0 302 0 196 +CCAGGTCAGTTTCTATCT TTTACCATGCTCTGCCAC 311 0 302 0 196 +ATGTCTTCTCGGTGTAAG CTCTGAGACGCGTACACA 312 0 303 0 196 +ACCGCCCGTCACTCTCCC CGGTGTAAGTGAGATGCT 314 0 305 0 196 +CGGTGTAAGTGAGATGCT GAACCCGGCTCTGAGACG 314 0 305 0 196 +CACCGCCCGTCACTCTCC CGGTGTAAGTGAGATGCT 314 0 305 0 196 +ACACATGCAAGTCTCCGC ACTGAGTCAAGTTTTCAC 323 0 314 0 196 +AGGCATGGTCCCGACCTT TCCCTTGGGGGTGTGGCT 300 0 292 0 196 +AAAGGCATGGTCCCGACC TCCCTTGGGGGTGTGGCT 300 0 292 0 196 +AAGGCATGGTCCCGACCT TCCCTTGGGGGTGTGGCT 300 0 292 0 196 +ACCGGAGCAATCCAGGTC TGCCACCCCAGTATGTCC 308 0 300 0 196 +GAGCAATCCAGGTCAGTT TGCCACCCCAGTATGTCC 308 0 300 0 196 +CCATGCTCTGCCACCCCA TACGAAAGGATCGGAAAA 315 0 307 0 196 +ACACATGCAAGTCTCCGC CTGAGTCAAGTTTTCACT 323 0 315 0 196 +GACGAGAAGACCCTTTGG TCGCCCCAACCGAAGGTA 308 0 299 0 195 +CACCGCCCGTCACTCTCC TGTCTTCTCGGTGTAAGT 311 0 302 0 195 +ACCGCCCGTCACTCTCCC TGTCTTCTCGGTGTAAGT 311 0 302 0 195 +ACCGCCCGTCACTCTCCC ATGTCTTCTCGGTGTAAG 311 0 302 0 195 +ATGTCTTCTCGGTGTAAG CACCGCCCGTCACTCTCC 311 0 302 0 195 +AGGAGGATTTAGTAGTAA TTACCGTGTTACGACTTG 315 0 306 0 195 +ACAGAGACGGGTTGGCCC CCGCAAGGGAAAGCTGAA 318 0 309 0 195 +TCTAGTGGGCCTAAAAGC TTCTTGTTACTCATTTTA 320 0 311 0 195 +ACTGAGTCAAGTTTTCAC CTTACACATGCAAGTCTC 322 0 313 0 195 +CAGGTCAGTTTCTATCTG TGCCACCCCAGTATGTCC 307 0 299 0 195 +GGAGCAATCCAGGTCAGT TGCCACCCCAGTATGTCC 307 0 299 0 195 +CAATCCAGGTCAGTTTCT TGCCACCCCAGTATGTCC 307 0 299 0 195 +AATCCAGGTCAGTTTCTA TGCCACCCCAGTATGTCC 307 0 299 0 195 +ATCCAGGTCAGTTTCTAT TGCCACCCCAGTATGTCC 307 0 299 0 195 +TCCAGGTCAGTTTCTATC TGCCACCCCAGTATGTCC 307 0 299 0 195 +CCAGGTCAGTTTCTATCT TGCCACCCCAGTATGTCC 307 0 299 0 195 +AGGTCAGTTTCTATCTGT TGCCACCCCAGTATGTCC 307 0 299 0 195 +GGTCAGTTTCTATCTGTA TGCCACCCCAGTATGTCC 307 0 299 0 195 +CCATGCTCTGCCACCCCA GAGCAATCCAGGTCAGTT 309 0 301 0 195 +CCTAGGGTAACTTGGTTC CTTCGGTTGGGGCGACCA 296 0 287 0 194 +ATATGGACTCTGGGAGAG CGGTTGGGGCGACCACGG 296 0 287 0 194 +CGTCGATATGGACTCTGG GGGGCGACCACGGAGGAA 301 0 292 0 194 +CGTCGATATGGACTCTGG GGGCGACCACGGAGGAAA 301 0 292 0 194 +CTGAGTCAAGTTTTCACT CTTACACATGCAAGTCTC 322 0 314 0 195 +GTCAGTTTCTATCTGTAA TTTACCATGCTCTGCCAC 307 0 298 0 194 +GTCAGTTTCTATCTGTAA TTACCATGCTCTGCCACC 307 0 298 0 194 +AGACCCTTTGGAGCTTAA TCGCCCCAACCGAAGGTA 307 0 298 0 194 +AAGACCCTTTGGAGCTTA TCGCCCCAACCGAAGGTA 307 0 298 0 194 +ATGTCTTCTCGGTGTAAG GCTCTGAGACGCGTACAC 308 0 299 0 194 +TCCAGGTCAGTTTCTATC TTTACCATGCTCTGCCAC 309 0 300 0 194 +ATCCAGGTCAGTTTCTAT TTACCATGCTCTGCCACC 309 0 300 0 194 +AGGAGGATTTAGTAGTAA GTTACGACTTGCCTCCCC 316 0 307 0 194 +ATATGGACTCTGGGAGAG GGTTGGGGCGACCACGGA 295 0 286 0 193 +GATATGGACTCTGGGAGA GGTTGGGGCGACCACGGA 295 0 286 0 193 +GGGGCGACCACGGAGGAA GGGTAACTTGGTTCGTTG 297 0 288 0 193 +GGGCGACCACGGAGGAAA GGGTAACTTGGTTCGTTG 297 0 288 0 193 +CCTATATACCGCCGTCGT TTACTACTAAATCCTCCT 298 0 289 0 193 +GGGGCGACCACGGAGGAA TCGTCGATATGGACTCTG 300 0 291 0 193 +CAAGACGAGAAGACCCTT TCGCCCCAACCGAAGGTA 306 0 297 0 193 +ACCGCCCGTCACTCTCCC CTCGGTGTAAGTGAGATG 311 0 302 0 193 +CGGTGTAAGTGAGATGCT TGAACCCGGCTCTGAGAC 311 0 302 0 193 +CACCGCCCGTCACTCTCC CTCGGTGTAAGTGAGATG 311 0 302 0 193 +AGTCAAGTTTTCACTTAT CAACTTACACATGCAAGT 318 0 309 0 193 +ACTCTTTTGCCACAGAGA GCTGAAAGAGAAATGAAA 325 0 316 0 193 +AGCAATCCAGGTCAGTTT CTGCCACCCCAGTATGTC 305 0 297 0 193 +CGGTTGGGGCGACCACGG TAGGGTAACTTGGTTCGT 294 0 285 0 192 +CGGTTGGGGCGACCACGG GATATGGACTCTGGGAGA 294 0 285 0 192 +ACCACTTTGATAGAGTGA TTCGGGGTATGGGCCCGA 300 0 291 0 192 +CCTAGGGTAACTTGGTTC TCGGTTGGGGCGACCACG 302 0 293 0 192 +CCGGCTCTGAGACGCGTA CTTCTCGGTGTAAGTGAG 306 0 297 0 192 +AATCCAGGTCAGTTTCTA ATTTACCATGCTCTGCCA 307 0 298 0 192 +AATCCAGGTCAGTTTCTA TTACCATGCTCTGCCACC 307 0 298 0 192 +ATCCAGGTCAGTTTCTAT TTTACCATGCTCTGCCAC 307 0 298 0 192 +CCGGCTCTGAGACGCGTA CGGTGTAAGTGAGATGCT 308 0 299 0 192 +CGGTGTAAGTGAGATGCT TTGAACCCGGCTCTGAGA 308 0 299 0 192 +AACCCGGCTCTGAGACGC CTCGGTGTAAGTGAGATG 310 0 301 0 192 +TACGAAAGGATCGGAAAA TTTACCATGCTCTGCCAC 312 0 303 0 192 +TACGAAAGGATCGGAAAA TTACCATGCTCTGCCACC 312 0 303 0 192 +CGGAGCAATCCAGGTCAG TGCCACCCCAGTATGTCC 304 0 296 0 192 +GACCGGAGCAATCCAGGT TGCCACCCCAGTATGTCC 304 0 296 0 192 +CCGGAGCAATCCAGGTCA TGCCACCCCAGTATGTCC 304 0 296 0 192 +CTGCCACCCCAGTATGTC GAGCAATCCAGGTCAGTT 304 0 296 0 192 +TGCCACCCCAGTATGTCC TTTTCCTAGTACGAAAGG 308 0 300 0 192 +GGTTGGGGCGACCACGGA TAGGGTAACTTGGTTCGT 293 0 284 0 191 +CTTCTTGTTACTCATTTT TCTAGTGGGCCTAAAAGC 317 0 309 0 192 +GTAACTTGGTTCGTTGAT TCGGTTGGGGCGACCACG 297 0 288 0 191 +ACCACTTTGATAGAGTGA CGGGGTATGGGCCCGAAA 299 0 290 0 191 +AATCCAGGTCAGTTTCTA TTTACCATGCTCTGCCAC 306 0 297 0 191 +CAATCCAGGTCAGTTTCT TTACCATGCTCTGCCACC 306 0 297 0 191 +AATTTACCATGCTCTGCC ATCCAGGTCAGTTTCTAT 306 0 297 0 191 +CCCGGCTCTGAGACGCGT CGGTGTAAGTGAGATGCT 307 0 298 0 191 +CTTTTGCCACAGAGACGG GCTGAAAGAGAAATGAAA 323 0 314 0 191 +CACTCTTTTGCCACAGAG GCTGAAAGAGAAATGAAA 323 0 314 0 191 +GGCATGGTCCCGACCTTA TCCCTTGGGGGTGTGGCT 295 0 287 0 191 +CTGCCACCCCAGTATGTC GGTCAGTTTCTATCTGTA 303 0 295 0 191 +CTGCCACCCCAGTATGTC GGAGCAATCCAGGTCAGT 303 0 295 0 191 +CAATCCAGGTCAGTTTCT CTGCCACCCCAGTATGTC 303 0 295 0 191 +AATCCAGGTCAGTTTCTA CTGCCACCCCAGTATGTC 303 0 295 0 191 +ATCCAGGTCAGTTTCTAT CTGCCACCCCAGTATGTC 303 0 295 0 191 +CTGCCACCCCAGTATGTC TCCAGGTCAGTTTCTATC 303 0 295 0 191 +CCAGGTCAGTTTCTATCT CTGCCACCCCAGTATGTC 303 0 295 0 191 +CAGGTCAGTTTCTATCTG CTGCCACCCCAGTATGTC 303 0 295 0 191 +AGGTCAGTTTCTATCTGT CTGCCACCCCAGTATGTC 303 0 295 0 191 +GGGTAACTTGGTTCGTTG TCGGTTGGGGCGACCACG 294 0 285 0 190 +ACCACTTTGATAGAGTGA TCGGGGTATGGGCCCGAA 298 0 289 0 190 +GCTGAAAGAGAAATGAAA TTGCCACAGAGACGGGTT 323 0 315 0 191 +GCTGAAAGAGAAATGAAA TTTGCCACAGAGACGGGT 323 0 315 0 191 +CCGGCTCTGAGACGCGTA TCTTCTCGGTGTAAGTGA 302 0 293 0 190 +CAATTTACCATGCTCTGC TCCAGGTCAGTTTCTATC 305 0 296 0 190 +CTGTCTCGGGGTAGCTCA TACCGCAAGGGAAAGCTG 321 0 312 0 190 +CCGCAAGGGAAAGCTGAA CTGTCTCGGGGTAGCTCA 321 0 312 0 190 +ACCGCAAGGGAAAGCTGA CTGTCTCGGGGTAGCTCA 321 0 312 0 190 +AGGCTGTCTCGGGGTAGC TACCGCAAGGGAAAGCTG 322 0 313 0 190 +ACCGCAAGGGAAAGCTGA AGGCTGTCTCGGGGTAGC 322 0 313 0 190 +AGGCTGTCTCGGGGTAGC CCGCAAGGGAAAGCTGAA 322 0 313 0 190 +GCTGAAAGAGAAATGAAA TTTTGCCACAGAGACGGG 322 0 313 0 190 +GGCTGTCTCGGGGTAGCT TACCGCAAGGGAAAGCTG 323 0 314 0 190 +ACCGCAAGGGAAAGCTGA GGCTGTCTCGGGGTAGCT 323 0 314 0 190 +GCTGTCTCGGGGTAGCTC TACCGCAAGGGAAAGCTG 323 0 314 0 190 +CCGCAAGGGAAAGCTGAA GGCTGTCTCGGGGTAGCT 323 0 314 0 190 +CCGCAAGGGAAAGCTGAA GCTGTCTCGGGGTAGCTC 323 0 314 0 190 +ACCGCAAGGGAAAGCTGA GCTGTCTCGGGGTAGCTC 323 0 314 0 190 +ACACATGCAAGTCTCCGC AGTCAAGTTTTCACTTAT 324 0 315 0 190 +AGACCGGAGCAATCCAGG TGCCACCCCAGTATGTCC 302 0 294 0 190 +GTCAGTTTCTATCTGTAA TGCCACCCCAGTATGTCC 302 0 294 0 190 +GGGAAAGCTGAAAGAGAA TTGCCACAGAGACGGGTT 309 0 301 0 190 +GGGGCGACCACGGAGGAA GGTAACTTGGTTCGTTGA 293 0 284 0 189 +GGGCGACCACGGAGGAAA GGTAACTTGGTTCGTTGA 293 0 284 0 189 +CGTCGATATGGACTCTGG GTTGGGGCGACCACGGAG 294 0 285 0 189 +CCACAGAGACGGGTTGGC GCTGAAAGAGAAATGAAA 322 0 314 0 190 +CCCGGCTCTGAGACGCGT CTTCTCGGTGTAAGTGAG 301 0 292 0 189 +CCGGCTCTGAGACGCGTA CTCGGTGTAAGTGAGATG 305 0 296 0 189 +GCAATTTACCATGCTCTG TTTTCCTAGTACGAAAGG 308 0 299 0 189 +AGGAGGATTTAGTAGTAA TGTTACGACTTGCCTCCC 311 0 302 0 189 +ACCGCAAGGGAAAGCTGA ATAGGCTGTCTCGGGGTA 321 0 312 0 189 +AATAGGCTGTCTCGGGGT TACCGCAAGGGAAAGCTG 321 0 312 0 189 +ATAGGCTGTCTCGGGGTA TACCGCAAGGGAAAGCTG 321 0 312 0 189 +TACCGCAAGGGAAAGCTG TAGGCTGTCTCGGGGTAG 321 0 312 0 189 +AATAGGCTGTCTCGGGGT ACCGCAAGGGAAAGCTGA 321 0 312 0 189 +ACCGCAAGGGAAAGCTGA TAATAGGCTGTCTCGGGG 321 0 312 0 189 +CCGCAAGGGAAAGCTGAA TAGGCTGTCTCGGGGTAG 321 0 312 0 189 +AATAGGCTGTCTCGGGGT CCGCAAGGGAAAGCTGAA 321 0 312 0 189 +ACCGCAAGGGAAAGCTGA TAGGCTGTCTCGGGGTAG 321 0 312 0 189 +TAATAGGCTGTCTCGGGG TACCGCAAGGGAAAGCTG 321 0 312 0 189 +CCGCAAGGGAAAGCTGAA TAATAGGCTGTCTCGGGG 321 0 312 0 189 +ATAGGCTGTCTCGGGGTA CCGCAAGGGAAAGCTGAA 321 0 312 0 189 +AGTCAAGTTTTCACTTAT CTTACACATGCAAGTCTC 323 0 314 0 189 +CCACAGAGACGGGTTGGC GGGAAAGCTGAAAGAGAA 308 0 300 0 189 +GGGAAAGCTGAAAGAGAA TTTGCCACAGAGACGGGT 308 0 300 0 189 +AGTTTCTATCTGTAACGC ATTTACCATGCTCTGCCA 299 0 290 0 188 +GACGCGTACACACCGCCC TTGCATGGATGTCTTCTC 302 0 293 0 188 +CCCGGCTCTGAGACGCGT CTCGGTGTAAGTGAGATG 304 0 295 0 188 +ACTGAGTCAAGTTTTCAC CACATGCAAGTCTCCGCA 315 0 306 0 188 +ACAGAGACGGGTTGGCCC GCTGAAAGAGAAATGAAA 320 0 311 0 188 +CAGGTCAGTTTCTATCTG CATGCTCTGCCACCCCAG 296 0 288 0 188 +CATGCTCTGCCACCCCAG CCAGGTCAGTTTCTATCT 296 0 288 0 188 +CATGCTCTGCCACCCCAG TCCAGGTCAGTTTCTATC 296 0 288 0 188 +ATCCAGGTCAGTTTCTAT CATGCTCTGCCACCCCAG 296 0 288 0 188 +AATCCAGGTCAGTTTCTA CATGCTCTGCCACCCCAG 296 0 288 0 188 +CAATCCAGGTCAGTTTCT CATGCTCTGCCACCCCAG 296 0 288 0 188 +AGGTCAGTTTCTATCTGT CATGCTCTGCCACCCCAG 296 0 288 0 188 +CATGCTCTGCCACCCCAG GGTCAGTTTCTATCTGTA 296 0 288 0 188 +CAGACCGGAGCAATCCAG TGCCACCCCAGTATGTCC 300 0 292 0 188 +ACCGGAGCAATCCAGGTC CTGCCACCCCAGTATGTC 300 0 292 0 188 +CCGGAGCAATCCAGGTCA CTGCCACCCCAGTATGTC 300 0 292 0 188 +CGGAGCAATCCAGGTCAG CTGCCACCCCAGTATGTC 300 0 292 0 188 +CCATGCTCTGCCACCCCA GGAGCAATCCAGGTCAGT 302 0 294 0 188 +CTGCCACCCCAGTATGTC TTTTCCTAGTACGAAAGG 304 0 296 0 188 +CGATATGGACTCTGGGAG TCGGTTGGGGCGACCACG 294 0 285 0 187 +CACATGCAAGTCTCCGCA CTGAGTCAAGTTTTCACT 315 0 307 0 188 +AATTTACCATGCTCTGCC AGTTTCTATCTGTAACGC 298 0 289 0 187 +AGTTTCTATCTGTAACGC CAATTTACCATGCTCTGC 298 0 289 0 187 +CTCGGTGTAAGTGAGATG GAACCCGGCTCTGAGACG 303 0 294 0 187 +ACAGAGACGGGTTGGCCC GGGAAAGCTGAAAGAGAA 306 0 297 0 187 +ATGCTCTGCCACCCCAGT GGTCAGTTTCTATCTGTA 295 0 287 0 187 +AATCCAGGTCAGTTTCTA ATGCTCTGCCACCCCAGT 295 0 287 0 187 +ATGCTCTGCCACCCCAGT CAATCCAGGTCAGTTTCT 295 0 287 0 187 +ATGCTCTGCCACCCCAGT CCAGGTCAGTTTCTATCT 295 0 287 0 187 +ATGCTCTGCCACCCCAGT CAGGTCAGTTTCTATCTG 295 0 287 0 187 +AGGTCAGTTTCTATCTGT ATGCTCTGCCACCCCAGT 295 0 287 0 187 +ATGCTCTGCCACCCCAGT TCCAGGTCAGTTTCTATC 295 0 287 0 187 +ATCCAGGTCAGTTTCTAT ATGCTCTGCCACCCCAGT 295 0 287 0 187 +CTGCCACCCCAGTATGTC GTCAGTTTCTATCTGTAA 299 0 291 0 187 +TACGAAAGGATCGGAAAA TGCCACCCCAGTATGTCC 304 0 296 0 187 +CCTCCGTGGTCGCCCCAA CTTTGGAGCTTAAGGTAC 295 0 286 0 186 +CTCCGTGGTCGCCCCAAC CTTTGGAGCTTAAGGTAC 295 0 286 0 186 +CTTTGGAGCTTAAGGTAC GTGGTCGCCCCAACCGAA 295 0 286 0 186 +CTCCGTGGTCGCCCCAAC TTTGGAGCTTAAGGTACA 295 0 286 0 186 +CCTCCGTGGTCGCCCCAA TTTGGAGCTTAAGGTACA 295 0 286 0 186 +GTGGTCGCCCCAACCGAA TTTGGAGCTTAAGGTACA 295 0 286 0 186 +CGTGGTCGCCCCAACCGA TTTGGAGCTTAAGGTACA 295 0 286 0 186 +CGTGGTCGCCCCAACCGA CTTTGGAGCTTAAGGTAC 295 0 286 0 186 +CACAGAGACGGGTTGGCC GCTGAAAGAGAAATGAAA 319 0 311 0 187 +CTCTGAGACGCGTACACA GGATGTCTTCTCGGTGTA 300 0 291 0 186 +GCAATTTACCATGCTCTG GGTCAGTTTCTATCTGTA 301 0 292 0 186 +CAATCCAGGTCAGTTTCT TTTACCATGCTCTGCCAC 301 0 292 0 186 +TCGCAATTTACCATGCTC TTTTCCTAGTACGAAAGG 305 0 296 0 186 +TTCGCAATTTACCATGCT TTTTCCTAGTACGAAAGG 305 0 296 0 186 +CTCTTTTGCCACAGAGAC GCTGAAAGAGAAATGAAA 318 0 309 0 186 +GCTGAAAGAGAAATGAAA TCTTTTGCCACAGAGACG 318 0 309 0 186 +CTGCCACCCCAGTATGTC GACCGGAGCAATCCAGGT 298 0 290 0 186 +TCAGACCGGAGCAATCCA TGCCACCCCAGTATGTCC 298 0 290 0 186 +CACAGAGACGGGTTGGCC GGGAAAGCTGAAAGAGAA 305 0 297 0 186 +CAACTTACACATGCAAGT CAAGTTTTCACTTATGGC 311 0 303 0 186 +TTTCCTCCGTGGTCGCCC TTTGGAGCTTAAGGTACA 294 0 285 0 185 +CTTTGGAGCTTAAGGTAC GTCGCCCCAACCGAAGGT 294 0 285 0 185 +CTTTGGAGCTTAAGGTAC GGTCGCCCCAACCGAAGG 294 0 285 0 185 +CTTTGGAGCTTAAGGTAC TGGTCGCCCCAACCGAAG 294 0 285 0 185 +CTTTGGAGCTTAAGGTAC TCCTCCGTGGTCGCCCCA 294 0 285 0 185 +CTTTGGAGCTTAAGGTAC TTCCTCCGTGGTCGCCCC 294 0 285 0 185 +CTTTGGAGCTTAAGGTAC TTTCCTCCGTGGTCGCCC 294 0 285 0 185 +GTCGCCCCAACCGAAGGT TTTGGAGCTTAAGGTACA 294 0 285 0 185 +GGTCGCCCCAACCGAAGG TTTGGAGCTTAAGGTACA 294 0 285 0 185 +TGGTCGCCCCAACCGAAG TTTGGAGCTTAAGGTACA 294 0 285 0 185 +TCCTCCGTGGTCGCCCCA TTTGGAGCTTAAGGTACA 294 0 285 0 185 +TTCCTCCGTGGTCGCCCC TTTGGAGCTTAAGGTACA 294 0 285 0 185 +ACCCGGCTCTGAGACGCG CTTCTCGGTGTAAGTGAG 299 0 290 0 185 +TTTCGCAATTTACCATGC TTTTCCTAGTACGAAAGG 304 0 295 0 185 +CTGTTCTAGTGGGCCTAA TTCTTGTTACTCATTTTA 309 0 300 0 185 +CTGTCTCGGGGTAGCTCA GTACCGCAAGGGAAAGCT 316 0 307 0 185 +AGGCTGTCTCGGGGTAGC GTACCGCAAGGGAAAGCT 317 0 308 0 185 +CATGCTCTGCCACCCCAG GTCAGTTTCTATCTGTAA 293 0 285 0 185 +ATGCTCTGCCACCCCAGT GTCAGTTTCTATCTGTAA 293 0 285 0 185 +AGCAATCCAGGTCAGTTT ATGCTCTGCCACCCCAGT 293 0 285 0 185 +AGTTTCTATCTGTAACGC CCATGCTCTGCCACCCCA 295 0 287 0 185 +CATGCTCTGCCACCCCAG TTTTCCTAGTACGAAAGG 297 0 289 0 185 +CTTTGGAGCTTAAGGTAC TCGCCCCAACCGAAGGTA 293 0 284 0 184 +CCGTGGTCGCCCCAACCG CTTTGGAGCTTAAGGTAC 293 0 284 0 184 +CTTTGGAGCTTAAGGTAC TCCGTGGTCGCCCCAACC 293 0 284 0 184 +TCGCCCCAACCGAAGGTA TTTGGAGCTTAAGGTACA 293 0 284 0 184 +TCCGTGGTCGCCCCAACC TTTGGAGCTTAAGGTACA 293 0 284 0 184 +CCGTGGTCGCCCCAACCG TTTGGAGCTTAAGGTACA 293 0 284 0 184 +GAGACGCGTACACACCGC GCATGGATGTCTTCTCGG 298 0 289 0 184 +AGACGCGTACACACCGCC TGCATGGATGTCTTCTCG 298 0 289 0 184 +AGGTCAGTTTCTATCTGT GCAATTTACCATGCTCTG 299 0 290 0 184 +GCAATTTACCATGCTCTG TACGAAAGGATCGGAAAA 304 0 295 0 184 +AGGAGGATTTAGTAGTAA TACGACTTGCCTCCCCTT 304 0 295 0 184 +TAAGTAGAAGTGAAAATC TGAGCGCTTAGCTGTTAA 311 0 302 0 184 +TAAGTAGAAGTGAAAATC TTGAGCGCTTAGCTGTTA 311 0 302 0 184 +AGACCGGAGCAATCCAGG CTGCCACCCCAGTATGTC 296 0 288 0 184 +ATGCTCTGCCACCCCAGT TTTTCCTAGTACGAAAGG 296 0 288 0 184 +GGCCTTTCGCAATTTACC TTTTCCTAGTACGAAAGG 302 0 293 0 183 +AGCGCTTAGCTGTTAACT TAAGTAGAAGTGAAAATC 310 0 301 0 183 +GCGCTTAGCTGTTAACTA TAAGTAGAAGTGAAAATC 310 0 301 0 183 +GAGCGCTTAGCTGTTAAC TAAGTAGAAGTGAAAATC 310 0 301 0 183 +AGTACCGCAAGGGAAAGC CTGTCTCGGGGTAGCTCA 314 0 305 0 183 +CTGCCACCCCAGTATGTC TACGAAAGGATCGGAAAA 300 0 292 0 183 +CCACAGAGACGGGTTGGC GCAAGGGAAAGCTGAAAG 304 0 296 0 183 +GCAATTTACCATGCTCTG GTCAGTTTCTATCTGTAA 295 0 286 0 182 +CGGCTCTGAGACGCGTAC TGTCTTCTCGGTGTAAGT 296 0 287 0 182 +CAGGTCAGTTTCTATCTG GCAATTTACCATGCTCTG 297 0 288 0 182 +ACAGAGACGGGTTGGCCC GCAAGGGAAAGCTGAAAG 303 0 294 0 182 +CTGTCTCGGGGTAGCTCA GGGAAAGCTGAAAGAGAA 308 0 299 0 182 +AATAGGCTGTCTCGGGGT GGGAAAGCTGAAAGAGAA 309 0 300 0 182 +AGGCTGTCTCGGGGTAGC GGGAAAGCTGAAAGAGAA 309 0 300 0 182 +GGGAAAGCTGAAAGAGAA TAATAGGCTGTCTCGGGG 309 0 300 0 182 +ATAGGCTGTCTCGGGGTA GGGAAAGCTGAAAGAGAA 309 0 300 0 182 +GCTGTCTCGGGGTAGCTC GGGAAAGCTGAAAGAGAA 310 0 301 0 182 +GGCTGTCTCGGGGTAGCT GGGAAAGCTGAAAGAGAA 310 0 301 0 182 +CAGACCGGAGCAATCCAG CTGCCACCCCAGTATGTC 294 0 286 0 182 +AAGTTTTCACTTATGGCT CAACTTACACATGCAAGT 307 0 299 0 182 +CTGTTCTAGTGGGCCTAA CTTCTTGTTACTCATTTT 308 0 300 0 182 +ATGTCTTCTCGGTGTAAG GGCTCTGAGACGCGTACA 295 0 286 0 181 +TACGAAAGGATCGGAAAA TCGCAATTTACCATGCTC 301 0 292 0 181 +TACGAAAGGATCGGAAAA TTCGCAATTTACCATGCT 301 0 292 0 181 +GGGAAAGCTGAAAGAGAA TAGGCTGTCTCGGGGTAG 308 0 299 0 181 +CACAGAGACGGGTTGGCC GCAAGGGAAAGCTGAAAG 302 0 294 0 181 +AACTTACACATGCAAGTC ACTGAGTCAAGTTTTCAC 293 0 284 0 180 +CGGTGTAAGTGAGATGCT TTTGAACCCGGCTCTGAG 296 0 287 0 180 +AGGCCTTTCGCAATTTAC TTTTCCTAGTACGAAAGG 299 0 290 0 180 +GCCTTTCGCAATTTACCA TTTTCCTAGTACGAAAGG 299 0 290 0 180 +TACGAAAGGATCGGAAAA TTTCGCAATTTACCATGC 300 0 291 0 180 +AACTTACACATGCAAGTC CTGAGTCAAGTTTTCACT 293 0 285 0 180 +CATGCTCTGCCACCCCAG TACGAAAGGATCGGAAAA 293 0 285 0 180 +GGTCAGTTTCTATCTGTA TCGCAATTTACCATGCTC 294 0 285 0 179 +CCAGGTCAGTTTCTATCT GCAATTTACCATGCTCTG 294 0 285 0 179 +CTTTCGCAATTTACCATG TTTTCCTAGTACGAAAGG 298 0 289 0 179 +GGCCTTTCGCAATTTACC TACGAAAGGATCGGAAAA 298 0 289 0 178 +CAACTTACACATGCAAGT GTCAAGTTTTCACTTATG 303 0 294 0 178 +CAACTTACACATGCAAGT TCAAGTTTTCACTTATGG 302 0 293 0 177 +ACCTTCGGTTGGGGCGAC TCAGATGTTCTGTGGCTT 296 0 291 0 179 +AGGAGGATTTAGTAGTAA CGACTTGCCTCCCCTTGT 295 0 286 0 176 +CCTTTCGCAATTTACCAT TTTTCCTAGTACGAAAGG 295 0 286 0 176 +AGGCCTTTCGCAATTTAC TACGAAAGGATCGGAAAA 295 0 286 0 175 +GCCTTTCGCAATTTACCA TACGAAAGGATCGGAAAA 295 0 286 0 175 +CTTTCGCAATTTACCATG TACGAAAGGATCGGAAAA 295 0 286 0 175 +TACCTTCGGTTGGGGCGA TCAGATGTTCTGTGGCTT 294 0 289 0 177 +CCTTCGGTTGGGGCGACC TCAGATGTTCTGTGGCTT 294 0 289 0 177 +TCAGATGTTCTGTGGCTT TTTACCTTCGGTTGGGGC 294 0 289 0 177 +AACTTACACATGCAAGTC AGTCAAGTTTTCACTTAT 294 0 285 0 174 +TCAGATGTTCTGTGGCTT TTTTACCTTCGGTTGGGG 293 0 288 0 176 +CGGTTGGGGCGACCACGG TCAGATGTTCTGTGGCTT 297 0 292 0 176 +GGTTGGGGCGACCACGGA TCAGATGTTCTGTGGCTT 296 0 291 0 175 diff --git a/pkg/obitools/obiprimers/obiprimers.go b/pkg/obitools/obiprimers/obiprimers.go index 0362434..399d212 100644 --- a/pkg/obitools/obiprimers/obiprimers.go +++ b/pkg/obitools/obiprimers/obiprimers.go @@ -53,6 +53,10 @@ func CLIPrimerPairs(sequences obiiter.IBioSequence) ([]obiprimer.PairStats, erro pairs, info, err := obiprimer.Search(corpus, taxa, params) if info != nil { log.Infof("%d strict words (found without error in at least %d examples)", info.StrictWords, info.MinStrict) + if params.NoMultiMatch { + log.Infof("no multi-match: %d strict words dropped as repeated, %d words dropped for matching several times in a sequence", + info.Repeated, info.MultiMatch) + } log.Infof("%d approximate occurrences; %d conserved primers, %d good", info.Matches, info.Pair.Primers, info.Pair.GoodPrimers) log.Infof("%d candidate pairs, %d amplifying enough examples, %d retained", diff --git a/pkg/obitools/obiprimers/obiprimers_test.go b/pkg/obitools/obiprimers/obiprimers_test.go index 9c33a27..b31474f 100644 --- a/pkg/obitools/obiprimers/obiprimers_test.go +++ b/pkg/obitools/obiprimers/obiprimers_test.go @@ -20,7 +20,7 @@ func samplePairs() []obiprimer.PairStats { } func TestPairsToCSVIterator(t *testing.T) { - it := PairsToCSVIterator(samplePairs()) + it := PairsToCSVIterator(samplePairs(), false) header := it.Header() if len(header) != len(csvColumns) || header[0] != "index" || header[1] != "primer1" { @@ -60,7 +60,7 @@ func TestPairsToCSVIterator(t *testing.T) { } func TestEmptyResultStillHasAHeader(t *testing.T) { - it := PairsToCSVIterator(nil) + it := PairsToCSVIterator(nil, false) if len(it.Header()) != len(csvColumns) { t.Fatalf("header %v", it.Header()) } @@ -88,6 +88,9 @@ func TestEveryColumnIsProduced(t *testing.T) { } func TestCLISearchParams(t *testing.T) { + savedMethod, savedSalt := _saltMethod, _salt + defer func() { _saltMethod, _salt = savedMethod, savedSalt }() + p, err := CLISearchParams() if err != nil { t.Fatal(err) @@ -113,3 +116,47 @@ func TestCLISearchParams(t *testing.T) { t.Error("a negative salt concentration must be refused") } } + +func TestReferenceColumns(t *testing.T) { + pairs := samplePairs() + pairs[0].RefLocation = "join(61..78,119..136)" + pairs[0].RefAmplicon = "acgt" + + it := PairsToCSVIterator(pairs, true) + header := it.Header() + n := len(csvColumns) + if len(header) != n+2 || header[n] != "reference_location" || header[n+1] != "reference_amplicon" { + t.Fatalf("header %v", header) + } + + var records []map[string]interface{} + for it.Next() { + batch := it.Get() + for _, r := range batch.Slice() { + records = append(records, r) + } + } + if records[0]["reference_location"] != "join(61..78,119..136)" || records[0]["reference_amplicon"] != "acgt" { + t.Errorf("reference columns of the first pair: %v", records[0]) + } + if _, present := records[1]["reference_location"]; present { + t.Error("a pair that does not amplify the reference must have no location (NA)") + } + + // without a reference the columns are absent + if got := len(PairsToCSVIterator(pairs, false).Header()); got != n { + t.Errorf("%d columns without reference, want %d", got, n) + } +} + +func TestSearchParamsCarryTheNewOptions(t *testing.T) { + _noMultiMatch, _reference = true, "NC_000001" + defer func() { _noMultiMatch, _reference = false, "" }() + p, err := CLISearchParams() + if err != nil { + t.Fatal(err) + } + if !p.NoMultiMatch || p.Reference != "NC_000001" || CLIReference() != "NC_000001" { + t.Errorf("-U/-R not carried: %+v", p) + } +} diff --git a/pkg/obitools/obiprimers/options.go b/pkg/obitools/obiprimers/options.go index 84cb611..985b505 100644 --- a/pkg/obitools/obiprimers/options.go +++ b/pkg/obitools/obiprimers/options.go @@ -28,6 +28,8 @@ var _minLength = 0 var _maxLength = 1000 var _threePrime = 0 var _circular = false +var _noMultiMatch = false +var _reference = "" var _strictQuorum = 0.7 var _sensitivity = 0.9 @@ -72,6 +74,17 @@ func OptionSet(options *getoptions.GetOpt) { options.Alias("c"), options.Description("Considers the sequences as circular (whole organelle genomes).")) + options.BoolVar(&_noMultiMatch, "no-multi-match", _noMultiMatch, + options.Alias("U"), + options.Description("Discards the primers that match, with the allowed mismatches, more than once "+ + "in any sequence (examples and counterexamples, both strands).")) + + options.StringVar(&_reference, "reference", _reference, + options.Alias("R"), + options.ArgName("SEQUENCE_ID"), + options.Description("Identifier of an example sequence taken as reference: for each pair amplifying it, "+ + "adds the location of the primer sites and the amplified sequence.")) + options.IntVar(&_primerLength, "primer-length", _primerLength, options.ArgName("LENGTH"), options.Description("Size of the primers, from 2 to 31.")) @@ -146,6 +159,9 @@ func CLIIgnoreTaxa() []string { return _ignore } // CLIExceptionTaxa returns the taxids of the sub-clades turned into counterexamples. func CLIExceptionTaxa() []string { return _except } +// CLIReference returns the identifier of the reference sequence, or "". +func CLIReference() string { return _reference } + // CLIRank returns the taxonomic rank of the coverage and specificity indices. func CLIRank() string { return _rank } @@ -176,6 +192,8 @@ func CLISearchParams() (obiprimer.SearchParams, error) { FalsePositive: _falsePositive, MinSpecificity: _specificity, Circular: _circular, + NoMultiMatch: _noMultiMatch, + Reference: _reference, Partitions: _partitions, Model: model, }, nil diff --git a/pkg/obitools/obiprimers/output.go b/pkg/obitools/obiprimers/output.go index 49c3dc6..c2f987d 100644 --- a/pkg/obitools/obiprimers/output.go +++ b/pkg/obitools/obiprimers/output.go @@ -24,6 +24,9 @@ var csvColumns = []string{ "min_length", "max_length", "mean_length", // 19-21 } +// referenceColumns are added when a reference sequence is given. +var referenceColumns = []string{"reference_location", "reference_amplicon"} + // round returns x rounded to the given number of decimals; the CSV writer // prints it with its shortest representation. func round(x float64, decimals int) float64 { @@ -43,6 +46,10 @@ func pairRecord(index int, p obiprimer.PairStats) obiitercsv.CSVRecord { "bc": round(float64(p.Bc), 4), "identified_taxa": p.Identified, "bs": round(float64(p.Bs), 4), "min_length": p.MinLen, "max_length": p.MaxLen, "mean_length": round(p.MeanLen, 2), } + if p.RefLocation != "" { + r["reference_location"] = p.RefLocation + r["reference_amplicon"] = p.RefAmplicon + } for key, v := range map[string]float64{"tm1": p.Tm1, "tm1_min": p.MinTm1, "tm2": p.Tm2, "tm2_min": p.MinTm2} { if !math.IsNaN(v) { r[key] = round(v, 1) @@ -51,13 +58,20 @@ func pairRecord(index int, p obiprimer.PairStats) obiitercsv.CSVRecord { return r } -// PairsToCSVIterator streams the pairs, in order, as CSV records. -func PairsToCSVIterator(pairs []obiprimer.PairStats) *obiitercsv.ICSVRecord { +// PairsToCSVIterator streams the pairs, in order, as CSV records. With +// withReference the two reference columns are written (NA for the pairs that +// do not amplify the reference sequence). +func PairsToCSVIterator(pairs []obiprimer.PairStats, withReference bool) *obiitercsv.ICSVRecord { iterator := obiitercsv.NewICSVRecord() iterator.Add(1) for _, column := range csvColumns { iterator.AppendField(column) } + if withReference { + for _, column := range referenceColumns { + iterator.AppendField(column) + } + } go func() { defer iterator.Done() From 574221df42231009aa703fdccfac0d9cc559dae8 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Tue, 3 Nov 2026 16:00:15 +1100 Subject: [PATCH 19/24] Refactor taxonomy handling and improve performance This update introduces logic for handling embedded taxonomy, allowing users to specify taxonomy or extract it from sequences, while optimizing performance through reduced memory usage. It also adds batch reordering capabilities and implements comprehensive testing to ensure correct precedence rules and robust error handling for missing taxonomic paths. --- OBIPRIMERS_NOTES.md | 56 ++++++++- cmd/obitools/obiprimers/main.go | 15 +-- pkg/obiiter/batchiterator.go | 8 +- pkg/obitools/obiprimers/embedded_test.go | 141 +++++++++++++++++++++++ pkg/obitools/obiprimers/obiprimers.go | 81 ++++++++++++- pkg/obitools/obiprimers/options.go | 2 +- 6 files changed, 284 insertions(+), 19 deletions(-) create mode 100644 pkg/obitools/obiprimers/embedded_test.go diff --git a/OBIPRIMERS_NOTES.md b/OBIPRIMERS_NOTES.md index 551b60d..0a9717e 100644 --- a/OBIPRIMERS_NOTES.md +++ b/OBIPRIMERS_NOTES.md @@ -423,7 +423,7 @@ Options (noms alignés sur `obipcr` et `obigrep` quand ils existent) : | Option | ecoPrimers | Défaut | |---|---|---| -| `-t/--taxonomy` (obligatoire) | `-d` (base) | | +| `-t/--taxonomy` (facultatif depuis la section 16) | `-d` (base) | taxonomie embarquée | | `-r/--restrict-to-taxon` | `-r` | tous les exemples | | `-i/--ignore-taxon` | `-i` | | | `-E/--exception-taxon` | `-E` | | @@ -532,3 +532,57 @@ l'intention, pas le C : - `obiutils` : `TestSetString` est **instable** (échoue ~4 fois sur 6) : `Set.String()` parcourt une map Go et le test attend `[1 2 3]`. Sans rapport avec obiprimers, non corrigé. - Un défaut de mon test précédent (`TestCLISearchParams` laissait `_salt = -1`) a été corrigé. + +## 16. Taxonomie embarquée dans les séquences (2026-11-03) + +Demande : comme `obitag`, `obiprimers` doit pouvoir se passer de `-t` quand les séquences ont été annotées +avec `obiannotate --taxonomic-path` (attribut `taxonomic_path` : liste « code:taxid [nom]@rang » de la racine +au taxon). + +Code : `pkg/obitools/obiprimers/obiprimers.go` (`readSequencesAndTaxonomy`, `embeddedTaxonomyInMemory`, +`CLIReadSequencesAndTaxonomy`), `-t` rendu facultatif (`LoadTaxonomyOptionSet(options, false, false)`), +`CLIPrimerPairs(sequences, taxonomy)` reçoit désormais la taxonomie. Réutilise l'existant : +`IBioSequence.ExtractTaxonomy` et `BioSequenceSlice.ExtractTaxonomy` (le mécanisme d'`obitag`). + +Règles : `-t` donné (taxonomie par défaut déjà chargée) : il l'emporte, les chemins embarqués sont ignorés ; +sinon la taxonomie est extraite des séquences et définie comme taxonomie par défaut, comme `obitag`. +- **Fichiers** : deux passes. La première ne retient que les chemins taxonomiques (mémoire = taxonomie + seule), la seconde lit le corpus. Une seule passe obligerait à garder toutes les séquences en mémoire, + puisque la sélection des exemples (`-r`, `-i`, `-E`) a besoin de la taxonomie. +- **Entrée standard** (non relisible) : séquences gardées en mémoire (`Load()`), taxonomie extraite, puis + réémises par `IBatchOver`. +- **Pas de chemin** : erreur « no taxonomy: use the -t option, or give sequences annotated with their + taxonomic path (obiannotate --taxonomic-path): path is empty ». L'extraction est stricte, comme celle + d'`obitag` : une seule séquence sans chemin fait échouer l'extraction. +- `--with-leaves` (séquence = feuille de son taxon), acceptée par le jeu d'options partagé de taxonomie, + est **sans effet** sur obiprimers. + +**Contrôle** (fichier `mito.vert.path.fasta.gz` produit par `obiannotate -t ... --taxonomic-path`, 6,3 s) : + +| Cas | Résultat | +|---|---| +| sans `-t`, fichier | CSV **identique octet pour octet** (`cmp`) au CSV obtenu avec `-t` (3 448 paires) | +| sans `-t`, entrée standard | CSV identique | +| `-t` + fichier annoté | CSV identique, pas d'extraction (le `-t` l'emporte) | +| sans `-t`, fichier non annoté | erreur explicite, code de sortie 1 | + +La taxonomie embarquée ne contient que 13 039 taxons (ceux des chemins) contre 2,6 millions de nœuds pour +la taxonomie NCBI complète. **Effet sur la mémoire et le temps (Cyprinidae)** : + +| | avec `-t` NCBI | taxonomie embarquée (fichier) | embarquée (stdin) | +|---|---|---|---| +| temps | 7,4 s | **2,65 s** | 1,27 s (lecture depuis `gzcat`) | +| pic mémoire | 2,82 Go | **1,50 Go** | 1,23 Go | + +(le fichier annoté pèse 43,9 Mo au lieu de 39,0 Mo en gzip.) Ce gain vient du chargeur de taxonomie NCBI +évité ; il ne change rien à l'algorithme. + +Tests (`embedded_test.go`) : extraction depuis des fichiers avec restriction `-r` sur un taxid de la +taxonomie embarquée et attribution des espèces, extraction en mémoire, erreur avec conseil, précédence de +`-t`, taxonomie nulle refusée. + +**Correctif de code partagé** : en testant sous `-race` la lecture des fichiers, le détecteur a signalé une +course dans `obiiter.IBioSequence.RebatchBySize` (appelée par `CLIReadBioSequences`) : la goroutine +réaffectait la variable capturée `iterator` (`iterator = iterator.SortBatches()`) pendant que la fonction +appelante lisait `iterator.IsPaired()`. Corrigé avec une variable locale (`sorted`), comportement inchangé ; +les tests de `obiiter`, `obiformats` et `obitools/...` passent. diff --git a/cmd/obitools/obiprimers/main.go b/cmd/obitools/obiprimers/main.go index 14cb7ba..c78eb29 100644 --- a/cmd/obitools/obiprimers/main.go +++ b/cmd/obitools/obiprimers/main.go @@ -5,9 +5,7 @@ import ( log "github.com/sirupsen/logrus" - "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obidefault" "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obioptions" - "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obitools/obiconvert" "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obitools/obicsv" "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obitools/obiprimers" "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiutils" @@ -16,21 +14,18 @@ import ( func main() { optionParser := obioptions.GenerateOptionParser( "obiprimers", - "designs the primer pairs amplifying a set of example sequences and evaluates the DNA barcode they delimit", + "designs the primer pairs amplifying a set of example sequences and evaluates the DNA barcode they delimit. "+ + "The taxonomy comes from the -t option, or is read from the taxonomic paths embedded in the sequences (obiannotate --taxonomic-path)", obiprimers.OptionSet) _, args := optionParser(os.Args) - if !obidefault.HasSelectedTaxonomy() { - log.Fatal("you must indicate a taxonomy using the -t or --taxonomy option") - } - - sequences, err := obiconvert.CLIReadBioSequences(args...) + sequences, taxonomy, err := obiprimers.CLIReadSequencesAndTaxonomy(args) if err != nil { - log.Fatalf("Cannot open the sequence files: %v", err) + log.Fatalf("Cannot read the sequences: %v", err) } - pairs, err := obiprimers.CLIPrimerPairs(sequences) + pairs, err := obiprimers.CLIPrimerPairs(sequences, taxonomy) if err != nil { log.Fatalf("%v", err) } diff --git a/pkg/obiiter/batchiterator.go b/pkg/obiiter/batchiterator.go index 606fd68..f8afd60 100644 --- a/pkg/obiiter/batchiterator.go +++ b/pkg/obiiter/batchiterator.go @@ -465,7 +465,9 @@ func (iterator IBioSequence) RebatchBySize(maxBytes int, maxCount int) IBioSeque go func() { order := 0 - iterator = iterator.SortBatches() + // a local variable: reassigning the captured iterator would race with + // the IsPaired call below, made by the caller on the same variable + sorted := iterator.SortBatches() buffer := obiseq.MakeBioSequenceSlice() bufBytes := 0 source := "" @@ -479,8 +481,8 @@ func (iterator IBioSequence) RebatchBySize(maxBytes int, maxCount int) IBioSeque } } - for iterator.Next() { - seqs := iterator.Get() + for sorted.Next() { + seqs := sorted.Get() source = seqs.Source() for _, s := range seqs.Slice() { sz := s.MemorySize() diff --git a/pkg/obitools/obiprimers/embedded_test.go b/pkg/obitools/obiprimers/embedded_test.go new file mode 100644 index 0000000..daf0b3e --- /dev/null +++ b/pkg/obitools/obiprimers/embedded_test.go @@ -0,0 +1,141 @@ +package obiprimers + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiiter" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiprimer" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obitax" +) + +// Three sequences below two families (5 and 6) of the same root, annotated like +// obiannotate --taxonomic-path does. +const embeddedFasta = `>s1 {"taxid":"taxon:10 [Alpha beta]@species","taxonomic_path":["taxon:1 [root]@no rank","taxon:5 [Famone]@family","taxon:10 [Alpha beta]@species"]} +acgtacgtacgtacgtacgtacgtacgtacgtacgtacgt +>s2 {"taxid":"taxon:11 [Alpha gamma]@species","taxonomic_path":["taxon:1 [root]@no rank","taxon:5 [Famone]@family","taxon:11 [Alpha gamma]@species"]} +acgtacgtacgtacgtacgtacgtacgtacgtacgtacgt +>s3 {"taxid":"taxon:20 [Delta eps]@species","taxonomic_path":["taxon:1 [root]@no rank","taxon:6 [Famtwo]@family","taxon:20 [Delta eps]@species"]} +acgtacgtacgtacgtacgtacgtacgtacgtacgtacgt +` + +func writeTemp(t *testing.T, content string) string { + t.Helper() + path := filepath.Join(t.TempDir(), "seqs.fasta") + if err := os.WriteFile(path, []byte(content), 0o644); err != nil { + t.Fatal(err) + } + return path +} + +func drain(it obiiter.IBioSequence) (n int) { + for it.Next() { + batch := it.Get() + n += len(batch.Slice()) + } + return +} + +func TestEmbeddedTaxonomyFromFiles(t *testing.T) { + path := writeTemp(t, embeddedFasta) + + sequences, taxonomy, err := readSequencesAndTaxonomy([]string{path}, nil) + if err != nil { + t.Fatal(err) + } + if taxonomy == nil || taxonomy.Len() != 6 { // root, 2 families, 3 species + t.Fatalf("expected an embedded taxonomy of 6 taxa, got %v", taxonomy) + } + + // the sequences are read in full on the second pass + restrict, err := obiprimer.BelongingTo(taxonomy, []string{"5"}) + if err != nil { + t.Fatalf("a taxid of the embedded taxonomy must be usable with -r: %v", err) + } + corpus, err := obiprimer.LoadCorpus(sequences, obiprimer.Selection{Restrict: restrict}) + if err != nil { + t.Fatal(err) + } + if c := corpus.Counts(); c.Examples != 2 || c.Ignored != 1 { + t.Errorf("restricting to family 5: %+v", c) + } + + taxa, err := corpus.AssignTaxa(taxonomy, "species") + if err != nil { + t.Fatal(err) + } + if taxa.ExampleTaxa != 2 { + t.Errorf("%d example species, want 2", taxa.ExampleTaxa) + } +} + +func TestEmbeddedTaxonomyFromMemory(t *testing.T) { // the standard input case + var slice obiseq.BioSequenceSlice + for _, s := range []struct{ id, taxid string }{{"a", "10"}, {"b", "20"}} { + seq := obiseq.NewBioSequence(s.id, []byte("acgtacgtacgt"), "") + seq.SetAttribute("taxonomic_path", []string{ + "taxon:1 [root]@no rank", "taxon:" + s.taxid + " [Sp" + s.taxid + "]@species"}) + slice = append(slice, seq) + } + + sequences, taxonomy, err := embeddedTaxonomyInMemory(obiiter.IBatchOver("stdin", slice, 10)) + if err != nil { + t.Fatal(err) + } + if taxonomy.Len() != 3 { + t.Errorf("%d taxa, want 3", taxonomy.Len()) + } + if n := drain(sequences); n != 2 { + t.Errorf("the sequences must be given back intact: %d of 2", n) + } +} + +func TestMissingEmbeddedTaxonomyGivesAdvice(t *testing.T) { + path := writeTemp(t, ">s1 {\"taxid\":\"taxon:10 [Alpha beta]@species\"}\nacgtacgtacgtacgt\n") + _, _, err := readSequencesAndTaxonomy([]string{path}, nil) + if err == nil { + t.Fatal("sequences without a taxonomic path and no -t must be refused") + } + for _, want := range []string{"-t", "obiannotate --taxonomic-path"} { + if !strings.Contains(err.Error(), want) { + t.Errorf("the error should mention %q: %v", want, err) + } + } + + var slice obiseq.BioSequenceSlice + slice = append(slice, obiseq.NewBioSequence("a", []byte("acgt"), "")) + if _, _, err := embeddedTaxonomyInMemory(obiiter.IBatchOver("stdin", slice, 10)); err == nil || + !strings.Contains(err.Error(), "obiannotate --taxonomic-path") { + t.Errorf("the in-memory case must give the same advice: %v", err) + } +} + +func TestGivenTaxonomyHasPrecedence(t *testing.T) { + path := writeTemp(t, embeddedFasta) + _, embedded, err := readSequencesAndTaxonomy([]string{path}, nil) + if err != nil { + t.Fatal(err) + } + + // With a taxonomy given (-t), the embedded paths are ignored, even if present. + other := obitax.NewTaxonomy("other", "other", obitax.DefaultTaxidAlphabet) + sequences, used, err := readSequencesAndTaxonomy([]string{path}, other) + if err != nil { + t.Fatal(err) + } + if used != other || used == embedded { + t.Error("the given taxonomy must be returned as is") + } + if n := drain(sequences); n != 3 { + t.Errorf("%d sequences read, want 3", n) + } +} + +func TestPrimerPairsNeedsATaxonomy(t *testing.T) { + if _, err := CLIPrimerPairs(obiiter.NilIBioSequence, nil); err == nil { + t.Error("a nil taxonomy must be refused") + } +} diff --git a/pkg/obitools/obiprimers/obiprimers.go b/pkg/obitools/obiprimers/obiprimers.go index 399d212..84f5a7a 100644 --- a/pkg/obitools/obiprimers/obiprimers.go +++ b/pkg/obitools/obiprimers/obiprimers.go @@ -3,19 +3,92 @@ package obiprimers import ( "fmt" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obidefault" "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiiter" "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiprimer" "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obitax" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obitools/obiconvert" log "github.com/sirupsen/logrus" ) +const noTaxonomyAdvice = "no taxonomy: use the -t option, or give sequences annotated with their " + + "taxonomic path (obiannotate --taxonomic-path)" + +// embeddedTaxonomyInMemory extracts the taxonomy from the taxonomic paths +// carried by the sequences, which it has to hold in memory since the iterator +// cannot be read twice. It returns an iterator over the same sequences. +func embeddedTaxonomyInMemory(sequences obiiter.IBioSequence) (obiiter.IBioSequence, *obitax.Taxonomy, error) { + source, slice := sequences.Load() + taxonomy, err := slice.ExtractTaxonomy(nil, false) + if err != nil { + return obiiter.NilIBioSequence, nil, fmt.Errorf("%s: %w", noTaxonomyAdvice, err) + } + return obiiter.IBatchOver(source, slice, obidefault.BatchSize()), taxonomy, nil +} + +// readSequencesAndTaxonomy returns the sequences and the taxonomy to use: the +// given one when there is one (the -t option) and otherwise the taxonomy +// embedded in the sequences. Like obitag, it needs no -t option when the +// sequence files were annotated with obiannotate --taxonomic-path. +// +// For files the embedded taxonomy is collected by a first pass over them that +// keeps nothing but the taxonomic paths, and the sequences are then read a +// second time: the selection of the examples needs the taxonomy, so reading in +// one pass would mean holding every sequence in memory. Standard input cannot +// be read twice, its sequences are held in memory. +func readSequencesAndTaxonomy(args []string, given *obitax.Taxonomy) (obiiter.IBioSequence, *obitax.Taxonomy, error) { + if given != nil { + sequences, err := obiconvert.CLIReadBioSequences(args...) + return sequences, given, err + } + + if len(args) == 0 { + log.Infof("No taxonomy given: reading the standard input in memory to extract its embedded taxonomy") + sequences, err := obiconvert.CLIReadBioSequences() + if err != nil { + return obiiter.NilIBioSequence, nil, err + } + return embeddedTaxonomyInMemory(sequences) + } + + log.Infof("No taxonomy given: extracting the taxonomy embedded in the sequence files (first pass)") + first, err := obiconvert.CLIReadBioSequences(args...) + if err != nil { + return obiiter.NilIBioSequence, nil, err + } + taxonomy, err := first.ExtractTaxonomy(false) + if err != nil || taxonomy == nil { + if err == nil { + err = fmt.Errorf("the sequences carry no taxonomic path") + } + return obiiter.NilIBioSequence, nil, fmt.Errorf("%s: %w", noTaxonomyAdvice, err) + } + log.Infof("Embedded taxonomy extracted: %d taxa", taxonomy.Len()) + + sequences, err := obiconvert.CLIReadBioSequences(args...) + return sequences, taxonomy, err +} + +// CLIReadSequencesAndTaxonomy reads the sequences of the command line and +// returns them with the taxonomy to use, set as the default taxonomy: the one +// of the -t option, or else the one embedded in the sequences. +func CLIReadSequencesAndTaxonomy(args []string) (obiiter.IBioSequence, *obitax.Taxonomy, error) { + given := obitax.DefaultTaxonomy() + sequences, taxonomy, err := readSequencesAndTaxonomy(args, given) + if err != nil { + return obiiter.NilIBioSequence, nil, err + } + if given == nil { + taxonomy.SetAsDefault() + } + return sequences, taxonomy, nil +} + // CLIPrimerPairs reads the sequences, selects the examples and the // counterexamples from the taxonomy options, and searches the primer pairs. -// The default taxonomy must be loaded. -func CLIPrimerPairs(sequences obiiter.IBioSequence) ([]obiprimer.PairStats, error) { - taxonomy := obitax.DefaultTaxonomy() +func CLIPrimerPairs(sequences obiiter.IBioSequence, taxonomy *obitax.Taxonomy) ([]obiprimer.PairStats, error) { if taxonomy == nil { - return nil, fmt.Errorf("a taxonomy is required: use the -t option") + return nil, fmt.Errorf(noTaxonomyAdvice) } sel := obiprimer.Selection{} diff --git a/pkg/obitools/obiprimers/options.go b/pkg/obitools/obiprimers/options.go index 985b505..968ed22 100644 --- a/pkg/obitools/obiprimers/options.go +++ b/pkg/obitools/obiprimers/options.go @@ -46,7 +46,7 @@ var _partitions = 256 func OptionSet(options *getoptions.GetOpt) { obiconvert.InputOptionSet(options) obiconvert.OutputModeOptionSet(options, true) - obioptions.LoadTaxonomyOptionSet(options, true, false) + obioptions.LoadTaxonomyOptionSet(options, false, false) options.StringSliceVar(&_restrict, "restrict-to-taxon", 1, 1, options.Alias("r"), From d7d63f315c23dddd9d954cfdc104f2ba233f2288 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Tue, 3 Nov 2026 20:55:35 +1100 Subject: [PATCH 20/24] Refactor primer logic and enhance taxonomy features This commit introduces refinements to primer design strategies, species identification rules, and performance analysis. It also updates taxonomy handling by implementing methods for setting and dropping taxonomic paths, along with necessary utility improvements for set handling and comprehensive test coverage. --- OBIPRIMERS_NOTES.md | 494 ++++++++++++++++++ pkg/obiprimer/search_real_test.go | 70 ++- .../ecoprimers_Cyprinidae_I1.pairs.tsv | 173 ++++++ pkg/obiseq/taxonomy_methods.go | 8 +- pkg/obitools/obiprimers/embedded_test.go | 56 ++ pkg/obitools/obiprimers/obiprimers.go | 25 +- pkg/obiutils/set.go | 43 +- pkg/obiutils/set_test.go | 26 + 8 files changed, 886 insertions(+), 9 deletions(-) create mode 100644 pkg/obiprimer/testdata/ecoprimers_Cyprinidae_I1.pairs.tsv diff --git a/OBIPRIMERS_NOTES.md b/OBIPRIMERS_NOTES.md index 0a9717e..81f35ec 100644 --- a/OBIPRIMERS_NOTES.md +++ b/OBIPRIMERS_NOTES.md @@ -586,3 +586,497 @@ course dans `obiiter.IBioSequence.RebatchBySize` (appelée par `CLIReadBioSequen réaffectait la variable capturée `iterator` (`iterator = iterator.SortBatches()`) pendant que la fonction appelante lisait `iterator.IsPaired()`. Corrigé avec une variable locale (`sorted`), comportement inchangé ; les tests de `obiiter`, `obiformats` et `obitools/...` passent. + +### 16 bis. Suppression de `taxonomic_path` après extraction (2026-11-03, demande de l'utilisateur) + +Une fois la taxonomie construite, l'attribut `taxonomic_path` (liste d'une vingtaine de chaînes par +séquence) ne sert plus : la recherche lit les taxons par le `taxid`. Il est supprimé dans les trois +chemins de lecture (`obiprimers.go`) : fichiers (worker `obiseq.AnnotatorToSeqWorker` sur la seconde +passe), entrée standard (en place sur la tranche gardée en mémoire, avant de la réémettre) et `-t` donné +avec un fichier annoté (les chemins sont inutiles). Réutilisation : `AnnotatorToSeqWorker` et +`BioSequence.DeleteAttribute` existants ; seul ajout partagé : la constante +`obiseq.TaxonomicPathAttribute`, utilisée par `SetPath`/`Path` à la place du littéral. Test : +`TestTaxonomicPathIsDroppedAfterExtraction` (le chemin disparaît, le `taxid` reste). + +**Mesures, sans gain visible sur le pic de mémoire du processus** : +- tas réellement libéré (7 822 séquences, 131 Mb de bases, `GC` forcé, tranche gardée vivante) : + **19,2 Mo sur 193,7 Mo (9,9 %), 2,46 Ko par séquence**. Un premier essai donnait « 99,9 % » : il était faux + (la tranche n'était plus référencée, donc toutes les séquences avaient été libérées) ; +- pic de mémoire du processus (entrée standard, 8 mesures entrelacées chacune) : médiane **1 334 Mo avant, + 1 346 Mo après**, dispersion > 250 Mo : aucune différence détectable. + +Interprétation : le gain est réel mais de l'ordre de 10 % des séquences gardées, soit ~1 % du pic ici ; il +deviendrait important sur de très grandes bases (à 2,46 Ko par séquence, ~2,5 Go pour un million de +séquences). **Le pic de 1,3 Go est sans commune mesure avec les 194 Mo de séquences gardées** : l'essentiel +du pic vient d'ailleurs (lecture/analyse des 131 Mb du FASTA, retard du ramasse-miettes : `GOGC=25` avait +fait passer le pic de 2,82 à 1,98 Go). C'est la piste à analyser pour la mémoire. + +## 17. Validation avec contre-exemples : `-r 7953 -i 1` (2026-11-03) + +Premier cas **avec contre-exemples** comparé à ecoPrimers (la section 1 de l'inventaire : jusque-là tous les +jeux de référence avaient 0 contre-exemple). Les 325 Cyprinidae sont les exemples (`-r 7953`) et toutes les +autres séquences de la base (7 497, 7 387 espèces) sont des contre-exemples (`-i 1`, la racine). Paramètres : +`-e 3 -3 2 -l 30 -L 150 -c`. (« -I 1 » demandé : ecoPrimers n'a pas d'option `-I` ; sa boucle `getopt` +ignore silencieusement une option inconnue, donc c'était `-i`.) + +| | ecoPrimers | obiprimers | +|---|---|---| +| exemples / contre-exemples | 325 (316 esp.) / 7 497 (7 387 esp.) | identique | +| quorum de contre-exemples | 749 / 7 497 | identique | +| paires retenues | **173** | **173** | +| temps | **2 728 s (45,5 min)**, 1 cœur | **1,9 s**, 16 workers | +| pic mémoire | 756 Mo | 781 Mo (taxonomie embarquée) | + +**Les deux tables sont identiques sur tous les champs** (173/173 paires, 0 différence : amorces, drapeaux +good, séquences et taxons amplifiés côté exemples ET contre-exemples, Yule, Bc, Bs, longueurs, GC, Tm). +Répartition des drapeaux good : 97 `bG` et 76 `Gb` (une seule amorce good par paire, comme attendu avec +des contre-exemples aussi nombreux) ; contre-exemples amplifiés par paire : de 264 à 513 séquences. +Notre journal : 6 850 691 occurrences approchées (contre 227 549 sans contre-exemples), 1 200 amorces +conservées dont **14 good**, 2 037 paires candidates, 200 amplifiant assez d'exemples, 173 retenues +(ecoPrimers : « Total pair count 2050 », écart de 13 attendu : il a un mot strict de plus, section 10). + +Fixé dans `testdata/ecoprimers_Cyprinidae_I1.pairs.tsv` ; test opt-in `TestCounterExamplesAgainstEcoPrimers`. +Cela couvre désormais : quorum de faux positifs, drapeau good avec contre-exemples, comptes de taxons +contre-exemples. Reste à comparer (inventaire section 1) : séquences linéaires, `--rank` autre qu'espèce, +`-e 0`, autres `-e` / `-O` / quorums, et `-E` (exception). + +## 18. Validation par variantes d'options, Cyprinidae `-r 7953` (2026-11-03) + +16 exécutions d'ecoPrimers lancées en parallèle (1 cœur chacune, 2 à 6 minutes sans contre-exemples), +comparées ligne à ligne (multi-ensembles de lignes, car des paires d'amorces AT répétées existent des deux +côtés) à `obiprimers` avec les options équivalentes. Scripts jetables : `/tmp/run_val*.sh`, +`/tmp/run_mine.sh`, `/tmp/compare_val.py` (non versionnés). + +**Identiques sur toutes les lignes et tous les champs** : + +| Variante | Lignes | +|---|---| +| séquences linéaires (sans `-c`) | 3 448 | +| `-e 1` | 2 031 | +| `-e 5` | 4 080 | +| `-O 20` | 2 288 | +| `-O 25` | 867 | +| `-q 0.8 -s 0.95` | 1 110 | +| `-q 0.5 -s 0.8` | 13 941 | +| `-3 0` | 5 405 | +| `-3 4` | 2 262 | +| `-l 10 -L 80` | 223 | +| `-E 7954` (Danio, 5 contre-exemples hors des exemples) | 11 | +| `-E 7954 -x 0.05 -s 0.8` | 16 | +| `-E 84644 -x 0.95` (Labeo : 304 exemples, 21 contre-exemples dans les exemples) | 54 | + +`-E 84644` seul et `-E 84644 -x 0.05 -s 0.8` donnent 0 paire des deux côtés : identiques mais sans valeur, +les Labeo étant amplifiés par toutes les amorces conservées ; d'où la variante `-x 0.95`. + +**`--rank genus` et `-T 0.3` : différents, mais entièrement expliqués** (par mesure) : +1. **118 et 119 paires seulement chez ecoPrimers** : toutes contiennent le mot strict en trop d'ecoPrimers + `TTTATGAAAACAAATAAA` (section 10 : présent dans 225 séquences, quorum 227). Aucune des paires absentes + ne l'ignore ; aucune ligne des variantes identiques ne le contient. Leurs comptes d'exemples vont de 294 + à 325 : l'hypothèse « effet de seuil à 293 » avait été testée et réfutée. +2. **52 et 65 lignes avec une séquence d'exemple de plus chez ecoPrimers** (exactement +1 exemple, +0 + contre-exemple à chaque fois) : l'artefact du `N` dans la vérification du 3' (section 12). +3. **9 écarts de Tm minimale** (mêmes comptes de séquences) : ecoPrimers décode les sites contenant une + base ambiguë avec le même registre à décalage ; nous ignorons ces sites (section 12). +Preuve : un binaire temporaire émulant exactement `extractSite` (3' et sites de Tm), sources restaurées +ensuite (diff identique aux sauvegardes), supprime **toutes** les différences de champs ; ne restent que +les 118/119 paires du mot en trop. + +## 19. Décisions de l'utilisateur et options `-b`, `-M`, `-k` (2026-11-03) + +- **Taxonomie embarquée : extraction stricte** (comme `obitag`) : comportement actuel conservé, rien à changer. +- **Test instable corrigé** : `obiutils.Set.String()` parcourait une map Go (ordre aléatoire) ; elle trie + désormais ses membres (entiers, flottants et chaînes par valeur, y compris types nommés ; repli sur la forme + texte pour les autres types). `Members()` est inchangé. Test `TestSetStringIsStable` ; `TestSetString` passe + 200 fois de suite (il échouait ~4 fois sur 6) et tout le paquet passe. +- **`-b`, `-M`, `-k`, `-p`** : lus dans `PrimerSets.c` (voir ci-dessous) ; porter ou abandonner : à trancher. + +### Ce que sont ces options dans ecoPrimers (lu dans le code) + +Un « lien » entre deux paires = le nombre de séquences que **les deux** paires identifient sans ambiguïté +(`wellIdentifiedSeqs[k] == 1` pour les deux). Le but est de choisir des paires complémentaires (plusieurs +marqueurs) : peu de liens = les paires identifient des taxons différents. +- `-b` : filtre la liste finale des paires avec `primers_changeSortedArray` (puis réimprime). Une paire est + gardée s'il existe une autre paire avec laquelle elle a au plus 5 liens ou plus de la moitié du plus petit + nombre de taxons identifiés ; elle n'est écartée que si, avec **toutes** les autres, ses liens sont entre 6 + et cette moitié. Règle heuristique sans justification dans le code. +- `-M` : seuil absolu de liens (nom trompeur : « percent » mais utilisé comme un nombre). Quand il est donné, + la boucle contient un `break` inconditionnel : chaque paire n'est comparée qu'à la **première** autre paire + de la liste, donc le filtre ne fait pas ce qu'il annonce. +- `-k` : n'est lu que par `ok_to_add` (construction d'ensembles d'amorces), inatteignable puisque `-p` est + désactivé : option morte. +- `-p` : sa ligne d'affectation est commentée dans `ecoprimer.c` ; elle ne fait rien. Le code d'ensembles + associé (glouton, recuit simulé, tabou, force brute) n'est jamais exécuté. +- `primers_filterWithGivenLinks` et `primers_graph_graphviz` ne sont jamais appelées. +Dépendance technique d'un éventuel portage : il faudrait garder, pour chaque paire retenue, l'ensemble des +séquences qu'elle identifie (aujourd'hui seule leur nombre de taxons est conservé). + +## 20. Le problème multi-marqueurs : spécification (discussion du 2026-11-03) + +**Statut : spécification en cours, aucun code écrit.** Décisions de l'utilisateur ci-dessous ; les points +marqués « proposition » ou « question ouverte » sont de moi et n'ont pas été validés. + +### 20.1 Origine de `-b`, `-M`, `-k`, `-p` : abandonnées + +Ces options sont des pistes explorées pendant la thèse de Tiayyba Riaz (auteur de l'article), il y a une +quinzaine d'années, jamais menées à terme : le problème était mal posé. L'utilisateur avait oublié qu'elles +traînaient dans le code. **Décision : elles ne sont pas portées** (confirmant l'analyse de la section 19 : deux +options mortes, une buguée, un filtre heuristique non justifié). + +Le modèle de la thèse : chaque paire d'amorces couvre et reconnaît une partie des espèces, avec un +recouvrement partiel entre paires ; on cherche la meilleure combinaison (graphe de « liens », glouton, recuit +simulé, tabou, force brute). Ce modèle est quasi inapplicable : un recouvrement d'espèces entre deux paires +visant des clades différents signifie qu'une amorce est partagée entre ces clades, donc que le recouvrement +est presque toujours **complet**. Les ensembles de couverture sont emboîtés ou disjoints par clade. + +### 20.2 Formulation correcte + +But : un ensemble de marqueurs (un multiplex) qui, ensemble, discriminent un maximum d'espèces. +Structure : +1. **une paire de base** qui couvre la totalité du groupe visé (Bc proche de 1) ; +2. **une collection de paires spécialisées**, chacune sur un clade que les paires précédentes résolvent mal. + +### 20.3 Réponses de l'utilisateur + +1. **Granularité des clades à raffiner : elle dépend du nombre de paires autorisées dans le multiplex.** + Avec 2 paires : une paire générique + une seule paire spécialisée, placée assez haut dans la taxonomie, + autour d'un gros groupe très mal résolu (celle qui récupère le plus d'espèces). Avec 30 paires : on peut + éclater presque chaque famille ou chaque genre mal résolu. +2. **Éviter d'amplifier les autres clades : pas par défaut, mais une option.** Elle permet de concentrer + l'effort d'amplification sur le clade d'intérêt sans le disperser, surtout s'il est sous-représenté dans la + matrice d'ADN. +3. **Critère de résolution : une espèce est résolue si au moins un marqueur la résout.** +4. **Budget : deux formulations possibles, non tranchées** (maximiser les espèces résolues avec au plus K + paires ; ou minimiser le nombre de paires pour atteindre un taux donné). +5. **La paire de base est choisie d'abord**, ce qui élimine les espèces bien reconnues ; on raisonne ensuite + sur ce qui reste (glouton hiérarchique). + +### 20.4 Ce que cela implique (propositions, à valider) + +- **Algorithme glouton hiérarchique** : paire de base sur tout le groupe (tri Bc puis Bs, comme le mode A) ; + puis, tant que le budget le permet, parmi les nœuds de la taxonomie contenant des espèces non résolues, + choisir la paire spécialisée au **plus grand gain marginal** (espèces nouvellement résolues). La + granularité exigée en 20.3-1 en découle sans règle séparée : un petit budget favorise les nœuds hauts, un + grand budget finit par les genres. Les deux formulations du budget (20.3-4) sont le même gain marginal avec + deux conditions d'arrêt (K paires atteint, ou taux visé atteint) : le choix n'a pas à être tranché pour + écrire la boucle. +- **Réutilisation** : le mode A avec `-r ` produit déjà les paires conservées dans un clade et leur Bs. + Le mode A étant rapide (de 2 à 3 s pour Teleostei), faire une recherche par clade candidat est envisageable ; + le corpus devrait alors être chargé une seule fois et la sélection exemples/contre-exemples refaite en + mémoire pour chaque clade (aujourd'hui `LoadCorpus` relit les fichiers). +- **Manque 1 : les ensembles d'espèces résolues.** Aujourd'hui `PairStats` ne garde que des nombres + (`Identified`, `ExampleTaxa`). Il faut, pour chaque paire, l'ensemble des taxons identifiés et les classes + de taxons partageant un amplicon (même besoin qu'un portage de `-b`). +- **Question ouverte (moi) : relativement à quel ensemble une espèce est-elle « résolue » par un spécialiste + qui peut amplifier d'autres clades (20.3-2) ?** Le Bs actuel ne juge l'unicité de l'amplicon que parmi les + exemples. Si le spécialiste amplifie aussi des espèces extérieures au clade, un amplicon partagé avec un + extérieur ne permet pas d'identifier l'espèce dans un échantillon réel. Je propose de juger l'unicité sur + **tout ce que la paire amplifie dans la base de référence**. Cela demande un troisième rôle de séquence, + « contexte » : non exigée amplifiée, non pénalisée, mais comptée dans le partage d'amplicons. +- **Lien avec la voie B** : avec l'option « éviter les autres clades » (20.3-2), les voisins deviennent des + contre-exemples ; sans elle, ils sont du contexte. La voie B (amorces spécifiques d'une espèce ou de + quelques espèces proches) et l'étape de spécialisation ressemblent donc beaucoup : je propose de les + spécifier ensemble, comme un seul mécanisme avec une option d'exclusion. +- **Non abordé, à demander** : compatibilité des amorces d'un même multiplex (Tm voisines, absence de + dimères entre amorces de paires différentes). `obithermo` fournit déjà les Tm. +- **Sortie** : un ensemble ordonné de paires avec, pour chacune, les espèces qu'elle ajoute (gain marginal) ; + la courbe cumulée des espèces résolues en fonction du nombre de paires répond aux deux formulations du + budget. + +### 20.5 Prochaines étapes + +Pas de code avant accord sur 20.4. À trancher : (a) le jugement d'unicité (tout ce qui est amplifié, ou +seulement le clade), donc le rôle « contexte » ; (b) si les voies B et de spécialisation sont spécifiées +ensemble ; (c) les contraintes de multiplex. Le reste de l'inventaire du mode A (validation `-e 0`, nettoyage, +mémoire, tests shell, documentation) est indépendant et peut avancer. + +## 21. Suite de la spécification : résolution, voie B, optimisation de Tm (2026-11-03) + +**Statut : spécification, aucun code.** « Décision » = réponse de l'utilisateur ; « Proposition » et +« Question » = de moi, non validées. + +### 21.1 Décisions + +1. **Une espèce est résolue par rapport à tout ce qui est amplifié**, dans le clade cible **et** dans le clade + voisin non cible. Confirme le jugement d'unicité sur tout ce que la paire amplifie dans la référence. +2. **La voie B doit être spécifiée complètement** avant tout code (et avec la spécialisation, 20.4). +3. **Tm** : idéalement oui, des Tm voisines entre amorces. Aujourd'hui la longueur est fixée et la Tm subie. + Idée de l'utilisateur : **fixer la Tm et optimiser la longueur à la fin pour équilibrer les Tm**, et cela + **pour n'importe quelle paire**. Ajouter à `obiprimers` une **phase d'optimisation de Tm** avec deux + paramètres : **un écart de Tm maximal entre les deux amorces** et **une Tm cible pour les paires**. + +### 21.2 Conséquence de 21.1-1 : quatre rôles de séquences (proposition) + +| Rôle | Exigence sur l'amplification | Compte dans l'unicité des amplicons | +|---|---|---| +| Exemple | doit être amplifiée (quorum de sensibilité) | oui | +| Contre-exemple | ne doit pas l'être (quorum de faux positifs) | **oui, si amplifiée** | +| Contexte | libre, aucune pénalité | **oui, si amplifiée** | +| Ignorée | sans objet | non | + +Le rôle « contexte » est nouveau (aujourd'hui : exemple, contre-exemple, ignorée). L'option « éviter les autres +clades » (20.3-2) transforme simplement leurs séquences de contexte en contre-exemples. +**Écart avec ecoPrimers à noter** : le Bs d'ecoPrimers ne juge l'unicité que parmi les exemples, même quand des +contre-exemples sont amplifiés. Proposition : **garder le Bs d'ecoPrimers inchangé** (c'est ce qui reproduit +les tables de référence) et ajouter un second indice calculé sur tout ce qui est amplifié (nom à choisir, +p. ex. `bs_all`), qui sert à la sélection multi-marqueurs ; il coïncide avec `bs` quand il n'y a ni +contre-exemple amplifié ni contexte. + +### 21.3 Optimisation de Tm (proposition, paramètres de l'utilisateur) + +- **Paramètres** : `Tm cible` et `écart de Tm maximal`. Proposition d'interprétation : la cible porte sur la + **moyenne des deux amorces** et l'écart borne leur différence, ce qui revient à imposer à chaque amorce + `cible ± écart/2`. +- **Où** : phase **après** la sélection des paires, sur chaque paire retenue : choisir les longueurs + `(L1, L2)` des deux amorces satisfaisant la cible et l'écart. +- **Ce qui rend la phase possible** : raccourcir une amorce **par son extrémité 5'** ne peut que conserver ou + réduire ses erreurs sur chaque site (c'est une sous-chaîne), donc la sensibilité est conservée. Allonger + par le 5' demande de **revérifier** les sites. Raccourcir fait en revanche perdre de la spécificité + (l'amorce raccourcie peut s'apparier ailleurs), d'où la nécessité de **réévaluer la paire** (sensibilité, + faux positifs, Bc, Bs, Tm) après chaque changement de longueur. +- **Réutilisation** : `obithermo` (Tm, Tm minimale), `MatchWords` et `FindPairs` sur une poignée de mots : + le coût est celui d'une évaluation par variante de longueur et par paire. +- **Intérêt pour le multiplex** : une Tm cible commune à toutes les paires d'un ensemble les rend compatibles + par construction. +- **Questions** : (a) la cible porte-t-elle sur la Tm de l'appariement parfait ou sur la Tm minimale sur les + sites (la pire), ou sur une valeur intermédiaire ? (b) bornes de longueur [Lmin, Lmax] ? Le mot actuel est + limité à 31 bases (codage 2 bits sur 64). (c) extension et raccourcissement seulement par le 5', en gardant + l'ancrage 3' ? (d) « pour n'importe quelle paire » : phase intégrée à la recherche, ou aussi outil + indépendant appliqué à des amorces fournies par l'utilisateur ? (e) une paire dont la Tm ne peut pas être + satisfaite dans les bornes : écartée ou signalée ? + +### 21.4 Voie B : esquisse de spécification (proposition) + +**But.** Une paire qui amplifie les cibles T (une espèce, ou quelques espèces très proches) et, autant que +possible, aucun de leurs voisins non cibles C. Facteur de conception : l'espace de recherche est élagué en +se concentrant sur les régions **non conservées** (celles où T diffère de C), l'inverse du mode A. + +**Rôles.** T = exemples ; C = contre-exemples (voisins à ne pas amplifier) ; le reste, contexte ou ignoré. + +**Constat.** Le pipeline actuel fait déjà fonctionner un cas voie B de façon naïve (`-r 8028 -i 8015`, *Salmo* +contre les autres Salmonidae : 7 exemples, 76 contre-exemples, 15 534 mots stricts), mais sans élagage, et le +critère « un quorum de faux positifs de 10 % » y est trop permissif pour une amorce spécifique. + +**Principe d'une paire spécifique.** Une paire n'amplifie un voisin que si **ses deux amorces** s'y apparient. +Il suffit donc d'**une amorce spécifique** (loin de tous les sites de C) et d'une **amorce partenaire** +quelconque et conservée dans T, à distance d'amplicon permise. Les candidats se génèrent ainsi : mots de T +(quorum de T) dont aucun site à <= `e_exclusion` erreurs n'existe dans C (au-delà du quorum de faux +positifs) ; puis le partenaire dans la fenêtre d'amplicon de chaque occurrence dans T. + +**Deux tolérances (proposition).** `-e` (sensibilité sur T) et une tolérance d'exclusion **`e_exclusion`** sur C, +éventuellement plus grande : un voisin est « amplifié » s'il s'apparie à `e_exclusion` erreurs ou moins. Cela +donne une marge de sécurité sans dégrader la sensibilité sur T. + +**Score de marge (proposition).** Pour une amorce : nombre minimal d'erreurs d'un site de C ; pour la paire : +la meilleure des deux amorces (un voisin n'est amplifié que si les deux s'apparient), avec un poids +particulier pour les erreurs proches du 3'. Il compléterait Yule x Bs pour classer. + +**Sortie.** Colonnes du mode A plus la marge des deux amorces. + +**Échelle (à prévoir).** Pour T = un génome, le nombre de mots candidats est celui des positions du génome +(16 000 pour une mitochondrie, des millions pour une bactérie) : l'index de graines (`MatchWords`) indexe les +**mots** et balaie le texte de C ; pour des millions de mots il faudra les traiter par lots, chacun balayant C +une fois. À dimensionner selon l'usage visé. + +**Questions pour finaliser.** +1. **Exclusion stricte ou marge** : une paire est-elle admise seulement si elle n'amplifie aucun voisin à + `e_exclusion` erreurs, ou classe-t-on par marge sans seuil dur ? Faut-il une contrainte particulière sur + les 3' derniers nucléotides des voisins (au moins une erreur dans les 5 dernières bases) ? +2. **Tailles typiques** de T et de C (une espèce avec combien de séquences ? voisins = un genre, une famille, + toute la base de 8 000 génomes mitochondriaux, des génomes entiers de bactéries ?). +3. **Type de séquences** : génomes organellaires entiers (comme le tutoriel), loci courts, génomes complets ? +4. **Plusieurs cibles** (quelques espèces proches) : amplifier le groupe sans les distinguer (détection de + groupe), ou les amplifier et les **résoudre** entre elles (Bs au sein de T, comme la spécialisation) ? +5. **Une amorce spécifique + une générale** est-il acceptable, ou les deux amorces doivent-elles être + spécifiques ? +6. **Sensibilité sur T** : quorum comme en mode A (0,9), ou toutes les séquences de T (intra-spécifique) ? + +### 21.5 Travaux préalables communs (sans les engager) + +Rôle « contexte » ; indice de résolution sur tout ce qui est amplifié ; ensembles d'espèces résolues par +paire ; corpus chargé une seule fois et sélection refaite en mémoire ; réévaluation d'une paire pour des +longueurs d'amorces variables ; marge d'une amorce (erreurs minimales sur C). + +## 22. Réponses de l'utilisateur sur la résolution et la Tm (2026-11-03) + +**Statut : spécification, aucun code.** La voie B attend les réponses de l'utilisateur (« encore un peu »). + +### 22.1 Redéfinir Bs : oui (décision de principe, détails à confirmer) + +Question de l'utilisateur : ecoPrimers calcule Bs sur les seules séquences amplifiées parce que ce sont les +seules chargées en mémoire ; ne vaudrait-il pas mieux **charger tout le monde, chercher les amorces +seulement dans une sélection, et tester sur tout le monde** ? Réponse : **oui**, et cela redéfinit Bs. Trois +populations distinctes : + +| Population | Sert à | +|---|---| +| exemples (sélection de recherche) | générer les mots conservés ; sensibilité ; dénominateur de Bc et de Bs | +| contre-exemples | quorum de faux positifs | +| **tout le monde** (population d'évaluation) | juger l'unicité des amplicons : une espèce exemple est résolue si son amplicon n'est partagé avec aucun autre taxon amplifié, où qu'il soit dans la base | + +Proposition de définition : `Bs = espèces exemples dont l'amplicon est unique parmi tous les taxons amplifiés de +la population d'évaluation / espèces exemples amplifiées`. Elle coïncide avec le Bs d'ecoPrimers quand la +population d'évaluation est la sélection (aucun contre-exemple ni contexte amplifié). **Piège à ne pas +oublier** : avec `-i 1` (validation de la section 17) le Bs d'ecoPrimers ignore les amplicons des +contre-exemples amplifiés ; la nouvelle définition changera donc ces valeurs. Pour garder les tests de +non-régression, proposition : conserver aussi le Bs d'ecoPrimers dans une colonne à part (p. ex. +`bs_examples`) et définir `bs` comme le nouvel indice ; à confirmer. Colonne informative à ajouter : +nombre de taxons hors exemples amplifiés. + +**Mémoire (proposition)** : charger tout le monde n'est pas nécessaire pour un passage simple. Après la +recherche (sur la sélection, en mémoire), un **passage d'évaluation en flux** relit la base entière avec les +seules amorces des paires retenues (quelques centaines de mots : `MatchWords` en 0,04 s pour 65 Mb) et ne +conserve que, par paire, un hash d'amplicon et un taxon par séquence amplifiée, jamais les séquences. Coût : +une relecture du fichier (comme la double lecture de la taxonomie embarquée). Pour la sélection multi-marqueurs +(évaluations répétées par clade), une population d'évaluation **en mémoire sous forme compacte** (2 bits par +base, soit ~33 Mo pour les 131 Mb du tutoriel) serait préférable. À trancher à la spécification de la +sélection multi-marqueurs. + +### 22.2 Longueur des amorces : 61 nucléotides (question de l'utilisateur) + +L'utilisateur rappelle qu'agrep avait été réimplémenté en 64 bits pour des amorces jusqu'à 61 nucléotides +(`MAX_PAT_LEN` vaut 64 dans `obiapat/apat.h`). État **actuel** de obiprimers : **31 bases au maximum**, limite +inscrite dans `match.go:48`, `words.go:44`, `pairs.go:31`, dans `obikmer` (`encodekmer.go`, +`seqquorum.go:63` : k-mers sur 64 bits, 2 bits par base) et par des tampons fixes `[32]byte` +(`pairs_eval.go:208`). `obithermo` n'a pas de limite de longueur (tranches). + +La stratégie de l'index de graines **n'est pas bloquée** : elle se généralise à 61 bases (et 64) avec une fenêtre +de **128 bits** (deux mots de 64 bits, décalage et popcount sur les deux) ; les moitiés (<= 31 bases) tiennent +dans un mot et le chemin d'index haché pour les grandes graines est déjà utilisé et testé (k = 25 et 31). +L'oracle de test existant (`matchNaive`) fixe la justesse. Le compteur de mots stricts (k-mers sur 64 bits, +type partagé d'`obikmer`) resterait limité à 31 : **proposition** : chercher les mots conservés avec un k <= 31 +(le « coeur » conservé), puis obtenir les amorces plus longues par **extension** pendant la phase de Tm +(22.3), vérifiée par l'appariement généralisé. + +### 22.3 Optimisation de Tm : réponses aux questions de 21.3 + +(a) **Cible sur l'apparié non parfait** (la Tm de l'amorce sur les sites réels, avec leurs erreurs), pas sur +l'appariement parfait. *Question restante* : quelle statistique des Tm de sites ? Le minimum (`tm_min` d'ecoPrimers) +est dominé par des valeurs aberrantes : sur les tables de référence on lit des minimums de 12 à 45 °C pour des +amorces dont la Tm parfaite est de 52 à 62 °C ; une cible sur le minimum imposerait des longueurs absurdes. +Proposition : cible sur la **moyenne ou la médiane** des Tm de sites (éventuellement pondérées par taxon), le +minimum restant rapporté ; ou un quantile bas réglable. À choisir. +(b) bornes de longueur : traitées en 22.2 (jusqu'à 61 bases possible avec la généralisation). +(c) **Extension et raccourcissement du côté le mieux conservé** (et non seulement par le 5', contrairement à ma +proposition). Cela se calcule avec un profil de conservation par position (fraction des sites cibles qui +portent la même base, comme le PSSM du tutoriel) autour de chaque amorce, tiré des occurrences déjà +trouvées. Attention : la contrainte `-3` s'applique à l'extrémité 3' *de l'amorce*, quelle que soit sa +position sur la séquence ; étendre ou raccourcir du côté 3' déplace donc cette extrémité et modifie la +longueur d'amplicon. +(d) **Intégrée à la recherche, en phase de post-traitement** de la même exécution de obiprimers (pas d'outil +indépendant pour l'instant). +(e) Une paire dont la Tm n'est pas atteignable dans les bornes est **écartée**. + +### 22.4 En attente + +Réponses de l'utilisateur sur la voie B (questions de 21.4) ; choix de la statistique de Tm (22.3-a) ; nom et +sort de l'ancien Bs (22.1) ; en-têtes éventuels à prévoir pour les colonnes de population d'évaluation. + +## 23. Décisions : un seul Bs, tout en mémoire (2026-11-03) + +**Statut : spécification, aucun code.** Cette section **remplace** la proposition de 22.1 (colonne `bs_examples`, +passage d'évaluation en flux). + +### 23.1 Décisions de l'utilisateur + +1. **Un seul indice Bs, calculé d'une seule façon.** Pas de colonne `bs_examples`, pas de variante : « on décide + un indice, il se calcule d'une certaine façon, point barre ». Le Bs d'ecoPrimers disparaît. +2. **Tout charger en mémoire.** Ne pas tout charger interdirait de lire la base de référence sur l'entrée + standard, que les obitools permettent ; la base est lue par le mécanisme de lecture standard + (comme `obiconvert`), sans format dédié. Le passage d'évaluation en flux de 22.1 est abandonné. +3. **Définition de Bs** : le nombre de taxons **bien identifiés parmi les cibles**, en considérant **tous les + barcodes amplifiés, y compris ceux hors des cibles et ceux des contre-exemples**. Une espèce cible est + mal identifiée si son barcode, unique parmi les cibles, est partagé avec une espèce non cible ou un + contre-exemple. + +### 23.2 Définition formelle retenue (traduction de 23.1-3) + +Pour une paire, soit A l'ensemble des séquences qu'elle amplifie, **de n'importe quel rôle** (cibles, +contre-exemples, autres). Pour un amplicon a, T(a) est l'ensemble des taxons (au rang choisi) des séquences de A +qui le portent. Un taxon cible t est **bien identifié** s'il existe un amplicon a produit par t avec T(a) = {t}. + +### 23.3 Conséquences + +- **Fin de la notion de séquence « ignorée »** : les séquences qui ne sont ni cibles ni contre-exemples ne sont + plus écartées à la lecture (comportement actuel, hérité d'ecoPrimers) ; elles sont **non cibles** : elles ne + comptent ni dans les quorums de sensibilité et de faux positifs, mais leurs amplicons comptent dans Bs. + Pour exclure des séquences de l'analyse, on filtre l'entrée en amont (`obigrep`), à la manière des obitools. + Le rôle « contexte » de 21.2 devient donc simplement « non cible ». +- **Les valeurs changent, y compris pour le tutoriel** : une paire conservée chez tous les vertébrés amplifie + aussi des non-Cyprinidae ; son Bs baissera par rapport aux tables d'ecoPrimers, même sans `-i`. Les tests de + non-régression de la section 17 et de la section 18 ne peuvent plus comparer Bs/identifiés avec ecoPrimers sur + la base entière. **Proposition, sans option ni colonne supplémentaire** : au niveau de la bibliothèque, un corpus + construit avec les seules cibles et contre-exemples (c'est ce que fait `LoadCorpus` aujourd'hui) redonne + exactement la définition d'ecoPrimers, car il n'y a alors aucune séquence hors sélection à amplifier ; les tests de + non-régression continuent d'utiliser ce corpus, la CLI charge toujours tout. +- **Coût mémoire** : tout le fichier est conservé (131 Mb de bases pour 194 Mo de tas dans le tutoriel, contre + 5 Mb pour Cyprinidae aujourd'hui). Une représentation compacte (2 bits par base) reste possible plus tard ; + non décidée. +- **Coût de calcul** : l'appariement approché des mots et l'énumération des amplicons passent de la sélection à + tout le corpus (l'exécution `-r 7953 -i 1` du tutoriel, qui charge déjà tout, prend 1,9 s) ; les quorums ne + comptent toujours que cibles et contre-exemples. + +### 23.4 Points à confirmer (propositions, non validées) + +1. **Dénominateur de Bs** : taxons cibles **amplifiés** (comme le code d'ecoPrimers : 2 650 identifiés sur 3 805 + amplifiés = 0,696) ou **tous** les taxons cibles ? Avec le premier, Bc (couverture) et Bs (discrimination) + restent indépendants et `Bc x Bs` est la part des cibles résolues par la paire, ce qui est le gain mesuré par + la sélection multi-marqueurs. Avec le second, Bs <= Bc et ce produit disparaît. **Je recommande le + premier.** L'article (« nombre de taxons identifiés sur |E| ») et le code divergent déjà. +2. **Taxon à plusieurs amplicons** : bien identifié dès qu'**un** de ses amplicons est unique (règle d'ecoPrimers), + ou seulement si **tous** le sont ? Je propose de garder la règle d'ecoPrimers. +3. **Séquence non cible sans taxon au rang choisi** : la traiter comme un taxon anonyme distinct (prudent : + elle empêche l'identification d'une cible qui partage son amplicon) ou l'ignorer ? Je propose le premier. + +## 24. Règle d'identification de Bs, et l'indice BD (2026-11-03) + +**Statut : spécification, aucun code.** Complète et corrige la section 23. + +### 24.1 Décisions de l'utilisateur + +1. **Dénominateur de Bs : les taxons cibles amplifiés** (comme ecoPrimers : 2 650 / 3 805 = 0,696). + Bc (couverture) et Bs (discrimination) restent indépendants ; `Bc x Bs` est la part des cibles résolues. +2. **Règle d'identification : une espèce est bien identifiée si TOUS ses barcodes ne sont associés qu'à + elle.** Dès qu'un barcode est partagé entre deux espèces, **les deux** sont mal identifiées. Cette règle + remplace celle de 23.4-2 (que j'avais proposé de garder). +3. **Nouvel indice BD (barcode discrimination)** proposé par l'utilisateur : la part des **barcodes** qui + mènent sans ambiguïté à une espèce cible. Exemple de l'utilisateur : A porte les barcodes A1 et A2, B porte + A2 et B1. Ni A ni B ne sont bien identifiées (elles partagent A2), mais **A1 et B1 sont discriminants**, A2 + ne l'est pas. Ici Bs = 0/2 et BD = 2/3. (Avec la règle d'ecoPrimers on aurait Bs = 2/2.) + +### 24.2 ecoPrimers applique bien l'autre règle (vérifié) + +Lecture de `taxstats.c` : pour chaque amplicon distinct (parmi les exemples), on dresse l'ensemble de ses +taxons ; si cet ensemble n'a qu'**un** taxon (`taxoncount == 1`), ce taxon est marqué bien identifié. Un +taxon est donc compté **dès qu'un** de ses amplicons lui est propre. Confirmations : (i) mon implémentation de +cette règle reproduit toutes les colonnes `identifiés` et Bs de plus de vingt tables d'ecoPrimers (sections +12, 15, 17, 18) ; (ii) recalcul indépendant avec `obipcr` + Python sur la paire n° 1 de `Cyprinidae.ecoprimers` +(320 amplicons chez `obipcr`, 318 chez ecoPrimers qui impose `-3 2`) : règle « au moins un barcode unique » +239/312 = 0,766 contre 237/310 = 0,765 pour ecoPrimers ; règle « tous uniques » 238/312 = 0,763 ; **BD = +240/265 = 0,906** (265 barcodes distincts). **Portée de (ii)** : sur cette paire les deux règles ne diffèrent +que d'**une** espèce sur 312, car les données du tutoriel ont un génome par espèce (4 espèces seulement +portent plusieurs barcodes) : ce recalcul ne tranche pas entre les règles à lui seul ; la preuve est le code +et les tables reproduites. + +### 24.3 Conséquences + +- **Les différences entre règles seront faibles sur le tutoriel** (un génome par espèce) et **plus fortes** + sur des bases à plusieurs séquences par espèce (variation intra-spécifique), typiquement des bases de loci courts. +- **Non-régression** : la règle changeant, l'égalité des colonnes `identifiés` et Bs avec ecoPrimers n'est plus + atteignable, même avec un corpus restreint aux cibles et contre-exemples (annule la proposition de 23.3 sur ce + point). Toutes les autres colonnes (séquences, taxons, Bc, Yule, longueurs, GC, Tm, drapeaux good) restent + comparables. Pour Bs, BD et `identifiés` : tests sur cas synthétiques (dont l'exemple A1/A2/B1 ci-dessus) et + **vérification indépendante** par `obipcr` + script, comme en 24.2, sur des paires choisies. +- **Seuil `-T` (spécificité minimale, 0,6 par défaut)** : calibré sur l'ancienne règle, plus indulgente. Le + défaut sera peut-être à revoir ; à décider avec la spécification complète. +- **Implémentation (plus tard)** : la phase 2 de `FindPairs` doit enregistrer le hash d'amplicon de **toutes** les + séquences amplifiées (cibles, contre-exemples, non-cibles) et non plus des seuls exemples ; un taxon cible est + mal identifié si l'un de ses amplicons est porté par un autre taxon. + +### 24.4 Questions restantes + +1. **BD** : l'inclure dans la sortie, et dans le classement ? Dénominateur proposé : les barcodes distincts + portés par **au moins une cible** (un barcode propre à une non-cible ne « mène » pas à une espèce cible) ; + comptage par barcode distinct, sans pondération par le nombre de séquences. +2. **Non-cible sans taxon au rang choisi** (23.4-3, proposé, non confirmé) : taxon anonyme distinct, donc + barcode partagé = ambigu (prudent). +3. **`-T` par défaut** (voir ci-dessus). diff --git a/pkg/obiprimer/search_real_test.go b/pkg/obiprimer/search_real_test.go index babb400..53b9466 100644 --- a/pkg/obiprimer/search_real_test.go +++ b/pkg/obiprimer/search_real_test.go @@ -16,6 +16,12 @@ import ( // loadTutorial reads the Cyprinidae examples of the tutorial data (the opt-in // tests need OBIPRIMERS_TUTORIAL, see TestStrictWordsAgainstEcoPrimers). func loadTutorial(t *testing.T) (*Corpus, *Taxa, *obitax.Taxonomy) { + t.Helper() + return loadTutorialWith(t, nil) +} + +// loadTutorialWith is loadTutorial with counterexample clades (ecoPrimers -i). +func loadTutorialWith(t *testing.T, ignore []string) (*Corpus, *Taxa, *obitax.Taxonomy) { t.Helper() dir := os.Getenv("OBIPRIMERS_TUTORIAL") if dir == "" { @@ -33,7 +39,11 @@ func loadTutorial(t *testing.T) (*Corpus, *Taxa, *obitax.Taxonomy) { if err != nil { t.Fatal(err) } - corpus, err := LoadCorpus(seqs, Selection{Restrict: restrict}) + sel := Selection{Restrict: restrict} + if sel.Ignore, err = BelongingTo(taxonomy, ignore); err != nil { + t.Fatal(err) + } + corpus, err := LoadCorpus(seqs, sel) if err != nil { t.Fatal(err) } @@ -173,3 +183,61 @@ func TestReferenceSitesAreRealSites(t *testing.T) { }) } } + +// ecoPrimers with counterexamples: -r 7953 -i 1 (the Cyprinidae against all the +// other sequences of the database, 7497 counterexamples of 7387 species), -e 3 +// -3 2 -l 30 -L 150 -c. ecoPrimers needed 45 minutes and keeps 173 pairs. +// testdata/ecoprimers_Cyprinidae_I1.pairs.tsv holds for each: the primers, the +// good flags, the amplified example and counterexample sequences, the amplified +// example and counterexample taxa, and the identified taxa. +func TestCounterExamplesAgainstEcoPrimers(t *testing.T) { + corpus, taxa, _ := loadTutorialWith(t, []string{"1"}) + if c := corpus.Counts(); c.Examples != 325 || c.CounterExamples != 7497 || taxa.CounterExamples != 7387 { + t.Fatalf("counts %+v, %d counterexample species (ecoPrimers: 325, 7497, 7387)", c, taxa.CounterExamples) + } + pairs, info, err := Search(corpus, taxa, tutorialParams(t)) + if err != nil { + t.Fatal(err) + } + t.Logf("%+v", *info) + + f, err := os.Open(filepath.Join("testdata", "ecoprimers_Cyprinidae_I1.pairs.tsv")) + if err != nil { + t.Fatal(err) + } + defer f.Close() + type row struct { + good string + v [5]int + } + want := map[string]row{} + sc := bufio.NewScanner(f) + for sc.Scan() { + var p1, p2 string + var r row + if _, err := fmt.Sscanf(sc.Text(), "%s\t%s\t%s\t%d\t%d\t%d\t%d\t%d", &p1, &p2, &r.good, + &r.v[0], &r.v[1], &r.v[2], &r.v[3], &r.v[4]); err != nil { + t.Fatal(err) + } + want[p1+"/"+p2] = r + } + + if len(pairs) != len(want) { + t.Errorf("%d pairs, ecoPrimers reports %d", len(pairs), len(want)) + } + bad := 0 + for _, g := range pairs { + w, ok := want[g.Primer1+"/"+g.Primer2] + if !ok { + t.Errorf("pair %s/%s is not in the ecoPrimers result", g.Primer1, g.Primer2) + continue + } + good := string("bG"[b2i(g.Good1)]) + string("bG"[b2i(g.Good2)]) + got := [5]int{g.Examples, g.CounterExamples, g.ExampleTaxa, g.CounterTaxa, g.Identified} + if got != w.v || good != w.good { + if bad++; bad <= 3 { + t.Errorf("pair %s/%s: got %s %v, ecoPrimers %s %v", g.Primer1, g.Primer2, good, got, w.good, w.v) + } + } + } +} diff --git a/pkg/obiprimer/testdata/ecoprimers_Cyprinidae_I1.pairs.tsv b/pkg/obiprimer/testdata/ecoprimers_Cyprinidae_I1.pairs.tsv new file mode 100644 index 0000000..ae1da51 --- /dev/null +++ b/pkg/obiprimer/testdata/ecoprimers_Cyprinidae_I1.pairs.tsv @@ -0,0 +1,173 @@ +AGCAATCCAGGTCAGTTT TGCCACCCCAGTATGTCC bG 309 454 301 452 197 +GCAATCCAGGTCAGTTTC TGCCACCCCAGTATGTCC bG 309 455 301 453 197 +AGCAATCCAGGTCAGTTT CCACCCCAGTATGTCCTT bG 311 484 302 482 197 +CCACCCCAGTATGTCCTT GCAATCCAGGTCAGTTTC Gb 311 485 302 483 197 +AGCAATCCAGGTCAGTTT GCCACCCCAGTATGTCCT bG 308 445 300 444 196 +GCAATCCAGGTCAGTTTC GCCACCCCAGTATGTCCT bG 308 446 300 445 196 +ACCGGAGCAATCCAGGTC TGCCACCCCAGTATGTCC bG 308 453 300 451 196 +GAGCAATCCAGGTCAGTT TGCCACCCCAGTATGTCC bG 308 453 300 451 196 +AGACCGGAGCAATCCAGG CCACCCCAGTATGTCCTT bG 310 483 301 481 196 +CCACCCCAGTATGTCCTT GAGCAATCCAGGTCAGTT Gb 310 483 301 481 196 +ACCGGAGCAATCCAGGTC CCACCCCAGTATGTCCTT bG 310 484 301 482 196 +CCACCCCAGTATGTCCTT GACCGGAGCAATCCAGGT Gb 310 484 301 482 196 +CCACCCCAGTATGTCCTT GGTCAGTTTCTATCTGTA Gb 309 460 300 459 195 +AGGTCAGTTTCTATCTGT TGCCACCCCAGTATGTCC bG 307 438 299 437 195 +CAGGTCAGTTTCTATCTG TGCCACCCCAGTATGTCC bG 307 440 299 439 195 +GGTCAGTTTCTATCTGTA TGCCACCCCAGTATGTCC bG 307 441 299 440 195 +GAGCAATCCAGGTCAGTT GCCACCCCAGTATGTCCT bG 307 444 299 443 195 +ACCGGAGCAATCCAGGTC GCCACCCCAGTATGTCCT bG 307 445 299 444 195 +GACCGGAGCAATCCAGGT GCCACCCCAGTATGTCCT bG 307 445 299 444 195 +AGGTCAGTTTCTATCTGT CCACCCCAGTATGTCCTT bG 309 469 300 467 195 +CAGGTCAGTTTCTATCTG CCACCCCAGTATGTCCTT bG 309 470 300 468 195 +GGAGCAATCCAGGTCAGT TGCCACCCCAGTATGTCC bG 307 454 299 452 195 +CCAGGTCAGTTTCTATCT TGCCACCCCAGTATGTCC bG 307 455 299 453 195 +AATCCAGGTCAGTTTCTA TGCCACCCCAGTATGTCC bG 307 455 299 453 195 +CAATCCAGGTCAGTTTCT TGCCACCCCAGTATGTCC bG 307 455 299 453 195 +ATCCAGGTCAGTTTCTAT TGCCACCCCAGTATGTCC bG 307 455 299 453 195 +TCCAGGTCAGTTTCTATC TGCCACCCCAGTATGTCC bG 307 455 299 453 195 +CCACCCCAGTATGTCCTT GGAGCAATCCAGGTCAGT Gb 309 484 300 482 195 +CAATCCAGGTCAGTTTCT CCACCCCAGTATGTCCTT bG 309 485 300 483 195 +AATCCAGGTCAGTTTCTA CCACCCCAGTATGTCCTT bG 309 485 300 483 195 +ATCCAGGTCAGTTTCTAT CCACCCCAGTATGTCCTT bG 309 485 300 483 195 +CCACCCCAGTATGTCCTT TCCAGGTCAGTTTCTATC Gb 309 485 300 483 195 +CCACCCCAGTATGTCCTT CCAGGTCAGTTTCTATCT Gb 309 485 300 483 195 +GCCACCCCAGTATGTCCT GGTCAGTTTCTATCTGTA Gb 306 437 298 437 194 +AGGTCAGTTTCTATCTGT GCCACCCCAGTATGTCCT bG 306 440 298 439 194 +CAGGTCAGTTTCTATCTG GCCACCCCAGTATGTCCT bG 306 442 298 441 194 +GCCACCCCAGTATGTCCT GGAGCAATCCAGGTCAGT Gb 306 445 298 444 194 +GCCACCCCAGTATGTCCT TCCAGGTCAGTTTCTATC Gb 306 446 298 445 194 +CCAGGTCAGTTTCTATCT GCCACCCCAGTATGTCCT bG 306 446 298 445 194 +CAATCCAGGTCAGTTTCT GCCACCCCAGTATGTCCT bG 306 446 298 445 194 +AATCCAGGTCAGTTTCTA GCCACCCCAGTATGTCCT bG 306 446 298 445 194 +ATCCAGGTCAGTTTCTAT GCCACCCCAGTATGTCCT bG 306 446 298 445 194 +CGGAGCAATCCAGGTCAG TGCCACCCCAGTATGTCC bG 304 445 296 444 192 +CCGGAGCAATCCAGGTCA TGCCACCCCAGTATGTCC bG 304 445 296 444 192 +TGCCACCCCAGTATGTCC TTTTCCTAGTACGAAAGG Gb 308 453 300 452 192 +CCACCCCAGTATGTCCTT CCGGAGCAATCCAGGTCA Gb 306 476 297 474 192 +CCACCCCAGTATGTCCTT CGGAGCAATCCAGGTCAG Gb 306 476 297 474 192 +GACCGGAGCAATCCAGGT TGCCACCCCAGTATGTCC bG 304 453 296 451 192 +CCACCCCAGTATGTCCTT GTCAGTTTCTATCTGTAA Gb 304 408 295 407 190 +CCACCCCAGTATGTCCTT TTTTCCTAGTACGAAAGG Gb 310 484 301 482 192 +CAGACCGGAGCAATCCAG CCACCCCAGTATGTCCTT bG 306 484 297 482 192 +GTCAGTTTCTATCTGTAA TGCCACCCCAGTATGTCC bG 302 390 294 390 190 +AGCAATCCAGGTCAGTTT CCCAGTATGTCCTTATTT bG 301 310 292 309 187 +CCCAGTATGTCCTTATTT GCAATCCAGGTCAGTTTC Gb 301 311 292 310 187 +ACTTTTCCTAGTACGAAA TGCCACCCCAGTATGTCC bG 307 443 299 442 191 +GCCACCCCAGTATGTCCT TTTTCCTAGTACGAAAGG Gb 307 445 299 444 191 +CGGAGCAATCCAGGTCAG GCCACCCCAGTATGTCCT bG 303 443 295 442 191 +CCGGAGCAATCCAGGTCA GCCACCCCAGTATGTCCT bG 303 443 295 442 191 +CTTTTCCTAGTACGAAAG TGCCACCCCAGTATGTCC bG 307 448 299 447 191 +AGACCGGAGCAATCCAGG GCCACCCCAGTATGTCCT bG 303 445 295 444 191 +ACTTTTCCTAGTACGAAA CCACCCCAGTATGTCCTT bG 309 478 300 476 191 +CCACCCCAGTATGTCCTT CTTTTCCTAGTACGAAAG Gb 309 482 300 480 191 +GCCACCCCAGTATGTCCT GTCAGTTTCTATCTGTAA Gb 301 390 293 390 189 +AGACCGGAGCAATCCAGG CCCAGTATGTCCTTATTT bG 300 310 291 309 186 +CAGACCGGAGCAATCCAG CCCAGTATGTCCTTATTT bG 300 310 291 309 186 +CCCAGTATGTCCTTATTT GAGCAATCCAGGTCAGTT Gb 300 310 291 309 186 +ACCGGAGCAATCCAGGTC CCCAGTATGTCCTTATTT bG 300 311 291 310 186 +CCCAGTATGTCCTTATTT GACCGGAGCAATCCAGGT Gb 300 311 291 310 186 +CCCAGTATGTCCTTATTT TCAGACCGGAGCAATCCA Gb 300 311 291 310 186 +CCCAGTATGTCCTTATTT TTCAGACCGGAGCAATCC Gb 300 311 291 310 186 +CCCAGTATGTCCTTATTT GTTCAGACCGGAGCAATC Gb 300 311 291 310 186 +CTTTTCCTAGTACGAAAG GCCACCCCAGTATGTCCT bG 306 443 298 442 190 +ACTTTTCCTAGTACGAAA GCCACCCCAGTATGTCCT bG 306 443 298 442 190 +AGACCGGAGCAATCCAGG TGCCACCCCAGTATGTCC bG 302 453 294 451 190 +CCACCCCAGTATGTCCTT TCAGACCGGAGCAATCCA Gb 304 485 295 483 190 +AGGTCAGTTTCTATCTGT CCCAGTATGTCCTTATTT bG 299 309 290 308 185 +CAGGTCAGTTTCTATCTG CCCAGTATGTCCTTATTT bG 299 309 290 308 185 +CCCAGTATGTCCTTATTT GGTCAGTTTCTATCTGTA Gb 299 309 290 308 185 +ATCCAGGTCAGTTTCTAT CCCAGTATGTCCTTATTT bG 299 311 290 310 185 +CCAGGTCAGTTTCTATCT CCCAGTATGTCCTTATTT bG 299 311 290 310 185 +CCCAGTATGTCCTTATTT GGAGCAATCCAGGTCAGT Gb 299 311 290 310 185 +CCCAGTATGTCCTTATTT TCCAGGTCAGTTTCTATC Gb 299 311 290 310 185 +CAATCCAGGTCAGTTTCT CCCAGTATGTCCTTATTT bG 299 311 290 310 185 +AATCCAGGTCAGTTTCTA CCCAGTATGTCCTTATTT bG 299 311 290 310 185 +TGCCACCCCAGTATGTCC TTTCCTAGTACGAAAGGA Gb 305 449 297 447 189 +CAGACCGGAGCAATCCAG GCCACCCCAGTATGTCCT bG 301 446 293 445 189 +CCACCCCAGTATGTCCTT TTTCCTAGTACGAAAGGA Gb 307 479 298 477 189 +CCACCCCAGTATGTCCTT TACGAAAGGATCGGAAAA Gb 307 453 298 452 188 +CCCAGTATGTCCTTATTT CCGGAGCAATCCAGGTCA Gb 298 309 289 308 184 +CCCAGTATGTCCTTATTT CGGAGCAATCCAGGTCAG Gb 298 309 289 308 184 +GCCACCCCAGTATGTCCT TTTCCTAGTACGAAAGGA Gb 304 440 296 439 188 +GCAATTTACCATGCTCTG TTTTCCTAGTACGAAAGG Gb 308 505 299 501 189 +ACTTTTCCTAGTACGAAA GCAATTTACCATGCTCTG bG 307 478 298 474 188 +CAGACCGGAGCAATCCAG TGCCACCCCAGTATGTCC bG 300 454 292 452 188 +CCTAGTACGAAAGGATCG TGCCACCCCAGTATGTCC bG 303 427 295 425 187 +CCACCCCAGTATGTCCTT TTCAGACCGGAGCAATCC Gb 302 485 293 483 188 +TACGAAAGGATCGGAAAA TGCCACCCCAGTATGTCC bG 304 430 296 430 187 +GCCACCCCAGTATGTCCT TACGAAAGGATCGGAAAA Gb 304 430 296 430 187 +CCACCCCAGTATGTCCTT CCTAGTACGAAAGGATCG Gb 305 459 296 457 187 +CTTTTCCTAGTACGAAAG GCAATTTACCATGCTCTG bG 307 498 298 494 188 +CGCAATTTACCATGCTCT TTTTCCTAGTACGAAAGG Gb 306 463 297 459 187 +GCAATTTACCATGCTCTG TTTCCTAGTACGAAAGGA Gb 307 506 298 502 188 +GCAATTTACCATGCTCTG GGTCAGTTTCTATCTGTA Gb 301 433 292 429 186 +GCCACCCCAGTATGTCCT TCAGACCGGAGCAATCCA Gb 299 446 291 445 187 +CCTAGTACGAAAGGATCG GCCACCCCAGTATGTCCT bG 302 419 294 418 186 +CCCAGTATGTCCTTATTT GTCAGTTTCTATCTGTAA Gb 295 264 286 263 181 +ACTTTTCCTAGTACGAAA CGCAATTTACCATGCTCT bG 305 449 296 445 186 +TCGCAATTTACCATGCTC TTTTCCTAGTACGAAAGG Gb 305 456 296 452 186 +TTCGCAATTTACCATGCT TTTTCCTAGTACGAAAGG Gb 305 456 296 452 186 +AGTTCAGACCGGAGCAAT CCCAGTATGTCCTTATTT bG 296 310 287 309 182 +CGCAATTTACCATGCTCT CTTTTCCTAGTACGAAAG Gb 305 460 296 456 186 +CGCAATTTACCATGCTCT TTTCCTAGTACGAAAGGA Gb 305 463 296 459 186 +CCTAGTACGAAAGGATCG GCAATTTACCATGCTCTG bG 305 472 296 468 186 +TCAGACCGGAGCAATCCA TGCCACCCCAGTATGTCC bG 298 454 290 452 186 +ACTTTTCCTAGTACGAAA TCGCAATTTACCATGCTC bG 304 447 295 443 185 +ACTTTTCCTAGTACGAAA TTCGCAATTTACCATGCT bG 304 447 295 443 185 +CTTTTCCTAGTACGAAAG TCGCAATTTACCATGCTC bG 304 453 295 449 185 +CTTTTCCTAGTACGAAAG TTCGCAATTTACCATGCT bG 304 453 295 449 185 +CCACCCCAGTATGTCCTT GTTCAGACCGGAGCAATC Gb 300 485 291 483 186 +TCGCAATTTACCATGCTC TTTCCTAGTACGAAAGGA Gb 304 456 295 452 185 +TTTCGCAATTTACCATGC TTTTCCTAGTACGAAAGG Gb 304 456 295 452 185 +TTCGCAATTTACCATGCT TTTCCTAGTACGAAAGGA Gb 304 456 295 452 185 +GCAATTTACCATGCTCTG TACGAAAGGATCGGAAAA Gb 304 427 295 425 184 +CCTAGTACGAAAGGATCG CGCAATTTACCATGCTCT bG 303 433 294 429 184 +AGGTCAGTTTCTATCTGT GCAATTTACCATGCTCTG bG 299 431 290 427 184 +GCCACCCCAGTATGTCCT TTCAGACCGGAGCAATCC Gb 297 446 289 445 185 +ACTTTTCCTAGTACGAAA TTTCGCAATTTACCATGC bG 303 447 294 443 184 +GCAATTTACCATGCTCTG GTCAGTTTCTATCTGTAA Gb 295 375 286 373 182 +CTTTTCCTAGTACGAAAG TTTCGCAATTTACCATGC bG 303 453 294 449 184 +TTTCCTAGTACGAAAGGA TTTCGCAATTTACCATGC bG 303 456 294 452 184 +CCCAGTATGTCCTTATTT TTTTCCTAGTACGAAAGG Gb 300 311 291 310 180 +CCCAGTATGTCCTTATTT GAGTTCAGACCGGAGCAA Gb 294 310 285 309 180 +CCTAGTACGAAAGGATCG TCGCAATTTACCATGCTC bG 302 430 293 426 183 +CCTAGTACGAAAGGATCG TTCGCAATTTACCATGCT bG 302 430 293 426 183 +CGCAATTTACCATGCTCT TACGAAAGGATCGGAAAA Gb 302 420 293 418 182 +ACTTTTCCTAGTACGAAA CCCAGTATGTCCTTATTT bG 299 311 290 310 179 +CCCAGTATGTCCTTATTT CTTTTCCTAGTACGAAAG Gb 299 311 290 310 179 +CGCAATTTACCATGCTCT GGTCAGTTTCTATCTGTA Gb 297 426 288 422 182 +CCTAGTACGAAAGGATCG TTTCGCAATTTACCATGC bG 301 430 292 426 182 +CAGGTCAGTTTCTATCTG GCAATTTACCATGCTCTG bG 297 431 288 427 182 +TACGAAAGGATCGGAAAA TTCGCAATTTACCATGCT bG 301 418 292 416 181 +TACGAAAGGATCGGAAAA TCGCAATTTACCATGCTC bG 301 418 292 416 181 +GGCCTTTCGCAATTTACC TTTTCCTAGTACGAAAGG Gb 302 510 293 506 183 +ACTTTTCCTAGTACGAAA GGCCTTTCGCAATTTACC bG 301 479 292 475 182 +TACGAAAGGATCGGAAAA TTTCGCAATTTACCATGC bG 300 418 291 416 180 +CCCAGTATGTCCTTATTT TTTCCTAGTACGAAAGGA Gb 297 310 288 309 177 +CTTTTCCTAGTACGAAAG GGCCTTTCGCAATTTACC bG 301 500 292 496 182 +AGGTCAGTTTCTATCTGT CGCAATTTACCATGCTCT bG 295 428 286 424 180 +GGCCTTTCGCAATTTACC TTTCCTAGTACGAAAGGA Gb 301 511 292 507 182 +GCCTTTCGCAATTTACCA TTTTCCTAGTACGAAAGG Gb 299 440 290 436 180 +CTTTCGCAATTTACCATG TTTTCCTAGTACGAAAGG Gb 298 412 289 408 179 +CCCAGTATGTCCTTATTT CCTAGTACGAAAGGATCG Gb 296 305 287 304 176 +CCCAGTATGTCCTTATTT TACGAAAGGATCGGAAAA Gb 297 307 288 306 176 +GGTCAGTTTCTATCTGTA TCGCAATTTACCATGCTC bG 294 425 285 421 179 +ACTTTTCCTAGTACGAAA GCCTTTCGCAATTTACCA bG 298 436 289 432 179 +CTTTTCCTAGTACGAAAG GCCTTTCGCAATTTACCA bG 298 438 289 434 179 +GCCTTTCGCAATTTACCA TTTCCTAGTACGAAAGGA Gb 298 440 289 436 179 +CCTAGTACGAAAGGATCG GGCCTTTCGCAATTTACC bG 299 484 290 480 180 +ACTTTTCCTAGTACGAAA CTTTCGCAATTTACCATG bG 297 411 288 407 178 +CTTTCGCAATTTACCATG CTTTTCCTAGTACGAAAG Gb 297 411 288 407 178 +CTTTCGCAATTTACCATG TTTCCTAGTACGAAAGGA Gb 297 412 288 408 178 +GGCCTTTCGCAATTTACC TACGAAAGGATCGGAAAA Gb 298 425 289 423 178 +AGTTCAGACCGGAGCAAT CCACCCCAGTATGTCCTT bG 293 484 284 482 179 +CCTAGTACGAAAGGATCG GCCTTTCGCAATTTACCA bG 296 416 287 412 177 +CCTAGTACGAAAGGATCG CTTTCGCAATTTACCATG bG 295 390 286 386 176 +CCAGGTCAGTTTCTATCT GCAATTTACCATGCTCTG bG 294 513 285 509 179 +CCTTTCGCAATTTACCAT TTTTCCTAGTACGAAAGG Gb 295 411 286 407 176 +GCAATTTACCATGCTCTG TTCCTAGTACGAAAGGAT Gb 293 423 284 419 176 +CTTTCGCAATTTACCATG TACGAAAGGATCGGAAAA Gb 295 398 286 396 175 +ACTTTTCCTAGTACGAAA CCTTTCGCAATTTACCAT bG 294 409 285 405 175 +CCTTTCGCAATTTACCAT CTTTTCCTAGTACGAAAG Gb 294 409 285 405 175 +CCTTTCGCAATTTACCAT TTTCCTAGTACGAAAGGA Gb 294 410 285 406 175 +GCCTTTCGCAATTTACCA TACGAAAGGATCGGAAAA Gb 295 413 286 411 175 diff --git a/pkg/obiseq/taxonomy_methods.go b/pkg/obiseq/taxonomy_methods.go index e11356d..4934bcb 100644 --- a/pkg/obiseq/taxonomy_methods.go +++ b/pkg/obiseq/taxonomy_methods.go @@ -176,6 +176,10 @@ func (sequence *BioSequence) SetFamily(taxonomy *obitax.Taxonomy) *obitax.Taxon return sequence.SetTaxonAtRank(taxonomy, "family") } +// TaxonomicPathAttribute is the attribute holding the taxonomic path of a +// sequence, from the root to its taxon (see obiannotate --taxonomic-path). +const TaxonomicPathAttribute = "taxonomic_path" + func (sequence *BioSequence) SetPath(taxonomy *obitax.Taxonomy) []string { taxon := sequence.Taxon(taxonomy) path := taxon.Path() @@ -186,13 +190,13 @@ func (sequence *BioSequence) SetPath(taxonomy *obitax.Taxonomy) []string { spath[lpath-i] = path.Get(i).FullString(taxonomy.Code()) } - sequence.SetAttribute("taxonomic_path", spath) + sequence.SetAttribute(TaxonomicPathAttribute, spath) return spath } func (sequence *BioSequence) Path() []string { - path, ok := sequence.GetAttribute("taxonomic_path") + path, ok := sequence.GetAttribute(TaxonomicPathAttribute) if !ok { if taxo := obitax.DefaultTaxonomy(); taxo != nil { diff --git a/pkg/obitools/obiprimers/embedded_test.go b/pkg/obitools/obiprimers/embedded_test.go index daf0b3e..8f6f7ed 100644 --- a/pkg/obitools/obiprimers/embedded_test.go +++ b/pkg/obitools/obiprimers/embedded_test.go @@ -139,3 +139,59 @@ func TestPrimerPairsNeedsATaxonomy(t *testing.T) { t.Error("a nil taxonomy must be refused") } } + +// hasPath reports whether any sequence still carries its taxonomic path, and +// how many still have their taxid (which the search needs). +func scanSequences(it obiiter.IBioSequence) (withPath, withTaxid, total int) { + for it.Next() { + batch := it.Get() + for _, s := range batch.Slice() { + total++ + if s.HasAttribute(obiseq.TaxonomicPathAttribute) { + withPath++ + } + if s.HasAttribute("taxid") { + withTaxid++ + } + } + } + return +} + +// Once the taxonomy is built the paths only take memory: they are dropped, +// whatever the way the taxonomy was obtained, and nothing else is. +func TestTaxonomicPathIsDroppedAfterExtraction(t *testing.T) { + path := writeTemp(t, embeddedFasta) + + sequences, _, err := readSequencesAndTaxonomy([]string{path}, nil) // two passes over a file + if err != nil { + t.Fatal(err) + } + if withPath, withTaxid, total := scanSequences(sequences); total != 3 || withPath != 0 || withTaxid != 3 { + t.Errorf("file: %d sequences, %d with a path left, %d with a taxid", total, withPath, withTaxid) + } + + other := obitax.NewTaxonomy("other", "other", obitax.DefaultTaxidAlphabet) // -t given + sequences, _, err = readSequencesAndTaxonomy([]string{path}, other) + if err != nil { + t.Fatal(err) + } + if withPath, _, total := scanSequences(sequences); total != 3 || withPath != 0 { + t.Errorf("with -t: %d sequences, %d with a path left", total, withPath) + } + + var slice obiseq.BioSequenceSlice // standard input + for _, id := range []string{"a", "b"} { + seq := obiseq.NewBioSequence(id, []byte("acgtacgt"), "") + seq.SetAttribute("taxid", "taxon:10 [X]@species") + seq.SetAttribute(obiseq.TaxonomicPathAttribute, []string{"taxon:1 [root]@no rank", "taxon:10 [X]@species"}) + slice = append(slice, seq) + } + sequences, _, err = embeddedTaxonomyInMemory(obiiter.IBatchOver("stdin", slice, 10)) + if err != nil { + t.Fatal(err) + } + if withPath, withTaxid, total := scanSequences(sequences); total != 2 || withPath != 0 || withTaxid != 2 { + t.Errorf("stdin: %d sequences, %d with a path left, %d with a taxid", total, withPath, withTaxid) + } +} diff --git a/pkg/obitools/obiprimers/obiprimers.go b/pkg/obitools/obiprimers/obiprimers.go index 84f5a7a..21d036e 100644 --- a/pkg/obitools/obiprimers/obiprimers.go +++ b/pkg/obitools/obiprimers/obiprimers.go @@ -6,6 +6,7 @@ import ( "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obidefault" "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiiter" "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiprimer" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq" "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obitax" "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obitools/obiconvert" log "github.com/sirupsen/logrus" @@ -14,6 +15,16 @@ import ( const noTaxonomyAdvice = "no taxonomy: use the -t option, or give sequences annotated with their " + "taxonomic path (obiannotate --taxonomic-path)" +// dropTaxonomicPath deletes the taxonomic path of the sequences. The primer +// search needs the taxa of the sequences, which it reads from their taxid, not +// their path: once the taxonomy is built, the paths (a list of about twenty +// strings per sequence) only take memory. +func dropTaxonomicPath(sequences obiiter.IBioSequence) obiiter.IBioSequence { + return sequences.MakeIWorker(obiseq.AnnotatorToSeqWorker(func(s *obiseq.BioSequence) { + s.DeleteAttribute(obiseq.TaxonomicPathAttribute) + }), false) +} + // embeddedTaxonomyInMemory extracts the taxonomy from the taxonomic paths // carried by the sequences, which it has to hold in memory since the iterator // cannot be read twice. It returns an iterator over the same sequences. @@ -23,6 +34,10 @@ func embeddedTaxonomyInMemory(sequences obiiter.IBioSequence) (obiiter.IBioSeque if err != nil { return obiiter.NilIBioSequence, nil, fmt.Errorf("%s: %w", noTaxonomyAdvice, err) } + // every sequence is held in memory here: free the paths right away + for _, s := range slice { + s.DeleteAttribute(obiseq.TaxonomicPathAttribute) + } return obiiter.IBatchOver(source, slice, obidefault.BatchSize()), taxonomy, nil } @@ -39,7 +54,10 @@ func embeddedTaxonomyInMemory(sequences obiiter.IBioSequence) (obiiter.IBioSeque func readSequencesAndTaxonomy(args []string, given *obitax.Taxonomy) (obiiter.IBioSequence, *obitax.Taxonomy, error) { if given != nil { sequences, err := obiconvert.CLIReadBioSequences(args...) - return sequences, given, err + if err != nil { + return obiiter.NilIBioSequence, nil, err + } + return dropTaxonomicPath(sequences), given, nil } if len(args) == 0 { @@ -66,7 +84,10 @@ func readSequencesAndTaxonomy(args []string, given *obitax.Taxonomy) (obiiter.IB log.Infof("Embedded taxonomy extracted: %d taxa", taxonomy.Len()) sequences, err := obiconvert.CLIReadBioSequences(args...) - return sequences, taxonomy, err + if err != nil { + return obiiter.NilIBioSequence, nil, err + } + return dropTaxonomicPath(sequences), taxonomy, nil } // CLIReadSequencesAndTaxonomy reads the sequences of the command line and diff --git a/pkg/obiutils/set.go b/pkg/obiutils/set.go index c6c71b3..d73ad90 100644 --- a/pkg/obiutils/set.go +++ b/pkg/obiutils/set.go @@ -1,6 +1,10 @@ package obiutils -import "fmt" +import ( + "fmt" + "reflect" + "slices" +) type Set[E comparable] map[E]struct{} @@ -61,12 +65,43 @@ func (s Set[E]) Members() []E { return result } +// compareMembers orders two set members: numbers by value, strings +// alphabetically, anything else by its printed form. It is only used to give +// the string representation of a set a stable order. +func compareMembers(a, b any) int { + va, vb := reflect.ValueOf(a), reflect.ValueOf(b) + switch { + case va.CanInt() && vb.CanInt(): + return cmpOrdered(va.Int(), vb.Int()) + case va.CanUint() && vb.CanUint(): + return cmpOrdered(va.Uint(), vb.Uint()) + case va.CanFloat() && vb.CanFloat(): + return cmpOrdered(va.Float(), vb.Float()) + case va.Kind() == reflect.String && vb.Kind() == reflect.String: + return cmpOrdered(va.String(), vb.String()) + } + return cmpOrdered(fmt.Sprint(a), fmt.Sprint(b)) +} + +func cmpOrdered[T int64 | uint64 | float64 | string](a, b T) int { + switch { + case a < b: + return -1 + case a > b: + return 1 + } + return 0 +} + // String returns a string representation of the set. // -// It returns a string representation of the set by formatting the set's members using the fmt.Sprintf function. -// The resulting string is then returned. +// The members are formatted with fmt, in increasing order: a Go map has no +// order, and a representation changing from one call to the next is of no use +// to print, log or compare. func (s Set[E]) String() string { - return fmt.Sprintf("%v", s.Members()) + members := s.Members() + slices.SortFunc(members, func(a, b E) int { return compareMembers(a, b) }) + return fmt.Sprintf("%v", members) } // Union returns a new set that is the union of the current set and the specified set. diff --git a/pkg/obiutils/set_test.go b/pkg/obiutils/set_test.go index 1754bbb..fa6688c 100644 --- a/pkg/obiutils/set_test.go +++ b/pkg/obiutils/set_test.go @@ -300,3 +300,29 @@ func TestIntersection(t *testing.T) { t.Errorf("Intersection of two sets with no common elements returned incorrect result. Expected: %v, got: %v", expectedResult4, result4) } } + +// The representation of a set must not depend on the (random) order of the map. +func TestSetStringIsStable(t *testing.T) { + ints := NewSet(30, 4, 100, 2, 7, 15, 1) + for i := 0; i < 50; i++ { + if got := ints.String(); got != "[1 2 4 7 15 30 100]" { + t.Fatalf("ints: %s", got) + } + } + if got := NewSet("pear", "apple", "fig").String(); got != "[apple fig pear]" { + t.Errorf("strings: %s", got) + } + if got := NewSet(2.5, -1.0, 0.25).String(); got != "[-1 0.25 2.5]" { + t.Errorf("floats: %s", got) + } + type id int + if got := NewSet(id(3), id(1), id(2)).String(); got != "[1 2 3]" { + t.Errorf("named integer type: %s", got) + } + type pair struct{ a, b int } + for i := 0; i < 20; i++ { // no natural order: still stable + if got := NewSet(pair{2, 1}, pair{1, 2}, pair{1, 1}).String(); got != "[{1 1} {1 2} {2 1}]" { + t.Fatalf("structs: %s", got) + } + } +} From d6dd39f878b5b56936adf17395acf84db62f50b5 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Tue, 3 Nov 2026 21:12:04 +1100 Subject: [PATCH 21/24] Introduce sequence roles and filtering logic Implement new data structures for corpus management, multi-match filtering, and barcode resolution. --- OBIPRIMERS_MEMORY_PROMPT.md | 149 ++++++++++ OBIPRIMERS_NOTES.md | 137 ++++++++- pkg/obiprimer/amplicons.go | 232 +++++++++++++++ pkg/obiprimer/corpus.go | 41 ++- pkg/obiprimer/match.go | 8 +- pkg/obiprimer/multimatch.go | 11 +- pkg/obiprimer/pairs.go | 3 + pkg/obiprimer/pairs_eval.go | 323 ++++++--------------- pkg/obiprimer/pairs_test.go | 35 ++- pkg/obiprimer/resolution.go | 133 +++++++++ pkg/obiprimer/resolution_test.go | 153 ++++++++++ pkg/obiprimer/roles.go | 13 +- pkg/obiprimer/search.go | 2 +- pkg/obiprimer/search_real_test.go | 55 ++-- pkg/obiprimer/search_test.go | 81 +++++- pkg/obiprimer/taxa.go | 5 +- pkg/obiprimer/words_test.go | 12 +- pkg/obitools/obiprimers/embedded_test.go | 2 +- pkg/obitools/obiprimers/obiprimers.go | 8 +- pkg/obitools/obiprimers/obiprimers_test.go | 6 +- pkg/obitools/obiprimers/options.go | 8 +- pkg/obitools/obiprimers/output.go | 8 +- 22 files changed, 1083 insertions(+), 342 deletions(-) create mode 100644 OBIPRIMERS_MEMORY_PROMPT.md create mode 100644 pkg/obiprimer/amplicons.go create mode 100644 pkg/obiprimer/resolution.go create mode 100644 pkg/obiprimer/resolution_test.go diff --git a/OBIPRIMERS_MEMORY_PROMPT.md b/OBIPRIMERS_MEMORY_PROMPT.md new file mode 100644 index 0000000..a9666dd --- /dev/null +++ b/OBIPRIMERS_MEMORY_PROMPT.md @@ -0,0 +1,149 @@ +# Prompt : optimisation de l'usage mémoire de `obiprimers` + +Tu travailles dans le dépôt Go `/Users/coissac/Sync/travail/__MOI__/GO/obitools4` (module +`git.metabarcoding.org/obitools/obitools4/obitools4`, Go : `/usr/local/go/bin/go`, non présent dans le PATH). +Ta mission : **réduire l'usage mémoire de la commande `obiprimers`** sans changer ses résultats, en procédant +par la mesure. + +## 1. Contexte + +`obiprimers` est la ré-implémentation OBITools4 d'ecoPrimers (design de paires d'amorces à partir de génomes +entiers, avec évaluation de la qualité du marqueur délimité). Le mode « voie A » est terminé et validé. +Avant toute chose, **lis `OBIPRIMERS_NOTES.md`** (racine du dépôt) : la section « État courant », puis les +sections 11 à 13 (appariement approché), 16 et 16 bis (taxonomie embarquée ; **une erreur de mesure à ne pas +refaire**), 25 (définitions de Bc, Bs, Bd et élagage de la phase 2). + +Code concerné : +- `cmd/obitools/obiprimers/main.go`, `pkg/obitools/obiprimers/` (options, lecture, sortie CSV) ; +- `pkg/obiprimer/` : `corpus.go` (`LoadCorpus`), `taxa.go`, `words.go` (`FindStrictWords`), `match.go` et + `match_seed.go` (`MatchWords`), `pairs.go` (`selectPrimers`, `countPairs`), `pairs_eval.go` (`FindPairs`), + `amplicons.go` (`collectAmplicons`), `resolution.go`, `search.go` (`Search`) ; +- `pkg/obikmer/seqquorum*.go` (`SeqKmerCounter`, comptage par partitions sur disque) ; +- `pkg/obithermo/` (Tm), code partagé : `pkg/obiformats`, `pkg/obiiter`, `pkg/obiseq`, `pkg/obitax`. + +## 2. Décisions de l'utilisateur à respecter (non négociables) + +- **Tout est chargé en mémoire** : toutes les séquences de la base (cibles, contre-exemples, non-cibles) sont + gardées, parce que la lecture sur l'**entrée standard** doit continuer à marcher comme pour tout obitool. + Tu peux **compacter** ce qui est gardé ; tu ne peux pas passer à un schéma qui exige de relire le fichier. +- **Un seul calcul par indice** : ne touche pas aux définitions de Bc, Bs et Bd (`resolution.go`). Pas de + colonne ou d'option alternative. +- **Les résultats ne changent pas.** Critère : la sortie CSV doit rester **identique octet pour octet** sur les + cas de référence du §4. Si tu penses qu'un changement de résultat est justifié, **demande avant**. +- Aucune commande git qui modifie quoi que ce soit (pas de commit, add, mv, rm, reset…). Lecture seule + (`status`, `diff`, `log`) autorisée. Déplace les fichiers avec `mv`/`Write`/`Edit`. +- Fichiers source de moins d'environ 500 lignes. Pas de `.collect()` d'une grande structure quand un flux + suffit. Réutilise l'existant avant d'écrire du code ; enrichis une brique partagée plutôt que d'écrire du + code ad hoc, **mais** toute modification d'un paquet partagé (obiformats, obiiter, obiseq, obitax) doit + être signalée clairement et validée par les tests de ces paquets. +- **Pas de spéculation présentée comme un fait** : ce qui n'est pas mesuré est une hypothèse, dis-le. + Une régression de justesse est pire qu'un gain de mémoire : avant d'échanger l'une contre l'autre, demande. + +## 3. Ce qui est mesuré aujourd'hui (macOS, M3 Max, 16 cœurs) + +Jeu : 7 822 génomes mitochondriaux de vertébrés (131 Mb de bases), `-r 7953` (Cyprinidae : 325 cibles, 7 497 +non-cibles). Commande : +`obiprimers -r 7953 -e 3 --three-prime 2 -l 30 -L 150 -c -o out.csv` → 3 247 paires +(3 248 lignes avec l'en-tête). + +| Mesure | Valeur | +|---|---| +| pic RSS, taxonomie embarquée, fichier | **1 217 à 1 372 Mo** (bruit d'environ ±100 Mo d'une exécution à l'autre), 4,7 à 5,3 s | +| pic RSS, taxonomie embarquée, entrée standard | médiane **1 334 à 1 346 Mo** sur 8 exécutions entrelacées | +| tas des 7 822 séquences chargées | **193,7 Mo** (dont `taxonomic_path` : 19,2 Mo, soit 2,46 Ko par séquence) | +| même exécution **sans** l'élagage de phase 2 | 5 604 Mo (c'est ce qu'on a corrigé, voir §25 des notes) | +| avec `-t` (taxonomie NCBI complète) | +1,07 Go pour le seul chargeur (mesuré avec `obitaxonomy`), pic ~2,8 Go (mesuré avant les nouvelles définitions) | +| `GOGC=25` (ancienne mesure, `-t` NCBI) | pic 2,82 Go → 1,98 Go, mais temps CPU utilisateur ×2 | +| ecoPrimers (C) sur les mêmes jeux | 1,86 Go (Cyprinidae), 2,11 Go (Teleostei) | + +Tailles d'enregistrements : `ampRecord` 24 octets, `Match` 12, `seqMatch` 12, `hit` (intermédiaire de +`matchCorpus`) 16. Environ 6,85 millions d'occurrences approchées quand toutes les séquences sont chargées +(cas `-i 1`). + +**Le constat qui motive la mission** : les séquences gardées pèsent ~194 Mo, le pic du processus est ~1,3 Go. +Les 1,1 Go restants ne sont **pas expliqués** (hypothèses ci-dessous, aucune n'est vérifiée). + +## 4. Garde-fous à mettre en place AVANT de modifier quoi que ce soit + +1. **Binaire de référence** : `cd /Users/coissac/Sync/travail/__MOI__/GO/obitools4 && + GOEXPERIMENT=simd /usr/local/go/bin/go build -o /tmp/obiprimers.baseline ./cmd/obitools/obiprimers` +2. **Données** (les fichiers de `/tmp` peuvent avoir disparu, regénère-les) : dossier du tutoriel + `D="$HOME/Documents/Enseignement/DNA metabarcoding spring school/ecoPrimers-Fish"` : `mito.vert.fasta.gz`, + `ncbitaxo_20250211.tgz`, et pour le test d'échelle `mito.all.fasta.gz` / `mito.one.fasta.gz`. Fichier annoté + (taxonomie embarquée, évite le chargeur NCBI) : + `/Users/coissac/travail/__MOI__/GO/obitools4/build/obiannotate -t "$D/ncbitaxo_20250211.tgz" + --taxonomic-path -Z -o /tmp/mito.vert.path.fasta.gz "$D/mito.vert.fasta.gz"` + (les binaires déjà compilés sont dans `/Users/coissac/travail/__MOI__/GO/obitools4/build/`, sans `Sync`). +3. **CSV de référence**, à produire avec le binaire de référence et à conserver : Cyprinidae `-r 7953`, + Teleostei `-r 32443`, un cas avec contre-exemples `-r 7953 -i 1`, un cas `-U`, un cas `-R NC_083213`, et la + lecture sur entrée standard (`gzcat fichier | obiprimers ...`). Après chaque modification : `cmp` octet pour + octet. +4. **Tests** : `go test ./pkg/obiprimer/ ./pkg/obitools/obiprimers/ ./pkg/obithermo/ ./pkg/obikmer/ -race + -count=1`, et les tests opt-in sur les données réelles (durent ~75 s hors `-race`) : + `OBIPRIMERS_TUTORIAL="$D" go test ./pkg/obiprimer/ -count=1`. +5. **Méthode de mesure** (le piège principal) : le pic RSS bouge de ±100 Mo entre deux exécutions identiques + (rythme du ramasse-miettes). Mesure **au moins 5 exécutions entrelacées avant/après et compare les + médianes** (`/usr/bin/time -l` : octets sur macOS). Pour attribuer la mémoire, ne te fie pas au RSS : utilise + des profils de tas (`--pprof` de la CLI sert `/debug/pprof/heap` sur `localhost:6060`, `net/http/pprof` est + importé ; ou `runtime/pprof.WriteHeapProfile` aux frontières d'étapes ; `go tool pprof -sample_index=inuse_space` + et `alloc_space` ; `GODEBUG=gctrace=1`). **Erreur déjà commise (voir la section 16 bis des notes)** : en + mesurant le tas libéré par la suppression d'un attribut, la tranche de séquences n'était plus référencée + après la boucle et le GC a libéré *tout* ; utilise `runtime.KeepAlive` sur ce que tu veux garder vivant. + +## 5. Pistes à évaluer (toutes des HYPOTHÈSES à confirmer par un profil, classées sans ordre) + +- **Retard du ramasse-miettes** (`GOGC=100` laisse le tas monter jusqu'à ~2× le vivant). Pistes : limite + mémoire douce (`debug.SetMemoryLimit`) ou `debug.SetGCPercent` ajustés par la commande après la lecture ; + mesurer le coût CPU (l'ancienne mesure avec `GOGC=25` doublait le temps utilisateur). +- **Lecture** (code partagé `obiformats`/`obiiter`) : 131 Mb sont analysés, avec une table d'attributs et des + chaînes JSON par séquence. L'option `--batch-mem` (lots limités en mémoire) existe déjà : l'évaluer. +- **Séquences gardées** : `BioSequence` garde 1 octet par base et une table d'attributs. Ne garder que + l'identifiant, la séquence et le `taxid` (retirer les autres attributs, comme déjà fait pour + `taxonomic_path`, dans `dropTaxonomicPath` de `pkg/obitools/obiprimers/obiprimers.go`) ; ou stocker la + séquence à 2 bits par base (~33 Mo pour 131 Mb) avec une liste d'exceptions pour les bases ambiguës. Ce + second point touche `forEachOccurrence`, `ampHash`, `windowAsIs`, `windowRevComp` et `MatchWords`, qui lisent + les octets : à ne tenter que si le profil le justifie. +- **Occurrences stockées deux fois** : `[]WordMatches` (`matchCorpus`, intermédiaire `hit` de 16 octets par + occurrence puis recopie) et `primerSet.perSeq` (`selectPrimers`). Vérifier à quel moment chacune est + libérée, construire directement les structures finales, présizer les tranches (les `append` en doublement + créent un pic transitoire). +- **Phase 2b** (`collectAmplicons` sur les seules paires survivantes) : `ampRecord` de 24 octets (16 possibles + en empaquetant le rôle), enregistrements par worker ; **traitement des paires survivantes par lots bornés** + si le nombre d'enregistrements estimé dépasse un budget (idée notée en 25.2). Cas dur : `-T 0` (aucune paire + élaguée), où les tests atteignent ~5,5 Go de tas. +- **`SeqKmerCounter`** (`pkg/obikmer/seqquorum*.go`) : environ 16 octets par occurrence de k-mer pour les + partitions en cours de traitement ; le nombre de partitions est **fixé à 256**, alors que le total de bases + est connu une fois le corpus chargé : le choisir automatiquement. +- **Lecture en deux passes** pour la taxonomie embarquée (`readSequencesAndTaxonomy`) : la première passe ne + garde que les chemins, mais analyse tout. +- **Chargeur de taxonomie NCBI** : 1,07 Go à lui seul ; c'est du code partagé (`obitax`), hors périmètre sauf + si tu y trouves une économie sûre ; sinon, documente-le dans l'aide (la taxonomie embarquée évite ce coût). + +## 6. Test d'échelle (jamais fait) + +Tout est validé sur des génomes mitochondriaux (5 à 131 Mb). Mesure la croissance sur un fichier plus gros +(`mito.all.fasta.gz`, 172 Mo compressés, avec `--restrict-to-taxon` sur un grand clade, p. ex. les Metazoa) : +temps et pic RSS en fonction de la taille, pour savoir ce qui grossit vite (corpus, occurrences, amplicons, +compteur de k-mers). + +## 7. Livrables + +1. Un **rapport de profil** : où va la mémoire à chaque étape (lecture, sélection, mots stricts, appariement, + paires, phase 2a/2b), avec les profils de tas et les chiffres, avant toute modification. +2. Les optimisations **classées par gain mesuré et par risque**, puis implémentées une par une, chacune avec + mesure avant/après (médianes d'au moins 5 exécutions entrelacées), `cmp` des CSV de référence, tests verts + sous `-race`. +3. Une **nouvelle section** à la fin de `OBIPRIMERS_NOTES.md` (ne réécris pas l'historique) : mesures, + décisions, pistes écartées et pourquoi, ce qui reste. Pas de commit. +4. Si une optimisation exige de changer un résultat, une décision ou un paquet partagé : **arrête-toi et + demande**. + +## 8. Pièges connus de l'environnement + +- zsh ne découpe pas une variable en arguments : utilise des tableaux (`A=(-r 7953 ...)` puis `"${A[@]}"`) + ou des scripts `bash`. +- macOS : `sed -i ''` (sans suffixe) ; `/usr/bin/time -l` donne le pic en **octets** (`maximum resident set size`). +- `OBIPRIMERS_WORKERS` n'est lue que par les tests opt-in ; la CLI utilise `--max-cpu`. +- Les avertissements C de `obiformats/fastseq_read.h` sont connus et sans gravité. +- `go test -race` sur les tests opt-in est lent (~90 s) ; réserve-le aux tests unitaires. +- Le test `obiutils/TestSetString` était instable : il est corrigé (tri des membres dans `Set.String`). diff --git a/OBIPRIMERS_NOTES.md b/OBIPRIMERS_NOTES.md index 81f35ec..bec957a 100644 --- a/OBIPRIMERS_NOTES.md +++ b/OBIPRIMERS_NOTES.md @@ -3,6 +3,32 @@ Ré-implémentation d'ecoPrimers dans obitools4. Ce fichier garde l'état des recherches, des mesures et de l'avancement. Dernière mise à jour : 2026-11-03. +## État courant (à lire en premier) + +**Les trois indices de qualité d'une paire** (définitions en vigueur depuis la section 25 ; elles remplacent +celles d'ecoPrimers, qui figurent encore dans les sections 1 à 18 comme historique) : + +| Indice | Définition | +|---|---| +| **Bc** | taxons cibles amplifiés / taxons cibles | +| **Bs** | taxons cibles **identifiés** / taxons cibles amplifiés. Un taxon cible est identifié si **tous** ses barcodes ne sont portés que par lui, parmi **toutes** les séquences amplifiées par la paire (cibles, non-cibles, contre-exemples). Un barcode partagé rend mal identifiés **les deux** taxons. | +| **Bd** (nouveau) | part des barcodes portés par les cibles qui mènent sans ambiguïté à **un seul** taxon | + +Un seul calcul pour chacun, aucune variante ni colonne alternative. Les trois sont dans **toutes** les sorties +CSV (`bc`, `bs`, `bd`). Toute séquence sans taxon au rang choisi est un taxon anonyme distinct (un barcode +qu'elle partage est ambigu). Le seuil `-T` reste à 0,6. + +**Rôles des séquences** : cible (exemple), contre-exemple, **non-cible** (toutes les autres, qui ne sont plus +écartées : elles sont chargées en mémoire et leurs amplicons comptent dans Bs et Bd, pas dans les quorums). +Pour exclure des séquences de l'analyse, filtrer l'entrée en amont (`obigrep`). + +**Conséquence pour les validations contre ecoPrimers (sections 12, 15, 17, 18)** : elles restent exactes sur +ce qu'elles comparent (paires, drapeaux good, séquences et taxons amplifiés, Bc, Yule, longueurs, GC, Tm), mais +**Bs et `identifiés` ne sont plus comparables** à ceux d'ecoPrimers ; ils sont validés autrement (section 25). + +Sections en attente de décision de l'utilisateur : voie B (21.4), statistique de Tm (22.3-a), nombre de partitions +automatique, mémoire, tests shell, documentation, test `-e 0` (ecoPrimers n'avait pas terminé). + Sources de référence : - Article : `~/Sync/travail/__MOI__/ecoprimers/Documents/Riaz et al. 2011 ... .pdf` (NAR 39:e145) - Code C : `~/Sync/travail/__MOI__/ecoprimers/src/` (binaire : `/usr/local/bin/ecoprimers`) @@ -1027,7 +1053,7 @@ qui le portent. Un taxon cible t est **bien identifié** s'il existe un amplicon 3. **Séquence non cible sans taxon au rang choisi** : la traiter comme un taxon anonyme distinct (prudent : elle empêche l'identification d'une cible qui partage son amplicon) ou l'ignorer ? Je propose le premier. -## 24. Règle d'identification de Bs, et l'indice BD (2026-11-03) +## 24. Règle d'identification de Bs, et l'indice Bd (2026-11-03) **Statut : spécification, aucun code.** Complète et corrige la section 23. @@ -1038,10 +1064,10 @@ qui le portent. Un taxon cible t est **bien identifié** s'il existe un amplicon 2. **Règle d'identification : une espèce est bien identifiée si TOUS ses barcodes ne sont associés qu'à elle.** Dès qu'un barcode est partagé entre deux espèces, **les deux** sont mal identifiées. Cette règle remplace celle de 23.4-2 (que j'avais proposé de garder). -3. **Nouvel indice BD (barcode discrimination)** proposé par l'utilisateur : la part des **barcodes** qui +3. **Nouvel indice Bd (barcode discrimination)** proposé par l'utilisateur : la part des **barcodes** qui mènent sans ambiguïté à une espèce cible. Exemple de l'utilisateur : A porte les barcodes A1 et A2, B porte A2 et B1. Ni A ni B ne sont bien identifiées (elles partagent A2), mais **A1 et B1 sont discriminants**, A2 - ne l'est pas. Ici Bs = 0/2 et BD = 2/3. (Avec la règle d'ecoPrimers on aurait Bs = 2/2.) + ne l'est pas. Ici Bs = 0/2 et Bd = 2/3. (Avec la règle d'ecoPrimers on aurait Bs = 2/2.) ### 24.2 ecoPrimers applique bien l'autre règle (vérifié) @@ -1051,7 +1077,7 @@ taxon est donc compté **dès qu'un** de ses amplicons lui est propre. Confirmat cette règle reproduit toutes les colonnes `identifiés` et Bs de plus de vingt tables d'ecoPrimers (sections 12, 15, 17, 18) ; (ii) recalcul indépendant avec `obipcr` + Python sur la paire n° 1 de `Cyprinidae.ecoprimers` (320 amplicons chez `obipcr`, 318 chez ecoPrimers qui impose `-3 2`) : règle « au moins un barcode unique » -239/312 = 0,766 contre 237/310 = 0,765 pour ecoPrimers ; règle « tous uniques » 238/312 = 0,763 ; **BD = +239/312 = 0,766 contre 237/310 = 0,765 pour ecoPrimers ; règle « tous uniques » 238/312 = 0,763 ; **Bd = 240/265 = 0,906** (265 barcodes distincts). **Portée de (ii)** : sur cette paire les deux règles ne diffèrent que d'**une** espèce sur 312, car les données du tutoriel ont un génome par espèce (4 espèces seulement portent plusieurs barcodes) : ce recalcul ne tranche pas entre les règles à lui seul ; la preuve est le code @@ -1064,7 +1090,7 @@ et les tables reproduites. - **Non-régression** : la règle changeant, l'égalité des colonnes `identifiés` et Bs avec ecoPrimers n'est plus atteignable, même avec un corpus restreint aux cibles et contre-exemples (annule la proposition de 23.3 sur ce point). Toutes les autres colonnes (séquences, taxons, Bc, Yule, longueurs, GC, Tm, drapeaux good) restent - comparables. Pour Bs, BD et `identifiés` : tests sur cas synthétiques (dont l'exemple A1/A2/B1 ci-dessus) et + comparables. Pour Bs, Bd et `identifiés` : tests sur cas synthétiques (dont l'exemple A1/A2/B1 ci-dessus) et **vérification indépendante** par `obipcr` + script, comme en 24.2, sur des paires choisies. - **Seuil `-T` (spécificité minimale, 0,6 par défaut)** : calibré sur l'ancienne règle, plus indulgente. Le défaut sera peut-être à revoir ; à décider avec la spécification complète. @@ -1074,9 +1100,108 @@ et les tables reproduites. ### 24.4 Questions restantes -1. **BD** : l'inclure dans la sortie, et dans le classement ? Dénominateur proposé : les barcodes distincts +1. **Bd** : l'inclure dans la sortie, et dans le classement ? Dénominateur proposé : les barcodes distincts portés par **au moins une cible** (un barcode propre à une non-cible ne « mène » pas à une espèce cible) ; comptage par barcode distinct, sans pondération par le nombre de séquences. 2. **Non-cible sans taxon au rang choisi** (23.4-3, proposé, non confirmé) : taxon anonyme distinct, donc barcode partagé = ambigu (prudent). 3. **`-T` par défaut** (voir ci-dessus). + +## 25. Mise en œuvre des nouvelles définitions de Bc, Bs et Bd (2026-11-03) + +Demande de l'utilisateur : mettre à jour le document et `obiprimers` pour que les trois indices soient calculés +selon leur nouvelle définition. Décisions prises dans la discussion (sections 23 et 24) : un seul Bs ; tout en +mémoire ; dénominateur de Bs = cibles amplifiées ; identification par **tous** les barcodes ; Bd ajouté et +présent dans toutes les sorties ; non-cible sans taxon = taxon anonyme (ambigu) ; seuil `-T` inchangé (0,6). + +### 25.1 Changements de code + +- **Rôles** (`roles.go`, `corpus.go`) : `Ignored` devient `Other` (non-cible) ; `LoadCorpus` conserve toutes les + séquences ; `Counts.Others` ; `Corpus.Ignored` supprimé. Les quorums (sensibilité, faux positifs) ne + comptent que cibles et contre-exemples (`SequencesMatched`, phase 1 de `countPairs` qui saute les + non-cibles) ; `Taxa.CounterExamples` ne compte que les contre-exemples. +- **`-U`** (`DropMultiMatch(wm, corpus)`) : ne considère que les cibles et contre-exemples, comme ecoPrimers + (la validation de la section 15 reste exacte) ; une non-cible qui contient deux sites ne fait pas écarter le mot. +- **`resolution.go`** (nouveau) : `resolve` calcule, à partir des amplicons d'une paire, les cibles + amplifiées, les contre-exemples amplifiés, les taxons identifiés, les barcodes portés par les cibles et ceux + qui sont discriminants. États de barcode : ambigu / anonyme / porté par une cible. `lenientIdentified` est + la borne supérieure décrite en 25.2. +- **`amplicons.go`** (nouveau) : enregistrement d'un amplicon par séquence amplifiée, **tout rôle confondu** + (hash 64 bits, paire, taxon, rôle ; 24 octets). Chaque worker trie ses enregistrements par paire ; ils ne + sont ni copiés ni regroupés (l'ancienne version les dupliquait) ; les Tm minimales sont des minima par + worker, fusionnés à la fin ; l'évaluation des paires est parallèle. +- **`pairs_eval.go`** : `PairStats.Bd` ; `evaluate` utilise `resolve`. +- **Sortie CSV** : colonne `bd` ajoutée juste après `bs`. L'aide de `-r` décrit les non-cibles ; le journal + indique « other sequences » et le nombre de paires qui passent la borne de Bs. + +### 25.2 Un problème de mémoire, et son remède exact + +Première version : le pic de mémoire passait de **1,36 Go à 5,6 Go** (Cyprinidae, `-r 7953`), car la phase 2 +gardait l'amplicon de toutes les séquences amplifiées pour les 25 699 paires qui passent les quorums, alors que +seules ~3 400 seront retenues. + +Remède, **sans aucun changement de résultat** : un taxon identifié par la nouvelle règle (tous ses barcodes +propres parmi tout ce qui est amplifié) l'est aussi par la règle d'ecoPrimers (au moins un barcode propre parmi les +seules cibles), avec le même nombre de taxons cibles amplifiés. Donc `Bs_nouveau <= Bs_borne`. La phase 2 est +scindée : **2a**, amplicons des seules cibles (comme avant : Tm minimales, sites de la référence, borne de +Bs) ; **2b**, amplicons de toutes les séquences **pour les seules paires dont la borne atteint le seuil**. +Propriété vérifiée par `TestLenientIdentifiedIsAnUpperBound` (3 000 tirages aléatoires avec taxons anonymes, +plusieurs rôles, enregistrements en plusieurs morceaux). + +Résultat (Cyprinidae `-r 7953 -e 3 --three-prime 2 -l 30 -L 150 -c`, M3 Max) : + +| | pic mémoire | temps | paires | +|---|---|---|---| +| avant (ancienne définition) | 1 363 Mo | 6,5 s | 3 448 | +| nouvelle définition, sans élagage | 5 604 Mo | non mesuré | 3 247 | +| **nouvelle définition, avec élagage** | **1 217 à 1 372 Mo** | **4,7 à 5,3 s** | **3 247** | + +42 506 paires candidates, 25 699 passent les quorums, **3 448** passent la borne (c'est exactement le nombre de +l'ancienne table), **3 247** passent le seuil avec la nouvelle règle. Mise en garde : avec `-T 0` aucune paire +n'est élaguée et la mémoire remonte (les tests à `-T 0` atteignent ~5,5 Go de tas, taxonomie NCBI comprise). +Piste non engagée si cela devient gênant : traiter les paires survivantes par lots bornés. + +### 25.3 Valeurs : ancienne contre nouvelle règle (Cyprinidae, paire n° 1) + +`CAAGCCAGCCACATAACC` / `TTCTATAACGGGTGATGC` : 310 taxons cibles amplifiés ; ecoPrimers : 237 identifiés, +Bs = 0,7645 ; obiprimers : **233** identifiés, **Bs = 0,7516**, **Bd = 0,8935**. L'écart est faible ici car la +base a un génome par espèce (peu de barcodes multiples) et que les non-cibles amplifiées par cette paire sont +peu nombreuses à partager un barcode ; il sera plus marqué sur des bases à plusieurs séquences par espèce. + +### 25.4 Validation indépendante (obipcr + Python, base entière, `--three-prime 0`, `-T 0`) + +Six paires (les deux premières de la sortie et quatre tirées au hasard), amplicons obtenus par `obipcr -e 3 +-l 31 -L 149 -c` sur les **7 822 séquences**, Bs et Bd recalculés par un script qui n'utilise aucun code d'obiprimers. +Les paires amplifient de 3 202 à 7 796 séquences de la base entière (dont des milliers de non-cibles) : + +| Paire | taxons cibles amplifiés | identifiés | Bs | Bd | +|---|---|---|---|---| +| 1 | 314 = 314 | 237 = 237 | 0,755 = 0,755 | 0,892 = 0,892 | +| 2 | 314 = 314 | 237 = 237 | 0,755 = 0,755 | 0,892 = 0,892 | +| 3 | 314 = 314 | 5 = 5 | 0,016 = 0,016 | 0,357 = 0,357 | +| 4 | 315 = 315 | 14 = 14 | 0,044 = 0,044 | 14/27 = 0,5185 = 0,5185 | +| 5 | 315 = 315 | 7 = 7 | 0,022 = 0,022 | 0,233 = 0,233 | +| 6 | 314 = 314 | 15 = 15 | 0,048 = 0,048 | 0,469 = 0,469 | + +(format « obipcr+Python = obiprimers ».) Accord exact sur les 6 paires. Ces paires ont des Bs très bas : avec +tout ce qui est amplifié, un amplicon est vite partagé avec une non-cible. C'est l'effet recherché de la nouvelle +définition. Limite : la paire de référence 1 reste un cas où les deux règles diffèrent peu ; le cas qui les +distingue nettement (plusieurs barcodes par espèce) est couvert par les tests synthétiques (exemple A1/A2/B1 de +l'utilisateur, non-cible et contre-exemple partageant un barcode, taxon anonyme). + +### 25.5 Tests + +Nouveaux : `resolution_test.go` (exemple de l'utilisateur : Bs = 0/2, Bd = 2/3 ; tous uniques ; même espèce +plusieurs séquences ; non-cible ou contre-exemple partageant un barcode ; comptes de taxons ; taxon anonyme ; +ordre et découpage en morceaux ; borne supérieure) ; `TestNonTargetSharingABarcodeLowersBsAndBd` (de bout en +bout : une non-cible copie l'amplicon d'une cible, Bs = 7/8 et Bd = 7/8, quorums inchangés ; puis deux +contre-exemples amplifiés : 6 identifiés, Bd = 0,75, taxons contre-exemples = 2). + +Adaptés : les tests opt-in contre ecoPrimers (`TestPairsAgainstEcoPrimers`, `TestNoMultiMatchAgainstEcoPrimers`, +`TestCounterExamplesAgainstEcoPrimers`) désactivent le seuil de spécificité, vérifient que **chaque paire +d'ecoPrimers est trouvée** avec les mêmes comptes de séquences et de taxons, Yule, Bc, longueurs, GC, Tm et +drapeaux good, et que les taxons identifiés ne dépassent jamais ceux de la règle indulgente d'ecoPrimers. Résultats : +Cyprinidae 3 448 paires, 0 absente, 0 champ différent ; Teleostei 407, 0 absente, 6 à une séquence de moins +(l'artefact du `N`, section 12) ; `-U` 1 366, 0 absente, 0 différente ; contre-exemples 173, 0 absente, 0 +différente. obiprimers produit en plus 22 251 (Cyprinidae) et 2 389 (Teleostei) paires qu'ecoPrimers avait écartées avec son +propre Bs. diff --git a/pkg/obiprimer/amplicons.go b/pkg/obiprimer/amplicons.go new file mode 100644 index 0000000..828bd8a --- /dev/null +++ b/pkg/obiprimer/amplicons.go @@ -0,0 +1,232 @@ +package obiprimer + +import ( + "cmp" + "math" + "slices" + "sync" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obidefault" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obithermo" +) + +// ampRecord is an amplicon of a retained pair, in one sequence. +type ampRecord struct { + hash uint64 // the barcode: see ampHash + pair int32 + taxon int32 // taxon of the sequence at the chosen rank, -1 when it has none + role Role +} + +// refHit is an amplicon of a retained pair on the reference sequence. +type refHit struct { + up, down seqMatch + swapped bool +} + +// ampHash hashes the amplicon between the two primers read from the side of +// the first primer of the pair (reverse complemented when the upstream match is +// the second primer), so that identical amplicons give the same value +// whatever their strand. Non ACGT bases hash as a fifth symbol. +func ampHash(raw []byte, from, to int, reverse bool) uint64 { + const prime = 1099511628211 + h := uint64(14695981039346656037) ^ uint64(to-from) + h *= prime + if !reverse { + for _, b := range raw[from:to] { + h = (h ^ uint64(baseCode[b])) * prime + } + return h + } + for i := to - 1; i >= from; i-- { + c := baseCode[raw[i]] + if c < 4 { + c = 3 - c + } + h = (h ^ uint64(c)) * prime + } + return h +} + +// amplicons holds what the second phase gathers on the retained pairs. +type amplicons struct { + parts [][]ampRecord // one slice per worker, each sorted by pair + minTm1 []float32 // by pair: lowest Tm of the first primer over the example sites + minTm2 []float32 + refs map[int32]refHit +} + +// pair returns the amplicons of one retained pair, as one slice per worker. +func (a *amplicons) pair(i int32) [][]ampRecord { + out := make([][]ampRecord, 0, len(a.parts)) + byPair := func(r ampRecord, p int32) int { return cmp.Compare(r.pair, p) } + for _, part := range a.parts { + lo, _ := slices.BinarySearchFunc(part, i, byPair) + hi, _ := slices.BinarySearchFunc(part, i+1, byPair) + if hi > lo { + out = append(out, part[lo:hi]) + } + } + return out +} + +// collectOptions selects what collectAmplicons gathers. +type collectOptions struct { + examplesOnly bool // skip the counterexamples and the other sequences + model *obithermo.Model // melting temperatures of the example sites (nil: none) + reference bool // record the amplicons on the reference sequence +} + +// collectAmplicons gathers, in parallel, one record per amplicon of a retained +// pair, in the sequences of the corpus (all of them, or the examples only), and +// the lowest melting temperatures over the example sites. The records are not +// copied nor regrouped: each worker keeps its own, sorted by pair. +func (ps *primerSet) collectAmplicons(c *Corpus, taxa *Taxa, p PairParams, index map[pairKey]int32, opt collectOptions) *amplicons { + model := opt.model + lp := p.PrimerLength + workers := obidefault.ParallelWorkers() + npairs := len(index) + + res := &licons{ + parts: make([][]ampRecord, workers), + minTm1: make([]float32, npairs), + minTm2: make([]float32, npairs), + refs: map[int32]refHit{}, // written by the single worker that reads the reference + } + inf := float32(math.Inf(1)) + for i := range npairs { + res.minTm1[i], res.minTm2[i] = inf, inf + } + var tmMu sync.Mutex + + // oriented[primer][0] is the reverse complement of the word, [1] the word itself. + oriented := make([][2][]byte, len(ps.primers)) + for i, pr := range ps.primers { + oriented[i][0] = []byte(orientedWord(pr.Word, lp, false)) + oriented[i][1] = []byte(orientedWord(pr.Word, lp, true)) + } + + jobs := make(chan int) + var wg sync.WaitGroup + for w := range workers { + wg.Add(1) + go func() { + defer wg.Done() + site1, site2 := make([]byte, lp), make([]byte, lp) + lowTm1, lowTm2 := map[int32]float32{}, map[int32]float32{} + for s := range jobs { + raw := c.Seqs[s].Sequence() + n := len(raw) + role := c.Roles[s] + if opt.examplesOnly && role != Example { + continue + } + ps.forEachOccurrence(c, s, p, func(o occurrence) { + pi, ok := index[o.key] + if !ok { + return + } + if opt.reference && p.HasReference && s == p.Reference { + if old, seen := res.refs[pi]; !seen || o.up.pos < old.up.pos { + res.refs[pi] = refHit{o.up, o.down, o.swapped} + } + } + res.parts[w] = append(res.parts[w], ampRecord{ + hash: ampHash(raw, int(o.up.pos)+lp, int(o.down.pos), o.swapped), + pair: pi, + taxon: taxa.Of[s], + role: role, + }) + + if role != Example || model == nil { + return + } + fp := oriented[o.up.primer][b2i(!o.up.reverse)] + rp := oriented[o.down.primer][b2i(o.down.reverse)] + tmF, tmR := inf, inf + if windowAsIs(site1, raw, int(o.up.pos), n, p.Circular) { + if t, err := model.Tm(fp, site1); err == nil && !math.IsNaN(t) { + tmF = float32(t) + } + } + if windowRevComp(site2, raw, int(o.down.pos), n, p.Circular) { + if t, err := model.Tm(rp, site2); err == nil && !math.IsNaN(t) { + tmR = float32(t) + } + } + if o.swapped { + tmF, tmR = tmR, tmF // slot 1 is the downstream primer + } + lowTm1[pi] = min(lowOr(lowTm1, pi, inf), tmF) + lowTm2[pi] = min(lowOr(lowTm2, pi, inf), tmR) + }) + } + + slices.SortFunc(res.parts[w], func(a, b ampRecord) int { return cmp.Compare(a.pair, b.pair) }) + tmMu.Lock() + for pi, t := range lowTm1 { + res.minTm1[pi] = min(res.minTm1[pi], t) + } + for pi, t := range lowTm2 { + res.minTm2[pi] = min(res.minTm2[pi], t) + } + tmMu.Unlock() + }() + } + for s := range c.Seqs { + jobs <- s + } + close(jobs) + wg.Wait() + return res +} + +func lowOr(m map[int32]float32, k int32, def float32) float32 { + if v, ok := m[k]; ok { + return v + } + return def +} + +func b2i(b bool) int { + if b { + return 1 + } + return 0 +} + +const upper = "ACGT" + +// windowAsIs copies the len(dst) bases at pos as upper-case ACGT; it reports +// false if one of them is not A, C, G or T. +func windowAsIs(dst, raw []byte, pos, n int, circular bool) bool { + for t := range dst { + i := pos + t + if circular { + i %= n + } + c := baseCode[raw[i]] + if c > 3 { + return false + } + dst[t] = upper[c] + } + return true +} + +// windowRevComp writes the reverse complement of the len(dst) bases at pos. +func windowRevComp(dst, raw []byte, pos, n int, circular bool) bool { + l := len(dst) + for t := range dst { + i := pos + l - 1 - t + if circular { + i %= n + } + c := baseCode[raw[i]] + if c > 3 { + return false + } + dst[t] = upper[3-c] + } + return true +} diff --git a/pkg/obiprimer/corpus.go b/pkg/obiprimer/corpus.go index 489bf36..c64ea34 100644 --- a/pkg/obiprimer/corpus.go +++ b/pkg/obiprimer/corpus.go @@ -15,29 +15,32 @@ import ( type Counts struct { Examples int CounterExamples int - Ignored int + Others int } -// Corpus is the set of sequences used by the primer search: the examples and -// the counterexamples, in input order. Sequences with the Ignored role are not -// kept (as in ecoPrimers), only counted. +// Corpus is the set of sequences of the primer search, in input order, each +// with its role: the examples (the targets), the counterexamples, and all the +// other sequences, which are loaded as well since the amplicons they give +// decide whether a target is identified. // // The primer search needs several passes over the sequences (words are // searched approximately in every one of them), so the corpus lives in memory. type Corpus struct { - Seqs []*obiseq.BioSequence - Roles []Role // Roles[i] is Example or CounterExample - Ignored int // number of sequences dropped by the selection + Seqs []*obiseq.BioSequence + Roles []Role // Roles[i] is the role of Seqs[i] } // Counts returns the number of sequences by role. func (c *Corpus) Counts() Counts { - n := Counts{Ignored: c.Ignored} + var n Counts for _, r := range c.Roles { - if r == Example { + switch r { + case Example: n.Examples++ - } else { + case CounterExample: n.CounterExamples++ + default: + n.Others++ } } return n @@ -49,31 +52,24 @@ type corpusEntry struct { role Role } -// LoadCorpus reads every sequence of seqs, once, and keeps those selected as -// examples or counterexamples. The order of the input is preserved whatever +// LoadCorpus reads every sequence of seqs, once, and keeps all of them with the +// role given by the selection. The order of the input is preserved whatever // the number of reader goroutines. func LoadCorpus(seqs obiiter.IBioSequence, sel Selection) (*Corpus, error) { var mu sync.Mutex var entries []corpusEntry - ignored := 0 consumer := func(it obiiter.IBioSequence, wg *sync.WaitGroup) { defer wg.Done() var local []corpusEntry - localIgnored := 0 for it.Next() { batch := it.Get() for i, seq := range batch.Slice() { - if role := sel.Role(seq); role == Ignored { - localIgnored++ - } else { - local = append(local, corpusEntry{batch.Order(), i, seq, role}) - } + local = append(local, corpusEntry{batch.Order(), i, seq, sel.Role(seq)}) } } mu.Lock() entries = append(entries, local...) - ignored += localIgnored mu.Unlock() } @@ -94,9 +90,8 @@ func LoadCorpus(seqs obiiter.IBioSequence, sel Selection) (*Corpus, error) { }) c := &Corpus{ - Seqs: make([]*obiseq.BioSequence, len(entries)), - Roles: make([]Role, len(entries)), - Ignored: ignored, + Seqs: make([]*obiseq.BioSequence, len(entries)), + Roles: make([]Role, len(entries)), } for i, e := range entries { c.Seqs[i], c.Roles[i] = e.seq, e.role diff --git a/pkg/obiprimer/match.go b/pkg/obiprimer/match.go index 9a14f72..8f4216b 100644 --- a/pkg/obiprimer/match.go +++ b/pkg/obiprimer/match.go @@ -138,7 +138,8 @@ func matchCorpus(c *Corpus, words []uint64, mk scannerMaker) []WordMatches { } // SequencesMatched returns, for the matches of one word, the number of -// distinct example and counterexample sequences it occurs in. +// distinct example and counterexample sequences it occurs in. The other +// sequences are not counted. func (w WordMatches) SequencesMatched(c *Corpus) (examples, counterExamples int) { last := ^uint32(0) for _, m := range w.Matches { @@ -146,9 +147,10 @@ func (w WordMatches) SequencesMatched(c *Corpus) (examples, counterExamples int) continue } last = m.Seq - if c.Roles[m.Seq] == Example { + switch c.Roles[m.Seq] { + case Example: examples++ - } else { + case CounterExample: counterExamples++ } } diff --git a/pkg/obiprimer/multimatch.go b/pkg/obiprimer/multimatch.go index c06713f..fdfce0a 100644 --- a/pkg/obiprimer/multimatch.go +++ b/pkg/obiprimer/multimatch.go @@ -18,15 +18,16 @@ func DropRepeatedWords(words []obikmer.SeqKmerCount) (kept []obikmer.SeqKmerCoun } // DropMultiMatch removes the words having two or more approximate occurrences, -// both strands counted, in at least one sequence of the corpus, whatever its -// role. It is the second half of the ecoPrimers -U option: the primers that -// can anneal at several places of a sequence are discarded. -func DropMultiMatch(wm []WordMatches) (kept []WordMatches, dropped int) { +// both strands counted, in at least one example or counterexample sequence. It +// is the second half of the ecoPrimers -U option: the primers that can anneal +// at several places of a sequence they must (or must not) amplify are +// discarded. The other sequences are not considered. +func DropMultiMatch(wm []WordMatches, c *Corpus) (kept []WordMatches, dropped int) { kept = make([]WordMatches, 0, len(wm)) for _, w := range wm { multi := false for i := 1; i < len(w.Matches); i++ { - if w.Matches[i].Seq == w.Matches[i-1].Seq { + if w.Matches[i].Seq == w.Matches[i-1].Seq && c.Roles[w.Matches[i].Seq] != Other { multi = true break } diff --git a/pkg/obiprimer/pairs.go b/pkg/obiprimer/pairs.go index eaa2142..8457285 100644 --- a/pkg/obiprimer/pairs.go +++ b/pkg/obiprimer/pairs.go @@ -221,6 +221,9 @@ func (ps *primerSet) countPairs(c *Corpus, p PairParams) map[pairKey]*pairAcc { defer wg.Done() acc := partial[w] for s := range jobs { + if c.Roles[s] == Other { + continue // only the quorums are counted here + } isExample := c.Roles[s] == Example ps.forEachOccurrence(c, s, p, func(o occurrence) { a := acc[o.key] diff --git a/pkg/obiprimer/pairs_eval.go b/pkg/obiprimer/pairs_eval.go index 51c5189..aee5b5d 100644 --- a/pkg/obiprimer/pairs_eval.go +++ b/pkg/obiprimer/pairs_eval.go @@ -21,11 +21,12 @@ type PairStats struct { CounterExamples int Yule float32 // Examples/|E| - CounterExamples/|C| - ExampleTaxa int // taxa amplified among the examples - CounterTaxa int // taxa amplified among the counterexamples - Bc float32 // coverage: ExampleTaxa over all the example taxa - Identified int // example taxa with an amplicon no other taxon shares + ExampleTaxa int // target taxa amplified + CounterTaxa int // counterexample taxa amplified + Bc float32 // coverage: ExampleTaxa over all the target taxa + Identified int // target taxa all of whose barcodes belong to them alone (see resolution.go) Bs float32 // specificity: Identified over ExampleTaxa + Bd float32 // barcode discrimination: share of the target barcodes leading to a single taxon MinLen, MaxLen int MeanLen float64 Tm1, Tm2 float64 // melting temperature of the primers against their perfect target @@ -50,44 +51,7 @@ type PairInfo struct { GoodPrimers int // of which good ones CandidatePairs int // pairs delimiting an amplicon in at least one sequence RetainedPairs int // pairs passing the sensitivity and false positive quorums, before the Bs filter -} - -// ampRecord is an amplicon of a retained pair, in one sequence. -type ampRecord struct { - pair int32 - seq int32 - hash uint64 - tm1, tm2 float32 // NaN when unavailable -} - -// refHit is an amplicon of a retained pair on the reference sequence. -type refHit struct { - up, down seqMatch - swapped bool -} - -// ampHash hashes the amplicon between the two primers read from the side of -// the first primer of the pair (reverse complemented when the upstream match is -// the second primer), so that identical amplicons give the same value -// whatever their strand. Non ACGT bases hash as a fifth symbol. -func ampHash(raw []byte, from, to int, reverse bool) uint64 { - const prime = 1099511628211 - h := uint64(14695981039346656037) ^ uint64(to-from) - h *= prime - if !reverse { - for _, b := range raw[from:to] { - h = (h ^ uint64(baseCode[b])) * prime - } - return h - } - for i := to - 1; i >= from; i-- { - c := baseCode[raw[i]] - if c < 4 { - c = 3 - c - } - h = (h ^ uint64(c)) * prime - } - return h + Candidates int // of which pairs whose Bs upper bound reaches the threshold (their amplicons are gathered in full) } // FindPairs searches the primer pairs from the approximate occurrences of the @@ -145,24 +109,63 @@ func FindPairs(c *Corpus, taxa *Taxa, wm []WordMatches, p PairParams, model *obi index[k] = int32(i) } - // Phase 2: the amplicons of the retained pairs only. - records, refs := ps.collectAmplicons(c, p, index, model) - byPair := make([][]ampRecord, len(kept)) - for _, r := range records { - byPair[r.pair] = append(byPair[r.pair], r) + // Phase 2a: the amplicons of the target sequences, which give the melting + // temperatures, the reference sites, and an upper bound of Bs. It is cheap + // and rejects most pairs on the specificity threshold. + first := ps.collectAmplicons(c, taxa, p, index, collectOptions{examplesOnly: true, model: model, reference: true}) + var survivors []int // indices in kept + for i := range kept { + targets, identified := lenientIdentified(first.pair(int32(i))...) + if targets > 0 && float32(identified)/float32(targets) >= float32(p.MinSpecificity) { + survivors = append(survivors, i) + } } + first.parts = nil + info.Candidates = len(survivors) + + // Phase 2b: for the survivors only, the amplicons of every sequence, whatever + // its role: they decide which target taxa are identified. + index2 := make(map[pairKey]int32, len(survivors)) + for j, i := range survivors { + index2[kept[i]] = int32(j) + } + amps := ps.collectAmplicons(c, taxa, p, index2, collectOptions{}) + + results := make([]*PairStats, len(survivors)) + jobs := make(chan int) + var wg sync.WaitGroup + for range obidefault.ParallelWorkers() { + wg.Add(1) + go func() { + defer wg.Done() + for j := range jobs { + i := survivors[j] + k := kept[i] + st, ok := ps.evaluate(taxa, counts, p, k, acc[k], resolve(amps.pair(int32(j))...), + first.minTm1[i], first.minTm2[i], model) + if !ok { + continue + } + if h, found := first.refs[int32(i)]; found { + // evaluate orders the primers by string; the amplicon is read from the first one + s1 := orientedWord(ps.primers[k.p1].Word, p.PrimerLength, k.a1) + s2 := orientedWord(ps.primers[k.p2].Word, p.PrimerLength, k.a2) + st.RefLocation, st.RefAmplicon = referenceSites(c.Seqs[p.Reference].Sequence(), h, p, s2 < s1) + } + results[j] = &st + } + }() + } + for j := range survivors { + jobs <- j + } + close(jobs) + wg.Wait() var out []PairStats - for i, k := range kept { - st, ok := ps.evaluate(c, taxa, p, k, acc[k], byPair[i], model) - if ok { - if h, found := refs[int32(i)]; found { - // evaluate orders the primers by string; the amplicon is read from the first one - s1 := orientedWord(ps.primers[k.p1].Word, p.PrimerLength, k.a1) - s2 := orientedWord(ps.primers[k.p2].Word, p.PrimerLength, k.a2) - st.RefLocation, st.RefAmplicon = referenceSites(c.Seqs[p.Reference].Sequence(), h, p, s2 < s1) - } - out = append(out, st) + for _, st := range results { + if st != nil { + out = append(out, *st) } } slices.SortFunc(out, func(a, b PairStats) int { @@ -181,174 +184,21 @@ func boolCmp(a, b bool) int { return 1 } -// collectAmplicons gathers, in parallel, one record per amplicon of a retained pair. -func (ps *primerSet) collectAmplicons(c *Corpus, p PairParams, index map[pairKey]int32, model *obithermo.Model) ([]ampRecord, map[int32]refHit) { - lp := p.PrimerLength - workers := obidefault.ParallelWorkers() - parts := make([][]ampRecord, workers) +// evaluate puts the indices of one retained pair in a PairStats. It reports +// false when the pair must be dropped (no target taxon amplified, or +// specificity under the threshold). +func (ps *primerSet) evaluate(taxa *Taxa, counts Counts, p PairParams, k pairKey, a *pairAcc, res resolution, + minTm1, minTm2 float32, model *obithermo.Model) (PairStats, bool) { - // oriented[primer][0] is the reverse complement of the word, [1] the word itself. - oriented := make([][2][]byte, len(ps.primers)) - for i, pr := range ps.primers { - oriented[i][0] = []byte(orientedWord(pr.Word, lp, false)) - oriented[i][1] = []byte(orientedWord(pr.Word, lp, true)) - } - nan := float32(math.NaN()) - - // The reference sequence is handled by a single worker, which is the only - // writer of refs; the first (leftmost) amplicon of each pair is kept. - refs := map[int32]refHit{} - - jobs := make(chan int) - var wg sync.WaitGroup - for w := range workers { - wg.Add(1) - go func() { - defer wg.Done() - var site1, site2 [32]byte - for s := range jobs { - raw := c.Seqs[s].Sequence() - n := len(raw) - isExample := c.Roles[s] == Example - ps.forEachOccurrence(c, s, p, func(o occurrence) { - pi, ok := index[o.key] - if !ok { - return - } - if p.HasReference && s == p.Reference { - if old, seen := refs[pi]; !seen || o.up.pos < old.up.pos { - refs[pi] = refHit{o.up, o.down, o.swapped} - } - } - rec := ampRecord{pair: pi, seq: int32(s), tm1: nan, tm2: nan} - if isExample { - rec.hash = ampHash(raw, int(o.up.pos)+lp, int(o.down.pos), o.swapped) - if model != nil { - fp := oriented[o.up.primer][b2i(!o.up.reverse)] - rp := oriented[o.down.primer][b2i(o.down.reverse)] - fsite, rsite := site1[:lp], site2[:lp] - okF := windowAsIs(fsite, raw, int(o.up.pos), n, p.Circular) - okR := windowRevComp(rsite, raw, int(o.down.pos), n, p.Circular) - tmF, tmR := nan, nan - if okF { - if t, err := model.Tm(fp, fsite); err == nil { - tmF = float32(t) - } - } - if okR { - if t, err := model.Tm(rp, rsite); err == nil { - tmR = float32(t) - } - } - if o.swapped { - rec.tm1, rec.tm2 = tmR, tmF - } else { - rec.tm1, rec.tm2 = tmF, tmR - } - } - } - parts[w] = append(parts[w], rec) - }) - } - }() - } - for s := range c.Seqs { - jobs <- s - } - close(jobs) - wg.Wait() - - var all []ampRecord - for _, part := range parts { - all = append(all, part...) - } - return all, refs -} - -func b2i(b bool) int { - if b { - return 1 - } - return 0 -} - -const upper = "ACGT" - -// windowAsIs copies the len(dst) bases at pos as upper-case ACGT; it reports -// false if one of them is not A, C, G or T. -func windowAsIs(dst, raw []byte, pos, n int, circular bool) bool { - for t := range dst { - i := pos + t - if circular { - i %= n - } - c := baseCode[raw[i]] - if c > 3 { - return false - } - dst[t] = upper[c] - } - return true -} - -// windowRevComp writes the reverse complement of the len(dst) bases at pos. -func windowRevComp(dst, raw []byte, pos, n int, circular bool) bool { - l := len(dst) - for t := range dst { - i := pos + l - 1 - t - if circular { - i %= n - } - c := baseCode[raw[i]] - if c > 3 { - return false - } - dst[t] = upper[3-c] - } - return true -} - -// evaluate computes the indices of one retained pair. It reports false when the -// pair must be dropped (no taxon amplified, or specificity under the threshold). -func (ps *primerSet) evaluate(c *Corpus, taxa *Taxa, p PairParams, k pairKey, a *pairAcc, - recs []ampRecord, model *obithermo.Model) (PairStats, bool) { - - exTaxa := map[int32]struct{}{} - coTaxa := map[int32]struct{}{} - ampTaxon := map[uint64]int32{} // amplicon -> its taxon, or -2 when shared - for _, r := range recs { - t := taxa.Of[r.seq] - if t < 0 { - continue - } - if c.Roles[r.seq] != Example { - coTaxa[t] = struct{}{} - continue - } - exTaxa[t] = struct{}{} - if prev, seen := ampTaxon[r.hash]; !seen { - ampTaxon[r.hash] = t - } else if prev != t { - ampTaxon[r.hash] = -2 - } - } - if len(exTaxa) == 0 { + if res.targetTaxa == 0 { return PairStats{}, false } - identified := map[int32]struct{}{} - for _, t := range ampTaxon { - if t >= 0 { - identified[t] = struct{}{} - } - } - - counts := c.Counts() st := PairStats{ Examples: int(a.examples), CounterExamples: int(a.counters), - ExampleTaxa: len(exTaxa), - CounterTaxa: len(coTaxa), - Identified: len(identified), + ExampleTaxa: res.targetTaxa, + CounterTaxa: res.counterTaxa, + Identified: res.identified, MinLen: int(a.minLen), MaxLen: int(a.maxLen), MeanLen: float64(a.sumLen) / float64(a.amplicons), @@ -359,12 +209,21 @@ func (ps *primerSet) evaluate(c *Corpus, taxa *Taxa, p PairParams, k pairKey, a qfp = float32(a.counters) / float32(counts.CounterExamples) } st.Yule = q - qfp - st.Bc = float32(st.ExampleTaxa) / float32(taxa.ExampleTaxa) - st.Bs = float32(st.Identified) / float32(st.ExampleTaxa) + st.Bc = float32(res.targetTaxa) / float32(taxa.ExampleTaxa) + st.Bs = float32(res.identified) / float32(res.targetTaxa) + st.Bd = float32(res.discriminant) / float32(res.barcodes) if st.Bs < float32(p.MinSpecificity) { return PairStats{}, false } + return ps.finish(st, p, k, a, minTm1, minTm2, model), true +} + +// finish adds the primers, their composition and melting temperatures, and puts +// the two primers in string order. +func (ps *primerSet) finish(st PairStats, p PairParams, k pairKey, a *pairAcc, + minTm1, minTm2 float32, model *obithermo.Model) PairStats { + lp := p.PrimerLength st.Primer1 = orientedWord(ps.primers[k.p1].Word, lp, k.a1) st.Primer2 = orientedWord(ps.primers[k.p2].Word, lp, k.a2) @@ -375,23 +234,11 @@ func (ps *primerSet) evaluate(c *Corpus, taxa *Taxa, p PairParams, k pairKey, a if model != nil { st.Tm1, _ = model.SelfTm([]byte(st.Primer1)) st.Tm2, _ = model.SelfTm([]byte(st.Primer2)) - m1, m2 := float32(math.Inf(1)), float32(math.Inf(1)) - for _, r := range recs { - if c.Roles[r.seq] != Example { - continue - } - if r.tm1 == r.tm1 { // not NaN - m1 = min(m1, r.tm1) - } - if r.tm2 == r.tm2 { - m2 = min(m2, r.tm2) - } + if !math.IsInf(float64(minTm1), 1) { + st.MinTm1 = float64(minTm1) } - if !math.IsInf(float64(m1), 1) { - st.MinTm1 = float64(m1) - } - if !math.IsInf(float64(m2), 1) { - st.MinTm2 = float64(m2) + if !math.IsInf(float64(minTm2), 1) { + st.MinTm2 = float64(minTm2) } } @@ -402,5 +249,5 @@ func (ps *primerSet) evaluate(c *Corpus, taxa *Taxa, p PairParams, k pairKey, a st.Tm1, st.Tm2 = st.Tm2, st.Tm1 st.MinTm1, st.MinTm2 = st.MinTm2, st.MinTm1 } - return st, true + return st } diff --git a/pkg/obiprimer/pairs_test.go b/pkg/obiprimer/pairs_test.go index 9160cd7..563b5a9 100644 --- a/pkg/obiprimer/pairs_test.go +++ b/pkg/obiprimer/pairs_test.go @@ -74,9 +74,18 @@ func readEcoRows(t *testing.T, path string) []ecoRow { // Opt-in end-to-end comparison with the complete ecoPrimers result tables of // the tutorial: same parameters (-e 3 -3 2 -l 30 -L 150 -c, -r taxid). // -// Cyprinidae is reproduced exactly: its 3448 pairs agree on every column -// (primers, good flags, sequence and taxon counts, Bc, Bs, lengths, GC, Tm and -// lowest Tm). Teleostei finds the same 407 pairs, and 6 of them count one +// Bs and the number of identified taxa are NOT compared: obiprimers defines +// them differently (a taxon is identified only if all its barcodes are its own, +// and every amplified sequence counts, targets or not), so they are lower. The +// specificity threshold is switched off so that the pairs ecoPrimers dropped +// with its own Bs are still produced, and only the pairs of ecoPrimers are +// checked. What is checked: that every pair of ecoPrimers is found, with the +// same good flags, sequence and taxon counts, Yule, Bc, lengths, GC, Tm and +// lowest Tm; and that identified taxa never exceed those of ecoPrimers' more +// lenient rule on the same amplified sequences. +// +// Cyprinidae is reproduced exactly on these columns for its 3448 pairs. +// Teleostei finds the same 407 pairs, and 6 of them count one // sequence less than ecoPrimers (NC_033548, whose downstream site is // NGGGGAGGCAAGTCGTAA). The 3' check of ecoPrimers reads an N through the // shift register of extractSite, where the code 4 of N spills on the next base @@ -143,7 +152,7 @@ func TestPairsAgainstEcoPrimers(t *testing.T) { lap("approximate search") got, _, err := FindPairs(corpus, taxa, wm, PairParams{ PrimerLength: 18, MinLength: 30, MaxLength: 150, ThreePrime: 2, - Sensitivity: 0.9, FalsePositive: 0.1, MinSpecificity: 0.6, Circular: true, + Sensitivity: 0.9, FalsePositive: 0.1, MinSpecificity: 0, Circular: true, }, obithermo.DefaultModel()) if err != nil { t.Fatal(err) @@ -186,7 +195,7 @@ func TestPairsAgainstEcoPrimers(t *testing.T) { // amplicon (NC_033548) whose 3' primer site ends with an N. We reject it, // so the counts can be exactly one lower and nothing else may differ. lower := func(got, want int) bool { return got == want || got == want-1 } - diverges := g.Examples != w.in || g.ExampleTaxa != w.intaxa || g.Identified != w.well + diverges := g.Examples != w.in || g.ExampleTaxa != w.intaxa if diverges { divergent++ } @@ -196,15 +205,15 @@ func TestPairsAgainstEcoPrimers(t *testing.T) { if !lower(g.ExampleTaxa, w.intaxa) || g.CounterTaxa != w.outtax { note("taxa", w, "got %d/%d want %d/%d", g.ExampleTaxa, g.CounterTaxa, w.intaxa, w.outtax) } - if !lower(g.Identified, w.well) { - note("identified", w, "got %d want %d", g.Identified, w.well) + if !diverges && g.Identified > w.well { + note("identified", w, "got %d, more than the %d of ecoPrimers' lenient rule", g.Identified, w.well) } tol := 0.0006 if diverges { tol = 0.0021 } - if !near(float64(g.Yule), w.yule, tol) || !near(float64(g.Bc), w.bc, tol) || !near(float64(g.Bs), w.bs, tol) { - note("indices", w, "yule/bc/bs got %.4f/%.4f/%.4f want %.3f/%.3f/%.3f", g.Yule, g.Bc, g.Bs, w.yule, w.bc, w.bs) + if !near(float64(g.Yule), w.yule, tol) || !near(float64(g.Bc), w.bc, tol) { + note("indices", w, "yule/bc got %.4f/%.4f want %.3f/%.3f", g.Yule, g.Bc, w.yule, w.bc) } if g.MinLen != w.mind || g.MaxLen != w.maxd || !near(g.MeanLen, w.avg, 0.006) { note("lengths", w, "got %d/%d/%.2f want %d/%d/%.2f", g.MinLen, g.MaxLen, g.MeanLen, w.mind, w.maxd, w.avg) @@ -220,17 +229,11 @@ func TestPairsAgainstEcoPrimers(t *testing.T) { note("mintm", w, "got %.2f/%.2f want %.1f/%.1f", g.MinTm1+0.15, g.MinTm2+0.15, w.mtm1, w.mtm2) } } - t.Logf("missing from ours: %d; extra in ours: %d; mismatching fields: %v; pairs one sequence below ecoPrimers: %d", + t.Logf("missing from ours: %d; pairs of ours that ecoPrimers dropped with its Bs: %d; mismatching fields: %v; pairs one sequence below ecoPrimers: %d", missing, len(byKey), bad, divergent) if divergent != tc.divergent { t.Errorf("%d pairs differ from ecoPrimers by one sequence, expected %d", divergent, tc.divergent) } - for k := range byKey { - if len(byKey) > 0 { - t.Errorf("extra pair %s", k) - } - break - } }) } _ = obikmer.DecodeKmer diff --git a/pkg/obiprimer/resolution.go b/pkg/obiprimer/resolution.go new file mode 100644 index 0000000..86d0c89 --- /dev/null +++ b/pkg/obiprimer/resolution.go @@ -0,0 +1,133 @@ +package obiprimer + +// The three quality indices of a pair, over the sequences it amplifies. +// +// Every sequence the pair amplifies gives a barcode (the amplicon between the +// primers); all of them are taken into account, whatever their role: examples +// (the targets), counterexamples and non-targets. +// +// - Bc, coverage: target taxa amplified / target taxa. +// - Bs, specificity: target taxa identified / target taxa amplified. A target +// taxon is identified when ALL its barcodes belong to it alone: as soon as a +// barcode is shared with another taxon, target or not, counterexample or +// not, both taxa are badly identified. +// - Bd, barcode discrimination: the share of the barcodes carried by the +// targets that lead without ambiguity to one taxon. +// +// A sequence with no taxon at the chosen rank is an anonymous taxon, distinct +// from every other: a barcode it shares with a taxon is ambiguous. + +// barcodeState is what is known of one barcode while the amplicons are scanned. +type barcodeState struct { + taxon int32 // 1 + the first taxon met, 0 while none + flags uint8 +} + +const ( + barcodeAmbiguous = 1 << iota // two distinct taxa carry it + barcodeAnonymous // a sequence with no taxon carries it + barcodeTarget // a target sequence with a taxon carries it +) + +// ambiguous reports whether the barcode does not lead to a single taxon. +func (b barcodeState) ambiguous() bool { + return b.flags&barcodeAmbiguous != 0 || (b.flags&barcodeAnonymous != 0 && b.taxon != 0) +} + +// resolution is the outcome of the identification of the taxa of one pair. +type resolution struct { + targetTaxa int // target taxa amplified + counterTaxa int // counterexample taxa amplified + identified int // target taxa whose barcodes all belong to them alone + barcodes int // distinct barcodes carried by the targets + discriminant int // of which lead to a single taxon +} + +// resolve computes the resolution of one pair from its amplicons. The slices +// need not be sorted; they are only read. +func resolve(parts ...[]ampRecord) resolution { + var res resolution + + state := map[uint64]barcodeState{} + targets := map[int32]struct{}{} + counters := map[int32]struct{}{} + + for _, part := range parts { + for _, r := range part { + st := state[r.hash] + switch { + case r.taxon < 0: + st.flags |= barcodeAnonymous + case st.taxon == 0: + st.taxon = r.taxon + 1 + case st.taxon != r.taxon+1: + st.flags |= barcodeAmbiguous + } + if r.taxon >= 0 { + switch r.role { + case Example: + st.flags |= barcodeTarget + targets[r.taxon] = struct{}{} + case CounterExample: + counters[r.taxon] = struct{}{} + } + } + state[r.hash] = st + } + } + + bad := map[int32]struct{}{} + for _, part := range parts { + for _, r := range part { + if r.role == Example && r.taxon >= 0 && state[r.hash].ambiguous() { + bad[r.taxon] = struct{}{} + } + } + } + + for _, st := range state { + if st.flags&barcodeTarget != 0 { + res.barcodes++ + if !st.ambiguous() { + res.discriminant++ + } + } + } + res.targetTaxa, res.counterTaxa = len(targets), len(counters) + res.identified = len(targets) - len(bad) + return res +} + +// lenientIdentified is an upper bound of resolution.identified, computed on +// the target sequences alone and with the lenient rule of ecoPrimers (a taxon +// counts as soon as ONE of its barcodes belongs to it alone, among the targets). +// +// A taxon identified by resolve has all its barcodes alone among every amplified +// sequence, hence among the targets: it has at least one such barcode and is +// counted here too. So identified <= the second result, with the same number of +// target taxa, which lets a pair be rejected on the specificity threshold without +// gathering the amplicons of the non-target sequences. +func lenientIdentified(parts ...[]ampRecord) (targetTaxa, identified int) { + owner := map[uint64]int32{} // barcode -> its taxon, or -1 when shared + targets := map[int32]struct{}{} + for _, part := range parts { + for _, r := range part { + if r.role != Example || r.taxon < 0 { + continue + } + targets[r.taxon] = struct{}{} + if prev, seen := owner[r.hash]; !seen { + owner[r.hash] = r.taxon + } else if prev != r.taxon { + owner[r.hash] = -1 + } + } + } + alone := map[int32]struct{}{} + for _, t := range owner { + if t >= 0 { + alone[t] = struct{}{} + } + } + return len(targets), len(alone) +} diff --git a/pkg/obiprimer/resolution_test.go b/pkg/obiprimer/resolution_test.go new file mode 100644 index 0000000..4f625c9 --- /dev/null +++ b/pkg/obiprimer/resolution_test.go @@ -0,0 +1,153 @@ +package obiprimer + +import ( + "math/rand" + "testing" +) + +// rec builds an amplicon record: barcode, taxon (-1: none at the rank), role. +func rec(barcode uint64, taxon int32, role Role) ampRecord { + return ampRecord{hash: barcode, taxon: taxon, role: role} +} + +func TestResolutionUserExample(t *testing.T) { + // A has the barcodes A1 and A2, B has A2 and B1. A2 is shared: neither A + // nor B is identified, but A1 and B1 lead to one species each. + const A, B = 0, 1 + const A1, A2, B1 = 11, 12, 21 + res := resolve([]ampRecord{ + rec(A1, A, Example), rec(A2, A, Example), + rec(A2, B, Example), rec(B1, B, Example), + }) + want := resolution{targetTaxa: 2, identified: 0, barcodes: 3, discriminant: 2} + if res != want { + t.Fatalf("got %+v, want %+v", res, want) + } +} + +func TestResolutionAllUnique(t *testing.T) { + res := resolve([]ampRecord{rec(1, 0, Example), rec(2, 1, Example), rec(3, 2, Example)}) + if res.identified != 3 || res.discriminant != 3 || res.barcodes != 3 || res.targetTaxa != 3 { + t.Errorf("all barcodes distinct: %+v", res) + } +} + +func TestResolutionSameTaxonSharesItsBarcodeFreely(t *testing.T) { + // several sequences of one species with the same barcode are not ambiguous, + // and several barcodes for one species are fine when none is shared + res := resolve([]ampRecord{ + rec(1, 0, Example), rec(1, 0, Example), rec(1, 0, Example), + rec(2, 0, Example), + }) + if res.identified != 1 || res.barcodes != 2 || res.discriminant != 2 { + t.Errorf("%+v", res) + } +} + +// The barcode of a target that is unique among the targets but shared with a +// non-target, or with a counterexample, does not identify it. +func TestResolutionNonTargetsCount(t *testing.T) { + for _, role := range []Role{Other, CounterExample} { + res := resolve([]ampRecord{ + rec(1, 0, Example), // unique among the targets... + rec(1, 5, role), // ...but shared with a non-target taxon + rec(2, 1, Example), // identified + rec(3, 6, role), // a barcode of non-targets only + }) + if res.targetTaxa != 2 || res.identified != 1 { + t.Errorf("role %d: %+v, want 2 target taxa and 1 identified", role, res) + } + // barcodes carried by the targets: 1 (ambiguous) and 2; barcode 3 leads to a non-target + if res.barcodes != 2 || res.discriminant != 1 { + t.Errorf("role %d: Bd counts %d/%d, want 1/2", role, res.discriminant, res.barcodes) + } + } +} + +func TestResolutionAmplifiedTaxaCounts(t *testing.T) { + res := resolve([]ampRecord{ + rec(1, 0, Example), rec(2, 1, CounterExample), rec(3, 1, CounterExample), + rec(4, 2, CounterExample), rec(5, 3, Other), + }) + if res.targetTaxa != 1 || res.counterTaxa != 2 { + t.Errorf("target taxa %d (want 1), counterexample taxa %d (want 2: others are not counted)", + res.targetTaxa, res.counterTaxa) + } +} + +// A sequence with no taxon at the rank is an anonymous taxon, distinct from all +// the others: a barcode shared with it is ambiguous. +func TestResolutionAnonymousTaxon(t *testing.T) { + res := resolve([]ampRecord{ + rec(1, 0, Example), rec(1, -1, Other), // shared with an unknown: ambiguous + rec(2, 1, Example), + }) + if res.identified != 1 || res.barcodes != 2 || res.discriminant != 1 { + t.Errorf("target sharing with an anonymous sequence: %+v", res) + } + // two anonymous sequences with the same barcode never count: no target carries it + res = resolve([]ampRecord{rec(7, -1, Other), rec(7, -1, Other), rec(8, 0, Example)}) + if res.barcodes != 1 || res.discriminant != 1 || res.identified != 1 { + t.Errorf("anonymous barcodes alone: %+v", res) + } + // a target with no taxon is not a target taxon, and is anonymous too + res = resolve([]ampRecord{rec(1, -1, Example), rec(1, 0, Example), rec(2, 0, Example)}) + if res.targetTaxa != 1 || res.identified != 0 { + t.Errorf("an anonymous target sequence sharing a barcode must make the taxon ambiguous: %+v", res) + } +} + +func TestResolutionSeveralPartsAndOrder(t *testing.T) { + // the amplicons come as one slice per worker, in any order + a := []ampRecord{rec(1, 0, Example), rec(2, 1, Example)} + b := []ampRecord{rec(2, 2, Other), rec(3, 1, Example)} + if res := resolve(a, b); res.targetTaxa != 2 || res.identified != 1 { + t.Errorf("taxon 1 shares barcode 2 with taxon 2: %+v", res) + } + if res := resolve(b, a); res.targetTaxa != 2 || res.identified != 1 { + t.Errorf("order must not matter: %+v", res) + } +} + +// ecoPrimers' lenient rule on the user's example: A and B each own one barcode, +// so both count (the new rule counts none), and the bound is what lets a pair +// be rejected early. +func TestLenientIdentifiedUserExample(t *testing.T) { + recs := []ampRecord{ + rec(11, 0, Example), rec(12, 0, Example), + rec(12, 1, Example), rec(21, 1, Example), + } + if targets, identified := lenientIdentified(recs); targets != 2 || identified != 2 { + t.Errorf("lenient rule: %d of %d, want 2 of 2", identified, targets) + } + if res := resolve(recs); res.identified != 0 { + t.Errorf("strict rule: %d identified, want 0", res.identified) + } +} + +// The pruning of phase 2 is only exact if the bound never falls under the +// true count, with the same number of target taxa. +func TestLenientIdentifiedIsAnUpperBound(t *testing.T) { + rng := rand.New(rand.NewSource(3)) + for trial := 0; trial < 3000; trial++ { + var recs []ampRecord + n := 1 + rng.Intn(40) + for range n { + role := []Role{Example, Example, Example, CounterExample, Other}[rng.Intn(5)] + recs = append(recs, rec(uint64(rng.Intn(12)), int32(rng.Intn(8))-1, role)) // taxon -1: none + } + // split in a few parts, as the workers do + cut := rng.Intn(len(recs) + 1) + res := resolve(recs[:cut], recs[cut:]) + targets, lenient := lenientIdentified(recs[:cut], recs[cut:]) + if targets != res.targetTaxa { + t.Fatalf("trial %d: %d target taxa for the bound, %d for resolve", trial, targets, res.targetTaxa) + } + if res.identified > lenient { + t.Fatalf("trial %d: identified %d exceeds the bound %d for %v", trial, res.identified, lenient, recs) + } + if res.discriminant > res.barcodes || res.identified > res.targetTaxa { + t.Fatalf("trial %d: inconsistent %+v", trial, res) + } + } +} diff --git a/pkg/obiprimer/roles.go b/pkg/obiprimer/roles.go index 607399b..2ae584e 100644 --- a/pkg/obiprimer/roles.go +++ b/pkg/obiprimer/roles.go @@ -14,9 +14,12 @@ import ( type Role uint8 const ( - // Ignored sequences are not used at all. - Ignored Role = iota - // Example sequences are those the primers must amplify. + // Other sequences are non-targets: nothing is required of them, so they + // count neither in the sensitivity nor in the false positive quorums, but + // the amplicons they give are taken into account to decide whether a target + // is identified (see Bs and Bd). + Other Role = iota + // Example sequences are the targets: the primers must amplify them. Example // CounterExample sequences are those the primers must not amplify. CounterExample @@ -28,7 +31,7 @@ const ( // - a sequence is an Example when it belongs to Restrict (all sequences when // Restrict is nil) and not to Except; // - otherwise it is a CounterExample when it belongs to Ignore or to Except; -// - otherwise it is Ignored. +// - otherwise it is Other. // // Nil predicates mean "no such constraint". type Selection struct { @@ -48,7 +51,7 @@ func (s Selection) Role(seq *obiseq.BioSequence) Role { case inExcept, s.Ignore != nil && s.Ignore(seq): return CounterExample default: - return Ignored + return Other } } diff --git a/pkg/obiprimer/search.go b/pkg/obiprimer/search.go index 61d14fc..f4a5a8f 100644 --- a/pkg/obiprimer/search.go +++ b/pkg/obiprimer/search.go @@ -87,7 +87,7 @@ func Search(c *Corpus, taxa *Taxa, p SearchParams) ([]PairStats, *SearchInfo, er return nil, info, err } if p.NoMultiMatch { - wm, info.MultiMatch = DropMultiMatch(wm) + wm, info.MultiMatch = DropMultiMatch(wm, c) } for _, w := range wm { info.Matches += len(w.Matches) diff --git a/pkg/obiprimer/search_real_test.go b/pkg/obiprimer/search_real_test.go index 53b9466..3e047f4 100644 --- a/pkg/obiprimer/search_real_test.go +++ b/pkg/obiprimer/search_real_test.go @@ -64,10 +64,14 @@ func tutorialParams(t *testing.T) SearchParams { // ecoPrimers -U on the Cyprinidae tutorial case (-e 3 -3 2 -l 30 -L 150 -c -r 7953 -U) // keeps 1366 pairs. testdata/ecoprimers_Cyprinidae_U.pairs.tsv holds, for each, // the primers, the amplified example and counterexample sequences, the amplified -// example and counterexample taxa, and the identified taxa. +// example and counterexample taxa, and the identified taxa. The identified taxa +// are not compared (obiprimers defines them differently, see +// TestPairsAgainstEcoPrimers) and the specificity threshold is off, so that +// the pairs ecoPrimers dropped with its own Bs do not hide any of its pairs. func TestNoMultiMatchAgainstEcoPrimers(t *testing.T) { corpus, taxa, _ := loadTutorial(t) p := tutorialParams(t) + p.MinSpecificity = 0 p.NoMultiMatch = true pairs, info, err := Search(corpus, taxa, p) if err != nil { @@ -91,23 +95,28 @@ func TestNoMultiMatchAgainstEcoPrimers(t *testing.T) { want[p1+"/"+p2] = v } - if len(pairs) != len(want) { - t.Errorf("%d pairs, ecoPrimers -U reports %d", len(pairs), len(want)) - } - bad := 0 + ours := map[string]PairStats{} for _, g := range pairs { - v, ok := want[g.Primer1+"/"+g.Primer2] + ours[g.Primer1+"/"+g.Primer2] = g + } + bad, missing := 0, 0 + for key, v := range want { + g, ok := ours[key] if !ok { - t.Errorf("pair %s/%s is not in the ecoPrimers -U result", g.Primer1, g.Primer2) + if missing++; missing <= 3 { + t.Errorf("ecoPrimers -U pair %s is not found", key) + } continue } - got := [5]int{g.Examples, g.CounterExamples, g.ExampleTaxa, g.CounterTaxa, g.Identified} - if got != v { + got := [4]int{g.Examples, g.CounterExamples, g.ExampleTaxa, g.CounterTaxa} + if got != [4]int{v[0], v[1], v[2], v[3]} || g.Identified > v[4] { if bad++; bad <= 3 { - t.Errorf("pair %s/%s: got %v, ecoPrimers %v", g.Primer1, g.Primer2, got, v) + t.Errorf("pair %s: got %v (identified %d), ecoPrimers %v", key, got, g.Identified, v) } } } + t.Logf("%d pairs of ecoPrimers -U checked, %d missing, %d different; obiprimers has %d pairs (%d more, dropped by ecoPrimers' own Bs)", + len(want), missing, bad, len(pairs), len(pairs)-len(want)+missing) } var locRE = regexp.MustCompile(`(\d+)\.\.(\d+)`) @@ -195,7 +204,9 @@ func TestCounterExamplesAgainstEcoPrimers(t *testing.T) { if c := corpus.Counts(); c.Examples != 325 || c.CounterExamples != 7497 || taxa.CounterExamples != 7387 { t.Fatalf("counts %+v, %d counterexample species (ecoPrimers: 325, 7497, 7387)", c, taxa.CounterExamples) } - pairs, info, err := Search(corpus, taxa, tutorialParams(t)) + pc := tutorialParams(t) + pc.MinSpecificity = 0 // ecoPrimers' own Bs filter is not reproduced, see TestPairsAgainstEcoPrimers + pairs, info, err := Search(corpus, taxa, pc) if err != nil { t.Fatal(err) } @@ -222,22 +233,26 @@ func TestCounterExamplesAgainstEcoPrimers(t *testing.T) { want[p1+"/"+p2] = r } - if len(pairs) != len(want) { - t.Errorf("%d pairs, ecoPrimers reports %d", len(pairs), len(want)) - } - bad := 0 + ours := map[string]PairStats{} for _, g := range pairs { - w, ok := want[g.Primer1+"/"+g.Primer2] + ours[g.Primer1+"/"+g.Primer2] = g + } + bad, missing := 0, 0 + for key, w := range want { + g, ok := ours[key] if !ok { - t.Errorf("pair %s/%s is not in the ecoPrimers result", g.Primer1, g.Primer2) + if missing++; missing <= 3 { + t.Errorf("ecoPrimers pair %s is not found", key) + } continue } good := string("bG"[b2i(g.Good1)]) + string("bG"[b2i(g.Good2)]) - got := [5]int{g.Examples, g.CounterExamples, g.ExampleTaxa, g.CounterTaxa, g.Identified} - if got != w.v || good != w.good { + got := [4]int{g.Examples, g.CounterExamples, g.ExampleTaxa, g.CounterTaxa} + if got != [4]int{w.v[0], w.v[1], w.v[2], w.v[3]} || good != w.good || g.Identified > w.v[4] { if bad++; bad <= 3 { - t.Errorf("pair %s/%s: got %s %v, ecoPrimers %s %v", g.Primer1, g.Primer2, good, got, w.good, w.v) + t.Errorf("pair %s: got %s %v (identified %d), ecoPrimers %s %v", key, good, got, g.Identified, w.good, w.v) } } } + t.Logf("%d pairs of ecoPrimers checked, %d missing, %d different; obiprimers has %d pairs", len(want), missing, bad, len(pairs)) } diff --git a/pkg/obiprimer/search_test.go b/pkg/obiprimer/search_test.go index 9bcc92e..2324ef2 100644 --- a/pkg/obiprimer/search_test.go +++ b/pkg/obiprimer/search_test.go @@ -74,7 +74,7 @@ func TestSearchFindsThePlantedPair(t *testing.T) { if p == nil { t.Fatalf("the planted pair was not found among %d pairs (%+v)", len(pairs), info) } - if p.Examples != 8 || p.MinLen != 40 || p.MaxLen != 40 || p.Bs != 1 || p.Bc != 1 { + if p.Examples != 8 || p.MinLen != 40 || p.MaxLen != 40 || p.Bs != 1 || p.Bc != 1 || p.Bd != 1 { t.Errorf("unexpected indices %+v", *p) } if p.RefLocation != "" { @@ -183,9 +183,11 @@ func TestDropMultiMatch(t *testing.T) { {Word: 2, Matches: []Match{{0, 5, 0, false}, {0, 50, 1, false}}}, // two in sequence 0 {Word: 3, Matches: []Match{{2, 5, 0, false}, {2, 5, 0, true}}}, // both strands of one sequence {Word: 4}, // no match at all: kept + {Word: 5, Matches: []Match{{3, 1, 0, false}, {3, 8, 0, false}}}, // twice, but in a non-target: kept } - kept, dropped := DropMultiMatch(wm) - if dropped != 2 || len(kept) != 2 || kept[0].Word != 1 || kept[1].Word != 4 { + c := corpusOf([]Role{Example, Example, CounterExample, Other}, "a", "a", "a", "a") + kept, dropped := DropMultiMatch(wm, c) + if dropped != 2 || len(kept) != 3 || kept[0].Word != 1 || kept[1].Word != 4 || kept[2].Word != 5 { t.Errorf("kept %v, dropped %d", kept, dropped) } } @@ -197,3 +199,76 @@ func TestDropRepeatedWords(t *testing.T) { t.Errorf("kept %v, dropped %d", kept, dropped) } } + +// A non-target amplified with the very barcode of a target taxon makes this +// target badly identified, although its barcode is unique among the targets; +// it counts in neither the sensitivity nor the false positive quorums. +func TestNonTargetSharingABarcodeLowersBsAndBd(t *testing.T) { + s := newSynthetic(8, -1, 11) + + alone, _, err := Search(s.corpus, s.taxa, s.params()) + if err != nil { + t.Fatal(err) + } + p0 := find(alone, s.f, s.r) + if p0 == nil || p0.Bs != 1 || p0.Bd != 1 { + t.Fatalf("without the non-target: %+v", p0) + } + + // a copy of sequence 3, of another taxon, that is not a target + copy3 := append([]byte(nil), s.corpus.Seqs[3].Sequence()...) + s.corpus.Seqs = append(s.corpus.Seqs, obiseq.NewBioSequence("other", copy3, "")) + s.corpus.Roles = append(s.corpus.Roles, Other) + s.taxa.Of = append(s.taxa.Of, 100) + + pairs, _, err := Search(s.corpus, s.taxa, s.params()) + if err != nil { + t.Fatal(err) + } + p := find(pairs, s.f, s.r) + if p == nil { + t.Fatal("the planted pair must still be found") + } + if p.Examples != 8 || p.ExampleTaxa != 8 || p.Bc != 1 { + t.Errorf("the non-target must not change the quorums nor Bc: %+v", *p) + } + if p.Identified != 7 || p.Bs != 0.875 { + t.Errorf("target 3 shares its barcode with the non-target: identified %d, Bs %v (want 7, 0.875)", p.Identified, p.Bs) + } + if p.Bd != 0.875 { + t.Errorf("Bd %v, want 7 discriminant barcodes of 8", p.Bd) + } + if p.CounterTaxa != 0 || p.CounterExamples != 0 { + t.Errorf("a non-target is not a counterexample: %+v", *p) + } + + // Now counterexamples: two copies (of sequences 3 and 5) that the pair amplifies and + // one unrelated sequence that it does not. 2 of 3 amplified is under the 0.9 quorum. + s.corpus.Roles[len(s.corpus.Roles)-1] = CounterExample + copy5 := append([]byte(nil), s.corpus.Seqs[5].Sequence()...) + s.corpus.Seqs = append(s.corpus.Seqs, obiseq.NewBioSequence("counter5", copy5, "")) + s.corpus.Roles = append(s.corpus.Roles, CounterExample) + s.taxa.Of = append(s.taxa.Of, 101) + unrelated := []byte(strings.ToLower(randDNA(rand.New(rand.NewSource(5)), 300))) + s.corpus.Seqs = append(s.corpus.Seqs, obiseq.NewBioSequence("unrelated", unrelated, "")) + s.corpus.Roles = append(s.corpus.Roles, CounterExample) + s.taxa.Of = append(s.taxa.Of, 102) + + pq := s.params() + pq.FalsePositive = 0.9 + pairs, _, err = Search(s.corpus, s.taxa, pq) + if err != nil { + t.Fatal(err) + } + pc := find(pairs, s.f, s.r) + if pc == nil { + t.Fatal("the pair amplifies 2 of 3 counterexamples (< 0.9): it must be retained") + } + if pc.CounterExamples != 2 || pc.CounterTaxa != 2 { + t.Errorf("counterexamples amplified %d (taxa %d), want 2 and 2", pc.CounterExamples, pc.CounterTaxa) + } + if pc.Identified != 6 || pc.Bd != 0.75 { + t.Errorf("targets 3 and 5 share their barcode with a counterexample: identified %d (want 6), Bd %v (want 0.75)", + pc.Identified, pc.Bd) + } +} diff --git a/pkg/obiprimer/taxa.go b/pkg/obiprimer/taxa.go index bd3417c..89698b0 100644 --- a/pkg/obiprimer/taxa.go +++ b/pkg/obiprimer/taxa.go @@ -58,9 +58,10 @@ func (c *Corpus) AssignTaxa(taxonomy *obitax.Taxonomy, rank string) (*Taxa, erro t.nodes = append(t.nodes, taxon) } t.Of[i] = id - if c.Roles[i] == Example { + switch c.Roles[i] { + case Example: exSeen[id] = struct{}{} - } else { + case CounterExample: coSeen[id] = struct{}{} } } diff --git a/pkg/obiprimer/words_test.go b/pkg/obiprimer/words_test.go index 8abaa18..1d22835 100644 --- a/pkg/obiprimer/words_test.go +++ b/pkg/obiprimer/words_test.go @@ -27,18 +27,18 @@ func TestSelectionRoles(t *testing.T) { }{ {"nothing set: everything is an example", Selection{}, map[string]Role{"r1": Example, "x1": Example, "i1": Example, "o1": Example}}, - {"restrict only: the rest is dropped", + {"restrict only: the rest are non-targets", Selection{Restrict: idPrefix("r")}, - map[string]Role{"r1": Example, "x1": Ignored, "i1": Ignored, "o1": Ignored}}, + map[string]Role{"r1": Example, "x1": Other, "i1": Other, "o1": Other}}, {"restrict + ignore", Selection{Restrict: idPrefix("r"), Ignore: idPrefix("i")}, - map[string]Role{"r1": Example, "i1": CounterExample, "o1": Ignored}}, + map[string]Role{"r1": Example, "i1": CounterExample, "o1": Other}}, {"exception inside restrict becomes a counterexample", Selection{Restrict: idPrefix("r").Or(idPrefix("x")), Except: idPrefix("x")}, - map[string]Role{"r1": Example, "x1": CounterExample, "o1": Ignored}}, + map[string]Role{"r1": Example, "x1": CounterExample, "o1": Other}}, {"example wins over ignore when both match", Selection{Restrict: idPrefix("r"), Ignore: idPrefix("r").Or(idPrefix("i"))}, - map[string]Role{"r1": Example, "i1": CounterExample, "o1": Ignored}}, + map[string]Role{"r1": Example, "i1": CounterExample, "o1": Other}}, } for _, tc := range cases { for id, want := range tc.want { @@ -116,7 +116,7 @@ func TestFindStrictWords(t *testing.T) { t.Fatal(err) } - if res.Counts != (Counts{Examples: 10, CounterExamples: 1, Ignored: 1}) { + if res.Counts != (Counts{Examples: 10, CounterExamples: 1, Others: 1}) { t.Fatalf("counts = %+v", res.Counts) } if res.MinSequences != 7 { diff --git a/pkg/obitools/obiprimers/embedded_test.go b/pkg/obitools/obiprimers/embedded_test.go index 8f6f7ed..cef413e 100644 --- a/pkg/obitools/obiprimers/embedded_test.go +++ b/pkg/obitools/obiprimers/embedded_test.go @@ -59,7 +59,7 @@ func TestEmbeddedTaxonomyFromFiles(t *testing.T) { if err != nil { t.Fatal(err) } - if c := corpus.Counts(); c.Examples != 2 || c.Ignored != 1 { + if c := corpus.Counts(); c.Examples != 2 || c.Others != 1 { t.Errorf("restricting to family 5: %+v", c) } diff --git a/pkg/obitools/obiprimers/obiprimers.go b/pkg/obitools/obiprimers/obiprimers.go index 21d036e..59e6dc1 100644 --- a/pkg/obitools/obiprimers/obiprimers.go +++ b/pkg/obitools/obiprimers/obiprimers.go @@ -140,9 +140,9 @@ func CLIPrimerPairs(sequences obiiter.IBioSequence, taxonomy *obitax.Taxonomy) ( } counts := corpus.Counts() - log.Infof("%d example sequences (%d %s), %d counterexamples (%d %s), %d ignored", + log.Infof("%d example sequences (%d %s), %d counterexamples (%d %s), %d other sequences", counts.Examples, taxa.ExampleTaxa, CLIRank(), - counts.CounterExamples, taxa.CounterExamples, CLIRank(), counts.Ignored) + counts.CounterExamples, taxa.CounterExamples, CLIRank(), counts.Others) pairs, info, err := obiprimer.Search(corpus, taxa, params) if info != nil { @@ -153,8 +153,8 @@ func CLIPrimerPairs(sequences obiiter.IBioSequence, taxonomy *obitax.Taxonomy) ( } log.Infof("%d approximate occurrences; %d conserved primers, %d good", info.Matches, info.Pair.Primers, info.Pair.GoodPrimers) - log.Infof("%d candidate pairs, %d amplifying enough examples, %d retained", - info.Pair.CandidatePairs, info.Pair.RetainedPairs, len(pairs)) + log.Infof("%d candidate pairs, %d amplifying enough examples, %d possibly specific enough, %d retained", + info.Pair.CandidatePairs, info.Pair.RetainedPairs, info.Pair.Candidates, len(pairs)) } return pairs, err } diff --git a/pkg/obitools/obiprimers/obiprimers_test.go b/pkg/obitools/obiprimers/obiprimers_test.go index b31474f..e13cce4 100644 --- a/pkg/obitools/obiprimers/obiprimers_test.go +++ b/pkg/obitools/obiprimers/obiprimers_test.go @@ -12,7 +12,7 @@ func samplePairs() []obiprimer.PairStats { { Primer1: "ACACCGCCCGTCACTCTC", Primer2: "CCAAGTGCACCTTCCGGT", Good1: true, Good2: false, Examples: 3837, CounterExamples: 2, Yule: 0.98249996, ExampleTaxa: 3805, CounterTaxa: 1, - Bc: 0.98169, Identified: 2650, Bs: 0.69647, MinLen: 54, MaxLen: 140, MeanLen: 74.75123, + Bc: 0.98169, Identified: 2650, Bs: 0.69647, Bd: 0.90566, MinLen: 54, MaxLen: 140, MeanLen: 74.75123, Tm1: 62.46, Tm2: 60.74, MinTm1: 36.81, MinTm2: math.NaN(), GC1: 12, GC2: 11, }, {Primer1: "AAAA", Primer2: "TTTT", Tm1: math.NaN(), Tm2: math.NaN(), MinTm1: math.NaN(), MinTm2: math.NaN()}, @@ -42,8 +42,8 @@ func TestPairsToCSVIterator(t *testing.T) { if r["index"] != 1 || r["primer1"] != "ACACCGCCCGTCACTCTC" || r["good1"] != true || r["good2"] != false { t.Errorf("unexpected record %v", r) } - if r["yule"] != 0.9825 || r["bc"] != 0.9817 || r["bs"] != 0.6965 || r["mean_length"] != 74.75 { - t.Errorf("indices are not rounded as expected: %v %v %v %v", r["yule"], r["bc"], r["bs"], r["mean_length"]) + if r["yule"] != 0.9825 || r["bc"] != 0.9817 || r["bs"] != 0.6965 || r["bd"] != 0.9057 || r["mean_length"] != 74.75 { + t.Errorf("indices are not rounded as expected: %v %v %v %v %v", r["yule"], r["bc"], r["bs"], r["bd"], r["mean_length"]) } if r["tm1"] != 62.5 || r["tm2"] != 60.7 || r["tm1_min"] != 36.8 { t.Errorf("temperatures: %v %v %v", r["tm1"], r["tm2"], r["tm1_min"]) diff --git a/pkg/obitools/obiprimers/options.go b/pkg/obitools/obiprimers/options.go index 968ed22..13d37f4 100644 --- a/pkg/obitools/obiprimers/options.go +++ b/pkg/obitools/obiprimers/options.go @@ -51,9 +51,11 @@ func OptionSet(options *getoptions.GetOpt) { options.StringSliceVar(&_restrict, "restrict-to-taxon", 1, 1, options.Alias("r"), options.ArgName("TAXID"), - options.Description("Restricts the examples (the sequences the primers must amplify) to the "+ - "sequences belonging to this taxon. Other sequences are ignored, unless they are "+ - "counterexamples. Can be repeated. Without this option every sequence is an example.")) + options.Description("Restricts the examples (the targets, the sequences the primers must amplify) to "+ + "the sequences belonging to this taxon. The other sequences are non-targets: they are not "+ + "required to be amplified, but the barcodes they give are taken into account to decide "+ + "whether a target is identified (Bs, Bd). Can be repeated. Without this option every "+ + "sequence is an example.")) options.StringSliceVar(&_ignore, "ignore-taxon", 1, 1, options.Alias("i"), diff --git a/pkg/obitools/obiprimers/output.go b/pkg/obitools/obiprimers/output.go index c2f987d..af37b7a 100644 --- a/pkg/obitools/obiprimers/output.go +++ b/pkg/obitools/obiprimers/output.go @@ -18,9 +18,10 @@ var csvColumns = []string{ "examples", "counterexamples", // 11-12 sequences amplified "yule", // 13 "example_taxa", "counterexample_taxa", // 14-15 taxa amplified - "bc", // 16 coverage - "identified_taxa", // 17 - "bs", // 18 specificity + "bc", // 16 coverage: target taxa amplified / target taxa + "identified_taxa", // 17 target taxa all of whose barcodes belong to them alone + "bs", // 18 specificity: identified / amplified target taxa + "bd", // barcode discrimination (new): share of the target barcodes leading to one taxon "min_length", "max_length", "mean_length", // 19-21 } @@ -44,6 +45,7 @@ func pairRecord(index int, p obiprimer.PairStats) obiitercsv.CSVRecord { "yule": round(float64(p.Yule), 4), "example_taxa": p.ExampleTaxa, "counterexample_taxa": p.CounterTaxa, "bc": round(float64(p.Bc), 4), "identified_taxa": p.Identified, "bs": round(float64(p.Bs), 4), + "bd": round(float64(p.Bd), 4), "min_length": p.MinLen, "max_length": p.MaxLen, "mean_length": round(p.MeanLen, 2), } if p.RefLocation != "" { From 8cc02aa9c162bae26968e7bed81349664484e728 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Tue, 3 Nov 2026 22:33:09 +1100 Subject: [PATCH 22/24] Refactor sequence processing for parallel loading and memory management This commit introduces significant changes to the sequence processing pipeline, implementing parallel loading, k-mer budget control, explicit memory management, and refined logic for pair evaluation and resolution. --- OBIPRIMERS_NOTES.md | 149 ++++++++++++- cmd/obitools/obiprimers/main.go | 2 + pkg/obikmer/seqquorum.go | 55 ++++- pkg/obikmer/seqquorum_budget_test.go | 118 +++++++++++ pkg/obikmer/seqquorum_count.go | 56 ++++- pkg/obiprimer/amplicons.go | 257 ++++++++++++++--------- pkg/obiprimer/ampstore.go | 132 ++++++++++++ pkg/obiprimer/corpus.go | 125 ++++++----- pkg/obiprimer/corpus_load.go | 137 ++++++++++++ pkg/obiprimer/match.go | 58 +++-- pkg/obiprimer/match_naive.go | 5 +- pkg/obiprimer/match_seed.go | 5 +- pkg/obiprimer/packed.go | 80 +++++++ pkg/obiprimer/packed_test.go | 109 ++++++++++ pkg/obiprimer/pairs.go | 92 +++++--- pkg/obiprimer/pairs_batches_test.go | 78 +++++++ pkg/obiprimer/pairs_eval.go | 52 +---- pkg/obiprimer/pairs_phase2.go | 160 ++++++++++++++ pkg/obiprimer/reference.go | 4 +- pkg/obiprimer/resolution.go | 135 +++++++----- pkg/obiprimer/resolution_test.go | 50 +++-- pkg/obiprimer/search.go | 15 +- pkg/obiprimer/taxa.go | 47 +++-- pkg/obiprimer/words.go | 21 +- pkg/obitools/obiprimers/embedded_test.go | 50 +++++ pkg/obitools/obiprimers/memory.go | 45 ++++ pkg/obitools/obiprimers/obiprimers.go | 10 +- pkg/obitools/obiprimers/options.go | 8 +- 28 files changed, 1673 insertions(+), 382 deletions(-) create mode 100644 pkg/obikmer/seqquorum_budget_test.go create mode 100644 pkg/obiprimer/ampstore.go create mode 100644 pkg/obiprimer/corpus_load.go create mode 100644 pkg/obiprimer/packed.go create mode 100644 pkg/obiprimer/packed_test.go create mode 100644 pkg/obiprimer/pairs_batches_test.go create mode 100644 pkg/obiprimer/pairs_phase2.go create mode 100644 pkg/obitools/obiprimers/memory.go diff --git a/OBIPRIMERS_NOTES.md b/OBIPRIMERS_NOTES.md index bec957a..9b39c4d 100644 --- a/OBIPRIMERS_NOTES.md +++ b/OBIPRIMERS_NOTES.md @@ -27,7 +27,7 @@ ce qu'elles comparent (paires, drapeaux good, séquences et taxons amplifiés, B **Bs et `identifiés` ne sont plus comparables** à ceux d'ecoPrimers ; ils sont validés autrement (section 25). Sections en attente de décision de l'utilisateur : voie B (21.4), statistique de Tm (22.3-a), nombre de partitions -automatique, mémoire, tests shell, documentation, test `-e 0` (ecoPrimers n'avait pas terminé). +automatique (fait, section 26), mémoire (fait, section 26), tests shell, documentation, test `-e 0` (ecoPrimers n'avait pas terminé). Sources de référence : - Article : `~/Sync/travail/__MOI__/ecoprimers/Documents/Riaz et al. 2011 ... .pdf` (NAR 39:e145) @@ -1205,3 +1205,150 @@ Cyprinidae 3 448 paires, 0 absente, 0 champ différent ; Teleostei 407, 0 absent (l'artefact du `N`, section 12) ; `-U` 1 366, 0 absente, 0 différente ; contre-exemples 173, 0 absente, 0 différente. obiprimers produit en plus 22 251 (Cyprinidae) et 2 389 (Teleostei) paires qu'ecoPrimers avait écartées avec son propre Bs. + +## 26. Optimisation de la mémoire (2026-11-03) + +Mission : réduire la mémoire de `obiprimers` **sans changer un octet de la sortie**, en procédant par la mesure. +Contraintes respectées : tout reste en mémoire (l'entrée standard marche), un seul calcul par indice +(`resolution.go` garde ses définitions), aucun paquet partagé modifié (`obiformats`, `obiiter`, `obiseq`, +`obitax`, `obidefault` : `git diff` vide ; seuls `pkg/obiprimer`, `pkg/obikmer/seqquorum*.go`, +`pkg/obitools/obiprimers` et `cmd/obitools/obiprimers` changent). Pas de commit. + +### 26.1 Méthode, et pièges + +- **Garde-fou** : binaire de référence construit avant toute modification ; six CSV de référence (Cyprinidae + `-r 7953`, Teleostei `-r 32443`, `-r 7953 -i 1`, `-U`, `-R NC_083213`, entrée standard) comparés par `cmp` + après **chaque** étape : identiques à chaque fois. Idem pour `-T 0` (25 699 paires), `-t` (taxonomie NCBI) et le + test d'échelle (Metazoa) comparés au binaire de référence. +- **Mesure** : exécutions entrelacées, médiane (5 à 9 exécutions ; le pic RSS d'une même commande varie de ±100 Mo + avant, de ±10 Mo après). Le **temps CPU utilisateur est trompeur** : 40 % des échantillons d'un profil CPU sont du + `runtime.usleep` des P inactifs. Pour juger le vrai coût d'un changement, comparer aussi avec `--max-cpu 2`. +- **Le RSS n'est pas le tas** (observation, pas hypothèse : mesures croisées `ps` + `runtime.MemStats` toutes les + 60 ms). Sur cette machine le RSS ne descend jamais pendant l'exécution et suit `Sys`, même quand le runtime + annonce avoir *relâché* 258 Mo (`HeapReleased`). Une cause plausible, **non vérifiée** : macOS continue à compter + les pages rendues par `madvise` tant que le système ne les reprend pas. Conséquence pratique, vérifiée par + l'expérience du §26.3 (étapes 5 et 6) : ce qui fait monter le RSS, ce sont les très gros tableaux contigus + alloués successivement, qui n'entrent pas dans les trous du tas et font mapper de la mémoire neuve. +- Les profils de tas par étape (tas après GC forcé, `inuse_space`) sont faits sur une copie du dépôt dans `/tmp` + avec des marqueurs temporaires (`MemStage`) ; rien n'en reste dans le code. + +### 26.2 Rapport de profil, avant toute modification (Cyprinidae, 7 822 séquences) + +Tas vivant après GC, par étape, et ce qui le compose : + +| Étape | vivant | composition | +|---|---|---| +| lecture terminée | 178 Mo | 165 Mo d'octets de séquence (131 Mo utiles : 26 % de marge laissée par les `append`) + attributs | +| appariement approché | 274 Mo | + 97 Mo de `[]WordMatches` (82 Mo utiles) | +| `selectPrimers` | 283 Mo | `perSeq` 109 Mo (82 utiles), construit par `append` (238 Mo alloués), tri stable en série : **0,74 s** | +| phase 2a | 511 Mo | + **214 Mo** d'`ampRecord` (8,1 millions × 24 o, avec la marge des `append`) | +| phase 2b | 517 Mo | + **220 Mo** (9,2 millions × 24 o) ; 2,17 Go alloués au total par ces `append` | +| fin | pic du tas 1 005 Mo | environ 2 × le vivant (`GOGC=100`) | + +Les 1,1 Go « inexpliqués » étaient donc : (a) les enregistrements d'amplicons, 24 octets, marge des `append`, +copiés par pair puis triés ; (b) les occurrences stockées deux fois pendant `selectPrimers` (`WordMatches` puis +`perSeq`) avec des `append` en doublement ; (c) le retard du ramasse-miettes (le tas monte à ~2 × le vivant) ; +(d) la mémoire libérée par une étape qui n'est pas réutilisée par la suivante (§26.1). **Hypothèses réfutées** : +la lecture n'est pas le pic à cette taille (tas ≤ 330 Mo) ; `SeqKmerCounter` n'y est pour rien (il n'indexe que +les 325 exemples, 5 Mo) ; `GOGC=25` ne double pas le temps utilisateur ici (+7 % sur la référence, +5 % avec les +changements ci-dessous). + +### 26.3 Optimisations, dans l'ordre où elles ont été faites + +Médianes entrelacées (RSS en Mo, temps réel, temps utilisateur) ; « avant » est le binaire de l'étape précédente +mesuré **dans la même série** (les valeurs absolues changent d'une série à l'autre, d'où les deux colonnes). + +| # | Changement | RSS avant → après | réel | utilisateur | +|---|---|---|---|---| +| 1 | **Amplicons** (`ampstore.go`, `amplicons.go`, `pairs_phase2.go`) : enregistrement de **12 octets** (deux tableaux parallèles `hash`/`taxon+rôle` au lieu de 24 octets), taille exacte connue avant l'écriture (passe de comptage par worker, puis écriture sans compteur partagé), ni copie ni tri ; **lots bornés** de paires (budget `DefaultAmpBudget` = 20 millions d'enregistrements, 240 Mo) ; maps de `resolve` réutilisées | 1 402 → 866 | 3,76 → 2,77 s | 18,9 → 13,5 s | +| 2 | **Occurrences** (`match.go`, `pairs.go`) : `hit` de 12 octets, regroupement par mot en tranches de taille exacte, `seqMatch` de **8 octets**, tris parallélisés | 949 → 704 | 2,80 → 2,04 s | 13,5 → 13,2 s | +| 3 | **Séquences à 2 bits par base** (`packed.go`) : plages d'exceptions pour tout octet ≠ a/c/g/t (restitution exacte), décodage dans un tampon par worker ; 178 → 42 Mo vivants | 705 → 628 | = | 13,4 → 13,8 s | +| 4 | **Compactage à la lecture** (`LoadPackedCorpus`, `corpus_load.go`) : chaque séquence est compactée et son taxon cherché par les goroutines de lecture, l'objet `BioSequence` n'est jamais gardé | petit jeu : sans effet mesurable ; **échelle : 1 999 → 1 390** | = | = | +| 5 | **Crochet `Release`** (`SearchParams.Release`, branché sur `debug.FreeOSMemory`) aux frontières d'étapes ; `debug.SetGCPercent` (étape 7) | 660 → 532 | = | = | +| 6 | **Tranches exactes par mot / par séquence / par paire** au lieu d'un seul tableau de 55 à 110 Mo | 546 → 365 | 2,12 → 2,19 s | 13,6 → 13,8 s | +| 7 | **Ramasse-miettes** : `SetGCPercent` dès le début de la commande (`memory.go`), sauf si `GOGC` est fixé par l'utilisateur | 370 → 305 avec 25 ; 292 avec 50 (autre série) | = | 13,9 → 14,4 s avec 25 | + +**Étape 1, un faux départ instructif** : la première version (comptage exact, écriture dans un tableau partagé, +compteurs atomiques) faisait gagner 15 % de RSS mais coûtait **+18 % de CPU** (la passe de comptage énumère +deux fois les 7 822 séquences). Deux corrections : des curseurs privés par worker (le plan de comptage fixe +aussi quel worker lit quelle séquence, la passe d'écriture le rejoue), et surtout ne garder pour ces deux passes +que les occurrences **des amorces qui figurent dans les paires concernées** (613 amorces sur 1 200 pour les +survivants de la phase 2b) : l'énumération des paires d'occurrences est quadratique en densité. Résultat à +`--max-cpu 2` : réel 10,35 → 7,33 s et CPU 18,8 → 13,3 s **par rapport à la référence**. Même résultat par +construction : une paire dont une des deux amorces est absente de la liste ne peut pas être produite. + +**Étape 5, le choix du pourcentage** : `GOGC=50` donne l'essentiel du gain pour presque rien de CPU ; `25` retire +encore 150 Mo à l'échelle pour +5 % de CPU. Retenu : 50 (`gcPercent` dans `memory.go`). + +### 26.4 Résultat final (médianes entrelacées ; binaire de référence → binaire final) + +| Cas | RSS | temps réel | temps utilisateur | +|---|---|---|---| +| Cyprinidae, fichier (9 exécutions) | **1 376 → 292 Mo** (−79 %) | 3,77 → 2,15 s | 19,2 → 14,2 s | +| Cyprinidae, entrée standard (7) | 1 365 → 369 Mo | 2,91 → 1,33 s | 17,2 → 12,7 s | +| Teleostei `-r 32443` (5) | 1 047 → 378 Mo | 3,41 → 2,58 s | 21,6 → 20,8 s | +| Cyprinidae `-T 0`, aucune paire élaguée (3) | **5 883 → 409 Mo** | 6,06 → 4,82 s | 46,3 → 46,1 s | +| avec `-t` (taxonomie NCBI complète) (3) | 2 664 → 1 273 Mo | 10,2 → 8,7 s | 32,7 → 29,5 s | +| échelle : 18 061 génomes, 593 Mb, `-r 33208` (3) | 3 189 → 1 370 Mo | 9,06 → 8,95 s | 64,5 → 68,2 s | + +Pour comparaison, ecoPrimers (C) : 1,86 Go (Cyprinidae), 2,11 Go (Teleostei). Les tests : `go test ./pkg/obiprimer/ +./pkg/obitools/obiprimers/ ./pkg/obithermo/ ./pkg/obikmer/ -race -count=1` verts ; tests opt-in sur les données +réelles (`OBIPRIMERS_TUTORIAL`) verts (65 s) : mêmes paires qu'ecoPrimers, 0 absente, 0 champ différent. + +Tests ajoutés : `TestAmpBatches` et `TestAmpBudgetDoesNotChangeTheResult` (tout budget, jusqu'à un enregistrement, +donne les mêmes paires, indices et sites de référence) ; `TestPackedSeqRoundTrip` (toutes les longueurs modulo 4, +N, IUPAC, `u`, plages), `TestPackedCorpusGivesTheSameSearch` (recherche sur corpus compacté ou non, avec +ambiguïtés, référence, 3', circulaire) ; `TestLoadPackedCorpusEqualsLoadThenAssign` ; pour le compteur de k-mers : +budget sans effet sur les comptes, décompte des occurrences par partition, budget d'occurrences, +`DefaultPartitions`. + +### 26.5 Le test d'échelle (jamais fait avant) et le compteur de k-mers + +Sur 593 Mb d'un seul tenant (16 027 exemples), la mémoire n'est plus dominée par la phase 2 (aucune paire n'est +trouvée avec les Metazoa pour cibles : 18-mers conservés chez 70 % des cibles = 0) mais par deux autres postes, +que le petit jeu ne montrait pas : + +1. **Le comptage des mots stricts** (`SeqKmerCounter`, 16 octets par occurrence de k-mer, 526 millions + d'occurrences). Défauts trouvés : le tampon réutilisé par chaque worker gardait la taille de la plus grosse + partition traitée (jusqu'à 106 Mo) ; la plus grosse partition fait 6 fois la médiane. Corrigé + (`seqquorum*.go`) : les occurrences de chaque partition sont comptées à l'écriture et la partition est chargée + dans un tableau de taille exacte ; un **budget d'occurrences** (`DefaultOccurrenceBudget` = 32 millions, 512 Mo) + borne ce que les workers chargent ensemble, partitions les plus grosses d'abord (une partition plus grosse que le + budget passe seule) ; **nombre de partitions automatique** (`DefaultPartitions` : 256, jusqu'à 1 024 quand la + partition moyenne dépasserait ~1 million d'occurrences ; `--partitions 0`, nouveau défaut). La mémoire du comptage + ne dépend plus de la répartition des k-mers. Le budget est un compromis temps/mémoire, **mesuré** (16 cœurs, + RSS total / réel) : 8 M → 1 348 Mo / 14,4 s ; 16 M → 1 621 Mo / 10,2 s ; **32 M → 1 838 Mo / 8,75 s** ; sans + limite → 2 157 Mo / 8,70 s. 32 M ne coûte rien en temps. +2. **La lecture** : les lots de 2 000 séquences (défaut commun des obitools) sont de ~33 Mo de bases chacun ; + avec 16 workers et les lots qui attendent entre les étapes, 793 Mo restaient vivants pendant la lecture. + `--batch-size-max` à 100 par défaut pour cette commande (`SetReadingDefaults`, appelée avant l'analyse des + options, qui l'emportent) : à l'échelle 1 819 → 1 190 Mo (−35 %, budget de k-mers à 32 M), temps réel +2,5 %, CPU +7 % ; + petit jeu : 306 → 274 Mo, +4 % de CPU. `--batch-mem` (existant) + a aussi été évalué : 4 M → 2 312 → 1 582 Mo (+6 % CPU), mais le plafond en nombre de séquences est plus efficace. + **À signaler** : le texte d'aide commun de `--batch-size-max` (paquet `obioptions`, partagé, non modifié) dit + « default 2000 » alors que le défaut affiché par `getoptions` est maintenant 100 pour `obiprimers`. + +### 26.6 Pistes écartées, et pourquoi + +- **Clé `uint64` pour les tables de paires** (au lieu de la structure de 12 octets) : aucun gain mesurable + (22,8 → 22,1 s de CPU à 2 cœurs, bruit). Annulé. +- **`GOGC=25` par défaut** : 5 % de CPU de plus que 50 pour 150 Mo de moins à l'échelle, rien sur le petit jeu. + `GOGC` fixé par l'utilisateur est respecté, donc 25 reste à portée de main. +- **Budget d'amplicons plus petit** (`DefaultAmpBudget`) : non réglé finement. 20 millions d'enregistrements + (240 Mo) tiennent en un lot pour les cas usuels ; `-T 0` passe en plusieurs lots sans perte de temps mesurable. +- **Relire le fichier** : exclu (entrée standard). + +### 26.7 Ce qui reste, et décisions à prendre + +- **Entrée standard** (369 Mo au lieu de 292) : les séquences sont gardées comme objets pendant l'extraction de la + taxonomie embarquée (`embeddedTaxonomyInMemory`), avant d'être compactées. Les compacter dès la lecture + demanderait de scinder la sélection (qui a besoin de la taxonomie) : non fait. +- **Taxonomie NCBI (`-t`)** : ~1,07 Go à elle seule (chargeur `obitax`, partagé, hors périmètre). La taxonomie + embarquée (`obiannotate --taxonomic-path`) l'évite. À dire dans l'aide : non fait. +- **Budget du compteur de k-mers** (32 M occurrences) : décision de l'utilisateur si la mémoire prime sur le + temps (16 M : −12 % de RSS à l'échelle pour +16 % de temps). +- `Corpus.Pack()` n'est plus appelé que par les tests (et utile à qui bâtit un corpus à la main) ; l'ancien chemin + `LoadCorpus` + `AssignTaxa` reste pour les tests. À supprimer si l'on veut un seul chemin. +- Le crochet `Release` appelle une collecte complète à chaque frontière d'étape (4 appels dans le cas usuel : après + la lecture, avant l'appariement, avant chaque lot de 2a et de 2b) ; sans effet mesurable sur le temps. + diff --git a/cmd/obitools/obiprimers/main.go b/cmd/obitools/obiprimers/main.go index c78eb29..c8fa6c3 100644 --- a/cmd/obitools/obiprimers/main.go +++ b/cmd/obitools/obiprimers/main.go @@ -12,6 +12,8 @@ import ( ) func main() { + obiprimers.SetReadingDefaults() + optionParser := obioptions.GenerateOptionParser( "obiprimers", "designs the primer pairs amplifying a set of example sequences and evaluates the DNA barcode they delimit. "+ diff --git a/pkg/obikmer/seqquorum.go b/pkg/obikmer/seqquorum.go index 3471eaa..3b82da4 100644 --- a/pkg/obikmer/seqquorum.go +++ b/pkg/obikmer/seqquorum.go @@ -24,9 +24,12 @@ import ( // 2. CountAtLeast processes the partitions in parallel: each one is loaded, // expanded to (canonical k-mer, sequence id) pairs, sorted, and reduced. // -// Memory during phase 2 is about 16 bytes per k-mer occurrence of the largest -// partitions being processed at the same time, so P must grow with the total -// input size (total bases / P is the occurrence count of an average partition). +// Memory during phase 2 is 16 bytes per k-mer occurrence of the partitions +// being processed at the same time. Their number is bounded by an occurrence +// budget (see SetOccurrenceBudget), so that the memory does not depend on how +// the k-mers happen to be spread over the partitions; P only has to be large +// enough for the largest partition to be small against the budget (total bases +// / P is the occurrence count of an average partition, see DefaultPartitions). // // Bases other than A, C, G, T, U break a sequence into independent runs: no // k-mer ever contains an ambiguous base. @@ -39,13 +42,16 @@ type SeqKmerCounter struct { mu sync.Mutex firstErr error sealed bool + + budget int // k-mer occurrences held at once in phase 2 } type seqPartWriter struct { - mu sync.Mutex - w *bufio.Writer - file *os.File - path string + mu sync.Mutex + w *bufio.Writer + file *os.File + path string + kmers int // k-mer occurrences written, so that the partition is loaded in an array of the exact size } // SeqKmerCount is the result for one k-mer. @@ -78,7 +84,7 @@ func NewSeqKmerCounter(parentDir string, k, m, partitions int) (*SeqKmerCounter, return nil, err } - c := &SeqKmerCounter{dir: dir, k: k, m: m, p: partitions} + c := &SeqKmerCounter{dir: dir, k: k, m: m, p: partitions, budget: DefaultOccurrenceBudget} c.parts = make([]*seqPartWriter, partitions) for i := range c.parts { path := filepath.Join(dir, fmt.Sprintf("part_%04d.sqk", i)) @@ -92,6 +98,38 @@ func NewSeqKmerCounter(parentDir string, k, m, partitions int) (*SeqKmerCounter, return c, nil } +// DefaultOccurrenceBudget is the number of k-mer occurrences phase 2 holds at +// once by default: 32 million, 512 MB. A partition larger than the budget is +// processed alone. Measured on 16 cores, 526 million occurrences to count: with +// 32M the count is as fast as without any limit; 16M make it 15% slower, 8M +// 60% slower. +const DefaultOccurrenceBudget = 32 << 20 + +// SetOccurrenceBudget bounds the k-mer occurrences (16 bytes each) that +// CountAtLeast holds in memory at once. It must be called before CountAtLeast. +func (c *SeqKmerCounter) SetOccurrenceBudget(occurrences int) { + if occurrences > 0 { + c.budget = occurrences + } +} + +// partitionOccurrences is the occurrence count aimed at for an average +// partition by DefaultPartitions (a partition of 1M occurrences is 16 MB). +const partitionOccurrences = 1 << 20 + +// DefaultPartitions returns a partition count suited to a total of kmers +// k-mer occurrences: 256 at least, and more, as a power of two up to 1024, when +// the average partition would otherwise exceed about a million occurrences. +// The largest partitions hold several times the average. The limit keeps the +// number of simultaneously open files reasonable. +func DefaultPartitions(kmers int) int { + p := 256 + for p < 1024 && kmers/p > partitionOccurrences { + p *= 2 + } + return p +} + // K returns the k-mer size. func (c *SeqKmerCounter) K() int { return c.k } @@ -148,6 +186,7 @@ func (c *SeqKmerCounter) addRun(id uint32, run []byte, scratch []byte) []byte { pw := c.parts[int(sk.Minimizer%uint64(c.p))] pw.mu.Lock() _, err := pw.w.Write(scratch) + pw.kmers += len(sk.Sequence) - c.k + 1 pw.mu.Unlock() if err != nil { c.setErr(err) diff --git a/pkg/obikmer/seqquorum_budget_test.go b/pkg/obikmer/seqquorum_budget_test.go new file mode 100644 index 0000000..debe287 --- /dev/null +++ b/pkg/obikmer/seqquorum_budget_test.go @@ -0,0 +1,118 @@ +package obikmer + +import ( + "math/rand" + "reflect" + "sync" + "testing" + "time" +) + +// The occurrence budget only bounds the memory: any budget, down to one +// occurrence (every partition then goes alone), gives the same counts. +func TestSeqKmerCounterBudgetDoesNotChangeTheCounts(t *testing.T) { + rng := rand.New(rand.NewSource(7)) + seqs := randomSeqs(rng, 40, 20, 500, true) + build := func(budget int) []SeqKmerCount { + c, err := NewSeqKmerCounter(t.TempDir(), 9, 4, 11) + if err != nil { + t.Fatal(err) + } + defer c.Close() + if budget > 0 { + c.SetOccurrenceBudget(budget) + } + for i, s := range seqs { + c.AddSequence(uint32(i), s, i%2 == 0) + } + total := 0 + for _, pw := range c.parts { + total += pw.kmers + } + if total == 0 { + t.Fatal("no occurrence counted") + } + got, err := c.CountAtLeast(2) + if err != nil { + t.Fatal(err) + } + return got + } + want := build(0) + if len(want) == 0 { + t.Fatal("the test needs k-mers shared by several sequences") + } + for _, budget := range []int{1, 100, 5000} { + if got := build(budget); !reflect.DeepEqual(got, want) { + t.Errorf("budget %d changes the counts", budget) + } + } +} + +// The occurrences announced for a partition are those that its file holds. +func TestSeqKmerCounterPartitionOccurrenceCount(t *testing.T) { + rng := rand.New(rand.NewSource(3)) + seqs := randomSeqs(rng, 25, 10, 300, true) + c, err := NewSeqKmerCounter(t.TempDir(), 8, 3, 5) + if err != nil { + t.Fatal(err) + } + defer c.Close() + for i, s := range seqs { + c.AddSequence(uint32(i), s, i%3 == 0) + } + if err := c.seal(); err != nil { + t.Fatal(err) + } + for p, pw := range c.parts { + occ, err := c.loadPartition(p, nil) + if err != nil { + t.Fatal(err) + } + if len(occ) != pw.kmers { + t.Errorf("partition %d: %d occurrences loaded, %d counted when written", p, len(occ), pw.kmers) + } + } +} + +func TestOccurrenceBudget(t *testing.T) { + b := newOccurrenceBudget(10) + if got := b.acquire(4); got != 4 { + t.Fatalf("acquire(4) = %d", got) + } + // 7 does not fit in the 6 left: it must wait for the release + done := make(chan int) + go func() { done <- b.acquire(7) }() + select { + case <-done: + t.Fatal("acquire(7) must wait") + case <-time.After(50 * time.Millisecond): + } + b.release(4) + if got := <-done; got != 7 { + t.Fatalf("acquire(7) = %d", got) + } + b.release(7) + + // a request above the whole budget takes all of it, once everything is free + if got := b.acquire(1000); got != 10 { + t.Fatalf("an oversized request must take the whole budget, got %d", got) + } + var wg sync.WaitGroup + wg.Add(1) + go func() { defer wg.Done(); b.release(b.acquire(3)) }() + time.Sleep(20 * time.Millisecond) + b.release(10) + wg.Wait() +} + +func TestDefaultPartitions(t *testing.T) { + for _, tc := range []struct{ kmers, want int }{ + {0, 256}, {5_000_000, 256}, {256 << 20, 256}, {257 << 20, 512}, {526_000_000, 512}, + {600 << 20, 1024}, {1 << 40, 1024}, + } { + if got := DefaultPartitions(tc.kmers); got != tc.want { + t.Errorf("DefaultPartitions(%d) = %d, want %d", tc.kmers, got, tc.want) + } + } +} diff --git a/pkg/obikmer/seqquorum_count.go b/pkg/obikmer/seqquorum_count.go index 9af0cd6..7b216c7 100644 --- a/pkg/obikmer/seqquorum_count.go +++ b/pkg/obikmer/seqquorum_count.go @@ -41,6 +41,16 @@ func (c *SeqKmerCounter) CountAtLeast(minSeq int) ([]SeqKmerCount, error) { } workers := min(obidefault.ParallelWorkers(), c.p) + + // the largest partitions first: they are the hardest to fit in the budget + // and the last ones to finish, if they come late + order := make([]int, c.p) + for i := range order { + order[i] = i + } + slices.SortStableFunc(order, func(a, b int) int { return cmp.Compare(c.parts[b].kmers, c.parts[a].kmers) }) + budget := newOccurrenceBudget(c.budget) + jobs := make(chan int) results := make([][]SeqKmerCount, c.p) @@ -52,13 +62,19 @@ func (c *SeqKmerCounter) CountAtLeast(minSeq int) ([]SeqKmerCount, error) { wg.Add(1) go func() { defer wg.Done() - var occ []kmerOcc // reused between partitions + var occ []kmerOcc // reused between partitions of similar sizes for p := range jobs { + n := c.parts[p].kmers + held := budget.acquire(n) + if cap(occ) < n || cap(occ) > 2*n+1024 { + occ = make([]kmerOcc, 0, n) + } var err error occ, err = c.loadPartition(p, occ[:0]) if err == nil { results[p] = reduceOccurrences(occ, minSeq) } + budget.release(held) if err != nil { errMu.Lock() if firstErr == nil { @@ -69,7 +85,7 @@ func (c *SeqKmerCounter) CountAtLeast(minSeq int) ([]SeqKmerCount, error) { } }() } - for p := range c.p { + for _, p := range order { jobs <- p } close(jobs) @@ -153,3 +169,39 @@ func reduceOccurrences(occ []kmerOcc, minSeq int) []SeqKmerCount { } return out } + +// occurrenceBudget bounds the k-mer occurrences held in memory at once by the +// goroutines that load the partitions. +type occurrenceBudget struct { + mu sync.Mutex + freed *sync.Cond + total int + free int +} + +func newOccurrenceBudget(total int) *occurrenceBudget { + b := &occurrenceBudget{total: total, free: total} + b.freed = sync.NewCond(&b.mu) + return b +} + +// acquire waits until n occurrences fit in the budget and takes them; a request +// above the whole budget waits for the budget to be free and takes all of it. +// It returns what was taken, to give back with release. +func (b *occurrenceBudget) acquire(n int) int { + n = min(n, b.total) + b.mu.Lock() + for b.free < n { + b.freed.Wait() + } + b.free -= n + b.mu.Unlock() + return n +} + +func (b *occurrenceBudget) release(n int) { + b.mu.Lock() + b.free += n + b.mu.Unlock() + b.freed.Broadcast() +} diff --git a/pkg/obiprimer/amplicons.go b/pkg/obiprimer/amplicons.go index 828bd8a..d547089 100644 --- a/pkg/obiprimer/amplicons.go +++ b/pkg/obiprimer/amplicons.go @@ -1,23 +1,13 @@ package obiprimer import ( - "cmp" "math" - "slices" "sync" "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obidefault" "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obithermo" ) -// ampRecord is an amplicon of a retained pair, in one sequence. -type ampRecord struct { - hash uint64 // the barcode: see ampHash - pair int32 - taxon int32 // taxon of the sequence at the chosen rank, -1 when it has none - role Role -} - // refHit is an amplicon of a retained pair on the reference sequence. type refHit struct { up, down seqMatch @@ -48,47 +38,142 @@ func ampHash(raw []byte, from, to int, reverse bool) uint64 { return h } -// amplicons holds what the second phase gathers on the retained pairs. -type amplicons struct { - parts [][]ampRecord // one slice per worker, each sorted by pair - minTm1 []float32 // by pair: lowest Tm of the first primer over the example sites - minTm2 []float32 - refs map[int32]refHit -} +// eachAmplicon runs forEachOccurrence over the sequences of the corpus (the +// examples only, or all of them) in parallel, and calls visit for every +// amplicon of a pair of index. Every worker gets its own visit function, from +// newVisit, and done is called once the worker has no more sequence. +// +// Without a plan the sequences are handed to the workers as they get free, and +// the assignment is recorded in record. With one, every worker scans the +// sequences that the plan gave it. +func (ps *primerSet) eachAmplicon(c *Corpus, p PairParams, index map[pairKey]int32, examplesOnly bool, plan *ampPlan, record []uint16, + newVisit func(worker int) (visit func(s int, raw []byte, o occurrence, pair int32), done func())) { -// pair returns the amplicons of one retained pair, as one slice per worker. -func (a *amplicons) pair(i int32) [][]ampRecord { - out := make([][]ampRecord, 0, len(a.parts)) - byPair := func(r ampRecord, p int32) int { return cmp.Compare(r.pair, p) } - for _, part := range a.parts { - lo, _ := slices.BinarySearchFunc(part, i, byPair) - hi, _ := slices.BinarySearchFunc(part, i+1, byPair) - if hi > lo { - out = append(out, part[lo:hi]) - } + workers := obidefault.ParallelWorkers() + + // only the matches of the primers of the pairs of index can make an amplicon + // of these pairs, and dropping the others spares most of the comparisons + wanted := make([]bool, len(ps.primers)) + for k := range index { + wanted[k.p1], wanted[k.p2] = true, true } - return out + type scratch struct { + ms []seqMatch + bases []byte + } + scan := func(sc *scratch, visit func(int, []byte, occurrence, int32), s int) { + if examplesOnly && c.Roles[s] != Example { + return + } + ms := sc.ms[:0] + for _, m := range ps.perSeq[s] { + if wanted[m.primer] { + ms = append(ms, m) + } + } + sc.ms = ms + if len(ms) < 2 { + return // an amplicon needs two primer sites + } + var raw []byte + raw, sc.bases = c.bases(s, sc.bases) + ps.forEachOccurrenceIn(raw, ms, p, func(o occurrence) { + if pi, ok := index[o.key]; ok { + visit(s, raw, o, pi) + } + }) + } + + var wg sync.WaitGroup + if plan != nil { + mine := make([][]int, workers) + for s, w := range plan.owner { + mine[w] = append(mine[w], s) + } + for w := range workers { + wg.Add(1) + go func() { + defer wg.Done() + visit, done := newVisit(w) + var sc scratch + for _, s := range mine[w] { + scan(&sc, visit, s) + } + if done != nil { + done() + } + }() + } + wg.Wait() + return + } + + jobs := make(chan int) + for w := range workers { + wg.Add(1) + go func() { + defer wg.Done() + visit, done := newVisit(w) + var sc scratch + for s := range jobs { + record[s] = uint16(w) + scan(&sc, visit, s) + } + if done != nil { + done() + } + }() + } + for s := range c.Roles { + jobs <- s + } + close(jobs) + wg.Wait() } -// collectOptions selects what collectAmplicons gathers. +// planAmplicons counts the amplicons of every pair of index in the sequences +// of the corpus (all of them, or the examples only), by worker. +func (ps *primerSet) planAmplicons(c *Corpus, p PairParams, index map[pairKey]int32, examplesOnly bool) *ampPlan { + workers := obidefault.ParallelWorkers() + plan := &Plan{owner: make([]uint16, c.Len()), count: make([][]uint32, workers)} + for w := range plan.count { + plan.count[w] = make([]uint32, len(index)) + } + ps.eachAmplicon(c, p, index, examplesOnly, nil, plan.owner, func(w int) (func(int, []byte, occurrence, int32), func()) { + row := plan.count[w] + return func(_ int, _ []byte, _ occurrence, pi int32) { row[pi]++ }, nil + }) + return plan +} + +// ampSites is what the gathering of the amplicons of the targets learns besides +// the records. +type ampSites struct { + minTm1 []float32 // by pair: lowest Tm of the first primer over the example sites + minTm2 []float32 + refs map[int32]refHit // by pair: the amplicon on the reference sequence +} + +// collectOptions selects what collectAmplicons gathers besides the records. type collectOptions struct { examplesOnly bool // skip the counterexamples and the other sequences model *obithermo.Model // melting temperatures of the example sites (nil: none) reference bool // record the amplicons on the reference sequence } -// collectAmplicons gathers, in parallel, one record per amplicon of a retained -// pair, in the sequences of the corpus (all of them, or the examples only), and -// the lowest melting temperatures over the example sites. The records are not -// copied nor regrouped: each worker keeps its own, sorted by pair. -func (ps *primerSet) collectAmplicons(c *Corpus, taxa *Taxa, p PairParams, index map[pairKey]int32, opt collectOptions) *amplicons { +// collectAmplicons writes into store, in parallel, one record per amplicon of +// the pairs of index (whose values are the pair numbers in the store) and +// returns the lowest melting temperatures over the example sites. The store +// was sized from the plan, which says which worker scans which sequence; the +// batch only holds the pairs of index, the plan more. +func (ps *primerSet) collectAmplicons(c *Corpus, taxa *Taxa, p PairParams, index map[pairKey]int32, + plan *ampPlan, store *ampStore, opt collectOptions) *ampSites { + model := opt.model lp := p.PrimerLength - workers := obidefault.ParallelWorkers() npairs := len(index) - res := &licons{ - parts: make([][]ampRecord, workers), + res := &Sites{ minTm1: make([]float32, npairs), minTm2: make([]float32, npairs), refs: map[int32]refHit{}, // written by the single worker that reads the reference @@ -106,63 +191,43 @@ func (ps *primerSet) collectAmplicons(c *Corpus, taxa *Taxa, p PairParams, index oriented[i][1] = []byte(orientedWord(pr.Word, lp, true)) } - jobs := make(chan int) - var wg sync.WaitGroup - for w := range workers { - wg.Add(1) - go func() { - defer wg.Done() - site1, site2 := make([]byte, lp), make([]byte, lp) - lowTm1, lowTm2 := map[int32]float32{}, map[int32]float32{} - for s := range jobs { - raw := c.Seqs[s].Sequence() - n := len(raw) - role := c.Roles[s] - if opt.examplesOnly && role != Example { - continue + ps.eachAmplicon(c, p, index, opt.examplesOnly, plan, nil, func(w int) (func(int, []byte, occurrence, int32), func()) { + site1, site2 := make([]byte, lp), make([]byte, lp) + lowTm1, lowTm2 := map[int32]float32{}, map[int32]float32{} + + visit := func(s int, raw []byte, o occurrence, pi int32) { + n := len(raw) + role := c.Roles[s] + if opt.reference && p.HasReference && s == p.Reference { + if old, seen := res.refs[pi]; !seen || o.up.pos() < old.up.pos() { + res.refs[pi] = refHit{o.up, o.down, o.swapped} } - ps.forEachOccurrence(c, s, p, func(o occurrence) { - pi, ok := index[o.key] - if !ok { - return - } - if opt.reference && p.HasReference && s == p.Reference { - if old, seen := res.refs[pi]; !seen || o.up.pos < old.up.pos { - res.refs[pi] = refHit{o.up, o.down, o.swapped} - } - } - res.parts[w] = append(res.parts[w], ampRecord{ - hash: ampHash(raw, int(o.up.pos)+lp, int(o.down.pos), o.swapped), - pair: pi, - taxon: taxa.Of[s], - role: role, - }) - - if role != Example || model == nil { - return - } - fp := oriented[o.up.primer][b2i(!o.up.reverse)] - rp := oriented[o.down.primer][b2i(o.down.reverse)] - tmF, tmR := inf, inf - if windowAsIs(site1, raw, int(o.up.pos), n, p.Circular) { - if t, err := model.Tm(fp, site1); err == nil && !math.IsNaN(t) { - tmF = float32(t) - } - } - if windowRevComp(site2, raw, int(o.down.pos), n, p.Circular) { - if t, err := model.Tm(rp, site2); err == nil && !math.IsNaN(t) { - tmR = float32(t) - } - } - if o.swapped { - tmF, tmR = tmR, tmF // slot 1 is the downstream primer - } - lowTm1[pi] = min(lowOr(lowTm1, pi, inf), tmF) - lowTm2[pi] = min(lowOr(lowTm2, pi, inf), tmR) - }) } + store.add(w, pi, ampHash(raw, int(o.up.pos())+lp, int(o.down.pos()), o.swapped), packTaxon(taxa.Of[s], role)) - slices.SortFunc(res.parts[w], func(a, b ampRecord) int { return cmp.Compare(a.pair, b.pair) }) + if role != Example || model == nil { + return + } + fp := oriented[o.up.primer][b2i(!o.up.reverse())] + rp := oriented[o.down.primer][b2i(o.down.reverse())] + tmF, tmR := inf, inf + if windowAsIs(site1, raw, int(o.up.pos()), n, p.Circular) { + if t, err := model.Tm(fp, site1); err == nil && !math.IsNaN(t) { + tmF = float32(t) + } + } + if windowRevComp(site2, raw, int(o.down.pos()), n, p.Circular) { + if t, err := model.Tm(rp, site2); err == nil && !math.IsNaN(t) { + tmR = float32(t) + } + } + if o.swapped { + tmF, tmR = tmR, tmF // slot 1 is the downstream primer + } + lowTm1[pi] = min(lowOr(lowTm1, pi, inf), tmF) + lowTm2[pi] = min(lowOr(lowTm2, pi, inf), tmR) + } + done := func() { tmMu.Lock() for pi, t := range lowTm1 { res.minTm1[pi] = min(res.minTm1[pi], t) @@ -171,13 +236,9 @@ func (ps *primerSet) collectAmplicons(c *Corpus, taxa *Taxa, p PairParams, index res.minTm2[pi] = min(res.minTm2[pi], t) } tmMu.Unlock() - }() - } - for s := range c.Seqs { - jobs <- s - } - close(jobs) - wg.Wait() + } + return visit, done + }) return res } diff --git a/pkg/obiprimer/ampstore.go b/pkg/obiprimer/ampstore.go new file mode 100644 index 0000000..416d4ff --- /dev/null +++ b/pkg/obiprimer/ampstore.go @@ -0,0 +1,132 @@ +package obiprimer + +import "fmt" + +// ampRecord is the logical content of the record of one amplicon: its barcode +// (see ampHash), the taxon of its sequence at the chosen rank (-1 when it has +// none) and the role of the sequence. It is stored packed in an ampStore. +type ampRecord struct { + hash uint64 + taxon int32 + role Role +} + +const ( + roleBits = 2 + roleMask = 1<>roleBits) - 1, Role(t & roleMask) } + +// ampSlice is the amplicons of one pair: 12 bytes per amplicon, kept as two +// parallel arrays (a struct holding a uint64 and a uint32 would take 16). +type ampSlice struct { + hash []uint64 + tax []uint32 // packTaxon +} + +// ampPlan says how many amplicons each worker finds for each pair, and which +// worker scans which sequence: the write pass replays the counting pass with +// the very same assignment, so that every worker knows where to write its +// records without sharing any counter with the others. +type ampPlan struct { + owner []uint16 // owner[s]: the worker that scans sequence s + count [][]uint32 // count[w][pair]: amplicons of the pair in the sequences of worker w +} + +// total returns the number of amplicons of every pair. +func (pl *ampPlan) total() []int { + t := make([]int, len(pl.count[0])) + for _, row := range pl.count { + for b, n := range row { + t[b] += int(n) + } + } + return t +} + +// ampStore holds the amplicons of a batch of pairs, grouped by pair: for each +// pair, two slices of the exact size, since their number is known before they +// are written (see planAmplicons). Nothing grows by doubling, nothing is copied +// nor sorted afterwards. The records of a pair are those of worker 0, then those +// of worker 1, and so on. +// +// The pairs have slices of their own, rather than slices of two huge arrays: a +// few kB each find room in the free memory, where blocks of tens of MB make the +// process map new memory (and, on some systems, count the freed one still). +type ampStore struct { + plan *ampPlan + lo int // first pair of the batch in the plan + pairs []ampSlice + cursor [][]int // cursor[w][b]: next free slot of worker w for pair b +} + +// newAmpStore allocates the store of the pairs lo..hi-1 of the plan. +func newAmpStore(plan *ampPlan, lo, hi int) *ampStore { + n := hi - lo + s := &Store{plan: plan, lo: lo, pairs: make([]ampSlice, n), cursor: make([][]int, len(plan.count))} + for w := range s.cursor { + s.cursor[w] = make([]int, n) + } + for b := range n { + pos := 0 + for w, row := range plan.count { + s.cursor[w][b] = pos + pos += int(row[lo+b]) + } + if pos > 0 { + s.pairs[b] = ampSlice{make([]uint64, pos), make([]uint32, pos)} + } + } + return s +} + +// add writes one record of pair b (numbered in the batch), found by worker w. +func (s *ampStore) add(w int, b int32, hash uint64, taxon uint32) { + i := s.cursor[w][b] + s.cursor[w][b] = i + 1 + s.pairs[b].hash[i], s.pairs[b].tax[i] = hash, taxon +} + +// check panics if a worker did not write exactly the records counted for it: +// the counting and write passes must enumerate the same amplicons. +func (s *ampStore) check() { + for b := range s.pairs { + pos := 0 + for w, row := range s.plan.count { + pos += int(row[s.lo+b]) + if s.cursor[w][b] != pos { + panic(fmt.Sprintf("obiprimer: pair %d: worker %d wrote %d amplicons, %d were counted", + s.lo+b, w, s.cursor[w][b]-(pos-int(row[s.lo+b])), row[s.lo+b])) + } + } + } +} + +// pair returns the amplicons of pair b (numbered in the batch). +func (s *ampStore) pair(b int) ampSlice { return s.pairs[b] } + +// ampBatches cuts the pairs 0..len(count)-1 in consecutive ranges [lo,hi) +// whose amplicons together do not exceed budget records (a pair alone above the +// budget makes a batch of its own). A non positive budget gives one batch. +func ampBatches(count []int, budget int) [][2]int { + var out [][2]int + lo, sum := 0, 0 + for i, n := range count { + if budget > 0 && i > lo && sum+n > budget { + out = append(out, [2]int{lo, i}) + lo, sum = i, 0 + } + sum += n + } + if lo < len(count) { + out = append(out, [2]int{lo, len(count)}) + } + return out +} diff --git a/pkg/obiprimer/corpus.go b/pkg/obiprimer/corpus.go index c64ea34..bccc7ec 100644 --- a/pkg/obiprimer/corpus.go +++ b/pkg/obiprimer/corpus.go @@ -1,16 +1,15 @@ package obiprimer import ( - "cmp" "fmt" - "slices" - "sync" - "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obidefault" - "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiiter" "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq" ) +// maxSeqLen bounds the length of a sequence: the position of a primer site and +// its strand share 32 bits (see seqMatch). +const maxSeqLen = 1 << 31 + // Counts gives the number of sequences by role. type Counts struct { Examples int @@ -26,8 +25,11 @@ type Counts struct { // The primer search needs several passes over the sequences (words are // searched approximately in every one of them), so the corpus lives in memory. type Corpus struct { - Seqs []*obiseq.BioSequence - Roles []Role // Roles[i] is the role of Seqs[i] + Seqs []*obiseq.BioSequence // nil entries once the corpus is packed + Roles []Role // Roles[i] is the role of Seqs[i] + + packed []packedSeq // see Pack + ids []string } // Counts returns the number of sequences by role. @@ -46,68 +48,12 @@ func (c *Corpus) Counts() Counts { return n } -type corpusEntry struct { - batch, index int - seq *obiseq.BioSequence - role Role -} - -// LoadCorpus reads every sequence of seqs, once, and keeps all of them with the -// role given by the selection. The order of the input is preserved whatever -// the number of reader goroutines. -func LoadCorpus(seqs obiiter.IBioSequence, sel Selection) (*Corpus, error) { - var mu sync.Mutex - var entries []corpusEntry - - consumer := func(it obiiter.IBioSequence, wg *sync.WaitGroup) { - defer wg.Done() - var local []corpusEntry - for it.Next() { - batch := it.Get() - for i, seq := range batch.Slice() { - local = append(local, corpusEntry{batch.Order(), i, seq, sel.Role(seq)}) - } - } - mu.Lock() - entries = append(entries, local...) - mu.Unlock() - } - - var wg sync.WaitGroup - for i := 1; i < obidefault.ParallelWorkers(); i++ { - wg.Add(1) - go consumer(seqs.Split(), &wg) - } - wg.Add(1) - go consumer(seqs, &wg) - wg.Wait() - - slices.SortFunc(entries, func(a, b corpusEntry) int { - if c := cmp.Compare(a.batch, b.batch); c != 0 { - return c - } - return cmp.Compare(a.index, b.index) - }) - - c := &Corpus{ - Seqs: make([]*obiseq.BioSequence, len(entries)), - Roles: make([]Role, len(entries)), - } - for i, e := range entries { - c.Seqs[i], c.Roles[i] = e.seq, e.role - } - if c.Counts().Examples == 0 { - return nil, fmt.Errorf("obiprimer: no example sequence selected") - } - return c, nil -} - // IndexOfExample returns the index of the example sequence with the given // identifier. It is an error if there is none, or if the sequence is a // counterexample. func (c *Corpus) IndexOfExample(id string) (int, error) { - for i, s := range c.Seqs { - if s.Id() == id { + for i := range c.Roles { + if c.ID(i) == id { if c.Roles[i] != Example { return -1, fmt.Errorf("obiprimer: reference sequence %q is a counterexample, it must be an example", id) } @@ -116,3 +62,52 @@ func (c *Corpus) IndexOfExample(id string) (int, error) { } return -1, fmt.Errorf("obiprimer: reference sequence %q is not among the selected example sequences", id) } + +// Len returns the number of sequences. +func (c *Corpus) Len() int { return len(c.Roles) } + +// SeqLen returns the length of sequence i. +func (c *Corpus) SeqLen(i int) int { + if c.Seqs[i] != nil { + return c.Seqs[i].Len() + } + return c.packed[i].n +} + +// ID returns the identifier of sequence i. +func (c *Corpus) ID(i int) string { + if c.Seqs[i] != nil { + return c.Seqs[i].Id() + } + return c.ids[i] +} + +// bases returns the bases of sequence i, lower case. A packed sequence is +// unpacked into buf, grown if need be, which the caller keeps for the next call +// (the second result); an unpacked one is returned as it is. The result must +// not be modified, and is valid until the next call with the same buffer. +func (c *Corpus) bases(i int, buf []byte) (raw, newBuf []byte) { + if c.Seqs[i] != nil { + return c.Seqs[i].Sequence(), buf + } + buf = c.packed[i].unpack(buf) + return buf, buf +} + +// Pack replaces the sequences by their packed form, on 2 bits per base, and +// frees the sequence objects (their annotations, their slack): the primer search +// only needs the bases, the identifiers, and the taxa, which must have been +// assigned before (AssignTaxa). A packed corpus holds about a quarter of the +// memory of the bases themselves. +func (c *Corpus) Pack() { + if c.packed != nil { + return + } + c.packed = make([]packedSeq, len(c.Seqs)) + c.ids = make([]string, len(c.Seqs)) + parallelFor(len(c.Seqs), func(_, i int) { + c.packed[i] = packBases(c.Seqs[i].Sequence()) + c.ids[i] = c.Seqs[i].Id() + c.Seqs[i] = nil + }) +} diff --git a/pkg/obiprimer/corpus_load.go b/pkg/obiprimer/corpus_load.go new file mode 100644 index 0000000..f3d2848 --- /dev/null +++ b/pkg/obiprimer/corpus_load.go @@ -0,0 +1,137 @@ +package obiprimer + +import ( + "cmp" + "fmt" + "slices" + "sync" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obidefault" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiiter" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obitax" +) + +// corpusEntry is a sequence read by one of the loading goroutines: either the +// sequence itself, or, when it is packed as it is read, what is kept of it. +type corpusEntry struct { + batch, index int + role Role + + seq *obiseq.BioSequence // unpacked loading + + packed packedSeq // packed loading + id string + taxon *obitax.Taxon // at the chosen rank, nil when there is none +} + +// loadEntries reads every sequence of seqs, once, on all the cores, and returns +// the entries in the order of the input, whatever the number of reader +// goroutines. keep fills the entry of a sequence (besides its position and its +// role); it runs in the reading goroutines. +func loadEntries(seqs obiiter.IBioSequence, sel Selection, keep func(*obiseq.BioSequence, *corpusEntry)) []corpusEntry { + var mu sync.Mutex + var entries []corpusEntry + + consumer := func(it obiiter.IBioSequence, wg *sync.WaitGroup) { + defer wg.Done() + var local []corpusEntry + for it.Next() { + batch := it.Get() + for i, seq := range batch.Slice() { + e := corpusEntry{batch: batch.Order(), index: i, role: sel.Role(seq)} + keep(seq, &e) + local = append(local, e) + } + } + mu.Lock() + entries = append(entries, local...) + mu.Unlock() + } + + var wg sync.WaitGroup + for i := 1; i < obidefault.ParallelWorkers(); i++ { + wg.Add(1) + go consumer(seqs.Split(), &wg) + } + wg.Add(1) + go consumer(seqs, &wg) + wg.Wait() + + slices.SortFunc(entries, func(a, b corpusEntry) int { + if c := cmp.Compare(a.batch, b.batch); c != 0 { + return c + } + return cmp.Compare(a.index, b.index) + }) + return entries +} + +func checkSeqLen(id string, n int) error { + if n >= maxSeqLen { + return fmt.Errorf("obiprimer: sequence %q is too long (%d bases, the limit is %d)", id, n, maxSeqLen-1) + } + return nil +} + +// LoadCorpus reads every sequence of seqs, once, and keeps all of them with the +// role given by the selection. The order of the input is preserved whatever +// the number of reader goroutines. +func LoadCorpus(seqs obiiter.IBioSequence, sel Selection) (*Corpus, error) { + entries := loadEntries(seqs, sel, func(seq *obiseq.BioSequence, e *corpusEntry) { e.seq = seq }) + + c := &Corpus{ + Seqs: make([]*obiseq.BioSequence, len(entries)), + Roles: make([]Role, len(entries)), + } + for i, e := range entries { + if err := checkSeqLen(e.seq.Id(), e.seq.Len()); err != nil { + return nil, err + } + c.Seqs[i], c.Roles[i] = e.seq, e.role + } + if c.Counts().Examples == 0 { + return nil, fmt.Errorf("obiprimer: no example sequence selected") + } + return c, nil +} + +// LoadPackedCorpus is LoadCorpus followed by Pack and AssignTaxa, without ever +// holding the sequences themselves: each one is packed, and its taxon at rank +// looked up, as it is read, and then dropped. The corpus of a large database +// would otherwise first take, in memory, the sequences with all their +// annotations, four times the size of their bases and more. +func LoadPackedCorpus(seqs obiiter.IBioSequence, sel Selection, taxonomy *obitax.Taxonomy, rank string) (*Corpus, *Taxa, error) { + if err := checkRank(taxonomy, rank); err != nil { + return nil, nil, err + } + entries := loadEntries(seqs, sel, func(seq *obiseq.BioSequence, e *corpusEntry) { + e.packed = packBases(seq.Sequence()) + e.id = seq.Id() + e.taxon = seq.Taxon(taxonomy).TaxonAtRank(rank) + }) + + c := &Corpus{ + Seqs: make([]*obiseq.BioSequence, len(entries)), + Roles: make([]Role, len(entries)), + packed: make([]packedSeq, len(entries)), + ids: make([]string, len(entries)), + } + taxons := make([]*obitax.Taxon, len(entries)) + for i := range entries { + e := &entries[i] + if err := checkSeqLen(e.id, e.packed.n); err != nil { + return nil, nil, err + } + c.Roles[i], c.packed[i], c.ids[i], taxons[i] = e.role, e.packed, e.id, e.taxon + *e = corpusEntry{} // frees the entry as it goes + } + if c.Counts().Examples == 0 { + return nil, nil, fmt.Errorf("obiprimer: no example sequence selected") + } + taxa, err := buildTaxa(rank, taxons, c.Roles) + if err != nil { + return nil, nil, err + } + return c, taxa, nil +} diff --git a/pkg/obiprimer/match.go b/pkg/obiprimer/match.go index 8f4216b..0e9af27 100644 --- a/pkg/obiprimer/match.go +++ b/pkg/obiprimer/match.go @@ -8,7 +8,6 @@ import ( "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obidefault" "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obikmer" - "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq" ) // Match is one approximate occurrence of a word in a corpus sequence. @@ -54,15 +53,18 @@ func (p MatchParams) validate() error { return nil } -// hit is a Match before its sequence index is known to be final. +// hit is a Match before its sequence is known: the sequence it belongs to is +// the one being scanned. type hit struct { - word int - m Match + word uint32 + pos uint32 + errors uint8 + reverse bool } // scanFunc finds every occurrence of every word, on both strands, in one -// sequence and reports them through emit. -type scanFunc func(seq *obiseq.BioSequence, emit func(word, pos, errors int, reverse bool)) +// sequence (its bases) and reports them through emit. +type scanFunc func(raw []byte, emit func(word, pos, errors int, reverse bool)) // scannerMaker returns an independent scanFunc; each worker gets its own so // that scanners can keep private buffers. @@ -83,8 +85,13 @@ func wordStrings(words []uint64, k int) (fwd, rev []string) { // matchCorpus runs the scanners over every sequence of the corpus in parallel // and assembles the per-word results. The output does not depend on the number // of workers. +// +// The occurrences of a sequence are kept in a slice of their exact size, then +// counted per word, so that the occurrences of all the words are laid out in +// one allocation of the exact size too: nothing grows by doubling, nor is held +// twice for long. func matchCorpus(c *Corpus, words []uint64, mk scannerMaker) []WordMatches { - perSeq := make([][]hit, len(c.Seqs)) + perSeq := make([][]hit, c.Len()) jobs := make(chan int) var wg sync.WaitGroup @@ -93,31 +100,46 @@ func matchCorpus(c *Corpus, words []uint64, mk scannerMaker) []WordMatches { go func() { defer wg.Done() scan := mk() + var buf []hit + var raw, bases []byte for i := range jobs { - var hits []hit - scan(c.Seqs[i], func(word, pos, errors int, reverse bool) { - hits = append(hits, hit{word, Match{uint32(i), uint32(pos), uint8(errors), reverse}}) + buf = buf[:0] + raw, bases = c.bases(i, bases) + scan(raw, func(word, pos, errors int, reverse bool) { + buf = append(buf, hit{uint32(word), uint32(pos), uint8(errors), reverse}) }) - perSeq[i] = hits + if len(buf) > 0 { + perSeq[i] = slices.Clone(buf) + } } }() } - for i := range c.Seqs { + for i := range c.Roles { jobs <- i } close(jobs) wg.Wait() res := make([]WordMatches, len(words)) - for i, w := range words { - res[i].Word = w - } + count := make([]int, len(words)) for _, hits := range perSeq { for _, h := range hits { - res[h.word].Matches = append(res[h.word].Matches, h.m) + count[h.word]++ } } - for i := range res { + for i, w := range words { + res[i].Word = w + if count[i] > 0 { + res[i].Matches = make([]Match, 0, count[i]) + } + } + for s, hits := range perSeq { + for _, h := range hits { + res[h.word].Matches = append(res[h.word].Matches, Match{uint32(s), h.pos, h.errors, h.reverse}) + } + perSeq[s] = nil // the hits of the sequence are no longer needed + } + parallelFor(len(res), func(_, i int) { slices.SortFunc(res[i].Matches, func(a, b Match) int { if c := cmp.Compare(a.Seq, b.Seq); c != 0 { return c @@ -133,7 +155,7 @@ func matchCorpus(c *Corpus, words []uint64, mk scannerMaker) []WordMatches { } return 0 }) - } + }) return res } diff --git a/pkg/obiprimer/match_naive.go b/pkg/obiprimer/match_naive.go index 54e9567..a815c94 100644 --- a/pkg/obiprimer/match_naive.go +++ b/pkg/obiprimer/match_naive.go @@ -1,7 +1,5 @@ package obiprimer -import "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq" - // baseCode maps a nucleotide to 0..3 (A,C,G,T; U is T) and everything else to 4, // which never equals a word base. var baseCode = func() (t [256]byte) { @@ -39,8 +37,7 @@ func matchNaive(c *Corpus, words []uint64, p MatchParams) ([]WordMatches, error) mk := func() scanFunc { var codes []byte - return func(seq *obiseq.BioSequence, emit func(word, pos, errors int, reverse bool)) { - raw := seq.Sequence() + return func(raw []byte, emit func(word, pos, errors int, reverse bool)) { n := len(raw) if n < p.K { return diff --git a/pkg/obiprimer/match_seed.go b/pkg/obiprimer/match_seed.go index d720088..a63020d 100644 --- a/pkg/obiprimer/match_seed.go +++ b/pkg/obiprimer/match_seed.go @@ -3,8 +3,6 @@ package obiprimer import ( "math/bits" "slices" - - "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq" ) // seedIndex maps the value of one half of a window to the patterns that are @@ -136,8 +134,7 @@ func MatchWords(c *Corpus, words []uint64, p MatchParams) ([]WordMatches, error) maxErr := p.MaxErrors mk := func() scanFunc { - return func(seq *obiseq.BioSequence, emit func(word, pos, errors int, reverse bool)) { - raw := seq.Sequence() + return func(raw []byte, emit func(word, pos, errors int, reverse bool)) { n := len(raw) if n < k { return diff --git a/pkg/obiprimer/packed.go b/pkg/obiprimer/packed.go new file mode 100644 index 0000000..431be04 --- /dev/null +++ b/pkg/obiprimer/packed.go @@ -0,0 +1,80 @@ +package obiprimer + +import ( + "encoding/binary" + "slices" +) + +// packedSeq holds the bases of a sequence on 2 bits each, a quarter of the +// bytes of the sequence itself. The bytes that are not a, c, g or t (N, the +// IUPAC codes, u...) are kept apart in runs, so that unpacking gives back the +// sequence byte for byte. +type packedSeq struct { + n int + bits []byte // 4 bases per byte, the first in the low bits + exc []excRun // sorted by position +} + +// excRun is a run of length identical bytes b that are not a, c, g or t, +// starting at pos. +type excRun struct { + pos, length uint32 + b byte +} + +// packCode maps a, c, g, t to 0..3 and every other byte to 4. +var packCode = func() (t [256]byte) { + for i := range t { + t[i] = 4 + } + for i, c := range "acgt" { + t[c] = byte(i) + } + return +}() + +// unpackWord gives, for a byte of 4 packed bases, the 4 bytes they stand for. +var unpackWord = func() (t [256]uint32) { + for b := range t { + for k := range 4 { + t[b] |= uint32("acgt"[(b>>(2*k))&3]) << (8 * k) + } + } + return +}() + +// packBases packs raw (lower case). +func packBases(raw []byte) packedSeq { + p := packedSeq{n: len(raw), bits: make([]byte, (len(raw)+3)/4)} + for i, b := range raw { + code := packCode[b] + if code > 3 { + if k := len(p.exc) - 1; k >= 0 && p.exc[k].b == b && int(p.exc[k].pos)+int(p.exc[k].length) == i { + p.exc[k].length++ + } else { + p.exc = append(p.exc, excRun{uint32(i), 1, b}) + } + code = 0 + } + p.bits[i>>2] |= code << (2 * (i & 3)) + } + p.exc = slices.Clone(p.exc) // the exact size, nil when there is none + return p +} + +// unpack writes the sequence into dst, which it grows if need be, and returns it. +func (p packedSeq) unpack(dst []byte) []byte { + dst = slices.Grow(dst[:0], p.n)[:p.n] + for i := range p.n / 4 { + binary.LittleEndian.PutUint32(dst[4*i:], unpackWord[p.bits[i]]) + } + for t := p.n &^ 3; t < p.n; t++ { + dst[t] = "acgt"[(p.bits[t>>2]>>(2*(t&3)))&3] + } + for _, e := range p.exc { + for k := range e.length { + dst[e.pos+k] = e.b + } + } + return dst +} diff --git a/pkg/obiprimer/packed_test.go b/pkg/obiprimer/packed_test.go new file mode 100644 index 0000000..50a7858 --- /dev/null +++ b/pkg/obiprimer/packed_test.go @@ -0,0 +1,109 @@ +package obiprimer + +import ( + "bytes" + "math/rand" + "reflect" + "strings" + "testing" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq" +) + +func TestPackedSeqRoundTrip(t *testing.T) { + rng := rand.New(rand.NewSource(1)) + alphabet := []byte("acgtacgtacgtacgtnnrykmu-.") + for trial := 0; trial < 500; trial++ { + n := rng.Intn(70) // all the lengths modulo 4, the empty sequence included + raw := make([]byte, n) + for i := range raw { + raw[i] = alphabet[rng.Intn(len(alphabet))] + } + if trial%5 == 0 && n > 10 { // a long run of N, and one at each end + copy(raw[2:9], "nnnnnnn") + raw[0], raw[n-1] = 'n', 'n' + } + p := packBases(raw) + buf := make([]byte, 3, 8) // dirty, too small or too big: whatever + if got := p.unpack(buf); !bytes.Equal(got, raw) { + t.Fatalf("trial %d: %q unpacked as %q", trial, raw, got) + } + if len(p.bits) != (n+3)/4 { + t.Fatalf("trial %d: %d bytes for %d bases", trial, len(p.bits), n) + } + } + if p := packBases([]byte("acgtacgt")); p.exc != nil { + t.Errorf("no exception expected: %v", p.exc) + } +} + +// Packing the corpus must change nothing to the search, ambiguous bases and +// reference included. +func TestPackedCorpusGivesTheSameSearch(t *testing.T) { + build := func() synthetic { + s := newSynthetic(10, -1, 7) + rng := rand.New(rand.NewSource(3)) + for k, seq := range s.corpus.Seqs { // sprinkle ambiguous bases, some inside the sites + raw := append([]byte(nil), seq.Sequence()...) + for range 3 { // in the flanks, and in the insert past the first bases + at := rng.Intn(len(raw)) + if at >= s.pos-2 && at < s.pos+18+2 || at >= s.pos+18+40-2 && at < s.pos+18+40+18+2 { + continue + } + raw[at] = "nrykm"[rng.Intn(5)] + } + if k < 2 { // one mismatch inside a site, which the search tolerates + raw[s.pos+5] = 'n' + } + s.corpus.Seqs[k] = obiseq.NewBioSequence(seq.Id(), raw, "") + } + return s + } + run := func(pack bool, circular bool) []PairStats { + s := build() + if pack { + s.corpus.Pack() + for i := range s.corpus.Seqs { + if s.corpus.Seqs[i] != nil { + t.Fatal("Pack must free the sequences") + } + } + } + p := s.params() + p.Reference = "s3" + p.ThreePrime = 3 + p.Circular = circular + p.MinSpecificity = 0 + pairs, _, err := Search(s.corpus, s.taxa, p) + if err != nil { + t.Fatal(err) + } + return pairs + } + for _, circular := range []bool{false, true} { + want := run(false, circular) + if len(want) == 0 { + t.Fatal("the test needs pairs") + } + if got := run(true, circular); !reflect.DeepEqual(got, want) { + t.Errorf("circular=%v: packing changes the result (%d pairs, want %d)", circular, len(got), len(want)) + } + } +} + +func TestPackedCorpusAccessors(t *testing.T) { + c := &Corpus{ + Seqs: []*obiseq.BioSequence{obiseq.NewBioSequence("a", []byte(strings.ToLower("ACGTNNAC")), ""), obiseq.NewBioSequence("b", []byte("gg"), "")}, + Roles: []Role{Example, Other}, + } + c.Pack() + if c.Len() != 2 || c.SeqLen(0) != 8 || c.SeqLen(1) != 2 || c.ID(1) != "b" { + t.Errorf("accessors: %d %d %d %q", c.Len(), c.SeqLen(0), c.SeqLen(1), c.ID(1)) + } + if raw, _ := c.bases(0, nil); string(raw) != "acgtnnac" { + t.Errorf("bases: %q", raw) + } + if i, err := c.IndexOfExample("a"); err != nil || i != 0 { + t.Errorf("IndexOfExample: %d %v", i, err) + } +} diff --git a/pkg/obiprimer/pairs.go b/pkg/obiprimer/pairs.go index 8457285..a4f04f8 100644 --- a/pkg/obiprimer/pairs.go +++ b/pkg/obiprimer/pairs.go @@ -24,6 +24,8 @@ type PairParams struct { Circular bool // -c Reference int // -R: corpus index of the reference sequence (used when HasReference) HasReference bool + AmpBudget int // amplicon records held at once in phase 2 (0 = DefaultAmpBudget) + Release func() // see SearchParams.Release } func (p PairParams) validate() error { @@ -51,13 +53,20 @@ type Primer struct { Good bool } -// seqMatch is one occurrence of a primer in one sequence. +// seqMatch is one occurrence of a primer in one sequence, on 8 bytes: there +// are millions of them. type seqMatch struct { - primer int32 - pos uint32 - reverse bool + primer int32 + place uint32 // position << 1, plus 1 when the primer is found reversed } +func newSeqMatch(primer int32, pos uint32, reverse bool) seqMatch { + return seqMatch{primer, pos<<1 | uint32(b2i(reverse))} +} + +func (m seqMatch) pos() uint32 { return m.place >> 1 } +func (m seqMatch) reverse() bool { return m.place&1 == 1 } + // primerSet is the conserved words and, for each corpus sequence, their // occurrences sorted by position. type primerSet struct { @@ -76,21 +85,41 @@ func selectPrimers(c *Corpus, wm []WordMatches, p PairParams) *primerSet { inQ := quorumCount(counts.Examples, p.Sensitivity) outQ := quorumCount(counts.CounterExamples, p.FalsePositive) - ps := &primerSet{perSeq: make([][]seqMatch, len(c.Seqs))} - for _, w := range wm { - ex, co := w.SequencesMatched(c) - if ex < inQ { + ex, co := make([]int, len(wm)), make([]int, len(wm)) + parallelFor(len(wm), func(_, i int) { ex[i], co[i] = wm[i].SequencesMatched(c) }) + + // the occurrences of the words kept, by sequence, each in a slice of the + // exact size (and not in one big array: a few hundred bytes to some kB each + // find room in the free memory, which a single block of tens of MB does not) + ps := &primerSet{perSeq: make([][]seqMatch, c.Len())} + size := make([]int, c.Len()) + for i, w := range wm { + if ex[i] < inQ { continue } - id := int32(len(ps.primers)) - ps.primers = append(ps.primers, Primer{w.Word, ex, co, co <= outQ}) + ps.primers = append(ps.primers, Primer{w.Word, ex[i], co[i], co[i] <= outQ}) for _, m := range w.Matches { - ps.perSeq[m.Seq] = append(ps.perSeq[m.Seq], seqMatch{id, m.Pos, m.Reverse}) + size[m.Seq]++ } } - for _, ms := range ps.perSeq { - slices.SortStableFunc(ms, func(a, b seqMatch) int { return cmp.Compare(a.pos, b.pos) }) + for s, n := range size { + if n > 0 { + ps.perSeq[s] = make([]seqMatch, 0, n) + } } + id := int32(0) + for i, w := range wm { + if ex[i] < inQ { + continue + } + for _, m := range w.Matches { + ps.perSeq[m.Seq] = append(ps.perSeq[m.Seq], newSeqMatch(id, m.Pos, m.Reverse)) + } + id++ + } + parallelFor(c.Len(), func(_, s int) { + slices.SortStableFunc(ps.perSeq[s], func(a, b seqMatch) int { return cmp.Compare(a.pos(), b.pos()) }) + }) return ps } @@ -111,18 +140,28 @@ type occurrence struct { } // forEachOccurrence calls fn for every amplicon of sequence s, applying the -// length, good-primer and 3' constraints of ecoPrimers. -func (ps *primerSet) forEachOccurrence(c *Corpus, s int, p PairParams, fn func(occurrence)) { +// length, good-primer and 3' constraints of ecoPrimers. buf is the buffer in +// which the sequence is unpacked, if the corpus is packed. +func (ps *primerSet) forEachOccurrence(c *Corpus, s int, buf []byte, p PairParams, fn func(occurrence)) []byte { ms := ps.perSeq[s] - lp := p.PrimerLength var raw []byte - if p.ThreePrime > 0 { - raw = c.Seqs[s].Sequence() + if p.ThreePrime > 0 && len(ms) > 1 { + raw, buf = c.bases(s, buf) } + ps.forEachOccurrenceIn(raw, ms, p, fn) + return buf +} + +// forEachOccurrenceIn is forEachOccurrence over ms, the matches of a sequence +// sorted by position (all of them, or only those of the primers of interest, +// which yields the amplicons between these primers and none other), and raw its +// bases, needed when the 3' bases must match exactly. +func (ps *primerSet) forEachOccurrenceIn(raw []byte, ms []seqMatch, p PairParams, fn func(occurrence)) { + lp := p.PrimerLength for i := range ms { for j := i + 1; j < len(ms); j++ { - dpos := int(ms[j].pos) - int(ms[i].pos) + dpos := int(ms[j].pos()) - int(ms[i].pos()) if dpos <= lp { continue } @@ -137,7 +176,7 @@ func (ps *primerSet) forEachOccurrence(c *Corpus, s int, p PairParams, fn func(o // The upstream match is read as is (primer 1 of the pair when its word // is smaller), the downstream one as the reverse primer. - key := pairKey{up.primer, down.primer, !up.reverse, down.reverse} + key := pairKey{up.primer, down.primer, !up.reverse(), down.reverse()} swapped := !(ps.primers[key.p2].Word > ps.primers[key.p1].Word) if swapped { key = pairKey{key.p2, key.p1, key.a2, key.a1} @@ -172,23 +211,23 @@ func (ps *primerSet) threePrimeMatches(raw []byte, up, down seqMatch, p PairPara for t := lp - p.ThreePrime; t < lp; t++ { // forward primer: the word when found direct, its reverse complement otherwise var f byte - if !up.reverse { + if !up.reverse() { f = wordBase(fw, lp, t) } else { f = 3 - wordBase(fw, lp, lp-1-t) } - if s := at(int(up.pos) + t); s > 3 || s != f { + if s := at(int(up.pos()) + t); s > 3 || s != f { return false } // reverse primer: the word when found reverse, its reverse complement otherwise var r byte - if down.reverse { + if down.reverse() { r = wordBase(rv, lp, t) } else { r = 3 - wordBase(rv, lp, lp-1-t) } - if s := at(int(down.pos) + lp - 1 - t); s > 3 || 3-s != r { + if s := at(int(down.pos()) + lp - 1 - t); s > 3 || 3-s != r { return false } } @@ -220,12 +259,13 @@ func (ps *primerSet) countPairs(c *Corpus, p PairParams) map[pairKey]*pairAcc { go func() { defer wg.Done() acc := partial[w] + var buf []byte for s := range jobs { if c.Roles[s] == Other { continue // only the quorums are counted here } isExample := c.Roles[s] == Example - ps.forEachOccurrence(c, s, p, func(o occurrence) { + buf = ps.forEachOccurrence(c, s, buf, p, func(o occurrence) { a := acc[o.key] if a == nil { a = &pairAcc{minLen: ^uint32(0)} @@ -250,7 +290,7 @@ func (ps *primerSet) countPairs(c *Corpus, p PairParams) map[pairKey]*pairAcc { } }() } - for s := range c.Seqs { + for s := range c.Roles { jobs <- s } close(jobs) diff --git a/pkg/obiprimer/pairs_batches_test.go b/pkg/obiprimer/pairs_batches_test.go new file mode 100644 index 0000000..39bc27f --- /dev/null +++ b/pkg/obiprimer/pairs_batches_test.go @@ -0,0 +1,78 @@ +package obiprimer + +import ( + "math/rand" + "reflect" + "strings" + "testing" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq" +) + +func TestAmpBatches(t *testing.T) { + cases := []struct { + count []int + budget int + want [][2]int + }{ + {[]int{3, 3, 3}, 0, [][2]int{{0, 3}}}, + {[]int{3, 3, 3}, 100, [][2]int{{0, 3}}}, + {[]int{3, 3, 3}, 6, [][2]int{{0, 2}, {2, 3}}}, + {[]int{3, 3, 3}, 1, [][2]int{{0, 1}, {1, 2}, {2, 3}}}, // a pair above the budget goes alone + {[]int{9, 1, 1}, 5, [][2]int{{0, 1}, {1, 3}}}, + {nil, 5, nil}, + {[]int{0, 0}, 5, [][2]int{{0, 2}}}, + } + for _, c := range cases { + if got := ampBatches(c.count, c.budget); !reflect.DeepEqual(got, c.want) { + t.Errorf("ampBatches(%v, %d) = %v, want %v", c.count, c.budget, got, c.want) + } + } +} + +// The batches of phase 2 only bound the memory: whatever their size, the pairs, +// their indices and their sites on the reference are the same. +func TestAmpBudgetDoesNotChangeTheResult(t *testing.T) { + s := newSynthetic(10, -1, 5) + // non-targets sharing barcodes with targets, and counterexamples + for k, role := range []Role{Other, Other, CounterExample, Other, CounterExample} { + cp := append([]byte(nil), s.corpus.Seqs[k*2].Sequence()...) + if role == CounterExample { + for i := s.pos; i < s.pos+10; i++ { // no forward site: not amplified + cp[i] = "cgta"[strings.IndexByte("acgt", cp[i])] + } + } + s.corpus.Seqs = append(s.corpus.Seqs, obiseq.NewBioSequence(strings.Repeat("x", k+1), cp, "")) + s.corpus.Roles = append(s.corpus.Roles, role) + s.taxa.Of = append(s.taxa.Of, int32(100+k%3)) + } + // a few unrelated sequences, with a taxon of their own or none + rng := rand.New(rand.NewSource(9)) + for k := 0; k < 3; k++ { + s.corpus.Seqs = append(s.corpus.Seqs, obiseq.NewBioSequence("u"+strings.Repeat("y", k), []byte(strings.ToLower(randDNA(rng, 300))), "")) + s.corpus.Roles = append(s.corpus.Roles, Other) + s.taxa.Of = append(s.taxa.Of, int32(k-1)) + } + + run := func(budget int) []PairStats { + p := s.params() + p.MinSpecificity = 0 // keep every pair: the stress case of the batches + p.FalsePositive = 1 + p.Reference = "s3" + p.AmpBudget = budget + pairs, info, err := Search(s.corpus, s.taxa, p) + if err != nil { + t.Fatal(err) + } + if info.Pair.Candidates < 2 { + t.Fatalf("budget %d: only %d pairs reach phase 2b, the test needs several", budget, info.Pair.Candidates) + } + return pairs + } + want := run(0) + for _, budget := range []int{1, 7, 50, 1000} { + if got := run(budget); !reflect.DeepEqual(got, want) { + t.Errorf("budget %d changes the result:\n got %d pairs\nwant %d pairs", budget, len(got), len(want)) + } + } +} diff --git a/pkg/obiprimer/pairs_eval.go b/pkg/obiprimer/pairs_eval.go index aee5b5d..e5cc823 100644 --- a/pkg/obiprimer/pairs_eval.go +++ b/pkg/obiprimer/pairs_eval.go @@ -5,9 +5,7 @@ import ( "fmt" "math" "slices" - "sync" - "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obidefault" "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obithermo" ) @@ -69,10 +67,11 @@ func FindPairs(c *Corpus, taxa *Taxa, wm []WordMatches, p PairParams, model *obi if err := p.validate(); err != nil { return nil, info, err } - if taxa == nil || len(taxa.Of) != len(c.Seqs) { + if taxa == nil || len(taxa.Of) != c.Len() { return nil, info, fmt.Errorf("obiprimer: taxa do not match the corpus") } ps := selectPrimers(c, wm, p) + wm = nil // the occurrences are in ps now info.Primers = len(ps.primers) for _, pr := range ps.primers { if pr.Good { @@ -104,63 +103,22 @@ func FindPairs(c *Corpus, taxa *Taxa, wm []WordMatches, p PairParams, model *obi return cmp.Or(cmp.Compare(a.p1, b.p1), cmp.Compare(a.p2, b.p2), boolCmp(a.a1, b.a1), boolCmp(a.a2, b.a2)) }) - index := make(map[pairKey]int32, len(kept)) - for i, k := range kept { - index[k] = int32(i) - } // Phase 2a: the amplicons of the target sequences, which give the melting // temperatures, the reference sites, and an upper bound of Bs. It is cheap // and rejects most pairs on the specificity threshold. - first := ps.collectAmplicons(c, taxa, p, index, collectOptions{examplesOnly: true, model: model, reference: true}) + first := ps.boundPairs(c, taxa, p, kept, model) var survivors []int // indices in kept for i := range kept { - targets, identified := lenientIdentified(first.pair(int32(i))...) - if targets > 0 && float32(identified)/float32(targets) >= float32(p.MinSpecificity) { + if first.targets[i] > 0 && float32(first.identified[i])/float32(first.targets[i]) >= float32(p.MinSpecificity) { survivors = append(survivors, i) } } - first.parts = nil info.Candidates = len(survivors) // Phase 2b: for the survivors only, the amplicons of every sequence, whatever // its role: they decide which target taxa are identified. - index2 := make(map[pairKey]int32, len(survivors)) - for j, i := range survivors { - index2[kept[i]] = int32(j) - } - amps := ps.collectAmplicons(c, taxa, p, index2, collectOptions{}) - - results := make([]*PairStats, len(survivors)) - jobs := make(chan int) - var wg sync.WaitGroup - for range obidefault.ParallelWorkers() { - wg.Add(1) - go func() { - defer wg.Done() - for j := range jobs { - i := survivors[j] - k := kept[i] - st, ok := ps.evaluate(taxa, counts, p, k, acc[k], resolve(amps.pair(int32(j))...), - first.minTm1[i], first.minTm2[i], model) - if !ok { - continue - } - if h, found := first.refs[int32(i)]; found { - // evaluate orders the primers by string; the amplicon is read from the first one - s1 := orientedWord(ps.primers[k.p1].Word, p.PrimerLength, k.a1) - s2 := orientedWord(ps.primers[k.p2].Word, p.PrimerLength, k.a2) - st.RefLocation, st.RefAmplicon = referenceSites(c.Seqs[p.Reference].Sequence(), h, p, s2 < s1) - } - results[j] = &st - } - }() - } - for j := range survivors { - jobs <- j - } - close(jobs) - wg.Wait() + results := ps.evaluatePairs(c, taxa, p, kept, survivors, acc, first, model) var out []PairStats for _, st := range results { diff --git a/pkg/obiprimer/pairs_phase2.go b/pkg/obiprimer/pairs_phase2.go new file mode 100644 index 0000000..25d930b --- /dev/null +++ b/pkg/obiprimer/pairs_phase2.go @@ -0,0 +1,160 @@ +package obiprimer + +import ( + "sync" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obidefault" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obithermo" +) + +// DefaultAmpBudget is the number of amplicon records (12 bytes each) phase 2 +// holds at once: 20 million, 240 MB. The pairs are processed in batches whose +// amplicons fit in it. With the usual specificity threshold a search needs one +// batch; with none (-T 0) no pair is rejected early and there are many more +// amplicons than any memory could hold. +const DefaultAmpBudget = 20_000_000 + +func (p PairParams) release() { + if p.Release != nil { + p.Release() + } +} + +func (p PairParams) ampBudget() int { + if p.AmpBudget > 0 { + return p.AmpBudget + } + return DefaultAmpBudget +} + +// batchIndex maps the pairs kept[lo:hi] to their number in the batch. +func batchIndex(kept []pairKey, lo, hi int) map[pairKey]int32 { + index := make(map[pairKey]int32, hi-lo) + for i := lo; i < hi; i++ { + index[kept[i]] = int32(i - lo) + } + return index +} + +// parallelFor calls fn(i) for i in [0,n) on obidefault.ParallelWorkers() goroutines. +func parallelFor(n int, fn func(worker, i int)) { + jobs := make(chan int) + var wg sync.WaitGroup + for w := range obidefault.ParallelWorkers() { + wg.Add(1) + go func() { + defer wg.Done() + for i := range jobs { + fn(w, i) + } + }() + } + for i := 0; i < n; i++ { + jobs <- i + } + close(jobs) + wg.Wait() +} + +// bound is what phase 2a learns about each retained pair (indexed like kept). +type bound struct { + targets []int // target taxa amplified + identified []int // upper bound of the target taxa identified (lenientIdentified) + minTm1 []float32 + minTm2 []float32 + refs map[int32]refHit +} + +// boundPairs is phase 2a: from the amplicons of the target sequences alone, it +// gives each retained pair the upper bound of its Bs, the lowest melting +// temperatures and its site on the reference. The pairs go by batches whose +// amplicons fit in the budget; they are counted first, so that nothing is resized. +func (ps *primerSet) boundPairs(c *Corpus, taxa *Taxa, p PairParams, kept []pairKey, + model *obithermo.Model) *bound { + + plan := ps.planAmplicons(c, p, batchIndex(kept, 0, len(kept)), true) + res := &bound{ + targets: make([]int, len(kept)), + identified: make([]int, len(kept)), + minTm1: make([]float32, len(kept)), + minTm2: make([]float32, len(kept)), + refs: map[int32]refHit{}, + } + resolvers := make([]*resolver, obidefault.ParallelWorkers()) + for i := range resolvers { + resolvers[i] = newResolver() + } + + for _, b := range ampBatches(plan.total(), p.ampBudget()) { + lo, hi := b[0], b[1] + p.release() // the previous batch, or the stage before, is garbage + store := newAmpStore(plan, lo, hi) + sites := ps.collectAmplicons(c, taxa, p, batchIndex(kept, lo, hi), plan, store, + collectOptions{examplesOnly: true, model: model, reference: true}) + store.check() + parallelFor(hi-lo, func(w, i int) { + res.targets[lo+i], res.identified[lo+i] = resolvers[w].lenientIdentified(store.pair(i)) + }) + copy(res.minTm1[lo:hi], sites.minTm1) + copy(res.minTm2[lo:hi], sites.minTm2) + for i, h := range sites.refs { + res.refs[int32(lo)+i] = h + } + } + return res +} + +// evaluatePairs is phase 2b: for the surviving pairs (indices in kept), it +// gathers the amplicons of every sequence, whatever its role, by batches whose +// amplicons fit in the budget, and evaluates the pairs. The result is indexed +// like survivors; a nil entry is a pair dropped by the specificity threshold. +func (ps *primerSet) evaluatePairs(c *Corpus, taxa *Taxa, p PairParams, kept []pairKey, survivors []int, + acc map[pairKey]*pairAcc, first *bound, model *obithermo.Model) []*PairStats { + + results := make([]*PairStats, len(survivors)) + if len(survivors) == 0 { + return results + } + keys := make([]pairKey, len(survivors)) + for j, i := range survivors { + keys[j] = kept[i] + } + // the amplicons of every sequence are counted first: the store is allocated + // at its exact size, and the batches are cut on the exact counts + plan := ps.planAmplicons(c, p, batchIndex(keys, 0, len(keys)), false) + + counts := c.Counts() + var refBases []byte + if p.HasReference { + refBases, _ = c.bases(p.Reference, nil) + } + resolvers := make([]*resolver, obidefault.ParallelWorkers()) + for i := range resolvers { + resolvers[i] = newResolver() + } + for _, b := range ampBatches(plan.total(), p.ampBudget()) { + lo, hi := b[0], b[1] + p.release() + store := newAmpStore(plan, lo, hi) + ps.collectAmplicons(c, taxa, p, batchIndex(keys, lo, hi), plan, store, collectOptions{}) + store.check() + parallelFor(hi-lo, func(w, n int) { + j := lo + n + i := survivors[j] + k := keys[j] + st, ok := ps.evaluate(taxa, counts, p, k, acc[k], resolvers[w].resolve(store.pair(n)), + first.minTm1[i], first.minTm2[i], model) + if !ok { + return + } + if h, found := first.refs[int32(i)]; found { + // evaluate orders the primers by string; the amplicon is read from the first one + s1 := orientedWord(ps.primers[k.p1].Word, p.PrimerLength, k.a1) + s2 := orientedWord(ps.primers[k.p2].Word, p.PrimerLength, k.a2) + st.RefLocation, st.RefAmplicon = referenceSites(refBases, h, p, s2 < s1) + } + results[j] = &st + }) + } + return results +} diff --git a/pkg/obiprimer/reference.go b/pkg/obiprimer/reference.go index a2f6b31..835abc0 100644 --- a/pkg/obiprimer/reference.go +++ b/pkg/obiprimer/reference.go @@ -34,8 +34,8 @@ func referenceSites(raw []byte, h refHit, p PairParams, swappedByOrder bool) (lo // the exchange due to the string order flips it again. firstIsUpstream := h.swapped == swappedByOrder - location = "join(" + span(int(h.up.pos), lp, n, p.Circular) + "," + span(int(h.down.pos), lp, n, p.Circular) + ")" - region := []byte(strings.ToLower(string(raw[int(h.up.pos)+lp : int(h.down.pos)]))) + location = "join(" + span(int(h.up.pos()), lp, n, p.Circular) + "," + span(int(h.down.pos()), lp, n, p.Circular) + ")" + region := []byte(strings.ToLower(string(raw[int(h.up.pos())+lp : int(h.down.pos())]))) if firstIsUpstream { return location, string(region) } diff --git a/pkg/obiprimer/resolution.go b/pkg/obiprimer/resolution.go index 86d0c89..25858d8 100644 --- a/pkg/obiprimer/resolution.go +++ b/pkg/obiprimer/resolution.go @@ -43,49 +43,67 @@ type resolution struct { discriminant int // of which lead to a single taxon } -// resolve computes the resolution of one pair from its amplicons. The slices -// need not be sorted; they are only read. -func resolve(parts ...[]ampRecord) resolution { +// resolver computes the indices of pairs one after the other, reusing its maps +// (a pair needs a few thousand entries, and there are thousands of pairs). +type resolver struct { + state map[uint64]barcodeState + targets map[int32]struct{} + counters map[int32]struct{} + bad map[int32]struct{} + owner map[uint64]int32 +} + +func newResolver() *resolver { + return &resolver{ + state: map[uint64]barcodeState{}, + targets: map[int32]struct{}{}, + counters: map[int32]struct{}{}, + bad: map[int32]struct{}{}, + owner: map[uint64]int32{}, + } +} + +// resolve computes the resolution of one pair from its amplicons, in any order. +func resolve(a ampSlice) resolution { return newResolver().resolve(a) } + +func (rs *resolver) resolve(a ampSlice) resolution { var res resolution + clear(rs.state) + clear(rs.targets) + clear(rs.counters) + clear(rs.bad) - state := map[uint64]barcodeState{} - targets := map[int32]struct{}{} - counters := map[int32]struct{}{} + for i, h := range a.hash { + taxon, role := unpackTaxon(a.tax[i]) + st := rs.state[h] + switch { + case taxon < 0: + st.flags |= barcodeAnonymous + case st.taxon == 0: + st.taxon = taxon + 1 + case st.taxon != taxon+1: + st.flags |= barcodeAmbiguous + } + if taxon >= 0 { + switch role { + case Example: + st.flags |= barcodeTarget + rs.targets[taxon] = struct{}{} + case CounterExample: + rs.counters[taxon] = struct{}{} + } + } + rs.state[h] = st + } - for _, part := range parts { - for _, r := range part { - st := state[r.hash] - switch { - case r.taxon < 0: - st.flags |= barcodeAnonymous - case st.taxon == 0: - st.taxon = r.taxon + 1 - case st.taxon != r.taxon+1: - st.flags |= barcodeAmbiguous - } - if r.taxon >= 0 { - switch r.role { - case Example: - st.flags |= barcodeTarget - targets[r.taxon] = struct{}{} - case CounterExample: - counters[r.taxon] = struct{}{} - } - } - state[r.hash] = st + for i, h := range a.hash { + taxon, role := unpackTaxon(a.tax[i]) + if role == Example && taxon >= 0 && rs.state[h].ambiguous() { + rs.bad[taxon] = struct{}{} } } - bad := map[int32]struct{}{} - for _, part := range parts { - for _, r := range part { - if r.role == Example && r.taxon >= 0 && state[r.hash].ambiguous() { - bad[r.taxon] = struct{}{} - } - } - } - - for _, st := range state { + for _, st := range rs.state { if st.flags&barcodeTarget != 0 { res.barcodes++ if !st.ambiguous() { @@ -93,8 +111,8 @@ func resolve(parts ...[]ampRecord) resolution { } } } - res.targetTaxa, res.counterTaxa = len(targets), len(counters) - res.identified = len(targets) - len(bad) + res.targetTaxa, res.counterTaxa = len(rs.targets), len(rs.counters) + res.identified = len(rs.targets) - len(rs.bad) return res } @@ -107,27 +125,30 @@ func resolve(parts ...[]ampRecord) resolution { // counted here too. So identified <= the second result, with the same number of // target taxa, which lets a pair be rejected on the specificity threshold without // gathering the amplicons of the non-target sequences. -func lenientIdentified(parts ...[]ampRecord) (targetTaxa, identified int) { - owner := map[uint64]int32{} // barcode -> its taxon, or -1 when shared - targets := map[int32]struct{}{} - for _, part := range parts { - for _, r := range part { - if r.role != Example || r.taxon < 0 { - continue - } - targets[r.taxon] = struct{}{} - if prev, seen := owner[r.hash]; !seen { - owner[r.hash] = r.taxon - } else if prev != r.taxon { - owner[r.hash] = -1 - } +func lenientIdentified(a ampSlice) (targetTaxa, identified int) { + return newResolver().lenientIdentified(a) +} + +func (rs *resolver) lenientIdentified(a ampSlice) (targetTaxa, identified int) { + clear(rs.owner) // barcode -> its taxon, or -1 when shared + clear(rs.targets) + for i, h := range a.hash { + taxon, role := unpackTaxon(a.tax[i]) + if role != Example || taxon < 0 { + continue } + rs.targets[taxon] = struct{}{} + if prev, seen := rs.owner[h]; !seen { + rs.owner[h] = taxon + } else if prev != taxon { + rs.owner[h] = -1 + } } - alone := map[int32]struct{}{} - for _, t := range owner { + clear(rs.bad) // here: the taxa owning a barcode + for _, t := range rs.owner { if t >= 0 { - alone[t] = struct{}{} + rs.bad[t] = struct{}{} } } - return len(targets), len(alone) + return len(rs.targets), len(rs.bad) } diff --git a/pkg/obiprimer/resolution_test.go b/pkg/obiprimer/resolution_test.go index 4f625c9..37c6ce0 100644 --- a/pkg/obiprimer/resolution_test.go +++ b/pkg/obiprimer/resolution_test.go @@ -6,6 +6,18 @@ import ( ) // rec builds an amplicon record: barcode, taxon (-1: none at the rank), role. +// pack builds an ampSlice from logical records. +func pack(parts ...[]ampRecord) ampSlice { + var a ampSlice + for _, part := range parts { + for _, r := range part { + a.hash = append(a.hash, r.hash) + a.tax = append(a.tax, packTaxon(r.taxon, r.role)) + } + } + return a +} + func rec(barcode uint64, taxon int32, role Role) ampRecord { return ampRecord{hash: barcode, taxon: taxon, role: role} } @@ -15,10 +27,10 @@ func TestResolutionUserExample(t *testing.T) { // nor B is identified, but A1 and B1 lead to one species each. const A, B = 0, 1 const A1, A2, B1 = 11, 12, 21 - res := resolve([]ampRecord{ + res := resolve(pack([]ampRecord{ rec(A1, A, Example), rec(A2, A, Example), rec(A2, B, Example), rec(B1, B, Example), - }) + })) want := resolution{targetTaxa: 2, identified: 0, barcodes: 3, discriminant: 2} if res != want { t.Fatalf("got %+v, want %+v", res, want) @@ -26,7 +38,7 @@ func TestResolutionUserExample(t *testing.T) { } func TestResolutionAllUnique(t *testing.T) { - res := resolve([]ampRecord{rec(1, 0, Example), rec(2, 1, Example), rec(3, 2, Example)}) + res := resolve(pack([]ampRecord{rec(1, 0, Example), rec(2, 1, Example), rec(3, 2, Example)})) if res.identified != 3 || res.discriminant != 3 || res.barcodes != 3 || res.targetTaxa != 3 { t.Errorf("all barcodes distinct: %+v", res) } @@ -35,10 +47,10 @@ func TestResolutionAllUnique(t *testing.T) { func TestResolutionSameTaxonSharesItsBarcodeFreely(t *testing.T) { // several sequences of one species with the same barcode are not ambiguous, // and several barcodes for one species are fine when none is shared - res := resolve([]ampRecord{ + res := resolve(pack([]ampRecord{ rec(1, 0, Example), rec(1, 0, Example), rec(1, 0, Example), rec(2, 0, Example), - }) + })) if res.identified != 1 || res.barcodes != 2 || res.discriminant != 2 { t.Errorf("%+v", res) } @@ -48,12 +60,12 @@ func TestResolutionSameTaxonSharesItsBarcodeFreely(t *testing.T) { // non-target, or with a counterexample, does not identify it. func TestResolutionNonTargetsCount(t *testing.T) { for _, role := range []Role{Other, CounterExample} { - res := resolve([]ampRecord{ + res := resolve(pack([]ampRecord{ rec(1, 0, Example), // unique among the targets... rec(1, 5, role), // ...but shared with a non-target taxon rec(2, 1, Example), // identified rec(3, 6, role), // a barcode of non-targets only - }) + })) if res.targetTaxa != 2 || res.identified != 1 { t.Errorf("role %d: %+v, want 2 target taxa and 1 identified", role, res) } @@ -65,10 +77,10 @@ func TestResolutionNonTargetsCount(t *testing.T) { } func TestResolutionAmplifiedTaxaCounts(t *testing.T) { - res := resolve([]ampRecord{ + res := resolve(pack([]ampRecord{ rec(1, 0, Example), rec(2, 1, CounterExample), rec(3, 1, CounterExample), rec(4, 2, CounterExample), rec(5, 3, Other), - }) + })) if res.targetTaxa != 1 || res.counterTaxa != 2 { t.Errorf("target taxa %d (want 1), counterexample taxa %d (want 2: others are not counted)", res.targetTaxa, res.counterTaxa) @@ -78,20 +90,20 @@ func TestResolutionAmplifiedTaxaCounts(t *testing.T) { // A sequence with no taxon at the rank is an anonymous taxon, distinct from all // the others: a barcode shared with it is ambiguous. func TestResolutionAnonymousTaxon(t *testing.T) { - res := resolve([]ampRecord{ + res := resolve(pack([]ampRecord{ rec(1, 0, Example), rec(1, -1, Other), // shared with an unknown: ambiguous rec(2, 1, Example), - }) + })) if res.identified != 1 || res.barcodes != 2 || res.discriminant != 1 { t.Errorf("target sharing with an anonymous sequence: %+v", res) } // two anonymous sequences with the same barcode never count: no target carries it - res = resolve([]ampRecord{rec(7, -1, Other), rec(7, -1, Other), rec(8, 0, Example)}) + res = resolve(pack([]ampRecord{rec(7, -1, Other), rec(7, -1, Other), rec(8, 0, Example)})) if res.barcodes != 1 || res.discriminant != 1 || res.identified != 1 { t.Errorf("anonymous barcodes alone: %+v", res) } // a target with no taxon is not a target taxon, and is anonymous too - res = resolve([]ampRecord{rec(1, -1, Example), rec(1, 0, Example), rec(2, 0, Example)}) + res = resolve(pack([]ampRecord{rec(1, -1, Example), rec(1, 0, Example), rec(2, 0, Example)})) if res.targetTaxa != 1 || res.identified != 0 { t.Errorf("an anonymous target sequence sharing a barcode must make the taxon ambiguous: %+v", res) } @@ -101,10 +113,10 @@ func TestResolutionSeveralPartsAndOrder(t *testing.T) { // the amplicons come as one slice per worker, in any order a := []ampRecord{rec(1, 0, Example), rec(2, 1, Example)} b := []ampRecord{rec(2, 2, Other), rec(3, 1, Example)} - if res := resolve(a, b); res.targetTaxa != 2 || res.identified != 1 { + if res := resolve(pack(a, b)); res.targetTaxa != 2 || res.identified != 1 { t.Errorf("taxon 1 shares barcode 2 with taxon 2: %+v", res) } - if res := resolve(b, a); res.targetTaxa != 2 || res.identified != 1 { + if res := resolve(pack(b, a)); res.targetTaxa != 2 || res.identified != 1 { t.Errorf("order must not matter: %+v", res) } } @@ -117,10 +129,10 @@ func TestLenientIdentifiedUserExample(t *testing.T) { rec(11, 0, Example), rec(12, 0, Example), rec(12, 1, Example), rec(21, 1, Example), } - if targets, identified := lenientIdentified(recs); targets != 2 || identified != 2 { + if targets, identified := lenientIdentified(pack(recs)); targets != 2 || identified != 2 { t.Errorf("lenient rule: %d of %d, want 2 of 2", identified, targets) } - if res := resolve(recs); res.identified != 0 { + if res := resolve(pack(recs)); res.identified != 0 { t.Errorf("strict rule: %d identified, want 0", res.identified) } } @@ -138,8 +150,8 @@ func TestLenientIdentifiedIsAnUpperBound(t *testing.T) { } // split in a few parts, as the workers do cut := rng.Intn(len(recs) + 1) - res := resolve(recs[:cut], recs[cut:]) - targets, lenient := lenientIdentified(recs[:cut], recs[cut:]) + res := resolve(pack(recs[:cut], recs[cut:])) + targets, lenient := lenientIdentified(pack(recs[:cut], recs[cut:])) if targets != res.targetTaxa { t.Fatalf("trial %d: %d target taxa for the bound, %d for resolve", trial, targets, res.targetTaxa) } diff --git a/pkg/obiprimer/search.go b/pkg/obiprimer/search.go index f4a5a8f..9ef249b 100644 --- a/pkg/obiprimer/search.go +++ b/pkg/obiprimer/search.go @@ -22,6 +22,14 @@ type SearchParams struct { NoMultiMatch bool // -U: discard the primers matching several times in a sequence Reference string // -R: identifier of an example sequence on which the amplicons are located ("" = none) + // Release, if not nil, is called when a stage has just freed a lot of memory + // and the next one is about to take some: the command makes the runtime hand + // it back to the system then, which the garbage collector would only do + // slowly, and the peak of memory would be the sum of the stages. + Release func() + + AmpBudget int // amplicon records held at once by the pair evaluation (0 = DefaultAmpBudget) + Partitions int // partitions of the k-mer counter (default 256) TmpDir string @@ -33,7 +41,7 @@ func DefaultSearchParams() SearchParams { return SearchParams{ PrimerLength: 18, MaxErrors: 3, MinLength: 0, MaxLength: 1000, StrictQuorum: 0.7, Sensitivity: 0.9, FalsePositive: 0.1, MinSpecificity: 0.6, - Partitions: 256, Model: obithermo.DefaultModel(), + Model: obithermo.DefaultModel(), } } @@ -82,6 +90,9 @@ func Search(c *Corpus, taxa *Taxa, p SearchParams) ([]PairStats, *SearchInfo, er for i, w := range sw.Words { words[i] = w.Kmer } + if p.Release != nil { + p.Release() // the strict word search has just freed its occurrences + } wm, err := MatchWords(c, words, MatchParams{K: p.PrimerLength, MaxErrors: p.MaxErrors, Circular: p.Circular}) if err != nil { return nil, info, err @@ -104,6 +115,8 @@ func Search(c *Corpus, taxa *Taxa, p SearchParams) ([]PairStats, *SearchInfo, er Circular: p.Circular, Reference: refIndex, HasReference: refIndex >= 0, + AmpBudget: p.AmpBudget, + Release: p.Release, }, p.Model) info.Pair = pinfo return pairs, info, err diff --git a/pkg/obiprimer/taxa.go b/pkg/obiprimer/taxa.go index 89698b0..92f0351 100644 --- a/pkg/obiprimer/taxa.go +++ b/pkg/obiprimer/taxa.go @@ -2,6 +2,7 @@ package obiprimer import ( "fmt" + "slices" "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obitax" ) @@ -23,42 +24,54 @@ type Taxa struct { // Name returns the taxon with the given index, for reporting. func (t *Taxa) Name(i int32) *obitax.Taxon { return t.nodes[i] } -// AssignTaxa determines the taxon of every sequence of the corpus at rank. -func (c *Corpus) AssignTaxa(taxonomy *obitax.Taxonomy, rank string) (*Taxa, error) { +// checkRank reports an error if the taxonomy has no such rank. +func checkRank(taxonomy *obitax.Taxonomy, rank string) error { if taxonomy == nil { - return nil, fmt.Errorf("obiprimer: a taxonomy is required to compute taxon statistics") + return fmt.Errorf("obiprimer: a taxonomy is required to compute taxon statistics") } - ranks := taxonomy.RankList() - known := false - for _, r := range ranks { - if r == rank { - known = true - break - } - } - if !known { - return nil, fmt.Errorf("obiprimer: unknown taxonomic rank %q", rank) + if !slices.Contains(taxonomy.RankList(), rank) { + return fmt.Errorf("obiprimer: unknown taxonomic rank %q", rank) } + return nil +} - t := &Taxa{Rank: rank, Of: make([]int32, len(c.Seqs))} +// AssignTaxa determines the taxon of every sequence of the corpus at rank. The +// sequences must not be packed (see LoadPackedCorpus). +func (c *Corpus) AssignTaxa(taxonomy *obitax.Taxonomy, rank string) (*Taxa, error) { + if err := checkRank(taxonomy, rank); err != nil { + return nil, err + } + taxons := make([]*obitax.Taxon, len(c.Seqs)) + for i, seq := range c.Seqs { + taxons[i] = seq.Taxon(taxonomy).TaxonAtRank(rank) + } + return buildTaxa(rank, taxons, c.Roles) +} + +// buildTaxa gives the sequences, whose taxon at rank is taxons[i] (nil when +// there is none), their dense taxon index. +func buildTaxa(rank string, taxons []*obitax.Taxon, roles []Role) (*Taxa, error) { + t := &Taxa{Rank: rank, Of: make([]int32, len(taxons))} index := map[*obitax.TaxNode]int32{} exSeen := map[int32]struct{}{} coSeen := map[int32]struct{}{} - for i, seq := range c.Seqs { + for i, taxon := range taxons { t.Of[i] = -1 - taxon := seq.Taxon(taxonomy).TaxonAtRank(rank) if taxon == nil { continue } id, ok := index[taxon.Node] if !ok { + if len(t.nodes) >= maxTaxa { + return nil, fmt.Errorf("obiprimer: more than %d taxa at rank %q", maxTaxa, rank) + } id = int32(len(t.nodes)) index[taxon.Node] = id t.nodes = append(t.nodes, taxon) } t.Of[i] = id - switch c.Roles[i] { + switch roles[i] { case Example: exSeen[id] = struct{}{} case CounterExample: diff --git a/pkg/obiprimer/words.go b/pkg/obiprimer/words.go index d492039..868126f 100644 --- a/pkg/obiprimer/words.go +++ b/pkg/obiprimer/words.go @@ -14,7 +14,7 @@ type WordSearch struct { PrimerLength int // word size (ecoPrimers -O, 18 by default; 2..31) Quorum float64 // fraction of example sequences that must contain the word (ecoPrimers -q, 0.7) Circular bool // sequences are circular (ecoPrimers -c) - Partitions int // partition files of the counter (default 256) + Partitions int // partition files of the counter (0 = chosen from the size of the examples, see obikmer.DefaultPartitions) TmpDir string // parent of the temporary directory ("" = system default) } @@ -48,7 +48,15 @@ func FindStrictWords(c *Corpus, cfg WordSearch) (*StrictWords, error) { return nil, fmt.Errorf("obiprimer: quorum must be in ]0,1], got %g", cfg.Quorum) } if cfg.Partitions <= 0 { - cfg.Partitions = 256 + // the k-mer occurrences to count: those of the examples, which are the + // only sequences indexed + total := 0 + for i, r := range c.Roles { + if r == Example && c.SeqLen(i) > cfg.PrimerLength { + total += c.SeqLen(i) + } + } + cfg.Partitions = obikmer.DefaultPartitions(total) } counter, err := obikmer.NewSeqKmerCounter(cfg.TmpDir, cfg.PrimerLength, 0, cfg.Partitions) @@ -63,14 +71,17 @@ func FindStrictWords(c *Corpus, cfg WordSearch) (*StrictWords, error) { wg.Add(1) go func() { defer wg.Done() + var buf []byte for i := range jobs { - if seq := c.Seqs[i]; c.Roles[i] == Example && seq.Len() > cfg.PrimerLength { - counter.AddSequence(uint32(i), seq.Sequence(), cfg.Circular) + if c.Roles[i] == Example && c.SeqLen(i) > cfg.PrimerLength { + var raw []byte + raw, buf = c.bases(i, buf) + counter.AddSequence(uint32(i), raw, cfg.Circular) } } }() } - for i := range c.Seqs { + for i := range c.Roles { jobs <- i } close(jobs) diff --git a/pkg/obitools/obiprimers/embedded_test.go b/pkg/obitools/obiprimers/embedded_test.go index cef413e..c1f6438 100644 --- a/pkg/obitools/obiprimers/embedded_test.go +++ b/pkg/obitools/obiprimers/embedded_test.go @@ -3,9 +3,11 @@ package obiprimers import ( "os" "path/filepath" + "reflect" "strings" "testing" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obidefault" "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiiter" "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiprimer" "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq" @@ -195,3 +197,51 @@ func TestTaxonomicPathIsDroppedAfterExtraction(t *testing.T) { t.Errorf("stdin: %d sequences, %d with a path left, %d with a taxid", total, withPath, withTaxid) } } + +// LoadPackedCorpus must give the same corpus and taxa as LoadCorpus followed by +// AssignTaxa, whatever the number of reading goroutines. +func TestLoadPackedCorpusEqualsLoadThenAssign(t *testing.T) { + path := writeTemp(t, embeddedFasta) + defer obidefault.SetMaxCPU(obidefault.MaxCPU()) + for _, workers := range []int{1, 4} { + obidefault.SetMaxCPU(workers) + + sequences, taxonomy, err := readSequencesAndTaxonomy([]string{path}, nil) + if err != nil { + t.Fatal(err) + } + restrict, _ := obiprimer.BelongingTo(taxonomy, []string{"5"}) + sel := obiprimer.Selection{Restrict: restrict} + want, err := obiprimer.LoadCorpus(sequences, sel) + if err != nil { + t.Fatal(err) + } + wantTaxa, err := want.AssignTaxa(taxonomy, "species") + if err != nil { + t.Fatal(err) + } + + sequences, _, err = readSequencesAndTaxonomy([]string{path}, taxonomy) + if err != nil { + t.Fatal(err) + } + got, gotTaxa, err := obiprimer.LoadPackedCorpus(sequences, sel, taxonomy, "species") + if err != nil { + t.Fatal(err) + } + if got.Counts() != want.Counts() || got.Len() != want.Len() { + t.Errorf("workers %d: counts %+v / %+v", workers, got.Counts(), want.Counts()) + } + for i := range want.Roles { + if got.Roles[i] != want.Roles[i] || got.ID(i) != want.ID(i) || got.SeqLen(i) != want.SeqLen(i) || got.Seqs[i] != nil { + t.Errorf("workers %d, sequence %d differs", workers, i) + } + } + if !reflect.DeepEqual(gotTaxa.Of, wantTaxa.Of) || gotTaxa.ExampleTaxa != wantTaxa.ExampleTaxa { + t.Errorf("workers %d: taxa %v / %v", workers, gotTaxa.Of, wantTaxa.Of) + } + } + if _, _, err := obiprimer.LoadPackedCorpus(obiiter.NilIBioSequence, obiprimer.Selection{}, nil, "species"); err == nil { + t.Error("a nil taxonomy must be refused") + } +} diff --git a/pkg/obitools/obiprimers/memory.go b/pkg/obitools/obiprimers/memory.go new file mode 100644 index 0000000..04499e5 --- /dev/null +++ b/pkg/obitools/obiprimers/memory.go @@ -0,0 +1,45 @@ +package obiprimers + +import ( + "os" + "runtime/debug" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obidefault" +) + +// gcPercent is the garbage collector target of the command: the heap may grow +// by that percentage over the live data before the next collection (the Go +// default is 100). Reading the sequences and the search allocate large buffers +// and tables that die as soon as a stage is over, and with the default the +// process holds up to twice what it needs at its peak. Peak of memory and CPU +// time against the default (see OBIPRIMERS_NOTES.md, section 26): 7822 +// vertebrate genomes, 50 gives -8% and +0%, 25 -13% and +8%; 18061 genomes +// (593 Mb), 50 gives -20% and +3%, 25 -29% and +8.5%. +const gcPercent = 50 + +// batchSizeMax is the default of the --batch-size-max option for this command. +// The reading goroutines hand the sequences over by batches, and a batch of 2000 +// genomes of 16 kb (the general default) is 33 Mb of bases once parsed, times +// the number of workers and the batches waiting between the stages. Genomes are +// few and long: 100 per batch keeps all the cores busy. +const batchSizeMax = 100 + +// SetReadingDefaults gives the options shared by the OBITools their defaults for +// this command; it must be called before the options are parsed, which still +// override them. +func SetReadingDefaults() { + *obidefault.BatchSizeMaxPtr() = batchSizeMax +} + +// tuneMemory sets the collector target. A user who set GOGC keeps it. +func tuneMemory() { + if os.Getenv("GOGC") == "" { + debug.SetGCPercent(gcPercent) + } +} + +// releaseMemory collects the garbage and gives the free memory back to the +// system. Without it the memory freed by a stage stays in the process (the +// runtime returns it slowly), and the next stage takes new memory on top of it: +// the peak is then the sum of the stages instead of the largest. +func releaseMemory() { debug.FreeOSMemory() } diff --git a/pkg/obitools/obiprimers/obiprimers.go b/pkg/obitools/obiprimers/obiprimers.go index 59e6dc1..b1c27a0 100644 --- a/pkg/obitools/obiprimers/obiprimers.go +++ b/pkg/obitools/obiprimers/obiprimers.go @@ -94,6 +94,7 @@ func readSequencesAndTaxonomy(args []string, given *obitax.Taxonomy) (obiiter.IB // returns them with the taxonomy to use, set as the default taxonomy: the one // of the -t option, or else the one embedded in the sequences. func CLIReadSequencesAndTaxonomy(args []string) (obiiter.IBioSequence, *obitax.Taxonomy, error) { + tuneMemory() given := obitax.DefaultTaxonomy() sequences, taxonomy, err := readSequencesAndTaxonomy(args, given) if err != nil { @@ -130,11 +131,8 @@ func CLIPrimerPairs(sequences obiiter.IBioSequence, taxonomy *obitax.Taxonomy) ( } log.Infof("Reading the sequences") - corpus, err := obiprimer.LoadCorpus(sequences, sel) - if err != nil { - return nil, err - } - taxa, err := corpus.AssignTaxa(taxonomy, CLIRank()) + // the sequences are packed, and their taxa looked up, as they are read + corpus, taxa, err := obiprimer.LoadPackedCorpus(sequences, sel, taxonomy, CLIRank()) if err != nil { return nil, err } @@ -144,6 +142,8 @@ func CLIPrimerPairs(sequences obiiter.IBioSequence, taxonomy *obitax.Taxonomy) ( counts.Examples, taxa.ExampleTaxa, CLIRank(), counts.CounterExamples, taxa.CounterExamples, CLIRank(), counts.Others) + releaseMemory() // the sequences were read through large transient buffers + pairs, info, err := obiprimer.Search(corpus, taxa, params) if info != nil { log.Infof("%d strict words (found without error in at least %d examples)", info.StrictWords, info.MinStrict) diff --git a/pkg/obitools/obiprimers/options.go b/pkg/obitools/obiprimers/options.go index 13d37f4..85b2078 100644 --- a/pkg/obitools/obiprimers/options.go +++ b/pkg/obitools/obiprimers/options.go @@ -40,7 +40,7 @@ var _saltMethod = "santalucia" var _salt = obithermo.DefaultSalt var _primerConc = obithermo.DefaultPrimerConc -var _partitions = 256 +var _partitions = 0 // OptionSet registers the options of obiprimers. func OptionSet(options *getoptions.GetOpt) { @@ -148,8 +148,9 @@ func OptionSet(options *getoptions.GetOpt) { options.IntVar(&_partitions, "partitions", _partitions, options.ArgName("COUNT"), - options.Description("Number of temporary partition files used to count the conserved words. "+ - "Increase it for very large inputs (memory per partition grows with data size / COUNT).")) + options.Description("Number of temporary partition files used to count the conserved words "+ + "(0 = chosen from the size of the examples: 256 to 1024). The memory of the count is bounded "+ + "whatever the number; more files only make the largest partitions smaller.")) } // CLIRestrictTaxa returns the taxids restricting the examples. @@ -198,5 +199,6 @@ func CLISearchParams() (obiprimer.SearchParams, error) { Reference: _reference, Partitions: _partitions, Model: model, + Release: releaseMemory, }, nil } From c906e7e6bc0c6e18cf707e2252fbe5d783ed285a Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Tue, 3 Nov 2026 23:19:56 +1100 Subject: [PATCH 23/24] Release 4.5.1 --- pkg/obioptions/version.go | 2 +- version.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/obioptions/version.go b/pkg/obioptions/version.go index ac10bdb..5ab17f1 100644 --- a/pkg/obioptions/version.go +++ b/pkg/obioptions/version.go @@ -3,7 +3,7 @@ package obioptions // Version is automatically updated by the Makefile from version.txt // The patch number (third digit) is incremented on each push to the repository -var _Version = "Release 4.5.0" +var _Version = "Release 4.5.1" // Version returns the version of the obitools package. // diff --git a/version.txt b/version.txt index a84947d..4404a17 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -4.5.0 +4.5.1 From beaa9984e35292ebc08c3f8dd9f9aeb5a8c28b14 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Tue, 3 Nov 2026 23:26:39 +1100 Subject: [PATCH 24/24] Release 4.5.2 --- pkg/obioptions/version.go | 2 +- version.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/obioptions/version.go b/pkg/obioptions/version.go index 5ab17f1..277f4e1 100644 --- a/pkg/obioptions/version.go +++ b/pkg/obioptions/version.go @@ -3,7 +3,7 @@ package obioptions // Version is automatically updated by the Makefile from version.txt // The patch number (third digit) is incremented on each push to the repository -var _Version = "Release 4.5.1" +var _Version = "Release 4.5.2" // Version returns the version of the obitools package. // diff --git a/version.txt b/version.txt index 4404a17..6cedcff 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -4.5.1 +4.5.2