gam_problem/row_measure.rs
1//! Row-subsample mask handle for trust-region invariant enforcement.
2//!
3//! A `RowSubsampleMask` is the explicit identity of the set of rows + per-row
4//! weights used to evaluate any one of {Hessian, gradient, objective}
5//! during a single inner trust-region iteration. The trust-region
6//! globalization computes
7//!
8//! ρ = actual_reduction / predicted_reduction
9//! = [F(β) − F(β + δ)] / [−g·δ − ½·δᵀHδ]
10//!
11//! and accepts/rejects the step from ρ. All four quantities (F(β),
12//! F(β + δ), g, H) MUST be evaluated against the same row measure for
13//! ρ to be meaningful; otherwise the numerator and denominator estimate
14//! different objectives and ρ can take any sign, producing the observed
15//! ρ = -0.05 with predicted_reduction = +7.378e6 sign flip.
16//!
17//! `RowSubsampleMask::id` is a stable 64-bit content hash: equal masks
18//! (`Arc<OuterScoreSubsample>` pointer equality OR identical mask
19//! contents) ⇒ equal ids; differing masks ⇒ differing ids with high
20//! probability. The TR loop captures one `RowSubsampleMask` at the top of an
21//! iteration and hard-asserts that the id observed by each of the four
22//! quantities matches before computing ρ.
23//!
24//! The `BlockwiseFitOptions`-coupled `from_options` constructor stays up in
25//! `gam-solve` (it depends on the options type, which lives above this tier);
26//! the data type and its pure data methods live here so lower tiers can
27//! consume the measure without depending on `gam-solve`.
28
29use std::sync::Arc;
30
31use crate::outer_subsample::OuterScoreSubsample;
32
33/// Identifier-carrying handle for a single row subsample mask.
34///
35/// The handle is `Clone` and cheap to copy; the `Arc` is shared, not
36/// duplicated.
37#[derive(Clone, Debug)]
38pub struct RowSubsampleMask {
39 /// Stable 64-bit content hash. Same `mask` (by Arc pointer OR by
40 /// row content) ⇒ same id; different `mask` ⇒ different id.
41 pub id: u64,
42 /// `None` means full data (`0..n`, weight 1.0 per row).
43 /// `Some(_)` means the rows and HT weights inside the subsample.
44 pub mask: Option<Arc<OuterScoreSubsample>>,
45}
46
47impl RowSubsampleMask {
48 /// Full-data measure: walk `0..n` with weight 1.0 per row.
49 pub fn full_data(n: usize) -> Self {
50 Self {
51 id: hash_full(n),
52 mask: None,
53 }
54 }
55
56 /// Subsample measure: walk the mask's rows with their per-row HT
57 /// weights. Id is derived from the Arc pointer (cheap and stable
58 /// for the lifetime of the Arc) combined with mask metadata.
59 pub fn subsample(mask: Arc<OuterScoreSubsample>) -> Self {
60 let id = hash_subsample(&mask);
61 Self {
62 id,
63 mask: Some(mask),
64 }
65 }
66
67}
68
69/// Thin wrapper over the canonical SplitMix64 hash in
70/// [`gam_linalg::utils::splitmix64_hash`].
71fn splitmix64(x: u64) -> u64 {
72 gam_linalg::utils::splitmix64_hash(x)
73}
74
75const FULL_DATA_ROW_SUBSAMPLE_SENTINEL: u64 = 0xA5A5_5A5A_DEAD_BEEF;
76
77fn hash_full(n: usize) -> u64 {
78 let mut h = splitmix64(FULL_DATA_ROW_SUBSAMPLE_SENTINEL ^ (n as u64));
79 if h == 0 {
80 h = 0x1234_5678_9ABC_DEF0;
81 }
82 h
83}
84
85fn hash_subsample(mask: &Arc<OuterScoreSubsample>) -> u64 {
86 let ptr = Arc::as_ptr(mask) as u64;
87 let mut h = splitmix64(ptr);
88 h ^= splitmix64(mask.n_full as u64);
89 h ^= splitmix64(mask.len() as u64);
90 h ^= splitmix64(mask.seed);
91 h ^= splitmix64((mask.weight_scale.to_bits()) ^ 0xC0FF_EE00_0000_0000);
92 if h == 0 {
93 h = 0xDEAD_BEEF_FEED_FACE;
94 }
95 h
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101 use crate::outer_subsample::OuterScoreSubsample;
102
103 #[test]
104 fn full_data_id_is_stable_per_n() {
105 let a = RowSubsampleMask::full_data(100);
106 let b = RowSubsampleMask::full_data(100);
107 let c = RowSubsampleMask::full_data(101);
108 assert_eq!(a.id, b.id);
109 assert_ne!(a.id, c.id);
110 assert!(a.mask.is_none());
111 }
112
113 #[test]
114 fn subsample_id_matches_for_same_arc() {
115 let s = Arc::new(OuterScoreSubsample::from_uniform_inclusion_mask(
116 vec![1, 3, 5],
117 10,
118 42,
119 ));
120 let a = RowSubsampleMask::subsample(Arc::clone(&s));
121 let b = RowSubsampleMask::subsample(Arc::clone(&s));
122 assert_eq!(a.id, b.id);
123 }
124
125 #[test]
126 fn subsample_id_differs_for_different_arcs() {
127 let s1 = Arc::new(OuterScoreSubsample::from_uniform_inclusion_mask(
128 vec![1, 3, 5],
129 10,
130 42,
131 ));
132 let s2 = Arc::new(OuterScoreSubsample::from_uniform_inclusion_mask(
133 vec![1, 3, 5],
134 10,
135 42,
136 ));
137 let a = RowSubsampleMask::subsample(s1);
138 let b = RowSubsampleMask::subsample(s2);
139 // Different Arc allocations ⇒ different ids; this is intentional
140 // so the TR invariant catches mid-iteration mask rebuilds even
141 // when the resulting mask happens to be content-equal.
142 assert_ne!(a.id, b.id);
143 }
144
145}