Skip to main content

pickems/simulation/
mod.rs

1use rayon::iter::{IntoParallelIterator, ParallelIterator};
2
3use crate::{datatypes::Teams, reporting::Report};
4
5mod matching;
6mod rng;
7mod swiss_system;
8
9use matching::Matchups;
10pub use swiss_system::SwissSystem;
11
12/// Configuration for running repeated tournament simulations.
13#[derive(Debug, Clone)]
14pub struct Simulation {
15    /// Seed-ordered team data.
16    pub teams: Teams,
17    /// Standard deviation parameter for the logistic win-probability model.
18    pub sigma: f32,
19    /// Number of independent tournaments to simulate.
20    pub iterations: u64,
21}
22
23impl Simulation {
24    /// Construct a simulation from team data, sigma, and iteration count.
25    #[must_use]
26    pub const fn new(teams: Teams, sigma: f32, iterations: u64) -> Self {
27        Self {
28            teams,
29            sigma,
30            iterations,
31        }
32    }
33
34    /// Produce simulation with dummy data for testing purposes.
35    #[must_use]
36    pub fn dummy(iterations: u64) -> Self {
37        Self {
38            teams: Teams::dummy(),
39            sigma: 800.0,
40            iterations,
41        }
42    }
43
44    /// Run single-threaded bench test for profiling/benchmarking purposes.
45    pub fn bench_test<R: Report>(&self, mut report: R) -> R {
46        let mut ss = SwissSystem::new(self.teams.ratings, self.sigma);
47        let mut rng = rng::deterministic();
48
49        for _ in 0..self.iterations {
50            ss.reset();
51            ss.simulate_tournament(&mut rng);
52            report.update(&ss);
53        }
54
55        report
56    }
57
58    /// Run a tournament simulation to completion and return a report.
59    pub fn run<R: Report>(&self, fresh_report: R) -> R {
60        let fresh_ss = SwissSystem::new(self.teams.ratings, self.sigma);
61
62        (0..self.iterations)
63            .into_par_iter()
64            .map_init(
65                || (fresh_ss, rng::random()),
66                |(ss, rng), _| {
67                    // Reuse the precomputed probability matrices per worker and
68                    // reset only the mutable tournament state each iteration.
69                    ss.reset();
70                    ss.simulate_tournament(rng);
71                    let mut report = fresh_report;
72                    report.update(ss);
73                    report
74                },
75            )
76            .sum()
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    use crate::reporting::BasicReport;
85
86    /// Quick sanity test to check that things are generally working.
87    #[test]
88    fn sanity_test() {
89        let iterations = 1000;
90        let report = Simulation::dummy(iterations).bench_test(BasicReport::default());
91
92        // Total 3-0 stats should sum to 2 per iteration
93        assert_eq!(
94            (0..16)
95                .map(|index| report.stats[index].three_zero)
96                .sum::<u64>(),
97            iterations * 2
98        );
99
100        // Total 3-1/3-2 stats should sum to 6 per iteration
101        assert_eq!(
102            (0..16)
103                .map(|index| report.stats[index].advancing)
104                .sum::<u64>(),
105            iterations * 6
106        );
107
108        // Total 0-3 stats should sum to 2 per iteration
109        assert_eq!(
110            (0..16)
111                .map(|index| report.stats[index].zero_three)
112                .sum::<u64>(),
113            iterations * 2
114        );
115
116        // Best team should always have more 3-0 stats than the worst team
117        assert!(report.stats[0].three_zero > report.stats[15].three_zero);
118
119        // Best team should always have less 0-3 stats than the worst team
120        assert!(report.stats[0].zero_three < report.stats[15].zero_three);
121    }
122}