Compare commits
2
Commits
1629176199
...
0b9997354d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b9997354d | ||
|
|
c4194b0051 |
@@ -38,14 +38,15 @@ use crate::{
|
|||||||
/// ```
|
/// ```
|
||||||
#[must_use = "an event is only recorded by `.commit()`; a dropped builder \
|
#[must_use = "an event is only recorded by `.commit()`; a dropped builder \
|
||||||
silently ingests nothing"]
|
silently ingests nothing"]
|
||||||
pub struct EventBuilder<'h, T, D, O, K>
|
pub struct EventBuilder<'h, T, D, O, K, R>
|
||||||
where
|
where
|
||||||
T: Time,
|
T: Time,
|
||||||
D: Drift<T>,
|
D: Drift<T>,
|
||||||
O: Observer<T>,
|
O: Observer<T>,
|
||||||
K: Eq + std::hash::Hash + Clone,
|
K: Eq + std::hash::Hash + Clone,
|
||||||
|
R: crate::RatingRule<K>,
|
||||||
{
|
{
|
||||||
history: &'h mut History<K, T, D, O>,
|
history: &'h mut History<K, T, D, O, R>,
|
||||||
event: Event<T, K>,
|
event: Event<T, K>,
|
||||||
current_team_idx: Option<usize>,
|
current_team_idx: Option<usize>,
|
||||||
/// First validation failure seen while building, surfaced by `commit`.
|
/// First validation failure seen while building, surfaced by `commit`.
|
||||||
@@ -58,14 +59,15 @@ where
|
|||||||
error: Option<InferenceError>,
|
error: Option<InferenceError>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'h, T, D, O, K> EventBuilder<'h, T, D, O, K>
|
impl<'h, T, D, O, K, R> EventBuilder<'h, T, D, O, K, R>
|
||||||
where
|
where
|
||||||
T: Time,
|
T: Time,
|
||||||
D: Drift<T>,
|
D: Drift<T>,
|
||||||
O: Observer<T>,
|
O: Observer<T>,
|
||||||
K: Eq + std::hash::Hash + Clone,
|
K: Eq + std::hash::Hash + Clone,
|
||||||
|
R: crate::RatingRule<K>,
|
||||||
{
|
{
|
||||||
pub(crate) fn new(history: &'h mut History<K, T, D, O>, time: T) -> Self {
|
pub(crate) fn new(history: &'h mut History<K, T, D, O, R>, time: T) -> Self {
|
||||||
Self {
|
Self {
|
||||||
history,
|
history,
|
||||||
event: Event {
|
event: Event {
|
||||||
|
|||||||
+126
-23
@@ -17,6 +17,7 @@ use crate::{
|
|||||||
observer::{NullObserver, Observer},
|
observer::{NullObserver, Observer},
|
||||||
predict::Prediction,
|
predict::Prediction,
|
||||||
rating::Rating,
|
rating::Rating,
|
||||||
|
rating_rule::{FnRule, NoRule, RatingRule, StartingPoint},
|
||||||
sort_time,
|
sort_time,
|
||||||
storage::CompetitorStore,
|
storage::CompetitorStore,
|
||||||
time::Time,
|
time::Time,
|
||||||
@@ -32,7 +33,7 @@ use crate::{
|
|||||||
/// an unknown key. None of them can be changed after `build`, because they
|
/// an unknown key. None of them can be changed after `build`, because they
|
||||||
/// define the model the fit is of.
|
/// define the model the fit is of.
|
||||||
///
|
///
|
||||||
/// Parameterised as [`History`] is, `HistoryBuilder<K, T, D, O>`, with the
|
/// Parameterised as [`History`] is, `HistoryBuilder<K, T, D, O, R>`, with the
|
||||||
/// same defaults.
|
/// same defaults.
|
||||||
///
|
///
|
||||||
/// Two of the setters change the builder's *type* rather than a field —
|
/// Two of the setters change the builder's *type* rather than a field —
|
||||||
@@ -47,6 +48,7 @@ pub struct HistoryBuilder<
|
|||||||
T: Time = i64,
|
T: Time = i64,
|
||||||
D: Drift<T> = ConstantDrift,
|
D: Drift<T> = ConstantDrift,
|
||||||
O: Observer<T> = NullObserver,
|
O: Observer<T> = NullObserver,
|
||||||
|
R: RatingRule<K> = NoRule,
|
||||||
> {
|
> {
|
||||||
mu: f64,
|
mu: f64,
|
||||||
sigma: f64,
|
sigma: f64,
|
||||||
@@ -57,11 +59,14 @@ pub struct HistoryBuilder<
|
|||||||
convergence: ConvergenceOptions,
|
convergence: ConvergenceOptions,
|
||||||
observer: O,
|
observer: O,
|
||||||
unknown_keys: crate::UnknownKeys,
|
unknown_keys: crate::UnknownKeys,
|
||||||
|
rule: R,
|
||||||
_time: PhantomData<T>,
|
_time: PhantomData<T>,
|
||||||
_key: PhantomData<K>,
|
_key: PhantomData<K>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> HistoryBuilder<K, T, D, O> {
|
impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone, R: RatingRule<K>>
|
||||||
|
HistoryBuilder<K, T, D, O, R>
|
||||||
|
{
|
||||||
/// Prior mean skill.
|
/// Prior mean skill.
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
@@ -153,9 +158,10 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> HistoryBuilder<
|
|||||||
/// implementation. `converge` checks the variance each competitor actually
|
/// implementation. `converge` checks the variance each competitor actually
|
||||||
/// accumulates and reports `InvalidParameter` if it is negative or
|
/// accumulates and reports `InvalidParameter` if it is negative or
|
||||||
/// non-finite.
|
/// non-finite.
|
||||||
pub fn drift<D2: Drift<T>>(self, drift: D2) -> HistoryBuilder<K, T, D2, O> {
|
pub fn drift<D2: Drift<T>>(self, drift: D2) -> HistoryBuilder<K, T, D2, O, R> {
|
||||||
HistoryBuilder {
|
HistoryBuilder {
|
||||||
drift,
|
drift,
|
||||||
|
rule: self.rule,
|
||||||
mu: self.mu,
|
mu: self.mu,
|
||||||
sigma: self.sigma,
|
sigma: self.sigma,
|
||||||
beta: self.beta,
|
beta: self.beta,
|
||||||
@@ -252,13 +258,14 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> HistoryBuilder<
|
|||||||
/// assert!(h.current_skill(&"alice").unwrap().mu() > 0.0);
|
/// assert!(h.current_skill(&"alice").unwrap().mu() > 0.0);
|
||||||
/// # Ok::<(), trueskill_tt::InferenceError>(())
|
/// # Ok::<(), trueskill_tt::InferenceError>(())
|
||||||
/// ```
|
/// ```
|
||||||
pub fn time_type<T2>(self) -> HistoryBuilder<K, T2, D, O>
|
pub fn time_type<T2>(self) -> HistoryBuilder<K, T2, D, O, R>
|
||||||
where
|
where
|
||||||
T2: Time,
|
T2: Time,
|
||||||
D: Drift<T2>,
|
D: Drift<T2>,
|
||||||
O: Observer<T2>,
|
O: Observer<T2>,
|
||||||
{
|
{
|
||||||
HistoryBuilder {
|
HistoryBuilder {
|
||||||
|
rule: self.rule,
|
||||||
mu: self.mu,
|
mu: self.mu,
|
||||||
sigma: self.sigma,
|
sigma: self.sigma,
|
||||||
beta: self.beta,
|
beta: self.beta,
|
||||||
@@ -286,8 +293,79 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> HistoryBuilder<
|
|||||||
/// h.record_winner(&"alice".to_string(), &"bob".to_string(), 1)?;
|
/// h.record_winner(&"alice".to_string(), &"bob".to_string(), 1)?;
|
||||||
/// # Ok::<(), trueskill_tt::InferenceError>(())
|
/// # Ok::<(), trueskill_tt::InferenceError>(())
|
||||||
/// ```
|
/// ```
|
||||||
pub fn key_type<K2: Eq + Hash + Clone>(self) -> HistoryBuilder<K2, T, D, O> {
|
pub fn key_type<K2: Eq + Hash + Clone>(self) -> HistoryBuilder<K2, T, D, O, NoRule> {
|
||||||
HistoryBuilder {
|
HistoryBuilder {
|
||||||
|
// A `RatingRule<K>` cannot answer questions about `K2`, so
|
||||||
|
// changing the key type drops it. Set the key type first.
|
||||||
|
rule: NoRule,
|
||||||
|
mu: self.mu,
|
||||||
|
sigma: self.sigma,
|
||||||
|
beta: self.beta,
|
||||||
|
drift: self.drift,
|
||||||
|
p_draw: self.p_draw,
|
||||||
|
score_sigma: self.score_sigma,
|
||||||
|
convergence: self.convergence,
|
||||||
|
observer: self.observer,
|
||||||
|
unknown_keys: self.unknown_keys,
|
||||||
|
_time: PhantomData,
|
||||||
|
_key: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Supply a rule that configures competitors the history has not seen.
|
||||||
|
///
|
||||||
|
/// `register` states configuration for one competitor at a time, which
|
||||||
|
/// needs the key set up front. This states it for a *class* — "every
|
||||||
|
/// layout is static" — as one statement that cannot be forgotten on an
|
||||||
|
/// ingestion path.
|
||||||
|
///
|
||||||
|
/// Consulted once per competitor, at creation. Explicit configuration from
|
||||||
|
/// `register` or a [`Member`](crate::Member) overrides it field by field;
|
||||||
|
/// see [`RatingRule`] for why the specific beats the general here while
|
||||||
|
/// two explicit declarations that disagree stay an error.
|
||||||
|
///
|
||||||
|
/// Changes the builder's type — bind the result — and must come *after*
|
||||||
|
/// [`key_type`](HistoryBuilder::key_type), since a `RatingRule<K>` cannot
|
||||||
|
/// answer questions about a different key type.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use trueskill_tt::{Gaussian, History, StartingPoint};
|
||||||
|
/// let h = History::builder()
|
||||||
|
/// .default_rating_for(|key: &&'static str| {
|
||||||
|
/// key.starts_with("bot_")
|
||||||
|
/// .then(|| StartingPoint::new().drift_scale(0.0))
|
||||||
|
/// })
|
||||||
|
/// .build();
|
||||||
|
/// # let _ = h;
|
||||||
|
/// ```
|
||||||
|
pub fn default_rating_for<F>(self, rule: F) -> HistoryBuilder<K, T, D, O, FnRule<F>>
|
||||||
|
where
|
||||||
|
F: Fn(&K) -> Option<StartingPoint>,
|
||||||
|
{
|
||||||
|
HistoryBuilder {
|
||||||
|
rule: FnRule(rule),
|
||||||
|
mu: self.mu,
|
||||||
|
sigma: self.sigma,
|
||||||
|
beta: self.beta,
|
||||||
|
drift: self.drift,
|
||||||
|
p_draw: self.p_draw,
|
||||||
|
score_sigma: self.score_sigma,
|
||||||
|
convergence: self.convergence,
|
||||||
|
observer: self.observer,
|
||||||
|
unknown_keys: self.unknown_keys,
|
||||||
|
_time: PhantomData,
|
||||||
|
_key: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Supply a rule as a named type implementing [`RatingRule`].
|
||||||
|
///
|
||||||
|
/// The counterpart of [`default_rating_for`](HistoryBuilder::default_rating_for)
|
||||||
|
/// for when the resulting `History<..>` has to be written down in a struct
|
||||||
|
/// field, which a closure's type makes impossible.
|
||||||
|
pub fn rating_rule<R2: RatingRule<K>>(self, rule: R2) -> HistoryBuilder<K, T, D, O, R2> {
|
||||||
|
HistoryBuilder {
|
||||||
|
rule,
|
||||||
mu: self.mu,
|
mu: self.mu,
|
||||||
sigma: self.sigma,
|
sigma: self.sigma,
|
||||||
beta: self.beta,
|
beta: self.beta,
|
||||||
@@ -308,8 +386,9 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> HistoryBuilder<
|
|||||||
/// observer by value; to keep a handle on one that accumulates state, pass
|
/// observer by value; to keep a handle on one that accumulates state, pass
|
||||||
/// an `Arc` and keep a clone, or read it back with
|
/// an `Arc` and keep a clone, or read it back with
|
||||||
/// [`History::observer`] / [`History::into_observer`].
|
/// [`History::observer`] / [`History::into_observer`].
|
||||||
pub fn observer<O2: Observer<T>>(self, observer: O2) -> HistoryBuilder<K, T, D, O2> {
|
pub fn observer<O2: Observer<T>>(self, observer: O2) -> HistoryBuilder<K, T, D, O2, R> {
|
||||||
HistoryBuilder {
|
HistoryBuilder {
|
||||||
|
rule: self.rule,
|
||||||
mu: self.mu,
|
mu: self.mu,
|
||||||
sigma: self.sigma,
|
sigma: self.sigma,
|
||||||
beta: self.beta,
|
beta: self.beta,
|
||||||
@@ -327,8 +406,9 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> HistoryBuilder<
|
|||||||
/// Finish configuring and produce an empty [`History`].
|
/// Finish configuring and produce an empty [`History`].
|
||||||
///
|
///
|
||||||
/// Every parameter was validated as it was set, so this cannot fail.
|
/// Every parameter was validated as it was set, so this cannot fail.
|
||||||
pub fn build(self) -> History<K, T, D, O> {
|
pub fn build(self) -> History<K, T, D, O, R> {
|
||||||
History {
|
History {
|
||||||
|
rule: self.rule,
|
||||||
size: 0,
|
size: 0,
|
||||||
time_slices: Vec::new(),
|
time_slices: Vec::new(),
|
||||||
competitors: CompetitorStore::new(),
|
competitors: CompetitorStore::new(),
|
||||||
@@ -357,6 +437,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> HistoryBuilder<
|
|||||||
impl<T: Time, K: Eq + Hash + Clone> Default for HistoryBuilder<K, T, ConstantDrift, NullObserver> {
|
impl<T: Time, K: Eq + Hash + Clone> Default for HistoryBuilder<K, T, ConstantDrift, NullObserver> {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
rule: NoRule,
|
||||||
mu: MU,
|
mu: MU,
|
||||||
sigma: SIGMA,
|
sigma: SIGMA,
|
||||||
beta: BETA,
|
beta: BETA,
|
||||||
@@ -451,7 +532,7 @@ impl CompetitorConfig {
|
|||||||
///
|
///
|
||||||
/// # Type parameters
|
/// # Type parameters
|
||||||
///
|
///
|
||||||
/// `History<K, T, D, O>` — key type, time type, drift model, observer — all
|
/// `History<K, T, D, O, R>` — key type, time type, drift model, observer — all
|
||||||
/// defaulted, so `History` alone means `&'static str` keys on an `i64` time
|
/// defaulted, so `History` alone means `&'static str` keys on an `i64` time
|
||||||
/// axis with [`ConstantDrift`] and no observer, and `History<String>` is the
|
/// axis with [`ConstantDrift`] and no observer, and `History<String>` is the
|
||||||
/// whole spelling for owned keys.
|
/// whole spelling for owned keys.
|
||||||
@@ -465,6 +546,7 @@ pub struct History<
|
|||||||
T: Time = i64,
|
T: Time = i64,
|
||||||
D: Drift<T> = ConstantDrift,
|
D: Drift<T> = ConstantDrift,
|
||||||
O: Observer<T> = NullObserver,
|
O: Observer<T> = NullObserver,
|
||||||
|
R: RatingRule<K> = NoRule,
|
||||||
> {
|
> {
|
||||||
size: usize,
|
size: usize,
|
||||||
pub(crate) time_slices: Vec<TimeSlice<T>>,
|
pub(crate) time_slices: Vec<TimeSlice<T>>,
|
||||||
@@ -478,6 +560,8 @@ pub struct History<
|
|||||||
score_sigma: f64,
|
score_sigma: f64,
|
||||||
convergence: ConvergenceOptions,
|
convergence: ConvergenceOptions,
|
||||||
observer: O,
|
observer: O,
|
||||||
|
/// Supplies a starting point for competitors nobody declared explicitly.
|
||||||
|
rule: R,
|
||||||
unknown_keys: crate::UnknownKeys,
|
unknown_keys: crate::UnknownKeys,
|
||||||
/// Competitor configuration explicitly declared so far, by whichever route.
|
/// Competitor configuration explicitly declared so far, by whichever route.
|
||||||
///
|
///
|
||||||
@@ -552,7 +636,9 @@ impl<T: Time, K: Eq + Hash + Clone> HistoryBuilder<K, T, ConstantDrift, NullObse
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<K, T, D, O> {
|
impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone, R: RatingRule<K>>
|
||||||
|
History<K, T, D, O, R>
|
||||||
|
{
|
||||||
/// Promote a key to its [`Index`], creating the entry if it is new.
|
/// Promote a key to its [`Index`], creating the entry if it is new.
|
||||||
///
|
///
|
||||||
/// Crate-internal since #73: interning reserves a storage slot and nothing
|
/// Crate-internal since #73: interning reserves a storage slot and nothing
|
||||||
@@ -567,7 +653,9 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<K, T, D
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<K, T, D, O> {
|
impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone, R: RatingRule<K>>
|
||||||
|
History<K, T, D, O, R>
|
||||||
|
{
|
||||||
fn iteration(&mut self) -> (f64, f64) {
|
fn iteration(&mut self) -> (f64, f64) {
|
||||||
let mut step = (0.0, 0.0);
|
let mut step = (0.0, 0.0);
|
||||||
|
|
||||||
@@ -791,10 +879,13 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<K, T, D
|
|||||||
self.beta,
|
self.beta,
|
||||||
self.drift,
|
self.drift,
|
||||||
);
|
);
|
||||||
if let Some(prior) = member.prior {
|
// The rule first, then the explicit values over the top of it: the
|
||||||
|
// specific beats the general, field by field. See `RatingRule`.
|
||||||
|
let from_rule = self.rule.starting_point(&member.key).unwrap_or_default();
|
||||||
|
if let Some(prior) = member.prior.or(from_rule.prior) {
|
||||||
rating.prior = prior;
|
rating.prior = prior;
|
||||||
}
|
}
|
||||||
if let Some(scale) = member.drift_scale {
|
if let Some(scale) = member.drift_scale.or(from_rule.drift_scale) {
|
||||||
rating.drift_scale = scale;
|
rating.drift_scale = scale;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1617,7 +1708,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<K, T, D
|
|||||||
///
|
///
|
||||||
/// `JointUnavailable` if the history is empty, contains ranked events, or
|
/// `JointUnavailable` if the history is empty, contains ranked events, or
|
||||||
/// yields a precision matrix that is not positive-definite.
|
/// yields a precision matrix that is not positive-definite.
|
||||||
pub fn joint(&self) -> Result<Joint<'_, K, T, D, O>, InferenceError> {
|
pub fn joint(&self) -> Result<Joint<'_, K, T, D, O, R>, InferenceError> {
|
||||||
if self.time_slices.is_empty() {
|
if self.time_slices.is_empty() {
|
||||||
return Err(InferenceError::JointUnavailable {
|
return Err(InferenceError::JointUnavailable {
|
||||||
reason: "the history has no events",
|
reason: "the history has no events",
|
||||||
@@ -2088,7 +2179,9 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<K, T, D
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<K, T, D, O> {
|
impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone, R: RatingRule<K>>
|
||||||
|
History<K, T, D, O, R>
|
||||||
|
{
|
||||||
pub(crate) fn add_events_with_prior(
|
pub(crate) fn add_events_with_prior(
|
||||||
&mut self,
|
&mut self,
|
||||||
mut composition: Vec<Vec<Vec<Index>>>,
|
mut composition: Vec<Vec<Vec<Index>>>,
|
||||||
@@ -2325,10 +2418,17 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<K, T, D
|
|||||||
self.beta,
|
self.beta,
|
||||||
self.drift,
|
self.drift,
|
||||||
);
|
);
|
||||||
if let Some(prior) = config.prior {
|
// Same precedence as `register`: the rule supplies a starting
|
||||||
|
// point, explicit configuration overrides it field by field.
|
||||||
|
let from_rule = self
|
||||||
|
.keys
|
||||||
|
.key(*competitor)
|
||||||
|
.and_then(|key| self.rule.starting_point(key))
|
||||||
|
.unwrap_or_default();
|
||||||
|
if let Some(prior) = config.prior.or(from_rule.prior) {
|
||||||
rating.prior = prior;
|
rating.prior = prior;
|
||||||
}
|
}
|
||||||
if let Some(scale) = config.drift_scale {
|
if let Some(scale) = config.drift_scale.or(from_rule.drift_scale) {
|
||||||
rating.drift_scale = scale;
|
rating.drift_scale = scale;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2533,7 +2633,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<K, T, D
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Start a fluent event builder for a single match at `time`.
|
/// Start a fluent event builder for a single match at `time`.
|
||||||
pub fn event(&mut self, time: T) -> crate::event_builder::EventBuilder<'_, T, D, O, K> {
|
pub fn event(&mut self, time: T) -> crate::event_builder::EventBuilder<'_, T, D, O, K, R> {
|
||||||
crate::event_builder::EventBuilder::new(self, time)
|
crate::event_builder::EventBuilder::new(self, time)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2693,8 +2793,8 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<K, T, D
|
|||||||
///
|
///
|
||||||
/// It exists at all because without it a consumer cannot `#[derive(Debug)]` on
|
/// It exists at all because without it a consumer cannot `#[derive(Debug)]` on
|
||||||
/// any struct holding a `History`, which is how both known consumers store it.
|
/// any struct holding a `History`, which is how both known consumers store it.
|
||||||
impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> std::fmt::Debug
|
impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone, R: RatingRule<K>> std::fmt::Debug
|
||||||
for History<K, T, D, O>
|
for History<K, T, D, O, R>
|
||||||
{
|
{
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
f.debug_struct("History")
|
f.debug_struct("History")
|
||||||
@@ -2758,8 +2858,9 @@ pub struct Joint<
|
|||||||
T: Time = i64,
|
T: Time = i64,
|
||||||
D: Drift<T> = ConstantDrift,
|
D: Drift<T> = ConstantDrift,
|
||||||
O: Observer<T> = NullObserver,
|
O: Observer<T> = NullObserver,
|
||||||
|
R: RatingRule<K> = NoRule,
|
||||||
> {
|
> {
|
||||||
history: &'h History<K, T, D, O>,
|
history: &'h History<K, T, D, O, R>,
|
||||||
cholesky: crate::joint::Cholesky,
|
cholesky: crate::joint::Cholesky,
|
||||||
/// `(row, slice)` of each competitor's latest appearance.
|
/// `(row, slice)` of each competitor's latest appearance.
|
||||||
latest: HashMap<Index, (usize, usize)>,
|
latest: HashMap<Index, (usize, usize)>,
|
||||||
@@ -2771,8 +2872,8 @@ pub struct Joint<
|
|||||||
|
|
||||||
/// Deliberately does not print the factorisation, which is `n^2` floats and
|
/// Deliberately does not print the factorisation, which is `n^2` floats and
|
||||||
/// would make a `{:?}` of a large joint unreadable and slow.
|
/// would make a `{:?}` of a large joint unreadable and slow.
|
||||||
impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> std::fmt::Debug
|
impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone, R: RatingRule<K>> std::fmt::Debug
|
||||||
for Joint<'_, K, T, D, O>
|
for Joint<'_, K, T, D, O, R>
|
||||||
{
|
{
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
f.debug_struct("Joint")
|
f.debug_struct("Joint")
|
||||||
@@ -2781,7 +2882,9 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> std::fmt::Debug
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<K: Eq + Hash + Clone, T: Time, D: Drift<T>, O: Observer<T>> Joint<'_, K, T, D, O> {
|
impl<K: Eq + Hash + Clone, T: Time, D: Drift<T>, O: Observer<T>, R: RatingRule<K>>
|
||||||
|
Joint<'_, K, T, D, O, R>
|
||||||
|
{
|
||||||
/// Number of variables in the joint: the history's appearances, after
|
/// Number of variables in the joint: the history's appearances, after
|
||||||
/// collapsing consecutive pairs a competitor does not drift between.
|
/// collapsing consecutive pairs a competitor does not drift between.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ mod outcome;
|
|||||||
mod predict;
|
mod predict;
|
||||||
pub(crate) mod quadrature;
|
pub(crate) mod quadrature;
|
||||||
mod rating;
|
mod rating;
|
||||||
|
pub mod rating_rule;
|
||||||
pub(crate) mod storage;
|
pub(crate) mod storage;
|
||||||
mod time;
|
mod time;
|
||||||
mod time_slice;
|
mod time_slice;
|
||||||
@@ -162,6 +163,7 @@ pub use observer::{NullObserver, Observer};
|
|||||||
pub use outcome::Outcome;
|
pub use outcome::Outcome;
|
||||||
pub use predict::Prediction;
|
pub use predict::Prediction;
|
||||||
pub use rating::Rating;
|
pub use rating::Rating;
|
||||||
|
pub use rating_rule::{FnRule, NoRule, RatingRule, StartingPoint};
|
||||||
/// The `smallvec` crate, re-exported.
|
/// The `smallvec` crate, re-exported.
|
||||||
///
|
///
|
||||||
/// Four public items name `SmallVec` in their signatures: [`Event::teams`],
|
/// Four public items name `SmallVec` in their signatures: [`Event::teams`],
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
//! Declarative competitor configuration: a rule that supplies defaults for
|
||||||
|
//! competitors the history has not seen yet.
|
||||||
|
//!
|
||||||
|
//! [`History::register`](crate::History::register) states configuration for
|
||||||
|
//! *one* competitor, which covers a bot at a known strength or a handful of
|
||||||
|
//! reference points. It does not cover a *rule* — "every layout is static" —
|
||||||
|
//! because enumerating the keys means knowing the full key set up front, which
|
||||||
|
//! a consumer ingesting an event stream generally does not.
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! use trueskill_tt::{Gaussian, History, StartingPoint};
|
||||||
|
//!
|
||||||
|
//! let mut h = History::builder()
|
||||||
|
//! // Layouts do not improve; everybody else does.
|
||||||
|
//! .default_rating_for(|key: &&'static str| {
|
||||||
|
//! key.starts_with("layout_")
|
||||||
|
//! .then(|| StartingPoint::new().prior(Gaussian::from_ms(0.0, 1.0)).drift_scale(0.0))
|
||||||
|
//! })
|
||||||
|
//! .build();
|
||||||
|
//!
|
||||||
|
//! h.event(1).team(["layout_7"]).team(["alice"]).scores([3.0, 1.0]).commit()?;
|
||||||
|
//! h.converge()?;
|
||||||
|
//!
|
||||||
|
//! // The layout was pinned, so its uncertainty barely moved.
|
||||||
|
//! assert!(h.current_skill("layout_7").unwrap().sigma() < 1.0);
|
||||||
|
//! # Ok::<(), trueskill_tt::InferenceError>(())
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! # Why a trait, and why a fifth type parameter
|
||||||
|
//!
|
||||||
|
//! The rule is a type parameter on [`History`](crate::History), defaulted to
|
||||||
|
//! [`NoRule`], so it costs a caller who does not use one exactly nothing —
|
||||||
|
//! `History<String>` still spells out in full. A boxed `dyn Fn` would have
|
||||||
|
//! avoided the parameter at the price of `HistoryBuilder`'s derived `Clone`
|
||||||
|
//! and `Debug`.
|
||||||
|
//!
|
||||||
|
//! It is a trait rather than a bare `Fn` bound because a closure's type cannot
|
||||||
|
//! be written down, and the motivating consumer holds its `History` in
|
||||||
|
//! application state — so it has to name the type in a struct field. Implement
|
||||||
|
//! [`RatingRule`] on a named type of your own and that field is spellable.
|
||||||
|
//!
|
||||||
|
//! # What a rule may set, and what it may not
|
||||||
|
//!
|
||||||
|
//! A [`StartingPoint`], which is the same pair a
|
||||||
|
//! [`Member`](crate::Member) may carry: the prior and the drift scale. Not
|
||||||
|
//! `beta` and not the drift model — those describe the *history*, not one
|
||||||
|
//! competitor, and a rule that could vary them would be describing a different
|
||||||
|
//! model per competitor rather than a starting point within one.
|
||||||
|
//!
|
||||||
|
//! Keeping the rule to those two also keeps it independent of the history's
|
||||||
|
//! time and drift types, so [`HistoryBuilder::drift`](crate::HistoryBuilder::drift)
|
||||||
|
//! and [`HistoryBuilder::time_type`](crate::HistoryBuilder::time_type) still
|
||||||
|
//! work after a rule is set.
|
||||||
|
|
||||||
|
use crate::gaussian::Gaussian;
|
||||||
|
|
||||||
|
/// What a [`RatingRule`] may say about a competitor.
|
||||||
|
///
|
||||||
|
/// Both fields are optional and are applied independently, so a rule that sets
|
||||||
|
/// only `drift_scale` does not also assert a prior — the same reason
|
||||||
|
/// `Member`'s configuration is carried as "what was explicitly set" rather
|
||||||
|
/// than as a merged `Rating`.
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||||
|
#[must_use]
|
||||||
|
pub struct StartingPoint {
|
||||||
|
pub(crate) prior: Option<Gaussian>,
|
||||||
|
pub(crate) drift_scale: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StartingPoint {
|
||||||
|
/// A starting point that says nothing yet.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start this competitor from `prior` instead of the history's
|
||||||
|
/// `mu`/`sigma`.
|
||||||
|
pub fn prior(mut self, prior: Gaussian) -> Self {
|
||||||
|
self.prior = Some(prior);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scale how fast this competitor drifts, relative to the history's drift
|
||||||
|
/// model. `0.0` pins them still.
|
||||||
|
pub fn drift_scale(mut self, drift_scale: f64) -> Self {
|
||||||
|
self.drift_scale = Some(drift_scale);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Supplies a [`StartingPoint`] for competitors the history has not seen.
|
||||||
|
///
|
||||||
|
/// Consulted once per competitor, when that competitor is created — not per
|
||||||
|
/// event and not per sweep. Returning `None` means "no opinion": the
|
||||||
|
/// competitor takes the history's own defaults.
|
||||||
|
///
|
||||||
|
/// # Precedence
|
||||||
|
///
|
||||||
|
/// Explicit configuration wins, field by field. A `prior` or `drift_scale`
|
||||||
|
/// from [`History::register`](crate::History::register) or from a
|
||||||
|
/// [`Member`](crate::Member) overrides whatever the rule returned for that
|
||||||
|
/// competitor. The specific beats the general, which is the only reading that
|
||||||
|
/// lets a rule have exceptions — treating the disagreement as
|
||||||
|
/// `ConflictingCompetitorConfig` would make one exceptional competitor
|
||||||
|
/// incompatible with having any rule at all.
|
||||||
|
///
|
||||||
|
/// Two *explicit* declarations that disagree remain an error. Neither of those
|
||||||
|
/// is more specific than the other, so there is nothing to prefer.
|
||||||
|
pub trait RatingRule<K> {
|
||||||
|
/// Where this competitor should start, or `None` for the history's
|
||||||
|
/// defaults.
|
||||||
|
fn starting_point(&self, key: &K) -> Option<StartingPoint>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The default rule: no opinion about anybody.
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||||
|
pub struct NoRule;
|
||||||
|
|
||||||
|
impl<K> RatingRule<K> for NoRule {
|
||||||
|
#[inline]
|
||||||
|
fn starting_point(&self, _key: &K) -> Option<StartingPoint> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [`RatingRule`] built from a closure by
|
||||||
|
/// [`HistoryBuilder::default_rating_for`](crate::HistoryBuilder::default_rating_for).
|
||||||
|
///
|
||||||
|
/// Public so it can be named where a closure's own type cannot be, though
|
||||||
|
/// implementing [`RatingRule`] on a named type of your own is the better way
|
||||||
|
/// to get a `History<..>` you can write down in a struct field.
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||||
|
pub struct FnRule<F>(pub F);
|
||||||
|
|
||||||
|
impl<K, F: Fn(&K) -> Option<StartingPoint>> RatingRule<K> for FnRule<F> {
|
||||||
|
#[inline]
|
||||||
|
fn starting_point(&self, key: &K) -> Option<StartingPoint> {
|
||||||
|
(self.0)(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
//! `HistoryBuilder::default_rating_for`: configuring a *class* of competitors
|
||||||
|
//! rather than one at a time (#53).
|
||||||
|
//!
|
||||||
|
//! Every test carries a control — a key the rule does not match — so none can
|
||||||
|
//! pass by the rule firing for everybody, which would be indistinguishable
|
||||||
|
//! from changing the history defaults.
|
||||||
|
|
||||||
|
use trueskill_tt::{
|
||||||
|
ConstantDrift, Gaussian, History, HistoryBuilder, InferenceError, Member, NullObserver,
|
||||||
|
RatingRule, StartingPoint,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Pinned: no drift, and a tight prior at a known strength.
|
||||||
|
fn pinned() -> StartingPoint {
|
||||||
|
StartingPoint::new()
|
||||||
|
.prior(Gaussian::from_ms(5.0, 0.5))
|
||||||
|
.drift_scale(0.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn play<R: RatingRule<&'static str>>(
|
||||||
|
h: &mut History<&'static str, i64, ConstantDrift, NullObserver, R>,
|
||||||
|
) {
|
||||||
|
for t in 1..=6 {
|
||||||
|
h.event(t)
|
||||||
|
.team(["layout_a"])
|
||||||
|
.team(["alice"])
|
||||||
|
.scores([3.0, 1.0])
|
||||||
|
.commit()
|
||||||
|
.expect("ingests");
|
||||||
|
}
|
||||||
|
h.converge().expect("converges");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_rule_configures_every_matching_key_without_naming_them() {
|
||||||
|
let mut ruled = History::builder()
|
||||||
|
.gamma(0.5)
|
||||||
|
.default_rating_for(|key: &&'static str| key.starts_with("layout_").then(pinned))
|
||||||
|
.build();
|
||||||
|
play(&mut ruled);
|
||||||
|
|
||||||
|
let mut plain = History::builder().gamma(0.5).build();
|
||||||
|
play(&mut plain);
|
||||||
|
|
||||||
|
let layout = ruled.current_skill("layout_a").expect("played");
|
||||||
|
|
||||||
|
// The rule pinned the layout: tight prior, no drift.
|
||||||
|
assert!(
|
||||||
|
layout.sigma() < 0.5,
|
||||||
|
"the layout should stay near its pinned prior, got sigma {}",
|
||||||
|
layout.sigma()
|
||||||
|
);
|
||||||
|
assert_ne!(
|
||||||
|
layout.sigma(),
|
||||||
|
plain.current_skill("layout_a").unwrap().sigma(),
|
||||||
|
"the rule must actually change the fit"
|
||||||
|
);
|
||||||
|
|
||||||
|
// The control is the *configuration*, not the posterior. Alice's posterior
|
||||||
|
// legitimately moves — she is playing a differently-configured opponent,
|
||||||
|
// and what she learns from beating it depends on how sure the model is
|
||||||
|
// about it. What must not move is what the rule was asked about.
|
||||||
|
let alice = ruled.rating("alice").expect("played");
|
||||||
|
assert_eq!(
|
||||||
|
alice.drift_scale(),
|
||||||
|
1.0,
|
||||||
|
"a non-matching key keeps the default drift"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
(alice.prior().mu(), alice.prior().sigma()),
|
||||||
|
{
|
||||||
|
let p = plain.rating("alice").expect("played").prior();
|
||||||
|
(p.mu(), p.sigma())
|
||||||
|
},
|
||||||
|
"a non-matching key keeps the history's prior"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_rule_fires_for_a_competitor_first_seen_through_record_winner() {
|
||||||
|
// `record_winner` cannot carry configuration, which is the case a rule
|
||||||
|
// exists for.
|
||||||
|
let mut h = History::builder()
|
||||||
|
.default_rating_for(|key: &&'static str| key.starts_with("bot_").then(pinned))
|
||||||
|
.build();
|
||||||
|
h.record_winner(&"bot_1", &"human", 1).expect("ingests");
|
||||||
|
h.converge().expect("converges");
|
||||||
|
|
||||||
|
assert_eq!(h.rating("bot_1").expect("known").drift_scale(), 0.0);
|
||||||
|
assert_eq!(h.rating("human").expect("known").drift_scale(), 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn explicit_configuration_overrides_a_rule_field_by_field() {
|
||||||
|
let mut h = History::builder()
|
||||||
|
.default_rating_for(|_: &&'static str| Some(pinned()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Sets only the prior, so the rule's `drift_scale` must survive.
|
||||||
|
h.register(Member::new("a").with_prior(Gaussian::from_ms(-9.0, 2.0)))
|
||||||
|
.expect("new");
|
||||||
|
// Sets neither: the rule supplies both.
|
||||||
|
h.register(Member::new("b")).expect("new");
|
||||||
|
|
||||||
|
let a = h.rating("a").expect("registered");
|
||||||
|
assert_eq!(a.prior().mu(), -9.0, "explicit prior wins");
|
||||||
|
assert_eq!(a.drift_scale(), 0.0, "the rule's drift_scale survives");
|
||||||
|
|
||||||
|
let b = h.rating("b").expect("registered");
|
||||||
|
assert_eq!(b.prior().mu(), 5.0);
|
||||||
|
assert_eq!(b.drift_scale(), 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn two_explicit_declarations_that_disagree_are_still_an_error() {
|
||||||
|
// Precedence resolves rule-vs-explicit. It does not weaken the check
|
||||||
|
// between two explicit declarations, neither of which is more specific.
|
||||||
|
let mut h = History::builder()
|
||||||
|
.default_rating_for(|_: &&'static str| Some(pinned()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let err = h
|
||||||
|
.add_events(vec![
|
||||||
|
event(1, "x", Gaussian::from_ms(1.0, 1.0)),
|
||||||
|
event(2, "x", Gaussian::from_ms(2.0, 1.0)),
|
||||||
|
])
|
||||||
|
.expect_err("two different priors for one competitor");
|
||||||
|
assert!(
|
||||||
|
matches!(err, InferenceError::ConflictingCompetitorConfig { .. }),
|
||||||
|
"{err:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn event(time: i64, key: &'static str, prior: Gaussian) -> trueskill_tt::Event<i64, &'static str> {
|
||||||
|
trueskill_tt::Event {
|
||||||
|
time,
|
||||||
|
teams: [
|
||||||
|
trueskill_tt::Team::with_members([Member::new(key).with_prior(prior)]),
|
||||||
|
trueskill_tt::Team::with_members([Member::new("opponent")]),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.collect(),
|
||||||
|
outcome: trueskill_tt::Outcome::scores([2.0, 1.0]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A named rule type, so the `History<..>` can be written down in a field.
|
||||||
|
struct StaticLayouts;
|
||||||
|
|
||||||
|
impl RatingRule<&'static str> for StaticLayouts {
|
||||||
|
fn starting_point(&self, key: &&'static str) -> Option<StartingPoint> {
|
||||||
|
key.starts_with("layout_").then(pinned)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The reason this is a trait rather than a bare `Fn` bound: a consumer holds
|
||||||
|
/// its history in application state and has to name the type.
|
||||||
|
struct Ladder {
|
||||||
|
history: History<&'static str, i64, ConstantDrift, NullObserver, StaticLayouts>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_named_rule_type_can_be_stored_in_a_struct_field() {
|
||||||
|
let mut ladder = Ladder {
|
||||||
|
history: HistoryBuilder::default().rating_rule(StaticLayouts).build(),
|
||||||
|
};
|
||||||
|
play(&mut ladder.history);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
ladder
|
||||||
|
.history
|
||||||
|
.current_skill("layout_a")
|
||||||
|
.expect("played")
|
||||||
|
.sigma()
|
||||||
|
< 0.5
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ladder
|
||||||
|
.history
|
||||||
|
.rating("alice")
|
||||||
|
.expect("played")
|
||||||
|
.drift_scale(),
|
||||||
|
1.0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_rule_is_the_default_and_costs_nothing_to_spell() {
|
||||||
|
// The whole point of defaulting the parameter: `History<K>` still works.
|
||||||
|
let h: History<String> = History::builder().key_type::<String>().build();
|
||||||
|
assert_eq!(h.competitor_count(), 0);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user