somatize_study/sampler/sobol.rs
1//! Spread on purpose, and without a seam.
2
3use super::Sampler;
4use super::drawing::{draw, splitmix};
5use crate::{Point, Space};
6
7/// Cover the space evenly, one binary bisection per knob.
8///
9/// The same promise as [`Halton`](super::Halton) — uniform by construction for
10/// every prefix — paid for elsewhere: Halton reads knob `d` in base the `d`-th
11/// prime, so its cover thins as the primes grow, while this reads every knob in
12/// base two and separates them with a set of direction numbers each.
13///
14/// Chosen, and that is the price: a Sobol sequence built on the wrong numbers
15/// does not fail, it covers worse and nobody finds out. These are Joe and Kuo's
16/// (2008), and a test walks the first dimensions against published values,
17/// because reading a table is not a way of checking it. Past [`KNOBS`]
18/// dimensions `ask` answers `None` from the first trial rather than quietly in
19/// the middle. Its point is a function of the **seed and the index**.
20#[derive(Debug, Clone, PartialEq, Eq, Hash)]
21pub struct Sobol {
22 /// The seed, which here shifts the digits rather than drawing them: without
23 /// it a Sobol sequence is one fixed sequence and two studies of the same
24 /// space would walk it in exactly the same order.
25 pub seed: u64,
26}
27
28/// How many knobs the table reaches, and so how many this can search.
29pub const KNOBS: usize = 32;
30
31/// How many numbers each knob's direction gets, which is the width of the whole
32/// arithmetic and so how many trials there are before it comes round again.
33const BITS: usize = 32;
34
35impl Sobol {
36 /// The `trial`-th point, or `None` when there are more knobs than the table
37 /// has. It never runs out, and it never looks at what the finished trials
38 /// did.
39 pub fn ask(
40 &self,
41 space: &Space,
42 trial: usize,
43 _seen: &[(Point, Option<f64>)],
44 ) -> Option<Point> {
45 if space.is_empty() || space.len() > KNOBS {
46 return None;
47 }
48 // Gray code: consecutive trials differ in exactly one bit, so each point
49 // is the one before it with a single direction number flipped into it —
50 // which is what keeps every prefix balanced and not just the whole.
51 let step = trial as u64 ^ (trial as u64 >> 1);
52 Some(Point::of(
53 space
54 .dimensions()
55 .iter()
56 .enumerate()
57 .map(|(which, (name, dimension))| {
58 let numbers = directions(which);
59 let mut place = shift(self.seed, which);
60 for (bit, number) in numbers.iter().enumerate() {
61 if (step >> bit) & 1 == 1 {
62 place ^= number;
63 }
64 }
65 let u = place as f64 / (1u64 << BITS) as f64;
66 (name.clone(), draw(dimension, u))
67 })
68 .collect(),
69 ))
70 }
71}
72
73/// This knob's direction numbers: the `i`-th is folded in when bit `i` of the
74/// step is set. The first `s` come from the table; the rest are the recurrence
75/// the primitive polynomial stands for.
76fn directions(which: usize) -> [u32; BITS] {
77 let (a, m) = DIRECTIONS[which];
78 let mut numbers = [0u32; BITS];
79 // The first knob has no polynomial and no recurrence: its numbers are a
80 // single one walking rightwards, which is plain bisection.
81 if m.is_empty() {
82 for (i, number) in numbers.iter_mut().enumerate() {
83 *number = 1 << (BITS - 1 - i);
84 }
85 return numbers;
86 }
87 let s = m.len();
88 for i in 0..s {
89 numbers[i] = m[i] << (BITS - 1 - i);
90 }
91 for i in s..BITS {
92 numbers[i] = numbers[i - s] ^ (numbers[i - s] >> s);
93 for k in 1..s {
94 numbers[i] ^= ((a >> (s - 1 - k)) & 1) * numbers[i - k];
95 }
96 }
97 numbers
98}
99
100/// What this knob's numbers are shifted by. A digital shift and not a
101/// rearrangement: xoring a constant into every point moves the whole set without
102/// disturbing which cell each one is in, so the cover survives the seed intact.
103fn shift(seed: u64, which: usize) -> u32 {
104 let mut state = seed ^ (which as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15);
105 splitmix(&mut state) as u32
106}
107
108/// Joe and Kuo's direction numbers, `(the polynomial's middle coefficients, the
109/// numbers it starts from)`, one row per knob. *Constructing Sobol sequences
110/// with better two-dimensional projections*, SIAM J. Sci. Comput. 30, 2635–2654
111/// (2008) — the `new-joe-kuo-6` table.
112const DIRECTIONS: [(u32, &[u32]); KNOBS] = [
113 (0, &[]),
114 (0, &[1]),
115 (1, &[1, 3]),
116 (1, &[1, 3, 1]),
117 (2, &[1, 1, 1]),
118 (1, &[1, 1, 3, 3]),
119 (4, &[1, 3, 5, 13]),
120 (2, &[1, 1, 5, 5, 17]),
121 (4, &[1, 1, 5, 5, 5]),
122 (7, &[1, 1, 7, 11, 19]),
123 (11, &[1, 1, 5, 1, 1]),
124 (13, &[1, 1, 1, 3, 11]),
125 (14, &[1, 3, 5, 5, 31]),
126 (1, &[1, 3, 3, 9, 7, 49]),
127 (13, &[1, 1, 1, 15, 21, 21]),
128 (16, &[1, 3, 1, 13, 27, 49]),
129 (19, &[1, 1, 1, 15, 7, 5]),
130 (22, &[1, 3, 1, 15, 13, 25]),
131 (25, &[1, 1, 5, 5, 19, 61]),
132 (1, &[1, 3, 7, 11, 23, 15, 103]),
133 (4, &[1, 3, 7, 13, 13, 15, 69]),
134 (7, &[1, 1, 3, 13, 7, 35, 63]),
135 (8, &[1, 3, 5, 9, 1, 25, 53]),
136 (14, &[1, 3, 1, 13, 9, 35, 107]),
137 (19, &[1, 3, 1, 5, 27, 61, 31]),
138 (21, &[1, 1, 5, 11, 19, 41, 61]),
139 (28, &[1, 3, 5, 3, 3, 13, 69]),
140 (31, &[1, 1, 7, 13, 1, 19, 1]),
141 (32, &[1, 3, 7, 5, 13, 19, 59]),
142 (37, &[1, 1, 3, 9, 25, 29, 41]),
143 (41, &[1, 3, 5, 13, 23, 1, 55]),
144 (42, &[1, 3, 7, 3, 13, 59, 17]),
145];
146
147impl From<Sobol> for Sampler {
148 fn from(how: Sobol) -> Self {
149 Self::Sobol(how)
150 }
151}