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::vocabulary::{
331 BrightnessBand, ContextKey, MotionContext, MbotSensors, NoiseBand, Orientation,
332 PresenceSignature, TimePeriod,
333 };
334
335 fn make_key(
338 brightness: BrightnessBand,
339 noise: NoiseBand,
340 presence: PresenceSignature,
341 ) -> ContextKey<MbotSensors, 6> {
342 ContextKey::new(MbotSensors {
343 brightness,
344 noise,
345 presence,
346 motion: MotionContext::Static,
347 orientation: Orientation::Upright,
348 time_period: TimePeriod::Day,
349 })
350 }
351
352 fn bright_quiet_static() -> ContextKey<MbotSensors, 6> {
353 make_key(BrightnessBand::Bright, NoiseBand::Quiet, PresenceSignature::Absent)
354 }
355
356 fn dark_loud_close() -> ContextKey<MbotSensors, 6> {
357 make_key(BrightnessBand::Dark, NoiseBand::Loud, PresenceSignature::Close)
358 }
359
360 fn neutral_personality() -> Personality {
361 Personality {
362 curiosity_drive: 0.5,
363 startle_sensitivity: 0.5,
364 recovery_speed: 0.5,
365 }
366 }
367
368 #[test]
371 fn test_accumulator_positive_growth() {
372 let mut acc = CoherenceAccumulator::new();
373 assert_eq!(acc.value, 0.0);
374
375 for i in 0..50 {
376 acc.positive_interaction(0.5, i, false);
377 }
378 assert!(acc.value > 0.3, "value={}", acc.value);
379 assert!(acc.value < 1.0);
380 assert_eq!(acc.interaction_count, 50);
381 }
382
383 #[test]
384 fn test_accumulator_asymptotic_growth() {
385 let mut acc = CoherenceAccumulator::new();
386 for i in 0..500 {
387 acc.positive_interaction(0.5, i, false);
388 }
389 let high_value = acc.value;
390 for i in 500..510 {
391 acc.positive_interaction(0.5, i, false);
392 }
393 let delta = acc.value - high_value;
394 assert!(delta < 0.01, "delta should be small at high values: {}", delta);
395 }
396
397 #[test]
398 fn test_accumulator_personality_modulation() {
399 let mut fast = CoherenceAccumulator::new();
400 let mut slow = CoherenceAccumulator::new();
401
402 for i in 0..20 {
403 fast.positive_interaction(0.9, i, false);
404 slow.positive_interaction(0.1, i, false);
405 }
406 assert!(
407 fast.value > slow.value,
408 "fast={} should be > slow={}",
409 fast.value,
410 slow.value
411 );
412 }
413
414 #[test]
415 fn test_accumulator_negative_interaction() {
416 let mut acc = CoherenceAccumulator::new();
417 for i in 0..30 {
418 acc.positive_interaction(0.5, i, false);
419 }
420 let before = acc.value;
421 acc.negative_interaction(0.5, 31);
422 assert!(acc.value < before);
423 }
424
425 #[test]
426 fn test_accumulator_earned_floor() {
427 let mut acc = CoherenceAccumulator::new();
428 for i in 0..100 {
429 acc.positive_interaction(0.5, i, false);
430 }
431 let before = acc.value;
432 for i in 100..200 {
433 acc.negative_interaction(1.0, i);
434 }
435 assert!(
437 acc.value > 0.3,
438 "value={} should be above earned floor",
439 acc.value
440 );
441 assert!(acc.value < before);
442 }
443
444 #[test]
445 fn test_accumulator_decay_toward_floor() {
446 let mut acc = CoherenceAccumulator::new();
447 for i in 0..50 {
448 acc.positive_interaction(0.5, i, false);
449 }
450 let before = acc.value;
451 acc.decay(1000);
452 assert!(acc.value < before);
453 let floor = 0.5 * (1.0 - 1.0 / (1.0 + 50.0 / 20.0));
454 assert!(
455 acc.value >= floor,
456 "value={} should be >= floor={}",
457 acc.value,
458 floor
459 );
460 }
461
462 #[test]
463 fn test_cold_start_baseline() {
464 let acc = CoherenceAccumulator::new_with_baseline(1.0);
465 assert!((acc.value - 0.15).abs() < 0.001, "value={}", acc.value);
466
467 let acc = CoherenceAccumulator::new_with_baseline(0.2);
468 assert!((acc.value - 0.03).abs() < 0.001, "value={}", acc.value);
469
470 let acc = CoherenceAccumulator::new_with_baseline(0.0);
471 assert_eq!(acc.value, 0.0);
472 }
473
474 #[test]
475 fn test_alone_boost() {
476 let mut alone_acc = CoherenceAccumulator::new();
477 let mut social_acc = CoherenceAccumulator::new();
478
479 for i in 0..20 {
480 alone_acc.positive_interaction(0.5, i, true);
481 social_acc.positive_interaction(0.5, i, false);
482 }
483
484 assert!(
485 alone_acc.value > social_acc.value,
486 "alone={} should be > social={}",
487 alone_acc.value,
488 social_acc.value
489 );
490 }
491
492 #[test]
493 fn test_accumulator_value_bounded() {
494 let acc = CoherenceAccumulator::new_with_baseline(2.0); assert!(
497 acc.value >= 0.0 && acc.value <= 1.0,
498 "value={}",
499 acc.value
500 );
501
502 let mut acc = CoherenceAccumulator::new_with_baseline(0.5);
503 for i in 0..1000 {
504 acc.positive_interaction(1.0, i, true);
505 }
506 assert!(acc.value <= 1.0, "value={}", acc.value);
507 }
508
509 #[test]
512 fn test_coherence_field_effective_coherence_unfamiliar() {
513 let mut field: CoherenceField<MbotSensors, 6> = CoherenceField::new();
514 let key = bright_quiet_static();
515
516 let eff = field.effective_coherence(0.8, &key);
518 assert_eq!(eff, 0.0);
519
520 {
522 let acc = field.get_or_create(&key);
523 for i in 0..10 {
524 acc.positive_interaction(0.5, i, false);
525 }
526 }
527 let ctx_coh = field.context_coherence(&key);
528 assert!(ctx_coh > 0.0);
529 assert!(
530 ctx_coh < 0.3,
531 "ctx_coh={} should be < 0.3 for unfamiliar test",
532 ctx_coh
533 );
534
535 let eff = field.effective_coherence(0.8, &key);
537 assert!((eff - ctx_coh).abs() < 0.001);
538
539 let eff = field.effective_coherence(0.05, &key);
541 assert!((eff - 0.05).abs() < 0.001);
542 }
543
544 #[test]
545 fn test_coherence_field_effective_coherence_familiar() {
546 let mut field: CoherenceField<MbotSensors, 6> = CoherenceField::new();
547 let key = bright_quiet_static();
548
549 {
551 let acc = field.get_or_create(&key);
552 for i in 0..80 {
553 acc.positive_interaction(0.5, i, false);
554 }
555 }
556 let ctx_coh = field.context_coherence(&key);
557 assert!(
558 ctx_coh >= 0.3,
559 "ctx_coh={} should be >= 0.3 for familiar test",
560 ctx_coh
561 );
562
563 let eff = field.effective_coherence(0.8, &key);
565 let expected = 0.3 * 0.8 + 0.7 * ctx_coh;
566 assert!(
567 (eff - expected).abs() < 0.001,
568 "eff={} expected={}",
569 eff,
570 expected
571 );
572
573 let eff_low = field.effective_coherence(0.1, &key);
575 let expected_low = 0.3 * 0.1 + 0.7 * ctx_coh;
576 assert!(
577 (eff_low - expected_low).abs() < 0.001,
578 "eff_low={} expected_low={}",
579 eff_low,
580 expected_low
581 );
582 assert!(
583 eff_low > 0.1,
584 "familiar context should buffer: eff_low={}",
585 eff_low
586 );
587 }
588
589 #[test]
590 fn test_coherence_field_independent_contexts() {
591 let mut field: CoherenceField<MbotSensors, 6> = CoherenceField::new();
592 let key_a = bright_quiet_static();
593 let key_b = dark_loud_close();
594
595 {
596 let acc = field.get_or_create(&key_a);
597 for i in 0..50 {
598 acc.positive_interaction(0.5, i, false);
599 }
600 }
601
602 assert!(field.context_coherence(&key_a) > 0.3);
603 assert_eq!(field.context_coherence(&key_b), 0.0);
604 }
605
606 #[test]
607 fn test_coherence_field_interaction_via_personality() {
608 let mut field: CoherenceField<MbotSensors, 6> = CoherenceField::new();
609 let key = bright_quiet_static();
610 let p = neutral_personality();
611
612 for tick in 0..30 {
613 field.positive_interaction(&key, &p, tick, false);
614 }
615 assert!(field.context_coherence(&key) > 0.0);
616
617 let before = field.context_coherence(&key);
618 field.negative_interaction(&key, &p, 30);
619 assert!(field.context_coherence(&key) < before);
620 }
621
622 #[test]
623 fn test_coherence_field_eviction() {
624 let mut field: CoherenceField<MbotSensors, 6> = CoherenceField::new();
625 for i in 0..=MAX_CONTEXTS {
629 let presence = if i % 2 == 0 {
630 PresenceSignature::Absent
631 } else {
632 PresenceSignature::Close
633 };
634 let noise = if i % 3 == 0 {
635 NoiseBand::Quiet
636 } else if i % 3 == 1 {
637 NoiseBand::Moderate
638 } else {
639 NoiseBand::Loud
640 };
641 let brightness = if i % 4 < 2 {
642 BrightnessBand::Bright
643 } else {
644 BrightnessBand::Dark
645 };
646 let key = make_key(brightness, noise, presence);
647 let acc = field.get_or_create(&key);
648 acc.last_interaction_tick = i as u64;
649 }
650 assert!(field.context_count() <= MAX_CONTEXTS);
651 }
652
653 #[test]
654 fn test_coherence_field_decay_all() {
655 let mut field: CoherenceField<MbotSensors, 6> = CoherenceField::new();
656 let key = bright_quiet_static();
657
658 {
659 let acc = field.get_or_create(&key);
660 for i in 0..50 {
661 acc.positive_interaction(0.5, i, false);
662 }
663 }
664 let before = field.context_coherence(&key);
665 field.decay_all(1000);
666 assert!(
667 field.context_coherence(&key) < before,
668 "coherence should decay"
669 );
670 }
671
672 #[test]
673 fn test_coherence_field_fallback() {
674 let mut field: CoherenceField<MbotSensors, 6> = CoherenceField::new();
675 let key = bright_quiet_static();
676
677 assert_eq!(field.context_coherence(&key), 0.0);
679
680 field.set_fallback(Some(0.4));
682 assert!((field.context_coherence(&key) - 0.4).abs() < 0.001);
683
684 {
686 let acc = field.get_or_create(&key);
687 acc.value = 0.6;
688 }
689 assert!((field.context_coherence(&key) - 0.6).abs() < 0.001);
690
691 field.set_fallback(None);
693 assert!((field.context_coherence(&key) - 0.6).abs() < 0.001);
694 }
695
696 #[test]
697 fn test_asymmetric_gate_noise_resilience() {
698 let mut field: CoherenceField<MbotSensors, 6> = CoherenceField::new();
699 let key = bright_quiet_static();
700
701 {
702 let acc = field.get_or_create(&key);
703 for i in 0..100 {
704 acc.positive_interaction(0.5, i, false);
705 }
706 }
707 let ctx_coh = field.context_coherence(&key);
708 assert!(ctx_coh >= 0.3, "should be familiar");
709
710 let eff = field.effective_coherence(0.2, &key);
712 assert!(eff > 0.2, "familiar context should buffer noise: eff={}", eff);
714 }
715
716 #[test]
717 fn test_asymmetric_gate_unfamiliar_strict() {
718 let mut field: CoherenceField<MbotSensors, 6> = CoherenceField::new();
719 let key = bright_quiet_static();
720
721 {
722 let acc = field.get_or_create(&key);
723 for i in 0..5 {
724 acc.positive_interaction(0.5, i, false);
725 }
726 }
727 let ctx_coh = field.context_coherence(&key);
728 assert!(ctx_coh < 0.3);
729
730 let eff = field.effective_coherence(0.9, &key);
732 assert!(
733 (eff - ctx_coh).abs() < 0.001,
734 "unfamiliar gate should cap at ctx: eff={} ctx={}",
735 eff,
736 ctx_coh
737 );
738 }
739
740 #[test]
741 fn test_context_interaction_count() {
742 let mut field: CoherenceField<MbotSensors, 6> = CoherenceField::new();
743 let key = bright_quiet_static();
744 let p = neutral_personality();
745
746 assert_eq!(field.context_interaction_count(&key), 0);
747 for tick in 0..5 {
748 field.positive_interaction(&key, &p, tick, false);
749 }
750 assert_eq!(field.context_interaction_count(&key), 5);
751 }
752
753 #[test]
754 fn test_iter_and_context_count() {
755 let mut field: CoherenceField<MbotSensors, 6> = CoherenceField::new();
756 let key_a = bright_quiet_static();
757 let key_b = dark_loud_close();
758 let p = neutral_personality();
759
760 field.positive_interaction(&key_a, &p, 0, false);
761 field.positive_interaction(&key_b, &p, 1, false);
762
763 assert_eq!(field.context_count(), 2);
764 let count = field.iter().count();
765 assert_eq!(count, 2);
766 }
767}