# 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.