From aa468ec46294cdf2432b3cec1077d49beb47157b Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Thu, 5 Feb 2026 14:46:52 +0100 Subject: [PATCH] Refactor FrequencyFilter to use KmerSetGroup Refactor FrequencyFilter to inherit from KmerSetGroup for better code organization and maintainability. This change replaces the direct bitmap management with a group-based approach, simplifying the implementation and improving readability. --- pkg/obikmer/frequency_filter.go | 71 ++++-------- pkg/obikmer/kmer_set_group.go | 195 ++++++++++++++++++++++++++++++++ 2 files changed, 218 insertions(+), 48 deletions(-) create mode 100644 pkg/obikmer/kmer_set_group.go diff --git a/pkg/obikmer/frequency_filter.go b/pkg/obikmer/frequency_filter.go index 2bf9dcf..7caacf5 100644 --- a/pkg/obikmer/frequency_filter.go +++ b/pkg/obikmer/frequency_filter.go @@ -4,34 +4,21 @@ import ( "fmt" "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq" - "github.com/RoaringBitmap/roaring/roaring64" ) // FrequencyFilter filtre les k-mers par fréquence minimale -// Utilise v bitmaps où index[i] contient les k-mers vus au moins i+1 fois +// Spécialisation de KmerSetGroup où index[i] contient les k-mers vus au moins i+1 fois type FrequencyFilter struct { - K int - MinFreq int // v - fréquence minimale requise - index []*roaring64.Bitmap // index[i] = k-mers vus ≥(i+1) fois + *KmerSetGroup // Groupe de KmerSet (un par niveau de fréquence) + MinFreq int // v - fréquence minimale requise } // NewFrequencyFilter crée un nouveau filtre par fréquence // minFreq: nombre minimum d'occurrences requises (v) func NewFrequencyFilter(k, minFreq int) *FrequencyFilter { - if minFreq < 1 { - panic("minFreq must be >= 1") - } - - // Créer v bitmaps - bitmaps := make([]*roaring64.Bitmap, minFreq) - for i := range bitmaps { - bitmaps[i] = roaring64.New() - } - return &FrequencyFilter{ - K: k, - MinFreq: minFreq, - index: bitmaps, + KmerSetGroup: NewKmerSetGroup(k, minFreq), + MinFreq: minFreq, } } @@ -48,30 +35,30 @@ func (ff *FrequencyFilter) AddSequence(seq *obiseq.BioSequence) { func (ff *FrequencyFilter) addKmer(kmer uint64) { // Trouver le niveau actuel du k-mer c := 0 - for c < ff.MinFreq && ff.index[c].Contains(kmer) { + for c < ff.MinFreq && ff.Get(c).Contains(kmer) { c++ } // Ajouter au niveau suivant (si pas encore au maximum) if c < ff.MinFreq { - ff.index[c].Add(kmer) + ff.Get(c).Add(kmer) } } // GetFilteredSet retourne un KmerSet des k-mers avec fréquence ≥ minFreq func (ff *FrequencyFilter) GetFilteredSet() *KmerSet { // Les k-mers filtrés sont dans le dernier niveau - return NewKmerSetFromBitmap(ff.K, ff.index[ff.MinFreq-1].Clone()) + return ff.Get(ff.MinFreq - 1).Clone() } // GetKmersAtLevel retourne un KmerSet des k-mers vus au moins (level+1) fois // level doit être dans [0, minFreq-1] func (ff *FrequencyFilter) GetKmersAtLevel(level int) *KmerSet { - if level < 0 || level >= ff.MinFreq { + ks := ff.Get(level) + if ks == nil { return NewKmerSet(ff.K) } - - return NewKmerSetFromBitmap(ff.K, ff.index[level].Clone()) + return ks.Clone() } // Stats retourne des statistiques sur les niveaux de fréquence @@ -82,8 +69,9 @@ func (ff *FrequencyFilter) Stats() FrequencyFilterStats { } for i := 0; i < ff.MinFreq; i++ { - card := ff.index[i].GetCardinality() - sizeBytes := ff.index[i].GetSizeInBytes() + ks := ff.Get(i) + card := ks.Len() + sizeBytes := ks.MemoryUsage() stats.Levels[i] = LevelStats{ Level: i + 1, // Niveau 1 = freq ≥ 1 @@ -134,10 +122,9 @@ Level breakdown: } // Clear libère la mémoire de tous les niveaux +// (héritée de KmerSetGroup mais redéfinie pour clarté) func (ff *FrequencyFilter) Clear() { - for _, bitmap := range ff.index { - bitmap.Clear() - } + ff.KmerSetGroup.Clear() } // ================================== @@ -175,7 +162,7 @@ func (ff *FrequencyFilter) Load(path string) error { // Contains vérifie si un k-mer a atteint la fréquence minimale func (ff *FrequencyFilter) Contains(kmer uint64) bool { canonical := NormalizeKmer(kmer, ff.K) - return ff.index[ff.MinFreq-1].Contains(canonical) + return ff.Get(ff.MinFreq - 1).Contains(canonical) } // GetFrequency retourne la fréquence approximative d'un k-mer @@ -185,7 +172,7 @@ func (ff *FrequencyFilter) GetFrequency(kmer uint64) int { freq := 0 for i := 0; i < ff.MinFreq; i++ { - if ff.index[i].Contains(canonical) { + if ff.Get(i).Contains(canonical) { freq = i + 1 } else { break @@ -199,27 +186,15 @@ func (ff *FrequencyFilter) GetFrequency(kmer uint64) int { // Sans argument: retourne le nombre de k-mers avec freq ≥ minFreq (dernier niveau) // Avec argument level: retourne le nombre de k-mers avec freq ≥ (level+1) // Exemple: Len() pour les k-mers filtrés, Len(2) pour freq ≥ 3 +// (héritée de KmerSetGroup mais redéfinie pour la documentation) func (ff *FrequencyFilter) Len(level ...int) uint64 { - if len(level) == 0 { - // Sans argument: dernier niveau (k-mers filtrés) - return ff.index[ff.MinFreq-1].GetCardinality() - } - - // Avec argument: niveau spécifique - lvl := level[0] - if lvl < 0 || lvl >= ff.MinFreq { - return 0 - } - return ff.index[lvl].GetCardinality() + return ff.KmerSetGroup.Len(level...) } // MemoryUsage retourne l'utilisation mémoire en bytes +// (héritée de KmerSetGroup mais redéfinie pour clarté) func (ff *FrequencyFilter) MemoryUsage() uint64 { - total := uint64(0) - for _, bitmap := range ff.index { - total += bitmap.GetSizeInBytes() - } - return total + return ff.KmerSetGroup.MemoryUsage() } // ================================== @@ -228,7 +203,7 @@ func (ff *FrequencyFilter) MemoryUsage() uint64 { // CompareWithSimpleMap compare la mémoire avec une simple map func (ff *FrequencyFilter) CompareWithSimpleMap() string { - totalKmers := ff.index[0].GetCardinality() + totalKmers := ff.Get(0).Len() simpleMapBytes := totalKmers * 24 // ~24 bytes par entrée roaringBytes := ff.MemoryUsage() diff --git a/pkg/obikmer/kmer_set_group.go b/pkg/obikmer/kmer_set_group.go new file mode 100644 index 0000000..00dbf99 --- /dev/null +++ b/pkg/obikmer/kmer_set_group.go @@ -0,0 +1,195 @@ +package obikmer + +import ( + "fmt" + + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq" +) + +// KmerSetGroup représente un vecteur de KmerSet +// Utilisé pour gérer plusieurs ensembles de k-mers (par exemple, par niveau de fréquence) +type KmerSetGroup struct { + K int // Taille des k-mers + sets []*KmerSet // Vecteur de KmerSet +} + +// NewKmerSetGroup crée un nouveau groupe de n KmerSets +func NewKmerSetGroup(k int, n int) *KmerSetGroup { + if n < 1 { + panic("KmerSetGroup size must be >= 1") + } + + sets := make([]*KmerSet, n) + for i := range sets { + sets[i] = NewKmerSet(k) + } + + return &KmerSetGroup{ + K: k, + sets: sets, + } +} + +// Size retourne le nombre de KmerSet dans le groupe +func (ksg *KmerSetGroup) Size() int { + return len(ksg.sets) +} + +// Get retourne le KmerSet à l'index donné +// Retourne nil si l'index est invalide +func (ksg *KmerSetGroup) Get(index int) *KmerSet { + if index < 0 || index >= len(ksg.sets) { + return nil + } + return ksg.sets[index] +} + +// Set remplace le KmerSet à l'index donné +// Panique si l'index est invalide ou si le k ne correspond pas +func (ksg *KmerSetGroup) Set(index int, ks *KmerSet) { + if index < 0 || index >= len(ksg.sets) { + panic(fmt.Sprintf("Index out of bounds: %d (size: %d)", index, len(ksg.sets))) + } + if ks.K != ksg.K { + panic(fmt.Sprintf("KmerSet k mismatch: expected %d, got %d", ksg.K, ks.K)) + } + ksg.sets[index] = ks +} + +// Len retourne le nombre de k-mers dans un KmerSet spécifique +// Sans argument: retourne le nombre de k-mers dans le dernier KmerSet +// Avec argument index: retourne le nombre de k-mers dans le KmerSet à cet index +func (ksg *KmerSetGroup) Len(index ...int) uint64 { + if len(index) == 0 { + // Sans argument: dernier KmerSet + return ksg.sets[len(ksg.sets)-1].Len() + } + + // Avec argument: KmerSet spécifique + idx := index[0] + if idx < 0 || idx >= len(ksg.sets) { + return 0 + } + return ksg.sets[idx].Len() +} + +// MemoryUsage retourne l'utilisation mémoire totale en bytes +func (ksg *KmerSetGroup) MemoryUsage() uint64 { + total := uint64(0) + for _, ks := range ksg.sets { + total += ks.MemoryUsage() + } + return total +} + +// Clear vide tous les KmerSet du groupe +func (ksg *KmerSetGroup) Clear() { + for _, ks := range ksg.sets { + ks.Clear() + } +} + +// Clone crée une copie complète du groupe +func (ksg *KmerSetGroup) Clone() *KmerSetGroup { + clonedSets := make([]*KmerSet, len(ksg.sets)) + for i, ks := range ksg.sets { + clonedSets[i] = ks.Clone() + } + return &KmerSetGroup{ + K: ksg.K, + sets: clonedSets, + } +} + +// AddSequence ajoute tous les k-mers d'une séquence à un KmerSet spécifique +func (ksg *KmerSetGroup) AddSequence(seq *obiseq.BioSequence, index int) { + if index < 0 || index >= len(ksg.sets) { + panic(fmt.Sprintf("Index out of bounds: %d (size: %d)", index, len(ksg.sets))) + } + ksg.sets[index].AddSequence(seq) +} + +// AddSequences ajoute tous les k-mers de plusieurs séquences à un KmerSet spécifique +func (ksg *KmerSetGroup) AddSequences(sequences *obiseq.BioSequenceSlice, index int) { + if index < 0 || index >= len(ksg.sets) { + panic(fmt.Sprintf("Index out of bounds: %d (size: %d)", index, len(ksg.sets))) + } + ksg.sets[index].AddSequences(sequences) +} + +// Union retourne l'union de tous les KmerSet du groupe +func (ksg *KmerSetGroup) Union() *KmerSet { + if len(ksg.sets) == 0 { + return NewKmerSet(ksg.K) + } + + result := ksg.sets[0].Clone() + for i := 1; i < len(ksg.sets); i++ { + result = result.Union(ksg.sets[i]) + } + return result +} + +// Intersect retourne l'intersection de tous les KmerSet du groupe +func (ksg *KmerSetGroup) Intersect() *KmerSet { + if len(ksg.sets) == 0 { + return NewKmerSet(ksg.K) + } + + result := ksg.sets[0].Clone() + for i := 1; i < len(ksg.sets); i++ { + result = result.Intersect(ksg.sets[i]) + } + return result +} + +// Stats retourne des statistiques pour chaque KmerSet du groupe +type KmerSetGroupStats struct { + K int + Size int // Nombre de KmerSet + TotalBytes uint64 // Mémoire totale utilisée + Sets []KmerSetStats // Stats de chaque KmerSet +} + +type KmerSetStats struct { + Index int // Index du KmerSet dans le groupe + Len uint64 // Nombre de k-mers + SizeBytes uint64 // Taille en bytes +} + +func (ksg *KmerSetGroup) Stats() KmerSetGroupStats { + stats := KmerSetGroupStats{ + K: ksg.K, + Size: len(ksg.sets), + Sets: make([]KmerSetStats, len(ksg.sets)), + } + + for i, ks := range ksg.sets { + sizeBytes := ks.MemoryUsage() + stats.Sets[i] = KmerSetStats{ + Index: i, + Len: ks.Len(), + SizeBytes: sizeBytes, + } + stats.TotalBytes += sizeBytes + } + + return stats +} + +func (ksgs KmerSetGroupStats) String() string { + result := fmt.Sprintf(`KmerSetGroup Statistics (k=%d, size=%d): + Total memory: %.2f MB + +Set breakdown: +`, ksgs.K, ksgs.Size, float64(ksgs.TotalBytes)/1024/1024) + + for _, set := range ksgs.Sets { + result += fmt.Sprintf(" Set[%d]: %d k-mers (%.2f MB)\n", + set.Index, + set.Len, + float64(set.SizeBytes)/1024/1024) + } + + return result +}