1mod ppe_types;
53pub use ppe_types::*;
54
55mod ppe_sampling;
56use ppe_sampling::{
57 effective_sample_size, log_density, mh_propose, sample_prior, total_log_likelihood, uniform01,
58 xorshift64,
59};
60
61use std::collections::HashMap;
62
63impl ProbabilisticProgramEngine {
66 pub fn new(config: PpeEngineConfig) -> Self {
68 let seed = if config.seed == 0 {
69 0xDEAD_BEEF_CAFE_1234
70 } else {
71 config.seed
72 };
73 Self {
74 rng_state: seed,
75 config,
76 variables: HashMap::new(),
77 var_order: Vec::new(),
78 observations: HashMap::new(),
79 samples: HashMap::new(),
80 last_method: None,
81 last_result: None,
82 }
83 }
84
85 pub fn add_variable(&mut self, name: String, prior: PpePrior) -> VarId {
90 let mut id_bytes = [0u8; 16];
92 let a = xorshift64(&mut self.rng_state).to_le_bytes();
93 let b = xorshift64(&mut self.rng_state).to_le_bytes();
94 id_bytes[..8].copy_from_slice(&a);
95 id_bytes[8..].copy_from_slice(&b);
96 let id = PpeVarId(id_bytes);
97
98 let initial = sample_prior(&prior, &mut self.rng_state);
100 let var = ProbVar {
101 id,
102 name,
103 prior,
104 value: Some(initial),
105 };
106 self.variables.insert(id, var);
107 self.var_order.push(id);
108 id
109 }
110
111 pub fn observe(&mut self, var_id: VarId, value: f64) {
113 self.observations.insert(var_id, value);
114 if let Some(var) = self.variables.get_mut(&var_id) {
116 var.value = Some(value);
117 }
118 }
119
120 pub fn clear_observation(&mut self, var_id: VarId) {
122 self.observations.remove(&var_id);
123 }
124
125 pub fn sample(&mut self, method: PpeSamplingMethod) -> Result<PpeSampleResult, String> {
133 if self.variables.is_empty() {
134 return Err("No variables registered".to_string());
135 }
136 self.samples.clear();
137
138 let result = match method {
139 PpeSamplingMethod::MetropolisHastings => self.run_metropolis_hastings(),
140 PpeSamplingMethod::GibbsSampling => self.run_gibbs(),
141 PpeSamplingMethod::ImportanceSampling => self.run_importance_sampling(),
142 PpeSamplingMethod::RejectionSampling => self.run_rejection_sampling(),
143 };
144 self.last_method = Some(method);
145 self.last_result = Some(result.clone());
146 Ok(result)
147 }
148
149 fn run_metropolis_hastings(&mut self) -> PpeSampleResult {
152 let n_samples = self.config.n_samples;
153 let burn_in = self.config.burn_in;
154 let thinning = self.config.thinning.max(1);
155 let total_steps = burn_in + n_samples * thinning;
156
157 let var_ids: Vec<VarId> = self.var_order.clone();
159 for &id in &var_ids {
160 self.samples.insert(id, Vec::with_capacity(n_samples));
161 }
162
163 let mut current_values: HashMap<VarId, f64> = var_ids
165 .iter()
166 .filter_map(|&id| {
167 let v = self.variables.get(&id)?.value?;
168 Some((id, v))
169 })
170 .collect();
171
172 let mut accepted = 0usize;
173 let mut collected = 0usize;
174
175 for step in 0..total_steps {
176 let var_id = var_ids[step % var_ids.len()];
178 let prior = {
179 match self.variables.get(&var_id) {
180 Some(v) => v.prior.clone(),
181 None => continue,
182 }
183 };
184
185 let current_val = *current_values.get(&var_id).unwrap_or(&0.0);
186 let proposed_val = mh_propose(current_val, &prior, &mut self.rng_state);
187
188 let log_p_current = log_density(&prior, current_val);
190 let log_p_proposed = log_density(&prior, proposed_val);
191
192 let mut proposed_values = current_values.clone();
193 proposed_values.insert(var_id, proposed_val);
194
195 let ll_current =
196 total_log_likelihood(&self.variables, &self.observations, ¤t_values);
197 let ll_proposed =
198 total_log_likelihood(&self.variables, &self.observations, &proposed_values);
199
200 let log_ratio = (log_p_proposed + ll_proposed) - (log_p_current + ll_current);
201 let accept = log_ratio >= 0.0 || uniform01(&mut self.rng_state) < log_ratio.exp();
202
203 if accept {
204 current_values.insert(var_id, proposed_val);
205 accepted += 1;
206 }
207
208 if step >= burn_in && (step - burn_in).is_multiple_of(thinning) {
210 for &id in &var_ids {
211 let val = *current_values.get(&id).unwrap_or(&0.0);
212 if let Some(buf) = self.samples.get_mut(&id) {
213 buf.push(val);
214 }
215 }
216 collected += 1;
217 }
218 }
219
220 for (&id, &val) in ¤t_values {
222 if let Some(var) = self.variables.get_mut(&id) {
223 var.value = Some(val);
224 }
225 }
226
227 let acceptance_rate = if total_steps > 0 {
228 accepted as f64 / total_steps as f64
229 } else {
230 0.0
231 };
232
233 PpeSampleResult {
234 method: PpeSamplingMethod::MetropolisHastings,
235 total_steps,
236 accepted_samples: accepted,
237 acceptance_rate,
238 n_variables: var_ids.len(),
239 n_retained: collected,
240 }
241 }
242
243 fn run_gibbs(&mut self) -> PpeSampleResult {
246 let n_samples = self.config.n_samples;
247 let burn_in = self.config.burn_in;
248 let thinning = self.config.thinning.max(1);
249 let total_sweeps = burn_in + n_samples * thinning;
250
251 let var_ids: Vec<VarId> = self.var_order.clone();
252 for &id in &var_ids {
253 self.samples.insert(id, Vec::with_capacity(n_samples));
254 }
255
256 let mut current_values: HashMap<VarId, f64> = var_ids
257 .iter()
258 .filter_map(|&id| {
259 let v = self.variables.get(&id)?.value?;
260 Some((id, v))
261 })
262 .collect();
263
264 let mut collected = 0usize;
265
266 for sweep in 0..total_sweeps {
267 for &id in &var_ids {
269 if let Some(&obs) = self.observations.get(&id) {
271 current_values.insert(id, obs);
272 continue;
273 }
274 let prior = match self.variables.get(&id) {
276 Some(v) => v.prior.clone(),
277 None => continue,
278 };
279 let current_val = *current_values.get(&id).unwrap_or(&0.0);
283 let proposal = mh_propose(current_val, &prior, &mut self.rng_state);
284
285 let log_p_current = log_density(&prior, current_val);
286 let log_p_proposal = log_density(&prior, proposal);
287
288 let mut proposed_values = current_values.clone();
289 proposed_values.insert(id, proposal);
290
291 let ll_cur =
292 total_log_likelihood(&self.variables, &self.observations, ¤t_values);
293 let ll_prop =
294 total_log_likelihood(&self.variables, &self.observations, &proposed_values);
295
296 let log_ratio = (log_p_proposal + ll_prop) - (log_p_current + ll_cur);
297 if log_ratio >= 0.0 || uniform01(&mut self.rng_state) < log_ratio.exp() {
298 current_values.insert(id, proposal);
299 }
300 }
301
302 if sweep >= burn_in && (sweep - burn_in).is_multiple_of(thinning) {
303 for &id in &var_ids {
304 let val = *current_values.get(&id).unwrap_or(&0.0);
305 if let Some(buf) = self.samples.get_mut(&id) {
306 buf.push(val);
307 }
308 }
309 collected += 1;
310 }
311 }
312
313 for (&id, &val) in ¤t_values {
314 if let Some(var) = self.variables.get_mut(&id) {
315 var.value = Some(val);
316 }
317 }
318
319 PpeSampleResult {
320 method: PpeSamplingMethod::GibbsSampling,
321 total_steps: total_sweeps * var_ids.len(),
322 accepted_samples: total_sweeps * var_ids.len(),
323 acceptance_rate: 1.0,
324 n_variables: var_ids.len(),
325 n_retained: collected,
326 }
327 }
328
329 fn run_importance_sampling(&mut self) -> PpeSampleResult {
332 let n_samples = self.config.n_samples;
333 let var_ids: Vec<VarId> = self.var_order.clone();
334 for &id in &var_ids {
335 self.samples.insert(id, Vec::with_capacity(n_samples));
336 }
337
338 let n_proposal = (n_samples * 10).max(1000);
340 let mut log_weights: Vec<f64> = Vec::with_capacity(n_proposal);
341 let mut draws: Vec<HashMap<VarId, f64>> = Vec::with_capacity(n_proposal);
342
343 for _ in 0..n_proposal {
344 let mut draw: HashMap<VarId, f64> = HashMap::new();
345 for &id in &var_ids {
346 let prior = match self.variables.get(&id) {
347 Some(v) => v.prior.clone(),
348 None => continue,
349 };
350 let x = if let Some(&obs) = self.observations.get(&id) {
351 obs
352 } else {
353 sample_prior(&prior, &mut self.rng_state)
354 };
355 draw.insert(id, x);
356 }
357 let lw = total_log_likelihood(&self.variables, &self.observations, &draw);
358 log_weights.push(lw);
359 draws.push(draw);
360 }
361
362 let max_lw = log_weights
364 .iter()
365 .cloned()
366 .fold(f64::NEG_INFINITY, f64::max);
367 let weights: Vec<f64> = log_weights.iter().map(|&lw| (lw - max_lw).exp()).collect();
368 let total_w: f64 = weights.iter().sum();
369
370 let u_start = uniform01(&mut self.rng_state) / n_samples as f64;
372 let mut cumulative = 0.0_f64;
373 let mut draw_idx = 0usize;
374 let inv_n = 1.0 / n_samples as f64;
375
376 for s in 0..n_samples {
377 let threshold = u_start + s as f64 * inv_n;
378 while draw_idx < draws.len() - 1 {
380 let nw = if total_w > 1e-300 {
381 weights[draw_idx] / total_w
382 } else {
383 inv_n
384 };
385 if cumulative + nw >= threshold {
386 break;
387 }
388 cumulative += nw;
389 draw_idx += 1;
390 }
391 for &id in &var_ids {
392 let val = draws[draw_idx].get(&id).copied().unwrap_or(0.0);
393 if let Some(buf) = self.samples.get_mut(&id) {
394 buf.push(val);
395 }
396 }
397 }
398
399 PpeSampleResult {
400 method: PpeSamplingMethod::ImportanceSampling,
401 total_steps: n_proposal,
402 accepted_samples: n_samples,
403 acceptance_rate: n_samples as f64 / n_proposal as f64,
404 n_variables: var_ids.len(),
405 n_retained: n_samples,
406 }
407 }
408
409 fn run_rejection_sampling(&mut self) -> PpeSampleResult {
412 let n_samples = self.config.n_samples;
413 let var_ids: Vec<VarId> = self.var_order.clone();
414 for &id in &var_ids {
415 self.samples.insert(id, Vec::with_capacity(n_samples));
416 }
417
418 let mut collected = 0usize;
419 let mut total_attempts = 0usize;
420 let max_attempts = n_samples * 10_000;
421
422 while collected < n_samples && total_attempts < max_attempts {
425 total_attempts += 1;
426 let mut candidate: HashMap<VarId, f64> = HashMap::new();
427
428 for &id in &var_ids {
429 if let Some(&obs) = self.observations.get(&id) {
430 candidate.insert(id, obs);
431 } else if let Some(var) = self.variables.get(&id) {
432 let x = sample_prior(&var.prior, &mut self.rng_state);
433 candidate.insert(id, x);
434 }
435 }
436
437 let ll = total_log_likelihood(&self.variables, &self.observations, &candidate);
438
439 let accept_prob = ll.exp().min(1.0);
441 if uniform01(&mut self.rng_state) < accept_prob {
442 for &id in &var_ids {
443 let val = candidate.get(&id).copied().unwrap_or(0.0);
444 if let Some(buf) = self.samples.get_mut(&id) {
445 buf.push(val);
446 }
447 }
448 collected += 1;
449 }
450 }
451
452 while collected < n_samples {
454 for &id in &var_ids {
455 if let Some(var) = self.variables.get(&id) {
456 let x = if let Some(&obs) = self.observations.get(&id) {
457 obs
458 } else {
459 sample_prior(&var.prior, &mut self.rng_state)
460 };
461 if let Some(buf) = self.samples.get_mut(&id) {
462 buf.push(x);
463 }
464 }
465 }
466 collected += 1;
467 }
468
469 let acceptance_rate = if total_attempts > 0 {
470 collected as f64 / total_attempts as f64
471 } else {
472 0.0
473 };
474
475 PpeSampleResult {
476 method: PpeSamplingMethod::RejectionSampling,
477 total_steps: total_attempts,
478 accepted_samples: collected,
479 acceptance_rate,
480 n_variables: var_ids.len(),
481 n_retained: collected,
482 }
483 }
484
485 pub fn posterior_mean(&self, var_id: VarId) -> Option<f64> {
489 let samples = self.samples.get(&var_id)?;
490 if samples.is_empty() {
491 return None;
492 }
493 Some(samples.iter().sum::<f64>() / samples.len() as f64)
494 }
495
496 pub fn posterior_std(&self, var_id: VarId) -> Option<f64> {
498 let samples = self.samples.get(&var_id)?;
499 let n = samples.len();
500 if n < 2 {
501 return None;
502 }
503 let mean = samples.iter().sum::<f64>() / n as f64;
504 let variance = samples.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / (n - 1) as f64;
505 Some(variance.sqrt())
506 }
507
508 pub fn credible_interval(&self, var_id: VarId, alpha: f64) -> Option<(f64, f64)> {
512 let samples = self.samples.get(&var_id)?;
513 if samples.is_empty() {
514 return None;
515 }
516 let mut sorted = samples.clone();
517 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
518 let n = sorted.len();
519 let lo_idx = ((alpha / 2.0) * n as f64) as usize;
520 let hi_idx = (((1.0 - alpha / 2.0) * n as f64) as usize).min(n - 1);
521 Some((sorted[lo_idx], sorted[hi_idx]))
522 }
523
524 pub fn log_likelihood(&self, var_id: VarId, value: f64) -> f64 {
526 match self.variables.get(&var_id) {
527 Some(var) => log_density(&var.prior, value),
528 None => f64::NEG_INFINITY,
529 }
530 }
531
532 pub fn marginal_distribution(&self, var_id: VarId, n_bins: usize) -> Vec<(f64, f64)> {
536 let samples = match self.samples.get(&var_id) {
537 Some(s) if !s.is_empty() => s,
538 _ => return Vec::new(),
539 };
540 if n_bins == 0 {
541 return Vec::new();
542 }
543
544 let min = samples.iter().cloned().fold(f64::INFINITY, f64::min);
545 let max = samples.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
546
547 if (max - min).abs() < 1e-300 {
548 return vec![(min, samples.len() as f64)];
549 }
550
551 let bin_width = (max - min) / n_bins as f64;
552 let mut counts = vec![0u64; n_bins];
553
554 for &x in samples {
555 let idx = ((x - min) / bin_width) as usize;
556 let idx = idx.min(n_bins - 1);
557 counts[idx] += 1;
558 }
559
560 let n = samples.len() as f64;
561 counts
562 .iter()
563 .enumerate()
564 .map(|(i, &c)| {
565 let centre = min + (i as f64 + 0.5) * bin_width;
566 let freq = c as f64 / n;
567 (centre, freq)
568 })
569 .collect()
570 }
571
572 pub fn sampling_stats(&self) -> PpeSamplingStats {
574 let n_variables = self.variables.len();
575 let n_observed = self.observations.len();
576 let total_samples: usize = self.samples.values().map(Vec::len).sum();
577 let has_samples = total_samples > 0;
578
579 let min_ess = self
580 .samples
581 .values()
582 .map(|s| effective_sample_size(s))
583 .fold(f64::INFINITY, f64::min);
584 let min_ess = if min_ess.is_infinite() { 0.0 } else { min_ess };
585
586 PpeSamplingStats {
587 n_variables,
588 n_observed,
589 total_samples,
590 has_samples,
591 last_method: self.last_method,
592 min_ess,
593 }
594 }
595
596 pub fn config(&self) -> &PpeEngineConfig {
600 &self.config
601 }
602
603 pub fn get_variable(&self, var_id: VarId) -> Option<&ProbVar> {
605 self.variables.get(&var_id)
606 }
607
608 pub fn n_variables(&self) -> usize {
610 self.variables.len()
611 }
612
613 pub fn n_samples(&self, var_id: VarId) -> usize {
616 self.samples.get(&var_id).map(Vec::len).unwrap_or(0)
617 }
618
619 pub fn raw_samples(&self, var_id: VarId) -> Option<&[f64]> {
621 self.samples.get(&var_id).map(Vec::as_slice)
622 }
623
624 pub fn last_result(&self) -> Option<&PpeSampleResult> {
626 self.last_result.as_ref()
627 }
628}
629
630#[cfg(test)]
633mod tests {
634 use super::ppe_sampling::{box_muller, lgamma, sample_gamma, sample_standard_normal};
635 use super::*;
636
637 fn make_engine(seed: u64) -> ProbabilisticProgramEngine {
640 ProbabilisticProgramEngine::new(PpeEngineConfig {
641 n_samples: 300,
642 burn_in: 50,
643 thinning: 1,
644 seed,
645 })
646 }
647
648 fn make_small(seed: u64) -> ProbabilisticProgramEngine {
649 ProbabilisticProgramEngine::new(PpeEngineConfig {
650 n_samples: 100,
651 burn_in: 20,
652 thinning: 1,
653 seed,
654 })
655 }
656
657 #[test]
660 fn var_id_unique() {
661 let mut engine = make_engine(1);
662 let a = engine.add_variable(
663 "a".into(),
664 PpePrior::Normal {
665 mean: 0.0,
666 std: 1.0,
667 },
668 );
669 let b = engine.add_variable(
670 "b".into(),
671 PpePrior::Normal {
672 mean: 0.0,
673 std: 1.0,
674 },
675 );
676 assert_ne!(a, b);
677 }
678
679 #[test]
680 fn var_id_equality() {
681 let id = PpeVarId([1u8; 16]);
682 assert_eq!(id, PpeVarId([1u8; 16]));
683 assert_ne!(id, PpeVarId([2u8; 16]));
684 }
685
686 #[test]
689 fn add_variable_returns_id() {
690 let mut e = make_engine(2);
691 let id = e.add_variable(
692 "x".into(),
693 PpePrior::Uniform {
694 low: 0.0,
695 high: 1.0,
696 },
697 );
698 assert!(e.get_variable(id).is_some());
699 }
700
701 #[test]
702 fn add_multiple_variables() {
703 let mut e = make_engine(3);
704 for i in 0..10 {
705 e.add_variable(format!("v{i}"), PpePrior::Exponential { rate: 1.0 });
706 }
707 assert_eq!(e.n_variables(), 10);
708 }
709
710 #[test]
711 fn variable_name_preserved() {
712 let mut e = make_engine(4);
713 let id = e.add_variable("my_var".into(), PpePrior::Bernoulli { p: 0.3 });
714 assert_eq!(
715 e.get_variable(id)
716 .expect("test: variable should be registered")
717 .name,
718 "my_var"
719 );
720 }
721
722 #[test]
723 fn variable_has_initial_value() {
724 let mut e = make_engine(5);
725 let id = e.add_variable(
726 "v".into(),
727 PpePrior::Normal {
728 mean: 5.0,
729 std: 1.0,
730 },
731 );
732 assert!(e
733 .get_variable(id)
734 .expect("test: variable should be registered")
735 .value
736 .is_some());
737 }
738
739 #[test]
742 fn observe_sets_value() {
743 let mut e = make_engine(6);
744 let id = e.add_variable(
745 "mu".into(),
746 PpePrior::Normal {
747 mean: 0.0,
748 std: 1.0,
749 },
750 );
751 e.observe(id, 3.7);
752 assert_eq!(
753 e.get_variable(id)
754 .expect("test: variable should be registered")
755 .value,
756 Some(3.7)
757 );
758 }
759
760 #[test]
761 fn clear_observation_removes_obs() {
762 let mut e = make_engine(7);
763 let id = e.add_variable(
764 "mu".into(),
765 PpePrior::Normal {
766 mean: 0.0,
767 std: 1.0,
768 },
769 );
770 e.observe(id, 1.0);
771 e.clear_observation(id);
772 let stats = e.sampling_stats();
774 assert_eq!(stats.n_observed, 0);
775 }
776
777 #[test]
778 fn observe_nonexistent_var() {
779 let mut e = make_engine(8);
780 e.observe(PpeVarId([0u8; 16]), 1.0);
782 }
783
784 #[test]
787 fn sample_no_vars_returns_error() {
788 let mut e = make_engine(9);
789 assert!(e.sample(PpeSamplingMethod::MetropolisHastings).is_err());
790 }
791
792 #[test]
795 fn mh_produces_samples() {
796 let mut e = make_engine(10);
797 e.add_variable(
798 "x".into(),
799 PpePrior::Normal {
800 mean: 0.0,
801 std: 1.0,
802 },
803 );
804 let res = e
805 .sample(PpeSamplingMethod::MetropolisHastings)
806 .expect("test: sampling should succeed");
807 assert!(res.accepted_samples > 0);
808 assert_eq!(res.method, PpeSamplingMethod::MetropolisHastings);
809 }
810
811 #[test]
812 fn mh_correct_sample_count() {
813 let mut e = make_engine(11);
814 let id = e.add_variable(
815 "x".into(),
816 PpePrior::Normal {
817 mean: 0.0,
818 std: 1.0,
819 },
820 );
821 e.sample(PpeSamplingMethod::MetropolisHastings)
822 .expect("test: sampling should succeed");
823 assert_eq!(e.n_samples(id), e.config().n_samples);
824 }
825
826 #[test]
827 fn mh_normal_mean_near_observation() {
828 let mut e = make_engine(12);
829 let id = e.add_variable(
830 "mu".into(),
831 PpePrior::Normal {
832 mean: 0.0,
833 std: 5.0,
834 },
835 );
836 e.observe(id, 3.0);
837 e.sample(PpeSamplingMethod::MetropolisHastings)
838 .expect("test: sampling should succeed");
839 let mean = e
840 .posterior_mean(id)
841 .expect("test: posterior mean should be available");
842 assert!((mean - 3.0).abs() < 1.5, "mean={mean}");
844 }
845
846 #[test]
847 fn mh_acceptance_rate_in_range() {
848 let mut e = make_engine(13);
849 e.add_variable(
850 "x".into(),
851 PpePrior::Normal {
852 mean: 0.0,
853 std: 1.0,
854 },
855 );
856 let res = e
857 .sample(PpeSamplingMethod::MetropolisHastings)
858 .expect("test: sampling should succeed");
859 assert!(res.acceptance_rate >= 0.0);
860 assert!(res.acceptance_rate <= 1.0);
861 }
862
863 #[test]
866 fn gibbs_produces_samples() {
867 let mut e = make_engine(14);
868 e.add_variable(
869 "y".into(),
870 PpePrior::Uniform {
871 low: -1.0,
872 high: 1.0,
873 },
874 );
875 let res = e
876 .sample(PpeSamplingMethod::GibbsSampling)
877 .expect("test: sampling should succeed");
878 assert!(res.n_retained > 0);
879 assert_eq!(res.method, PpeSamplingMethod::GibbsSampling);
880 }
881
882 #[test]
883 fn gibbs_correct_sample_count() {
884 let mut e = make_engine(15);
885 let id = e.add_variable(
886 "y".into(),
887 PpePrior::Uniform {
888 low: 0.0,
889 high: 1.0,
890 },
891 );
892 e.sample(PpeSamplingMethod::GibbsSampling)
893 .expect("test: sampling should succeed");
894 assert_eq!(e.n_samples(id), e.config().n_samples);
895 }
896
897 #[test]
898 fn gibbs_observed_var_clamped() {
899 let mut e = make_engine(16);
900 let id = e.add_variable(
901 "y".into(),
902 PpePrior::Normal {
903 mean: 0.0,
904 std: 1.0,
905 },
906 );
907 e.observe(id, 7.7);
908 e.sample(PpeSamplingMethod::GibbsSampling)
909 .expect("test: sampling should succeed");
910 let samples = e
912 .raw_samples(id)
913 .expect("test: raw samples should exist after sampling");
914 for &s in samples {
915 assert!((s - 7.7).abs() < 1e-9, "s={s}");
916 }
917 }
918
919 #[test]
922 fn importance_produces_samples() {
923 let mut e = make_engine(17);
924 e.add_variable(
925 "z".into(),
926 PpePrior::Normal {
927 mean: 1.0,
928 std: 2.0,
929 },
930 );
931 let res = e
932 .sample(PpeSamplingMethod::ImportanceSampling)
933 .expect("test: sampling should succeed");
934 assert!(res.n_retained > 0);
935 }
936
937 #[test]
938 fn importance_sample_count_correct() {
939 let mut e = make_engine(18);
940 let id = e.add_variable(
941 "z".into(),
942 PpePrior::Normal {
943 mean: 0.0,
944 std: 1.0,
945 },
946 );
947 e.sample(PpeSamplingMethod::ImportanceSampling)
948 .expect("test: sampling should succeed");
949 assert_eq!(e.n_samples(id), e.config().n_samples);
950 }
951
952 #[test]
955 fn rejection_produces_samples() {
956 let mut e = make_small(19);
957 e.add_variable(
958 "r".into(),
959 PpePrior::Normal {
960 mean: 0.0,
961 std: 1.0,
962 },
963 );
964 let res = e
965 .sample(PpeSamplingMethod::RejectionSampling)
966 .expect("test: sampling should succeed");
967 assert!(res.n_retained > 0);
968 }
969
970 #[test]
971 fn rejection_sample_count_correct() {
972 let mut e = make_small(20);
973 let id = e.add_variable(
974 "r".into(),
975 PpePrior::Normal {
976 mean: 0.0,
977 std: 1.0,
978 },
979 );
980 e.sample(PpeSamplingMethod::RejectionSampling)
981 .expect("test: sampling should succeed");
982 assert_eq!(e.n_samples(id), e.config().n_samples);
983 }
984
985 #[test]
988 fn posterior_mean_none_before_sampling() {
989 let mut e = make_engine(21);
990 let id = e.add_variable(
991 "m".into(),
992 PpePrior::Normal {
993 mean: 0.0,
994 std: 1.0,
995 },
996 );
997 assert!(e.posterior_mean(id).is_none());
998 }
999
1000 #[test]
1001 fn posterior_mean_finite_after_mh() {
1002 let mut e = make_engine(22);
1003 let id = e.add_variable(
1004 "m".into(),
1005 PpePrior::Normal {
1006 mean: 0.0,
1007 std: 1.0,
1008 },
1009 );
1010 e.sample(PpeSamplingMethod::MetropolisHastings)
1011 .expect("test: sampling should succeed");
1012 let m = e
1013 .posterior_mean(id)
1014 .expect("test: posterior mean should be available");
1015 assert!(m.is_finite());
1016 }
1017
1018 #[test]
1019 fn posterior_mean_uniform_midpoint() {
1020 let mut e = make_engine(23);
1021 let id = e.add_variable(
1022 "u".into(),
1023 PpePrior::Uniform {
1024 low: 0.0,
1025 high: 2.0,
1026 },
1027 );
1028 e.sample(PpeSamplingMethod::MetropolisHastings)
1029 .expect("test: sampling should succeed");
1030 let m = e
1031 .posterior_mean(id)
1032 .expect("test: posterior mean should be available");
1033 assert!((m - 1.0).abs() < 0.5, "mean={m}");
1035 }
1036
1037 #[test]
1038 fn posterior_mean_bernoulli() {
1039 let mut e = make_engine(24);
1040 let id = e.add_variable("b".into(), PpePrior::Bernoulli { p: 0.7 });
1041 e.sample(PpeSamplingMethod::ImportanceSampling)
1042 .expect("test: sampling should succeed");
1043 let m = e
1044 .posterior_mean(id)
1045 .expect("test: posterior mean should be available");
1046 assert!((0.0..=1.0).contains(&m), "mean={m}");
1047 }
1048
1049 #[test]
1052 fn posterior_std_none_before_sampling() {
1053 let mut e = make_engine(25);
1054 let id = e.add_variable(
1055 "s".into(),
1056 PpePrior::Normal {
1057 mean: 0.0,
1058 std: 1.0,
1059 },
1060 );
1061 assert!(e.posterior_std(id).is_none());
1062 }
1063
1064 #[test]
1065 fn posterior_std_non_negative() {
1066 let mut e = make_engine(26);
1067 let id = e.add_variable(
1068 "s".into(),
1069 PpePrior::Normal {
1070 mean: 0.0,
1071 std: 2.0,
1072 },
1073 );
1074 e.sample(PpeSamplingMethod::MetropolisHastings)
1075 .expect("test: sampling should succeed");
1076 let std = e
1077 .posterior_std(id)
1078 .expect("test: posterior std should be available");
1079 assert!(std >= 0.0);
1080 }
1081
1082 #[test]
1085 fn credible_interval_none_before_sampling() {
1086 let mut e = make_engine(27);
1087 let id = e.add_variable(
1088 "c".into(),
1089 PpePrior::Normal {
1090 mean: 0.0,
1091 std: 1.0,
1092 },
1093 );
1094 assert!(e.credible_interval(id, 0.05).is_none());
1095 }
1096
1097 #[test]
1098 fn credible_interval_lower_lt_upper() {
1099 let mut e = make_engine(28);
1100 let id = e.add_variable(
1101 "c".into(),
1102 PpePrior::Normal {
1103 mean: 0.0,
1104 std: 1.0,
1105 },
1106 );
1107 e.sample(PpeSamplingMethod::MetropolisHastings)
1108 .expect("test: sampling should succeed");
1109 let (lo, hi) = e
1110 .credible_interval(id, 0.05)
1111 .expect("test: credible interval should be available");
1112 assert!(lo <= hi, "lo={lo}, hi={hi}");
1113 }
1114
1115 #[test]
1116 fn credible_interval_50pct() {
1117 let mut e = make_engine(29);
1118 let id = e.add_variable(
1119 "c".into(),
1120 PpePrior::Uniform {
1121 low: 0.0,
1122 high: 1.0,
1123 },
1124 );
1125 e.sample(PpeSamplingMethod::GibbsSampling)
1126 .expect("test: sampling should succeed");
1127 let (lo, hi) = e
1128 .credible_interval(id, 0.5)
1129 .expect("test: credible interval should be available");
1130 assert!(lo >= 0.0 && hi <= 1.0);
1131 assert!(lo <= hi);
1132 }
1133
1134 #[test]
1137 fn log_likelihood_normal_peak_at_mean() {
1138 let mut e = make_engine(30);
1139 let id = e.add_variable(
1140 "ll".into(),
1141 PpePrior::Normal {
1142 mean: 2.0,
1143 std: 1.0,
1144 },
1145 );
1146 let at_mean = e.log_likelihood(id, 2.0);
1147 let off = e.log_likelihood(id, 5.0);
1148 assert!(at_mean > off);
1149 }
1150
1151 #[test]
1152 fn log_likelihood_uniform_constant_inside() {
1153 let mut e = make_engine(31);
1154 let id = e.add_variable(
1155 "ll".into(),
1156 PpePrior::Uniform {
1157 low: 0.0,
1158 high: 1.0,
1159 },
1160 );
1161 let a = e.log_likelihood(id, 0.2);
1162 let b = e.log_likelihood(id, 0.8);
1163 assert!((a - b).abs() < 1e-9);
1164 }
1165
1166 #[test]
1167 fn log_likelihood_uniform_neg_inf_outside() {
1168 let mut e = make_engine(32);
1169 let id = e.add_variable(
1170 "ll".into(),
1171 PpePrior::Uniform {
1172 low: 0.0,
1173 high: 1.0,
1174 },
1175 );
1176 assert_eq!(e.log_likelihood(id, -1.0), f64::NEG_INFINITY);
1177 assert_eq!(e.log_likelihood(id, 2.0), f64::NEG_INFINITY);
1178 }
1179
1180 #[test]
1181 fn log_likelihood_exponential_positive() {
1182 let mut e = make_engine(33);
1183 let id = e.add_variable("ll".into(), PpePrior::Exponential { rate: 1.0 });
1184 let v = e.log_likelihood(id, 1.0);
1185 assert!(v.is_finite());
1186 }
1187
1188 #[test]
1189 fn log_likelihood_exponential_neg_inf_outside() {
1190 let mut e = make_engine(34);
1191 let id = e.add_variable("ll".into(), PpePrior::Exponential { rate: 1.0 });
1192 assert_eq!(e.log_likelihood(id, -0.1), f64::NEG_INFINITY);
1193 }
1194
1195 #[test]
1196 fn log_likelihood_beta_inside_unit_interval() {
1197 let mut e = make_engine(35);
1198 let id = e.add_variable(
1199 "ll".into(),
1200 PpePrior::Beta {
1201 alpha: 2.0,
1202 beta: 2.0,
1203 },
1204 );
1205 let v = e.log_likelihood(id, 0.5);
1206 assert!(v.is_finite());
1207 }
1208
1209 #[test]
1210 fn log_likelihood_beta_boundary_neg_inf() {
1211 let mut e = make_engine(36);
1212 let id = e.add_variable(
1213 "ll".into(),
1214 PpePrior::Beta {
1215 alpha: 2.0,
1216 beta: 2.0,
1217 },
1218 );
1219 assert_eq!(e.log_likelihood(id, 0.0), f64::NEG_INFINITY);
1220 assert_eq!(e.log_likelihood(id, 1.0), f64::NEG_INFINITY);
1221 }
1222
1223 #[test]
1224 fn log_likelihood_nonexistent_var() {
1225 let e = make_engine(37);
1226 assert_eq!(
1227 e.log_likelihood(PpeVarId([0u8; 16]), 1.0),
1228 f64::NEG_INFINITY
1229 );
1230 }
1231
1232 #[test]
1235 fn marginal_empty_before_sampling() {
1236 let mut e = make_engine(38);
1237 let id = e.add_variable(
1238 "m".into(),
1239 PpePrior::Normal {
1240 mean: 0.0,
1241 std: 1.0,
1242 },
1243 );
1244 assert!(e.marginal_distribution(id, 10).is_empty());
1245 }
1246
1247 #[test]
1248 fn marginal_correct_bin_count() {
1249 let mut e = make_engine(39);
1250 let id = e.add_variable(
1251 "m".into(),
1252 PpePrior::Normal {
1253 mean: 0.0,
1254 std: 1.0,
1255 },
1256 );
1257 e.sample(PpeSamplingMethod::MetropolisHastings)
1258 .expect("test: sampling should succeed");
1259 let hist = e.marginal_distribution(id, 20);
1260 assert_eq!(hist.len(), 20);
1261 }
1262
1263 #[test]
1264 fn marginal_frequencies_sum_to_one() {
1265 let mut e = make_engine(40);
1266 let id = e.add_variable(
1267 "m".into(),
1268 PpePrior::Uniform {
1269 low: 0.0,
1270 high: 1.0,
1271 },
1272 );
1273 e.sample(PpeSamplingMethod::GibbsSampling)
1274 .expect("test: sampling should succeed");
1275 let hist = e.marginal_distribution(id, 10);
1276 let total: f64 = hist.iter().map(|(_, f)| f).sum();
1277 assert!((total - 1.0).abs() < 1e-9, "total={total}");
1278 }
1279
1280 #[test]
1281 fn marginal_zero_bins_returns_empty() {
1282 let mut e = make_engine(41);
1283 let id = e.add_variable(
1284 "m".into(),
1285 PpePrior::Normal {
1286 mean: 0.0,
1287 std: 1.0,
1288 },
1289 );
1290 e.sample(PpeSamplingMethod::MetropolisHastings)
1291 .expect("test: sampling should succeed");
1292 assert!(e.marginal_distribution(id, 0).is_empty());
1293 }
1294
1295 #[test]
1298 fn sampling_stats_initial_state() {
1299 let mut e = make_engine(42);
1300 e.add_variable(
1301 "x".into(),
1302 PpePrior::Normal {
1303 mean: 0.0,
1304 std: 1.0,
1305 },
1306 );
1307 let stats = e.sampling_stats();
1308 assert_eq!(stats.n_variables, 1);
1309 assert!(!stats.has_samples);
1310 }
1311
1312 #[test]
1313 fn sampling_stats_after_mh() {
1314 let mut e = make_engine(43);
1315 e.add_variable(
1316 "x".into(),
1317 PpePrior::Normal {
1318 mean: 0.0,
1319 std: 1.0,
1320 },
1321 );
1322 e.sample(PpeSamplingMethod::MetropolisHastings)
1323 .expect("test: sampling should succeed");
1324 let stats = e.sampling_stats();
1325 assert!(stats.has_samples);
1326 assert_eq!(
1327 stats.last_method,
1328 Some(PpeSamplingMethod::MetropolisHastings)
1329 );
1330 }
1331
1332 #[test]
1333 fn sampling_stats_total_samples() {
1334 let mut e = make_engine(44);
1335 e.add_variable(
1336 "a".into(),
1337 PpePrior::Normal {
1338 mean: 0.0,
1339 std: 1.0,
1340 },
1341 );
1342 e.add_variable(
1343 "b".into(),
1344 PpePrior::Normal {
1345 mean: 1.0,
1346 std: 1.0,
1347 },
1348 );
1349 e.sample(PpeSamplingMethod::GibbsSampling)
1350 .expect("test: sampling should succeed");
1351 let stats = e.sampling_stats();
1352 assert_eq!(stats.total_samples, 2 * e.config().n_samples);
1353 }
1354
1355 #[test]
1356 fn sampling_stats_min_ess_positive() {
1357 let mut e = make_engine(45);
1358 e.add_variable(
1359 "x".into(),
1360 PpePrior::Normal {
1361 mean: 0.0,
1362 std: 1.0,
1363 },
1364 );
1365 e.sample(PpeSamplingMethod::MetropolisHastings)
1366 .expect("test: sampling should succeed");
1367 let stats = e.sampling_stats();
1368 assert!(stats.min_ess >= 0.0);
1369 }
1370
1371 #[test]
1374 fn last_result_none_before_sampling() {
1375 let e = make_engine(46);
1376 assert!(e.last_result().is_none());
1377 }
1378
1379 #[test]
1380 fn last_result_after_sampling() {
1381 let mut e = make_engine(47);
1382 e.add_variable(
1383 "x".into(),
1384 PpePrior::Normal {
1385 mean: 0.0,
1386 std: 1.0,
1387 },
1388 );
1389 e.sample(PpeSamplingMethod::ImportanceSampling)
1390 .expect("test: sampling should succeed");
1391 assert!(e.last_result().is_some());
1392 }
1393
1394 #[test]
1397 fn categorical_samples_valid_indices() {
1398 let mut e = make_small(48);
1399 let probs = vec![0.2, 0.5, 0.3];
1400 let id = e.add_variable("cat".into(), PpePrior::Categorical { probs });
1401 e.sample(PpeSamplingMethod::ImportanceSampling)
1402 .expect("test: sampling should succeed");
1403 let samples = e
1404 .raw_samples(id)
1405 .expect("test: raw samples should exist after sampling");
1406 for &s in samples {
1407 assert!((0.0..3.0).contains(&s), "s={s}");
1408 }
1409 }
1410
1411 #[test]
1412 fn exponential_samples_non_negative() {
1413 let mut e = make_small(49);
1414 let id = e.add_variable("exp".into(), PpePrior::Exponential { rate: 2.0 });
1415 e.sample(PpeSamplingMethod::GibbsSampling)
1416 .expect("test: sampling should succeed");
1417 let samples = e
1418 .raw_samples(id)
1419 .expect("test: raw samples should exist after sampling");
1420 for &s in samples {
1421 assert!(s >= 0.0, "s={s}");
1422 }
1423 }
1424
1425 #[test]
1426 fn beta_samples_in_unit_interval() {
1427 let mut e = make_small(50);
1428 let id = e.add_variable(
1429 "beta".into(),
1430 PpePrior::Beta {
1431 alpha: 2.0,
1432 beta: 5.0,
1433 },
1434 );
1435 e.sample(PpeSamplingMethod::MetropolisHastings)
1436 .expect("test: sampling should succeed");
1437 let samples = e
1438 .raw_samples(id)
1439 .expect("test: raw samples should exist after sampling");
1440 for &s in samples {
1441 assert!((0.0..=1.0).contains(&s), "s={s}");
1442 }
1443 }
1444
1445 #[test]
1446 fn bernoulli_samples_zero_or_one() {
1447 let mut e = make_small(51);
1448 let id = e.add_variable("bern".into(), PpePrior::Bernoulli { p: 0.6 });
1449 e.sample(PpeSamplingMethod::MetropolisHastings)
1450 .expect("test: sampling should succeed");
1451 let samples = e
1452 .raw_samples(id)
1453 .expect("test: raw samples should exist after sampling");
1454 for &s in samples {
1455 assert!(s == 0.0 || s == 1.0, "s={s}");
1456 }
1457 }
1458
1459 #[test]
1462 fn xorshift64_not_zero() {
1463 let mut state = 12345678u64;
1464 let r = xorshift64(&mut state);
1465 assert_ne!(r, 0);
1466 }
1467
1468 #[test]
1469 fn xorshift64_different_successive_values() {
1470 let mut state = 99999u64;
1471 let a = xorshift64(&mut state);
1472 let b = xorshift64(&mut state);
1473 assert_ne!(a, b);
1474 }
1475
1476 #[test]
1477 fn uniform01_in_range() {
1478 let mut state = 777u64;
1479 for _ in 0..1000 {
1480 let u = uniform01(&mut state);
1481 assert!((0.0..1.0).contains(&u), "u={u}");
1482 }
1483 }
1484
1485 #[test]
1486 fn box_muller_finite() {
1487 let bm = box_muller(0.5, 0.3);
1488 assert!(bm.is_finite());
1489 }
1490
1491 #[test]
1492 fn sample_standard_normal_finite() {
1493 let mut state = 4242u64;
1494 for _ in 0..100 {
1495 let n = sample_standard_normal(&mut state);
1496 assert!(n.is_finite(), "n={n}");
1497 }
1498 }
1499
1500 #[test]
1501 fn lgamma_positive_values() {
1502 assert!(lgamma(1.0).is_finite());
1503 assert!(lgamma(2.0).is_finite());
1504 assert!(lgamma(0.5).is_finite());
1505 }
1506
1507 #[test]
1508 fn lgamma_negative_inf_for_zero() {
1509 let v = lgamma(0.0);
1511 assert!(v.is_infinite() && v > 0.0, "v={v}");
1512 }
1513
1514 #[test]
1515 fn sample_gamma_positive() {
1516 let mut state = 123456u64;
1517 for shape in [0.5, 1.0, 2.0, 5.0] {
1518 let g = sample_gamma(shape, &mut state);
1519 assert!(g >= 0.0, "shape={shape}, g={g}");
1520 }
1521 }
1522
1523 #[test]
1526 fn thinning_respected() {
1527 let mut e = ProbabilisticProgramEngine::new(PpeEngineConfig {
1528 n_samples: 50,
1529 burn_in: 10,
1530 thinning: 3,
1531 seed: 9876,
1532 });
1533 let id = e.add_variable(
1534 "x".into(),
1535 PpePrior::Normal {
1536 mean: 0.0,
1537 std: 1.0,
1538 },
1539 );
1540 e.sample(PpeSamplingMethod::MetropolisHastings)
1541 .expect("test: sampling should succeed");
1542 assert_eq!(e.n_samples(id), 50);
1543 }
1544
1545 #[test]
1548 fn multiple_vars_all_sampled() {
1549 let mut e = make_engine(60);
1550 let ids: Vec<_> = (0..5)
1551 .map(|i| {
1552 e.add_variable(
1553 format!("v{i}"),
1554 PpePrior::Normal {
1555 mean: i as f64,
1556 std: 1.0,
1557 },
1558 )
1559 })
1560 .collect();
1561 e.sample(PpeSamplingMethod::GibbsSampling)
1562 .expect("test: sampling should succeed");
1563 for id in ids {
1564 assert_eq!(e.n_samples(id), e.config().n_samples);
1565 }
1566 }
1567
1568 #[test]
1569 fn multiple_observations() {
1570 let mut e = make_engine(61);
1571 let a = e.add_variable(
1572 "a".into(),
1573 PpePrior::Normal {
1574 mean: 0.0,
1575 std: 1.0,
1576 },
1577 );
1578 let b = e.add_variable(
1579 "b".into(),
1580 PpePrior::Normal {
1581 mean: 0.0,
1582 std: 1.0,
1583 },
1584 );
1585 e.observe(a, 1.0);
1586 e.observe(b, -1.0);
1587 e.sample(PpeSamplingMethod::MetropolisHastings)
1588 .expect("test: sampling should succeed");
1589 let stats = e.sampling_stats();
1590 assert_eq!(stats.n_observed, 2);
1591 }
1592
1593 #[test]
1596 fn credible_interval_full_alpha() {
1597 let mut e = make_engine(62);
1598 let id = e.add_variable(
1599 "x".into(),
1600 PpePrior::Normal {
1601 mean: 0.0,
1602 std: 1.0,
1603 },
1604 );
1605 e.sample(PpeSamplingMethod::MetropolisHastings)
1606 .expect("test: sampling should succeed");
1607 let (lo, hi) = e
1609 .credible_interval(id, 0.0)
1610 .expect("test: credible interval should be available");
1611 assert!(lo <= hi);
1612 }
1613
1614 #[test]
1615 fn raw_samples_none_before_sampling() {
1616 let mut e = make_engine(63);
1617 let id = e.add_variable(
1618 "x".into(),
1619 PpePrior::Normal {
1620 mean: 0.0,
1621 std: 1.0,
1622 },
1623 );
1624 assert!(e.raw_samples(id).is_none());
1625 }
1626
1627 #[test]
1628 fn default_config() {
1629 let cfg = PpeEngineConfig::default();
1630 assert!(cfg.n_samples > 0);
1631 assert!(cfg.thinning > 0);
1632 assert!(cfg.seed > 0);
1633 }
1634
1635 #[test]
1636 fn posterior_std_single_sample_none() {
1637 let mut e = ProbabilisticProgramEngine::new(PpeEngineConfig {
1638 n_samples: 1,
1639 burn_in: 0,
1640 thinning: 1,
1641 seed: 777,
1642 });
1643 let id = e.add_variable(
1644 "x".into(),
1645 PpePrior::Normal {
1646 mean: 0.0,
1647 std: 1.0,
1648 },
1649 );
1650 e.sample(PpeSamplingMethod::RejectionSampling)
1651 .expect("test: sampling should succeed");
1652 assert!(e.posterior_std(id).is_none());
1654 }
1655}