1use super::error::{EconConvergence, EconStatus};
27
28pub const MAX_STATES: usize = 32;
30
31const ROW_SUM_TOLERANCE: f64 = 1e-9;
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum MarkovError {
37 InvalidInput,
39 InvalidTransitionMatrix,
42 InvalidState,
44 BufferTooSmall,
46 NonFinite,
48 NonConverged,
50}
51
52impl MarkovError {
53 pub fn to_status(self) -> EconStatus {
55 match self {
56 MarkovError::InvalidInput => EconStatus::InvalidInput,
57 MarkovError::InvalidTransitionMatrix => EconStatus::InvalidInput,
58 MarkovError::InvalidState => EconStatus::InvalidInput,
59 MarkovError::BufferTooSmall => EconStatus::BufferTooSmall,
60 MarkovError::NonFinite => EconStatus::NonFinite,
61 MarkovError::NonConverged => EconStatus::MaxIterations,
62 }
63 }
64}
65
66impl From<MarkovError> for EconStatus {
67 #[inline]
68 fn from(err: MarkovError) -> Self {
69 err.to_status()
70 }
71}
72
73#[derive(Debug, Clone, Copy)]
81struct SplitMix64 {
82 state: u64,
83}
84
85impl SplitMix64 {
86 #[inline]
87 fn new(seed: u64) -> Self {
88 Self { state: seed }
89 }
90
91 #[inline]
92 fn next_u64(&mut self) -> u64 {
93 self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15);
94 let mut z = self.state;
95 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
96 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
97 z ^ (z >> 31)
98 }
99
100 #[inline]
102 fn unit_open(&mut self) -> f64 {
103 let bits = self.next_u64() >> 11;
104 ((bits as f64) + 0.5) * (1.0 / ((1u64 << 53) as f64))
105 }
106}
107
108fn check_dim(n: usize) -> Result<(), MarkovError> {
114 if n == 0 || n > MAX_STATES {
115 return Err(MarkovError::InvalidInput);
116 }
117 Ok(())
118}
119
120#[inline]
122fn idx(row: usize, col: usize, n: usize) -> usize {
123 row * n + col
124}
125
126pub fn validate_transition_matrix(p: &[f64], n: usize) -> Result<(), MarkovError> {
135 check_dim(n)?;
136 if p.len() < n * n {
137 return Err(MarkovError::InvalidInput);
138 }
139 for row in 0..n {
140 let mut sum = 0.0f64;
141 for col in 0..n {
142 let entry = p[idx(row, col, n)];
143 if !entry.is_finite() {
144 return Err(MarkovError::InvalidTransitionMatrix);
145 }
146 if entry < 0.0 {
147 return Err(MarkovError::InvalidTransitionMatrix);
148 }
149 sum += entry;
150 }
151 if (sum - 1.0).abs() > ROW_SUM_TOLERANCE {
152 return Err(MarkovError::InvalidTransitionMatrix);
153 }
154 }
155 Ok(())
156}
157
158pub fn transition_probability(
160 p: &[f64],
161 n: usize,
162 from: usize,
163 to: usize,
164) -> Result<f64, MarkovError> {
165 check_dim(n)?;
166 if p.len() < n * n {
167 return Err(MarkovError::InvalidInput);
168 }
169 if from >= n || to >= n {
170 return Err(MarkovError::InvalidState);
171 }
172 Ok(p[idx(from, to, n)])
173}
174
175pub fn expected_holding_time(p: &[f64], n: usize, state: usize) -> Result<f64, MarkovError> {
181 check_dim(n)?;
182 if p.len() < n * n {
183 return Err(MarkovError::InvalidInput);
184 }
185 if state >= n {
186 return Err(MarkovError::InvalidState);
187 }
188 let self_loop = p[idx(state, state, n)];
189 if !self_loop.is_finite() {
190 return Err(MarkovError::NonFinite);
191 }
192 let denom = 1.0 - self_loop;
193 if denom <= 0.0 {
194 return Err(MarkovError::NonFinite);
195 }
196 Ok(1.0 / denom)
197}
198
199pub fn stationary_distribution_into(
211 p: &[f64],
212 n: usize,
213 max_iterations: u32,
214 tolerance: f64,
215 out: &mut [f64],
216) -> Result<EconConvergence, MarkovError> {
217 check_dim(n)?;
218 if p.len() < n * n {
219 return Err(MarkovError::InvalidInput);
220 }
221 if out.len() < n {
222 return Err(MarkovError::BufferTooSmall);
223 }
224 if max_iterations == 0 || !tolerance.is_finite() || tolerance <= 0.0 {
225 return Err(MarkovError::InvalidInput);
226 }
227 validate_transition_matrix(p, n)?;
228
229 let mut current = [0.0f64; MAX_STATES];
231 let mut next = [0.0f64; MAX_STATES];
232
233 let uniform = 1.0 / n as f64;
234 for i in 0..n {
235 current[i] = uniform;
236 }
237
238 let mut iter: u32 = 0;
239 let mut residual = f64::INFINITY;
240 let mut converged = false;
241
242 while iter < max_iterations {
243 for j in 0..n {
245 next[j] = 0.0;
246 }
247 for i in 0..n {
248 let ci = current[i];
249 if ci == 0.0 {
250 continue;
251 }
252 let base = i * n;
253 for j in 0..n {
254 next[j] += ci * p[base + j];
255 }
256 }
257
258 let mut diff = 0.0f64;
260 for j in 0..n {
261 if !next[j].is_finite() {
262 return Err(MarkovError::NonFinite);
263 }
264 let d = (next[j] - current[j]).abs();
265 if d > diff {
266 diff = d;
267 }
268 }
269 residual = diff;
270
271 for j in 0..n {
273 current[j] = next[j];
274 }
275
276 iter += 1;
277 if residual <= tolerance {
278 converged = true;
279 break;
280 }
281 }
282
283 if !converged {
284 for j in 0..n {
286 out[j] = current[j];
287 }
288 return Err(MarkovError::NonConverged);
289 }
290
291 for j in 0..n {
292 out[j] = current[j];
293 }
294 Ok(EconConvergence::converged(iter, residual))
295}
296
297pub fn simulate_chain_into(
305 p: &[f64],
306 n: usize,
307 initial_state: usize,
308 steps: usize,
309 seed: u64,
310 out: &mut [usize],
311) -> Result<usize, MarkovError> {
312 check_dim(n)?;
313 if p.len() < n * n {
314 return Err(MarkovError::InvalidInput);
315 }
316 if initial_state >= n {
317 return Err(MarkovError::InvalidState);
318 }
319 if out.len() < steps + 1 {
320 return Err(MarkovError::BufferTooSmall);
321 }
322 validate_transition_matrix(p, n)?;
323
324 let mut rng = SplitMix64::new(seed);
325 let mut state = initial_state;
326 out[0] = state;
327
328 for step in 0..steps {
329 let u = rng.unit_open();
330 let base = state * n;
331 let mut acc = 0.0f64;
332 let mut next_state = n - 1; for col in 0..n {
334 acc += p[base + col];
335 if u < acc {
336 next_state = col;
337 break;
338 }
339 }
340 state = next_state;
341 out[step + 1] = state;
342 }
343 Ok(steps + 1)
344}
345
346pub fn mean_first_passage_time_into(
358 p: &[f64],
359 n: usize,
360 target: usize,
361 max_iterations: u32,
362 tolerance: f64,
363 out: &mut [f64],
364) -> Result<EconConvergence, MarkovError> {
365 check_dim(n)?;
366 if p.len() < n * n {
367 return Err(MarkovError::InvalidInput);
368 }
369 if target >= n {
370 return Err(MarkovError::InvalidState);
371 }
372 if out.len() < n {
373 return Err(MarkovError::BufferTooSmall);
374 }
375 if max_iterations == 0 || !tolerance.is_finite() || tolerance <= 0.0 {
376 return Err(MarkovError::InvalidInput);
377 }
378 validate_transition_matrix(p, n)?;
379
380 let mut current = [0.0f64; MAX_STATES];
381 let mut next = [0.0f64; MAX_STATES];
382
383 for i in 0..n {
385 current[i] = 0.0;
386 }
387
388 let mut iter: u32 = 0;
389 let mut residual = f64::INFINITY;
390 let mut converged = false;
391
392 while iter < max_iterations {
393 let mut diff = 0.0f64;
394 for i in 0..n {
395 if i == target {
396 next[i] = 0.0;
397 continue;
398 }
399 let base = i * n;
400 let mut s = 1.0f64;
401 for j in 0..n {
402 if j == target {
403 continue;
404 }
405 s += p[base + j] * current[j];
406 }
407 if !s.is_finite() {
408 return Err(MarkovError::NonFinite);
409 }
410 next[i] = s;
411 let d = (s - current[i]).abs();
412 if d > diff {
413 diff = d;
414 }
415 }
416 residual = diff;
417
418 for i in 0..n {
419 current[i] = next[i];
420 }
421
422 iter += 1;
423 if residual <= tolerance {
424 converged = true;
425 break;
426 }
427 }
428
429 for i in 0..n {
430 out[i] = current[i];
431 }
432
433 if !converged {
434 return Err(MarkovError::NonConverged);
435 }
436 Ok(EconConvergence::converged(iter, residual))
437}
438
439#[cfg(test)]
444mod tests {
445 use super::*;
446
447 #[test]
449 fn symmetric_two_state_stationary() {
450 let p = [0.5, 0.5, 0.5, 0.5];
452 let mut out = [0.0f64; MAX_STATES];
453 let conv = stationary_distribution_into(&p, 2, 10_000, 1e-12, &mut out).unwrap();
454 assert_eq!(conv.status, EconStatus::Converged);
455 assert!((out[0] - 0.5).abs() < 1e-9, "out[0]={}", out[0]);
456 assert!((out[1] - 0.5).abs() < 1e-9, "out[1]={}", out[1]);
457 }
458
459 #[test]
461 fn asymmetric_two_state_stationary() {
462 let p = [0.9, 0.1, 0.5, 0.5];
463 let mut out = [0.0f64; MAX_STATES];
464 let conv = stationary_distribution_into(&p, 2, 100_000, 1e-12, &mut out).unwrap();
465 assert_eq!(conv.status, EconStatus::Converged);
466 assert!((out[0] - 5.0 / 6.0).abs() < 1e-6, "out[0]={}", out[0]);
467 assert!((out[1] - 1.0 / 6.0).abs() < 1e-6, "out[1]={}", out[1]);
468 }
469
470 #[test]
472 fn validation_rejects_row_not_summing_to_one() {
473 let p = [0.9, 0.2, 0.5, 0.5]; let err = validate_transition_matrix(&p, 2).unwrap_err();
475 assert_eq!(err, MarkovError::InvalidTransitionMatrix);
476 }
477
478 #[test]
480 fn validation_rejects_negative_entry() {
481 let p = [1.2, -0.2, 0.5, 0.5];
482 let err = validate_transition_matrix(&p, 2).unwrap_err();
483 assert_eq!(err, MarkovError::InvalidTransitionMatrix);
484 }
485
486 #[test]
488 fn validation_rejects_nan_entry() {
489 let p = [f64::NAN, 1.0, 0.5, 0.5];
490 let err = validate_transition_matrix(&p, 2).unwrap_err();
491 assert_eq!(err, MarkovError::InvalidTransitionMatrix);
492 }
493
494 #[test]
496 fn validation_rejects_infinity_entry() {
497 let p = [f64::INFINITY, 0.0, 0.5, 0.5];
498 let err = validate_transition_matrix(&p, 2).unwrap_err();
499 assert_eq!(err, MarkovError::InvalidTransitionMatrix);
500 }
501
502 #[test]
504 fn validation_accepts_valid_matrix() {
505 let p = [0.9, 0.1, 0.5, 0.5];
506 assert!(validate_transition_matrix(&p, 2).is_ok());
507 }
508
509 #[test]
511 fn validation_rejects_bad_dimensions() {
512 let p = [0.0; 4];
513 assert_eq!(
514 validate_transition_matrix(&p, 0).unwrap_err(),
515 MarkovError::InvalidInput
516 );
517 assert_eq!(
518 validate_transition_matrix(&p, MAX_STATES + 1).unwrap_err(),
519 MarkovError::InvalidInput
520 );
521 }
522
523 #[test]
525 fn validation_rejects_undersized_slice() {
526 let p = [0.9, 0.1]; assert_eq!(
528 validate_transition_matrix(&p, 2).unwrap_err(),
529 MarkovError::InvalidInput
530 );
531 }
532
533 #[test]
535 fn simulation_is_reproducible() {
536 let p = [0.9, 0.1, 0.5, 0.5];
537 let steps = 200;
538 let mut a = [0usize; 256];
539 let mut b = [0usize; 256];
540 let na = simulate_chain_into(&p, 2, 0, steps, 42, &mut a).unwrap();
541 let nb = simulate_chain_into(&p, 2, 0, steps, 42, &mut b).unwrap();
542 assert_eq!(na, nb);
543 assert_eq!(na, steps + 1);
544 for k in 0..na {
545 assert_eq!(a[k], b[k], "path divergence at {}", k);
546 }
547 }
548
549 #[test]
551 fn different_seeds_diverge() {
552 let p = [0.5, 0.5, 0.5, 0.5];
553 let steps = 100;
554 let mut a = [0usize; 256];
555 let mut b = [0usize; 256];
556 simulate_chain_into(&p, 2, 0, steps, 1, &mut a).unwrap();
557 simulate_chain_into(&p, 2, 0, steps, 2, &mut b).unwrap();
558 let mut any_diff = false;
560 for k in 0..=steps {
561 if a[k] != b[k] {
562 any_diff = true;
563 break;
564 }
565 }
566 assert!(any_diff, "two different seeds produced identical paths");
567 }
568
569 #[test]
571 fn simulation_respects_transition_probabilities() {
572 let p = [0.9, 0.1, 0.5, 0.5];
574 let steps = 200_000;
575 let mut path = vec![0usize; steps + 1];
576 simulate_chain_into(&p, 2, 0, steps, 12345, &mut path).unwrap();
577
578 let mut from0_total = 0usize;
580 let mut from0_to1 = 0usize;
581 let mut from1_total = 0usize;
582 let mut from1_to0 = 0usize;
583 for k in 0..steps {
584 let s = path[k];
585 let ns = path[k + 1];
586 if s == 0 {
587 from0_total += 1;
588 if ns == 1 {
589 from0_to1 += 1;
590 }
591 } else {
592 from1_total += 1;
593 if ns == 0 {
594 from1_to0 += 1;
595 }
596 }
597 }
598 let p01 = from0_to1 as f64 / from0_total as f64;
599 let p10 = from1_to0 as f64 / from1_total as f64;
600 assert!((p01 - 0.1).abs() < 0.01, "empirical p01={}", p01);
601 assert!((p10 - 0.5).abs() < 0.01, "empirical p10={}", p10);
602 }
603
604 #[test]
606 fn holding_time_self_loop() {
607 let p = [0.9, 0.1, 0.5, 0.5];
608 let h = expected_holding_time(&p, 2, 0).unwrap();
609 assert!((h - 10.0).abs() < 1e-9, "holding time={}", h);
610 }
611
612 #[test]
614 fn holding_time_absorbing_errors() {
615 let p = [1.0, 0.0, 0.5, 0.5];
616 let err = expected_holding_time(&p, 2, 0).unwrap_err();
617 assert_eq!(err, MarkovError::NonFinite);
618 }
619
620 #[test]
626 fn mean_first_passage_time_small_chain() {
627 let p = [0.5, 0.5, 0.0, 0.0, 0.5, 0.5, 0.1, 0.1, 0.8];
631 let mut out = [0.0f64; MAX_STATES];
632 let conv = mean_first_passage_time_into(&p, 3, 2, 100_000, 1e-12, &mut out).unwrap();
633 assert_eq!(conv.status, EconStatus::Converged);
634 assert!(out[2].abs() < 1e-9, "target mfp must be 0, got {}", out[2]);
635 assert!(out[0] > 0.0 && out[0].is_finite(), "m_0={}", out[0]);
637 assert!(out[1] > 0.0 && out[1].is_finite(), "m_1={}", out[1]);
638 assert!(out[1] < out[0], "m_1={} should be < m_0={}", out[1], out[0]);
640 }
641
642 #[test]
647 fn mean_first_passage_time_two_state_closed_form() {
648 let p = [0.9, 0.1, 0.5, 0.5];
649 let mut out = [0.0f64; MAX_STATES];
650 let conv = mean_first_passage_time_into(&p, 2, 1, 100_000, 1e-12, &mut out).unwrap();
651 assert_eq!(conv.status, EconStatus::Converged);
652 assert!(out[1].abs() < 1e-9);
653 assert!((out[0] - 10.0).abs() < 1e-6, "m_0={}", out[0]);
654 }
655
656 #[test]
658 fn stationary_buffer_too_small() {
659 let p = [0.5, 0.5, 0.5, 0.5];
660 let mut out = [0.0f64; 1]; let err = stationary_distribution_into(&p, 2, 100, 1e-9, &mut out).unwrap_err();
662 assert_eq!(err, MarkovError::BufferTooSmall);
663 }
664
665 #[test]
667 fn simulation_buffer_too_small() {
668 let p = [0.5, 0.5, 0.5, 0.5];
669 let mut out = [0usize; 5]; let err = simulate_chain_into(&p, 2, 0, 10, 1, &mut out).unwrap_err();
671 assert_eq!(err, MarkovError::BufferTooSmall);
672 }
673
674 #[test]
676 fn mfp_buffer_too_small() {
677 let p = [0.5, 0.5, 0.5, 0.5];
678 let mut out = [0.0f64; 1];
679 let err = mean_first_passage_time_into(&p, 2, 0, 100, 1e-9, &mut out).unwrap_err();
680 assert_eq!(err, MarkovError::BufferTooSmall);
681 }
682
683 #[test]
685 fn transition_probability_invalid_state() {
686 let p = [0.5, 0.5, 0.5, 0.5];
687 assert_eq!(
688 transition_probability(&p, 2, 2, 0).unwrap_err(),
689 MarkovError::InvalidState
690 );
691 assert_eq!(
692 transition_probability(&p, 2, 0, 2).unwrap_err(),
693 MarkovError::InvalidState
694 );
695 }
696
697 #[test]
699 fn holding_time_invalid_state() {
700 let p = [0.5, 0.5, 0.5, 0.5];
701 assert_eq!(
702 expected_holding_time(&p, 2, 5).unwrap_err(),
703 MarkovError::InvalidState
704 );
705 }
706
707 #[test]
709 fn simulation_invalid_initial_state() {
710 let p = [0.5, 0.5, 0.5, 0.5];
711 let mut out = [0usize; 16];
712 assert_eq!(
713 simulate_chain_into(&p, 2, 5, 10, 1, &mut out).unwrap_err(),
714 MarkovError::InvalidState
715 );
716 }
717
718 #[test]
720 fn mfp_invalid_target() {
721 let p = [0.5, 0.5, 0.5, 0.5];
722 let mut out = [0.0f64; MAX_STATES];
723 assert_eq!(
724 mean_first_passage_time_into(&p, 2, 5, 100, 1e-9, &mut out).unwrap_err(),
725 MarkovError::InvalidState
726 );
727 }
728
729 #[test]
731 fn transition_probability_lookup() {
732 let p = [0.9, 0.1, 0.5, 0.5];
733 assert!((transition_probability(&p, 2, 0, 1).unwrap() - 0.1).abs() < 1e-12);
734 assert!((transition_probability(&p, 2, 1, 0).unwrap() - 0.5).abs() < 1e-12);
735 }
736
737 #[test]
739 fn stationary_distribution_normalizes() {
740 let p = [0.2, 0.6, 0.2, 0.3, 0.4, 0.3, 0.1, 0.2, 0.7];
742 let mut out = [0.0f64; MAX_STATES];
743 let conv = stationary_distribution_into(&p, 3, 100_000, 1e-12, &mut out).unwrap();
744 assert_eq!(conv.status, EconStatus::Converged);
745 let sum = out[0] + out[1] + out[2];
746 assert!((sum - 1.0).abs() < 1e-9, "sum={}", sum);
747 for j in 0..3 {
749 let mut pip = 0.0;
750 for i in 0..3 {
751 pip += out[i] * p[i * 3 + j];
752 }
753 assert!((pip - out[j]).abs() < 1e-6, "pi*P[{}] != pi[{}]", j, j);
754 }
755 }
756
757 #[test]
759 fn stationary_non_converged_on_tiny_budget() {
760 let p = [0.999, 0.001, 0.1, 0.9];
762 let mut out = [0.0f64; MAX_STATES];
763 let err = stationary_distribution_into(&p, 2, 1, 1e-15, &mut out).unwrap_err();
764 assert_eq!(err, MarkovError::NonConverged);
765 }
766
767 #[test]
769 fn mfp_non_converged_on_tiny_budget() {
770 let p = [0.9, 0.1, 0.5, 0.5];
771 let mut out = [0.0f64; MAX_STATES];
772 let err = mean_first_passage_time_into(&p, 2, 1, 1, 1e-15, &mut out).unwrap_err();
773 assert_eq!(err, MarkovError::NonConverged);
774 }
775
776 #[test]
778 fn simulation_zero_steps_writes_initial() {
779 let p = [0.5, 0.5, 0.5, 0.5];
780 let mut out = [0usize; 4];
781 let n = simulate_chain_into(&p, 2, 1, 0, 7, &mut out).unwrap();
782 assert_eq!(n, 1);
783 assert_eq!(out[0], 1);
784 }
785
786 #[test]
788 fn error_to_status_mapping() {
789 assert_eq!(
790 MarkovError::InvalidInput.to_status(),
791 EconStatus::InvalidInput
792 );
793 assert_eq!(
794 MarkovError::InvalidTransitionMatrix.to_status(),
795 EconStatus::InvalidInput
796 );
797 assert_eq!(
798 MarkovError::InvalidState.to_status(),
799 EconStatus::InvalidInput
800 );
801 assert_eq!(
802 MarkovError::BufferTooSmall.to_status(),
803 EconStatus::BufferTooSmall
804 );
805 assert_eq!(MarkovError::NonFinite.to_status(), EconStatus::NonFinite);
806 assert_eq!(
807 MarkovError::NonConverged.to_status(),
808 EconStatus::MaxIterations
809 );
810 }
811
812 #[test]
814 fn max_states_is_32() {
815 assert_eq!(MAX_STATES, 32);
816 }
817
818 #[test]
820 fn ring_chain_uniform_stationary() {
821 let p = [
824 0.1, 0.9, 0.0, 0.0, 0.0, 0.1, 0.9, 0.0, 0.0, 0.0, 0.1, 0.9, 0.9, 0.0, 0.0, 0.1, ];
829 let mut out = [0.0f64; MAX_STATES];
830 let conv = stationary_distribution_into(&p, 4, 100_000, 1e-12, &mut out).unwrap();
831 assert_eq!(conv.status, EconStatus::Converged);
832 for i in 0..4 {
833 assert!((out[i] - 0.25).abs() < 1e-6, "out[{}]={}", i, out[i]);
834 }
835 }
836}