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:
@@ -120,7 +120,11 @@ fn a_two_slice_joint_matches_the_exact_posterior() {
|
||||
|
||||
// The crate reads each competitor at their latest appearance: a1, b1.
|
||||
let exact_gap = (cov[2][2] + cov[3][3] - 2.0 * cov[2][3]).sqrt();
|
||||
let got = h.posterior_of(&[(&"a", 1.0), (&"b", -1.0)]).unwrap();
|
||||
let got = h
|
||||
.joint()
|
||||
.unwrap()
|
||||
.posterior_of(&[(&"a", 1.0), (&"b", -1.0)])
|
||||
.unwrap();
|
||||
assert!(
|
||||
(got.sigma() - exact_gap).abs() / exact_gap < 1e-9,
|
||||
"difference: got {} exact {exact_gap}",
|
||||
@@ -128,7 +132,7 @@ fn a_two_slice_joint_matches_the_exact_posterior() {
|
||||
);
|
||||
|
||||
let exact_single = cov[2][2].sqrt();
|
||||
let got_single = h.posterior_of(&[(&"a", 1.0)]).unwrap();
|
||||
let got_single = h.joint().unwrap().posterior_of(&[(&"a", 1.0)]).unwrap();
|
||||
assert!(
|
||||
(got_single.sigma() - exact_single).abs() / exact_single < 1e-9,
|
||||
"single node: got {} exact {exact_single}",
|
||||
@@ -154,6 +158,8 @@ fn competitors_last_seen_in_different_slices_are_comparable() {
|
||||
// b last appeared at time 0; a and c at time 20. All three must resolve.
|
||||
for (x, y) in [("a", "b"), ("b", "c"), ("a", "c")] {
|
||||
let g = h
|
||||
.joint()
|
||||
.unwrap()
|
||||
.posterior_of(&[(&x, 1.0), (&y, -1.0)])
|
||||
.unwrap_or_else(|e| panic!("{x} - {y} should resolve across slices: {e}"));
|
||||
assert!(g.sigma() > 0.0 && g.sigma().is_finite());
|
||||
@@ -175,7 +181,7 @@ fn means_agree_with_the_marginals() {
|
||||
|
||||
for k in ["a", "b", "c"] {
|
||||
let marginal = h.current_skill(&k).unwrap().mu();
|
||||
let joint = h.posterior_of(&[(&k, 1.0)]).unwrap().mu();
|
||||
let joint = h.joint().unwrap().posterior_of(&[(&k, 1.0)]).unwrap().mu();
|
||||
assert!(
|
||||
(marginal - joint).abs() < 1e-9,
|
||||
"{k}: marginal {marginal}, joint {joint}"
|
||||
@@ -197,7 +203,10 @@ fn zero_drift_makes_slice_layout_irrelevant() {
|
||||
])
|
||||
.unwrap();
|
||||
let _ = h.converge().unwrap();
|
||||
h.posterior_of(&[(&"a", 1.0), (&"b", -1.0)]).unwrap()
|
||||
h.joint()
|
||||
.unwrap()
|
||||
.posterior_of(&[(&"a", 1.0), (&"b", -1.0)])
|
||||
.unwrap()
|
||||
};
|
||||
let together = {
|
||||
let mut h = history(0.0);
|
||||
@@ -208,7 +217,10 @@ fn zero_drift_makes_slice_layout_irrelevant() {
|
||||
])
|
||||
.unwrap();
|
||||
let _ = h.converge().unwrap();
|
||||
h.posterior_of(&[(&"a", 1.0), (&"b", -1.0)]).unwrap()
|
||||
h.joint()
|
||||
.unwrap()
|
||||
.posterior_of(&[(&"a", 1.0), (&"b", -1.0)])
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
assert!(
|
||||
@@ -234,7 +246,11 @@ fn drift_widens_a_comparison_across_time() {
|
||||
let _ = h.converge().unwrap();
|
||||
|
||||
// b was last seen at time 0; a at time 100.
|
||||
let g = h.posterior_of(&[(&"a", 1.0), (&"b", -1.0)]).unwrap();
|
||||
let g = h
|
||||
.joint()
|
||||
.unwrap()
|
||||
.posterior_of(&[(&"a", 1.0), (&"b", -1.0)])
|
||||
.unwrap();
|
||||
assert!(
|
||||
g.sigma() > previous,
|
||||
"gamma={gamma}: sigma {} did not exceed {previous}",
|
||||
@@ -257,9 +273,21 @@ fn posterior_of_at_reads_as_of_a_time() {
|
||||
.unwrap();
|
||||
let _ = h.converge().unwrap();
|
||||
|
||||
let early = h.posterior_of_at(0, &[(&"a", 1.0), (&"b", -1.0)]).unwrap();
|
||||
let late = h.posterior_of_at(20, &[(&"a", 1.0), (&"b", -1.0)]).unwrap();
|
||||
let latest = h.posterior_of(&[(&"a", 1.0), (&"b", -1.0)]).unwrap();
|
||||
let early = h
|
||||
.joint()
|
||||
.unwrap()
|
||||
.posterior_of_at(0, &[(&"a", 1.0), (&"b", -1.0)])
|
||||
.unwrap();
|
||||
let late = h
|
||||
.joint()
|
||||
.unwrap()
|
||||
.posterior_of_at(20, &[(&"a", 1.0), (&"b", -1.0)])
|
||||
.unwrap();
|
||||
let latest = h
|
||||
.joint()
|
||||
.unwrap()
|
||||
.posterior_of(&[(&"a", 1.0), (&"b", -1.0)])
|
||||
.unwrap();
|
||||
|
||||
// Asking as of the final slice is the same as asking for the latest.
|
||||
assert!((late.mu() - latest.mu()).abs() < 1e-9);
|
||||
@@ -275,7 +303,12 @@ fn posterior_of_at_reads_as_of_a_time() {
|
||||
);
|
||||
|
||||
// A time before any event has nothing to read.
|
||||
assert!(h.posterior_of_at(-1, &[(&"a", 1.0)]).is_err());
|
||||
assert!(
|
||||
h.joint()
|
||||
.unwrap()
|
||||
.posterior_of_at(-1, &[(&"a", 1.0)])
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
/// Times between slices resolve to the latest appearance at or before them.
|
||||
@@ -289,8 +322,16 @@ fn a_time_between_slices_reads_the_previous_appearance() {
|
||||
.unwrap();
|
||||
let _ = h.converge().unwrap();
|
||||
|
||||
let at_zero = h.posterior_of_at(0, &[(&"a", 1.0)]).unwrap();
|
||||
let between = h.posterior_of_at(50, &[(&"a", 1.0)]).unwrap();
|
||||
let at_zero = h
|
||||
.joint()
|
||||
.unwrap()
|
||||
.posterior_of_at(0, &[(&"a", 1.0)])
|
||||
.unwrap();
|
||||
let between = h
|
||||
.joint()
|
||||
.unwrap()
|
||||
.posterior_of_at(50, &[(&"a", 1.0)])
|
||||
.unwrap();
|
||||
assert!((at_zero.mu() - between.mu()).abs() < 1e-12);
|
||||
assert!((at_zero.sigma() - between.sigma()).abs() < 1e-12);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user