1use super::util::{
3 solve_newton, standard_normal_cdf, standard_normal_pdf, Player, Rating, RatingSystem, TanhTerm,
4};
5use core::ops::Range;
6use rayon::prelude::*;
7use std::cmp::Ordering;
8use std::sync::atomic::{AtomicUsize, Ordering::Relaxed};
9use superslice::Ext;
10
11trait Term {
12 fn eval(&self, x: f64, order: Ordering, split_ties: bool) -> (f64, f64);
13 fn evals(&self, x: f64, ranks: &[usize], my_rank: usize, split_ties: bool) -> (f64, f64) {
16 if ranks.len() == 1 {
17 let (value, deriv) = self.eval(x, ranks[0].cmp(&my_rank), split_ties);
19 return (value, deriv);
20 }
21 let Range { start, end } = ranks.equal_range(&my_rank);
22 let equal = end - start;
23 let greater = ranks.len() - end;
24 let mut value = 0.;
25 let mut deriv = 0.;
26 if start > 0 {
27 let (v, p) = self.eval(x, Ordering::Less, split_ties);
28 value += start as f64 * v;
29 deriv += start as f64 * p;
30 }
31 if equal > 0 {
32 let (v, p) = self.eval(x, Ordering::Equal, split_ties);
33 value += equal as f64 * v;
34 deriv += equal as f64 * p;
35 }
36 if greater > 0 {
37 let (v, p) = self.eval(x, Ordering::Greater, split_ties);
38 value += greater as f64 * v;
39 deriv += greater as f64 * p;
40 }
41 (value, deriv)
42 }
43}
44
45impl Term for Rating {
46 fn eval(&self, x: f64, order: Ordering, split_ties: bool) -> (f64, f64) {
47 let z = (x - self.mu) / self.sig;
48 let pdf = standard_normal_pdf(z) / self.sig;
49 let pdf_prime = -z * pdf / self.sig;
50
51 match order {
52 Ordering::Less => {
53 let cdf_m1 = -standard_normal_cdf(-z);
55 let val = pdf / cdf_m1;
56 (val, pdf_prime / cdf_m1 - val * val)
57 }
58 Ordering::Greater => {
59 let cdf = standard_normal_cdf(z);
60 let val = pdf / cdf;
61 (val, pdf_prime / cdf - val * val)
62 }
63 Ordering::Equal => {
64 if split_ties {
65 let cdf = standard_normal_cdf(z);
66 let cdf_m1 = cdf - 1.;
67 let val0 = pdf / cdf;
68 let val1 = pdf / cdf_m1;
69 (
70 0.5 * (val0 + val1),
71 0.5 * (pdf_prime * (1. / cdf + 1. / cdf_m1) - val0 * val0 - val1 * val1),
72 )
73 } else {
74 let pdf_pp = -(pdf / self.sig + z * pdf_prime) / self.sig;
75 let val = pdf_prime / pdf;
76 (val, pdf_pp / pdf - val * val)
77 }
78 }
79 }
80 }
81}
82
83impl Term for TanhTerm {
84 fn eval(&self, x: f64, order: Ordering, split_ties: bool) -> (f64, f64) {
85 let z = (x - self.mu) * self.w_arg;
86 let val = -z.tanh() * self.w_out;
87 let val_prime = -z.cosh().powi(-2) * self.w_arg * self.w_out;
88
89 match order {
90 Ordering::Less => (val - self.w_out, val_prime),
91 Ordering::Greater => (val + self.w_out, val_prime),
92 Ordering::Equal => {
93 if split_ties {
94 (val, val_prime)
95 } else {
96 (2. * val, 2. * val_prime)
97 }
98 }
99 }
100 }
101}
102
103fn bucket(a: f64, width: f64) -> i32 {
104 (a / width).round() as i32
105}
106
107fn same_bucket(a: f64, b: f64, width: f64) -> bool {
108 bucket(a, width) == bucket(b, width)
109}
110
111fn cmp_by_bucket(a: f64, b: f64, width: f64) -> Ordering {
112 bucket(a, width).cmp(&bucket(b, width))
113}
114
115#[derive(Debug)]
116pub enum EloMMRVariant {
117 Gaussian,
118 Logistic(f64),
119}
120
121#[derive(Debug)]
122pub struct EloMMR {
123 pub beta: f64,
125 pub sig_limit: f64,
128 pub drift_per_sec: f64,
130 pub split_ties: bool,
132 pub subsample_size: usize,
134 pub subsample_bucket: f64,
136 pub variant: EloMMRVariant,
138}
139
140impl Default for EloMMR {
141 fn default() -> Self {
142 Self::from_limit(200., 80., false, false, EloMMRVariant::Logistic(1.))
143 }
144}
145
146impl EloMMR {
147 pub fn default_fast() -> Self {
148 Self::from_limit(200., 80., false, true, EloMMRVariant::Logistic(1.))
149 }
150
151 pub fn default_gaussian() -> Self {
152 Self::from_limit(200., 80., false, false, EloMMRVariant::Gaussian)
153 }
154
155 pub fn default_gaussian_fast() -> Self {
156 Self::from_limit(200., 80., false, true, EloMMRVariant::Gaussian)
157 }
158
159 pub fn from_limit(
162 beta: f64,
163 sig_limit: f64,
164 split_ties: bool,
165 fast: bool,
166 variant: EloMMRVariant,
167 ) -> Self {
168 assert!(sig_limit > 0.);
169 assert!(beta > sig_limit);
170 let subsample_size = if fast { 100 } else { usize::MAX };
171 let subsample_bucket = if fast { 2. } else { 1e-5 };
172 Self {
173 beta,
174 sig_limit,
175 drift_per_sec: 0.,
176 split_ties,
177 subsample_size,
178 subsample_bucket,
179 variant,
180 }
181 }
182
183 fn sig_perf_and_drift(&self, contest_weight: f64) -> (f64, f64) {
184 let excess_beta_sq =
185 (self.beta * self.beta - self.sig_limit * self.sig_limit) / contest_weight;
186 let sig_perf = (self.sig_limit * self.sig_limit + excess_beta_sq).sqrt();
187 let discrete_drift = self.sig_limit.powi(4) / excess_beta_sq;
188 (sig_perf, discrete_drift)
189 }
190
191 fn subsample(
192 terms: &[(Rating, Vec<usize>)],
193 rating: f64,
194 num_samples: usize,
195 subsample_bucket: f64,
196 ) -> impl Iterator<Item = usize> + Clone {
197 let mut beg = terms
199 .binary_search_by(|term| {
200 cmp_by_bucket(term.0.mu, rating, subsample_bucket).then(std::cmp::Ordering::Greater)
201 })
202 .unwrap_err();
203 let mut end = beg + 1;
204
205 let expand = (num_samples.saturating_sub(end - beg) + 1) / 2;
206 beg = beg.saturating_sub(expand);
207 end = terms.len().min(end + expand);
208
209 let expand = num_samples.saturating_sub(end - beg);
210 beg = beg.saturating_sub(expand);
211 end = terms.len().min(end + expand);
212
213 beg..end
214 }
217}
218
219impl RatingSystem for EloMMR {
220 fn round_update(&self, contest_weight: f64, mut standings: Vec<(&mut Player, usize, usize)>) {
221 let (sig_perf, discrete_drift) = self.sig_perf_and_drift(contest_weight);
222
223 let mut base_terms: Vec<(Rating, usize)> = standings
228 .par_iter_mut()
229 .map(|(player, lo, _)| {
230 let continuous_drift = self.drift_per_sec * player.update_time as f64;
231 let sig_drift = (discrete_drift + continuous_drift).sqrt();
232 match self.variant {
233 EloMMRVariant::Logistic(transfer_speed) if transfer_speed < f64::INFINITY => {
236 player.add_noise_best(sig_drift, transfer_speed)
237 }
238 _ => player.add_noise_and_collapse(sig_drift),
239 }
240 (player.approx_posterior.with_noise(sig_perf), *lo)
241 })
242 .collect();
243
244 base_terms.sort_unstable_by(|a, b| {
246 cmp_by_bucket(a.0.mu, b.0.mu, self.subsample_bucket)
247 .then_with(|| cmp_by_bucket(a.0.sig, b.0.sig, self.subsample_bucket))
248 .then_with(|| a.1.cmp(&b.1))
249 });
250 let mut normal_terms: Vec<(Rating, Vec<usize>)> = vec![];
251 for (term, lo) in base_terms {
252 if let Some((last_term, ranks)) = normal_terms.last_mut() {
253 if same_bucket(last_term.mu, term.mu, self.subsample_bucket)
254 && same_bucket(last_term.sig, term.sig, self.subsample_bucket)
255 {
256 let len = ranks.len() as f64;
257 last_term.mu = (len * last_term.mu + term.mu) / (len + 1.);
258 last_term.sig = (len * last_term.sig + term.sig) / (len + 1.);
259 ranks.push(lo);
260 continue;
261 }
262 }
263 normal_terms.push((term, vec![lo]));
264 }
265
266 let tanh_terms: Vec<(TanhTerm, Vec<usize>)> = normal_terms
268 .iter()
269 .map(|(rating, ranks)| ((*rating).into(), ranks.clone()))
270 .collect();
271
272 let idx_len_max = AtomicUsize::new(9999);
274
275 standings.into_par_iter().for_each(|(player, my_rank, _)| {
277 let player_mu = player.approx_posterior.mu;
278 let idx_subsample =
279 Self::subsample(&normal_terms, player_mu, self.subsample_size, self.subsample_bucket);
280 let idx_len_upper_bound = idx_subsample.size_hint().1.unwrap_or(usize::MAX);
282 if idx_len_max.fetch_max(idx_len_upper_bound, Relaxed) < idx_len_upper_bound {
283 eprintln!(
284 "WARNING: subsampling {} opponents might be slow; consider decreasing subsample_size.",
285 idx_len_upper_bound
286 );
287 }
288 let bounds = (-6000.0, 9000.0);
289
290 match self.variant {
291 EloMMRVariant::Gaussian => {
292 let idx_subsample = idx_subsample
293 .map(|i| &normal_terms[i]);
294 let f = |x| {
295 idx_subsample
296 .clone()
297 .map(|(rating, ranks)| {
298 rating.evals(x, &ranks, my_rank, self.split_ties)
299 })
300 .fold((0., 0.), |(s, sp), (v, vp)| (s + v, sp + vp))
301 };
302 let mu_perf = solve_newton(bounds, f);
303 player.update_rating_with_normal(Rating {
304 mu: mu_perf,
305 sig: sig_perf,
306 });
307 }
308 EloMMRVariant::Logistic(_) => {
309 let idx_subsample = idx_subsample
310 .map(|i| &tanh_terms[i]);
311 let f = |x| {
312 idx_subsample
313 .clone()
314 .map(|(rating, ranks)| {
315 rating.evals(x, &ranks, my_rank, self.split_ties)
316 })
317 .fold((0., 0.), |(s, sp), (v, vp)| (s + v, sp + vp))
318 };
319 let mu_perf = solve_newton(bounds, f);
320 player.update_rating_with_logistic(
321 Rating {
322 mu: mu_perf,
323 sig: sig_perf,
324 },
325 self.subsample_size,
326 );
327 }
328 };
329 });
330 }
331}