1use crate::data_processing::Contest;
2use serde::{Deserialize, Serialize};
3use std::cell::{RefCell, RefMut};
4use std::collections::{HashMap, VecDeque};
5
6pub const TANH_MULTIPLIER: f64 = std::f64::consts::PI / 1.7320508075688772;
7pub type PlayersByName = HashMap<String, RefCell<Player>>;
8
9#[derive(Clone, Copy, Debug)]
10pub struct Rating {
11 pub mu: f64,
12 pub sig: f64,
13}
14
15impl Rating {
16 pub fn with_noise(self, sig_noise: f64) -> Self {
17 Self {
18 mu: self.mu,
19 sig: self.sig.hypot(sig_noise),
20 }
21 }
22
23 pub fn towards_noise(self, decay: f64, limit: Self) -> Self {
26 let mu_diff = self.mu - limit.mu;
27 let sig_sq_diff = self.sig * self.sig - limit.sig * limit.sig;
28 Self {
29 mu: limit.mu + mu_diff * decay,
30 sig: (limit.sig * limit.sig + sig_sq_diff * decay * decay).sqrt(),
31 }
32 }
33}
34
35#[derive(Clone, Copy, Debug)]
36pub struct TanhTerm {
37 pub mu: f64,
38 pub w_arg: f64,
39 pub w_out: f64,
40}
41
42impl From<Rating> for TanhTerm {
43 fn from(rating: Rating) -> Self {
44 let w = TANH_MULTIPLIER / rating.sig;
45 Self {
46 mu: rating.mu,
47 w_arg: w * 0.5,
48 w_out: w,
49 }
50 }
51}
52
53impl TanhTerm {
54 pub fn get_weight(&self) -> f64 {
55 self.w_arg * self.w_out * 2. / TANH_MULTIPLIER.powi(2)
56 }
57}
58
59#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
60pub struct PlayerEvent {
61 pub contest_index: usize,
62 pub rating_mu: i32,
63 pub rating_sig: i32,
64 pub perf_score: i32,
65 pub place: usize,
66}
67
68pub struct Player {
69 normal_factor: Rating,
70 logistic_factors: VecDeque<TanhTerm>,
71 pub event_history: Vec<PlayerEvent>,
72 pub approx_posterior: Rating,
73 pub update_time: u64,
74 pub delta_time: u64,
75}
76
77impl Player {
78 pub fn with_rating(mu: f64, sig: f64, update_time: u64) -> Self {
79 Player {
80 normal_factor: Rating { mu, sig },
81 logistic_factors: VecDeque::new(),
82 event_history: vec![],
83 approx_posterior: Rating { mu, sig },
84 update_time,
85 delta_time: 0,
86 }
87 }
88
89 pub fn is_newcomer(&self) -> bool {
90 self.event_history.len() <= 1
91 }
92
93 pub fn update_rating(&mut self, rating: Rating, performance_score: f64) {
94 let last_event = self.event_history.last_mut().unwrap();
96 assert_eq!(last_event.rating_mu, 0);
97 assert_eq!(last_event.rating_sig, 0);
98 assert_eq!(last_event.perf_score, 0);
99
100 self.approx_posterior = rating;
101 last_event.rating_mu = rating.mu.round() as i32;
102 last_event.rating_sig = rating.sig.round() as i32;
103 last_event.perf_score = performance_score.round() as i32;
104 }
105
106 pub fn update_rating_with_normal(&mut self, performance: Rating) {
107 let wn = self.normal_factor.sig.powi(-2);
108 let wp = performance.sig.powi(-2);
109 self.normal_factor.mu = (wn * self.normal_factor.mu + wp * performance.mu) / (wn + wp);
110 self.normal_factor.sig = (wn + wp).recip().sqrt();
111
112 let new_rating = if self.logistic_factors.is_empty() {
113 self.normal_factor
114 } else {
115 self.approximate_posterior(performance.sig)
116 };
117 self.update_rating(new_rating, performance.mu);
118 }
119
120 pub fn update_rating_with_logistic(&mut self, performance: Rating, max_history: usize) {
121 if self.logistic_factors.len() >= max_history {
122 let logistic = self.logistic_factors.pop_front().unwrap();
125 let wn = self.normal_factor.sig.powi(-2);
126 let wl = logistic.get_weight();
127 self.normal_factor.mu = (wn * self.normal_factor.mu + wl * logistic.mu) / (wn + wl);
128 self.normal_factor.sig = (wn + wl).recip().sqrt();
129 }
130 self.logistic_factors.push_back(performance.into());
131
132 let new_rating = self.approximate_posterior(performance.sig);
133 self.update_rating(new_rating, performance.mu);
134 }
135
136 fn approximate_posterior(&self, perf_sig: f64) -> Rating {
139 let normal_weight = self.normal_factor.sig.powi(-2);
140 let mu = robust_average(
141 self.logistic_factors.iter().cloned(),
142 -self.normal_factor.mu * normal_weight,
143 normal_weight,
144 );
145 let sig = (self.approx_posterior.sig.powi(-2) + perf_sig.powi(-2))
146 .recip()
147 .sqrt();
148 Rating { mu, sig }
149 }
150
151 pub fn add_noise_and_collapse(&mut self, sig_noise: f64) {
154 self.approx_posterior = self.approx_posterior.with_noise(sig_noise);
155 self.normal_factor = self.approx_posterior;
156 self.logistic_factors.clear();
157 }
158
159 #[allow(dead_code)]
162 pub fn add_noise_in_front(&mut self, sig_noise: f64) {
163 let decay = 1.0f64.hypot(sig_noise / self.approx_posterior.sig);
164 self.approx_posterior.sig *= decay;
165
166 self.normal_factor.sig *= decay;
167 for rating in &mut self.logistic_factors {
168 rating.w_out /= decay * decay;
169 }
170 }
171
172 pub fn add_noise_best(&mut self, sig_noise: f64, transfer_speed: f64) {
176 let new_posterior = self.approx_posterior.with_noise(sig_noise);
177
178 let decay = (self.approx_posterior.sig / new_posterior.sig).powi(2);
179 let transfer = decay.powf(transfer_speed);
180 self.approx_posterior = new_posterior;
181
182 let wt_norm_old = self.normal_factor.sig.powi(-2);
183 let wt_from_norm_old = transfer * wt_norm_old;
184 let wt_from_transfers = (1. - transfer)
185 * (wt_norm_old
186 + self
187 .logistic_factors
188 .iter()
189 .map(TanhTerm::get_weight)
190 .sum::<f64>());
191 let wt_total = wt_from_norm_old + wt_from_transfers;
192
193 self.normal_factor.mu = (wt_from_norm_old * self.normal_factor.mu
194 + wt_from_transfers * self.approx_posterior.mu)
195 / wt_total;
196 self.normal_factor.sig = (decay * wt_total).recip().sqrt();
197 for r in &mut self.logistic_factors {
198 r.w_out *= transfer * decay;
199 }
200 }
201}
202
203#[allow(dead_code)]
204pub fn standard_logistic_pdf(z: f64) -> f64 {
205 0.25 * TANH_MULTIPLIER * (0.5 * TANH_MULTIPLIER * z).cosh().powi(-2)
206}
207
208pub fn standard_logistic_cdf(z: f64) -> f64 {
209 0.5 + 0.5 * (0.5 * TANH_MULTIPLIER * z).tanh()
210}
211
212#[allow(dead_code)]
213pub fn standard_logistic_cdf_inv(prob: f64) -> f64 {
214 (2. * prob - 1.).atanh() * 2. / TANH_MULTIPLIER
215}
216
217pub fn standard_normal_pdf(z: f64) -> f64 {
218 const NORMALIZE: f64 = 0.5 * std::f64::consts::FRAC_2_SQRT_PI / std::f64::consts::SQRT_2;
219 NORMALIZE * (-0.5 * z * z).exp()
220}
221
222pub fn standard_normal_cdf(z: f64) -> f64 {
223 0.5 * statrs::function::erf::erfc(-z / std::f64::consts::SQRT_2)
224 }
226
227pub fn standard_normal_cdf_inv(prob: f64) -> f64 {
228 -std::f64::consts::SQRT_2 * statrs::function::erf::erfc_inv(2. * prob)
229 }
231
232#[allow(dead_code)]
233pub fn solve_bisection((mut lo, mut hi): (f64, f64), f: impl Fn(f64) -> f64) -> f64 {
234 loop {
235 let flo = f(lo);
236 let guess = 0.5 * (lo + hi);
237 if lo >= guess || guess >= hi {
238 return guess;
239 }
240 if f(guess) * flo > 0. {
241 lo = guess;
242 } else {
243 hi = guess;
244 }
245 }
246}
247
248#[allow(dead_code)]
249pub fn solve_illinois((mut lo, mut hi): (f64, f64), f: impl Fn(f64) -> f64) -> f64 {
250 let (mut flo, mut fhi, mut side) = (f(lo), f(hi), 0i8);
251 loop {
252 let guess = (flo * hi - fhi * lo) / (flo - fhi);
253 if lo >= guess || guess >= hi {
254 return 0.5 * (lo + hi);
255 }
256 let fguess = f(guess);
257 if fguess * flo > 0. {
258 lo = guess;
259 flo = fguess;
260 if side == -1 {
261 fhi *= 0.5;
262 }
263 side = -1;
264 } else if fguess * fhi > 0. {
265 hi = guess;
266 fhi = fguess;
267 if side == 1 {
268 flo *= 0.5;
269 }
270 side = 1;
271 } else {
272 return guess;
273 }
274 }
275}
276
277pub fn solve_newton((mut lo, mut hi): (f64, f64), f: impl Fn(f64) -> (f64, f64)) -> f64 {
278 let mut guess = 0.5 * (lo + hi);
279 loop {
280 let (sum, sum_prime) = f(guess);
281 let extrapolate = guess - sum / sum_prime;
282 if extrapolate < guess {
283 hi = guess;
284 guess = extrapolate.max(0.75 * lo + 0.25 * hi).min(hi);
285 } else {
286 lo = guess;
287 guess = extrapolate.max(lo).min(0.25 * lo + 0.75 * hi);
288 }
289 if lo >= guess || guess >= hi {
290 if sum.abs() > 1e-10 {
291 eprintln!(
292 "WARNING: POSSIBLE FAILURE TO CONVERGE @ {}: s={}, s'={}",
293 guess, sum, sum_prime
294 );
295 }
296 return guess;
297 }
298 }
299}
300
301pub fn robust_average(
306 all_ratings: impl Iterator<Item = TanhTerm> + Clone,
307 offset: f64,
308 slope: f64,
309) -> f64 {
310 let bounds = (-6000.0, 9000.0);
311 let f = |x: f64| -> (f64, f64) {
312 all_ratings
313 .clone()
314 .map(|term| {
315 let tanh_z = ((x - term.mu) * term.w_arg).tanh();
316 (
317 tanh_z * term.w_out,
318 (1. - tanh_z * tanh_z) * term.w_arg * term.w_out,
319 )
320 })
321 .fold((offset + slope * x, slope), |(s, sp), (v, vp)| {
322 (s + v, sp + vp)
323 })
324 };
325 solve_newton(bounds, f)
326}
327
328pub trait RatingSystem: std::fmt::Debug {
329 fn round_update(&self, contest_weight: f64, standings: Vec<(&mut Player, usize, usize)>);
330}
331
332pub fn outcome_free<T>(standings: &[(T, usize, usize)]) -> bool {
333 standings.is_empty() || standings[0].2 + 1 >= standings.len()
334}
335
336pub fn simulate_contest(
337 players: &mut PlayersByName,
338 contest: &Contest,
339 system: &dyn RatingSystem,
340 mu_newbie: f64,
341 sig_newbie: f64,
342 contest_index: usize,
343) {
344 if outcome_free(&contest.standings) {
345 eprintln!(
346 "WARNING: ignoring contest {} because all players tied",
347 contest_index
348 );
349 return;
350 }
351
352 contest.standings.iter().for_each(|&(ref handle, _, _)| {
354 players.entry(handle.clone()).or_insert_with(|| {
357 RefCell::new(Player::with_rating(
358 mu_newbie,
359 sig_newbie,
360 contest.time_seconds,
361 ))
362 });
363 });
364
365 let mut guards: Vec<RefMut<Player>> = contest
368 .standings
369 .iter()
370 .map(|&(ref handle, _, _)| players.get(handle).expect("Duplicate handles").borrow_mut())
372 .collect();
373
374 let standings: Vec<(&mut Player, usize, usize)> = guards
376 .iter_mut()
377 .map(std::ops::DerefMut::deref_mut)
378 .zip(contest.standings.iter())
379 .map(|(player, &(_, lo, hi))| {
380 player.event_history.push(PlayerEvent {
381 contest_index,
382 rating_mu: 0, rating_sig: 0, perf_score: 0, place: lo,
386 });
387 player.delta_time = contest.time_seconds - player.update_time;
388 player.update_time = contest.time_seconds;
389 (player, lo, hi)
390 })
391 .collect();
392
393 system.round_update(contest.weight, standings);
394
395 }
398
399pub fn get_participant_ratings(
400 players: &mut PlayersByName,
401 contest_standings: &[(String, usize, usize)],
402 min_history: usize,
403) -> Vec<(Rating, usize, usize)> {
404 let mut standings: Vec<(Rating, usize, usize)> = vec![];
405
406 for &(ref handle, lo, hi) in contest_standings {
407 if let Some(player) = players.get(handle).map(RefCell::borrow) {
408 if player.event_history.len() >= min_history {
409 standings.push((player.approx_posterior, lo, hi));
410 }
411 }
412 }
413
414 let (mut last_k, mut last_v) = (usize::MAX, usize::MAX);
417 for (i, (_, lo, _)) in standings.iter_mut().enumerate() {
418 if *lo != last_k {
419 last_k = *lo;
420 last_v = i;
421 }
422 *lo = last_v;
423 }
424 let (mut last_k, mut last_v) = (usize::MAX, usize::MAX);
425 for (i, (_, _, hi)) in standings.iter_mut().enumerate().rev() {
426 if *hi != last_k {
427 last_k = *hi;
428 last_v = i;
429 }
430 *hi = last_v;
431 }
432 standings
433}