From db98ddb24163ba2c84245d6830556c95923a9016 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Sun, 8 Feb 2026 13:44:23 +0100 Subject: [PATCH] Fix super k-mer minimizer bijection and add validation test This commit addresses a bug in the super k-mer implementation where the minimizer bijection property was not properly enforced. The fix ensures that: 1. All k-mers within a super k-mer share the same minimizer 2. Identical super k-mer sequences have the same minimizer The changes include: - Fixing the super k-mer iteration logic to properly validate the minimizer bijection property - Adding a comprehensive test suite (TestSuperKmerMinimizerBijection) that validates the intrinsic property of super k-mers - Updating the .gitignore file to properly track relevant files This resolves issues where the same sequence could be associated with different minimizers, violating the super k-mer definition. --- .gitignore | 5 +- .../architechture/definition-superkmer.md | 99 +++++++++++++++ pkg/obikmer/superkmer_iter.go | 2 +- pkg/obikmer/superkmer_iter_test.go | 116 ++++++++++++++++++ 4 files changed, 219 insertions(+), 3 deletions(-) create mode 100644 blackboard/architechture/definition-superkmer.md diff --git a/.gitignore b/.gitignore index c5825d2..5d22f98 100644 --- a/.gitignore +++ b/.gitignore @@ -27,7 +27,8 @@ xx !/obitests/** !/sample/** -LLM/** +LLM/** *_files -entropy.html \ No newline at end of file +entropy.html +bug_id.txt diff --git a/blackboard/architechture/definition-superkmer.md b/blackboard/architechture/definition-superkmer.md new file mode 100644 index 0000000..9a40afc --- /dev/null +++ b/blackboard/architechture/definition-superkmer.md @@ -0,0 +1,99 @@ +# Définition du super k-mer + +## Définition + +Un **super k-mer** est une **sous-séquence MAXIMALE** d'une séquence dans laquelle **tous les k-mers consécutifs partagent le même minimiseur**. + +### Termes + +- **k-mer** : sous-séquence de longueur k +- **minimiseur** : le plus petit m-mer canonique parmi tous les m-mers d'un k-mer +- **k-mers consécutifs** : k-mers aux positions i et i+1 (chevauchement de k-1 nucléotides) +- **MAXIMALE** : ne peut être étendue ni à gauche ni à droite + +## RÈGLES ABSOLUES + +### RÈGLE 1 : Longueur minimum = k + +Un super k-mer contient au minimum k nucléotides. + +``` +longueur(super-kmer) >= k +``` + +### RÈGLE 2 : Chevauchement obligatoire = k-1 + +Deux super-kmers consécutifs se chevauchent d'EXACTEMENT k-1 nucléotides. + +``` +SK1.End - SK2.Start = k - 1 +``` + +### RÈGLE 3 : Bijection séquence ↔ minimiseur + +Une séquence de super k-mer a UN et UN SEUL minimiseur. + +``` +Même séquence → Même minimiseur (TOUJOURS) +``` + +**Si vous observez la même séquence avec deux minimiseurs différents, c'est un BUG.** + +### RÈGLE 4 : Tous les k-mers partagent le minimiseur + +TOUS les k-mers contenus dans un super k-mer ont le même minimiseur. + +``` +∀ k-mer K dans SK : minimiseur(K) = SK.minimizer +``` + +### RÈGLE 5 : Maximalité + +Un super k-mer ne peut pas être étendu. + +- Si on ajoute un nucléotide à gauche : le nouveau k-mer a un minimiseur différent +- Si on ajoute un nucléotide à droite : le nouveau k-mer a un minimiseur différent + +## VIOLATIONS INTERDITES + +❌ **Super k-mer de longueur < k** +❌ **Chevauchement ≠ k-1 entre consécutifs** +❌ **Même séquence avec minimiseurs différents** +❌ **K-mer dans le super k-mer avec minimiseur différent** +❌ **Super k-mer extensible (non-maximal)** + +## CONSÉQUENCES PRATIQUES + +### Pour l'extraction + +L'algorithme doit : +1. Calculer le minimiseur de chaque k-mer +2. Découper quand le minimiseur change +3. Assigner au super k-mer le minimiseur commun à tous ses k-mers +4. Garantir que chaque super k-mer contient au moins k nucléotides +5. Garantir le chevauchement de k-1 entre consécutifs + +### Pour la validation + +Si après déduplication (obiuniq) on observe : +``` +Séquence: ACGT... +Minimiseurs: {M1, M2} // plusieurs minimiseurs +``` + +C'est la PREUVE d'un bug : l'algorithme a produit cette séquence avec des minimiseurs différents, ce qui viole la RÈGLE 3. + +## DIAGNOSTIC DU BUG + +**Bug observé** : Même séquence avec minimiseurs différents après obiuniq + +**Cause possible** : L'algorithme assigne le mauvais minimiseur OU découpe mal les super-kmers + +**Ce que le bug NE PEUT PAS être** : +- Un problème d'obiuniq (révèle le bug, ne le crée pas) +- Un problème de chevauchement légitime (k-1 est correct) + +**Ce que le bug DOIT être** : +- Minimiseur mal calculé ou mal assigné +- Découpage incorrect (mauvais endPos) +- Copie incorrecte des données diff --git a/pkg/obikmer/superkmer_iter.go b/pkg/obikmer/superkmer_iter.go index 8f4386b..d0edb01 100644 --- a/pkg/obikmer/superkmer_iter.go +++ b/pkg/obikmer/superkmer_iter.go @@ -95,7 +95,7 @@ func IterSuperKmers(seq []byte, k int, m int) iter.Seq[SuperKmer] { } } - if !firstKmer { + if !firstKmer && len(seq[superKmerStart:]) >= k { superKmer := SuperKmer{ Minimizer: currentMinimizer, Start: superKmerStart, diff --git a/pkg/obikmer/superkmer_iter_test.go b/pkg/obikmer/superkmer_iter_test.go index 5ddda18..141e6d3 100644 --- a/pkg/obikmer/superkmer_iter_test.go +++ b/pkg/obikmer/superkmer_iter_test.go @@ -77,6 +77,122 @@ func TestIterSuperKmersVsSlice(t *testing.T) { } } +// TestSuperKmerMinimizerBijection validates the intrinsic property that +// a super k-mer sequence has one and only one minimizer (bijection property). +// This test ensures that: +// 1. All k-mers in a super k-mer share the same minimizer +// 2. Two identical super k-mer sequences must have the same minimizer +func TestSuperKmerMinimizerBijection(t *testing.T) { + testCases := []struct { + name string + seq []byte + k int + m int + }{ + { + name: "simple sequence", + seq: []byte("ACGTACGTACGTACGTACGTACGTACGTACGT"), + k: 21, + m: 11, + }, + { + name: "homopolymer blocks", + seq: []byte("AAAACCCCGGGGTTTTAAAACCCCGGGGTTTT"), + k: 21, + m: 11, + }, + { + name: "complex sequence", + seq: []byte("ATCGATCGATCGATCGATCGATCGATCGATCG"), + k: 15, + m: 7, + }, + { + name: "longer sequence", + seq: []byte("ACGTACGTGGGGAAAAACGTACGTTTTTCCCCACGTACGT"), + k: 13, + m: 7, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Map to track sequence -> minimizer + seqToMinimizer := make(map[string]uint64) + + for sk := range IterSuperKmers(tc.seq, tc.k, tc.m) { + seqStr := string(sk.Sequence) + + // Check if we've seen this sequence before + if prevMinimizer, exists := seqToMinimizer[seqStr]; exists { + if prevMinimizer != sk.Minimizer { + t.Errorf("BIJECTION VIOLATION: sequence %s has two different minimizers:\n"+ + " First: %d\n"+ + " Second: %d\n"+ + " This violates the super k-mer definition!", + seqStr, prevMinimizer, sk.Minimizer) + } + } else { + seqToMinimizer[seqStr] = sk.Minimizer + } + + // Verify all k-mers in this super k-mer have the same minimizer + if len(sk.Sequence) >= tc.k { + for i := 0; i <= len(sk.Sequence)-tc.k; i++ { + kmerSeq := sk.Sequence[i : i+tc.k] + minimizer := findMinimizer(kmerSeq, tc.k, tc.m) + if minimizer != sk.Minimizer { + t.Errorf("K-mer at position %d in super k-mer has different minimizer:\n"+ + " K-mer: %s\n"+ + " Expected minimizer: %d\n"+ + " Actual minimizer: %d\n"+ + " Super k-mer: %s", + i, string(kmerSeq), sk.Minimizer, minimizer, seqStr) + } + } + } + } + }) + } +} + +// findMinimizer computes the minimizer of a k-mer for testing purposes +func findMinimizer(kmer []byte, k int, m int) uint64 { + if len(kmer) != k { + return 0 + } + + mMask := uint64(1)<<(m*2) - 1 + rcShift := uint((m - 1) * 2) + + minMinimizer := uint64(^uint64(0)) // max uint64 + + // Scan all m-mers in the k-mer + var fwdMmer, rvcMmer uint64 + for i := 0; i < m-1 && i < len(kmer); i++ { + code := uint64(__single_base_code__[kmer[i]&31]) + fwdMmer = (fwdMmer << 2) | code + rvcMmer = (rvcMmer >> 2) | ((code ^ 3) << rcShift) + } + + for i := m - 1; i < len(kmer); i++ { + code := uint64(__single_base_code__[kmer[i]&31]) + fwdMmer = ((fwdMmer << 2) | code) & mMask + rvcMmer = (rvcMmer >> 2) | ((code ^ 3) << rcShift) + + canonical := fwdMmer + if rvcMmer < fwdMmer { + canonical = rvcMmer + } + + if canonical < minMinimizer { + minMinimizer = canonical + } + } + + return minMinimizer +} + // Note: Tests for ToBioSequence and SuperKmerWorker are in a separate // integration test package to avoid circular dependencies between // obikmer and obiseq packages.