1use std::path::Path;
12use std::time::{SystemTime, UNIX_EPOCH};
13
14use crate::epistemic::EvidenceLedger;
15use crate::hebbian::HebbianState;
16
17fn now_ms() -> u64 {
20 SystemTime::now()
21 .duration_since(UNIX_EPOCH)
22 .unwrap_or_default()
23 .as_millis() as u64
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum AbsencePattern {
31 ExpectedDisappearance,
33 LogicalAbsence,
35 Taboo,
37 InterruptedThought,
39 AntiHebbian,
41}
42
43impl std::fmt::Display for AbsencePattern {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 match self {
46 AbsencePattern::ExpectedDisappearance => write!(f, "disappearance"),
47 AbsencePattern::LogicalAbsence => write!(f, "logical_absence"),
48 AbsencePattern::Taboo => write!(f, "taboo"),
49 AbsencePattern::InterruptedThought => write!(f, "interrupted"),
50 AbsencePattern::AntiHebbian => write!(f, "anti_hebbian"),
51 }
52 }
53}
54
55impl AbsencePattern {
56 pub fn from_u8(v: u8) -> Self {
57 match v {
58 0 => Self::ExpectedDisappearance,
59 1 => Self::LogicalAbsence,
60 2 => Self::Taboo,
61 3 => Self::InterruptedThought,
62 4 => Self::AntiHebbian,
63 _ => Self::LogicalAbsence,
64 }
65 }
66
67 pub fn to_u8(self) -> u8 {
68 match self {
69 Self::ExpectedDisappearance => 0,
70 Self::LogicalAbsence => 1,
71 Self::Taboo => 2,
72 Self::InterruptedThought => 3,
73 Self::AntiHebbian => 4,
74 }
75 }
76}
77
78#[derive(Debug, Clone)]
82pub struct AbsentiaRecord {
83 pub expected_context_hash: u64,
85 pub missing_entity_hash: u64,
87 pub expected_frequency: f32,
89 pub actual_frequency: f32,
91 pub absence_score: f32,
93 pub first_detected_ms: u64,
95 pub duration_ms: u64,
97 pub pattern_type: AbsencePattern,
99 pub gradient_weight: f32,
101}
102
103#[derive(Debug, Clone)]
107pub struct AntiHebbianPair {
108 pub block_a: u32,
109 pub block_b: u32,
110 pub expected_coactivation: f32,
111 pub actual_coactivation: f32,
112 pub absence_score: f32,
113 pub first_detected_ms: u64,
114}
115
116#[derive(Default)]
120pub struct AbsentiaState {
121 pub records: Vec<AbsentiaRecord>,
123 pub anti_hebbian: Vec<AntiHebbianPair>,
125 pub negative_attractors: Vec<(f64, f64, f64, f64)>,
127 pub last_scan_ms: u64,
129}
130
131impl AbsentiaState {
132 pub fn new() -> Self {
133 Self::default()
134 }
135
136 pub fn load_or_init(output_dir: &Path) -> Self {
138 let path = output_dir.join("absentia.bin");
139 if let Ok(data) = std::fs::read(&path) {
140 if data.len() >= 4 && &data[0..4] == b"ABS1" {
141 return Self::decode(&data);
142 }
143 }
144 Self::new()
145 }
146
147 pub fn save(&self, output_dir: &Path) -> Result<(), String> {
149 let path = output_dir.join("absentia.bin");
150 let data = self.encode();
151 let tmp = output_dir.join("absentia.bin.tmp");
152 std::fs::write(&tmp, &data).map_err(|e| format!("write absentia: {}", e))?;
153 std::fs::rename(&tmp, &path).map_err(|e| format!("rename absentia: {}", e))?;
154 Ok(())
155 }
156
157 pub fn scan(&mut self, hebb: &HebbianState, _evidence: &EvidenceLedger, _block_count: usize) {
162 let now = now_ms();
163 self.last_scan_ms = now;
164
165 let mut active_blocks: Vec<(u32, f32)> = hebb
173 .activations
174 .iter()
175 .enumerate()
176 .filter(|(_, r)| r.energy > 0.3)
177 .map(|(i, r)| (i as u32, r.energy))
178 .collect();
179 active_blocks.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
180 active_blocks.truncate(50); for i in 0..active_blocks.len() {
184 for j in (i + 1)..active_blocks.len() {
185 let a = active_blocks[i].0.min(active_blocks[j].0);
186 let b = active_blocks[i].0.max(active_blocks[j].0);
187 let pair_key = (a, b);
188
189 if !hebb.coactivations.contains_key(&pair_key) {
191 let energy_a = active_blocks[i].1;
192 let energy_b = active_blocks[j].1;
193 let absence = (energy_a * energy_b).min(1.0);
195
196 if absence > 0.2 {
197 self.anti_hebbian.push(AntiHebbianPair {
199 block_a: a,
200 block_b: b,
201 expected_coactivation: absence,
202 actual_coactivation: 0.0,
203 absence_score: absence,
204 first_detected_ms: now,
205 });
206 }
207 }
208 }
209 }
210
211 for (i, rec) in hebb.activations.iter().enumerate() {
215 if rec.energy > 0.5 {
216 self.records.push(AbsentiaRecord {
220 expected_context_hash: i as u64,
221 missing_entity_hash: 0,
222 expected_frequency: rec.activation_count as f32,
223 actual_frequency: 0.0,
224 absence_score: rec.energy,
225 first_detected_ms: now,
226 duration_ms: 0,
227 pattern_type: AbsencePattern::LogicalAbsence,
228 gradient_weight: rec.energy * 0.5,
229 });
230 }
231 }
232
233 self.negative_attractors.clear();
237 for pair in &self.anti_hebbian {
238 let weight = -(pair.absence_score as f64);
241 self.negative_attractors.push((
242 pair.block_a as f64 * 0.1,
243 pair.block_b as f64 * 0.1,
244 0.0,
245 weight,
246 ));
247 }
248
249 if self.records.len() > 500 {
251 self.records.drain(0..self.records.len() - 500);
252 }
253 if self.anti_hebbian.len() > 200 {
254 self.anti_hebbian.drain(0..self.anti_hebbian.len() - 200);
255 }
256 }
257
258 pub fn stats(&self) -> AbsentiaStats {
260 let total_records = self.records.len();
261 let anti_hebbian_count = self.anti_hebbian.len();
262 let negative_attractor_count = self.negative_attractors.len();
263
264 let avg_absence = if total_records > 0 {
265 self.records
266 .iter()
267 .map(|r| r.absence_score as f64)
268 .sum::<f64>()
269 / total_records as f64
270 } else {
271 0.0
272 };
273
274 let avg_anti_hebbian = if anti_hebbian_count > 0 {
275 self.anti_hebbian
276 .iter()
277 .map(|p| p.absence_score as f64)
278 .sum::<f64>()
279 / anti_hebbian_count as f64
280 } else {
281 0.0
282 };
283
284 let causal_laundering_suspect = self
287 .anti_hebbian
288 .iter()
289 .filter(|p| p.absence_score > 0.5)
290 .count();
291
292 AbsentiaStats {
293 total_records,
294 anti_hebbian_count,
295 negative_attractor_count,
296 avg_absence,
297 avg_anti_hebbian,
298 causal_laundering_suspect,
299 last_scan_ms: self.last_scan_ms,
300 }
301 }
302
303 fn encode(&self) -> Vec<u8> {
306 let mut buf = Vec::new();
307 buf.extend_from_slice(b"ABS1");
308 buf.extend_from_slice(&(self.records.len() as u32).to_le_bytes());
309 buf.extend_from_slice(&(self.anti_hebbian.len() as u32).to_le_bytes());
310 buf.extend_from_slice(&(self.negative_attractors.len() as u32).to_le_bytes());
311 buf.extend_from_slice(&self.last_scan_ms.to_le_bytes());
312
313 for r in &self.records {
315 buf.extend_from_slice(&r.expected_context_hash.to_le_bytes());
316 buf.extend_from_slice(&r.missing_entity_hash.to_le_bytes());
317 buf.extend_from_slice(&r.expected_frequency.to_le_bytes());
318 buf.extend_from_slice(&r.actual_frequency.to_le_bytes());
319 buf.extend_from_slice(&r.absence_score.to_le_bytes());
320 buf.extend_from_slice(&r.first_detected_ms.to_le_bytes());
321 buf.extend_from_slice(&r.duration_ms.to_le_bytes());
322 buf.extend_from_slice(&r.pattern_type.to_u8().to_le_bytes());
323 buf.extend_from_slice(&r.gradient_weight.to_le_bytes());
324 }
325
326 for p in &self.anti_hebbian {
328 buf.extend_from_slice(&p.block_a.to_le_bytes());
329 buf.extend_from_slice(&p.block_b.to_le_bytes());
330 buf.extend_from_slice(&p.expected_coactivation.to_le_bytes());
331 buf.extend_from_slice(&p.actual_coactivation.to_le_bytes());
332 buf.extend_from_slice(&p.absence_score.to_le_bytes());
333 buf.extend_from_slice(&p.first_detected_ms.to_le_bytes());
334 }
335
336 for &(x, y, z, w) in &self.negative_attractors {
338 buf.extend_from_slice(&x.to_le_bytes());
339 buf.extend_from_slice(&y.to_le_bytes());
340 buf.extend_from_slice(&z.to_le_bytes());
341 buf.extend_from_slice(&w.to_le_bytes());
342 }
343
344 buf
345 }
346
347 fn decode(data: &[u8]) -> Self {
348 if data.len() < 20 {
349 return Self::new();
350 }
351 let record_count = u32::from_le_bytes(data[4..8].try_into().unwrap()) as usize;
352 let anti_count = u32::from_le_bytes(data[8..12].try_into().unwrap()) as usize;
353 let neg_count = u32::from_le_bytes(data[12..16].try_into().unwrap()) as usize;
354 let last_scan = u64::from_le_bytes(data[16..24].try_into().unwrap());
355
356 let mut off = 24;
357 let mut records = Vec::with_capacity(record_count);
358 for _ in 0..record_count {
359 if off + 41 > data.len() {
360 break;
361 }
362 let expected_context_hash = u64::from_le_bytes(data[off..off + 8].try_into().unwrap());
363 off += 8;
364 let missing_entity_hash = u64::from_le_bytes(data[off..off + 8].try_into().unwrap());
365 off += 8;
366 let expected_frequency = f32::from_le_bytes(data[off..off + 4].try_into().unwrap());
367 off += 4;
368 let actual_frequency = f32::from_le_bytes(data[off..off + 4].try_into().unwrap());
369 off += 4;
370 let absence_score = f32::from_le_bytes(data[off..off + 4].try_into().unwrap());
371 off += 4;
372 let first_detected_ms = u64::from_le_bytes(data[off..off + 8].try_into().unwrap());
373 off += 8;
374 let duration_ms = u64::from_le_bytes(data[off..off + 8].try_into().unwrap());
375 off += 8;
376 let pattern_byte = data[off];
377 off += 1;
378 let gradient_weight = f32::from_le_bytes(data[off..off + 4].try_into().unwrap());
379 off += 4;
380 records.push(AbsentiaRecord {
381 expected_context_hash,
382 missing_entity_hash,
383 expected_frequency,
384 actual_frequency,
385 absence_score,
386 first_detected_ms,
387 duration_ms,
388 pattern_type: AbsencePattern::from_u8(pattern_byte),
389 gradient_weight,
390 });
391 }
392
393 let mut anti_hebbian = Vec::with_capacity(anti_count);
394 for _ in 0..anti_count {
395 if off + 28 > data.len() {
396 break;
397 }
398 let block_a = u32::from_le_bytes(data[off..off + 4].try_into().unwrap());
399 off += 4;
400 let block_b = u32::from_le_bytes(data[off..off + 4].try_into().unwrap());
401 off += 4;
402 let expected_coactivation = f32::from_le_bytes(data[off..off + 4].try_into().unwrap());
403 off += 4;
404 let actual_coactivation = f32::from_le_bytes(data[off..off + 4].try_into().unwrap());
405 off += 4;
406 let absence_score = f32::from_le_bytes(data[off..off + 4].try_into().unwrap());
407 off += 4;
408 let first_detected_ms = u64::from_le_bytes(data[off..off + 8].try_into().unwrap());
409 off += 8;
410 anti_hebbian.push(AntiHebbianPair {
411 block_a,
412 block_b,
413 expected_coactivation,
414 actual_coactivation,
415 absence_score,
416 first_detected_ms,
417 });
418 }
419
420 let mut negative_attractors = Vec::with_capacity(neg_count);
421 for _ in 0..neg_count {
422 if off + 32 > data.len() {
423 break;
424 }
425 let x = f64::from_le_bytes(data[off..off + 8].try_into().unwrap());
426 off += 8;
427 let y = f64::from_le_bytes(data[off..off + 8].try_into().unwrap());
428 off += 8;
429 let z = f64::from_le_bytes(data[off..off + 8].try_into().unwrap());
430 off += 8;
431 let w = f64::from_le_bytes(data[off..off + 8].try_into().unwrap());
432 off += 8;
433 negative_attractors.push((x, y, z, w));
434 }
435
436 Self {
437 records,
438 anti_hebbian,
439 negative_attractors,
440 last_scan_ms: last_scan,
441 }
442 }
443}
444
445pub struct AbsentiaStats {
448 pub total_records: usize,
449 pub anti_hebbian_count: usize,
450 pub negative_attractor_count: usize,
451 pub avg_absence: f64,
452 pub avg_anti_hebbian: f64,
453 pub causal_laundering_suspect: usize,
454 pub last_scan_ms: u64,
455}
456
457pub fn apply_absentia_to_field(
467 absentia: &AbsentiaState,
468 field: &mut crate::morphogenesis::MorphogenField,
469 evidence_confidence: u8,
470) {
471 let keys: Vec<(i32, i32, i32)> = field.gradients.keys().cloned().collect();
474 for key in keys {
475 let shadow = compute_absence_shadow(
476 absentia,
477 key.0 as f64,
478 key.1 as f64,
479 key.2 as f64,
480 evidence_confidence,
481 );
482 if shadow > 0.01 {
483 if let Some(val) = field.gradients.get_mut(&key) {
484 *val *= 1.0 - shadow;
485 }
486 }
487 }
488}
489
490pub fn compute_absence_shadow(
498 absentia: &AbsentiaState,
499 x: f64,
500 y: f64,
501 z: f64,
502 evidence_confidence: u8,
503) -> f64 {
504 let mut shadow = 0.0f64;
505
506 for &(ax, ay, az, weight) in &absentia.negative_attractors {
508 let dx = x - ax;
509 let dy = y - ay;
510 let dz = z - az;
511 let dist_sq = dx * dx + dy * dy + dz * dz;
512 let local_shadow = weight.abs() / (1.0 + dist_sq);
514 shadow += local_shadow;
515 }
516
517 for rec in &absentia.records {
519 let ax = (rec.expected_context_hash % 100) as f64;
521 let ay = ((rec.expected_context_hash / 100) % 100) as f64;
522 let az = 0.0;
523 let dx = x - ax;
524 let dy = y - ay;
525 let dz = z - az;
526 let dist_sq = dx * dx + dy * dy + dz * dz;
527 let local_shadow = rec.gradient_weight as f64 / (1.0 + dist_sq);
528 shadow += local_shadow * 0.5; }
530
531 let base_shadow = shadow.min(0.95);
534
535 let evidence_factor = evidence_confidence as f64 / 100.0;
538 let final_shadow = base_shadow * (1.0 - evidence_factor * 0.8);
541 final_shadow.max(0.0)
542}
543
544pub fn test_presence_absence_inhibition(
550 _absentia: &AbsentiaState,
551 hebb_energy: f64,
552 evidence_confidence: u8,
553) -> PresenceAbsenceResult {
554 let absence_shadow = if evidence_confidence < 10 {
555 0.8
557 } else if evidence_confidence < 50 {
558 0.4
560 } else {
561 0.1
563 };
564
565 let effective_gradient = hebb_energy * (1.0 - absence_shadow);
566 let growth_inhibited = absence_shadow > 0.5;
567
568 PresenceAbsenceResult {
569 hebb_energy,
570 evidence_confidence,
571 absence_shadow,
572 effective_gradient,
573 growth_inhibited,
574 }
575}
576
577pub struct PresenceAbsenceResult {
578 pub hebb_energy: f64,
579 pub evidence_confidence: u8,
580 pub absence_shadow: f64,
581 pub effective_gradient: f64,
582 pub growth_inhibited: bool,
583}