1use super::stats::DEFAULT_COARSEST_SORT_DIM;
18use super::*;
19
20pub(super) fn pad_numeric_labels(cell_to_group: &[usize], k: usize) -> Vec<String> {
21 let width = {
22 let mut w = 1usize;
23 let mut n = k.max(1) - 1;
24 while n >= 10 {
25 w += 1;
26 n /= 10;
27 }
28 w
29 };
30 cell_to_group
31 .iter()
32 .map(|g| format!("{:0width$}", g, width = width))
33 .collect()
34}
35
36pub(super) fn fine_to_coarse_from_refined(
43 pbsamp_to_fine: &[usize],
44 pbsamp_to_coarse: &[usize],
45 num_fine: usize,
46) -> Vec<usize> {
47 let mut mapping = vec![usize::MAX; num_fine];
48 for pbsamp in 0..pbsamp_to_fine.len() {
49 let f = pbsamp_to_fine[pbsamp];
50 if mapping[f] == usize::MAX {
51 mapping[f] = pbsamp_to_coarse[pbsamp];
52 } else {
53 debug_assert_eq!(
54 mapping[f], pbsamp_to_coarse[pbsamp],
55 "refinement broke hierarchy at fine group {}",
56 f
57 );
58 }
59 }
60 mapping
61}
62
63pub(super) fn initial_per_level_from_hash(
72 fine_codes: &[usize],
73 pb_sample_to_cells: &[Vec<usize>],
74 level_dims: &[usize],
75 strata_bits: usize,
76) -> Vec<Vec<usize>> {
77 let num_pb = pb_sample_to_cells.len();
78 level_dims
79 .iter()
80 .map(|&d| {
81 let width = d.saturating_add(strata_bits);
82 let mask = if width >= usize::BITS as usize {
83 usize::MAX
84 } else {
85 (1_usize << width).wrapping_sub(1)
86 };
87 let codes: Vec<usize> = (0..num_pb)
88 .map(|pbsamp| fine_codes[pb_sample_to_cells[pbsamp][0]] & mask)
89 .collect();
90 crate::alg::refine_multilevel::compact_labels(&codes).0
91 })
92 .collect()
93}
94
95pub(super) fn build_reproject_offsets(
106 fine_codes: &[usize],
107 pb_sample_to_cells: &[Vec<usize>],
108 level_dims: &[usize],
109 strata_bits: usize,
110) -> Vec<Vec<usize>> {
111 let raw: Vec<usize> = pb_sample_to_cells
112 .iter()
113 .map(|cells| fine_codes[cells[0]])
114 .collect();
115 (0..level_dims.len())
116 .map(|level| {
117 if level + 1 < level_dims.len() {
118 let parent_dim = level_dims[level + 1].saturating_add(strata_bits);
119 let nbits = level_dims[level].saturating_sub(level_dims[level + 1]);
120 let mask = if nbits >= usize::BITS as usize {
121 usize::MAX
122 } else {
123 (1_usize << nbits).wrapping_sub(1)
124 };
125 raw.iter().map(|&c| (c >> parent_dim) & mask).collect()
126 } else {
127 Vec::new()
128 }
129 })
130 .collect()
131}
132
133pub(super) fn refine_or_identity(
139 allow_refine: bool,
140 inputs: &crate::alg::refine_multilevel::RefineInputs<'_>,
141 refine_params: &crate::alg::refine_multilevel::RefineParams,
142 pooled_matches: Option<&[Vec<(usize, f32)>]>,
143) -> anyhow::Result<crate::alg::refine_multilevel::RefinedAssignment> {
144 if allow_refine {
145 crate::alg::refine_multilevel::refine_assignments_with_matches(
146 inputs,
147 refine_params,
148 pooled_matches,
149 )
150 } else {
151 let mut pbsamp_to_group: Vec<Vec<usize>> =
152 Vec::with_capacity(inputs.initial_sc_to_group_per_level.len());
153 let mut num_groups_per_level =
154 Vec::with_capacity(inputs.initial_sc_to_group_per_level.len());
155 for lvl in inputs.initial_sc_to_group_per_level {
156 let (compact, k) = crate::alg::refine_multilevel::compact_labels(lvl);
157 num_groups_per_level.push(k);
158 pbsamp_to_group.push(compact);
159 }
160 Ok(crate::alg::refine_multilevel::RefinedAssignment {
161 pbsamp_to_group,
162 num_groups_per_level,
163 })
164 }
165}
166
167pub(super) struct CrossBatchMatches {
173 pooled: Option<Vec<Vec<(usize, f32)>>>,
174 anchored: Option<Vec<Vec<(usize, f32)>>>,
175}
176
177impl CrossBatchMatches {
178 pub(super) fn for_refinement(&self) -> Option<&[Vec<(usize, f32)>]> {
180 self.pooled.as_deref()
181 }
182
183 pub(super) fn for_matched_stat(&self) -> Option<&[Vec<(usize, f32)>]> {
185 self.anchored.as_deref().or(self.pooled.as_deref())
186 }
187}
188
189pub(super) fn cross_batch_matches(
194 layout: &PbSampleLayout,
195 batch_knn_lookup: &[ColumnDict<usize>],
196 knn: usize,
197 anchor_batches: Option<&[usize]>,
198 refine_params: &crate::alg::refine_multilevel::RefineParams,
199) -> anyhow::Result<CrossBatchMatches> {
200 if batch_knn_lookup.len() < 2 {
201 return Ok(CrossBatchMatches {
202 pooled: None,
203 anchored: None,
204 });
205 }
206 let refines = refine_params.num_gibbs > 0 || refine_params.num_greedy > 0;
207 let pooled = (refines || anchor_batches.is_none())
208 .then(|| per_batch_sc_neighbors(layout, batch_knn_lookup, knn, None))
209 .transpose()?;
210 let anchored = anchor_batches
211 .map(|anchors| per_batch_sc_neighbors(layout, batch_knn_lookup, knn, Some(anchors)))
212 .transpose()?;
213 Ok(CrossBatchMatches { pooled, anchored })
214}
215
216pub(super) fn split_anchored_finest_groups(
242 mut refined: crate::alg::refine_multilevel::RefinedAssignment,
243 layout: &PbSampleLayout,
244) -> crate::alg::refine_multilevel::RefinedAssignment {
245 let mut anchored: Vec<(usize, usize)> = layout
248 .singleton_col
249 .iter()
250 .enumerate()
251 .filter_map(|(p, col)| col.map(|c| (c, p)))
252 .collect();
253 if anchored.is_empty() {
254 return refined;
255 }
256 anchored.sort_unstable_by_key(|&(col, _)| col);
257
258 let finest = &mut refined.pbsamp_to_group[0];
259 let ordinary: Vec<usize> = layout
260 .singleton_col
261 .iter()
262 .zip(finest.iter())
263 .filter_map(|(a, &g)| a.is_none().then_some(g))
264 .collect();
265 let (compact, k_new) = crate::alg::refine_multilevel::compact_labels(&ordinary);
266 let mut compact = compact.into_iter();
267 for (p, a) in layout.singleton_col.iter().enumerate() {
268 if a.is_none() {
269 finest[p] = compact
270 .next()
271 .expect("one compacted label per ordinary pb-sample");
272 }
273 }
274 for (j, &(_, p)) in anchored.iter().enumerate() {
275 finest[p] = k_new + j;
276 }
277 refined.num_groups_per_level[0] = k_new + anchored.len();
278 info!(
279 "Append-only finest partition: {} new-data groups + {} carried + {} bulk singletons",
280 k_new,
281 anchored
282 .iter()
283 .filter(|&&(_, p)| !layout.is_bulk(p))
284 .count(),
285 anchored.iter().filter(|&&(_, p)| layout.is_bulk(p)).count(),
286 );
287 refined
288}
289
290#[derive(Clone, Copy)]
294pub(super) struct RefineCollectCtx<'a> {
295 pub(super) fine_codes: &'a [usize],
296 pub(super) group_to_cols_finest: &'a [Vec<usize>],
297 pub(super) level_dims: &'a [usize],
298 pub(super) num_features: usize,
299 pub(super) num_batches: usize,
300 pub(super) knn: usize,
301 pub(super) opt_iter: usize,
302 pub(super) refine_params: &'a crate::alg::refine_multilevel::RefineParams,
303 pub(super) output_calibration: legume_numeric::param::traits::CalibrateTarget,
306 pub(super) anchor_batches: Option<&'a [usize]>,
308 pub(super) summary_batches: Option<&'a [usize]>,
312 pub(super) bulk_batches: Option<&'a [usize]>,
314 pub(super) observe_panels: bool,
316 pub(super) keep_finest_stats: bool,
318 pub(super) pb_tree: Option<&'a PbTree>,
320 pub(super) cell_to_stratum: Option<&'a [usize]>,
322 pub(super) exclude_unmatched_from_delta: bool,
325 pub(super) strata_bits: usize,
328}
329
330pub(super) fn refine_and_collect_single_layer(
341 data_vec: &mut SparseIoVec,
342 proj_kn: &DMatrix<f32>,
343 ctx: &RefineCollectCtx<'_>,
344) -> anyhow::Result<MultilevelCollapseOut> {
345 let RefineCollectCtx {
346 fine_codes,
347 group_to_cols_finest: _,
348 level_dims,
349 num_features,
350 num_batches,
351 knn,
352 opt_iter,
353 refine_params,
354 output_calibration,
355 anchor_batches: _,
356 summary_batches: _,
357 bulk_batches: _,
358 observe_panels: _,
359 keep_finest_stats: _,
360 pb_tree: _,
361 cell_to_stratum: _,
362 exclude_unmatched_from_delta: _,
363 strata_bits,
364 } = *ctx;
365 info!(
366 "Multi-level refinement path (BBKNN + DC-SBM): {} levels",
367 level_dims.len()
368 );
369
370 let pb_samples = build_pb_samples(
372 data_vec,
373 proj_kn,
374 num_features,
375 ctx.summary_batches.unwrap_or(&[]),
376 ctx.bulk_batches.unwrap_or(&[]),
377 ctx.cell_to_stratum,
378 )?;
379 let num_pb = pb_samples.layout.cell_counts.len();
380 let ncells_dbg = proj_kn.ncols();
381 info!(
382 "Built {} pb-samples from {} cells (ratio {:.2}; knn={})",
383 num_pb,
384 ncells_dbg,
385 num_pb as f32 / ncells_dbg.max(1) as f32,
386 knn
387 );
388 if num_pb as f32 > 0.8 * ncells_dbg as f32 {
389 warn!(
390 "pb-sample count ({}) is close to cell count ({}) — hash partition is too fine \
391 (many 1-cell pb-samples). Consider lowering --sort-dim.",
392 num_pb, ncells_dbg
393 );
394 }
395
396 let ncols = proj_kn.ncols();
398 let pb_sample_to_cells = build_pb_sample_to_cells(&pb_samples.layout);
399
400 let initial_per_level =
401 initial_per_level_from_hash(fine_codes, &pb_sample_to_cells, level_dims, strata_bits);
402 let empty: [ColumnDict<usize>; 0] = [];
403 let batch_knn: &[ColumnDict<usize>] = if num_batches >= 2 {
404 data_vec
405 .batch_knn_lookup()
406 .ok_or_else(|| anyhow::anyhow!("batch_knn_lookup not built"))?
407 .as_slice()
408 } else {
409 &empty
410 };
411 let reproject_offsets =
412 build_reproject_offsets(fine_codes, &pb_sample_to_cells, level_dims, strata_bits);
413 let inputs = crate::alg::refine_multilevel::RefineInputs {
414 layout: &pb_samples.layout,
415 gene_sums: &pb_samples.gene_sums,
416 num_genes: num_features,
417 pb_sample_to_cells: &pb_sample_to_cells,
418 batch_knn_lookup: batch_knn,
419 k_per_batch: knn,
420 initial_sc_to_group_per_level: &initial_per_level,
421 reproject_offsets_per_level: &reproject_offsets,
422 };
423 let matches = cross_batch_matches(
424 &pb_samples.layout,
425 batch_knn,
426 knn,
427 ctx.anchor_batches,
428 refine_params,
429 )?;
430 let refined = split_anchored_finest_groups(
431 refine_or_identity(
432 num_batches >= 2,
433 &inputs,
434 refine_params,
435 matches.for_refinement(),
436 )?,
437 &pb_samples.layout,
438 );
439
440 {
448 let n_leaves = initial_per_level
449 .first()
450 .map(|lvl| lvl.iter().copied().max().map_or(0, |m| m + 1))
451 .unwrap_or(0);
452 let finest = &refined.pbsamp_to_group[0];
453 let k_fin = refined.num_groups_per_level[0];
454 let mut batch_mask = vec![0u128; k_fin];
456 let b2g = &pb_samples.layout.pb_sample_to_batch;
457 for (pb, &g) in finest.iter().enumerate() {
458 let b = b2g[pb];
459 if b < 128 {
460 batch_mask[g] |= 1u128 << b;
461 }
462 }
463 let spans: Vec<u32> = batch_mask.iter().map(|m| m.count_ones()).collect();
464 let multi = spans.iter().filter(|&&c| c > 1).count();
465 let max_b = spans.iter().copied().max().unwrap_or(0);
466 let mean_b = spans.iter().map(|&c| c as f64).sum::<f64>() / k_fin.max(1) as f64;
467 info!(
468 "collapse structure: {} pb-samples, {} batches, {} leaf codes (finest init), \
469 {} refined finest groups; finest groups spanning >1 batch: {}/{} \
470 (max {} batches/group, mean {:.2})",
471 num_pb, num_batches, n_leaves, k_fin, multi, k_fin, max_b, mean_b
472 );
473 }
474
475 let num_levels = level_dims.len();
479 let k_finest = refined.num_groups_per_level[0];
480 let mut cell_to_group_finest = vec![0usize; ncols];
481 for (pbsamp, cells) in pb_sample_to_cells.iter().enumerate() {
482 let g = refined.pbsamp_to_group[0][pbsamp];
483 for &c in cells {
484 cell_to_group_finest[c] = g;
485 }
486 }
487 let finest_str = pad_numeric_labels(&cell_to_group_finest, k_finest);
488 let nthreads = rayon::current_num_threads();
489 info!(
490 "Assigning {} cells to {} finest pb-sample groups ({} rayon threads) ...",
491 ncols, k_finest, nthreads
492 );
493 data_vec.assign_groups(&finest_str, None);
494 debug_assert_eq!(data_vec.num_groups(), k_finest);
495
496 let mut fine_stat = CollapsedStat::new(num_features, k_finest, num_batches);
497 fine_stat.exclude_unmatched_from_delta = ctx.exclude_unmatched_from_delta;
498 info!(
499 "Collecting observed stats over {} groups × {} batches from {} pb-samples ...",
500 k_finest, num_batches, num_pb
501 );
502 collect_observed_stat_from_pb_samples(
503 data_vec,
504 &pb_samples.layout,
505 &pb_samples.gene_sums,
506 &refined.pbsamp_to_group[0],
507 &mut fine_stat,
508 )?;
509 if let Some(matches) = matches.for_matched_stat() {
510 info!(
511 "Collecting cross-batch matched stats (knn={}) over {} pb-samples ...",
512 knn, num_pb
513 );
514 collect_matched_stat_coarse(
515 &pb_samples.layout,
516 &pb_samples.gene_sums,
517 &refined.pbsamp_to_group[0],
518 matches,
519 ctx.anchor_batches,
520 &mut fine_stat,
521 )?;
522 }
523
524 info!(
525 "Level 1/{}: refined k={} (finest; {} cells)",
526 num_levels, k_finest, ncols
527 );
528 if ctx.observe_panels {
529 attach_observability(&mut fine_stat, data_vec)?;
530 }
531
532 let mut results: Vec<CollapsedOut> = Vec::with_capacity(num_levels);
533 let finest_out = optimize(
534 &fine_stat,
535 (1.0, 1.0),
536 opt_iter,
537 &format!("Fit L1/{}", num_levels),
538 output_calibration,
539 ctx.keep_finest_stats,
540 )?;
541 results.push(finest_out);
542
543 let mut prev_stat = fine_stat;
544 for level in 1..num_levels {
545 let k_prev = refined.num_groups_per_level[level - 1];
546 let k_level = refined.num_groups_per_level[level];
547 let fine_to_coarse = fine_to_coarse_from_refined(
548 &refined.pbsamp_to_group[level - 1],
549 &refined.pbsamp_to_group[level],
550 k_prev,
551 );
552 let coarse_stat = merge_stat(&prev_stat, &fine_to_coarse, k_level);
553 info!(
554 "Level {}/{}: refined k={} (merged from {})",
555 level + 1,
556 num_levels,
557 k_level,
558 k_prev
559 );
560 let level_opt_iter = (opt_iter / 2).max(10);
561 let out = optimize(
562 &coarse_stat,
563 (1.0, 1.0),
564 level_opt_iter,
565 &format!("Fit L{}/{}", level + 1, num_levels),
566 output_calibration,
567 false,
568 )?;
569 results.push(out);
570 prev_stat = coarse_stat;
571 }
572 info!(
573 "Fitted pseudobulk posteriors for {} refined levels: k = {:?} (finest first)",
574 num_levels, refined.num_groups_per_level
575 );
576
577 let cell_to_pb_per_level = cell_to_pb_per_level(&refined, &pb_sample_to_cells, ncols);
578
579 let pb_tree = ctx.pb_tree.map(|tree| {
581 let mut tree = tree.clone();
582 let mut leaf_pbs: HashMap<usize, std::collections::BTreeSet<usize>> = HashMap::default();
583 if let Some(finest) = cell_to_pb_per_level.first() {
584 for (c, &code) in fine_codes.iter().enumerate() {
585 leaf_pbs.entry(code).or_default().insert(finest[c]);
586 }
587 }
588 let mut leaves: Vec<(usize, Vec<usize>)> = leaf_pbs
589 .into_iter()
590 .map(|(code, pbs)| (code, pbs.into_iter().collect()))
591 .collect();
592 leaves.sort_by_key(|x| x.0);
593 tree.leaf_to_finest_pb = leaves;
594 tree
595 });
596
597 Ok(MultilevelCollapseOut {
598 levels: results,
599 cell_to_pb_per_level,
600 pb_tree,
601 })
602}
603
604fn cell_to_pb_per_level(
607 refined: &crate::alg::refine_multilevel::RefinedAssignment,
608 pb_sample_to_cells: &[Vec<usize>],
609 ncols: usize,
610) -> Vec<Vec<usize>> {
611 refined
612 .pbsamp_to_group
613 .iter()
614 .map(|groups| {
615 let mut c2g = vec![0usize; ncols];
616 for (pbsamp, cells) in pb_sample_to_cells.iter().enumerate() {
617 for &c in cells {
618 c2g[c] = groups[pbsamp];
619 }
620 }
621 c2g
622 })
623 .collect()
624}
625
626pub struct StackCollapseOut {
630 pub levels: Vec<Vec<CollapsedOut>>,
631 pub cell_to_pb_per_level: Vec<Vec<usize>>,
632}
633
634pub(super) fn refine_and_collect_stack(
640 stack: &mut SparseIoStack,
641 proj_kn: &DMatrix<f32>,
642 ctx: &RefineCollectCtx<'_>,
643) -> anyhow::Result<StackCollapseOut> {
644 let RefineCollectCtx {
645 fine_codes,
646 group_to_cols_finest,
647 level_dims,
648 num_features: _,
649 num_batches,
650 knn,
651 opt_iter,
652 refine_params,
653 output_calibration,
654 anchor_batches: _,
655 summary_batches: _,
656 bulk_batches: _,
657 observe_panels: _,
658 keep_finest_stats,
659 pb_tree: _,
660 cell_to_stratum,
661 exclude_unmatched_from_delta,
662 strata_bits,
663 } = *ctx;
664 let num_layers = stack.num_types();
665 info!(
666 "Multi-level stack refinement (BBKNN + DC-SBM): {} layers × {} levels",
667 num_layers,
668 level_dims.len()
669 );
670
671 let ncols = proj_kn.ncols();
672 let col_to_batch: Vec<usize> = stack.stack[0].get_batch_membership(0..ncols);
673
674 let layout = build_pb_sample_layout(
677 group_to_cols_finest,
678 &col_to_batch,
679 proj_kn,
680 None,
681 &[],
682 &[],
683 cell_to_stratum,
684 )?;
685 let num_pb = layout.cell_counts.len();
686
687 let owner_num_features = stack.stack[0].num_rows();
689 let gene_sums_owner = collect_pb_sample_gene_sums(
690 &stack.stack[0],
691 group_to_cols_finest,
692 &layout.cell_to_pbsamp,
693 num_pb,
694 )?;
695
696 let pb_sample_to_cells = build_pb_sample_to_cells(&layout);
697
698 let initial_per_level =
699 initial_per_level_from_hash(fine_codes, &pb_sample_to_cells, level_dims, strata_bits);
700 let empty: [ColumnDict<usize>; 0] = [];
701 let batch_knn: &[ColumnDict<usize>] = if num_batches >= 2 {
702 stack.stack[0]
703 .batch_knn_lookup()
704 .ok_or_else(|| anyhow::anyhow!("batch_knn_lookup not built"))?
705 .as_slice()
706 } else {
707 &empty
708 };
709 let reproject_offsets =
710 build_reproject_offsets(fine_codes, &pb_sample_to_cells, level_dims, strata_bits);
711 let inputs = crate::alg::refine_multilevel::RefineInputs {
712 layout: &layout,
713 gene_sums: &gene_sums_owner,
714 num_genes: owner_num_features,
715 pb_sample_to_cells: &pb_sample_to_cells,
716 batch_knn_lookup: batch_knn,
717 k_per_batch: knn,
718 initial_sc_to_group_per_level: &initial_per_level,
719 reproject_offsets_per_level: &reproject_offsets,
720 };
721 let matches = cross_batch_matches(&layout, batch_knn, knn, ctx.anchor_batches, refine_params)?;
724 let refined = refine_or_identity(
725 num_batches >= 2,
726 &inputs,
727 refine_params,
728 matches.for_refinement(),
729 )?;
730
731 let mut per_layer_gene_sums: Vec<GeneSums> = Vec::with_capacity(num_layers);
733 for (d, layer) in stack.stack.iter().enumerate() {
734 if d == 0 {
735 per_layer_gene_sums.push(gene_sums_owner.clone());
736 } else {
737 per_layer_gene_sums.push(collect_pb_sample_gene_sums(
738 layer,
739 group_to_cols_finest,
740 &layout.cell_to_pbsamp,
741 num_pb,
742 )?);
743 }
744 }
745
746 let num_levels = level_dims.len();
749 let k_finest = refined.num_groups_per_level[0];
750 let mut cell_to_group_finest = vec![0usize; ncols];
751 for (pbsamp, cells) in pb_sample_to_cells.iter().enumerate() {
752 let g = refined.pbsamp_to_group[0][pbsamp];
753 for &c in cells {
754 cell_to_group_finest[c] = g;
755 }
756 }
757 let finest_str = pad_numeric_labels(&cell_to_group_finest, k_finest);
758 let nthreads = rayon::current_num_threads();
759 info!(
760 "Assigning {} cells to {} finest pb-sample groups across {} layers ({} rayon threads) ...",
761 ncols, k_finest, num_layers, nthreads
762 );
763 for layer in stack.stack.iter_mut() {
764 layer.assign_groups(&finest_str, None);
765 }
766
767 let mut fine_stats: Vec<CollapsedStat> = Vec::with_capacity(num_layers);
768 let mut finest_layer_results = Vec::with_capacity(num_layers);
769 for (d, layer) in stack.stack.iter().enumerate() {
770 let num_features = layer.num_rows();
771 let mut stat = CollapsedStat::new(num_features, k_finest, num_batches);
772 stat.exclude_unmatched_from_delta = exclude_unmatched_from_delta;
773 info!(
774 "Layer {}/{}: collecting observed stats over {} groups × {} batches ...",
775 d + 1,
776 num_layers,
777 k_finest,
778 num_batches
779 );
780 collect_observed_stat_from_pb_samples(
781 layer,
782 &layout,
783 &per_layer_gene_sums[d],
784 &refined.pbsamp_to_group[0],
785 &mut stat,
786 )?;
787 if let Some(matches) = matches.for_matched_stat() {
788 info!(
789 "Layer {}/{}: collecting cross-batch matched stats (knn={}) over {} pb-samples ...",
790 d + 1,
791 num_layers,
792 knn,
793 num_pb
794 );
795 collect_matched_stat_coarse(
796 &layout,
797 &per_layer_gene_sums[d],
798 &refined.pbsamp_to_group[0],
799 matches,
800 ctx.anchor_batches,
801 &mut stat,
802 )?;
803 }
804 let out = optimize(
805 &stat,
806 (1.0, 1.0),
807 opt_iter,
808 &format!("Fit L1/{} layer {}/{}", num_levels, d + 1, num_layers),
809 output_calibration,
810 keep_finest_stats,
811 )?;
812 finest_layer_results.push(out);
813 fine_stats.push(stat);
814 }
815 info!(
816 "Level 1/{}: refined k={} (finest; {} layers × {} cells)",
817 num_levels, k_finest, num_layers, ncols
818 );
819 let mut results: Vec<Vec<CollapsedOut>> = Vec::with_capacity(num_levels);
820 results.push(finest_layer_results);
821
822 let mut prev_stats = fine_stats;
823 for level in 1..num_levels {
824 let k_prev = refined.num_groups_per_level[level - 1];
825 let k_level = refined.num_groups_per_level[level];
826 let fine_to_coarse = fine_to_coarse_from_refined(
827 &refined.pbsamp_to_group[level - 1],
828 &refined.pbsamp_to_group[level],
829 k_prev,
830 );
831 let level_opt_iter = (opt_iter / 2).max(10);
832 let mut layer_results = Vec::with_capacity(num_layers);
833 let mut coarse_stats = Vec::with_capacity(num_layers);
834 for (d, prev_stat) in prev_stats.iter().enumerate() {
835 let coarse_stat = merge_stat(prev_stat, &fine_to_coarse, k_level);
836 let out = optimize(
837 &coarse_stat,
838 (1.0, 1.0),
839 level_opt_iter,
840 &format!(
841 "Fit L{}/{} layer {}/{}",
842 level + 1,
843 num_levels,
844 d + 1,
845 num_layers
846 ),
847 output_calibration,
848 false,
849 )?;
850 layer_results.push(out);
851 coarse_stats.push(coarse_stat);
852 }
853 info!(
854 "Level {}/{}: refined k={} (merged from {}, {} layers)",
855 level + 1,
856 num_levels,
857 k_level,
858 k_prev,
859 num_layers
860 );
861 results.push(layer_results);
862 prev_stats = coarse_stats;
863 }
864
865 Ok(StackCollapseOut {
866 levels: results,
867 cell_to_pb_per_level: cell_to_pb_per_level(&refined, &pb_sample_to_cells, ncols),
868 })
869}
870
871pub(super) fn compute_level_sort_dims(finest_sort_dim: usize, num_levels: usize) -> Vec<usize> {
875 if num_levels <= 1 {
876 return vec![finest_sort_dim];
877 }
878 let coarsest = DEFAULT_COARSEST_SORT_DIM.min(finest_sort_dim);
879 let mut dims = Vec::with_capacity(num_levels);
880 for level in 0..num_levels {
881 let t = level as f32 / (num_levels - 1) as f32;
883 let dim = finest_sort_dim as f32 - t * (finest_sort_dim - coarsest) as f32;
884 let dim = dim.round() as usize;
885 if dims.last() != Some(&dim) {
886 dims.push(dim);
887 }
888 }
889 dims
890}
891
892#[cfg(test)]
893#[path = "refine_tests.rs"]
894mod reproject_tests;