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.
This commit is contained in:
Eric Coissac committed 2026-11-03 09:25:34 +11:00
1 parent f76e7f03c6
commit 3087c70daa
17 files changed
+1791 -13

No files matched your search

+49 -11
View File
@@ -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)
}
}