1use std::fs;
13use std::path::Path;
14use std::time::{SystemTime, UNIX_EPOCH};
15
16use crate::archetype::ArchetypeState;
17use crate::attention::AttentionState;
18use crate::config::Config;
19use crate::emotional_episode::{EpisodeStore, PadState};
20use crate::hebbian::HebbianState;
21use crate::narrative::NarrativeState;
22use crate::reader::MicroscopeReader;
23use crate::self_reflect::ReflectionState;
24use crate::thought_graph::ThoughtGraphState;
25use colored::Colorize;
26use epistemic_core::binary as epi_binary;
27
28const MAX_STR_LEN: usize = 512;
29
30#[derive(Clone, Debug)]
31pub struct SelfModelSnapshot {
32 pub timestamp_ms: u64,
33 pub emotional: [f32; 21],
34 pub attention_weights: [f32; 7],
35 pub hebbian_energy: f32,
36 pub hot_count: u32,
37 pub archetype_count: u32,
38 pub pattern_count: u32,
39 pub block_count: u32,
40 pub session_count: u64,
41 pub narrative: String,
42 pub reflection: String,
43 pub pad: Option<PadState>,
45 pub episodes_active: u32,
46 pub episodes_passed: u32,
47 pub episodes_failed: u32,
48 pub awareness_confidence: Option<epistemic_core::types::SplitConfidence>,
51}
52
53impl SelfModelSnapshot {
54 pub fn from_bytes(data: &[u8]) -> Option<Self> {
55 if data.len() < 150 || &data[0..4] != b"SLF1" {
56 return None;
57 }
58 let mut pos = 4;
59 let ts = u64::from_le_bytes(data[pos..pos + 8].try_into().ok()?);
60 pos += 8;
61 let ver = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?);
62 pos += 2;
63 let mut emo = [0.0f32; 21];
64 for e in emo.iter_mut() {
65 *e = f32::from_le_bytes(data[pos..pos + 4].try_into().ok()?);
66 pos += 4;
67 }
68 let mut attn = [0.0f32; 7];
69 for a in attn.iter_mut() {
70 *a = f32::from_le_bytes(data[pos..pos + 4].try_into().ok()?);
71 pos += 4;
72 }
73 let he = f32::from_le_bytes(data[pos..pos + 4].try_into().ok()?);
74 pos += 4;
75 let hc = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?);
76 pos += 4;
77 let ac = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?);
78 pos += 4;
79 let pc = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?);
80 pos += 4;
81 let bc = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?);
82 pos += 4;
83 let sc = u64::from_le_bytes(data[pos..pos + 8].try_into().ok()?);
84 pos += 8;
85 let nlen = u16::from_le_bytes(data[pos..pos + 2].try_into().unwrap_or([0; 2])) as usize;
86 pos += 2;
87 let narr = if nlen > 0 && pos + nlen <= data.len() {
88 String::from_utf8_lossy(&data[pos..pos + nlen]).to_string()
89 } else {
90 String::new()
91 };
92 pos += nlen;
93 let rlen = u16::from_le_bytes(data[pos..pos + 2].try_into().unwrap_or([0; 2])) as usize;
94 pos += 2;
95 let refl = if rlen > 0 && pos + rlen <= data.len() {
96 String::from_utf8_lossy(&data[pos..pos + rlen]).to_string()
97 } else {
98 String::new()
99 };
100 pos += rlen;
101
102 let (pad, episodes_active, episodes_passed, episodes_failed, awareness_confidence) =
104 if ver >= 2 && pos + 28 <= data.len() {
105 let has_pad = data[pos];
106 pos += 1;
107 let pad = if has_pad == 1 {
108 let p = f64::from_le_bytes(data[pos..pos + 8].try_into().ok()?);
109 pos += 8;
110 let a = f64::from_le_bytes(data[pos..pos + 8].try_into().ok()?);
111 pos += 8;
112 let d = f64::from_le_bytes(data[pos..pos + 8].try_into().ok()?);
113 pos += 8;
114 Some(PadState::new(p, a, d))
115 } else {
116 pos += 24;
117 None
118 };
119 let ea = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?);
120 pos += 4;
121 let ep = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?);
122 pos += 4;
123 let ef = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?);
124 pos += 4;
125
126 let has_aware = data.get(pos).copied().unwrap_or(0);
127 pos += 1;
128 let ac = if has_aware == 1 && pos + 32 <= data.len() {
129 let ev = f64::from_le_bytes(data[pos..pos + 8].try_into().ok()?);
130 pos += 8;
131 let ce = f64::from_le_bytes(data[pos..pos + 8].try_into().ok()?);
132 pos += 8;
133 let re = f64::from_le_bytes(data[pos..pos + 8].try_into().ok()?);
134 pos += 8;
135 let na = f64::from_le_bytes(data[pos..pos + 8].try_into().ok()?);
136 Some(epistemic_core::types::SplitConfidence::new(ev, ce, re, na))
137 } else {
138 None
139 };
140
141 (pad, ea, ep, ef, ac)
142 } else {
143 (None, 0, 0, 0, None)
144 };
145
146 Some(Self {
147 timestamp_ms: ts,
148 emotional: emo,
149 attention_weights: attn,
150 hebbian_energy: he,
151 hot_count: hc,
152 archetype_count: ac,
153 pattern_count: pc,
154 block_count: bc,
155 session_count: sc,
156 narrative: narr,
157 reflection: refl,
158 pad,
159 episodes_active,
160 episodes_passed,
161 episodes_failed,
162 awareness_confidence,
163 })
164 }
165
166 pub fn to_bytes(&self) -> Vec<u8> {
167 let narr_b = self.narrative.as_bytes();
168 let refl_b = self.reflection.as_bytes();
169 let nlen = narr_b.len().min(MAX_STR_LEN) as u16;
170 let rlen = refl_b.len().min(MAX_STR_LEN) as u16;
171 let mut buf = Vec::with_capacity(250 + nlen as usize + rlen as usize);
172 buf.extend_from_slice(b"SLF1");
173 buf.extend_from_slice(&self.timestamp_ms.to_le_bytes());
174 buf.extend_from_slice(&2u16.to_le_bytes()); for v in self.emotional {
176 buf.extend_from_slice(&v.to_le_bytes());
177 }
178 for v in self.attention_weights {
179 buf.extend_from_slice(&v.to_le_bytes());
180 }
181 buf.extend_from_slice(&self.hebbian_energy.to_le_bytes());
182 buf.extend_from_slice(&self.hot_count.to_le_bytes());
183 buf.extend_from_slice(&self.archetype_count.to_le_bytes());
184 buf.extend_from_slice(&self.pattern_count.to_le_bytes());
185 buf.extend_from_slice(&self.block_count.to_le_bytes());
186 buf.extend_from_slice(&self.session_count.to_le_bytes());
187 buf.extend_from_slice(&nlen.to_le_bytes());
188 buf.extend_from_slice(&narr_b[..nlen as usize]);
189 buf.extend_from_slice(&rlen.to_le_bytes());
190 buf.extend_from_slice(&refl_b[..rlen as usize]);
191
192 if let Some(pad) = self.pad {
194 buf.push(1u8);
195 buf.extend_from_slice(&pad.pleasure.to_le_bytes());
196 buf.extend_from_slice(&pad.arousal.to_le_bytes());
197 buf.extend_from_slice(&pad.dominance.to_le_bytes());
198 } else {
199 buf.push(0u8);
200 buf.extend_from_slice(&[0u8; 24]);
201 }
202 buf.extend_from_slice(&self.episodes_active.to_le_bytes());
203 buf.extend_from_slice(&self.episodes_passed.to_le_bytes());
204 buf.extend_from_slice(&self.episodes_failed.to_le_bytes());
205
206 if let Some(ac) = self.awareness_confidence {
207 buf.push(1u8);
208 buf.extend_from_slice(&ac.evidence.to_le_bytes());
209 buf.extend_from_slice(&ac.counterevidence.to_le_bytes());
210 buf.extend_from_slice(&ac.reasoning.to_le_bytes());
211 buf.extend_from_slice(&ac.narrative.to_le_bytes());
212 } else {
213 buf.push(0u8);
214 }
215
216 buf
217 }
218}
219
220pub struct SelfModel {
221 pub snapshots: Vec<SelfModelSnapshot>,
222 pub current: Option<SelfModelSnapshot>,
223 pub previous: Option<SelfModelSnapshot>,
224}
225
226impl SelfModel {
227 pub fn load_or_init(output_dir: &Path) -> Self {
228 let path = output_dir.join("self_model.bin");
229 let mut snapshots = Vec::new();
230 if let Ok(data) = fs::read(&path) {
231 let mut pos = 0;
232 while pos + 4 <= data.len() {
233 if &data[pos..pos + 4] == b"SLF1" {
234 if let Some(snap) = SelfModelSnapshot::from_bytes(&data[pos..]) {
235 let base = 4 + 8 + 2 + 84 + 28 + 4 + 4 + 4 + 4 + 4 + 8 + 2 + 2;
237 let nlen = snap.narrative.len().min(MAX_STR_LEN);
238 let rlen = snap.reflection.len().min(MAX_STR_LEN);
239 let v2_ext = 1 + 24 + 4 + 4 + 4 + 1;
240 let has_aware = if snap.awareness_confidence.is_some() {
241 32
242 } else {
243 0
244 };
245 let size = base + nlen + rlen + v2_ext + has_aware;
246 pos += size;
247 snapshots.push(snap);
248 continue;
249 }
250 }
251 pos += 1;
252 }
253 }
254 let current = snapshots.last().cloned();
255 let previous = if snapshots.len() >= 2 {
256 snapshots.get(snapshots.len() - 2).cloned()
257 } else {
258 None
259 };
260 Self {
261 snapshots,
262 current,
263 previous,
264 }
265 }
266
267 pub fn save(&self, output_dir: &Path) -> Result<(), String> {
268 let path = output_dir.join("self_model.bin");
269 let tmp_path = output_dir.join("self_model.bin.tmp");
270 let mut buf = Vec::new();
271 for snap in &self.snapshots {
272 buf.extend_from_slice(&snap.to_bytes());
273 }
274 fs::write(&tmp_path, &buf).map_err(|e| format!("write self_model.bin: {}", e))?;
275 fs::rename(&tmp_path, &path).map_err(|e| format!("rename self_model.bin: {}", e))?;
276 Ok(())
277 }
278
279 pub fn take_snapshot(
280 &mut self,
281 _config: &Config,
282 reader: &MicroscopeReader,
283 output_dir: &Path,
284 ) -> SelfModelSnapshot {
285 let now_ms = SystemTime::now()
286 .duration_since(UNIX_EPOCH)
287 .unwrap_or_default()
288 .as_millis() as u64;
289 let hebb = HebbianState::load_or_init(output_dir, reader.block_count);
290 let attention = AttentionState::load_or_init(output_dir);
291 let archetypes = ArchetypeState::load_or_init(output_dir);
292 let narrative = NarrativeState::load_or_init(output_dir);
293 let thought_graph = ThoughtGraphState::load_or_init(output_dir);
294 let reflection = ReflectionState::load_or_init(output_dir);
295
296 let episode_store = EpisodeStore::load_or_init(output_dir);
298 let pad = episode_store.aggregate_pad();
299 let emotional = episode_store.aggregate_21d();
300 let active_eps = episode_store.active_episodes();
301 let episodes_active = active_eps.len() as u32;
302 let episodes_passed = active_eps.iter().filter(|e| e.gate_passed).count() as u32;
303 let episodes_failed = episodes_active - episodes_passed;
304
305 use epistemic_core::gate::{EpistemicGate, GateConfig, GateDecision};
310 use epistemic_core::graph::ReasoningGraph;
311 use epistemic_core::rules::{InferenceRule, RuleRegistry};
312 use epistemic_core::types::{CounterevidenceLink, SplitConfidence};
313
314 let mut awareness_graph = ReasoningGraph::new();
315 let reg = RuleRegistry::new();
316
317 let e1 = awareness_graph.add_evidence(1); let e2 = awareness_graph.add_evidence(2); let e3 = awareness_graph.add_evidence(3); let root = awareness_graph.add_conclusion(1, "I am aware of myself");
322
323 awareness_graph.add_step(e1, InferenceRule::ObservationToExistence, root, 0.80, ®);
325 awareness_graph.add_step(e2, InferenceRule::ObservationToExistence, root, 0.75, ®);
326 awareness_graph.add_step(e3, InferenceRule::ConvergentEvidence, root, 0.70, ®);
327 awareness_graph.set_root(root);
328
329 awareness_graph.add_counterevidence(
331 100,
332 0.50,
333 "awareness is template-generated, not emergent",
334 );
335 awareness_graph.add_counterevidence(
336 101,
337 0.40,
338 "self-report of awareness is not genuine awareness",
339 );
340
341 let awareness_ce = vec![
343 CounterevidenceLink {
344 evidence_id: 100,
345 weakening: 0.50,
346 contradicts: "awareness is template-generated, not emergent".into(),
347 },
348 CounterevidenceLink {
349 evidence_id: 101,
350 weakening: 0.40,
351 contradicts: "self-report of awareness != genuine awareness".into(),
352 },
353 ];
354 let awareness_ce_conf = SplitConfidence::compute_counterevidence(&awareness_ce);
355
356 let awareness_gate = EpistemicGate::new(GateConfig::default());
357 let awareness_decision =
358 awareness_gate.evaluate(0.80, awareness_ce_conf, &awareness_graph, 0.50);
359
360 let _ = epi_binary::save_graph(
362 &awareness_graph,
363 &output_dir.join("awareness_reasoning.bin"),
364 );
365
366 let awareness_confidence = match awareness_decision {
367 GateDecision::Pass {
368 evidence,
369 counterevidence,
370 reasoning,
371 narrative,
372 ..
373 } => Some(SplitConfidence::new(
374 evidence,
375 counterevidence,
376 reasoning,
377 narrative,
378 )),
379 GateDecision::Blocked {
380 evidence,
381 counterevidence,
382 reasoning,
383 narrative,
384 ..
385 } => Some(SplitConfidence::new(
386 evidence,
387 counterevidence,
388 reasoning,
389 narrative,
390 )),
391 };
392 let hot_count = hebb.activations.iter().filter(|a| a.energy > 0.1).count() as u32;
393 let hebbian_energy: f32 = hebb.activations.iter().map(|a| a.energy).sum();
394
395 let snap = SelfModelSnapshot {
396 timestamp_ms: now_ms,
397 emotional,
398 attention_weights: attention.learned_weights,
399 hebbian_energy,
400 hot_count,
401 archetype_count: archetypes.archetypes.len() as u32,
402 pattern_count: thought_graph.crystallized_count() as u32,
403 block_count: reader.block_count as u32,
404 session_count: narrative.session_count,
405 narrative: narrative.narrative.clone(),
406 reflection: reflection.last_reflection_text.clone(),
407 pad: Some(pad),
408 episodes_active,
409 episodes_passed,
410 episodes_failed,
411 awareness_confidence,
412 };
413
414 self.previous = self.current.clone();
415 self.current = Some(snap.clone());
416 self.snapshots.push(snap.clone());
417 let _ = self.save(output_dir);
418 snap
419 }
420
421 pub fn describe_change(&self) -> String {
422 match (&self.current, &self.previous) {
423 (Some(cur), Some(prev)) => {
424 let mut changes = Vec::new();
425 let emo_labels = crate::reader::EMOTION_DIMS;
426 for (i, label) in emo_labels.iter().enumerate() {
427 let diff = cur.emotional[i] - prev.emotional[i];
428 if diff.abs() > 0.1 {
429 let dir = if diff > 0.0 { "increased" } else { "decreased" };
430 changes.push(format!("{} {} by {:.2}", label, dir, diff.abs()));
431 }
432 }
433 let attn_labels = [
434 "Hebbian",
435 "Mirror",
436 "Resonance",
437 "Archetype",
438 "Emotional",
439 "ThoughtGraph",
440 "PredictiveCache",
441 ];
442 for (i, label) in attn_labels.iter().enumerate() {
443 let diff = cur.attention_weights[i] - prev.attention_weights[i];
444 if diff.abs() > 0.05 {
445 let dir = if diff > 0.0 { "up" } else { "down" };
446 changes.push(format!(
447 "{} focus {} by {:.0}%",
448 label,
449 dir,
450 diff.abs() * 100.0
451 ));
452 }
453 }
454 if cur.hot_count != prev.hot_count {
455 changes.push(format!(
456 "hot memories: {} -> {}",
457 prev.hot_count, cur.hot_count
458 ));
459 }
460 if cur.block_count != prev.block_count {
461 changes.push(format!(
462 "blocks: {} -> {}",
463 prev.block_count, cur.block_count
464 ));
465 }
466 if cur.episodes_active != prev.episodes_active {
468 changes.push(format!(
469 "active episodes: {} -> {}",
470 prev.episodes_active, cur.episodes_active
471 ));
472 }
473 if cur.episodes_passed != prev.episodes_passed {
474 changes.push(format!(
475 "gate-passed episodes: {} -> {}",
476 prev.episodes_passed, cur.episodes_passed
477 ));
478 }
479 if changes.is_empty() {
480 "I am stable, no significant changes.".to_string()
481 } else {
482 format!("I have changed: {}", changes.join(", "))
483 }
484 }
485 (Some(_), None) => "This is my first self-model snapshot.".to_string(),
486 (None, _) => "No self-model data yet.".to_string(),
487 }
488 }
489}
490
491pub fn format_self_model(snap: &SelfModelSnapshot, change_desc: &str) -> String {
492 let labels = crate::reader::EMOTION_DIMS;
493 let mut emotions: Vec<(usize, f32)> = snap
494 .emotional
495 .iter()
496 .enumerate()
497 .map(|(i, &v)| (i, v))
498 .collect();
499 emotions.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
500 let top_emo: Vec<String> = emotions
501 .iter()
502 .take(3)
503 .filter(|(_, v)| *v > 0.01)
504 .map(|(i, v)| format!("{}={:.2}", labels[*i], v))
505 .collect();
506 let emo_str = if top_emo.is_empty() {
507 "neutral".to_string()
508 } else {
509 top_emo.join(", ")
510 };
511
512 let attn_labels = [
513 "Hebbian",
514 "Mirror",
515 "Resonance",
516 "Archetype",
517 "Emotional",
518 "ThoughtGraph",
519 "PredictiveCache",
520 ];
521 let mut attn: Vec<(usize, f32)> = snap
522 .attention_weights
523 .iter()
524 .enumerate()
525 .map(|(i, &w)| (i, w))
526 .collect();
527 attn.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
528 let top_attn: Vec<String> = attn
529 .iter()
530 .take(3)
531 .map(|(i, w)| format!("{}={:.0}%", attn_labels[*i], w * 100.0))
532 .collect();
533
534 let pad_str = if let Some(pad) = snap.pad {
536 format!(
537 "P={:.2} A={:.2} D={:.2}",
538 pad.pleasure, pad.arousal, pad.dominance
539 )
540 } else {
541 "—".to_string()
542 };
543
544 let episode_str = if snap.episodes_active > 0 {
545 format!(
546 "{} active ({} passed, {} failed)",
547 snap.episodes_active, snap.episodes_passed, snap.episodes_failed
548 )
549 } else {
550 "none".to_string()
551 };
552
553 let epistemic_str = if let Some(ac) = snap.awareness_confidence {
555 format!(
556 "E={:.2} C={:.2} R={:.2} N={:.2}",
557 ac.evidence, ac.counterevidence, ac.reasoning, ac.narrative
558 )
559 } else {
560 "—".to_string()
561 };
562
563 format!(
564 " {} SELF-MODEL snapshot\n\
565 \x20 emotion: {}\n\
566 \x20 PAD: {}\n\
567 \x20 episodes: {}\n\
568 \x20 epistemic: {}\n\
569 \x20 focus: {}\n\
570 \x20 state: {} hot memories, {} archetypes, {} patterns, {} blocks\n\
571 \x20 change: {}\n\
572 \x20 self: \"{}\"",
573 "SELF:".cyan().bold(),
574 emo_str,
575 pad_str,
576 episode_str,
577 epistemic_str,
578 top_attn.join(", "),
579 snap.hot_count,
580 snap.archetype_count,
581 snap.pattern_count,
582 snap.block_count,
583 change_desc,
584 crate::safe_truncate(&snap.reflection, 80),
585 )
586}
587
588pub fn format_awareness_trace(output_dir: &Path) -> String {
593 let path = output_dir.join("awareness_reasoning.bin");
594 let graph = match epi_binary::load_graph(&path) {
595 Ok(g) => g,
596 Err(e) => {
597 return format!(
598 " {} AWARENESS TRACE\n Error: {} (no reasoning graph saved yet)",
599 "EPI:".red().bold(),
600 e
601 )
602 }
603 };
604
605 let mut out = format!(" {} AWARENESS TRACE\n", "EPI:".cyan().bold());
606 out.push_str(&format!(
607 " Reasoning graph: {} nodes, {} edges\n",
608 graph.nodes.len(),
609 graph.edges.len()
610 ));
611
612 out.push_str(" Nodes:\n");
614 for (i, node) in graph.nodes.iter().enumerate() {
615 let desc = match node {
616 epistemic_core::types::ReasoningNode::Evidence { id } => format!("evidence#{}", id),
617 epistemic_core::types::ReasoningNode::Conclusion { id, text } => {
618 format!("claim#{}: \"{}\"", id, text)
619 }
620 epistemic_core::types::ReasoningNode::Counterevidence {
621 id,
622 weakening,
623 contradicts,
624 } => format!(
625 "counterevidence#{} (w={:.2}): {}",
626 id, weakening, contradicts
627 ),
628 };
629 let marker = if i == graph.root.0 as usize {
630 " [ROOT]"
631 } else {
632 ""
633 };
634 out.push_str(&format!(" [{}] {}{}\n", i, desc, marker));
635 }
636
637 out.push_str(" Inference steps:\n");
639 for (i, step) in graph.edges.iter().enumerate() {
640 let penalized = if step.rule.is_penalized() {
641 " [PENALIZED]"
642 } else {
643 ""
644 };
645 out.push_str(&format!(
646 " [{}] node{} -> node{} via {} (conf={:.3}){}\n",
647 i, step.premise.0, step.conclusion.0, step.rule, step.confidence, penalized
648 ));
649 }
650
651 let trace = graph.trace_to_evidence();
653 if !trace.is_empty() {
654 out.push_str(" Trace (root -> evidence):\n");
655 for step in &trace {
656 let penalized = if step.rule.is_penalized() {
657 " [PENALIZED]"
658 } else {
659 ""
660 };
661 let node_desc = step.node_desc.as_deref().unwrap_or("?");
662 out.push_str(&format!(
663 " {} <- {} (conf={:.3}){}\n",
664 node_desc, step.rule, step.step_confidence, penalized
665 ));
666 }
667 }
668
669 let rc = graph.reasoning_confidence();
671 let nc = graph.narrative_confidence();
672 out.push_str(&format!(" Reasoning confidence: {:.3}\n", rc));
673 out.push_str(&format!(" Narrative confidence: {:.3}\n", nc));
674
675 let penalized_steps = graph.penalized_steps();
677 if !penalized_steps.is_empty() {
678 out.push_str(" Penalized rules used:\n");
679 for (step, conf) in &penalized_steps {
680 out.push_str(&format!(
681 " {} (penalty factor={:.2}, edge conf={:.3})\n",
682 step.rule,
683 step.rule.penalty_factor(),
684 conf
685 ));
686 }
687 } else {
688 out.push_str(" No penalized rules used.\n");
689 }
690
691 let ce_nodes = graph.counterevidence_nodes();
693 if !ce_nodes.is_empty() {
694 out.push_str(" Counterevidence:\n");
695 for (node, weakening) in &ce_nodes {
696 if let epistemic_core::types::ReasoningNode::Counterevidence {
697 id, contradicts, ..
698 } = node
699 {
700 let ce_conf = 1.0 - weakening;
701 out.push_str(&format!(
702 " evidence#{} (weakening={:.2}, remaining={:.2}): {}\n",
703 id, weakening, ce_conf, contradicts
704 ));
705 }
706 }
707 let combined: f64 = ce_nodes.iter().map(|(_, w)| 1.0 - w).product();
708 out.push_str(&format!(
709 " Combined counterevidence confidence: {:.3}\n",
710 combined
711 ));
712 } else {
713 out.push_str(" No counterevidence in graph.\n");
714 }
715
716 out
717}