diff --git a/benches/joint.rs b/benches/joint.rs index 41f358b..f6c0eb1 100644 --- a/benches/joint.rs +++ b/benches/joint.rs @@ -58,8 +58,11 @@ fn bench_joint(c: &mut Criterion) { bencher.iter(|| std::hint::black_box(h.joint().unwrap().variables())); }); + // Factorise-and-query, the cost the deleted `History::posterior_of` + // wrapper paid on every call. Kept as the baseline the cached query below + // is measured against. c.bench_function("posterior_of_one_shot_480_appearances", |bencher| { - bencher.iter(|| std::hint::black_box(h.posterior_of(&terms).unwrap())); + bencher.iter(|| std::hint::black_box(h.joint().unwrap().posterior_of(&terms).unwrap())); }); let joint = h.joint().unwrap(); diff --git a/src/history.rs b/src/history.rs index 0f334ef..bc4d9e4 100644 --- a/src/history.rs +++ b/src/history.rs @@ -1529,156 +1529,16 @@ impl, O: Observer, K: Eq + Hash + Clone> History(&self, terms: &[(&Q, f64)]) -> Result - where - K: Borrow, - Q: Hash + Eq + ?Sized + std::fmt::Debug, - { - self.joint()?.posterior_of(terms) - } - - /// Posterior of a linear combination, read as of `time`. - /// - /// Each competitor is taken at their latest appearance at or before `time`, - /// which is the same reading [`History::learning_curve`] gives. Use this - /// when a comparison must be anchored to a moment — "how did these two - /// stand at the end of last season" — rather than to wherever each - /// competitor was last seen. - /// - /// As with [`History::posterior_of`], this factorises the joint for one - /// question; [`History::joint`] amortises that across many. - /// - /// # Errors - /// - /// As [`History::posterior_of`], plus `UnknownKey` for a competitor with no - /// appearance at or before `time`. - pub fn posterior_of_at( - &self, - time: T, - terms: &[(&Q, f64)], - ) -> Result - where - K: Borrow, - Q: Hash + Eq + ?Sized + std::fmt::Debug, - { - self.joint()?.posterior_of_at(time, terms) - } - - /// How much observing this matchup would shrink the variance of `target`. - /// - /// `target` is a linear functional in the same shape - /// [`History::posterior_of`] takes, so the usual question — "which round - /// would best tell these two competitors apart" — is - /// `target = [(a, 1.0), (b, -1.0)]` scored across candidate matchups. - /// - /// This is the scored counterpart to - /// [`expected_information_gain`](crate::expected_information_gain), which - /// enumerates discrete outcomes and cannot be asked about a continuous - /// score. It is also far cheaper: one linear solve rather than a full - /// inference pass per possible outcome. - /// - /// Scoring a field of candidates is the whole point of this call, and each - /// candidate is one question against an unchanged fit — so use - /// [`Joint::expected_variance_reduction`] for anything past a single - /// candidate, or pay for the factorisation once per candidate. - /// - /// # There is no expectation to take - /// - /// Observing a scored event is a rank-one update to the precision matrix, - /// and by the Sherman-Morrison identity the resulting variance reduction is - /// - /// ```text - /// (c^T L^-1 a)^2 / (v + a^T L^-1 a) - /// ``` - /// - /// which depends on *which* matchup is played but not on how it turns out. - /// For a Gaussian likelihood the posterior variance is data-independent, so - /// the expectation over outcomes is over a constant. The name keeps the - /// term the active-learning literature uses; no averaging happens. - /// - /// Verified against an actual refit to six decimal places for four - /// candidate matchups. - /// - /// # Errors - /// - /// As [`History::posterior_of`], plus `MismatchedShape` unless exactly two - /// teams are supplied and `EmptyTeam` for an empty one. - pub fn expected_variance_reduction( - &self, - teams: &[&[&Q]], - target: &[(&Q, f64)], - ) -> Result - where - K: Borrow, - Q: Hash + Eq + ?Sized + std::fmt::Debug, - { - self.joint()?.expected_variance_reduction(teams, target) - } - /// Factorise the joint posterior once, to answer many questions against it. /// - /// [`History::posterior_of`] and its neighbours each build and factorise - /// the joint, use it once, and drop it. The factorisation is `O(n^3)` in - /// the history's *appearances* and depends only on the fit, so a caller - /// asking about every pair in a standings table, every cell in a grid, or - /// every candidate in an active-learning sweep pays for the same - /// factorisation once per question. + /// The factorisation is `O(n^3)` in the history's *appearances* and + /// depends only on the fit, so a caller asking about every pair in a + /// standings table, every cell in a grid, or every candidate in an + /// active-learning sweep should pay for it once rather than once per + /// question. This handle is the only way to ask those questions: `History` + /// carried one-shot wrappers that re-factorised every call, and they were + /// removed in #78 precisely because the borrow here is what makes the cost + /// visible. /// /// A `Joint` pays it once. Each subsequent query is `O(n^2)` — one forward /// substitution — and returns exactly what the one-shot call would. @@ -1815,7 +1675,7 @@ impl, O: Observer, K: Eq + Hash + Clone> History, O: Observer, K: Eq + Hash + Clone> Joint<'_, T, D, /// Posterior of a linear combination of competitors' skills. /// - /// Identical to [`History::posterior_of`], including which appearance each - /// competitor is read at, without re-paying the factorisation. + /// `terms` pairs each competitor with a coefficient, so + /// `[(a, 1.0), (b, -1.0)]` is the skill *gap* between them — with the + /// covariance between the two accounted for, which is the whole reason to + /// go through the joint rather than subtract two marginals. + /// + /// Each competitor is read at their own latest appearance. Use + /// [`Joint::posterior_of_at`] to anchor the reading to a moment instead. /// /// # Errors /// @@ -2930,8 +2795,11 @@ impl, O: Observer, K: Eq + Hash + Clone> Joint<'_, T, D, /// Posterior of a linear combination, read as of `time`. /// - /// Identical to [`History::posterior_of_at`] without re-paying the - /// factorisation. + /// As [`Joint::posterior_of`], but every competitor is read at their + /// latest appearance at or before `time` — the same reading + /// [`History::learning_curve`] gives. Use it when a comparison must be + /// anchored to a moment ("how did these two stand at the end of last + /// season") rather than to wherever each competitor was last seen. /// /// # Errors /// @@ -2970,9 +2838,14 @@ impl, O: Observer, K: Eq + Hash + Clone> Joint<'_, T, D, /// How much observing this matchup would shrink the variance of `target`. /// - /// Identical to [`History::expected_variance_reduction`] without re-paying - /// the factorisation, which is the shape this call is normally used in: - /// one target, a field of candidate matchups, one unchanged fit. + /// `target` is a linear functional in the same shape + /// [`Joint::posterior_of`] takes: the question you want sharpened. The + /// answer is how much observing this matchup would shrink that question's + /// variance. + /// + /// This is the shape the call is normally used in — one target, a field of + /// candidate matchups, one unchanged fit — which is why it lives on the + /// handle and the factorisation is paid once for the whole field. /// /// # Errors /// diff --git a/tests/additive_model.rs b/tests/additive_model.rs index fbe0c8e..c4f25bb 100644 --- a/tests/additive_model.rs +++ b/tests/additive_model.rs @@ -80,7 +80,7 @@ fn additive_structure_makes_sums_wide_and_differences_tight() { println!("\n== the same nodes via posterior_of (exact marginal) =="); for k in players.iter().chain(holes.iter()) { - let g = h.posterior_of(&[(k, 1.0)]).unwrap(); + let g = h.joint().unwrap().posterior_of(&[(k, 1.0)]).unwrap(); println!(" {k}: mu {:>8.4} sigma {:>8.4}", g.mu(), g.sigma()); } @@ -97,7 +97,7 @@ fn additive_structure_makes_sums_wide_and_differences_tight() { vec![(&"h0", 1.0), (&"h1", -1.0)], ), ] { - let joint = h.posterior_of(&terms).unwrap(); + let joint = h.joint().unwrap().posterior_of(&terms).unwrap(); // what a consumer gets today by adding marginals let naive: f64 = terms .iter() @@ -132,7 +132,12 @@ fn additive_structure_makes_sums_wide_and_differences_tight() { // shares with its partners is pinned only by the prior. for k in players.iter().chain(holes.iter()) { let bp = h.current_skill(k).unwrap().sigma(); - let exact = h.posterior_of(&[(k, 1.0)]).unwrap().sigma(); + let exact = h + .joint() + .unwrap() + .posterior_of(&[(k, 1.0)]) + .unwrap() + .sigma(); assert!( exact > 3.0 * bp, "{k}: exact marginal {exact} should be much wider than the reported \ diff --git a/tests/cross_process_determinism.rs b/tests/cross_process_determinism.rs index 0157cf8..c9f197a 100644 --- a/tests/cross_process_determinism.rs +++ b/tests/cross_process_determinism.rs @@ -84,13 +84,17 @@ fn fingerprint() -> String { let known = "p0".to_string(); terms.push((&known, -1.0)); - let posterior = h.posterior_of(&terms).unwrap(); + let posterior = h.joint().unwrap().posterior_of(&terms).unwrap(); let a = "p0".to_string(); let b = "p1".to_string(); let target = [(&a, 1.0), (&b, -1.0)]; let teams: [&[&String]; 2] = [&[&a], &[&b]]; - let evr = h.expected_variance_reduction(&teams, &target).unwrap(); + let evr = h + .joint() + .unwrap() + .expected_variance_reduction(&teams, &target) + .unwrap(); let curves = h.learning_curves(); let mut curve_bits: u64 = 0; diff --git a/tests/joint_handle.rs b/tests/joint_handle.rs index 2d4040d..bb63490 100644 --- a/tests/joint_handle.rs +++ b/tests/joint_handle.rs @@ -78,14 +78,19 @@ const PAIRS: [(&str, &str); 6] = [ ("c", "d"), ]; +/// A joint reused across questions answers exactly what a fresh one per +/// question does. That is the whole correctness claim behind caching the +/// factorisation (#51); it used to be checked against the `History` one-shot +/// wrappers, which were deleted in #78, so it is checked against a fresh +/// factorisation instead — the same comparison, without the wrapper. #[test] -fn a_joint_answers_exactly_what_the_one_shot_call_does() { +fn a_reused_joint_answers_exactly_what_a_fresh_one_does() { let h = fitted(UnknownKeys::Reject); let joint = h.joint().unwrap(); for (a, b) in PAIRS { let terms = [(&a, 1.0), (&b, -1.0)]; - let one_shot = h.posterior_of(&terms).unwrap(); + let one_shot = h.joint().unwrap().posterior_of(&terms).unwrap(); let cached = joint.posterior_of(&terms).unwrap(); assert_eq!(one_shot.mu(), cached.mu(), "{a} - {b}"); assert_eq!(one_shot.variance(), cached.variance(), "{a} - {b}"); @@ -100,7 +105,7 @@ fn a_joint_agrees_at_a_pinned_time_too() { for time in 1..=5 { for (a, b) in PAIRS { let terms = [(&a, 1.0), (&b, -1.0)]; - let one_shot = h.posterior_of_at(time, &terms); + let one_shot = h.joint().unwrap().posterior_of_at(time, &terms); let cached = joint.posterior_of_at(time, &terms); match (one_shot, cached) { (Ok(x), Ok(y)) => { @@ -123,7 +128,11 @@ fn a_joint_scores_candidate_matchups_identically() { for (x, y) in PAIRS { let teams: [&[&&str]; 2] = [&[&x], &[&y]]; - let one_shot = h.expected_variance_reduction(&teams, &target).unwrap(); + let one_shot = h + .joint() + .unwrap() + .expected_variance_reduction(&teams, &target) + .unwrap(); let cached = joint.expected_variance_reduction(&teams, &target).unwrap(); assert_eq!(one_shot, cached, "{x} vs {y}"); } @@ -252,15 +261,15 @@ fn unknown_keys_are_rejected_per_query() { } /// Under `Prior`, an unseen competitor is independent of everything in the -/// history, and the cached path must add the same prior variance the one-shot -/// path does. +/// history, and a reused joint must add the same prior variance a fresh one +/// does. #[test] -fn unseen_competitors_match_the_one_shot_path() { +fn unseen_competitors_match_a_fresh_factorisation() { let h = fitted(UnknownKeys::Prior); let joint = h.joint().unwrap(); let (a, z) = ("a", "nobody"); let terms = [(&a, 1.0), (&z, -1.0)]; - let one_shot = h.posterior_of(&terms).unwrap(); + let one_shot = h.joint().unwrap().posterior_of(&terms).unwrap(); let cached = joint.posterior_of(&terms).unwrap(); assert_eq!(one_shot.mu(), cached.mu()); assert_eq!(one_shot.variance(), cached.variance()); diff --git a/tests/key_ergonomics.rs b/tests/key_ergonomics.rs index 5faaa08..89d5352 100644 --- a/tests/key_ergonomics.rs +++ b/tests/key_ergonomics.rs @@ -62,19 +62,26 @@ fn every_team_shaped_query_accepts_the_same_slice() { #[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)]; + // A scored history, because the joint needs one. + let mut h: Owned = History::builder().key_type::().build(); + for t in 1..=4 { + h.event(t) + .team([String::from("alice")]) + .team([String::from("bob")]) + .scores([21.0, 9.0]) + .commit() + .expect("ingests"); + } + h.converge().expect("converges"); - // 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 + let terms: &[(&str, f64)] = &[("alice", 1.0), ("bob", -1.0)]; + let gap = h + .joint() + .expect("scored history has a joint") .posterior_of(terms) - .expect_err("ranked history has no joint"); - assert!( - format!("{err}").contains("ranked"), - "expected the joint-unavailable path, got {err}" - ); + .expect("both keys are known"); + + assert!(gap.mu() > 0.0, "alice outscored bob every round"); } /// `lookup` is gone with `Index` (#73); the accessors that answer the same diff --git a/tests/marginal_calibration.rs b/tests/marginal_calibration.rs index 2eccf55..bded762 100644 --- a/tests/marginal_calibration.rs +++ b/tests/marginal_calibration.rs @@ -281,6 +281,8 @@ fn posterior_of_matches_the_exact_joint() { for (i, j) in [(0usize, 1usize), (0, 2), (1, 3), (2, 4)] { let got = h + .joint() + .unwrap() .posterior_of(&[(&key(i), 1.0), (&key(j), -1.0)]) .expect("scored slice should have a joint"); let exact_sd = (cov[i][i] + cov[j][j] - 2.0 * cov[i][j]).sqrt(); @@ -302,7 +304,7 @@ fn posterior_of_matches_the_exact_joint() { // A single competitor: this is where the loopy marginal was 2x narrow. for (i, row) in cov.iter().enumerate() { - let got = h.posterior_of(&[(&key(i), 1.0)]).unwrap(); + let got = h.joint().unwrap().posterior_of(&[(&key(i), 1.0)]).unwrap(); let exact_sd = row[i].sqrt(); assert!( (got.sigma() - exact_sd).abs() / exact_sd < 1e-9, @@ -361,6 +363,8 @@ fn cost_scaling() { let t = Instant::now(); let g = h + .joint() + .unwrap() .posterior_of(&[(&names[0], 1.0), (&names[1], -1.0)]) .unwrap(); println!(" n={n:>4}: {:>10.2?} sigma {:.6}", t.elapsed(), g.sigma()); diff --git a/tests/predict_margin.rs b/tests/predict_margin.rs index c85866a..4e5b047 100644 --- a/tests/predict_margin.rs +++ b/tests/predict_margin.rs @@ -120,6 +120,8 @@ fn swapping_the_teams_negates_the_margin() { fn the_predictive_interval_exceeds_the_skill_uncertainty() { let h = fitted(UnknownKeys::Prior); let skill_gap = h + .joint() + .unwrap() .posterior_of(&[(&"veteran", 1.0), (&"regular", -1.0)]) .unwrap(); let predictive = h.predict_margin(&[&[&"veteran"], &[&"regular"]]).unwrap(); diff --git a/tests/time_expanded_joint.rs b/tests/time_expanded_joint.rs index 5ace6c1..c54e6aa 100644 --- a/tests/time_expanded_joint.rs +++ b/tests/time_expanded_joint.rs @@ -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); } diff --git a/tests/variance_reduction.rs b/tests/variance_reduction.rs index c1abcea..dd97897 100644 --- a/tests/variance_reduction.rs +++ b/tests/variance_reduction.rs @@ -60,15 +60,30 @@ fn fit(extra: Option>, 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 { .. }) )); }