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