1#![allow(dead_code)]
2
3use crate::alg::batch_mixing::*;
4use crate::sparse_data_visitors::*;
5use crate::sparse_io_stack::SparseIoStack;
6use crate::sparse_io_vector::SparseIoVec;
7use legume_numeric::matrix::dmatrix_util::*;
8use legume_numeric::matrix::rand_util::mix_seed;
9use std::sync::{Arc, Mutex};
10
11use legume_numeric::matrix::traits::*;
12use log::{info, warn};
13use nalgebra::DVector;
14
15struct ProjVisitorIn<'a> {
21 basis_kd: &'a nalgebra::DMatrix<f32>,
22}
23
24pub struct RandColProjOut {
25 pub basis: nalgebra::DMatrix<f32>,
26 pub proj: nalgebra::DMatrix<f32>,
27}
28
29pub struct RandRowProjOut {
30 pub basis: nalgebra::DMatrix<f32>,
31 pub proj: nalgebra::DMatrix<f32>,
32}
33
34pub const DEFAULT_PROJECTION_SEED: u64 = 0x50524F4A_50524F4A; pub trait RandProjOps {
44 fn project_columns(
60 &self,
61 target_dim: usize,
62 block_size: Option<usize>,
63 ) -> anyhow::Result<RandColProjOut> {
64 self.project_columns_with_batch_correction::<usize>(target_dim, block_size, None)
65 }
66
67 fn project_columns_with_batch_correction<T>(
83 &self,
84 target_dim: usize,
85 block_size: Option<usize>,
86 batch_membership: Option<&[T]>,
87 ) -> anyhow::Result<RandColProjOut>
88 where
89 T: Sync + Send + std::hash::Hash + Eq + Clone + ToString,
90 {
91 self.project_columns_with_batch_correction_seeded(
92 target_dim,
93 block_size,
94 batch_membership,
95 DEFAULT_PROJECTION_SEED,
96 )
97 }
98
99 fn project_columns_with_batch_correction_seeded<T>(
102 &self,
103 target_dim: usize,
104 block_size: Option<usize>,
105 batch_membership: Option<&[T]>,
106 seed: u64,
107 ) -> anyhow::Result<RandColProjOut>
108 where
109 T: Sync + Send + std::hash::Hash + Eq + Clone + ToString;
110
111 fn project_columns_weighted<T>(
118 &self,
119 target_dim: usize,
120 block_size: Option<usize>,
121 batch_membership: Option<&[T]>,
122 row_weights: &[f32],
123 ) -> anyhow::Result<RandColProjOut>
124 where
125 T: Sync + Send + std::hash::Hash + Eq + Clone + ToString,
126 {
127 self.project_columns_weighted_seeded(
128 target_dim,
129 block_size,
130 batch_membership,
131 row_weights,
132 DEFAULT_PROJECTION_SEED,
133 )
134 }
135
136 fn project_columns_weighted_seeded<T>(
139 &self,
140 target_dim: usize,
141 block_size: Option<usize>,
142 batch_membership: Option<&[T]>,
143 row_weights: &[f32],
144 seed: u64,
145 ) -> anyhow::Result<RandColProjOut>
146 where
147 T: Sync + Send + std::hash::Hash + Eq + Clone + ToString;
148
149 fn partition_columns_to_groups(
158 &mut self,
159 proj_kn: &nalgebra::DMatrix<f32>,
160 num_features: Option<usize>,
161 ncols_per_group: Option<usize>,
162 ) -> anyhow::Result<usize>;
163
164 fn partition_columns_to_mixed_groups<T>(
170 &mut self,
171 proj_kn: &nalgebra::DMatrix<f32>,
172 num_features: Option<usize>,
173 batch_membership: &[T],
174 min_batches: usize,
175 merge_levels: usize,
176 ) -> anyhow::Result<usize>
177 where
178 T: std::hash::Hash + Eq + Clone,
179 {
180 let part = mixed_partition(
181 proj_kn,
182 num_features,
183 batch_membership,
184 min_batches,
185 merge_levels,
186 )?;
187 self.assign_group_labels(&part.labels);
188 Ok(part.num_groups)
189 }
190
191 fn assign_group_labels(&mut self, labels: &[usize]);
193
194 }
197
198fn project_columns_visitor(
200 job: (usize, usize),
201 data_vec: &SparseIoVec,
202 shared_in: &ProjVisitorIn<'_>,
203 arc_proj_kn: Arc<Mutex<&mut nalgebra::DMatrix<f32>>>,
204) -> anyhow::Result<()> {
205 let (lb, ub) = job;
206 let basis_kd = shared_in.basis_kd;
207 let target_dim = basis_kd.nrows();
208 let n_block = ub - lb;
209
210 let mut xx_dm = data_vec.read_columns_csc(lb..ub)?;
211 for x in xx_dm.values_mut() {
212 *x = x.ln_1p();
213 }
214 xx_dm.normalize_columns_inplace();
215
216 let mut chunk = nalgebra::DMatrix::<f32>::zeros(target_dim, n_block);
220 for (j, col) in xx_dm.col_iter().enumerate() {
221 for (&i, &v) in col.row_indices().iter().zip(col.values()) {
222 chunk.column_mut(j).axpy(v, &basis_kd.column(i), 1.0);
223 }
224 }
225
226 let mut proj_kn = arc_proj_kn.lock().expect("proj_kn lock");
227 proj_kn.columns_range_mut(lb..ub).copy_from(&chunk);
228 Ok(())
229}
230
231impl RandProjOps for SparseIoStack {
232 fn project_columns_with_batch_correction_seeded<T>(
233 &self,
234 target_dim: usize,
235 block_size: Option<usize>,
236 batch_membership: Option<&[T]>,
237 seed: u64,
238 ) -> anyhow::Result<RandColProjOut>
239 where
240 T: Sync + Send + std::hash::Hash + Eq + Clone + ToString,
241 {
242 let batch_membership = membership_of_columns(batch_membership, self.num_columns()?);
243
244 let proj_vec = self
245 .stack
246 .iter()
247 .enumerate()
248 .map(|(m, data_vec)| -> anyhow::Result<_> {
249 data_vec.project_columns_with_batch_correction_seeded(
252 target_dim,
253 block_size,
254 batch_membership,
255 mix_seed(seed, m as u64),
256 )
257 })
258 .collect::<anyhow::Result<Vec<_>>>()?;
259
260 stack_projections(proj_vec)
261 }
262
263 fn project_columns_weighted_seeded<T>(
264 &self,
265 target_dim: usize,
266 block_size: Option<usize>,
267 batch_membership: Option<&[T]>,
268 row_weights: &[f32],
269 seed: u64,
270 ) -> anyhow::Result<RandColProjOut>
271 where
272 T: Sync + Send + std::hash::Hash + Eq + Clone + ToString,
273 {
274 let batch_membership = membership_of_columns(batch_membership, self.num_columns()?);
275
276 let total_rows: usize = self.stack.iter().map(|v| v.num_rows()).sum();
281 if row_weights.len() != total_rows {
282 return Err(anyhow::anyhow!(
283 "row_weights length {} does not match total feature count {} \
284 (sum of num_rows() across the stack)",
285 row_weights.len(),
286 total_rows
287 ));
288 }
289
290 let mut offset = 0usize;
291 let mut proj_vec = Vec::with_capacity(self.stack.len());
292 for (m, data_vec) in self.stack.iter().enumerate() {
293 let nrows = data_vec.num_rows();
294 let slice = &row_weights[offset..offset + nrows];
295 offset += nrows;
296 proj_vec.push(data_vec.project_columns_weighted_seeded(
297 target_dim,
298 block_size,
299 batch_membership,
300 slice,
301 mix_seed(seed, m as u64),
302 )?);
303 }
304
305 stack_projections(proj_vec)
306 }
307
308 fn partition_columns_to_groups(
309 &mut self,
310 proj_kn: &nalgebra::DMatrix<f32>,
311 num_sorting_features: Option<usize>,
312 ncols_per_group: Option<usize>,
313 ) -> anyhow::Result<usize> {
314 let nn = proj_kn.ncols();
315 if nn != self.num_columns()? {
316 return Err(anyhow::anyhow!("number of columns mismatch"));
317 }
318
319 let target_kk = num_sorting_features.unwrap_or(proj_kn.nrows());
320 let kk = proj_kn.nrows().min(target_kk).min(nn);
321
322 let binary_codes = binary_sort_columns(proj_kn, kk)?;
323 let max_group = *binary_codes
324 .iter()
325 .max()
326 .ok_or(anyhow::anyhow!("unable to determine max element"))?;
327
328 for x in self.stack.iter_mut() {
329 x.assign_groups(&binary_codes, ncols_per_group);
330 }
331
332 Ok(max_group + 1)
333 }
334
335 fn assign_group_labels(&mut self, labels: &[usize]) {
336 for x in self.stack.iter_mut() {
337 x.assign_groups(labels, None);
338 }
339 }
340}
341
342impl RandProjOps for SparseIoVec {
343 fn project_columns_with_batch_correction_seeded<T>(
344 &self,
345 target_dim: usize,
346 block_size: Option<usize>,
347 batch_membership: Option<&[T]>,
348 seed: u64,
349 ) -> anyhow::Result<RandColProjOut>
350 where
351 T: Sync + Send + std::hash::Hash + Eq + Clone + ToString,
352 {
353 let nrows = self.num_rows();
354 let ncols = self.num_columns();
355
356 let mut proj_kn = nalgebra::DMatrix::<f32>::zeros(target_dim, ncols);
357
358 let basis_dk = nalgebra::DMatrix::<f32>::rnorm_seeded(nrows, target_dim, seed);
359 let basis_kd = basis_dk.transpose();
362
363 let shared_in = ProjVisitorIn {
364 basis_kd: &basis_kd,
365 };
366 info!(
367 "starting random projection loop (project_columns_with_batch_correction): \
368 D={nrows}, N={ncols}, K={target_dim}, block_size={:?}",
369 block_size
370 );
371 self.visit_columns_by_block(
372 &project_columns_visitor,
373 &shared_in,
374 &mut proj_kn,
375 block_size,
376 )?;
377 info!("finished random projection loop");
378
379 if let Some(col_to_batch) = batch_membership {
380 adjust_batch_biases(&mut proj_kn, col_to_batch)?;
381 }
382
383 let (lb, ub) = (-4., 4.);
384 proj_kn.scale_columns_inplace();
385
386 if proj_kn.max() > ub || proj_kn.min() < lb {
387 proj_kn.iter_mut().for_each(|x| {
389 *x = x.clamp(lb, ub);
390 });
391 proj_kn.scale_columns_inplace();
392 }
393 Ok(RandColProjOut {
394 basis: basis_dk,
395 proj: proj_kn,
396 })
397 }
398
399 fn project_columns_weighted_seeded<T>(
403 &self,
404 target_dim: usize,
405 block_size: Option<usize>,
406 batch_membership: Option<&[T]>,
407 row_weights: &[f32],
408 seed: u64,
409 ) -> anyhow::Result<RandColProjOut>
410 where
411 T: Sync + Send + std::hash::Hash + Eq + Clone + ToString,
412 {
413 let nrows = self.num_rows();
414 let ncols = self.num_columns();
415
416 assert_eq!(row_weights.len(), nrows, "row_weights length mismatch");
417
418 let mut proj_kn = nalgebra::DMatrix::<f32>::zeros(target_dim, ncols);
419
420 let mut basis_dk = nalgebra::DMatrix::<f32>::rnorm_seeded(nrows, target_dim, seed);
421
422 for (r, &w) in row_weights.iter().enumerate() {
424 if w <= 0.0 {
425 basis_dk.row_mut(r).fill(0.0);
426 } else if (w - 1.0).abs() > 1e-6 {
427 basis_dk.row_mut(r).scale_mut(w);
428 }
429 }
430
431 let basis_kd = basis_dk.transpose();
432 let shared_in = ProjVisitorIn {
433 basis_kd: &basis_kd,
434 };
435 info!(
436 "starting random projection loop (project_columns_weighted): \
437 D={nrows}, N={ncols}, K={target_dim}, block_size={:?}",
438 block_size
439 );
440 self.visit_columns_by_block(
441 &project_columns_visitor,
442 &shared_in,
443 &mut proj_kn,
444 block_size,
445 )?;
446 info!("finished random projection loop");
447
448 if let Some(col_to_batch) = batch_membership {
449 adjust_batch_biases(&mut proj_kn, col_to_batch)?;
450 }
451
452 let (lb, ub) = (-4., 4.);
453 proj_kn.scale_columns_inplace();
454 if proj_kn.max() > ub || proj_kn.min() < lb {
455 proj_kn.iter_mut().for_each(|x| {
456 *x = x.clamp(lb, ub);
457 });
458 proj_kn.scale_columns_inplace();
459 }
460 Ok(RandColProjOut {
461 basis: basis_dk,
462 proj: proj_kn,
463 })
464 }
465
466 fn partition_columns_to_groups(
476 &mut self,
477 proj_kn: &nalgebra::DMatrix<f32>,
478 num_sorting_features: Option<usize>,
479 ncols_per_group: Option<usize>,
480 ) -> anyhow::Result<usize> {
481 let nn = proj_kn.ncols();
482 if nn != self.num_columns() {
483 return Err(anyhow::anyhow!("number of columns mismatch"));
484 }
485
486 let target_kk = num_sorting_features.unwrap_or(proj_kn.nrows());
487 let kk = proj_kn.nrows().min(target_kk).min(nn);
488
489 let binary_codes = binary_sort_columns(proj_kn, kk)?;
490 let max_group = *binary_codes
491 .iter()
492 .max()
493 .ok_or(anyhow::anyhow!("unable to determine max element"))?;
494 self.assign_groups(&binary_codes, ncols_per_group);
495 Ok(max_group + 1)
496 }
497
498 fn assign_group_labels(&mut self, labels: &[usize]) {
499 self.assign_groups(labels, None);
500 }
501}
502
503fn membership_of_columns<T>(batch_membership: Option<&[T]>, ncols: usize) -> Option<&[T]> {
508 let col_to_batch = batch_membership?;
509 let size = col_to_batch.len();
510 if size > ncols {
511 warn!(
512 "The batch membership size {size} exceeds the number of columns {ncols}; \
513 using its first {ncols} entries ..."
514 );
515 } else if size < ncols {
516 warn!(
517 "The batch membership size {size} falls short of the number of columns {ncols}; \
518 no batch adjustment ..."
519 );
520 }
521 col_to_batch.get(..ncols)
522}
523
524fn stack_projections(proj_vec: Vec<RandColProjOut>) -> anyhow::Result<RandColProjOut> {
527 let (bases, projs): (Vec<_>, Vec<_>) = proj_vec.into_iter().map(|x| (x.basis, x.proj)).unzip();
528 Ok(RandColProjOut {
529 basis: concatenate_vertical(&bases)?,
530 proj: concatenate_vertical(&projs)?,
531 })
532}
533
534fn adjust_batch_biases<T>(
539 proj_kn: &mut nalgebra::DMatrix<f32>,
540 col_to_batch: &[T],
541) -> anyhow::Result<()>
542where
543 T: std::hash::Hash + Eq + Clone,
544{
545 let ncols = proj_kn.ncols();
546 if col_to_batch.len() != ncols {
547 warn!(
548 "The batch membership size {} mismatches with the number of columns {} ...",
549 col_to_batch.len(),
550 ncols
551 );
552 return Ok(());
553 }
554 let batch = batch_indices(col_to_batch);
555 let n_batches = batch.iter().max().map_or(0, |&m| m + 1);
556 let bits = default_state_bits(ncols, n_batches, proj_kn.nrows());
557 info!(
558 "adjusting batch biases within cell state ({} batches, {} state bits) ...",
559 n_batches, bits
560 );
561 *proj_kn = centre_batches_within_state(proj_kn, &batch, bits)?;
562 Ok(())
563}
564
565pub struct MixedPartition {
568 pub codes: Vec<usize>,
570 pub labels: Vec<usize>,
572 pub num_groups: usize,
573 pub bits: usize,
575 pub batch: Vec<usize>,
577 pub min_batches: usize,
579}
580
581pub fn mixed_partition<T>(
585 proj_kn: &nalgebra::DMatrix<f32>,
586 num_features: Option<usize>,
587 batch_membership: &[T],
588 min_batches: usize,
589 merge_levels: usize,
590) -> anyhow::Result<MixedPartition>
591where
592 T: std::hash::Hash + Eq + Clone,
593{
594 let nn = proj_kn.ncols();
595 anyhow::ensure!(
596 batch_membership.len() == nn,
597 "batch membership size {} mismatches the number of columns {}",
598 batch_membership.len(),
599 nn
600 );
601 let batch = batch_indices(batch_membership);
602 let n_batches = batch.iter().max().map_or(0, |&m| m + 1);
603 let min_batches = min_batches.min(n_batches);
604 let bits = proj_kn
605 .nrows()
606 .min(num_features.unwrap_or(proj_kn.nrows()))
607 .min(nn);
608 let codes = binary_sort_columns(proj_kn, bits)?;
609 let labels = merge_poorly_mixed_bins(&codes, &batch, bits, merge_levels, min_batches);
610 let num_groups = labels
611 .iter()
612 .collect::<std::collections::HashSet<_>>()
613 .len();
614 info!(
615 "partitioned columns into {} groups ({} bits, bins with fewer than {} batches merged up to {} levels)",
616 num_groups, bits, min_batches, merge_levels
617 );
618 Ok(MixedPartition {
619 codes,
620 labels,
621 num_groups,
622 bits,
623 batch,
624 min_batches,
625 })
626}
627
628pub fn binary_sort_columns(
635 proj_kn: &nalgebra::DMatrix<f32>,
636 kk: usize,
637) -> anyhow::Result<Vec<usize>> {
638 anyhow::ensure!(
639 kk < usize::BITS as usize,
640 "{kk} sorting features do not fit a {}-bit binary code: use fewer than {}",
641 usize::BITS,
642 usize::BITS
643 );
644 let nn = proj_kn.ncols();
653 let (_, _, mut q_nk) = proj_kn.rsvd(kk)?;
654 q_nk.scale_columns_inplace();
655
656 let mut binary_codes = DVector::<usize>::zeros(nn);
657 for k in 0..kk {
658 let binary_shift = |x: f32| -> usize {
659 if x > 0.0 {
660 1 << k
661 } else {
662 0
663 }
664 };
665 binary_codes += q_nk.column(k).map(binary_shift);
666 }
667
668 Ok(binary_codes.data.as_vec().clone())
669}
670
671pub fn max_binary_code(kk: usize) -> usize {
673 (1 << kk) - 1
674}
675
676#[cfg(test)]
677mod repro_tests {
678 use super::*;
679 use crate::sparse_io::{create_sparse_from_triplets, SparseIoBackend};
680 use std::sync::Arc;
681
682 fn tiny_data_vec(dir: &tempfile::TempDir) -> anyhow::Result<SparseIoVec> {
684 let path = dir.path().join("proj.zarr");
685 let path_str = path.to_str().unwrap();
686 let (d, n) = (8usize, 12usize);
687 let mut triplets: Vec<(u64, u64, f32)> = Vec::new();
689 for i in 0..d {
690 for j in 0..n {
691 if (i * 7 + j * 3) % 5 < 3 {
692 triplets.push((i as u64, j as u64, 1.0 + ((i + j) % 4) as f32));
693 }
694 }
695 }
696 let nnz = triplets.len();
697 let mut backend = create_sparse_from_triplets(
698 &triplets,
699 (d, n, nnz),
700 Some(path_str),
701 Some(&SparseIoBackend::Zarr),
702 )?;
703 let gene_names: Vec<Box<str>> = (0..d).map(|i| format!("g{i}").into()).collect();
704 let cell_names: Vec<Box<str>> = (0..n).map(|j| format!("c{j}").into()).collect();
705 backend.register_row_names_vec(&gene_names);
706 backend.register_column_names_vec(&cell_names);
707 let mut data_vec = SparseIoVec::new();
708 data_vec.push(Arc::from(backend), None)?;
709 Ok(data_vec)
710 }
711
712 #[test]
715 fn seeded_projection_is_reproducible() -> anyhow::Result<()> {
716 let dir = tempfile::tempdir()?;
717 let data_vec = tiny_data_vec(&dir)?;
718 let a =
719 data_vec.project_columns_with_batch_correction_seeded::<usize>(4, None, None, 123)?;
720 let b =
721 data_vec.project_columns_with_batch_correction_seeded::<usize>(4, None, None, 123)?;
722 assert_eq!(a.basis, b.basis, "same seed → identical basis");
723 assert_eq!(a.proj, b.proj, "same seed → identical projection");
724 Ok(())
725 }
726
727 #[test]
730 fn default_projection_is_reproducible() -> anyhow::Result<()> {
731 let dir = tempfile::tempdir()?;
732 let data_vec = tiny_data_vec(&dir)?;
733 let a = data_vec.project_columns(4, None)?;
734 let b = data_vec.project_columns(4, None)?;
735 assert_eq!(a.basis, b.basis);
736 assert_eq!(a.proj, b.proj);
737 Ok(())
738 }
739
740 #[test]
743 fn distinct_seeds_diverge() -> anyhow::Result<()> {
744 let dir = tempfile::tempdir()?;
745 let data_vec = tiny_data_vec(&dir)?;
746 let a = data_vec.project_columns_with_batch_correction_seeded::<usize>(4, None, None, 1)?;
747 let b = data_vec.project_columns_with_batch_correction_seeded::<usize>(4, None, None, 2)?;
748 assert_ne!(a.basis, b.basis, "distinct seeds → distinct basis");
749 Ok(())
750 }
751
752 fn tiny_stack(
754 dir_a: &tempfile::TempDir,
755 dir_b: &tempfile::TempDir,
756 ) -> anyhow::Result<SparseIoStack> {
757 let mut stack = SparseIoStack::new();
758 stack.push(tiny_data_vec(dir_a)?)?;
759 stack.push(tiny_data_vec(dir_b)?)?;
760 Ok(stack)
761 }
762
763 #[test]
766 fn stacked_projection_concatenates_the_modalities() -> anyhow::Result<()> {
767 let (dir_a, dir_b) = (tempfile::tempdir()?, tempfile::tempdir()?);
768 let stack = tiny_stack(&dir_a, &dir_b)?;
769 let batch: Vec<usize> = (0..12).map(|j| j % 2).collect();
770 let weights: Vec<f32> = (0..16).map(|r| (r % 3) as f32).collect();
771
772 let plain =
773 stack.project_columns_with_batch_correction_seeded(4, None, Some(&batch[..]), 9)?;
774 let weighted =
775 stack.project_columns_weighted_seeded(4, None, Some(&batch[..]), &weights, 9)?;
776
777 for (out, weighted_rows) in [(&plain, false), (&weighted, true)] {
778 assert_eq!(out.basis.shape(), (16, 4));
779 assert_eq!(out.proj.shape(), (8, 12));
780 for (m, data_vec) in stack.stack.iter().enumerate() {
781 let sub_seed = mix_seed(9, m as u64);
782 let one = if weighted_rows {
783 data_vec.project_columns_weighted_seeded(
784 4,
785 None,
786 Some(&batch[..]),
787 &weights[m * 8..(m + 1) * 8],
788 sub_seed,
789 )?
790 } else {
791 data_vec.project_columns_with_batch_correction_seeded(
792 4,
793 None,
794 Some(&batch[..]),
795 sub_seed,
796 )?
797 };
798 assert_eq!(out.basis.rows(m * 8, 8), one.basis);
799 assert_eq!(out.proj.rows(m * 4, 4), one.proj);
800 }
801 }
802 Ok(())
803 }
804
805 fn stack_projections_with(
807 stack: &SparseIoStack,
808 batch: Option<&[usize]>,
809 ) -> anyhow::Result<[nalgebra::DMatrix<f32>; 2]> {
810 let weights = vec![1.0f32; 16];
811 Ok([
812 stack
813 .project_columns_with_batch_correction_seeded(4, None, batch, 9)?
814 .proj,
815 stack
816 .project_columns_weighted_seeded(4, None, batch, &weights, 9)?
817 .proj,
818 ])
819 }
820
821 #[test]
824 fn stack_ignores_a_batch_membership_that_is_too_short() -> anyhow::Result<()> {
825 let (dir_a, dir_b) = (tempfile::tempdir()?, tempfile::tempdir()?);
826 let stack = tiny_stack(&dir_a, &dir_b)?;
827 let none = stack_projections_with(&stack, None)?;
828
829 let fitting: Vec<usize> = (0..12).map(|j| j % 2).collect();
830 let corrected = stack_projections_with(&stack, Some(&fitting))?;
831 assert_ne!(corrected, none, "the adjustment changes the fixture");
832
833 assert_eq!(stack_projections_with(&stack, Some(&fitting[..11]))?, none);
834 Ok(())
835 }
836
837 #[test]
840 fn stack_truncates_a_batch_membership_that_is_too_long() -> anyhow::Result<()> {
841 let (dir_a, dir_b) = (tempfile::tempdir()?, tempfile::tempdir()?);
842 let stack = tiny_stack(&dir_a, &dir_b)?;
843 let none = stack_projections_with(&stack, None)?;
844
845 let fitting: Vec<usize> = (0..12).map(|j| j % 2).collect();
846 let corrected = stack_projections_with(&stack, Some(&fitting))?;
847 assert_ne!(corrected, none, "the adjustment changes the fixture");
848
849 let mut too_long = fitting.clone();
851 too_long.push(7);
852 assert_eq!(stack_projections_with(&stack, Some(&too_long))?, corrected);
853 Ok(())
854 }
855
856 #[test]
859 fn more_sorting_features_than_code_bits_are_refused() {
860 let bits = usize::BITS as usize;
861 let proj = nalgebra::DMatrix::<f32>::from_fn(bits, bits, |i, j| {
862 ((i * 31 + j * 17) % 7) as f32 - 3.0
863 });
864 assert!(binary_sort_columns(&proj, bits).is_err());
865 assert!(binary_sort_columns(&proj, 6).is_ok());
866 }
867}