Skip to main content

plato_genepool_tile/
lib.rs

1//! plato-genepool-tile — lossless bridge between cuda-genepool Gene and Plato Tile.
2//!
3//! GeneTile is a *union* struct: every Gene field stored verbatim alongside its
4//! tile-spec mirror, so gene_to_tile → tile_to_gene is a perfect round-trip.
5
6use std::time::{SystemTime, UNIX_EPOCH};
7fn now_ns() -> u64 { SystemTime::now().duration_since(UNIX_EPOCH).map_or(0, |d| d.as_nanos() as u64) }
8fn gen_id(p: &str) -> String { format!("{}-{:x}", p, now_ns() & 0xFFFF_FFFF) }
9
10// ─── Enums ───────────────────────────────────────────────────────────────────
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub enum Instinct { Perceive, Navigate, Survive, Communicate, Learn, Share, Rest, Explore, Defend, Cooperate }
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub enum GeneCategory {
17    Algorithm, DataStructure, Optimization, ErrorHandling, Security, UI,
18    Integration, Testing, Perception, Navigation, Survival, Communication, Learning,
19}
20
21fn instinct_str(i: Instinct) -> &'static str {
22    match i { Instinct::Perceive=>"perceive",Instinct::Navigate=>"navigate",Instinct::Survive=>"survive",
23        Instinct::Communicate=>"communicate",Instinct::Learn=>"learn",Instinct::Share=>"share",
24        Instinct::Rest=>"rest",Instinct::Explore=>"explore",Instinct::Defend=>"defend",
25        Instinct::Cooperate=>"cooperate" }
26}
27fn category_str(c: GeneCategory) -> &'static str {
28    match c { GeneCategory::Algorithm=>"algorithm",GeneCategory::DataStructure=>"data_structure",
29        GeneCategory::Optimization=>"optimization",GeneCategory::ErrorHandling=>"error_handling",
30        GeneCategory::Security=>"security",GeneCategory::UI=>"ui",GeneCategory::Integration=>"integration",
31        GeneCategory::Testing=>"testing",GeneCategory::Perception=>"perception",
32        GeneCategory::Navigation=>"navigation",GeneCategory::Survival=>"survival",
33        GeneCategory::Communication=>"communication",GeneCategory::Learning=>"learning" }
34}
35fn category_instinct(c: GeneCategory) -> Option<Instinct> {
36    match c { GeneCategory::Perception=>Some(Instinct::Perceive),GeneCategory::Navigation=>Some(Instinct::Navigate),
37        GeneCategory::Survival=>Some(Instinct::Survive),GeneCategory::Communication=>Some(Instinct::Communicate),
38        GeneCategory::Learning=>Some(Instinct::Learn),GeneCategory::Integration=>Some(Instinct::Cooperate),
39        GeneCategory::Security=>Some(Instinct::Defend),_=>None }
40}
41fn instinct_energy(i: Instinct) -> f64 {
42    match i { Instinct::Perceive=>0.02,Instinct::Navigate=>0.05,Instinct::Survive=>0.01,
43        Instinct::Communicate=>0.03,Instinct::Learn=>0.04,Instinct::Share=>0.02,
44        Instinct::Rest=>0.001,Instinct::Explore=>0.06,Instinct::Defend=>0.04,Instinct::Cooperate=>0.02 }
45}
46
47// ─── Gene ────────────────────────────────────────────────────────────────────
48
49#[derive(Debug, Clone)]
50pub struct Gene {
51    pub id: String, pub pattern: String, pub category: GeneCategory,
52    pub fitness: f64, pub energy_cost: f64, pub expression_level: f64, pub dominance: f64,
53    pub usage_count: u64, pub success_count: u64, pub failure_count: u64,
54    pub generation: u32, pub origin_agent: String, pub parents: Vec<String>,
55    pub quarantined: bool, pub quarantine_reason: Option<String>, pub instinct: Option<Instinct>,
56}
57impl Gene {
58    pub fn new(pattern: &str, category: GeneCategory, fitness: f64) -> Self {
59        let instinct = category_instinct(category);
60        Self { id: gen_id("gene"), pattern: pattern.to_string(), category,
61            fitness: fitness.clamp(0.0, 1.0), energy_cost: instinct.map_or(0.03, instinct_energy),
62            expression_level: fitness.clamp(0.1, 1.0), dominance: 0.5,
63            usage_count: 0, success_count: 0, failure_count: 0, generation: 0,
64            origin_agent: String::new(), parents: vec![],
65            quarantined: false, quarantine_reason: None, instinct }
66    }
67}
68
69// ─── GeneTile ─────────────────────────────────────────────────────────────────
70
71/// Union of Gene + Tile fields. All gene fields stored verbatim → lossless round-trip.
72#[derive(Debug, Clone)]
73pub struct GeneTile {
74    // Gene fields (verbatim)
75    pub gene_id: String,    pub pattern: String,   pub category: GeneCategory,
76    pub fitness: f64,       pub energy_cost: f64,  pub expression_level: f64,
77    pub dominance: f64,     pub usage_count: u64,  pub success_count: u64,
78    pub failure_count: u64, pub generation: u32,   pub origin_agent: String,
79    pub parents: Vec<String>, pub quarantined: bool, pub quarantine_reason: Option<String>,
80    pub instinct: Option<Instinct>,
81    // Tile fields (derived from gene semantics)
82    pub tile_id: String,          pub confidence: f64,         pub question: String,
83    pub answer: String,           pub tags: Vec<String>,       pub anchors: Vec<String>,
84    pub weight: f64,              pub use_count: u64,          pub active: bool,
85    pub last_used_tick: u64,      pub constraint_tolerance: f64, pub constraint_threshold: f64,
86    // Bridge
87    pub conversion_score: f64,    pub activation_count: u64,
88}
89
90/// Standalone Tile with gene metadata stripped — result of GeneTilePool::harvest.
91#[derive(Debug, Clone)]
92pub struct Tile {
93    pub id: String, pub confidence: f64, pub question: String, pub answer: String,
94    pub tags: Vec<String>, pub anchors: Vec<String>, pub weight: f64,
95    pub use_count: u64, pub active: bool, pub last_used_tick: u64,
96    pub constraint_tolerance: f64, pub constraint_threshold: f64,
97}
98
99// ─── Conversion ───────────────────────────────────────────────────────────────
100
101/// Gene → GeneTile. All gene fields preserved; tile fields derived from gene semantics.
102pub fn gene_to_tile(g: &Gene) -> GeneTile {
103    let mut tags = vec![category_str(g.category).to_string()];
104    if let Some(i) = g.instinct { tags.push(instinct_str(i).to_string()); }
105    let cs = (g.fitness * g.expression_level
106        + if g.instinct.is_some() { 0.1 } else { 0.0 }
107        - if g.quarantined { 0.3 } else { 0.0 }).clamp(0.0, 1.0);
108    GeneTile {
109        gene_id: g.id.clone(), pattern: g.pattern.clone(), category: g.category,
110        fitness: g.fitness, energy_cost: g.energy_cost, expression_level: g.expression_level,
111        dominance: g.dominance, usage_count: g.usage_count, success_count: g.success_count,
112        failure_count: g.failure_count, generation: g.generation,
113        origin_agent: g.origin_agent.clone(), parents: g.parents.clone(),
114        quarantined: g.quarantined, quarantine_reason: g.quarantine_reason.clone(),
115        instinct: g.instinct, tile_id: gen_id("tile"), confidence: g.fitness,
116        question: g.pattern.clone(), answer: format!("[{}] {}", category_str(g.category), g.pattern),
117        tags, anchors: g.parents.clone(), weight: g.expression_level,
118        use_count: g.usage_count, active: !g.quarantined, last_used_tick: 0,
119        constraint_tolerance: 0.05, constraint_threshold: g.fitness.max(0.1),
120        conversion_score: cs, activation_count: 0,
121    }
122}
123
124/// GeneTile → Gene. Exact inverse — lossless by construction.
125pub fn tile_to_gene(gt: &GeneTile) -> Gene {
126    Gene { id: gt.gene_id.clone(), pattern: gt.pattern.clone(), category: gt.category,
127        fitness: gt.fitness, energy_cost: gt.energy_cost, expression_level: gt.expression_level,
128        dominance: gt.dominance, usage_count: gt.usage_count, success_count: gt.success_count,
129        failure_count: gt.failure_count, generation: gt.generation,
130        origin_agent: gt.origin_agent.clone(), parents: gt.parents.clone(),
131        quarantined: gt.quarantined, quarantine_reason: gt.quarantine_reason.clone(),
132        instinct: gt.instinct }
133}
134
135pub fn batch_convert(genes: &[Gene]) -> Vec<GeneTile> { genes.iter().map(gene_to_tile).collect() }
136
137// ─── GeneTilePool ─────────────────────────────────────────────────────────────
138
139pub struct GeneTilePool { tiles: Vec<GeneTile>, generation: u32 }
140
141impl Default for GeneTilePool { fn default() -> Self { Self::new() } }
142
143impl GeneTilePool {
144    pub fn new() -> Self { Self { tiles: vec![], generation: 0 } }
145    pub fn insert(&mut self, gt: GeneTile) { self.tiles.push(gt); }
146
147    pub fn best_for_query(&self, query: &str) -> Option<&GeneTile> {
148        let q = query.to_lowercase();
149        self.tiles.iter()
150            .filter(|gt| gt.active && !gt.quarantined
151                && (gt.tags.iter().any(|t| q.contains(t.as_str()) || t.contains(q.as_str()))
152                    || gt.pattern.to_lowercase().contains(&q)))
153            .max_by(|a, b| (a.fitness * a.expression_level)
154                .partial_cmp(&(b.fitness * b.expression_level))
155                .unwrap_or(std::cmp::Ordering::Equal))
156    }
157
158    /// One generation: crossover top-2, drift expression toward fitness, prune bottom quartile.
159    pub fn evolve(&mut self) {
160        self.generation += 1;
161        let top: Vec<GeneTile> = {
162            let mut v: Vec<&GeneTile> = self.tiles.iter()
163                .filter(|gt| gt.active && !gt.quarantined).collect();
164            v.sort_by(|a, b| b.fitness.partial_cmp(&a.fitness).unwrap_or(std::cmp::Ordering::Equal));
165            v.iter().take(2).map(|gt| (*gt).clone()).collect()
166        };
167        if top.len() >= 2 { self.tiles.push(crossover(&top[0], &top[1], self.generation)); }
168        let gen = self.generation;
169        for gt in &mut self.tiles {
170            if !gt.quarantined {
171                gt.expression_level = (gt.expression_level + (gt.fitness - gt.expression_level) * 0.05)
172                    .clamp(0.05, 1.0);
173                gt.weight = gt.expression_level; gt.generation = gen;
174            }
175        }
176        let n = self.tiles.len();
177        if n > 4 {
178            let mut ord: Vec<usize> = (0..n).collect();
179            ord.sort_by(|&a, &b| self.tiles[a].fitness.partial_cmp(&self.tiles[b].fitness)
180                .unwrap_or(std::cmp::Ordering::Equal));
181            for &i in ord.iter().take(n / 4) { self.tiles[i].active = false; }
182        }
183    }
184
185    pub fn quarantine_below(&mut self, threshold: f64) -> usize {
186        let mut n = 0;
187        for gt in &mut self.tiles {
188            if gt.fitness < threshold && !gt.quarantined {
189                gt.quarantined = true; gt.active = false; n += 1;
190                gt.quarantine_reason = Some(format!("fitness {:.3} < {:.3}", gt.fitness, threshold));
191            }
192        }
193        n
194    }
195
196    pub fn harvest(&self, n: usize) -> Vec<Tile> {
197        let mut v: Vec<&GeneTile> = self.tiles.iter().filter(|gt| gt.active && !gt.quarantined).collect();
198        v.sort_by(|a, b| b.fitness.partial_cmp(&a.fitness).unwrap_or(std::cmp::Ordering::Equal));
199        v.iter().take(n).map(|gt| Tile {
200            id: gt.tile_id.clone(), confidence: gt.confidence, question: gt.question.clone(),
201            answer: gt.answer.clone(), tags: gt.tags.clone(), anchors: gt.anchors.clone(),
202            weight: gt.weight, use_count: gt.use_count, active: gt.active,
203            last_used_tick: gt.last_used_tick, constraint_tolerance: gt.constraint_tolerance,
204            constraint_threshold: gt.constraint_threshold,
205        }).collect()
206    }
207
208    pub fn len(&self) -> usize { self.tiles.len() }
209    pub fn is_empty(&self) -> bool { self.tiles.is_empty() }
210    pub fn active_count(&self) -> usize { self.tiles.iter().filter(|gt| gt.active).count() }
211    pub fn generation(&self) -> u32 { self.generation }
212}
213
214fn crossover(a: &GeneTile, b: &GeneTile, gen: u32) -> GeneTile {
215    let mut tags = a.tags.clone();
216    for t in &b.tags { if !tags.contains(t) { tags.push(t.clone()); } }
217    let f = (a.fitness + b.fitness) / 2.0;
218    GeneTile { gene_id: gen_id("gene"), tile_id: gen_id("tile"),
219        pattern: a.pattern.clone(), category: a.category, fitness: f,
220        energy_cost: (a.energy_cost + b.energy_cost) / 2.0,
221        expression_level: (a.expression_level + b.expression_level) / 2.0,
222        dominance: (a.dominance + b.dominance) / 2.0,
223        usage_count: 0, success_count: 0, failure_count: 0, generation: gen,
224        origin_agent: a.origin_agent.clone(), parents: vec![a.gene_id.clone(), b.gene_id.clone()],
225        quarantined: false, quarantine_reason: None, instinct: a.instinct,
226        confidence: f, question: a.question.clone(), answer: a.answer.clone(), tags,
227        anchors: a.anchors.clone(), weight: (a.weight + b.weight) / 2.0,
228        use_count: 0, active: true, last_used_tick: 0,
229        constraint_tolerance: (a.constraint_tolerance + b.constraint_tolerance) / 2.0,
230        constraint_threshold: (a.constraint_threshold + b.constraint_threshold) / 2.0,
231        conversion_score: (a.conversion_score + b.conversion_score) / 2.0, activation_count: 0 }
232}
233
234// ─── Tests ────────────────────────────────────────────────────────────────────
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    fn nav() -> Gene {
241        let mut g = Gene::new("navigate_path", GeneCategory::Navigation, 0.8);
242        g.origin_agent = "scout".into();
243        g.usage_count = 10; g.success_count = 8; g.failure_count = 2; g
244    }
245    fn pool4() -> GeneTilePool {
246        let mut p = GeneTilePool::new();
247        p.insert(gene_to_tile(&Gene::new("survive",      GeneCategory::Survival,   0.9)));
248        p.insert(gene_to_tile(&Gene::new("navigate",     GeneCategory::Navigation, 0.8)));
249        p.insert(gene_to_tile(&Gene::new("learn_pattern",GeneCategory::Learning,   0.6)));
250        p.insert(gene_to_tile(&Gene::new("bad_pattern",  GeneCategory::Algorithm,  0.2)));
251        p
252    }
253
254    #[test] fn test_conversion_fitness_to_confidence() {
255        let g = nav(); assert!((gene_to_tile(&g).confidence - g.fitness).abs() < f64::EPSILON);
256    }
257    #[test] fn test_conversion_pattern_to_question() {
258        assert_eq!(gene_to_tile(&nav()).question, nav().pattern);
259    }
260    #[test] fn test_conversion_quarantine_to_active() {
261        let mut g = nav(); g.quarantined = true;
262        assert!(!gene_to_tile(&g).active); assert!(gene_to_tile(&nav()).active);
263    }
264    #[test] fn test_conversion_instinct_in_tags() {
265        let gt = gene_to_tile(&Gene::new("nav", GeneCategory::Navigation, 0.8));
266        assert!(gt.tags.contains(&"navigate".to_string()) && gt.tags.contains(&"navigation".to_string()));
267    }
268    #[test] fn test_roundtrip_fitness() {
269        let g = nav(); assert!((g.fitness - tile_to_gene(&gene_to_tile(&g)).fitness).abs() < f64::EPSILON);
270    }
271    #[test] fn test_roundtrip_energy_cost() {
272        let g = nav(); assert!((g.energy_cost - tile_to_gene(&gene_to_tile(&g)).energy_cost).abs() < f64::EPSILON);
273    }
274    #[test] fn test_roundtrip_instinct() {
275        let g = nav(); assert_eq!(g.instinct, tile_to_gene(&gene_to_tile(&g)).instinct);
276    }
277    #[test] fn test_roundtrip_quarantine() {
278        let mut g = nav(); g.quarantined = true; g.quarantine_reason = Some("bad actor".into());
279        let g2 = tile_to_gene(&gene_to_tile(&g));
280        assert!(g2.quarantined); assert_eq!(g2.quarantine_reason, Some("bad actor".into()));
281    }
282    #[test] fn test_roundtrip_counts() {
283        let g2 = tile_to_gene(&gene_to_tile(&nav()));
284        assert_eq!((g2.usage_count, g2.success_count, g2.failure_count), (10, 8, 2));
285    }
286    #[test] fn test_batch_convert() {
287        let gs = vec![Gene::new("a",GeneCategory::Algorithm,0.7),
288                      Gene::new("b",GeneCategory::Security,0.8),
289                      Gene::new("c",GeneCategory::Navigation,0.9)];
290        let ts = batch_convert(&gs);
291        assert_eq!(ts.len(), 3); assert!((ts[2].fitness - 0.9).abs() < f64::EPSILON);
292    }
293    #[test] fn test_pool_insert() {
294        let mut p = GeneTilePool::new(); p.insert(gene_to_tile(&nav()));
295        assert_eq!(p.len(), 1); assert_eq!(p.active_count(), 1);
296    }
297    #[test] fn test_pool_best_for_query() {
298        let p = pool4();
299        let gt = p.best_for_query("navigate").unwrap();
300        assert!(gt.tags.contains(&"navigate".to_string()) || gt.pattern.contains("navigate"));
301        // highest fitness wins among matches
302        let mut p2 = GeneTilePool::new();
303        p2.insert(gene_to_tile(&Gene::new("survive_lo", GeneCategory::Survival, 0.5)));
304        p2.insert(gene_to_tile(&Gene::new("survive_hi", GeneCategory::Survival, 0.95)));
305        assert!(p2.best_for_query("survive").unwrap().fitness >= 0.95 - f64::EPSILON);
306    }
307    #[test] fn test_pool_quarantine_below() {
308        let mut p = pool4(); let count = p.quarantine_below(0.5);
309        assert!(count > 0);
310        for gt in p.tiles.iter() {
311            if gt.fitness < 0.5 { assert!(gt.quarantined && !gt.active); } else { assert!(!gt.quarantined); }
312        }
313    }
314    #[test] fn test_pool_evolve() {
315        let mut p = pool4(); let before = p.len(); p.evolve();
316        assert_eq!(p.generation(), 1); assert!(p.len() > before);
317        let child = &p.tiles[before];
318        assert_eq!(child.generation, 1);
319        assert!((child.fitness - (0.9 + 0.8) / 2.0).abs() < 1e-10);
320    }
321    #[test] fn test_pool_harvest() {
322        let p = pool4(); let tiles = p.harvest(2);
323        assert_eq!(tiles.len(), 2);
324        assert!(!tiles[0].id.is_empty() && tiles[0].confidence >= tiles[1].confidence);
325    }
326    #[test] fn test_conversion_score_range() {
327        let cases = [Gene::new("a", GeneCategory::Survival, 1.0),
328                     Gene::new("b", GeneCategory::Algorithm, 0.0),
329                     { let mut g = Gene::new("c", GeneCategory::Navigation, 0.5); g.quarantined = true; g }];
330        for gene in &cases {
331            let s = gene_to_tile(gene).conversion_score;
332            assert!((0.0..=1.0).contains(&s), "score {s} out of range");
333        }
334    }
335}