refactor!: the joint is reached through Joint, not mirrored on History
`posterior_of`, `posterior_of_at` and `expected_variance_reduction`
existed twice: once on `Joint`, and once on `History` as one-shot
wrappers whose whole body was `self.joint()?.<same>(..)`.
The wrappers re-factorised on every call — their own docs said so,
warning the reader to take a `Joint` instead — and they were what
smuggled the scored-only precondition onto the flat surface. A user
following the quickstart builds a ranked history, sees `posterior_of` in
the method list, and it never works. `h.joint()?.posterior_of(..)` is
one call longer and tells the truth: you need a joint, and a joint needs
a scored history.
That leaves three tiers instead of a flat surface with a hidden
precondition: `History` fits and reads, `predict_*` forecasts, `Joint`
answers exact joint questions.
`predict_margin` was itself calling `self.posterior_of`; it goes through
`self.joint()?` directly now.
The `Joint` methods' docs referred back to the wrappers for their real
content ("Identical to `History::posterior_of`, without re-paying the
factorisation"), so they now carry it: what a linear functional means,
which appearance each competitor is read at, and why
`expected_variance_reduction` belongs on the handle.
`tests/joint_handle.rs` had three tests comparing the wrapper against
the handle. That comparison is gone, but the property behind it is not —
they now compare a *reused* joint against a *fresh* one per question,
which is the actual correctness claim behind caching the factorisation
(#51), without the wrapper in the middle.
Closes #78.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011hcFjNDmHXZF8URGLku5zZ
This commit is contained in:
@@ -60,15 +60,30 @@ fn fit(extra: Option<Event<i64, &'static str>>, policy: UnknownKeys) -> H {
|
||||
fn the_closed_form_matches_an_actual_refit() {
|
||||
let h = fit(None, UnknownKeys::Reject);
|
||||
let target: Vec<(&&str, f64)> = vec![(&"a", 1.0), (&"b", -1.0)];
|
||||
let before = h.posterior_of(&target).unwrap().sigma().powi(2);
|
||||
let before = h
|
||||
.joint()
|
||||
.unwrap()
|
||||
.posterior_of(&target)
|
||||
.unwrap()
|
||||
.sigma()
|
||||
.powi(2);
|
||||
|
||||
for (x, y) in [("a", "b"), ("c", "d"), ("a", "c"), ("b", "d")] {
|
||||
let predicted = h
|
||||
.joint()
|
||||
.unwrap()
|
||||
.expected_variance_reduction(&[&[&x], &[&y]], &target)
|
||||
.unwrap();
|
||||
|
||||
let after = fit(Some(round(x, y, 3.0, 1.0)), UnknownKeys::Reject);
|
||||
let actual = before - after.posterior_of(&target).unwrap().sigma().powi(2);
|
||||
let actual = before
|
||||
- after
|
||||
.joint()
|
||||
.unwrap()
|
||||
.posterior_of(&target)
|
||||
.unwrap()
|
||||
.sigma()
|
||||
.powi(2);
|
||||
|
||||
assert!(
|
||||
(predicted - actual).abs() / actual.abs() < 1e-9,
|
||||
@@ -84,12 +99,27 @@ fn the_closed_form_matches_an_actual_refit() {
|
||||
fn the_outcome_does_not_change_the_reduction() {
|
||||
let target: Vec<(&&str, f64)> = vec![(&"a", 1.0), (&"b", -1.0)];
|
||||
let h = fit(None, UnknownKeys::Reject);
|
||||
let before = h.posterior_of(&target).unwrap().sigma().powi(2);
|
||||
let before = h
|
||||
.joint()
|
||||
.unwrap()
|
||||
.posterior_of(&target)
|
||||
.unwrap()
|
||||
.sigma()
|
||||
.powi(2);
|
||||
|
||||
let mut seen = Vec::new();
|
||||
for (sa, sb) in [(3.0, 1.0), (100.0, -50.0), (0.0, 0.0)] {
|
||||
let after = fit(Some(round("c", "d", sa, sb)), UnknownKeys::Reject);
|
||||
seen.push(before - after.posterior_of(&target).unwrap().sigma().powi(2));
|
||||
seen.push(
|
||||
before
|
||||
- after
|
||||
.joint()
|
||||
.unwrap()
|
||||
.posterior_of(&target)
|
||||
.unwrap()
|
||||
.sigma()
|
||||
.powi(2),
|
||||
);
|
||||
}
|
||||
for w in seen.windows(2) {
|
||||
assert!(
|
||||
@@ -107,9 +137,13 @@ fn it_ranks_candidates_by_how_much_they_answer_the_question() {
|
||||
let target: Vec<(&&str, f64)> = vec![(&"a", 1.0), (&"b", -1.0)];
|
||||
|
||||
let direct = h
|
||||
.joint()
|
||||
.unwrap()
|
||||
.expected_variance_reduction(&[&[&"a"], &[&"b"]], &target)
|
||||
.unwrap();
|
||||
let unrelated = h
|
||||
.joint()
|
||||
.unwrap()
|
||||
.expected_variance_reduction(&[&[&"c"], &[&"d"]], &target)
|
||||
.unwrap();
|
||||
|
||||
@@ -128,6 +162,8 @@ fn an_unrelated_unseen_matchup_teaches_nothing_about_the_target() {
|
||||
let target: Vec<(&&str, f64)> = vec![(&"a", 1.0), (&"b", -1.0)];
|
||||
|
||||
let reduction = h
|
||||
.joint()
|
||||
.unwrap()
|
||||
.expected_variance_reduction(&[&[&"stranger"], &[&"nobody"]], &target)
|
||||
.unwrap();
|
||||
assert!(
|
||||
@@ -142,7 +178,9 @@ fn shape_errors_are_reported() {
|
||||
let target: Vec<(&&str, f64)> = vec![(&"a", 1.0), (&"b", -1.0)];
|
||||
|
||||
assert!(matches!(
|
||||
h.expected_variance_reduction(&[&[&"a"]], &target),
|
||||
h.joint()
|
||||
.unwrap()
|
||||
.expected_variance_reduction(&[&[&"a"]], &target),
|
||||
Err(InferenceError::MismatchedShape {
|
||||
expected: 2,
|
||||
got: 1,
|
||||
@@ -150,7 +188,9 @@ fn shape_errors_are_reported() {
|
||||
})
|
||||
));
|
||||
assert!(matches!(
|
||||
h.expected_variance_reduction(&[&[&"a"], &[&"ghost"]], &target),
|
||||
h.joint()
|
||||
.unwrap()
|
||||
.expected_variance_reduction(&[&[&"a"], &[&"ghost"]], &target),
|
||||
Err(InferenceError::UnknownKey { .. })
|
||||
));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user