Skip to main content

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}