1use core::fmt;
4
5#[derive(Clone, Copy, Debug, Eq, PartialEq)]
7pub enum SamplerAlgorithm {
8 SplitMix64V1,
10}
11
12#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14pub struct SamplerState {
15 pub algorithm: SamplerAlgorithm,
17 pub state: u64,
19 pub words_generated: u64,
21 pub seed: u64,
23 pub stream: u64,
25}
26
27#[derive(Clone, Copy, Debug, Eq, PartialEq)]
29pub struct SamplerReceipt {
30 pub state: SamplerState,
32 pub max_words: Option<u64>,
34}
35
36#[derive(Clone, Debug, Eq, PartialEq)]
42pub struct SeededSampler {
43 state: SamplerState,
44 max_words: Option<u64>,
45}
46
47impl SeededSampler {
48 pub fn new(seed: u64) -> Self {
51 Self::from_parts(seed, 0, seed, None)
52 }
53
54 pub fn with_max_words(seed: u64, max_words: u64) -> Self {
56 Self::from_parts(seed, 0, seed, Some(max_words))
57 }
58
59 fn from_parts(seed: u64, stream: u64, state: u64, max_words: Option<u64>) -> Self {
60 Self {
61 state: SamplerState {
62 algorithm: SamplerAlgorithm::SplitMix64V1,
63 state,
64 words_generated: 0,
65 seed,
66 stream,
67 },
68 max_words,
69 }
70 }
71
72 pub fn replay(receipt: SamplerReceipt) -> Self {
74 Self {
75 state: receipt.state,
76 max_words: receipt.max_words,
77 }
78 }
79
80 pub fn receipt(&self) -> SamplerReceipt {
82 SamplerReceipt {
83 state: self.state,
84 max_words: self.max_words,
85 }
86 }
87
88 pub fn fork(&self, label: u64) -> Self {
91 let state = mix(self.state.seed
92 ^ label.wrapping_mul(0xd2b7_4407_b1ce_6e93)
93 ^ 0xa076_1d64_78bd_642f);
94 Self::from_parts(self.state.seed, label, state, self.max_words)
95 }
96
97 pub fn try_next_u64(&mut self) -> Result<u64, DesignError> {
99 if self
100 .max_words
101 .is_some_and(|limit| self.state.words_generated >= limit)
102 {
103 return Err(DesignError::WorkLimit {
104 required: self.state.words_generated.saturating_add(1),
105 limit: self.max_words.unwrap_or(0),
106 });
107 }
108 self.state.state = self.state.state.wrapping_add(0x9e37_79b9_7f4a_7c15);
109 self.state.words_generated += 1;
110 Ok(mix(self.state.state))
111 }
112
113 pub(crate) fn next_u64(&mut self) -> u64 {
115 self.try_next_u64()
116 .expect("unbounded or preflighted sampler")
117 }
118
119 pub fn unit_interval(&mut self) -> f64 {
121 (self.next_u64() >> 11) as f64 * (1.0 / ((1_u64 << 53) as f64))
122 }
123
124 pub(crate) fn index_modulo(&mut self, length: usize) -> usize {
126 (self.next_u64() % length as u64) as usize
127 }
128
129 pub(crate) fn index_multiply_high(&mut self, length: usize) -> usize {
131 ((u128::from(self.next_u64()) * length as u128) >> 64) as usize
132 }
133}
134
135fn mix(mut value: u64) -> u64 {
136 value = (value ^ (value >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
137 value = (value ^ (value >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
138 value ^ (value >> 31)
139}
140
141#[derive(Clone, Copy, Debug, Eq, PartialEq)]
143pub enum Scramble {
144 None,
146 DigitalShift,
148}
149
150#[derive(Clone, Debug, PartialEq)]
152pub struct UntestedRegion {
153 pub label: String,
155 pub reason: String,
157}
158
159#[derive(Clone, Debug, PartialEq)]
161pub struct CoverageEvidence {
162 pub sequence_identity: String,
164 pub boundary_injections: Vec<usize>,
166 pub duplicates: Vec<(usize, usize)>,
168 pub stratum_occupancy: Vec<Vec<usize>>,
170 pub sampler: Option<SamplerReceipt>,
172 pub untested_regions: Vec<UntestedRegion>,
174 pub work: u64,
176}
177
178#[derive(Clone, Debug, PartialEq)]
180pub struct SampleDesign {
181 pub points: Vec<Vec<f64>>,
183 pub coverage: CoverageEvidence,
185}
186
187#[derive(Clone, Debug, PartialEq)]
189pub struct LatinHypercubePlan {
190 pub dimensions: usize,
192 pub points: usize,
194 pub seed: u64,
196 pub max_work: u64,
198 pub untested_regions: Vec<UntestedRegion>,
200}
201
202impl LatinHypercubePlan {
203 pub fn generate(&self) -> Result<SampleDesign, DesignError> {
205 validate_shape(self.dimensions, self.points, 64)?;
206 let required = (self.dimensions as u64)
207 .checked_mul(self.points.saturating_sub(1) as u64)
208 .ok_or(DesignError::Overflow)?;
209 if required > self.max_work {
210 return Err(DesignError::WorkLimit {
211 required,
212 limit: self.max_work,
213 });
214 }
215 let mut sampler = SeededSampler::with_max_words(self.seed, self.max_work);
216 let mut points = vec![vec![0.0; self.dimensions]; self.points];
217 let occupancy = vec![vec![1; self.points]; self.dimensions];
218 for (dimension, _) in occupancy.iter().enumerate() {
219 let mut permutation = (0..self.points).collect::<Vec<_>>();
220 for end in (1..self.points).rev() {
221 let chosen = sampler.index_modulo(end + 1);
222 permutation.swap(end, chosen);
223 }
224 for (row, stratum) in permutation.into_iter().enumerate() {
225 points[row][dimension] = (stratum as f64 + 0.5) / self.points as f64;
226 }
227 }
228 Ok(design(
229 points,
230 format!("latin-hypercube/centered-v1;seed={}", self.seed),
231 vec![],
232 occupancy,
233 Some(sampler.receipt()),
234 self.untested_regions.clone(),
235 required,
236 ))
237 }
238}
239
240#[derive(Clone, Debug, PartialEq)]
242pub struct SobolPlan {
243 pub dimensions: usize,
245 pub points: usize,
247 pub skip: u64,
249 pub scramble: Scramble,
251 pub seed: u64,
253 pub max_work: u64,
255 pub untested_regions: Vec<UntestedRegion>,
257}
258
259impl SobolPlan {
260 pub fn generate(&self) -> Result<SampleDesign, DesignError> {
262 validate_shape(self.dimensions, self.points, 4)?;
263 let end = self
264 .skip
265 .checked_add(self.points as u64)
266 .ok_or(DesignError::Overflow)?;
267 if end > u32::MAX as u64 {
268 return Err(DesignError::UnsupportedPoint { point: end });
269 }
270 let required = (self.dimensions as u64)
271 .checked_mul(self.points as u64)
272 .ok_or(DesignError::Overflow)?;
273 if required > self.max_work {
274 return Err(DesignError::WorkLimit {
275 required,
276 limit: self.max_work,
277 });
278 }
279 let mut sampler = SeededSampler::new(self.seed);
280 let shifts = (0..self.dimensions)
281 .map(|_| {
282 if self.scramble == Scramble::DigitalShift {
283 sampler.next_u64()
284 } else {
285 0
286 }
287 })
288 .collect::<Vec<_>>();
289 let mut points = Vec::with_capacity(self.points);
290 for index in self.skip..end {
291 let gray = index ^ (index >> 1);
292 let mut row = Vec::with_capacity(self.dimensions);
293 for (dimension, shift) in shifts.iter().copied().enumerate() {
294 let mut bits = 0_u64;
295 for bit in 0..32 {
296 if gray & (1_u64 << bit) != 0 {
297 bits ^= direction(dimension, bit);
298 }
299 }
300 row.push(((bits ^ shift) >> 11) as f64 * (1.0 / ((1_u64 << 53) as f64)));
301 }
302 points.push(row);
303 }
304 let receipt = (self.scramble == Scramble::DigitalShift).then(|| sampler.receipt());
305 Ok(design(
306 points,
307 format!(
308 "sobol/joe-kuo-reviewed-4d-v1;skip={};scramble={:?};seed={}",
309 self.skip, self.scramble, self.seed
310 ),
311 vec![],
312 vec![],
313 receipt,
314 self.untested_regions.clone(),
315 required,
316 ))
317 }
318}
319
320fn direction(dimension: usize, bit: usize) -> u64 {
322 if dimension == 0 {
323 return 1_u64 << (63 - bit);
324 }
325 let (s, a, initial): (usize, u32, &[u32]) = match dimension {
326 1 => (1, 0, &[1]),
327 2 => (2, 1, &[1, 3]),
328 3 => (3, 1, &[1, 3, 1]),
329 _ => unreachable!(),
330 };
331 let mut values = [0_u64; 32];
332 for index in 0..s {
333 values[index] = (initial[index] as u64) << (63 - index);
334 }
335 for index in s..=bit {
336 let mut value = values[index - s] ^ (values[index - s] >> s);
337 for k in 1..s {
338 if ((a >> (s - 1 - k)) & 1) != 0 {
339 value ^= values[index - k];
340 }
341 }
342 values[index] = value;
343 }
344 values[bit]
345}
346
347#[derive(Clone, Debug, PartialEq)]
349pub struct SweepPlan {
350 pub inject_lower_boundary: bool,
352 pub inject_upper_boundary: bool,
354 pub untested_regions: Vec<UntestedRegion>,
356}
357
358impl SweepPlan {
359 pub fn apply(&self, mut design: SampleDesign) -> SampleDesign {
361 let dimensions = design.points.first().map_or(0, Vec::len);
362 let mut boundaries = Vec::new();
363 if self.inject_lower_boundary {
364 design.points.insert(0, vec![0.0; dimensions]);
365 boundaries.push(0);
366 }
367 if self.inject_upper_boundary {
368 boundaries.push(design.points.len());
369 design.points.push(vec![1.0; dimensions]);
370 }
371 design.coverage.boundary_injections = boundaries;
372 design.coverage.duplicates = duplicates(&design.points);
373 design
374 .coverage
375 .untested_regions
376 .extend(self.untested_regions.clone());
377 design
378 .coverage
379 .sequence_identity
380 .push_str(";sweep-boundaries-v1");
381 design
382 }
383}
384
385fn validate_shape(
386 dimensions: usize,
387 points: usize,
388 max_dimensions: usize,
389) -> Result<(), DesignError> {
390 if dimensions == 0 || dimensions > max_dimensions {
391 return Err(DesignError::UnsupportedDimension {
392 requested: dimensions,
393 maximum: max_dimensions,
394 });
395 }
396 if points == 0 {
397 return Err(DesignError::InvalidPointCount);
398 }
399 Ok(())
400}
401fn design(
402 points: Vec<Vec<f64>>,
403 identity: String,
404 boundaries: Vec<usize>,
405 occupancy: Vec<Vec<usize>>,
406 sampler: Option<SamplerReceipt>,
407 untested_regions: Vec<UntestedRegion>,
408 work: u64,
409) -> SampleDesign {
410 let duplicates = duplicates(&points);
411 SampleDesign {
412 points,
413 coverage: CoverageEvidence {
414 sequence_identity: identity,
415 boundary_injections: boundaries,
416 duplicates,
417 stratum_occupancy: occupancy,
418 sampler,
419 untested_regions,
420 work,
421 },
422 }
423}
424fn duplicates(points: &[Vec<f64>]) -> Vec<(usize, usize)> {
425 let mut result = Vec::new();
426 for later in 0..points.len() {
427 if let Some(earlier) = (0..later).find(|&earlier| points[earlier] == points[later]) {
428 result.push((later, earlier));
429 }
430 }
431 result
432}
433
434#[derive(Clone, Debug, Eq, PartialEq)]
436pub enum DesignError {
437 UnsupportedDimension {
439 requested: usize,
441 maximum: usize,
443 },
444 InvalidPointCount,
446 UnsupportedPoint {
448 point: u64,
450 },
451 WorkLimit {
453 required: u64,
455 limit: u64,
457 },
458 Overflow,
460}
461impl fmt::Display for DesignError {
462 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
463 write!(f, "{self:?}")
464 }
465}
466impl std::error::Error for DesignError {}