1use crate::eisenstein::{EisensteinConstraint, SnapResult, COVERING_RADIUS};
40use crate::temporal::{TemporalAgent, AgentAction, FunnelPhase, ChiralityState};
41use std::collections::HashMap;
42
43const MAX_ITERATIONS: usize = 64;
45
46#[derive(Debug, Clone)]
48pub struct DiscoveryTile {
49 pub role: String,
51 pub pattern: String,
53 pub optimal_params: TileParams,
55 pub iterations: usize,
57 pub crystallization_score: f64,
59 pub discovery_entropy: f64,
61 pub dominant_actions: Vec<(AgentAction, f64)>,
63 pub phase_distribution: HashMap<String, f64>,
65 pub generation: u32,
67}
68
69#[derive(Debug, Clone, Copy)]
71pub struct TileParams {
72 pub decay_rate: f64,
73 pub prediction_horizon: usize,
74 pub anomaly_sigma: f64,
75 pub learning_rate: f64,
76 pub chirality_lock_threshold: u16,
77 pub merge_trust: f64,
78}
79
80impl Default for TileParams {
81 fn default() -> Self {
82 TileParams {
83 decay_rate: 1.0,
84 prediction_horizon: 4,
85 anomaly_sigma: 2.0,
86 learning_rate: 0.1,
87 chirality_lock_threshold: 500,
88 merge_trust: 0.5,
89 }
90 }
91}
92
93#[derive(Debug, Clone)]
95pub struct IterationScore {
96 pub iteration: usize,
98 pub params: TileParams,
100 pub final_error: f64,
102 pub convergence_steps: usize,
104 pub anomaly_count: usize,
106 pub chirality_locked: bool,
108 pub precision_energy: f64,
110 pub dominant_action: AgentAction,
112 pub score: f64,
114}
115
116pub struct SeedDiscovery {
118 constraint: EisensteinConstraint,
120 role: String,
122 iterations: Vec<IterationScore>,
124 best_score: f64,
126 best_params: TileParams,
128 generation: u32,
130}
131
132impl SeedDiscovery {
133 pub fn new(role: &str) -> Self {
134 SeedDiscovery {
135 constraint: EisensteinConstraint::new(),
136 role: role.to_string(),
137 iterations: Vec::with_capacity(MAX_ITERATIONS),
138 best_score: f64::NEG_INFINITY,
139 best_params: TileParams::default(),
140 generation: 0,
141 }
142 }
143
144 pub fn run_iteration(
149 &mut self,
150 params: TileParams,
151 trajectory: &[(f64, f64)],
152 ) -> IterationScore {
153 let mut agent = TemporalAgent::new();
154 agent.decay_rate = params.decay_rate;
155 agent.prediction_horizon = params.prediction_horizon;
156 agent.anomaly_sigma = params.anomaly_sigma;
157 agent.learning_rate = params.learning_rate;
158 agent.chirality_lock_threshold = params.chirality_lock_threshold;
159 agent.merge_trust = params.merge_trust;
160
161 let mut anomaly_count = 0;
162 let mut convergence_step = trajectory.len();
163 let mut final_error = COVERING_RADIUS;
164 let mut action_counts: HashMap<AgentAction, usize> = HashMap::new();
165
166 for (step, &(x, y)) in trajectory.iter().enumerate() {
167 let update = agent.observe(x, y);
168
169 if update.is_anomaly {
170 anomaly_count += 1;
171 }
172
173 if update.snap.error < 0.05 * COVERING_RADIUS && convergence_step == trajectory.len() {
174 convergence_step = step;
175 }
176
177 final_error = update.snap.error;
178 *action_counts.entry(update.action).or_insert(0) += 1;
179 }
180
181 let dominant_action = action_counts
182 .iter()
183 .max_by_key(|(_, &c)| c)
184 .map(|(&a, _)| a)
185 .unwrap_or(AgentAction::Continue);
186
187 let chirality_locked = matches!(agent.summary().chirality, ChiralityState::Locked { .. });
188
189 let convergence_bonus = 1.0 - (convergence_step as f64 / trajectory.len() as f64).min(1.0);
191 let error_score = 1.0 - (final_error / COVERING_RADIUS).min(1.0);
192 let anomaly_penalty = (anomaly_count as f64 * 0.1).min(1.0);
193 let chirality_bonus = if chirality_locked { 0.1 } else { 0.0 };
194 let energy_penalty = (agent.summary().precision_energy * 0.001).min(0.5);
195
196 let score = convergence_bonus * 0.3
197 + error_score * 0.3
198 + (1.0 - anomaly_penalty) * 0.2
199 + chirality_bonus * 0.1
200 + (1.0 - energy_penalty) * 0.1;
201
202 let iter_score = IterationScore {
203 iteration: self.iterations.len(),
204 params,
205 final_error,
206 convergence_steps: convergence_step,
207 anomaly_count,
208 chirality_locked,
209 precision_energy: agent.summary().precision_energy,
210 dominant_action,
211 score,
212 };
213
214 if score > self.best_score {
215 self.best_score = score;
216 self.best_params = params;
217 }
218
219 self.iterations.push(iter_score.clone());
220 iter_score
221 }
222
223 pub fn run_sweep(&mut self, trajectory: &[(f64, f64)], n_variations: usize) {
227 for i in 0..n_variations {
228 let params = self.generate_variation(i, n_variations);
229 self.run_iteration(params, trajectory);
230 }
231 }
232
233 fn generate_variation(&self, index: usize, total: usize) -> TileParams {
237 let t = index as f64 / total as f64;
238 let base = self.best_params;
239
240 let phase = t * std::f64::consts::PI * 2.0;
242 let r = 0.5; TileParams {
245 decay_rate: (base.decay_rate + r * (phase * 1.0).sin()).max(0.1).min(10.0),
246 prediction_horizon: (base.prediction_horizon as f64 + 4.0 * (phase * 2.0).sin())
247 .round()
248 .max(1.0)
249 .min(16.0) as usize,
250 anomaly_sigma: (base.anomaly_sigma + r * 2.0 * (phase * 3.0).sin()).max(0.5).min(5.0),
251 learning_rate: (base.learning_rate + 0.3 * (phase * 5.0).sin())
252 .max(0.01)
253 .min(1.0),
254 chirality_lock_threshold: ((base.chirality_lock_threshold as f64
255 + 200.0 * (phase * 7.0).sin())
256 .round()
257 .max(100.0)
258 .min(900.0)) as u16,
259 merge_trust: (base.merge_trust + 0.3 * (phase * 11.0).sin())
260 .max(0.0)
261 .min(1.0),
262 }
263 }
264
265 pub fn crystallize(&self) -> DiscoveryTile {
270 let top_scores: Vec<&IterationScore> = {
271 let mut sorted: Vec<&IterationScore> = self.iterations.iter().collect();
272 sorted.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal));
273 sorted.into_iter().take(10).collect()
274 };
275
276 let mut action_counts: HashMap<AgentAction, f64> = HashMap::new();
278 for iter in &top_scores {
279 *action_counts.entry(iter.dominant_action).or_insert(0.0) += iter.score;
280 }
281 let total_action_weight: f64 = action_counts.values().sum();
282 let mut dominant_actions: Vec<(AgentAction, f64)> = action_counts
283 .into_iter()
284 .map(|(a, w)| (a, w / total_action_weight))
285 .collect();
286 dominant_actions.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
287
288 let scores: Vec<f64> = self.iterations.iter().map(|i| i.score).collect();
290 let mean_score = scores.iter().sum::<f64>() / scores.len() as f64;
291 let variance = scores.iter().map(|s| (s - mean_score).powi(2)).sum::<f64>() / scores.len() as f64;
292 let discovery_entropy = (variance.sqrt() / mean_score).min(1.0);
293
294 let pattern = self.build_pattern(&top_scores);
296
297 let mut phase_dist: HashMap<String, f64> = HashMap::new();
299 phase_dist.insert("convergent".to_string(), self.best_score);
302 phase_dist.insert("exploratory".to_string(), 1.0 - self.best_score);
303
304 DiscoveryTile {
305 role: self.role.clone(),
306 pattern,
307 optimal_params: self.best_params,
308 iterations: self.iterations.len(),
309 crystallization_score: self.best_score,
310 discovery_entropy,
311 dominant_actions,
312 phase_distribution: phase_dist,
313 generation: self.generation,
314 }
315 }
316
317 fn build_pattern(&self, top: &[&IterationScore]) -> String {
319 let avg_convergence: f64 =
320 top.iter().map(|i| i.convergence_steps as f64).sum::<f64>() / top.len() as f64;
321 let avg_anomaly: f64 =
322 top.iter().map(|i| i.anomaly_count as f64).sum::<f64>() / top.len() as f64;
323 let locked_ratio: f64 =
324 top.iter().filter(|i| i.chirality_locked).count() as f64 / top.len() as f64;
325
326 format!(
327 "Role: {}\n\
328 Optimal decay_rate: {:.3} (funnel speed)\n\
329 Optimal horizon: {} (prediction depth)\n\
330 Optimal anomaly_sigma: {:.2} (surprise sensitivity)\n\
331 Optimal learning_rate: {:.3} (memory plasticity)\n\
332 Optimal chirality_lock: {} (commitment threshold)\n\
333 Convergence: ~{:.0} steps average\n\
334 Anomaly rate: ~{:.1} per trajectory\n\
335 Chirality lock: {:.0}% of top runs\n\
336 Score: {:.3}\n\
337 Discovery entropy: {:.3}\n\
338 Generation: {}",
339 self.role,
340 self.best_params.decay_rate,
341 self.best_params.prediction_horizon,
342 self.best_params.anomaly_sigma,
343 self.best_params.learning_rate,
344 self.best_params.chirality_lock_threshold,
345 avg_convergence,
346 avg_anomaly,
347 locked_ratio * 100.0,
348 self.best_score,
349 self.discovery_entropy(),
350 self.generation,
351 )
352 }
353
354 fn discovery_entropy(&self) -> f64 {
355 if self.iterations.is_empty() {
356 return 0.0;
357 }
358 let scores: Vec<f64> = self.iterations.iter().map(|i| i.score).collect();
359 let mean = scores.iter().sum::<f64>() / scores.len() as f64;
360 let var = scores.iter().map(|s| (s - mean).powi(2)).sum::<f64>() / scores.len() as f64;
361 (var.sqrt() / mean).min(1.0)
362 }
363
364 pub fn refine(&mut self, trajectory: &[(f64, f64)], n_variations: usize) {
367 self.generation += 1;
368 self.run_sweep(trajectory, n_variations);
369 }
370}
371
372#[derive(Debug, Default)]
374pub struct TileRegistry {
375 tiles: HashMap<String, DiscoveryTile>,
376}
377
378impl TileRegistry {
379 pub fn new() -> Self {
380 TileRegistry {
381 tiles: HashMap::new(),
382 }
383 }
384
385 pub fn register(&mut self, tile: DiscoveryTile) {
387 self.tiles.insert(tile.role.clone(), tile);
388 }
389
390 pub fn get(&self, role: &str) -> Option<&DiscoveryTile> {
392 self.tiles.get(role)
393 }
394
395 pub fn get_params(&self, role: &str) -> TileParams {
397 self.tiles
398 .get(role)
399 .map(|t| t.optimal_params)
400 .unwrap_or_default()
401 }
402
403 pub fn conditioning_prompt(&self, role: &str) -> String {
406 match self.tiles.get(role) {
407 Some(tile) => format!(
408 "# Discovered Inner Logic for: {}\n\
409 # (crystallized from {} seed iterations, generation {})\n\
410 # Score: {:.3}, Entropy: {:.3}\n\
411 \n\
412 {}\n\
413 \n\
414 # Optimal Parameters:\n\
415 decay_rate = {:.3} # funnel speed (square-root base)\n\
416 prediction_horizon = {} # steps ahead\n\
417 anomaly_sigma = {:.2} # surprise threshold (sigma)\n\
418 learning_rate = {:.3} # memory plasticity\n\
419 chirality_lock = {} # commitment threshold (milli)\n\
420 merge_trust = {:.2} # fleet vs local\n\
421 \n\
422 # Dominant actions: {}\n\
423 \n\
424 Use these parameters when performing this role. \
425 The seed experimentation has proven these are optimal \
426 for the constraint geometry of this domain.",
427 tile.role,
428 tile.iterations,
429 tile.generation,
430 tile.crystallization_score,
431 tile.discovery_entropy,
432 tile.pattern,
433 tile.optimal_params.decay_rate,
434 tile.optimal_params.prediction_horizon,
435 tile.optimal_params.anomaly_sigma,
436 tile.optimal_params.learning_rate,
437 tile.optimal_params.chirality_lock_threshold,
438 tile.optimal_params.merge_trust,
439 tile.dominant_actions
440 .iter()
441 .take(3)
442 .map(|(a, w)| format!("{:?} ({:.0}%)", a, w * 100.0))
443 .collect::<Vec<_>>()
444 .join(", "),
445 ),
446 None => "# No seed tile found for this role. Use defaults.".to_string(),
447 }
448 }
449
450 pub fn list(&self) -> Vec<&DiscoveryTile> {
452 self.tiles.values().collect()
453 }
454}
455
456pub fn converging_spiral(steps: usize, radius: f64, turns: f64) -> Vec<(f64, f64)> {
458 (0..steps)
459 .map(|i| {
460 let t = i as f64 / steps as f64;
461 let r = radius * (1.0 - t);
462 let angle = turns * 2.0 * std::f64::consts::PI * t;
463 (r * angle.cos(), r * angle.sin())
464 })
465 .collect()
466}
467
468pub fn noisy_sensor(steps: usize, center: (f64, f64), noise: f64) -> Vec<(f64, f64)> {
470 (0..steps)
471 .map(|i| {
472 let t = i as f64 / steps as f64;
473 let angle = t * 7.0 * std::f64::consts::PI;
474 let r = noise * (angle.sin() * 0.7 + angle.cos() * 0.3);
475 (center.0 + r * angle.cos(), center.1 + r * angle.sin())
476 })
477 .collect()
478}
479
480pub fn step_trajectory(steps: usize, jump_at: usize) -> Vec<(f64, f64)> {
482 (0..steps)
483 .map(|i| {
484 if i < jump_at {
485 (0.1, 0.1)
486 } else {
487 (2.0, 2.0)
488 }
489 })
490 .collect()
491}
492
493#[cfg(test)]
494mod tests {
495 use super::*;
496
497 #[test]
498 fn test_seed_discovery_converging() {
499 let trajectory = converging_spiral(50, COVERING_RADIUS * 2.0, 2.0);
500 let mut discovery = SeedDiscovery::new("converging-tracker");
501 discovery.run_sweep(&trajectory, 20);
502
503 let tile = discovery.crystallize();
504 assert!(tile.crystallization_score > 0.0);
505 assert_eq!(tile.role, "converging-tracker");
506 assert_eq!(tile.iterations, 20);
507 }
508
509 #[test]
510 fn test_seed_discovery_noisy() {
511 let trajectory = noisy_sensor(50, (0.0, 0.0), 0.1);
512 let mut discovery = SeedDiscovery::new("noisy-sensor");
513 discovery.run_sweep(&trajectory, 20);
514
515 let tile = discovery.crystallize();
516 assert!(tile.crystallization_score > 0.0);
517 }
520
521 #[test]
522 fn test_seed_discovery_step() {
523 let trajectory = step_trajectory(50, 25);
524 let mut discovery = SeedDiscovery::new("step-detector");
525 discovery.run_sweep(&trajectory, 20);
526
527 let tile = discovery.crystallize();
528 assert!(tile.crystallization_score > 0.0);
529 }
531
532 #[test]
533 fn test_tile_registry() {
534 let trajectory = converging_spiral(50, COVERING_RADIUS * 2.0, 2.0);
535 let mut discovery = SeedDiscovery::new("test-role");
536 discovery.run_sweep(&trajectory, 10);
537 let tile = discovery.crystallize();
538
539 let mut registry = TileRegistry::new();
540 registry.register(tile);
541
542 assert!(registry.get("test-role").is_some());
543 assert!(registry.get("nonexistent").is_none());
544
545 let prompt = registry.conditioning_prompt("test-role");
546 assert!(prompt.contains("test-role"));
547 assert!(prompt.contains("decay_rate"));
548 }
549
550 #[test]
551 fn test_refinement_improves() {
552 let trajectory = converging_spiral(50, COVERING_RADIUS * 2.0, 2.0);
553 let mut discovery = SeedDiscovery::new("refinement-test");
554
555 discovery.run_sweep(&trajectory, 10);
557 let _score_gen0 = discovery.crystallize().crystallization_score;
558
559 discovery.refine(&trajectory, 10);
561 let score_gen1 = discovery.crystallize().crystallization_score;
562
563 assert!(score_gen1 > 0.0);
565 assert_eq!(discovery.crystallize().generation, 1);
566 }
567
568 #[test]
569 fn test_trajectory_generators() {
570 let spiral = converging_spiral(20, 1.0, 1.0);
571 assert_eq!(spiral.len(), 20);
572 assert!(spiral[0].0.abs() > spiral[19].0.abs()); let noisy = noisy_sensor(20, (1.0, 1.0), 0.5);
575 assert_eq!(noisy.len(), 20);
576
577 let step = step_trajectory(20, 10);
578 assert_eq!(step.len(), 20);
579 assert!((step[5].0 - 0.1).abs() < 0.01);
580 assert!((step[15].0 - 2.0).abs() < 0.01);
581 }
582
583 #[test]
584 fn test_conditioning_prompt_structure() {
585 let trajectory = converging_spiral(30, COVERING_RADIUS, 1.5);
586 let mut discovery = SeedDiscovery::new("structured-role");
587 discovery.run_sweep(&trajectory, 15);
588 let tile = discovery.crystallize();
589
590 let mut registry = TileRegistry::new();
591 registry.register(tile);
592
593 let prompt = registry.conditioning_prompt("structured-role");
594 assert!(prompt.contains("Discovered Inner Logic"));
595 assert!(prompt.contains("seed iterations"));
596 assert!(prompt.contains("decay_rate"));
597 assert!(prompt.contains("prediction_horizon"));
598 assert!(prompt.contains("anomaly_sigma"));
599 assert!(prompt.contains("learning_rate"));
600 assert!(prompt.contains("chirality_lock"));
601 assert!(prompt.contains("merge_trust"));
602 }
603}