1use super::*;
2use std::collections::HashSet;
3
4#[derive(Debug, Clone)]
5pub struct CollocationOperatorMatrices {
6 pub d0: Array2<f64>,
7 pub d1: Array2<f64>,
8 pub d2: Array2<f64>,
9 pub collocation_points: Array2<f64>,
10 pub kernel_nullspace_transform: Option<Array2<f64>>,
14 pub polynomial_block_cols: usize,
17}
18
19#[derive(Debug, Clone)]
20pub struct DuchonOperatorPenaltyMatrices {
21 pub mass: Array2<f64>,
22 pub tension: Array2<f64>,
23 pub stiffness: Array2<f64>,
24}
25
26#[derive(Debug, Clone)]
27pub struct ThinPlatePenaltyMatrix {
28 pub penalty: Array2<f64>,
29}
30
31pub(crate) fn validate_center_count(num_centers: usize) -> Result<(), BasisError> {
32 if num_centers == 0 {
33 crate::bail_invalid_basis!("center count must be positive");
34 }
35 Ok(())
36}
37
38struct R42MersenneTwister {
47 state: [u32; 624],
48 index: usize,
49}
50
51impl R42MersenneTwister {
52 fn seeded(seed: u32) -> Self {
53 let mut word = seed;
54 for _ in 0..50 {
55 word = word.wrapping_mul(69_069).wrapping_add(1);
56 }
57
58 word = word.wrapping_mul(69_069).wrapping_add(1);
61 let mut state = [0_u32; 624];
62 for slot in &mut state {
63 word = word.wrapping_mul(69_069).wrapping_add(1);
64 *slot = word;
65 }
66 Self { state, index: 624 }
67 }
68
69 fn next_word(&mut self) -> u32 {
70 const M: usize = 397;
71 const MATRIX_A: u32 = 0x9908_b0df;
72 const UPPER_MASK: u32 = 0x8000_0000;
73 const LOWER_MASK: u32 = 0x7fff_ffff;
74
75 if self.index >= self.state.len() {
76 for k in 0..(self.state.len() - M) {
77 let y = (self.state[k] & UPPER_MASK) | (self.state[k + 1] & LOWER_MASK);
78 self.state[k] =
79 self.state[k + M] ^ (y >> 1) ^ if y & 1 == 0 { 0 } else { MATRIX_A };
80 }
81 for k in (self.state.len() - M)..(self.state.len() - 1) {
82 let y = (self.state[k] & UPPER_MASK) | (self.state[k + 1] & LOWER_MASK);
83 self.state[k] = self.state[k + M - self.state.len()]
84 ^ (y >> 1)
85 ^ if y & 1 == 0 { 0 } else { MATRIX_A };
86 }
87 let last = self.state.len() - 1;
88 let y = (self.state[last] & UPPER_MASK) | (self.state[0] & LOWER_MASK);
89 self.state[last] = self.state[M - 1] ^ (y >> 1) ^ if y & 1 == 0 { 0 } else { MATRIX_A };
90 self.index = 0;
91 }
92
93 let mut y = self.state[self.index];
94 self.index += 1;
95 y ^= y >> 11;
96 y ^= (y << 7) & 0x9d2c_5680;
97 y ^= (y << 15) & 0xefc6_0000;
98 y ^= y >> 18;
99 y
100 }
101
102 fn uniform_index(&mut self, upper: usize) -> usize {
104 assert!(upper > 0, "uniform-index population must be positive");
105 let bits = usize::BITS as usize - (upper - 1).leading_zeros() as usize;
106 loop {
107 let mut value = 0_u128;
108 for _ in (0..bits).step_by(16) {
109 value = (value << 16) | u128::from(self.next_word() >> 16);
110 }
111 let mask = if bits == 0 { 0 } else { (1_u128 << bits) - 1 };
112 let candidate = (value & mask) as usize;
113 if candidate < upper {
114 return candidate;
115 }
116 }
117 }
118}
119
120fn coordinate_row_key<'a>(values: impl Iterator<Item = &'a f64>) -> Vec<u64> {
127 values
128 .map(|&value| gam_data::canonical_level_bits(value))
129 .collect()
130}
131
132pub(crate) fn count_unique_coordinate_rows(values: ArrayView2<'_, f64>, cols: &[usize]) -> usize {
143 let mut seen = HashSet::<Vec<u64>>::with_capacity(values.nrows());
144 let mut unique = 0usize;
145 for row in 0..values.nrows() {
146 if seen.insert(coordinate_row_key(cols.iter().map(|&col| &values[[row, col]]))) {
147 unique += 1;
148 }
149 }
150 unique
151}
152
153pub(crate) fn select_r_uniform_subsample_centers(
164 data: ArrayView2<'_, f64>,
165 num_centers: usize,
166 seed: u32,
167) -> Result<Array2<f64>, BasisError> {
168 validate_center_count(num_centers)?;
169 if data.ncols() == 0 {
170 crate::bail_invalid_basis!("uniform subsampling requires at least one column");
171 }
172
173 let mut seen = HashSet::<Vec<u64>>::with_capacity(data.nrows());
174 let mut unique_rows = Vec::<usize>::with_capacity(data.nrows().min(num_centers));
175 for row in 0..data.nrows() {
176 if seen.insert(coordinate_row_key(data.row(row).iter())) {
177 unique_rows.push(row);
178 }
179 }
180 if unique_rows.len() < num_centers {
181 crate::bail_invalid_basis!(
182 "uniform subsampling requested {num_centers} centers but data has only {} unique rows",
183 unique_rows.len()
184 );
185 }
186
187 let selected = if unique_rows.len() == num_centers {
188 unique_rows
189 } else {
190 let mut rng = R42MersenneTwister::seeded(seed);
191 let mut remaining = unique_rows.len();
192 let mut selected = Vec::with_capacity(num_centers);
193 for _ in 0..num_centers {
194 let choice = rng.uniform_index(remaining);
195 selected.push(unique_rows[choice]);
196 remaining -= 1;
197 unique_rows[choice] = unique_rows[remaining];
198 }
199 selected
200 };
201
202 let mut centers = Array2::<f64>::zeros((num_centers, data.ncols()));
203 for (center, row) in selected.into_iter().enumerate() {
204 centers.row_mut(center).assign(&data.row(row));
205 }
206 Ok(centers)
207}
208
209pub(crate) fn select_equal_mass_centers(
210 data: ArrayView2<'_, f64>,
211 num_centers: usize,
212) -> Result<Array2<f64>, BasisError> {
213 validate_center_count(num_centers)?;
214 let n = data.nrows();
215 let d = data.ncols();
216 if num_centers > n {
217 crate::bail_invalid_basis!(
218 "equal-mass center selection requested {num_centers} centers but data has {n} rows"
219 );
220 }
221 if d == 0 {
222 crate::bail_invalid_basis!("equal-mass center selection requires at least one column");
223 }
224 #[derive(Clone, Copy)]
225 struct Leaf {
226 pub(crate) start: usize,
227 pub(crate) end: usize,
228 }
229
230 let mut order: Vec<usize> = (0..n).collect();
247 let mut leaves = vec![Leaf { start: 0, end: n }];
248
249 let principal_axis = |slice: &[usize]| -> Option<Vec<f64>> {
258 let m = slice.len();
259 if m < 2 {
260 return None;
261 }
262 let mut centroid = vec![0.0_f64; d];
263 for &idx in slice {
264 for j in 0..d {
265 centroid[j] += data[[idx, j]];
266 }
267 }
268 let inv = 1.0 / m as f64;
269 for v in &mut centroid {
270 *v *= inv;
271 }
272 let mut cov = Array2::<f64>::zeros((d, d));
275 for &idx in slice {
276 for a in 0..d {
277 let da = data[[idx, a]] - centroid[a];
278 for b in a..d {
279 let db = data[[idx, b]] - centroid[b];
280 cov[[a, b]] += da * db;
281 }
282 }
283 }
284 for a in 0..d {
285 cov[[a, a]] *= inv;
286 for b in (a + 1)..d {
287 cov[[a, b]] *= inv;
288 cov[[b, a]] = cov[[a, b]];
289 }
290 }
291 if cov.iter().any(|v| !v.is_finite()) {
292 return None;
293 }
294 let mut axis: Vec<f64> = if d == 2 {
312 let sxx = cov[[0, 0]];
313 let syy = cov[[1, 1]];
314 let sxy = cov[[0, 1]];
315 if sxy == 0.0 && sxx == syy {
316 return None;
317 }
318 let angle = 0.5 * (2.0 * sxy).atan2(sxx - syy);
319 vec![angle.cos(), angle.sin()]
320 } else {
321 let (evals, evecs) = cov.eigh(Side::Lower).ok()?;
324 let last = evals.len().checked_sub(1)?;
325 if !(evals[last] > 0.0) {
326 return None;
327 }
328 (0..d).map(|r| evecs[[r, last]]).collect()
329 };
330 if axis.iter().any(|v| !v.is_finite()) {
331 return None;
332 }
333 let mut far_idx = slice[0];
344 let mut far_d2 = f64::NEG_INFINITY;
345 for &idx in slice {
346 let mut d2 = 0.0_f64;
347 for j in 0..d {
348 let delta = data[[idx, j]] - centroid[j];
349 d2 += delta * delta;
350 }
351 if d2 > far_d2 || (d2 == far_d2 && idx < far_idx) {
352 far_d2 = d2;
353 far_idx = idx;
354 }
355 }
356 let mut proj = 0.0_f64;
357 for j in 0..d {
358 proj += (data[[far_idx, j]] - centroid[j]) * axis[j];
359 }
360 if proj < 0.0 {
361 for v in &mut axis {
362 *v = -*v;
363 }
364 } else if proj == 0.0 {
365 let mut pivot = 0usize;
369 for r in 1..d {
370 if axis[r].abs() > axis[pivot].abs() {
371 pivot = r;
372 }
373 }
374 if axis[pivot] < 0.0 {
375 for v in &mut axis {
376 *v = -*v;
377 }
378 }
379 }
380 Some(axis)
381 };
382
383 while leaves.len() < num_centers {
384 let mut split_pos = None;
385 let mut split_size = 0usize;
386 for (i, leaf) in leaves.iter().enumerate() {
387 let leaf_size = leaf.end - leaf.start;
388 if leaf_size > split_size && leaf_size > 1 {
389 split_size = leaf_size;
390 split_pos = Some(i);
391 }
392 }
393 let Some(pos) = split_pos else {
394 break;
395 };
396
397 let leaf = leaves.swap_remove(pos);
398 let axis = principal_axis(&order[leaf.start..leaf.end]);
399 match axis {
400 Some(axis) => {
401 order[leaf.start..leaf.end].sort_by(|&a, &b| {
405 let mut pa = 0.0_f64;
406 let mut pb = 0.0_f64;
407 for j in 0..d {
408 pa += data[[a, j]] * axis[j];
409 pb += data[[b, j]] * axis[j];
410 }
411 let ord = pa.total_cmp(&pb);
412 if ord.is_eq() { a.cmp(&b) } else { ord }
413 });
414 }
415 None => {
416 order[leaf.start..leaf.end].sort_by(|&a, &b| {
420 for j in 0..d {
421 let ord = data[[a, j]].total_cmp(&data[[b, j]]);
422 if !ord.is_eq() {
423 return ord;
424 }
425 }
426 a.cmp(&b)
427 });
428 }
429 }
430 let mid = leaf.start + (split_size / 2);
431
432 if mid == leaf.start || mid == leaf.end {
433 leaves.push(leaf);
434 break;
435 }
436
437 leaves.push(Leaf {
438 start: leaf.start,
439 end: mid,
440 });
441 leaves.push(Leaf {
442 start: mid,
443 end: leaf.end,
444 });
445 }
446
447 if leaves.len() < num_centers {
448 crate::bail_invalid_basis!(
449 "equal-mass partition produced {} leaves, expected {num_centers}",
450 leaves.len()
451 );
452 }
453
454 let mut centers = Array2::<f64>::zeros((num_centers, d));
455 for (c, leaf) in leaves.iter().take(num_centers).enumerate() {
456 let slice = &order[leaf.start..leaf.end];
457 let m = slice.len() as f64;
458 let mut centroid = vec![0.0_f64; d];
459 for &idx in slice {
460 for j in 0..d {
461 centroid[j] += data[[idx, j]];
462 }
463 }
464 for v in &mut centroid {
465 *v /= m.max(1.0);
466 }
467
468 let best_idx = slice
469 .par_iter()
470 .filter_map(|&idx| {
471 let mut d2 = 0.0;
472 for j in 0..d {
473 let delta = data[[idx, j]] - centroid[j];
474 d2 += delta * delta;
475 }
476 if d2.is_finite() {
477 Some((idx, d2))
478 } else {
479 None
480 }
481 })
482 .reduce_with(|a, b| {
483 if b.1 < a.1 || (b.1 == a.1 && b.0 < a.0) {
484 b
485 } else {
486 a
487 }
488 })
489 .map(|(idx, _)| idx)
490 .unwrap_or(slice[0]);
491 centers.row_mut(c).assign(&data.row(best_idx));
492 }
493 Ok(centers)
494}
495
496pub(crate) fn select_equal_mass_covar_representative_centers(
497 data: ArrayView2<'_, f64>,
498 num_centers: usize,
499) -> Result<Array2<f64>, BasisError> {
500 validate_center_count(num_centers)?;
501 let n = data.nrows();
502 let d = data.ncols();
503 if num_centers > n {
504 crate::bail_invalid_basis!(
505 "equal-mass covariate-representative center selection requested {num_centers} centers but data has {n} rows"
506 );
507 }
508 if d == 0 {
509 crate::bail_invalid_basis!(
510 "equal-mass covariate-representative center selection requires at least one column"
511 .to_string(),
512 );
513 }
514
515 let mut split_dim = 0usize;
516 let mut best_span = f64::NEG_INFINITY;
517 for j in 0..d {
518 let mut minv = f64::INFINITY;
519 let mut maxv = f64::NEG_INFINITY;
520 for i in 0..n {
521 let v = data[[i, j]];
522 if v < minv {
523 minv = v;
524 }
525 if v > maxv {
526 maxv = v;
527 }
528 }
529 let span = maxv - minv;
530 if span > best_span {
531 best_span = span;
532 split_dim = j;
533 }
534 }
535
536 let mut sorted: Vec<usize> = (0..n).collect();
537 sorted.sort_by(|&a, &b| {
538 let ord = data[[a, split_dim]].total_cmp(&data[[b, split_dim]]);
539 if ord.is_eq() { a.cmp(&b) } else { ord }
540 });
541
542 let mut centers = Array2::<f64>::zeros((num_centers, d));
543 for c in 0..num_centers {
544 let lo = (c * n) / num_centers;
545 let hi = ((c + 1) * n) / num_centers;
546 let chunk = &sorted[lo..hi.max(lo + 1)];
547 let mid = chunk[chunk.len() / 2];
548 centers.row_mut(c).assign(&data.row(mid));
549 }
550 Ok(centers)
551}
552
553pub(crate) fn select_kmeans_centers(
554 data: ArrayView2<'_, f64>,
555 num_centers: usize,
556 max_iter: usize,
557) -> Result<Array2<f64>, BasisError> {
558 validate_center_count(num_centers)?;
559 let n = data.nrows();
560 let d = data.ncols();
561 if num_centers > n {
562 crate::bail_invalid_basis!("kmeans requested {num_centers} centers but data has {n} rows");
563 }
564 const KMEANS_PILOT_MAX_ROWS: usize = 20_000;
565 if n > KMEANS_PILOT_MAX_ROWS {
566 let pilot_n = KMEANS_PILOT_MAX_ROWS.max(num_centers);
567 log::info!(
572 "kmeans center selection using {}-row pilot subsample instead of full {} rows",
573 pilot_n,
574 n
575 );
576 let pilot = select_equal_mass_covar_representative_centers(data, pilot_n)?;
577 return select_kmeans_centers(pilot.view(), num_centers, max_iter);
578 }
579 let mut centers = select_thin_plate_knots(data, num_centers)?;
580 let mut assign = vec![0usize; n];
581 let iters = max_iter.max(1);
582
583 let use_parallel = n >= 10_000;
586
587 for _ in 0..iters {
588 if use_parallel {
590 const KMEANS_CHUNK: usize = 4096;
591 assign
592 .par_chunks_mut(KMEANS_CHUNK)
593 .enumerate()
594 .for_each(|(ci, chunk)| {
595 let base = ci * KMEANS_CHUNK;
596 for (local, slot) in chunk.iter_mut().enumerate() {
597 let i = base + local;
598 let mut best = 0usize;
599 let mut best_d2 = f64::INFINITY;
600 for k in 0..num_centers {
601 let mut d2 = 0.0;
602 for c in 0..d {
603 let delta = data[[i, c]] - centers[[k, c]];
604 d2 += delta * delta;
605 }
606 if d2 < best_d2 {
607 best_d2 = d2;
608 best = k;
609 }
610 }
611 *slot = best;
612 }
613 });
614 } else {
615 for i in 0..n {
616 let mut best = 0usize;
617 let mut best_d2 = f64::INFINITY;
618 for k in 0..num_centers {
619 let mut d2 = 0.0;
620 for c in 0..d {
621 let delta = data[[i, c]] - centers[[k, c]];
622 d2 += delta * delta;
623 }
624 if d2 < best_d2 {
625 best_d2 = d2;
626 best = k;
627 }
628 }
629 assign[i] = best;
630 }
631 }
632 let mut sums = Array2::<f64>::zeros((num_centers, d));
634 let mut counts = vec![0usize; num_centers];
635 for i in 0..n {
636 let k = assign[i];
637 counts[k] += 1;
638 for c in 0..d {
639 sums[[k, c]] += data[[i, c]];
640 }
641 }
642 for k in 0..num_centers {
643 if counts[k] == 0 {
644 continue;
645 }
646 let inv = 1.0 / counts[k] as f64;
647 for c in 0..d {
648 centers[[k, c]] = sums[[k, c]] * inv;
649 }
650 }
651 }
652 Ok(centers)
653}
654
655pub(crate) fn cartesian_grid_axes(axes: &[Array1<f64>]) -> Result<Array2<f64>, BasisError> {
656 if axes.is_empty() {
657 crate::bail_invalid_basis!("uniform grid requires at least one axis");
658 }
659 let d = axes.len();
660 let total = axes.iter().try_fold(1usize, |acc, axis| {
661 acc.checked_mul(axis.len())
662 .ok_or_else(|| BasisError::DimensionMismatch("uniform grid is too large".to_string()))
663 })?;
664 let mut out = Array2::<f64>::zeros((total, d));
665 for r in 0..total {
666 let mut q = r;
667 for c in (0..d).rev() {
668 let len = axes[c].len();
669 let idx = q % len;
670 q /= len;
671 out[[r, c]] = axes[c][idx];
672 }
673 }
674 Ok(out)
675}
676
677pub(crate) fn select_uniform_grid_centers(
678 data: ArrayView2<'_, f64>,
679 points_per_dim: usize,
680) -> Result<Array2<f64>, BasisError> {
681 if points_per_dim == 0 {
682 crate::bail_invalid_basis!("uniform-grid points_per_dim must be positive");
683 }
684 let d = data.ncols();
685 if d == 0 {
686 crate::bail_invalid_basis!("uniform-grid center selection requires at least one column");
687 }
688 let mut axes = Vec::with_capacity(d);
689 for c in 0..d {
690 let col = data.column(c);
691 let minv = col.iter().fold(f64::INFINITY, |a, &b| a.min(b));
692 let maxv = col.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
693 axes.push(Array::linspace(minv, maxv, points_per_dim));
694 }
695 cartesian_grid_axes(&axes)
696}
697
698#[cfg(test)]
699mod tests {
700 use super::*;
701
702 #[test]
711 fn unique_row_count_is_exactly_the_budget_the_sampler_can_fill_2623() {
712 let rows = 523usize;
716 let mut data = Array2::<f64>::zeros((rows, 2));
717 for row in 0..rows - 1 {
718 data[[row, 0]] = row as f64;
719 data[[row, 1]] = (row * 2) as f64;
720 }
721 data[[rows - 1, 0]] = 0.0;
722 data[[rows - 1, 1]] = 0.0;
723
724 let cols = [0usize, 1usize];
725 let unique = count_unique_coordinate_rows(data.view(), &cols);
726 assert_eq!(
727 unique,
728 rows - 1,
729 "one repeated coordinate row must reduce the distinct count by exactly one"
730 );
731
732 select_r_uniform_subsample_centers(data.view(), unique, 1)
734 .expect("a budget equal to the distinct-row count must always be fillable");
735
736 let err = select_r_uniform_subsample_centers(data.view(), rows, 1)
739 .expect_err("asking for more centers than distinct rows must still refuse");
740 assert!(
741 format!("{err}").contains("522 unique rows"),
742 "the refusal must name the distinct-row count it measured, got: {err}"
743 );
744 }
745
746 #[test]
747 fn r_uniform_subsample_matches_r_4_2_sample_without_replacement() {
748 let data = Array2::from_shape_fn((4800, 1), |(row, _)| (row + 1) as f64);
749 let centers =
750 select_r_uniform_subsample_centers(data.view(), 20, 1).expect("sample centers");
751 assert_eq!(
752 centers.column(0).to_vec(),
753 vec![
754 1017.0, 4775.0, 2177.0, 1533.0, 4567.0, 2347.0, 270.0, 4050.0, 3379.0, 4065.0,
755 597.0, 1301.0, 330.0, 1799.0, 3913.0, 1749.0, 37.0, 1129.0, 729.0, 878.0,
756 ]
757 );
758 }
759
760 #[test]
761 fn r_uniform_subsample_matches_r_when_integer_sampling_needs_multiple_words() {
762 let data = Array2::from_shape_fn((70_000, 1), |(row, _)| (row + 1) as f64);
763 let centers =
764 select_r_uniform_subsample_centers(data.view(), 20, 1).expect("sample centers");
765 assert_eq!(
766 centers.column(0).to_vec(),
767 vec![
768 24_388.0, 59_521.0, 43_307.0, 69_586.0, 11_571.0, 25_173.0, 32_618.0, 13_903.0,
769 8_229.0, 25_305.0, 22_306.0, 12_204.0, 43_809.0, 36_244.0, 45_399.0, 6_519.0,
770 19_242.0, 21_875.0, 58_472.0, 62_956.0,
771 ]
772 );
773 }
774
775 #[test]
776 fn r_uniform_subsample_deduplicates_in_encounter_order() {
777 let data = Array2::from_shape_vec(
778 (5, 2),
779 vec![1.0, 2.0, 3.0, 4.0, 1.0, 2.0, -0.0, 5.0, 0.0, 5.0],
780 )
781 .expect("duplicate-row fixture");
782 let centers =
783 select_r_uniform_subsample_centers(data.view(), 3, 1).expect("unique centers");
784 assert_eq!(centers, data.select(Axis(0), &[0, 1, 3]));
785 }
786
787 #[test]
788 fn one_dimensional_uniform_grid_is_the_interval_minimax_mesh() {
789 let data = Array2::from_shape_vec((5, 1), vec![-3.0, -2.4, -0.1, 1.7, 3.0])
790 .expect("one-dimensional fixture");
791 let centers = select_uniform_grid_centers(data.view(), 4).expect("uniform centers");
792
793 assert_eq!(centers.column(0).to_vec(), vec![-3.0, -1.0, 1.0, 3.0]);
794 let gaps: Vec<f64> = centers
795 .column(0)
796 .windows(2)
797 .into_iter()
798 .map(|window| window[1] - window[0])
799 .collect();
800 assert_eq!(gaps, vec![2.0, 2.0, 2.0]);
801 }
802
803 fn make_points() -> Array2<f64> {
808 let n_side = 11usize;
809 let n = n_side * n_side;
810 let mut pts = Array2::<f64>::zeros((n, 2));
811 let mut state: u64 = 0x9E37_79B9_7F4A_7C15;
812 let mut next = || {
813 state ^= state >> 12;
815 state ^= state << 25;
816 state ^= state >> 27;
817 let v = state.wrapping_mul(0x2545_F491_4F6C_DD1D);
818 ((v >> 11) as f64) / ((1u64 << 53) as f64)
819 };
820 let mut r = 0usize;
821 for i in 0..n_side {
822 for j in 0..n_side {
823 let x = i as f64;
824 let y = 0.35 * i as f64 + 1.7 * j as f64;
827 pts[[r, 0]] = x + 0.05 * (next() - 0.5);
828 pts[[r, 1]] = y + 0.05 * (next() - 0.5);
829 r += 1;
830 }
831 }
832 pts
833 }
834
835 fn assert_center_sets_match(
839 expected: ArrayView2<'_, f64>,
840 actual: ArrayView2<'_, f64>,
841 tol: f64,
842 ) {
843 assert_eq!(expected.nrows(), actual.nrows(), "center counts differ");
844 let k = expected.nrows();
845 let mut used = vec![false; k];
846 let mut worst = 0.0_f64;
847 for ei in 0..k {
848 let mut best = usize::MAX;
849 let mut best_d2 = f64::INFINITY;
850 for ai in 0..k {
851 if used[ai] {
852 continue;
853 }
854 let dx = expected[[ei, 0]] - actual[[ai, 0]];
855 let dy = expected[[ei, 1]] - actual[[ai, 1]];
856 let d2 = dx * dx + dy * dy;
857 if d2 < best_d2 {
858 best_d2 = d2;
859 best = ai;
860 }
861 }
862 assert!(best != usize::MAX, "no unmatched center available");
863 used[best] = true;
864 worst = worst.max(best_d2.sqrt());
865 }
866 assert!(
867 worst <= tol,
868 "rotation-equivariance violated: worst center match residual {worst:.3e} > tol {tol:.3e}"
869 );
870 }
871
872 #[test]
876 fn equal_mass_centers_are_permutation_invariant() {
877 let pts = make_points();
878 let num_centers = 16usize;
879 let base = select_equal_mass_centers(pts.view(), num_centers).unwrap();
880
881 let n = pts.nrows();
882 let mut perm: Vec<usize> = (0..n).collect();
883 let mut state: u64 = 0xD1B5_4A32_D192_ED03;
885 for i in (1..n).rev() {
886 state ^= state >> 12;
887 state ^= state << 25;
888 state ^= state >> 27;
889 let j = (state.wrapping_mul(0x2545_F491_4F6C_DD1D) % (i as u64 + 1)) as usize;
890 perm.swap(i, j);
891 }
892 let mut permuted = Array2::<f64>::zeros((n, 2));
893 for (new_r, &old_r) in perm.iter().enumerate() {
894 permuted[[new_r, 0]] = pts[[old_r, 0]];
895 permuted[[new_r, 1]] = pts[[old_r, 1]];
896 }
897 let permuted_centers = select_equal_mass_centers(permuted.view(), num_centers).unwrap();
898 assert_center_sets_match(base.view(), permuted_centers.view(), 1e-13);
899 }
900
901 #[test]
911 fn equal_mass_centers_are_rotation_equivariant() {
912 let n = 300usize;
915 let mut pts = Array2::<f64>::zeros((n, 2));
916 let mut state: u64 = 0x1234_5678_9ABC_DEF0;
917 let mut next = || {
918 state ^= state >> 12;
919 state ^= state << 25;
920 state ^= state >> 27;
921 let v = state.wrapping_mul(0x2545_F491_4F6C_DD1D);
922 ((v >> 11) as f64) / ((1u64 << 53) as f64)
923 };
924 for r in 0..n {
925 let u = next();
926 let v = next();
927 pts[[r, 0]] = 2.0 * u - 1.0 + 0.6 * (2.0 * v - 1.0);
929 pts[[r, 1]] = 2.0 * v - 1.0;
930 }
931 let num_centers = 48usize;
932 let base = select_equal_mass_centers(pts.view(), num_centers).unwrap();
933
934 let mut cx = 0.0;
936 let mut cy = 0.0;
937 for r in 0..n {
938 cx += pts[[r, 0]];
939 cy += pts[[r, 1]];
940 }
941 cx /= n as f64;
942 cy /= n as f64;
943
944 for &(ca, sa) in &[(0.0_f64, 1.0_f64), (0.7f64.cos(), 0.7f64.sin())] {
945 let mut rot = Array2::<f64>::zeros((n, 2));
946 for r in 0..n {
947 let x = pts[[r, 0]] - cx;
948 let y = pts[[r, 1]] - cy;
949 rot[[r, 0]] = ca * x - sa * y + cx;
950 rot[[r, 1]] = sa * x + ca * y + cy;
951 }
952 let rotated_centers = select_equal_mass_centers(rot.view(), num_centers).unwrap();
953 let mut unrotated = Array2::<f64>::zeros((num_centers, 2));
956 for r in 0..num_centers {
957 let x = rotated_centers[[r, 0]] - cx;
958 let y = rotated_centers[[r, 1]] - cy;
959 unrotated[[r, 0]] = ca * x + sa * y + cx;
960 unrotated[[r, 1]] = -sa * x + ca * y + cy;
961 }
962 assert_center_sets_match(base.view(), unrotated.view(), 1e-9);
963 }
964 }
965}