Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ab56248bb | ||
|
|
8dff7513f7 |
@@ -45,7 +45,7 @@ grows proportionally to time:
|
||||
variance_delta = elapsed * γ²
|
||||
```
|
||||
|
||||
This is the standard TrueSkill Through Time model. Pass a `ConstantDrift(gamma)`
|
||||
This is the standard TrueSkill Through Time model. Pass a `ConstantDrift::new(gamma)`
|
||||
when constructing a `Rating`:
|
||||
|
||||
```rust
|
||||
@@ -53,9 +53,9 @@ use trueskill_tt::{ConstantDrift, Gaussian, Rating};
|
||||
|
||||
// gamma = 0.1 means skill can shift ~0.1 per time unit.
|
||||
let rating: Rating<i64, ConstantDrift> =
|
||||
Rating::new(Gaussian::from_ms(0.0, 6.0), 1.0, ConstantDrift(0.1));
|
||||
Rating::new(Gaussian::from_ms(0.0, 6.0), 1.0, ConstantDrift::new(0.1));
|
||||
|
||||
assert_eq!(rating.drift().0, 0.1);
|
||||
assert_eq!(rating.drift().gamma(), 0.1);
|
||||
```
|
||||
|
||||
The type annotation is load-bearing: `ConstantDrift` implements `Drift<T>` for
|
||||
@@ -98,14 +98,14 @@ assert_eq!(history.log_evidence(), 0.0);
|
||||
```
|
||||
|
||||
`HistoryBuilder::drift` is the only way to set a history's drift model; there is
|
||||
no `gamma()` shorthand. The default is `ConstantDrift(GAMMA)`.
|
||||
no `gamma()` shorthand. The default is `ConstantDrift::new(GAMMA)`.
|
||||
|
||||
### Per-competitor drift
|
||||
|
||||
A `History` has one drift model, but individual competitors can scale it.
|
||||
`Member::with_drift_scale(s)` multiplies the drift *variance* that competitor
|
||||
accumulates, so `s` is in the same units as `gamma`: `ConstantDrift(g)` at
|
||||
scale `s` behaves exactly as `ConstantDrift(g * s)` would, for that competitor
|
||||
accumulates, so `s` is in the same units as `gamma`: `ConstantDrift::new(g)` at
|
||||
scale `s` behaves exactly as `ConstantDrift::new(g * s)` would, for that competitor
|
||||
alone.
|
||||
|
||||
`0.0` pins a competitor still. That is what makes a **fixed reference point**
|
||||
@@ -115,7 +115,7 @@ strength, a rating floor, a course difficulty:
|
||||
```rust
|
||||
use trueskill_tt::{ConstantDrift, Event, History, Member, Outcome, Team};
|
||||
|
||||
let mut h = History::builder().drift(ConstantDrift(0.1)).build();
|
||||
let mut h = History::builder().drift(ConstantDrift::new(0.1)).build();
|
||||
|
||||
h.add_events(vec![Event {
|
||||
time: 0,
|
||||
|
||||
+5
-1
@@ -17,7 +17,11 @@ fn criterion_benchmark(criterion: &mut Criterion) {
|
||||
agents.insert(
|
||||
agent,
|
||||
Competitor {
|
||||
rating: Rating::new(Gaussian::from_ms(MU, SIGMA), BETA, ConstantDrift(GAMMA)),
|
||||
rating: Rating::new(
|
||||
Gaussian::from_ms(MU, SIGMA),
|
||||
BETA,
|
||||
ConstantDrift::new(GAMMA),
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
@@ -47,7 +47,7 @@ fn build_history_1v1(
|
||||
.mu(25.0)
|
||||
.sigma(25.0 / 3.0)
|
||||
.beta(25.0 / 6.0)
|
||||
.drift(ConstantDrift(25.0 / 300.0))
|
||||
.drift(ConstantDrift::new(25.0 / 300.0))
|
||||
.convergence(ConvergenceOptions {
|
||||
max_iter: 30,
|
||||
epsilon: 1e-6,
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ fn fitted() -> History<i64, ConstantDrift, trueskill_tt::NullObserver, String> {
|
||||
.sigma(6.0)
|
||||
.beta(1.0)
|
||||
.score_sigma(2.0)
|
||||
.drift(ConstantDrift(0.05))
|
||||
.drift(ConstantDrift::new(0.05))
|
||||
.convergence(ConvergenceOptions {
|
||||
max_iter: 30,
|
||||
epsilon: 1e-10,
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ fn bench_scored_history(c: &mut Criterion) {
|
||||
.mu(25.0)
|
||||
.sigma(25.0 / 3.0)
|
||||
.beta(25.0 / 6.0)
|
||||
.drift(ConstantDrift(0.03))
|
||||
.drift(ConstantDrift::new(0.03))
|
||||
.score_sigma(2.0)
|
||||
.build();
|
||||
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ fn main() {
|
||||
|
||||
let mut hist: History<i64, _, _, String> = History::builder_with_key()
|
||||
.sigma(1.6)
|
||||
.drift(ConstantDrift(0.036))
|
||||
.drift(ConstantDrift::new(0.036))
|
||||
.convergence(trueskill_tt::ConvergenceOptions {
|
||||
// This history needs 30 sweeps to reach the epsilon below. It was
|
||||
// capped at 10 until the `#[must_use]` on `ConvergenceReport`
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ fn main() {
|
||||
.mu(25.0)
|
||||
.sigma(25.0 / 3.0)
|
||||
.beta(25.0 / 6.0)
|
||||
.drift(ConstantDrift(0.03))
|
||||
.drift(ConstantDrift::new(0.03))
|
||||
.score_sigma(2.0) // tune to data; smaller = trust margins more
|
||||
.build();
|
||||
|
||||
|
||||
+5
-1
@@ -208,7 +208,11 @@ mod tests {
|
||||
type R = Rating<i64, ConstantDrift>;
|
||||
|
||||
fn rating(mu: f64, sigma: f64) -> R {
|
||||
R::new(Gaussian::from_ms(mu, sigma), BETA, ConstantDrift(GAMMA))
|
||||
R::new(
|
||||
Gaussian::from_ms(mu, sigma),
|
||||
BETA,
|
||||
ConstantDrift::new(GAMMA),
|
||||
)
|
||||
}
|
||||
|
||||
fn options(p_draw: f64) -> GameOptions {
|
||||
|
||||
+46
-13
@@ -22,24 +22,57 @@ pub trait Drift<T: Time>: Copy + Debug + Send + Sync {
|
||||
/// For `Time = i64`: variance added is `(to - from) * gamma^2`.
|
||||
/// For `Time = Untimed`: elapsed is always 0, so drift is always 0.
|
||||
///
|
||||
/// # The sign of `gamma` is not meaningful
|
||||
/// # Why the field is private
|
||||
///
|
||||
/// `gamma` enters only as `gamma * gamma`, so `ConstantDrift(-0.05)` produces
|
||||
/// results **bit identical** to `ConstantDrift(0.05)`. That is the same
|
||||
/// sign-absorption `HistoryBuilder::sigma`, `HistoryBuilder::beta`,
|
||||
/// `Gaussian::from_ms` and `Rating::new` all reject outright.
|
||||
/// `gamma` enters only as `gamma * gamma`, so a negative value is squared away:
|
||||
/// measured against the old public-field form, `ConstantDrift(-0.0833)` produced
|
||||
/// results **bit identical** to `ConstantDrift(0.0833)`. The sign was neither
|
||||
/// rejected nor honoured — it vanished. That is the same sign-absorption `HistoryBuilder::sigma`,
|
||||
/// `HistoryBuilder::beta`, `Gaussian::from_ms` and `Rating::new` all reject.
|
||||
///
|
||||
/// It is not rejected here because the field is public and positional, so
|
||||
/// there is no constructor to intercept — sealing it would break every
|
||||
/// `ConstantDrift(x)` in existence for a case whose *resulting model* is
|
||||
/// perfectly valid, just not the one a caller writing a minus sign expected.
|
||||
/// It could not be checked while the field was a public tuple position, because
|
||||
/// there was no constructor to intercept. Validating inside
|
||||
/// `variance_for_elapsed` would have been worse: it runs inside the sweep, so a
|
||||
/// construction-time mistake would panic mid-inference — and `Gaussian::from_ms`
|
||||
/// is a worked example of why that is the wrong place for a guard, where
|
||||
/// rejecting NaN turned the `NonFiniteResult` reporting path into a crash.
|
||||
///
|
||||
/// A non-finite `gamma` is a different matter and **is** rejected:
|
||||
/// So [`ConstantDrift::new`] is the only way in, and it checks. Read the value
|
||||
/// back with [`ConstantDrift::gamma`].
|
||||
///
|
||||
/// A non-finite gamma is caught a second time regardless:
|
||||
/// `History::converge` validates the drift variance each competitor actually
|
||||
/// accumulates, which also covers a custom [`Drift`] implementation, and
|
||||
/// reports `InferenceError::InvalidParameter`.
|
||||
/// accumulates, which also covers a custom [`Drift`] implementation.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct ConstantDrift(pub f64);
|
||||
pub struct ConstantDrift(f64);
|
||||
|
||||
impl ConstantDrift {
|
||||
/// Drift of `gamma` standard deviations per unit time.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics unless `gamma` is finite and non-negative.
|
||||
///
|
||||
/// The field is private and this is the only constructor precisely so that
|
||||
/// there is somewhere to check. While it was a public tuple field there was
|
||||
/// nothing to intercept, and a negative gamma was silently squared away —
|
||||
/// see the type docs.
|
||||
#[must_use]
|
||||
pub fn new(gamma: f64) -> Self {
|
||||
assert!(
|
||||
gamma.is_finite() && gamma >= 0.0,
|
||||
"gamma must be finite and non-negative (got {gamma}); it is only ever \
|
||||
squared, so a negative value would silently behave as its absolute value"
|
||||
);
|
||||
Self(gamma)
|
||||
}
|
||||
|
||||
/// Standard deviations of drift accumulated per unit time.
|
||||
#[must_use]
|
||||
pub fn gamma(&self) -> f64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Time> Drift<T> for ConstantDrift {
|
||||
fn variance_delta(&self, from: &T, to: &T) -> f64 {
|
||||
|
||||
+2
-2
@@ -99,8 +99,8 @@ impl<K> Member<K> {
|
||||
/// Scale how fast this competitor drifts, relative to the history's drift.
|
||||
///
|
||||
/// The scale multiplies the drift *variance*, so it is in the same units as
|
||||
/// `gamma`: `ConstantDrift(g)` at `scale = s` behaves exactly as
|
||||
/// `ConstantDrift(g * s)` would for this competitor alone.
|
||||
/// `gamma`: `ConstantDrift::new(g)` at `scale = s` behaves exactly as
|
||||
/// `ConstantDrift::new(g * s)` would for this competitor alone.
|
||||
///
|
||||
/// `0.0` pins the competitor still — useful for a reference point that
|
||||
/// shares a scale with moving competitors but should not itself move: a bot
|
||||
|
||||
+63
-39
@@ -623,12 +623,12 @@ mod tests {
|
||||
let t_a = R::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
ConstantDrift::new(25.0 / 300.0),
|
||||
);
|
||||
let t_b = R::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
ConstantDrift::new(25.0 / 300.0),
|
||||
);
|
||||
|
||||
let w = [vec![1.0], vec![1.0]];
|
||||
@@ -651,12 +651,12 @@ mod tests {
|
||||
let t_a = R::new(
|
||||
Gaussian::from_ms(29.0, 1.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(GAMMA),
|
||||
ConstantDrift::new(GAMMA),
|
||||
);
|
||||
let t_b = R::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(GAMMA),
|
||||
ConstantDrift::new(GAMMA),
|
||||
);
|
||||
|
||||
let w = [vec![1.0], vec![1.0]];
|
||||
@@ -676,8 +676,16 @@ mod tests {
|
||||
assert_ulps_eq!(a, Gaussian::from_ms(28.896475, 0.996604), epsilon = 1e-6);
|
||||
assert_ulps_eq!(b, Gaussian::from_ms(32.189211, 6.062063), epsilon = 1e-6);
|
||||
|
||||
let t_a = R::new(Gaussian::from_ms(1.139, 0.531), 1.0, ConstantDrift(0.2125));
|
||||
let t_b = R::new(Gaussian::from_ms(15.568, 0.51), 1.0, ConstantDrift(0.2125));
|
||||
let t_a = R::new(
|
||||
Gaussian::from_ms(1.139, 0.531),
|
||||
1.0,
|
||||
ConstantDrift::new(0.2125),
|
||||
);
|
||||
let t_b = R::new(
|
||||
Gaussian::from_ms(15.568, 0.51),
|
||||
1.0,
|
||||
ConstantDrift::new(0.2125),
|
||||
);
|
||||
|
||||
let w = [vec![1.0], vec![1.0]];
|
||||
let g = Game::ranked_with_arena(
|
||||
@@ -699,17 +707,17 @@ mod tests {
|
||||
vec![R::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
ConstantDrift::new(25.0 / 300.0),
|
||||
)],
|
||||
vec![R::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
ConstantDrift::new(25.0 / 300.0),
|
||||
)],
|
||||
vec![R::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
ConstantDrift::new(25.0 / 300.0),
|
||||
)],
|
||||
];
|
||||
|
||||
@@ -779,12 +787,12 @@ mod tests {
|
||||
let t_a = R::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
ConstantDrift::new(25.0 / 300.0),
|
||||
);
|
||||
let t_b = R::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
ConstantDrift::new(25.0 / 300.0),
|
||||
);
|
||||
|
||||
let w = [vec![1.0], vec![1.0]];
|
||||
@@ -811,12 +819,12 @@ mod tests {
|
||||
let t_a = R::new(
|
||||
Gaussian::from_ms(25.0, 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
ConstantDrift::new(25.0 / 300.0),
|
||||
);
|
||||
let t_b = R::new(
|
||||
Gaussian::from_ms(29.0, 2.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
ConstantDrift::new(25.0 / 300.0),
|
||||
);
|
||||
|
||||
let w = [vec![1.0], vec![1.0]];
|
||||
@@ -842,17 +850,17 @@ mod tests {
|
||||
let t_a = R::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
ConstantDrift::new(25.0 / 300.0),
|
||||
);
|
||||
let t_b = R::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
ConstantDrift::new(25.0 / 300.0),
|
||||
);
|
||||
let t_c = R::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
ConstantDrift::new(25.0 / 300.0),
|
||||
);
|
||||
|
||||
let w = [vec![1.0], vec![1.0], vec![1.0]];
|
||||
@@ -879,17 +887,17 @@ mod tests {
|
||||
let t_a = R::new(
|
||||
Gaussian::from_ms(25.0, 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
ConstantDrift::new(25.0 / 300.0),
|
||||
);
|
||||
let t_b = R::new(
|
||||
Gaussian::from_ms(25.0, 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
ConstantDrift::new(25.0 / 300.0),
|
||||
);
|
||||
let t_c = R::new(
|
||||
Gaussian::from_ms(29.0, 2.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
ConstantDrift::new(25.0 / 300.0),
|
||||
);
|
||||
|
||||
let w = [vec![1.0], vec![1.0], vec![1.0]];
|
||||
@@ -918,29 +926,29 @@ mod tests {
|
||||
R::new(
|
||||
Gaussian::from_ms(12.0, 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
ConstantDrift::new(25.0 / 300.0),
|
||||
),
|
||||
R::new(
|
||||
Gaussian::from_ms(18.0, 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
ConstantDrift::new(25.0 / 300.0),
|
||||
),
|
||||
];
|
||||
let t_b = vec![R::new(
|
||||
Gaussian::from_ms(30.0, 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
ConstantDrift::new(25.0 / 300.0),
|
||||
)];
|
||||
let t_c = vec![
|
||||
R::new(
|
||||
Gaussian::from_ms(14.0, 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
ConstantDrift::new(25.0 / 300.0),
|
||||
),
|
||||
R::new(
|
||||
Gaussian::from_ms(16., 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
ConstantDrift::new(25.0 / 300.0),
|
||||
),
|
||||
];
|
||||
|
||||
@@ -970,12 +978,12 @@ mod tests {
|
||||
let t_a = vec![R::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(0.0),
|
||||
ConstantDrift::new(0.0),
|
||||
)];
|
||||
let t_b = vec![R::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(0.0),
|
||||
ConstantDrift::new(0.0),
|
||||
)];
|
||||
|
||||
let w = [w_a, w_b];
|
||||
@@ -1053,8 +1061,16 @@ mod tests {
|
||||
let w_a = vec![1.0];
|
||||
let w_b = vec![0.0];
|
||||
|
||||
let t_a = vec![R::new(Gaussian::from_ms(2.0, 6.0), 1.0, ConstantDrift(0.0))];
|
||||
let t_b = vec![R::new(Gaussian::from_ms(2.0, 6.0), 1.0, ConstantDrift(0.0))];
|
||||
let t_a = vec![R::new(
|
||||
Gaussian::from_ms(2.0, 6.0),
|
||||
1.0,
|
||||
ConstantDrift::new(0.0),
|
||||
)];
|
||||
let t_b = vec![R::new(
|
||||
Gaussian::from_ms(2.0, 6.0),
|
||||
1.0,
|
||||
ConstantDrift::new(0.0),
|
||||
)];
|
||||
|
||||
let w = [w_a, w_b];
|
||||
let g = Game::ranked_with_arena(
|
||||
@@ -1081,8 +1097,16 @@ mod tests {
|
||||
let w_a = vec![1.0];
|
||||
let w_b = vec![-1.0];
|
||||
|
||||
let t_a = vec![R::new(Gaussian::from_ms(2.0, 6.0), 1.0, ConstantDrift(0.0))];
|
||||
let t_b = vec![R::new(Gaussian::from_ms(2.0, 6.0), 1.0, ConstantDrift(0.0))];
|
||||
let t_a = vec![R::new(
|
||||
Gaussian::from_ms(2.0, 6.0),
|
||||
1.0,
|
||||
ConstantDrift::new(0.0),
|
||||
)];
|
||||
let t_b = vec![R::new(
|
||||
Gaussian::from_ms(2.0, 6.0),
|
||||
1.0,
|
||||
ConstantDrift::new(0.0),
|
||||
)];
|
||||
|
||||
let w = [w_a, w_b];
|
||||
let g = Game::ranked_with_arena(
|
||||
@@ -1125,7 +1149,7 @@ mod tests {
|
||||
let prior = R::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
ConstantDrift::new(25.0 / 300.0),
|
||||
);
|
||||
let teams = vec![vec![prior], vec![prior]];
|
||||
let result = vec![10.0, 0.0]; // a beat b by 10
|
||||
@@ -1175,7 +1199,7 @@ mod tests {
|
||||
let prior = R::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
ConstantDrift::new(25.0 / 300.0),
|
||||
);
|
||||
let opts = GameOptions {
|
||||
score_sigma: 1.0,
|
||||
@@ -1191,7 +1215,7 @@ mod tests {
|
||||
let prior = R::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
ConstantDrift::new(25.0 / 300.0),
|
||||
);
|
||||
let err = Game::scored(
|
||||
&[&[prior], &[prior]],
|
||||
@@ -1210,7 +1234,7 @@ mod tests {
|
||||
let prior = R::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
ConstantDrift::new(25.0 / 300.0),
|
||||
);
|
||||
let opts = GameOptions {
|
||||
score_sigma: 0.0,
|
||||
@@ -1237,12 +1261,12 @@ mod tests {
|
||||
R::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(0.0),
|
||||
ConstantDrift::new(0.0),
|
||||
),
|
||||
R::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(0.0),
|
||||
ConstantDrift::new(0.0),
|
||||
),
|
||||
];
|
||||
let w_a = vec![0.4, 0.8];
|
||||
@@ -1251,12 +1275,12 @@ mod tests {
|
||||
R::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(0.0),
|
||||
ConstantDrift::new(0.0),
|
||||
),
|
||||
R::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(0.0),
|
||||
ConstantDrift::new(0.0),
|
||||
),
|
||||
];
|
||||
let w_b = vec![0.9, 0.6];
|
||||
@@ -1370,7 +1394,7 @@ mod tests {
|
||||
vec![R::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(0.0),
|
||||
ConstantDrift::new(0.0),
|
||||
)],
|
||||
],
|
||||
&[1.0, 0.0],
|
||||
|
||||
+18
-18
@@ -139,8 +139,8 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> HistoryBuilder<
|
||||
/// Panics if `score_sigma` is not strictly positive.
|
||||
pub fn score_sigma(mut self, score_sigma: f64) -> Self {
|
||||
assert!(
|
||||
score_sigma > 0.0,
|
||||
"score_sigma must be positive (got {score_sigma})"
|
||||
score_sigma.is_finite() && score_sigma > 0.0,
|
||||
"score_sigma must be finite and positive (got {score_sigma})"
|
||||
);
|
||||
self.score_sigma = score_sigma;
|
||||
self
|
||||
@@ -224,7 +224,7 @@ impl Default for HistoryBuilder<i64, ConstantDrift, NullObserver, &'static str>
|
||||
mu: MU,
|
||||
sigma: SIGMA,
|
||||
beta: BETA,
|
||||
drift: ConstantDrift(GAMMA),
|
||||
drift: ConstantDrift::new(GAMMA),
|
||||
p_draw: P_DRAW,
|
||||
score_sigma: 1.0,
|
||||
convergence: ConvergenceOptions::default(),
|
||||
@@ -358,7 +358,7 @@ impl<K: Eq + Hash + Clone> History<i64, ConstantDrift, NullObserver, K> {
|
||||
mu: MU,
|
||||
sigma: SIGMA,
|
||||
beta: BETA,
|
||||
drift: ConstantDrift(GAMMA),
|
||||
drift: ConstantDrift::new(GAMMA),
|
||||
p_draw: P_DRAW,
|
||||
score_sigma: 1.0,
|
||||
convergence: ConvergenceOptions::default(),
|
||||
@@ -1663,7 +1663,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
||||
// instead, which also covers a custom impl.
|
||||
//
|
||||
// `ConstantDrift` returns `elapsed * gamma * gamma`, so a negative
|
||||
// gamma is squared away: measured, `ConstantDrift(-0.0833)` gave
|
||||
// gamma is squared away: measured, `ConstantDrift::new(-0.0833)` gave
|
||||
// results **bit identical** to `+0.0833`, the same sign-absorption
|
||||
// defect already rejected for `sigma` and `beta`. A non-finite gamma
|
||||
// poisons every posterior derived from it.
|
||||
@@ -2606,7 +2606,7 @@ mod tests {
|
||||
.mu(25.0)
|
||||
.sigma(25.0 / 3.0)
|
||||
.beta(25.0 / 6.0)
|
||||
.drift(ConstantDrift(0.15 * 25.0 / 3.0))
|
||||
.drift(ConstantDrift::new(0.15 * 25.0 / 3.0))
|
||||
.build();
|
||||
|
||||
let events = make_events_1v1(
|
||||
@@ -2671,7 +2671,7 @@ mod tests {
|
||||
.mu(25.0)
|
||||
.sigma(25.0 / 3.0)
|
||||
.beta(25.0 / 6.0)
|
||||
.drift(ConstantDrift(0.15 * 25.0 / 3.0))
|
||||
.drift(ConstantDrift::new(0.15 * 25.0 / 3.0))
|
||||
.build();
|
||||
|
||||
let events = make_events_1v1(
|
||||
@@ -2716,7 +2716,7 @@ mod tests {
|
||||
.mu(25.0)
|
||||
.sigma(25.0 / 3.0)
|
||||
.beta(25.0 / 6.0)
|
||||
.drift(ConstantDrift(25.0 / 300.0))
|
||||
.drift(ConstantDrift::new(25.0 / 300.0))
|
||||
.build();
|
||||
|
||||
let events = make_events_1v1(
|
||||
@@ -2764,7 +2764,7 @@ mod tests {
|
||||
.mu(25.0)
|
||||
.sigma(25.0 / 3.0)
|
||||
.beta(25.0 / 6.0)
|
||||
.drift(ConstantDrift(25.0 / 300.0))
|
||||
.drift(ConstantDrift::new(25.0 / 300.0))
|
||||
.build();
|
||||
|
||||
let events = make_events_1v1(
|
||||
@@ -2806,7 +2806,7 @@ mod tests {
|
||||
.mu(25.0)
|
||||
.sigma(25.0 / 3.0)
|
||||
.beta(25.0 / 6.0)
|
||||
.drift(ConstantDrift(25.0 / 300.0))
|
||||
.drift(ConstantDrift::new(25.0 / 300.0))
|
||||
.build();
|
||||
|
||||
let events = make_events_1v1(
|
||||
@@ -2851,7 +2851,7 @@ mod tests {
|
||||
.mu(0.0)
|
||||
.sigma(6.0)
|
||||
.beta(1.0)
|
||||
.drift(ConstantDrift(0.0))
|
||||
.drift(ConstantDrift::new(0.0))
|
||||
.build();
|
||||
|
||||
let events: Vec<Event<i64, &'static str>> = vec![
|
||||
@@ -2957,7 +2957,7 @@ mod tests {
|
||||
.mu(0.0)
|
||||
.sigma(2.0)
|
||||
.beta(1.0)
|
||||
.drift(ConstantDrift(0.0))
|
||||
.drift(ConstantDrift::new(0.0))
|
||||
.build();
|
||||
|
||||
let events = make_events_1v1(
|
||||
@@ -3054,7 +3054,7 @@ mod tests {
|
||||
.mu(0.0)
|
||||
.sigma(2.0)
|
||||
.beta(1.0)
|
||||
.drift(ConstantDrift(0.0))
|
||||
.drift(ConstantDrift::new(0.0))
|
||||
.build();
|
||||
|
||||
let events = make_events_1v1(
|
||||
@@ -3227,7 +3227,7 @@ mod tests {
|
||||
.mu(0.0)
|
||||
.sigma(2.0)
|
||||
.beta(1.0)
|
||||
.drift(ConstantDrift(0.0))
|
||||
.drift(ConstantDrift::new(0.0))
|
||||
.build();
|
||||
|
||||
let events = make_events_1v1(
|
||||
@@ -3330,7 +3330,7 @@ mod tests {
|
||||
.mu(0.0)
|
||||
.sigma(2.0)
|
||||
.beta(1.0)
|
||||
.drift(ConstantDrift(0.0))
|
||||
.drift(ConstantDrift::new(0.0))
|
||||
.build();
|
||||
|
||||
let events = make_events_1v1(
|
||||
@@ -3434,7 +3434,7 @@ mod tests {
|
||||
.mu(2.0)
|
||||
.sigma(6.0)
|
||||
.beta(1.0)
|
||||
.drift(ConstantDrift(0.0))
|
||||
.drift(ConstantDrift::new(0.0))
|
||||
.build();
|
||||
|
||||
// empty results in old API = team 0 wins: a wins event 1, b wins event 2
|
||||
@@ -3501,7 +3501,7 @@ mod tests {
|
||||
.mu(0.0)
|
||||
.sigma(2.0)
|
||||
.beta(1.0)
|
||||
.drift(ConstantDrift(0.0))
|
||||
.drift(ConstantDrift::new(0.0))
|
||||
.convergence(ConvergenceOptions {
|
||||
max_iter: 30,
|
||||
epsilon: 1e-6,
|
||||
@@ -3528,7 +3528,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "score_sigma must be positive")]
|
||||
#[should_panic(expected = "score_sigma must be finite and positive")]
|
||||
fn history_builder_rejects_zero_score_sigma() {
|
||||
let _ = History::builder().score_sigma(0.0).build();
|
||||
}
|
||||
|
||||
+1
-1
@@ -110,7 +110,7 @@ impl Default for Rating<i64, ConstantDrift> {
|
||||
Self {
|
||||
prior: Gaussian::default(),
|
||||
beta: BETA,
|
||||
drift: ConstantDrift(GAMMA),
|
||||
drift: ConstantDrift::new(GAMMA),
|
||||
drift_scale: 1.0,
|
||||
_time: PhantomData,
|
||||
}
|
||||
|
||||
+4
-4
@@ -911,7 +911,7 @@ mod tests {
|
||||
rating: Rating::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
ConstantDrift::new(25.0 / 300.0),
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
@@ -988,7 +988,7 @@ mod tests {
|
||||
rating: Rating::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
ConstantDrift::new(25.0 / 300.0),
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
@@ -1068,7 +1068,7 @@ mod tests {
|
||||
rating: Rating::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
ConstantDrift::new(25.0 / 300.0),
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
@@ -1171,7 +1171,7 @@ mod tests {
|
||||
rating: Rating::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
ConstantDrift::new(25.0 / 300.0),
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
|
||||
@@ -34,7 +34,7 @@ fn additive_structure_makes_sums_wide_and_differences_tight() {
|
||||
.sigma(6.0)
|
||||
.beta(1.0)
|
||||
.score_sigma(2.0)
|
||||
.drift(ConstantDrift(0.0))
|
||||
.drift(ConstantDrift::new(0.0))
|
||||
.convergence(ConvergenceOptions {
|
||||
max_iter: 20_000,
|
||||
epsilon: 1e-12,
|
||||
|
||||
+4
-4
@@ -11,7 +11,7 @@ fn add_events_bulk_via_iter() {
|
||||
.sigma(2.0)
|
||||
.beta(1.0)
|
||||
.p_draw(0.0)
|
||||
.drift(ConstantDrift(0.0))
|
||||
.drift(ConstantDrift::new(0.0))
|
||||
.convergence(ConvergenceOptions {
|
||||
max_iter: 30,
|
||||
epsilon: 1e-6,
|
||||
@@ -53,7 +53,7 @@ fn add_events_draw() {
|
||||
.sigma(25.0 / 3.0)
|
||||
.beta(25.0 / 6.0)
|
||||
.p_draw(0.25)
|
||||
.drift(ConstantDrift(25.0 / 300.0))
|
||||
.drift(ConstantDrift::new(25.0 / 300.0))
|
||||
.build();
|
||||
|
||||
let events: Vec<Event<i64, &'static str>> = vec![Event {
|
||||
@@ -181,7 +181,7 @@ fn log_evidence_total_vs_subset() {
|
||||
.sigma(6.0)
|
||||
.beta(1.0)
|
||||
.p_draw(0.0)
|
||||
.drift(ConstantDrift(0.0))
|
||||
.drift(ConstantDrift::new(0.0))
|
||||
.build();
|
||||
h.record_winner(&"a", &"b", 1).unwrap();
|
||||
h.record_winner(&"b", &"a", 2).unwrap();
|
||||
@@ -236,7 +236,7 @@ fn fluent_event_builder_scores() {
|
||||
.mu(25.0)
|
||||
.sigma(25.0 / 3.0)
|
||||
.beta(25.0 / 6.0)
|
||||
.drift(ConstantDrift(0.0))
|
||||
.drift(ConstantDrift::new(0.0))
|
||||
.build();
|
||||
|
||||
h.event(1)
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
//! Every public entry point that takes a magnitude, in one place.
|
||||
//!
|
||||
//! This defect class was closed three times in one session and reopened twice,
|
||||
//! because each fix validated the layer it had just touched and inferred the
|
||||
//! rest: `HistoryBuilder` first, then `Game`'s own entry points, then the
|
||||
//! constructors beneath both. A per-site fix cannot notice the site nobody
|
||||
//! thought of.
|
||||
//!
|
||||
//! So this enumerates them. `sigma`, `beta` and `gamma` all enter inference
|
||||
//! only as squares, which means a negative value does not fail — it behaves as
|
||||
//! its absolute value, bit for bit, and the sign vanishes with no diagnostic.
|
||||
//! Non-finite values poison every posterior derived from them.
|
||||
//!
|
||||
//! Adding a public constructor that takes one of these and not adding it here
|
||||
//! is the failure this file exists to make harder.
|
||||
|
||||
use std::panic::{AssertUnwindSafe, catch_unwind};
|
||||
|
||||
use trueskill_tt::{ConstantDrift, Gaussian, History, Member, Outcome, Rating};
|
||||
|
||||
/// Did the entry point refuse the value, by panic or by `Err`?
|
||||
fn refuses(f: impl FnOnce() -> bool) -> bool {
|
||||
catch_unwind(AssertUnwindSafe(f)).unwrap_or(true)
|
||||
}
|
||||
|
||||
/// One entry point, as a name and a closure that applies a value to it.
|
||||
type Case = (&'static str, Box<dyn Fn(f64) -> bool>);
|
||||
|
||||
/// Entry points that must reject a negative magnitude.
|
||||
///
|
||||
/// Each closure returns `true` if it refused by returning an error; a panic is
|
||||
/// also a refusal and is caught.
|
||||
#[test]
|
||||
fn every_magnitude_parameter_rejects_a_negative_value() {
|
||||
let cases: Vec<Case> = vec![
|
||||
(
|
||||
"Gaussian::from_ms(sigma)",
|
||||
Box::new(|v| {
|
||||
let _ = Gaussian::from_ms(25.0, v);
|
||||
false
|
||||
}),
|
||||
),
|
||||
(
|
||||
"Rating::new(beta)",
|
||||
Box::new(|v| {
|
||||
let _ = Rating::<i64, ConstantDrift>::new(
|
||||
Gaussian::default(),
|
||||
v,
|
||||
ConstantDrift::new(0.0),
|
||||
);
|
||||
false
|
||||
}),
|
||||
),
|
||||
(
|
||||
"ConstantDrift::new(gamma)",
|
||||
Box::new(|v| {
|
||||
let _ = ConstantDrift::new(v);
|
||||
false
|
||||
}),
|
||||
),
|
||||
(
|
||||
"HistoryBuilder::sigma",
|
||||
Box::new(|v| {
|
||||
let _ = History::builder().sigma(v);
|
||||
false
|
||||
}),
|
||||
),
|
||||
(
|
||||
"HistoryBuilder::beta",
|
||||
Box::new(|v| {
|
||||
let _ = History::builder().beta(v);
|
||||
false
|
||||
}),
|
||||
),
|
||||
(
|
||||
"HistoryBuilder::score_sigma",
|
||||
Box::new(|v| {
|
||||
let _ = History::builder().score_sigma(v);
|
||||
false
|
||||
}),
|
||||
),
|
||||
(
|
||||
"HistoryBuilder::p_draw",
|
||||
Box::new(|v| {
|
||||
let _ = History::builder().p_draw(v);
|
||||
false
|
||||
}),
|
||||
),
|
||||
(
|
||||
"Member::with_drift_scale (at ingestion)",
|
||||
Box::new(|v| {
|
||||
let mut h = History::builder().build();
|
||||
h.add_events(vec![trueskill_tt::Event {
|
||||
time: 1i64,
|
||||
teams: smallvec::smallvec![
|
||||
trueskill_tt::Team::with_members([Member::new("a").with_drift_scale(v)]),
|
||||
trueskill_tt::Team::with_members([Member::new("b")]),
|
||||
],
|
||||
outcome: Outcome::winner(0, 2),
|
||||
}])
|
||||
.is_err()
|
||||
}),
|
||||
),
|
||||
(
|
||||
"Outcome::scores_with_sigma (at ingestion)",
|
||||
Box::new(|v| {
|
||||
let mut h = History::builder().build();
|
||||
h.add_events(vec![trueskill_tt::Event {
|
||||
time: 1i64,
|
||||
teams: smallvec::smallvec![
|
||||
trueskill_tt::Team::with_members([Member::new("a")]),
|
||||
trueskill_tt::Team::with_members([Member::new("b")]),
|
||||
],
|
||||
outcome: Outcome::scores_with_sigma([3.0, 1.0], v),
|
||||
}])
|
||||
.is_err()
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
let mut accepted = Vec::new();
|
||||
for (name, f) in &cases {
|
||||
if !refuses(|| f(-1.0)) {
|
||||
accepted.push(*name);
|
||||
}
|
||||
}
|
||||
|
||||
assert!(
|
||||
accepted.is_empty(),
|
||||
"these accepted a negative magnitude, which is squared away silently \
|
||||
rather than honoured or refused:\n {}",
|
||||
accepted.join("\n ")
|
||||
);
|
||||
}
|
||||
|
||||
/// Same set, for NaN and infinity.
|
||||
///
|
||||
/// `Gaussian::from_ms` is deliberately absent: a broken fit produces a NaN
|
||||
/// sigma legitimately and `converge` reports it as `NonFiniteResult`. Rejecting
|
||||
/// it in the constructor turned that reporting path into a panic inside
|
||||
/// inference — see the comment on `from_ms`.
|
||||
#[test]
|
||||
fn every_magnitude_parameter_rejects_a_non_finite_value() {
|
||||
let cases: Vec<Case> = vec![
|
||||
(
|
||||
"Rating::new(beta)",
|
||||
Box::new(|v| {
|
||||
let _ = Rating::<i64, ConstantDrift>::new(
|
||||
Gaussian::default(),
|
||||
v,
|
||||
ConstantDrift::new(0.0),
|
||||
);
|
||||
false
|
||||
}),
|
||||
),
|
||||
(
|
||||
"ConstantDrift::new(gamma)",
|
||||
Box::new(|v| {
|
||||
let _ = ConstantDrift::new(v);
|
||||
false
|
||||
}),
|
||||
),
|
||||
(
|
||||
"HistoryBuilder::sigma",
|
||||
Box::new(|v| {
|
||||
let _ = History::builder().sigma(v);
|
||||
false
|
||||
}),
|
||||
),
|
||||
(
|
||||
"HistoryBuilder::beta",
|
||||
Box::new(|v| {
|
||||
let _ = History::builder().beta(v);
|
||||
false
|
||||
}),
|
||||
),
|
||||
(
|
||||
"HistoryBuilder::mu",
|
||||
Box::new(|v| {
|
||||
let _ = History::builder().mu(v);
|
||||
false
|
||||
}),
|
||||
),
|
||||
(
|
||||
"HistoryBuilder::score_sigma",
|
||||
Box::new(|v| {
|
||||
let _ = History::builder().score_sigma(v);
|
||||
false
|
||||
}),
|
||||
),
|
||||
(
|
||||
"HistoryBuilder::p_draw",
|
||||
Box::new(|v| {
|
||||
let _ = History::builder().p_draw(v);
|
||||
false
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
let mut accepted = Vec::new();
|
||||
for (name, f) in &cases {
|
||||
for bad in [f64::NAN, f64::INFINITY] {
|
||||
if !refuses(|| f(bad)) {
|
||||
accepted.push(format!("{name} accepted {bad}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert!(
|
||||
accepted.is_empty(),
|
||||
"these accepted a non-finite magnitude:\n {}",
|
||||
accepted.join("\n ")
|
||||
);
|
||||
}
|
||||
|
||||
/// The suite must not pass by refusing everything.
|
||||
#[test]
|
||||
fn ordinary_values_are_still_accepted() {
|
||||
let _ = Gaussian::from_ms(25.0, 8.33);
|
||||
let _ = Rating::<i64, ConstantDrift>::new(Gaussian::default(), 4.17, ConstantDrift::new(0.05));
|
||||
let _ = ConstantDrift::new(0.0833);
|
||||
let _ = History::builder()
|
||||
.mu(25.0)
|
||||
.sigma(8.33)
|
||||
.beta(4.17)
|
||||
.score_sigma(1.0)
|
||||
.p_draw(0.1);
|
||||
|
||||
// Zero beta and zero gamma are legitimate, not degenerate.
|
||||
let _ = ConstantDrift::new(0.0);
|
||||
let _ = Rating::<i64, ConstantDrift>::new(Gaussian::default(), 0.0, ConstantDrift::new(0.0));
|
||||
}
|
||||
@@ -30,7 +30,7 @@ fn capped(max_iter: usize) -> H {
|
||||
.sigma(6.0)
|
||||
.beta(1.0)
|
||||
.score_sigma(2.0)
|
||||
.drift(ConstantDrift(0.5))
|
||||
.drift(ConstantDrift::new(0.5))
|
||||
.convergence(ConvergenceOptions {
|
||||
max_iter,
|
||||
epsilon: 1e-13,
|
||||
@@ -112,7 +112,7 @@ fn the_default_cap_clears_an_ordinary_history() {
|
||||
.sigma(6.0)
|
||||
.beta(1.0)
|
||||
.score_sigma(2.0)
|
||||
.drift(ConstantDrift(0.05))
|
||||
.drift(ConstantDrift::new(0.05))
|
||||
.build();
|
||||
|
||||
let mut events = Vec::new();
|
||||
|
||||
@@ -32,7 +32,7 @@ fn fitted() -> H {
|
||||
.sigma(6.0)
|
||||
.beta(1.0)
|
||||
.score_sigma(2.0)
|
||||
.drift(ConstantDrift(0.05))
|
||||
.drift(ConstantDrift::new(0.05))
|
||||
.unknown_keys(UnknownKeys::Prior)
|
||||
.convergence(ConvergenceOptions {
|
||||
max_iter: 20_000,
|
||||
|
||||
@@ -17,7 +17,7 @@ fn rating() -> R {
|
||||
R::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
ConstantDrift::new(25.0 / 300.0),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -281,8 +281,16 @@ fn log_evidence_survives_a_long_diff_chain() {
|
||||
/// `erfc` approximation; the evidence floor keeps `ln` finite.
|
||||
#[test]
|
||||
fn log_evidence_finite_for_near_certain_outcome() {
|
||||
let overwhelming = R::new(Gaussian::from_ms(5_000.0, 0.5), 1.0, ConstantDrift(0.0));
|
||||
let hopeless = R::new(Gaussian::from_ms(-5_000.0, 0.5), 1.0, ConstantDrift(0.0));
|
||||
let overwhelming = R::new(
|
||||
Gaussian::from_ms(5_000.0, 0.5),
|
||||
1.0,
|
||||
ConstantDrift::new(0.0),
|
||||
);
|
||||
let hopeless = R::new(
|
||||
Gaussian::from_ms(-5_000.0, 0.5),
|
||||
1.0,
|
||||
ConstantDrift::new(0.0),
|
||||
);
|
||||
let a = [overwhelming];
|
||||
let b = [hopeless];
|
||||
let teams: Vec<&[R]> = vec![&a, &b];
|
||||
|
||||
@@ -43,7 +43,7 @@ fn build_and_converge() -> Fingerprint {
|
||||
.mu(25.0)
|
||||
.sigma(25.0 / 3.0)
|
||||
.beta(25.0 / 6.0)
|
||||
.drift(ConstantDrift(25.0 / 300.0))
|
||||
.drift(ConstantDrift::new(25.0 / 300.0))
|
||||
.convergence(ConvergenceOptions {
|
||||
max_iter: 20_000,
|
||||
epsilon: 1e-9,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//!
|
||||
//! The scale multiplies the *variance* the history's `Drift` contributes for
|
||||
//! that competitor, so `scale` is in the same units as `gamma`:
|
||||
//! `ConstantDrift(g)` at `scale = s` behaves as `ConstantDrift(g * s)` would.
|
||||
//! `ConstantDrift::new(g)` at `scale = s` behaves as `ConstantDrift::new(g * s)` would.
|
||||
//! `scale = 0.0` pins a competitor still — an anchor, a rating floor, a course
|
||||
//! difficulty — while everyone around them keeps drifting.
|
||||
|
||||
@@ -53,7 +53,7 @@ fn fit(events: Vec<Event<i64, &'static str>>, gamma: f64) -> Fit {
|
||||
.sigma(25.0 / 3.0)
|
||||
.beta(25.0 / 6.0)
|
||||
.p_draw(0.0)
|
||||
.drift(ConstantDrift(gamma))
|
||||
.drift(ConstantDrift::new(gamma))
|
||||
.convergence(CONVERGENCE)
|
||||
.build();
|
||||
|
||||
@@ -160,7 +160,7 @@ fn scale_is_equivalent_to_scaling_gamma() {
|
||||
assert_eq!(t_l, t_r);
|
||||
assert!(
|
||||
(g_l.mu() - g_r.mu()).abs() < 1e-9 && (g_l.sigma() - g_r.sigma()).abs() < 1e-9,
|
||||
"ConstantDrift(0.3) at scale 0.5 must equal ConstantDrift(0.15) for {key} at \
|
||||
"ConstantDrift::new(0.3) at scale 0.5 must equal ConstantDrift::new(0.15) for {key} at \
|
||||
t={t_l}: ({}, {}) vs ({}, {})",
|
||||
g_l.mu(),
|
||||
g_l.sigma(),
|
||||
@@ -218,7 +218,7 @@ fn mixed_static_and_drifting_graph_converges() {
|
||||
.sigma(25.0 / 3.0)
|
||||
.beta(25.0 / 6.0)
|
||||
.p_draw(0.0)
|
||||
.drift(ConstantDrift(25.0 / 300.0))
|
||||
.drift(ConstantDrift::new(25.0 / 300.0))
|
||||
.convergence(CONVERGENCE)
|
||||
.build();
|
||||
|
||||
@@ -259,7 +259,7 @@ fn mixed_static_and_drifting_graph_converges() {
|
||||
|
||||
fn reject(scale: f64) -> InferenceError {
|
||||
let mut h = History::builder()
|
||||
.drift(ConstantDrift(25.0 / 300.0))
|
||||
.drift(ConstantDrift::new(25.0 / 300.0))
|
||||
.build();
|
||||
|
||||
let events: Vec<Event<i64, &'static str>> = vec![Event {
|
||||
@@ -360,7 +360,7 @@ fn drift_scale_applies_when_set_after_first_appearance() {
|
||||
.sigma(25.0 / 3.0)
|
||||
.beta(25.0 / 6.0)
|
||||
.p_draw(0.0)
|
||||
.drift(ConstantDrift(25.0 / 300.0))
|
||||
.drift(ConstantDrift::new(25.0 / 300.0))
|
||||
.convergence(CONVERGENCE)
|
||||
.build();
|
||||
|
||||
|
||||
@@ -12,7 +12,11 @@ use trueskill_tt::{ConstantDrift, Game, GameOptions, Gaussian, Outcome, Rating};
|
||||
type R = Rating<i64, ConstantDrift>;
|
||||
|
||||
fn ts_rating(mu: f64, sigma: f64, beta: f64, gamma: f64) -> R {
|
||||
R::new(Gaussian::from_ms(mu, sigma), beta, ConstantDrift(gamma))
|
||||
R::new(
|
||||
Gaussian::from_ms(mu, sigma),
|
||||
beta,
|
||||
ConstantDrift::new(gamma),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -19,7 +19,7 @@ fn history() -> H {
|
||||
.sigma(6.0)
|
||||
.beta(1.0)
|
||||
.score_sigma(2.0)
|
||||
.drift(ConstantDrift(0.5))
|
||||
.drift(ConstantDrift::new(0.5))
|
||||
.convergence(ConvergenceOptions {
|
||||
max_iter: 20_000,
|
||||
epsilon: 1e-13,
|
||||
|
||||
+3
-3
@@ -8,7 +8,7 @@ fn default_rating() -> R {
|
||||
R::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(25.0 / 300.0),
|
||||
ConstantDrift::new(25.0 / 300.0),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ fn game_one_v_one_shortcut() {
|
||||
|
||||
#[test]
|
||||
fn game_ranked_rejects_bad_p_draw() {
|
||||
let a = R::new(Gaussian::default(), 1.0, ConstantDrift(0.0));
|
||||
let a = R::new(Gaussian::default(), 1.0, ConstantDrift::new(0.0));
|
||||
let err = Game::<i64, _>::ranked(
|
||||
&[&[a], &[a]],
|
||||
Outcome::winner(0, 2),
|
||||
@@ -56,7 +56,7 @@ fn game_ranked_rejects_bad_p_draw() {
|
||||
|
||||
#[test]
|
||||
fn game_ranked_rejects_mismatched_ranks() {
|
||||
let a = R::new(Gaussian::default(), 1.0, ConstantDrift(0.0));
|
||||
let a = R::new(Gaussian::default(), 1.0, ConstantDrift::new(0.0));
|
||||
let err = Game::<i64, _>::ranked(
|
||||
&[&[a], &[a]],
|
||||
Outcome::ranking([0, 1, 2]),
|
||||
|
||||
@@ -41,7 +41,7 @@ fn history(unknown: UnknownKeys) -> H {
|
||||
.sigma(6.0)
|
||||
.beta(1.0)
|
||||
.score_sigma(2.0)
|
||||
.drift(ConstantDrift(0.5))
|
||||
.drift(ConstantDrift::new(0.5))
|
||||
.unknown_keys(unknown)
|
||||
.convergence(ConvergenceOptions {
|
||||
max_iter: 20_000,
|
||||
@@ -158,7 +158,7 @@ fn drift_free_competitors_shrink_the_joint_by_the_slice_count() {
|
||||
.sigma(6.0)
|
||||
.beta(1.0)
|
||||
.score_sigma(2.0)
|
||||
.drift(ConstantDrift(gamma))
|
||||
.drift(ConstantDrift::new(gamma))
|
||||
.convergence(ConvergenceOptions {
|
||||
max_iter: 20_000,
|
||||
epsilon: 1e-13,
|
||||
@@ -197,7 +197,7 @@ fn pinned_competitors_collapse_consecutive_appearances() {
|
||||
.sigma(6.0)
|
||||
.beta(1.0)
|
||||
.score_sigma(2.0)
|
||||
.drift(ConstantDrift(0.0))
|
||||
.drift(ConstantDrift::new(0.0))
|
||||
.convergence(ConvergenceOptions {
|
||||
max_iter: 20_000,
|
||||
epsilon: 1e-13,
|
||||
@@ -284,7 +284,7 @@ fn a_drift_too_small_to_represent_collapses_rather_than_corrupting() {
|
||||
.sigma(6.0)
|
||||
.beta(1.0)
|
||||
.score_sigma(2.0)
|
||||
.drift(ConstantDrift(0.5))
|
||||
.drift(ConstantDrift::new(0.5))
|
||||
.convergence(ConvergenceOptions {
|
||||
max_iter: 20_000,
|
||||
epsilon: 1e-13,
|
||||
|
||||
@@ -27,7 +27,7 @@ fn nan_after_fit(players: usize) -> usize {
|
||||
let mut h: History<i64, ConstantDrift, NullObserver, String> = History::builder_with_key()
|
||||
.beta(1.0)
|
||||
.sigma(6.0)
|
||||
.drift(ConstantDrift(0.1))
|
||||
.drift(ConstantDrift::new(0.1))
|
||||
.convergence(ConvergenceOptions {
|
||||
max_iter: ITERATIONS,
|
||||
epsilon: EPSILON,
|
||||
|
||||
@@ -140,7 +140,7 @@ fn fitted(
|
||||
.sigma(SIGMA0)
|
||||
.beta(BETA)
|
||||
.score_sigma(SCORE_SIGMA)
|
||||
.drift(ConstantDrift(0.0))
|
||||
.drift(ConstantDrift::new(0.0))
|
||||
.convergence(ConvergenceOptions {
|
||||
max_iter: 20_000,
|
||||
epsilon: 1e-13,
|
||||
@@ -324,7 +324,7 @@ fn cost_scaling() {
|
||||
let names: Vec<String> = (0..n).map(|i| format!("c{i}")).collect();
|
||||
let mut h: History<i64, _, _, String> = History::builder_with_key()
|
||||
.score_sigma(2.0)
|
||||
.drift(ConstantDrift(0.0))
|
||||
.drift(ConstantDrift::new(0.0))
|
||||
.convergence(ConvergenceOptions {
|
||||
max_iter: 200,
|
||||
epsilon: 1e-8,
|
||||
|
||||
@@ -191,7 +191,7 @@ fn a_narrow_draw_margin_far_into_the_tail_still_fits() {
|
||||
.sigma(sd)
|
||||
.beta(beta)
|
||||
.p_draw(p_draw)
|
||||
.drift(ConstantDrift(0.0))
|
||||
.drift(ConstantDrift::new(0.0))
|
||||
.build();
|
||||
h.add_events(vec![Event {
|
||||
time: 1i64,
|
||||
|
||||
@@ -14,7 +14,7 @@ fn builder(
|
||||
.sigma(6.0)
|
||||
.beta(1.0)
|
||||
.score_sigma(2.0)
|
||||
.drift(ConstantDrift(0.0))
|
||||
.drift(ConstantDrift::new(0.0))
|
||||
.unknown_keys(policy)
|
||||
.convergence(ConvergenceOptions {
|
||||
max_iter: 5_000,
|
||||
|
||||
@@ -74,8 +74,16 @@ fn information_gain_never_exceeds_the_entropy_of_the_outcome() {
|
||||
let sigma_b = rng.log_uniform(1e-4, 1e2);
|
||||
let beta = rng.log_uniform(1e-4, 1e1);
|
||||
|
||||
let a = R::new(Gaussian::from_ms(mu_a, sigma_a), beta, ConstantDrift(0.0));
|
||||
let b = R::new(Gaussian::from_ms(mu_b, sigma_b), beta, ConstantDrift(0.0));
|
||||
let a = R::new(
|
||||
Gaussian::from_ms(mu_a, sigma_a),
|
||||
beta,
|
||||
ConstantDrift::new(0.0),
|
||||
);
|
||||
let b = R::new(
|
||||
Gaussian::from_ms(mu_b, sigma_b),
|
||||
beta,
|
||||
ConstantDrift::new(0.0),
|
||||
);
|
||||
let options = GameOptions {
|
||||
p_draw: 0.0,
|
||||
..GameOptions::default()
|
||||
@@ -129,12 +137,12 @@ fn the_known_ceiling_violation_no_longer_answers_wrongly() {
|
||||
let a = R::new(
|
||||
Gaussian::from_ms(9.577_887_112_129_012, 0.000_132_507_526_585_134_38),
|
||||
0.000_307_235_559_013_096_2,
|
||||
ConstantDrift(0.0),
|
||||
ConstantDrift::new(0.0),
|
||||
);
|
||||
let b = R::new(
|
||||
Gaussian::from_ms(-14.114_932_828_525_696, 91.586_690_140_921_16),
|
||||
0.000_307_235_559_013_096_2,
|
||||
ConstantDrift(0.0),
|
||||
ConstantDrift::new(0.0),
|
||||
);
|
||||
let options = GameOptions {
|
||||
p_draw: 0.0,
|
||||
|
||||
@@ -6,7 +6,7 @@ fn record_winner_builds_history() {
|
||||
.mu(25.0)
|
||||
.sigma(25.0 / 3.0)
|
||||
.beta(25.0 / 6.0)
|
||||
.drift(ConstantDrift(25.0 / 300.0))
|
||||
.drift(ConstantDrift::new(25.0 / 300.0))
|
||||
.convergence(ConvergenceOptions {
|
||||
max_iter: 30,
|
||||
epsilon: 1e-6,
|
||||
@@ -43,7 +43,7 @@ fn record_draw_with_p_draw_set() {
|
||||
.mu(25.0)
|
||||
.sigma(25.0 / 3.0)
|
||||
.beta(25.0 / 6.0)
|
||||
.drift(ConstantDrift(25.0 / 300.0))
|
||||
.drift(ConstantDrift::new(25.0 / 300.0))
|
||||
.p_draw(0.25)
|
||||
.build();
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ fn history() -> H {
|
||||
.sigma(6.0)
|
||||
.beta(1.0)
|
||||
.score_sigma(2.0)
|
||||
.drift(ConstantDrift(0.5))
|
||||
.drift(ConstantDrift::new(0.5))
|
||||
.convergence(ConvergenceOptions {
|
||||
max_iter: 20_000,
|
||||
epsilon: 1e-13,
|
||||
|
||||
+3
-3
@@ -9,7 +9,7 @@ fn scored_two_team_one_event_pulls_winner_up() {
|
||||
.mu(0.0)
|
||||
.sigma(2.0)
|
||||
.beta(1.0)
|
||||
.drift(ConstantDrift(0.0))
|
||||
.drift(ConstantDrift::new(0.0))
|
||||
.score_sigma(1.0)
|
||||
.build();
|
||||
|
||||
@@ -46,7 +46,7 @@ fn scored_zero_margin_treats_as_tie() {
|
||||
.mu(0.0)
|
||||
.sigma(2.0)
|
||||
.beta(1.0)
|
||||
.drift(ConstantDrift(0.0))
|
||||
.drift(ConstantDrift::new(0.0))
|
||||
.score_sigma(1.0)
|
||||
.build();
|
||||
|
||||
@@ -88,7 +88,7 @@ fn scored_three_team_partial_order() {
|
||||
.mu(0.0)
|
||||
.sigma(2.0)
|
||||
.beta(1.0)
|
||||
.drift(ConstantDrift(0.0))
|
||||
.drift(ConstantDrift::new(0.0))
|
||||
.score_sigma(1.0)
|
||||
.build();
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ fn history(gamma: f64) -> H {
|
||||
.sigma(SIGMA0)
|
||||
.beta(BETA)
|
||||
.score_sigma(SCORE_SIGMA)
|
||||
.drift(ConstantDrift(gamma))
|
||||
.drift(ConstantDrift::new(gamma))
|
||||
.unknown_keys(UnknownKeys::Reject)
|
||||
.convergence(ConvergenceOptions {
|
||||
max_iter: 20_000,
|
||||
|
||||
+50
-17
@@ -22,7 +22,7 @@ fn rating() -> R {
|
||||
R::new(
|
||||
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||
25.0 / 6.0,
|
||||
ConstantDrift(0.0),
|
||||
ConstantDrift::new(0.0),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -286,33 +286,66 @@ mod constructor_parameters {
|
||||
#[test]
|
||||
#[should_panic(expected = "beta must be finite and non-negative")]
|
||||
fn a_negative_beta_is_rejected_by_rating_new() {
|
||||
let _ = Rating::<i64, ConstantDrift>::new(Gaussian::default(), -4.17, ConstantDrift(0.0));
|
||||
let _ =
|
||||
Rating::<i64, ConstantDrift>::new(Gaussian::default(), -4.17, ConstantDrift::new(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "beta must be finite and non-negative")]
|
||||
fn a_nan_beta_is_rejected_by_rating_new() {
|
||||
let _ =
|
||||
Rating::<i64, ConstantDrift>::new(Gaussian::default(), f64::NAN, ConstantDrift(0.0));
|
||||
let _ = Rating::<i64, ConstantDrift>::new(
|
||||
Gaussian::default(),
|
||||
f64::NAN,
|
||||
ConstantDrift::new(0.0),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_zero_beta_is_accepted_by_rating_new() {
|
||||
let _ = Rating::<i64, ConstantDrift>::new(Gaussian::default(), 0.0, ConstantDrift(0.0));
|
||||
let _ =
|
||||
Rating::<i64, ConstantDrift>::new(Gaussian::default(), 0.0, ConstantDrift::new(0.0));
|
||||
}
|
||||
|
||||
/// `ConstantDrift` rejects at construction now that its field is private.
|
||||
#[test]
|
||||
#[should_panic(expected = "gamma must be finite and non-negative")]
|
||||
fn a_negative_gamma_is_rejected_by_constant_drift_new() {
|
||||
let _ = ConstantDrift::new(-0.0833);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "gamma must be finite and non-negative")]
|
||||
fn a_non_finite_gamma_is_rejected_by_constant_drift_new() {
|
||||
let _ = ConstantDrift::new(f64::NAN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gamma_reads_back_what_was_given() {
|
||||
assert_eq!(ConstantDrift::new(0.25).gamma(), 0.25);
|
||||
assert_eq!(ConstantDrift::new(0.0).gamma(), 0.0);
|
||||
}
|
||||
|
||||
/// `HistoryBuilder::drift` is generic and cannot inspect an arbitrary
|
||||
/// `Drift`, so the check is on the variance each competitor actually
|
||||
/// accumulates. That also covers a custom implementation.
|
||||
/// `Drift`, so the check on the variance each competitor accumulates is
|
||||
/// still needed — it is the only thing standing between a custom
|
||||
/// implementation and a NaN fit. `ConstantDrift` can no longer reach it,
|
||||
/// so this uses an implementation that can.
|
||||
#[test]
|
||||
fn a_non_finite_drift_is_rejected_at_convergence() {
|
||||
for gamma in [f64::NAN, f64::INFINITY] {
|
||||
let mut h = History::builder()
|
||||
.mu(25.0)
|
||||
.sigma(25.0 / 3.0)
|
||||
.beta(25.0 / 6.0)
|
||||
.drift(ConstantDrift(gamma))
|
||||
.build();
|
||||
fn a_custom_drift_returning_a_bad_variance_is_rejected_at_convergence() {
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct BadDrift(f64);
|
||||
|
||||
impl trueskill_tt::Drift<i64> for BadDrift {
|
||||
fn variance_delta(&self, _from: &i64, _to: &i64) -> f64 {
|
||||
self.0
|
||||
}
|
||||
fn variance_for_elapsed(&self, _elapsed: i64) -> f64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
for bad in [f64::NAN, f64::INFINITY, -1.0] {
|
||||
let mut h = History::builder().drift(BadDrift(bad)).build();
|
||||
h.record_winner(&"a", &"b", 1).unwrap();
|
||||
h.record_winner(&"a", &"b", 5).unwrap();
|
||||
let err = h.converge().unwrap_err();
|
||||
@@ -324,7 +357,7 @@ mod constructor_parameters {
|
||||
..
|
||||
}
|
||||
),
|
||||
"gamma {gamma}: {err:?}"
|
||||
"drift {bad}: {err:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -333,7 +366,7 @@ mod constructor_parameters {
|
||||
#[test]
|
||||
fn an_ordinary_drift_still_converges() {
|
||||
let mut h = History::builder()
|
||||
.drift(ConstantDrift(25.0 / 300.0))
|
||||
.drift(ConstantDrift::new(25.0 / 300.0))
|
||||
.build();
|
||||
h.record_winner(&"a", &"b", 1).unwrap();
|
||||
h.record_winner(&"a", &"b", 5).unwrap();
|
||||
|
||||
@@ -35,7 +35,7 @@ fn fit(extra: Option<Event<i64, &'static str>>, policy: UnknownKeys) -> H {
|
||||
.sigma(6.0)
|
||||
.beta(1.0)
|
||||
.score_sigma(2.0)
|
||||
.drift(ConstantDrift(0.0))
|
||||
.drift(ConstantDrift::new(0.0))
|
||||
.unknown_keys(policy)
|
||||
.convergence(ConvergenceOptions {
|
||||
max_iter: 20_000,
|
||||
|
||||
Reference in New Issue
Block a user