1use hashbrown::HashMap;
30
31use crate::phase::Personality;
32use crate::vocabulary::{ContextKey, SensorVocabulary};
33
34#[derive(Clone, Debug)]
44pub struct CoherenceAccumulator {
45 pub value: f32,
47 pub interaction_count: u32,
49 pub last_interaction_tick: u64,
51}
52
53impl CoherenceAccumulator {
54 pub fn new() -> Self {
56 Self {
57 value: 0.0,
58 interaction_count: 0,
59 last_interaction_tick: 0,
60 }
61 }
62
63 pub fn new_with_baseline(curiosity: f32) -> Self {
68 Self {
69 value: (0.15 * curiosity).clamp(0.0, 1.0),
70 interaction_count: 0,
71 last_interaction_tick: 0,
72 }
73 }
74
75 pub fn earned_floor(&self) -> f32 {
88 0.5 * (1.0 - 1.0 / (1.0 + self.interaction_count as f32 / 20.0))
89 }
90
91 pub fn positive_interaction(&mut self, recovery_speed: f32, tick: u64, alone: bool) {
97 let mut delta = 0.02 * (0.5 + recovery_speed) * (1.0 - self.value);
98 if alone {
99 delta *= 2.0; }
101 self.value = (self.value + delta).min(1.0);
102 self.interaction_count = self.interaction_count.saturating_add(1);
103 self.last_interaction_tick = tick;
104 }
105
106 pub fn negative_interaction(&mut self, startle_sensitivity: f32, tick: u64) {
114 let floor = self.earned_floor();
115 let delta = 0.05 * (0.5 + startle_sensitivity);
116 self.value = (self.value - delta).max(floor);
117 self.last_interaction_tick = tick;
118 }
119
120 pub fn decay(&mut self, elapsed_ticks: u64) {
125 let floor = self.earned_floor();
126 if self.value > floor {
127 let decay_rate = 0.0001 * elapsed_ticks as f32;
128 self.value = (self.value - decay_rate).max(floor);
129 }
130 }
131}
132
133impl Default for CoherenceAccumulator {
134 fn default() -> Self {
135 Self::new()
136 }
137}
138
139const MAX_CONTEXTS: usize = 64;
143
144pub struct CoherenceField<V: SensorVocabulary<N>, const N: usize> {
151 accumulators: HashMap<ContextKey<V, N>, CoherenceAccumulator>,
153 personality_baseline: f32,
155 fallback_coherence: Option<f32>,
157}
158
159impl<V: SensorVocabulary<N>, const N: usize> CoherenceField<V, N> {
160 pub fn new() -> Self {
162 Self {
163 accumulators: HashMap::new(),
164 personality_baseline: 0.0,
165 fallback_coherence: None,
166 }
167 }
168
169 pub fn effective_coherence(&self, instant: f32, key: &ContextKey<V, N>) -> f32 {
176 let ctx = self.context_coherence(key);
177 if ctx < 0.3 {
178 if instant < ctx { instant } else { ctx }
179 } else {
180 (0.3 * instant + 0.7 * ctx).clamp(0.0, 1.0)
181 }
182 }
183
184 pub fn positive_interaction(
190 &mut self,
191 key: &ContextKey<V, N>,
192 personality: &Personality,
193 tick: u64,
194 alone: bool,
195 ) {
196 self.get_or_create(key)
197 .positive_interaction(personality.recovery_speed, tick, alone);
198 }
199
200 pub fn negative_interaction(
204 &mut self,
205 key: &ContextKey<V, N>,
206 personality: &Personality,
207 tick: u64,
208 ) {
209 self.get_or_create(key)
210 .negative_interaction(personality.startle_sensitivity, tick);
211 }
212
213 pub fn context_coherence(&self, key: &ContextKey<V, N>) -> f32 {
219 self.accumulators.get(key).map_or_else(
220 || self.fallback_coherence.unwrap_or(0.0),
221 |a| a.value,
222 )
223 }
224
225 pub fn context_interaction_count(&self, key: &ContextKey<V, N>) -> u32 {
227 self.accumulators.get(key).map_or(0, |a| a.interaction_count)
228 }
229
230 pub fn decay_all(&mut self, elapsed_ticks: u64) {
234 for acc in self.accumulators.values_mut() {
235 acc.decay(elapsed_ticks);
236 }
237 }
238
239 pub fn context_count(&self) -> usize {
243 self.accumulators.len()
244 }
245
246 pub fn iter(&self) -> impl Iterator<Item = (&ContextKey<V, N>, &CoherenceAccumulator)> {
248 self.accumulators.iter()
249 }
250
251 #[cfg(feature = "std")]
257 pub fn all_entries(&self) -> std::vec::Vec<(ContextKey<V, N>, f32, u32)> {
258 let mut entries: std::vec::Vec<(ContextKey<V, N>, f32, u32)> = self
259 .accumulators
260 .iter()
261 .map(|(k, acc)| (k.clone(), acc.value, acc.interaction_count))
262 .collect();
263 entries.sort_by(|a, b| b.2.cmp(&a.2));
264 entries
265 }
266
267 pub fn set_fallback(&mut self, value: Option<f32>) {
273 self.fallback_coherence = value;
274 }
275
276 pub fn get_or_create(&mut self, key: &ContextKey<V, N>) -> &mut CoherenceAccumulator {
282 if !self.accumulators.contains_key(key) {
283 if self.accumulators.len() >= MAX_CONTEXTS {
284 self.evict_oldest();
285 }
286 let curiosity = if self.personality_baseline > 0.0 {
287 (self.personality_baseline / 0.15).clamp(0.0, 1.0)
288 } else {
289 0.0
290 };
291 self.accumulators
292 .insert(key.clone(), CoherenceAccumulator::new_with_baseline(curiosity));
293 }
294 self.accumulators.get_mut(key).unwrap()
295 }
296
297 fn evict_oldest(&mut self) {
298 if let Some(oldest_key) = self
299 .accumulators
300 .iter()
301 .min_by_key(|(_, acc)| acc.last_interaction_tick)
302 .map(|(k, _)| k.clone())
303 {
304 self.accumulators.remove(&oldest_key);
305 }
306 }
307}
308
309impl<V: SensorVocabulary<N>, const N: usize> Default for CoherenceField<V, N> {
310 fn default() -> Self {
311 Self::new()
312 }
313}
314
315impl<V: SensorVocabulary<N>, const N: usize> core::fmt::Debug for CoherenceField<V, N> {
316 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
317 f.debug_struct("CoherenceField")
318 .field("context_count", &self.accumulators.len())
319 .field("personality_baseline", &self.personality_baseline)
320 .field("fallback_coherence", &self.fallback_coherence)
321 .finish()
322 }
323}
324
325#[cfg(test)]
328mod tests {
329 use super::*;
330 use crate::mbot::{
331 BrightnessBand, MbotSensors, MotionContext, NoiseBand, Orientation,
332 PresenceSignature, TimePeriod,
333 };
334 use crate::vocabulary::ContextKey;
335
336 fn make_key(
339 brightness: BrightnessBand,
340 noise: NoiseBand,
341 presence: PresenceSignature,
342 ) -> ContextKey<MbotSensors, 6> {
343 ContextKey::new(MbotSensors {
344 brightness,
345 noise,
346 presence,
347 motion: MotionContext::Static,
348 orientation: Orientation::Upright,
349 time_period: TimePeriod::Day,
350 })
351 }
352
353 fn bright_quiet_static() -> ContextKey<MbotSensors, 6> {
354 make_key(BrightnessBand::Bright, NoiseBand::Quiet, PresenceSignature::Absent)
355 }
356
357 fn dark_loud_close() -> ContextKey<MbotSensors, 6> {
358 make_key(BrightnessBand::Dark, NoiseBand::Loud, PresenceSignature::Close)
359 }
360
361 fn neutral_personality() -> Personality {
362 Personality {
363 curiosity_drive: 0.5,
364 startle_sensitivity: 0.5,
365 recovery_speed: 0.5,
366 }
367 }
368
369 #[test]
372 fn test_accumulator_positive_growth() {
373 let mut acc = CoherenceAccumulator::new();
374 assert_eq!(acc.value, 0.0);
375
376 for i in 0..50 {
377 acc.positive_interaction(0.5, i, false);
378 }
379 assert!(acc.value > 0.3, "value={}", acc.value);
380 assert!(acc.value < 1.0);
381 assert_eq!(acc.interaction_count, 50);
382 }
383
384 #[test]
385 fn test_accumulator_asymptotic_growth() {
386 let mut acc = CoherenceAccumulator::new();
387 for i in 0..500 {
388 acc.positive_interaction(0.5, i, false);
389 }
390 let high_value = acc.value;
391 for i in 500..510 {
392 acc.positive_interaction(0.5, i, false);
393 }
394 let delta = acc.value - high_value;
395 assert!(delta < 0.01, "delta should be small at high values: {}", delta);
396 }
397
398 #[test]
399 fn test_accumulator_personality_modulation() {
400 let mut fast = CoherenceAccumulator::new();
401 let mut slow = CoherenceAccumulator::new();
402
403 for i in 0..20 {
404 fast.positive_interaction(0.9, i, false);
405 slow.positive_interaction(0.1, i, false);
406 }
407 assert!(
408 fast.value > slow.value,
409 "fast={} should be > slow={}",
410 fast.value,
411 slow.value
412 );
413 }
414
415 #[test]
416 fn test_accumulator_negative_interaction() {
417 let mut acc = CoherenceAccumulator::new();
418 for i in 0..30 {
419 acc.positive_interaction(0.5, i, false);
420 }
421 let before = acc.value;
422 acc.negative_interaction(0.5, 31);
423 assert!(acc.value < before);
424 }
425
426 #[test]
427 fn test_accumulator_earned_floor() {
428 let mut acc = CoherenceAccumulator::new();
429 for i in 0..100 {
430 acc.positive_interaction(0.5, i, false);
431 }
432 let before = acc.value;
433 for i in 100..200 {
434 acc.negative_interaction(1.0, i);
435 }
436 assert!(
438 acc.value > 0.3,
439 "value={} should be above earned floor",
440 acc.value
441 );
442 assert!(acc.value < before);
443 }
444
445 #[test]
446 fn test_accumulator_decay_toward_floor() {
447 let mut acc = CoherenceAccumulator::new();
448 for i in 0..50 {
449 acc.positive_interaction(0.5, i, false);
450 }
451 let before = acc.value;
452 acc.decay(1000);
453 assert!(acc.value < before);
454 let floor = 0.5 * (1.0 - 1.0 / (1.0 + 50.0 / 20.0));
455 assert!(
456 acc.value >= floor,
457 "value={} should be >= floor={}",
458 acc.value,
459 floor
460 );
461 }
462
463 #[test]
464 fn test_cold_start_baseline() {
465 let acc = CoherenceAccumulator::new_with_baseline(1.0);
466 assert!((acc.value - 0.15).abs() < 0.001, "value={}", acc.value);
467
468 let acc = CoherenceAccumulator::new_with_baseline(0.2);
469 assert!((acc.value - 0.03).abs() < 0.001, "value={}", acc.value);
470
471 let acc = CoherenceAccumulator::new_with_baseline(0.0);
472 assert_eq!(acc.value, 0.0);
473 }
474
475 #[test]
476 fn test_alone_boost() {
477 let mut alone_acc = CoherenceAccumulator::new();
478 let mut social_acc = CoherenceAccumulator::new();
479
480 for i in 0..20 {
481 alone_acc.positive_interaction(0.5, i, true);
482 social_acc.positive_interaction(0.5, i, false);
483 }
484
485 assert!(
486 alone_acc.value > social_acc.value,
487 "alone={} should be > social={}",
488 alone_acc.value,
489 social_acc.value
490 );
491 }
492
493 #[test]
494 fn test_accumulator_value_bounded() {
495 let acc = CoherenceAccumulator::new_with_baseline(2.0); assert!(
498 acc.value >= 0.0 && acc.value <= 1.0,
499 "value={}",
500 acc.value
501 );
502
503 let mut acc = CoherenceAccumulator::new_with_baseline(0.5);
504 for i in 0..1000 {
505 acc.positive_interaction(1.0, i, true);
506 }
507 assert!(acc.value <= 1.0, "value={}", acc.value);
508 }
509
510 #[test]
513 fn test_coherence_field_effective_coherence_unfamiliar() {
514 let mut field: CoherenceField<MbotSensors, 6> = CoherenceField::new();
515 let key = bright_quiet_static();
516
517 let eff = field.effective_coherence(0.8, &key);
519 assert_eq!(eff, 0.0);
520
521 {
523 let acc = field.get_or_create(&key);
524 for i in 0..10 {
525 acc.positive_interaction(0.5, i, false);
526 }
527 }
528 let ctx_coh = field.context_coherence(&key);
529 assert!(ctx_coh > 0.0);
530 assert!(
531 ctx_coh < 0.3,
532 "ctx_coh={} should be < 0.3 for unfamiliar test",
533 ctx_coh
534 );
535
536 let eff = field.effective_coherence(0.8, &key);
538 assert!((eff - ctx_coh).abs() < 0.001);
539
540 let eff = field.effective_coherence(0.05, &key);
542 assert!((eff - 0.05).abs() < 0.001);
543 }
544
545 #[test]
546 fn test_coherence_field_effective_coherence_familiar() {
547 let mut field: CoherenceField<MbotSensors, 6> = CoherenceField::new();
548 let key = bright_quiet_static();
549
550 {
552 let acc = field.get_or_create(&key);
553 for i in 0..80 {
554 acc.positive_interaction(0.5, i, false);
555 }
556 }
557 let ctx_coh = field.context_coherence(&key);
558 assert!(
559 ctx_coh >= 0.3,
560 "ctx_coh={} should be >= 0.3 for familiar test",
561 ctx_coh
562 );
563
564 let eff = field.effective_coherence(0.8, &key);
566 let expected = 0.3 * 0.8 + 0.7 * ctx_coh;
567 assert!(
568 (eff - expected).abs() < 0.001,
569 "eff={} expected={}",
570 eff,
571 expected
572 );
573
574 let eff_low = field.effective_coherence(0.1, &key);
576 let expected_low = 0.3 * 0.1 + 0.7 * ctx_coh;
577 assert!(
578 (eff_low - expected_low).abs() < 0.001,
579 "eff_low={} expected_low={}",
580 eff_low,
581 expected_low
582 );
583 assert!(
584 eff_low > 0.1,
585 "familiar context should buffer: eff_low={}",
586 eff_low
587 );
588 }
589
590 #[test]
591 fn test_coherence_field_independent_contexts() {
592 let mut field: CoherenceField<MbotSensors, 6> = CoherenceField::new();
593 let key_a = bright_quiet_static();
594 let key_b = dark_loud_close();
595
596 {
597 let acc = field.get_or_create(&key_a);
598 for i in 0..50 {
599 acc.positive_interaction(0.5, i, false);
600 }
601 }
602
603 assert!(field.context_coherence(&key_a) > 0.3);
604 assert_eq!(field.context_coherence(&key_b), 0.0);
605 }
606
607 #[test]
608 fn test_coherence_field_interaction_via_personality() {
609 let mut field: CoherenceField<MbotSensors, 6> = CoherenceField::new();
610 let key = bright_quiet_static();
611 let p = neutral_personality();
612
613 for tick in 0..30 {
614 field.positive_interaction(&key, &p, tick, false);
615 }
616 assert!(field.context_coherence(&key) > 0.0);
617
618 let before = field.context_coherence(&key);
619 field.negative_interaction(&key, &p, 30);
620 assert!(field.context_coherence(&key) < before);
621 }
622
623 #[test]
624 fn test_coherence_field_eviction() {
625 let mut field: CoherenceField<MbotSensors, 6> = CoherenceField::new();
626 for i in 0..=MAX_CONTEXTS {
630 let presence = if i % 2 == 0 {
631 PresenceSignature::Absent
632 } else {
633 PresenceSignature::Close
634 };
635 let noise = if i % 3 == 0 {
636 NoiseBand::Quiet
637 } else if i % 3 == 1 {
638 NoiseBand::Moderate
639 } else {
640 NoiseBand::Loud
641 };
642 let brightness = if i % 4 < 2 {
643 BrightnessBand::Bright
644 } else {
645 BrightnessBand::Dark
646 };
647 let key = make_key(brightness, noise, presence);
648 let acc = field.get_or_create(&key);
649 acc.last_interaction_tick = i as u64;
650 }
651 assert!(field.context_count() <= MAX_CONTEXTS);
652 }
653
654 #[test]
655 fn test_coherence_field_decay_all() {
656 let mut field: CoherenceField<MbotSensors, 6> = CoherenceField::new();
657 let key = bright_quiet_static();
658
659 {
660 let acc = field.get_or_create(&key);
661 for i in 0..50 {
662 acc.positive_interaction(0.5, i, false);
663 }
664 }
665 let before = field.context_coherence(&key);
666 field.decay_all(1000);
667 assert!(
668 field.context_coherence(&key) < before,
669 "coherence should decay"
670 );
671 }
672
673 #[test]
674 fn test_coherence_field_fallback() {
675 let mut field: CoherenceField<MbotSensors, 6> = CoherenceField::new();
676 let key = bright_quiet_static();
677
678 assert_eq!(field.context_coherence(&key), 0.0);
680
681 field.set_fallback(Some(0.4));
683 assert!((field.context_coherence(&key) - 0.4).abs() < 0.001);
684
685 {
687 let acc = field.get_or_create(&key);
688 acc.value = 0.6;
689 }
690 assert!((field.context_coherence(&key) - 0.6).abs() < 0.001);
691
692 field.set_fallback(None);
694 assert!((field.context_coherence(&key) - 0.6).abs() < 0.001);
695 }
696
697 #[test]
698 fn test_asymmetric_gate_noise_resilience() {
699 let mut field: CoherenceField<MbotSensors, 6> = CoherenceField::new();
700 let key = bright_quiet_static();
701
702 {
703 let acc = field.get_or_create(&key);
704 for i in 0..100 {
705 acc.positive_interaction(0.5, i, false);
706 }
707 }
708 let ctx_coh = field.context_coherence(&key);
709 assert!(ctx_coh >= 0.3, "should be familiar");
710
711 let eff = field.effective_coherence(0.2, &key);
713 assert!(eff > 0.2, "familiar context should buffer noise: eff={}", eff);
715 }
716
717 #[test]
718 fn test_asymmetric_gate_unfamiliar_strict() {
719 let mut field: CoherenceField<MbotSensors, 6> = CoherenceField::new();
720 let key = bright_quiet_static();
721
722 {
723 let acc = field.get_or_create(&key);
724 for i in 0..5 {
725 acc.positive_interaction(0.5, i, false);
726 }
727 }
728 let ctx_coh = field.context_coherence(&key);
729 assert!(ctx_coh < 0.3);
730
731 let eff = field.effective_coherence(0.9, &key);
733 assert!(
734 (eff - ctx_coh).abs() < 0.001,
735 "unfamiliar gate should cap at ctx: eff={} ctx={}",
736 eff,
737 ctx_coh
738 );
739 }
740
741 #[test]
742 fn test_context_interaction_count() {
743 let mut field: CoherenceField<MbotSensors, 6> = CoherenceField::new();
744 let key = bright_quiet_static();
745 let p = neutral_personality();
746
747 assert_eq!(field.context_interaction_count(&key), 0);
748 for tick in 0..5 {
749 field.positive_interaction(&key, &p, tick, false);
750 }
751 assert_eq!(field.context_interaction_count(&key), 5);
752 }
753
754 #[test]
755 fn test_iter_and_context_count() {
756 let mut field: CoherenceField<MbotSensors, 6> = CoherenceField::new();
757 let key_a = bright_quiet_static();
758 let key_b = dark_loud_close();
759 let p = neutral_personality();
760
761 field.positive_interaction(&key_a, &p, 0, false);
762 field.positive_interaction(&key_b, &p, 1, false);
763
764 assert_eq!(field.context_count(), 2);
765 let count = field.iter().count();
766 assert_eq!(count, 2);
767 }
768}