1use std::{ops::Neg, simd::StdFloat};
2
3use arrayvec::ArrayVec;
4use rand::prelude::*;
5use std::simd::prelude::*;
6
7use crate::{
8 datatypes::{Index, Rating, Set},
9 simulation::Matchups,
10};
11
12#[derive(Debug, Clone, Copy)]
14pub struct SwissSystem {
15 pub wins: [u8; 16],
17 pub losses: [u8; 16],
19 pub diffs: [i8; 16],
21 pub opponents: [Set; 16],
23 pub probabilities_bo1: [[f32; 16]; 16],
25 pub probabilities_bo3: [[f32; 16]; 16],
27 pub ratings: [Rating; 16],
29 pub remaining: Set,
31 pub rounds_complete: u8,
33}
34
35impl SwissSystem {
36 const SEED_LANES: Simd<u16, 16> = {
38 let mut seeds = [0; 16];
39 let mut i = 1;
40
41 while i < 16 {
42 seeds[i] = i as u16;
43 i += 1;
44 }
45
46 Simd::from_array(seeds)
47 };
48
49 const INITIAL_SEED_MASK: u16 = 0x1F;
51
52 #[allow(clippy::many_single_char_names)]
53 #[must_use]
54 #[cfg_attr(feature = "pprof", inline(never))]
55 pub fn new(ratings: [Rating; 16], sigma: f32) -> Self {
57 const ONE: Simd<f32, 16> = Simd::splat(1.0);
58 const TWO: Simd<f32, 16> = Simd::splat(2.0);
59 let mut r = [0.0_f32; 16];
60
61 for i in 0..16 {
62 r[i] = ratings[i].to_f32();
63 }
64
65 let u = Simd::splat(10.0_f32.ln()) / Simd::splat(sigma);
77 let rb = Simd::from_array(r);
78 let mut probabilities_bo1 = [[0.0; 16]; 16];
79
80 for i in 0..16 {
81 let ra = Simd::splat(r[i]);
82 probabilities_bo1[i] = (ONE + (u * (rb - ra)).exp()).recip().to_array();
83 }
84
85 let mut probabilities_bo3 = [[0.0; 16]; 16];
99
100 for i in 0..16 {
101 let p = Simd::from_array(probabilities_bo1[i]);
102 let a = p * p;
103 let b = ONE - p;
104 probabilities_bo3[i] = TWO.mul_add(a * b, a).to_array();
105 }
106
107 let wins = [0; 16];
108 let losses = [0; 16];
109 let diffs = [0; 16];
110 let opponents = [Set::new(); 16];
111
112 Self {
113 wins,
114 losses,
115 diffs,
116 opponents,
117 probabilities_bo1,
118 probabilities_bo3,
119 ratings,
120 remaining: Set::full(),
121 rounds_complete: 0,
122 }
123 }
124
125 #[cfg_attr(feature = "pprof", inline(never))]
127 #[cfg_attr(not(feature = "pprof"), inline)]
128 pub const fn reset(&mut self) {
129 self.wins = [0; 16];
130 self.losses = [0; 16];
131 self.diffs = [0; 16];
132 self.opponents = [Set::new(); 16];
133 self.remaining = Set::full();
134 self.rounds_complete = 0;
135 }
136
137 #[cfg_attr(feature = "pprof", inline(never))]
139 fn buchholz(&self, team: Index) -> i8 {
140 const ONE: Simd<u16, 16> = Simd::splat(1);
141
142 let mask = {
143 let shifted = self.opponents[team.to_usize()].splat() >> Self::SEED_LANES;
147 (shifted & ONE).cast::<i8>().neg()
148 };
149
150 (Simd::from_array(self.diffs) & mask).reduce_sum()
151 }
152
153 #[allow(clippy::cast_sign_loss)]
161 #[cfg_attr(feature = "pprof", inline(never))]
162 pub(super) fn seed_teams(&self) -> ArrayVec<Index, 16> {
163 let mut seeding = ArrayVec::<u16, 16>::new();
164
165 for index in self.remaining.iter() {
167 let diff = (15 - self.diffs[index.to_usize()]) as u16;
169 let buchholz = (15 - self.buchholz(index)) as u16;
170
171 seeding.push(diff << 10 | buchholz << 5 | index.to_u16());
182 }
183
184 seeding.sort_unstable();
185
186 for packed_seed in &mut seeding {
188 *packed_seed &= Self::INITIAL_SEED_MASK;
189 }
190
191 unsafe { std::mem::transmute(seeding) }
194 }
195
196 #[cfg_attr(feature = "pprof", inline(never))]
198 fn simulate_match<R: rand::Rng>(&mut self, rng: &mut R, seed_a: Index, seed_b: Index) {
199 let r = rng.random();
200 let a = seed_a.to_usize();
201 let b = seed_b.to_usize();
202
203 let is_bo3 = self.wins[a] == 2 || self.losses[a] == 2;
205
206 let p = if is_bo3 {
208 self.probabilities_bo3[a][b]
209 } else {
210 self.probabilities_bo1[a][b]
211 };
212
213 let team_a_win = p > r;
214
215 if team_a_win {
217 self.wins[a] += 1;
218 self.losses[b] += 1;
219 self.diffs[a] += 1;
220 self.diffs[b] -= 1;
221 } else {
222 self.losses[a] += 1;
223 self.wins[b] += 1;
224 self.diffs[a] -= 1;
225 self.diffs[b] += 1;
226 }
227
228 self.opponents[a].insert(seed_b);
229 self.opponents[b].insert(seed_a);
230
231 if is_bo3 {
234 if self.wins[a] == 3 || self.losses[a] == 3 {
235 self.remaining.remove(seed_a);
236 }
237
238 if self.wins[b] == 3 || self.losses[b] == 3 {
239 self.remaining.remove(seed_b);
240 }
241 }
242 }
243
244 #[cfg_attr(feature = "pprof", inline(never))]
246 #[cfg_attr(not(feature = "pprof"), inline)]
247 fn simulate_round<R: rand::Rng>(&mut self, rng: &mut R) {
248 for (a, b) in Matchups::new(self) {
249 self.simulate_match(rng, a, b);
250 }
251
252 self.rounds_complete += 1;
253 }
254
255 #[cfg_attr(feature = "pprof", inline(never))]
257 #[cfg_attr(not(feature = "pprof"), inline)]
258 pub fn simulate_tournament<R: rand::Rng>(&mut self, rng: &mut R) {
259 while self.rounds_complete < 5 {
260 self.simulate_round(rng);
261 }
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use std::ops::{AddAssign, Div, Sub};
268
269 use super::*;
270
271 use crate::{datatypes::Teams, simulation::rng};
272
273 macro_rules! set {
274 ($($n:expr),*) => {
275 [$(Index::new::<$n>(),)*].into_iter().collect()
276 };
277 }
278
279 #[test]
282 fn exact_regression_test() {
283 let mut ss = SwissSystem::new(Teams::dummy().ratings, 800.0);
284 ss.simulate_tournament(&mut rng::HalfRng);
285
286 assert_eq!(ss.wins, [3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 1, 1, 1, 0, 0]);
287 assert_eq!(ss.losses, [0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3]);
288 assert_eq!(
289 ss.opponents,
290 [
291 set!(3, 7, 8),
292 set!(2, 6, 9),
293 set!(1, 5, 7, 10),
294 set!(0, 4, 6, 11),
295 set!(3, 5, 11, 12),
296 set!(2, 4, 9, 10, 13),
297 set!(1, 3, 8, 9, 14),
298 set!(0, 2, 8, 10, 15),
299 set!(0, 6, 7, 13, 15),
300 set!(1, 5, 6, 12, 14),
301 set!(2, 5, 7, 11, 13),
302 set!(3, 4, 10, 12),
303 set!(4, 9, 11, 15),
304 set!(5, 8, 10, 14),
305 set!(6, 9, 13),
306 set!(7, 8, 12),
307 ]
308 );
309 }
310
311 #[test]
313 #[allow(clippy::cast_sign_loss, clippy::unreadable_literal)]
314 fn statistical_regression_test() {
315 const ITERATIONS: usize = 100_000;
316 const ITER_SPLAT: Simd<f32, 16> = Simd::splat(ITERATIONS as f32);
317 const TOLERANCE: Simd<f32, 16> = Simd::splat(0.005);
318 const THREE: Simd<u8, 16> = Simd::splat(3);
319 const ZERO: Simd<u8, 16> = Simd::splat(0);
320
321 let fresh_ss = SwissSystem::new(Teams::dummy().ratings, 800.0);
322 let mut rng = rng::deterministic();
323 let mut total_three_zero: Simd<u64, 16> = Simd::splat(0);
324 let mut total_advancing: Simd<u64, 16> = Simd::splat(0);
325 let mut total_zero_three: Simd<u64, 16> = Simd::splat(0);
326
327 for _ in 0..ITERATIONS {
328 let mut ss = fresh_ss;
329 ss.simulate_tournament(&mut rng);
330
331 let wins = Simd::from_array(ss.wins);
332 let losses = Simd::from_array(ss.losses);
333
334 let three_wins = wins.simd_eq(THREE);
335 let zero_wins = wins.simd_eq(ZERO);
336 let three_losses = losses.simd_eq(THREE);
337 let zero_losses = losses.simd_eq(ZERO);
338
339 total_three_zero.add_assign((three_wins & zero_losses).to_simd().abs().cast());
340 total_advancing.add_assign((three_wins & !zero_losses).to_simd().abs().cast());
341 total_zero_three.add_assign((zero_wins & three_losses).to_simd().abs().cast());
342 }
343
344 let expected_three_zero = Simd::from_array([
345 0.467134, 0.381915, 0.30356, 0.239474, 0.18577, 0.141047, 0.106158, 0.077854, 0.029252,
346 0.022126, 0.016032, 0.010871, 0.007562, 0.005146, 0.003569, 0.00253,
347 ]);
348
349 let expected_advancing = Simd::from_array([
350 0.482817, 0.542122, 0.585634, 0.604685, 0.604085, 0.584402, 0.547824, 0.497656,
351 0.394673, 0.324406, 0.258943, 0.199543, 0.148371, 0.105615, 0.071796, 0.047428,
352 ]);
353
354 let expected_zero_three = Simd::from_array([
355 0.002564, 0.003679, 0.005201, 0.007576, 0.010758, 0.01579, 0.021963, 0.029273, 0.07803,
356 0.105759, 0.141881, 0.18603, 0.238006, 0.303477, 0.383103, 0.46691,
357 ]);
358
359 for (actual, expected) in [
360 (total_three_zero.cast().div(ITER_SPLAT), expected_three_zero),
361 (total_advancing.cast().div(ITER_SPLAT), expected_advancing),
362 (total_zero_three.cast().div(ITER_SPLAT), expected_zero_three),
363 ] {
364 assert!(
365 actual.sub(expected).abs().simd_lt(TOLERANCE).all(),
366 "Actual: {actual:#?}\n\nExpected: {expected:#?}"
367 );
368 }
369 }
370}