Compare commits
2
Commits
da55d2a7d1
...
4c2e98c54b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c2e98c54b | ||
|
|
92ae5fca17 |
+90
-38
@@ -507,6 +507,32 @@ impl<T: Time, K: Eq + Hash + Clone> HistoryBuilder<T, ConstantDrift, NullObserve
|
|||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the drift rate, in skill units per unit of time.
|
||||||
|
///
|
||||||
|
/// Shorthand for `.drift(ConstantDrift::new(gamma))`. Drift is the
|
||||||
|
/// most-tuned parameter after `sigma` and [`GAMMA`](crate::GAMMA) is a
|
||||||
|
/// public constant, but reaching it otherwise means first discovering
|
||||||
|
/// [`ConstantDrift`] — a type a caller has no other reason to name.
|
||||||
|
///
|
||||||
|
/// Only on the `ConstantDrift` builder: `gamma` is that model's parameter,
|
||||||
|
/// not a property every [`Drift`] has. Use
|
||||||
|
/// [`drift`](HistoryBuilder::drift) for anything else.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics unless `gamma` is finite and non-negative. Drift enters as
|
||||||
|
/// `gamma^2` per elapsed tick, so a negative value would behave as its
|
||||||
|
/// absolute value — the same sign-absorption already rejected for `sigma`
|
||||||
|
/// and `beta`.
|
||||||
|
pub fn gamma(self, 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.drift(ConstantDrift::new(gamma))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O, K> {
|
impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O, K> {
|
||||||
@@ -531,7 +557,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
pub fn lookup<Q>(&self, key: &Q) -> Option<Index>
|
pub fn lookup<Q>(&self, key: &Q) -> Option<Index>
|
||||||
where
|
where
|
||||||
K: Borrow<Q>,
|
K: Borrow<Q>,
|
||||||
Q: Hash + Eq + ToOwned<Owned = K> + ?Sized,
|
Q: Hash + Eq + ?Sized,
|
||||||
{
|
{
|
||||||
self.keys.get(key)
|
self.keys.get(key)
|
||||||
}
|
}
|
||||||
@@ -1128,9 +1154,10 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
/// reported rather than dropped — silently skipping them would turn a team
|
/// reported rather than dropped — silently skipping them would turn a team
|
||||||
/// of strangers into a confident-looking prediction about nobody, which is
|
/// of strangers into a confident-looking prediction about nobody, which is
|
||||||
/// the failure this replaced.
|
/// the failure this replaced.
|
||||||
fn member_skills(&self, teams: &[&[&K]]) -> Result<Vec<Vec<Gaussian>>, InferenceError>
|
fn member_skills<Q>(&self, teams: &[&[&Q]]) -> Result<Vec<Vec<Gaussian>>, InferenceError>
|
||||||
where
|
where
|
||||||
K: std::fmt::Debug,
|
K: Borrow<Q>,
|
||||||
|
Q: Hash + Eq + ?Sized + std::fmt::Debug,
|
||||||
{
|
{
|
||||||
if teams.len() < 2 {
|
if teams.len() < 2 {
|
||||||
return Err(InferenceError::NotEnoughTeams { got: teams.len() });
|
return Err(InferenceError::NotEnoughTeams { got: teams.len() });
|
||||||
@@ -1244,9 +1271,13 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// As `member_skills`.
|
/// As `member_skills`.
|
||||||
fn performances(&self, teams: &[&[&K]]) -> Result<(Vec<Gaussian>, Vec<usize>), InferenceError>
|
fn performances<Q>(
|
||||||
|
&self,
|
||||||
|
teams: &[&[&Q]],
|
||||||
|
) -> Result<(Vec<Gaussian>, Vec<usize>), InferenceError>
|
||||||
where
|
where
|
||||||
K: std::fmt::Debug,
|
K: Borrow<Q>,
|
||||||
|
Q: Hash + Eq + ?Sized + std::fmt::Debug,
|
||||||
{
|
{
|
||||||
let skills = self.member_skills(teams)?;
|
let skills = self.member_skills(teams)?;
|
||||||
|
|
||||||
@@ -1310,9 +1341,10 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
/// — the fit did not converge — and `InvalidParameter` if `beta` is zero
|
/// — the fit did not converge — and `InvalidParameter` if `beta` is zero
|
||||||
/// and every skill is a point mass, leaving no performance distribution to
|
/// and every skill is a point mass, leaving no performance distribution to
|
||||||
/// predict from.
|
/// predict from.
|
||||||
pub fn predict_quality(&self, teams: &[&[&K]]) -> Result<f64, InferenceError>
|
pub fn predict_quality<Q>(&self, teams: &[&[&Q]]) -> Result<f64, InferenceError>
|
||||||
where
|
where
|
||||||
K: std::fmt::Debug,
|
K: Borrow<Q>,
|
||||||
|
Q: Hash + Eq + ?Sized + std::fmt::Debug,
|
||||||
{
|
{
|
||||||
let groups = self.member_skills(teams)?;
|
let groups = self.member_skills(teams)?;
|
||||||
let group_refs: Vec<&[Gaussian]> = groups.iter().map(Vec::as_slice).collect();
|
let group_refs: Vec<&[Gaussian]> = groups.iter().map(Vec::as_slice).collect();
|
||||||
@@ -1448,14 +1480,15 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
///
|
///
|
||||||
/// `row_for` picks which appearance of a competitor the caller means —
|
/// `row_for` picks which appearance of a competitor the caller means —
|
||||||
/// their latest, or the one at a given time.
|
/// their latest, or the one at a given time.
|
||||||
fn resolve_terms(
|
fn resolve_terms<Q>(
|
||||||
&self,
|
&self,
|
||||||
terms: &[(&K, f64)],
|
terms: &[(&Q, f64)],
|
||||||
width: usize,
|
width: usize,
|
||||||
row_for: impl Fn(Index) -> Option<(usize, usize)>,
|
row_for: impl Fn(Index) -> Option<(usize, usize)>,
|
||||||
) -> Result<ResolvedTerms, InferenceError>
|
) -> Result<ResolvedTerms, InferenceError>
|
||||||
where
|
where
|
||||||
K: std::fmt::Debug,
|
K: Borrow<Q>,
|
||||||
|
Q: Hash + Eq + ?Sized + std::fmt::Debug,
|
||||||
{
|
{
|
||||||
let mut contrast = vec![0.0; width];
|
let mut contrast = vec![0.0; width];
|
||||||
let mut unseen: BTreeMap<String, f64> = BTreeMap::new();
|
let mut unseen: BTreeMap<String, f64> = BTreeMap::new();
|
||||||
@@ -1557,9 +1590,10 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
/// `UnknownKey` for a competitor the history has never seen, and
|
/// `UnknownKey` for a competitor the history has never seen, and
|
||||||
/// `JointUnavailable` for ranked events or a system that is not
|
/// `JointUnavailable` for ranked events or a system that is not
|
||||||
/// positive-definite.
|
/// positive-definite.
|
||||||
pub fn posterior_of(&self, terms: &[(&K, f64)]) -> Result<Gaussian, InferenceError>
|
pub fn posterior_of<Q>(&self, terms: &[(&Q, f64)]) -> Result<Gaussian, InferenceError>
|
||||||
where
|
where
|
||||||
K: std::fmt::Debug,
|
K: Borrow<Q>,
|
||||||
|
Q: Hash + Eq + ?Sized + std::fmt::Debug,
|
||||||
{
|
{
|
||||||
self.joint()?.posterior_of(terms)
|
self.joint()?.posterior_of(terms)
|
||||||
}
|
}
|
||||||
@@ -1579,9 +1613,14 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
///
|
///
|
||||||
/// As [`History::posterior_of`], plus `UnknownKey` for a competitor with no
|
/// As [`History::posterior_of`], plus `UnknownKey` for a competitor with no
|
||||||
/// appearance at or before `time`.
|
/// appearance at or before `time`.
|
||||||
pub fn posterior_of_at(&self, time: T, terms: &[(&K, f64)]) -> Result<Gaussian, InferenceError>
|
pub fn posterior_of_at<Q>(
|
||||||
|
&self,
|
||||||
|
time: T,
|
||||||
|
terms: &[(&Q, f64)],
|
||||||
|
) -> Result<Gaussian, InferenceError>
|
||||||
where
|
where
|
||||||
K: std::fmt::Debug,
|
K: Borrow<Q>,
|
||||||
|
Q: Hash + Eq + ?Sized + std::fmt::Debug,
|
||||||
{
|
{
|
||||||
self.joint()?.posterior_of_at(time, terms)
|
self.joint()?.posterior_of_at(time, terms)
|
||||||
}
|
}
|
||||||
@@ -1625,13 +1664,14 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
///
|
///
|
||||||
/// As [`History::posterior_of`], plus `MismatchedShape` unless exactly two
|
/// As [`History::posterior_of`], plus `MismatchedShape` unless exactly two
|
||||||
/// teams are supplied and `EmptyTeam` for an empty one.
|
/// teams are supplied and `EmptyTeam` for an empty one.
|
||||||
pub fn expected_variance_reduction(
|
pub fn expected_variance_reduction<Q>(
|
||||||
&self,
|
&self,
|
||||||
teams: &[&[&K]],
|
teams: &[&[&Q]],
|
||||||
target: &[(&K, f64)],
|
target: &[(&Q, f64)],
|
||||||
) -> Result<f64, InferenceError>
|
) -> Result<f64, InferenceError>
|
||||||
where
|
where
|
||||||
K: std::fmt::Debug,
|
K: Borrow<Q>,
|
||||||
|
Q: Hash + Eq + ?Sized + std::fmt::Debug,
|
||||||
{
|
{
|
||||||
self.joint()?.expected_variance_reduction(teams, target)
|
self.joint()?.expected_variance_reduction(teams, target)
|
||||||
}
|
}
|
||||||
@@ -1747,9 +1787,10 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
/// `UnknownKey` under [`UnknownKeys::Reject`](crate::UnknownKeys::Reject),
|
/// `UnknownKey` under [`UnknownKeys::Reject`](crate::UnknownKeys::Reject),
|
||||||
/// and `JointUnavailable` if the history is empty or holds ranked events in
|
/// and `JointUnavailable` if the history is empty or holds ranked events in
|
||||||
/// *any* slice — not merely the latest one.
|
/// *any* slice — not merely the latest one.
|
||||||
pub fn predict_margin(&self, teams: &[&[&K]]) -> Result<Gaussian, InferenceError>
|
pub fn predict_margin<Q>(&self, teams: &[&[&Q]]) -> Result<Gaussian, InferenceError>
|
||||||
where
|
where
|
||||||
K: std::fmt::Debug,
|
K: Borrow<Q>,
|
||||||
|
Q: Hash + Eq + ?Sized + std::fmt::Debug,
|
||||||
{
|
{
|
||||||
if teams.len() != 2 {
|
if teams.len() != 2 {
|
||||||
return Err(InferenceError::MismatchedShape {
|
return Err(InferenceError::MismatchedShape {
|
||||||
@@ -1759,7 +1800,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut terms: Vec<(&K, f64)> = Vec::new();
|
let mut terms: Vec<(&Q, f64)> = Vec::new();
|
||||||
let mut performance_noise = 0.0;
|
let mut performance_noise = 0.0;
|
||||||
|
|
||||||
for (team_idx, team) in teams.iter().enumerate() {
|
for (team_idx, team) in teams.iter().enumerate() {
|
||||||
@@ -1824,9 +1865,10 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
/// — the fit did not converge — and `InvalidParameter` if `beta` is zero
|
/// — the fit did not converge — and `InvalidParameter` if `beta` is zero
|
||||||
/// and every skill is a point mass, leaving no performance distribution to
|
/// and every skill is a point mass, leaving no performance distribution to
|
||||||
/// predict from.
|
/// predict from.
|
||||||
pub fn expected_information_gain(&self, teams: &[&[&K]]) -> Result<f64, InferenceError>
|
pub fn expected_information_gain<Q>(&self, teams: &[&[&Q]]) -> Result<f64, InferenceError>
|
||||||
where
|
where
|
||||||
K: std::fmt::Debug,
|
K: Borrow<Q>,
|
||||||
|
Q: Hash + Eq + ?Sized + std::fmt::Debug,
|
||||||
{
|
{
|
||||||
let skills = self.member_skills(teams)?;
|
let skills = self.member_skills(teams)?;
|
||||||
|
|
||||||
@@ -1884,9 +1926,10 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
/// — the fit did not converge — and `InvalidParameter` if `beta` is zero
|
/// — the fit did not converge — and `InvalidParameter` if `beta` is zero
|
||||||
/// and every skill is a point mass, leaving no performance distribution to
|
/// and every skill is a point mass, leaving no performance distribution to
|
||||||
/// predict from.
|
/// predict from.
|
||||||
pub fn predict_win_probabilities(&self, teams: &[&[&K]]) -> Result<Vec<f64>, InferenceError>
|
pub fn predict_win_probabilities<Q>(&self, teams: &[&[&Q]]) -> Result<Vec<f64>, InferenceError>
|
||||||
where
|
where
|
||||||
K: std::fmt::Debug,
|
K: Borrow<Q>,
|
||||||
|
Q: Hash + Eq + ?Sized + std::fmt::Debug,
|
||||||
{
|
{
|
||||||
let (performances, sizes) = self.performances(teams)?;
|
let (performances, sizes) = self.performances(teams)?;
|
||||||
Ok(crate::predict::win_probabilities(
|
Ok(crate::predict::win_probabilities(
|
||||||
@@ -1938,9 +1981,10 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
/// — the fit did not converge — and `InvalidParameter` if `beta` is zero
|
/// — the fit did not converge — and `InvalidParameter` if `beta` is zero
|
||||||
/// and every skill is a point mass, leaving no performance distribution to
|
/// and every skill is a point mass, leaving no performance distribution to
|
||||||
/// predict from.
|
/// predict from.
|
||||||
pub fn predict_outcome(&self, teams: &[&[&K]]) -> Result<Prediction, InferenceError>
|
pub fn predict_outcome<Q>(&self, teams: &[&[&Q]]) -> Result<Prediction, InferenceError>
|
||||||
where
|
where
|
||||||
K: std::fmt::Debug,
|
K: Borrow<Q>,
|
||||||
|
Q: Hash + Eq + ?Sized + std::fmt::Debug,
|
||||||
{
|
{
|
||||||
if teams.len() > crate::MAX_PREDICTED_TEAMS {
|
if teams.len() > crate::MAX_PREDICTED_TEAMS {
|
||||||
return Err(InferenceError::TooManyTeams {
|
return Err(InferenceError::TooManyTeams {
|
||||||
@@ -1987,9 +2031,10 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
/// — the fit did not converge — and `InvalidParameter` if `beta` is zero
|
/// — the fit did not converge — and `InvalidParameter` if `beta` is zero
|
||||||
/// and every skill is a point mass, leaving no performance distribution to
|
/// and every skill is a point mass, leaving no performance distribution to
|
||||||
/// predict from.
|
/// predict from.
|
||||||
pub fn predict_ranking(&self, teams: &[&[&K]], ranks: &[u32]) -> Result<f64, InferenceError>
|
pub fn predict_ranking<Q>(&self, teams: &[&[&Q]], ranks: &[u32]) -> Result<f64, InferenceError>
|
||||||
where
|
where
|
||||||
K: std::fmt::Debug,
|
K: Borrow<Q>,
|
||||||
|
Q: Hash + Eq + ?Sized + std::fmt::Debug,
|
||||||
{
|
{
|
||||||
if ranks.len() != teams.len() {
|
if ranks.len() != teams.len() {
|
||||||
return Err(InferenceError::MismatchedShape {
|
return Err(InferenceError::MismatchedShape {
|
||||||
@@ -2877,9 +2922,10 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> Joint<'_, T, D,
|
|||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// `UnknownKey` for a competitor the history has never seen.
|
/// `UnknownKey` for a competitor the history has never seen.
|
||||||
pub fn posterior_of(&self, terms: &[(&K, f64)]) -> Result<Gaussian, InferenceError>
|
pub fn posterior_of<Q>(&self, terms: &[(&Q, f64)]) -> Result<Gaussian, InferenceError>
|
||||||
where
|
where
|
||||||
K: std::fmt::Debug,
|
K: Borrow<Q>,
|
||||||
|
Q: Hash + Eq + ?Sized + std::fmt::Debug,
|
||||||
{
|
{
|
||||||
let resolved = self
|
let resolved = self
|
||||||
.history
|
.history
|
||||||
@@ -2895,9 +2941,14 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> Joint<'_, T, D,
|
|||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// `UnknownKey` for a competitor with no appearance at or before `time`.
|
/// `UnknownKey` for a competitor with no appearance at or before `time`.
|
||||||
pub fn posterior_of_at(&self, time: T, terms: &[(&K, f64)]) -> Result<Gaussian, InferenceError>
|
pub fn posterior_of_at<Q>(
|
||||||
|
&self,
|
||||||
|
time: T,
|
||||||
|
terms: &[(&Q, f64)],
|
||||||
|
) -> Result<Gaussian, InferenceError>
|
||||||
where
|
where
|
||||||
K: std::fmt::Debug,
|
K: Borrow<Q>,
|
||||||
|
Q: Hash + Eq + ?Sized + std::fmt::Debug,
|
||||||
{
|
{
|
||||||
let as_of = self.rows_as_of(time);
|
let as_of = self.rows_as_of(time);
|
||||||
let resolved = self
|
let resolved = self
|
||||||
@@ -2932,13 +2983,14 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> Joint<'_, T, D,
|
|||||||
///
|
///
|
||||||
/// `MismatchedShape` unless exactly two teams are supplied, `EmptyTeam` for
|
/// `MismatchedShape` unless exactly two teams are supplied, `EmptyTeam` for
|
||||||
/// an empty one, and `UnknownKey` for an unseen competitor.
|
/// an empty one, and `UnknownKey` for an unseen competitor.
|
||||||
pub fn expected_variance_reduction(
|
pub fn expected_variance_reduction<Q>(
|
||||||
&self,
|
&self,
|
||||||
teams: &[&[&K]],
|
teams: &[&[&Q]],
|
||||||
target: &[(&K, f64)],
|
target: &[(&Q, f64)],
|
||||||
) -> Result<f64, InferenceError>
|
) -> Result<f64, InferenceError>
|
||||||
where
|
where
|
||||||
K: std::fmt::Debug,
|
K: Borrow<Q>,
|
||||||
|
Q: Hash + Eq + ?Sized + std::fmt::Debug,
|
||||||
{
|
{
|
||||||
if teams.len() != 2 {
|
if teams.len() != 2 {
|
||||||
return Err(InferenceError::MismatchedShape {
|
return Err(InferenceError::MismatchedShape {
|
||||||
@@ -2950,7 +3002,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> Joint<'_, T, D,
|
|||||||
|
|
||||||
// The candidate matchup, expressed as the same kind of linear
|
// The candidate matchup, expressed as the same kind of linear
|
||||||
// functional as the target.
|
// functional as the target.
|
||||||
let mut matchup: Vec<(&K, f64)> = Vec::new();
|
let mut matchup: Vec<(&Q, f64)> = Vec::new();
|
||||||
let mut noise = self.history.score_sigma * self.history.score_sigma;
|
let mut noise = self.history.score_sigma * self.history.score_sigma;
|
||||||
for (team_idx, team) in teams.iter().enumerate() {
|
for (team_idx, team) in teams.iter().enumerate() {
|
||||||
if team.is_empty() {
|
if team.is_empty() {
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
//! The realistic program: keys arrive owned, queries are written with literals.
|
||||||
|
//!
|
||||||
|
//! Every prediction and joint query used to take `&[&[&K]]`, which at
|
||||||
|
//! `K = String` made a string literal *impossible* — the shape required three
|
||||||
|
//! levels of temporaries that all had to outlive the call. They are generic
|
||||||
|
//! over the borrowed key now, so one spelling works at both key types.
|
||||||
|
//!
|
||||||
|
//! Both key types are exercised in every test, because the point is that the
|
||||||
|
//! spelling is the same.
|
||||||
|
|
||||||
|
use trueskill_tt::{ConstantDrift, History, NullObserver};
|
||||||
|
|
||||||
|
type Owned = History<i64, ConstantDrift, NullObserver, String>;
|
||||||
|
type Borrowed = History<i64, ConstantDrift, NullObserver, &'static str>;
|
||||||
|
|
||||||
|
fn owned() -> Owned {
|
||||||
|
let mut h: Owned = History::builder().key_type::<String>().build();
|
||||||
|
for t in 1..=4 {
|
||||||
|
h.record_winner(&"alice".to_string(), &"bob".to_string(), t)
|
||||||
|
.expect("ingests");
|
||||||
|
}
|
||||||
|
h.converge().expect("converges");
|
||||||
|
h
|
||||||
|
}
|
||||||
|
|
||||||
|
fn borrowed() -> Borrowed {
|
||||||
|
let mut h = History::default();
|
||||||
|
for t in 1..=4 {
|
||||||
|
h.record_winner(&"alice", &"bob", t).expect("ingests");
|
||||||
|
}
|
||||||
|
h.converge().expect("converges");
|
||||||
|
h
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn predictions_take_literals_at_either_key_type() {
|
||||||
|
let teams: &[&[&str]] = &[&["alice"], &["bob"]];
|
||||||
|
|
||||||
|
let a = owned()
|
||||||
|
.predict_win_probabilities(teams)
|
||||||
|
.expect("K = String");
|
||||||
|
let b = borrowed()
|
||||||
|
.predict_win_probabilities(teams)
|
||||||
|
.expect("K = &'static str");
|
||||||
|
|
||||||
|
assert_eq!(a, b, "the same fit through the same spelling");
|
||||||
|
assert!(a[0] > a[1], "alice won every game");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_team_shaped_query_accepts_the_same_slice() {
|
||||||
|
let h = owned();
|
||||||
|
let teams: &[&[&str]] = &[&["alice"], &["bob"]];
|
||||||
|
|
||||||
|
h.predict_quality(teams).expect("quality");
|
||||||
|
let _ = h.predict_outcome(teams).expect("outcome");
|
||||||
|
h.predict_ranking(teams, &[0, 1]).expect("ranking");
|
||||||
|
h.expected_information_gain(teams)
|
||||||
|
.expect("information gain");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn linear_combinations_take_bare_keys() {
|
||||||
|
// `&[(&K, f64)]` at `K = String` meant `&[(&String, f64)]` — no literals.
|
||||||
|
let h = owned();
|
||||||
|
let terms: &[(&str, f64)] = &[("alice", 1.0), ("bob", -1.0)];
|
||||||
|
|
||||||
|
// Ranked history, so the joint is unavailable — but the *call* compiles,
|
||||||
|
// which is what this pins. The error proves it reached the joint check
|
||||||
|
// rather than failing to resolve a key.
|
||||||
|
let err = h
|
||||||
|
.posterior_of(terms)
|
||||||
|
.expect_err("ranked history has no joint");
|
||||||
|
assert!(
|
||||||
|
format!("{err}").contains("ranked"),
|
||||||
|
"expected the joint-unavailable path, got {err}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lookup_accepts_a_borrowed_key_like_its_neighbours() {
|
||||||
|
// `lookup` carried `ToOwned<Owned = K>`, copy-pasted from `intern`, which
|
||||||
|
// genuinely needs it to create the entry. `lookup` never creates.
|
||||||
|
let h = owned();
|
||||||
|
assert!(h.lookup("alice").is_some());
|
||||||
|
assert!(h.lookup("nobody").is_none());
|
||||||
|
|
||||||
|
// Control: its neighbours already accepted this and must still.
|
||||||
|
assert!(h.current_skill("alice").is_some());
|
||||||
|
assert!(h.rating("alice").is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gamma_sets_drift_without_naming_constant_drift() {
|
||||||
|
let mut a: Borrowed = History::builder().gamma(0.5).build();
|
||||||
|
let mut b: Borrowed = History::builder().drift(ConstantDrift::new(0.5)).build();
|
||||||
|
|
||||||
|
for h in [&mut a, &mut b] {
|
||||||
|
h.record_winner(&"x", &"y", 1).unwrap();
|
||||||
|
h.record_winner(&"y", &"x", 100).unwrap();
|
||||||
|
h.converge().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let (ga, gb) = (a.current_skill("x").unwrap(), b.current_skill("x").unwrap());
|
||||||
|
assert_eq!((ga.mu(), ga.sigma()), (gb.mu(), gb.sigma()));
|
||||||
|
|
||||||
|
// Control: the shorthand is not a no-op — a different gamma differs.
|
||||||
|
let mut c: Borrowed = History::builder().gamma(0.0).build();
|
||||||
|
c.record_winner(&"x", &"y", 1).unwrap();
|
||||||
|
c.record_winner(&"y", &"x", 100).unwrap();
|
||||||
|
c.converge().unwrap();
|
||||||
|
assert_ne!(c.current_skill("x").unwrap().sigma(), ga.sigma());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "gamma must be finite and non-negative")]
|
||||||
|
fn a_negative_gamma_is_rejected_rather_than_squared_away() {
|
||||||
|
let _: Borrowed = History::builder().gamma(-0.5).build();
|
||||||
|
}
|
||||||
+5
-1
@@ -60,8 +60,12 @@ fn degenerate_team_shapes_are_errors_rather_than_panics() {
|
|||||||
h.predict_outcome(&[&[&"a"]]).unwrap_err(),
|
h.predict_outcome(&[&[&"a"]]).unwrap_err(),
|
||||||
InferenceError::NotEnoughTeams { got: 1, .. }
|
InferenceError::NotEnoughTeams { got: 1, .. }
|
||||||
),);
|
),);
|
||||||
|
// An empty team list cannot infer the key type — nothing in `&[]` names it.
|
||||||
|
// The annotation is the cost of `predict_*` being generic over the borrowed
|
||||||
|
// key, and it only bites on the degenerate call.
|
||||||
|
let none: &[&[&str]] = &[];
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
h.predict_outcome(&[]).unwrap_err(),
|
h.predict_outcome(none).unwrap_err(),
|
||||||
InferenceError::NotEnoughTeams { got: 0, .. }
|
InferenceError::NotEnoughTeams { got: 0, .. }
|
||||||
),);
|
),);
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
|
|||||||
Reference in New Issue
Block a user