mirror of
https://github.com/metabarcoding/obitools4.git
synced 2026-03-25 13:30:52 +00:00
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.
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -27,7 +27,8 @@ xx
|
|||||||
|
|
||||||
!/obitests/**
|
!/obitests/**
|
||||||
!/sample/**
|
!/sample/**
|
||||||
LLM/**
|
LLM/**
|
||||||
*_files
|
*_files
|
||||||
|
|
||||||
entropy.html
|
entropy.html
|
||||||
|
bug_id.txt
|
||||||
|
|||||||
99
blackboard/architechture/definition-superkmer.md
Normal file
99
blackboard/architechture/definition-superkmer.md
Normal file
@@ -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
|
||||||
@@ -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{
|
superKmer := SuperKmer{
|
||||||
Minimizer: currentMinimizer,
|
Minimizer: currentMinimizer,
|
||||||
Start: superKmerStart,
|
Start: superKmerStart,
|
||||||
|
|||||||
@@ -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
|
// Note: Tests for ToBioSequence and SuperKmerWorker are in a separate
|
||||||
// integration test package to avoid circular dependencies between
|
// integration test package to avoid circular dependencies between
|
||||||
// obikmer and obiseq packages.
|
// obikmer and obiseq packages.
|
||||||
|
|||||||
Reference in New Issue
Block a user