scran_markers
Marker detection for single-cell data
Loading...
Searching...
No Matches
score_markers_best.hpp
Go to the documentation of this file.
1#ifndef SCRAN_MARKERS_SCORE_MARKERS_BEST_HPP
2#define SCRAN_MARKERS_SCORE_MARKERS_BEST_HPP
3
4#include <vector>
5#include <cstddef>
6#include <optional>
7
9#include "tatami/tatami.hpp"
10#include "tatami_stats/tatami_stats.hpp"
11#include "sanisizer/sanisizer.hpp"
12#include "topicks/topicks.hpp"
13#include "quickstats/quickstats.hpp"
14
15#include "scan_matrix.hpp"
16#include "average_group_stats.hpp"
17#include "block_averages.hpp"
18#include "create_combinations.hpp"
19#include "cohens_d.hpp"
20#include "simple_diff.hpp"
21#include "utils.hpp"
22
28namespace scran_markers {
29
39 double threshold = 0;
40
45 int num_threads = 1;
46
50 bool compute_group_mean = true;
51
56
60 bool compute_cohens_d = true;
61
65 bool compute_auc = true;
66
70 bool compute_delta_mean = true;
71
76
81 bool largest_cohens_d = true;
82
87 bool largest_auc = true;
88
93 bool largest_delta_mean = true;
94
100
106 std::optional<double> threshold_cohens_d = 0;
107
113 std::optional<double> threshold_auc = 0.5;
114
120 std::optional<double> threshold_delta_mean = 0;
121
127 std::optional<double> threshold_delta_detected = 0;
128
132 bool keep_ties = false;
133
139 BlockAveragePolicy block_average_policy = BlockAveragePolicy::MEAN;
140
153 scran_blocks::WeightPolicy block_weight_policy = scran_blocks::WeightPolicy::VARIABLE;
154
161
166 double block_quantile = 0.5;
167};
168
174template<typename Stat_, typename Index_>
180 std::vector<std::vector<Stat_> > mean;
181
186 std::vector<std::vector<Stat_> > detected;
187
198 std::vector<std::vector<std::vector<std::pair<Index_, Stat_> > > > cohens_d;
199
210 std::vector<std::vector<std::vector<std::pair<Index_, Stat_> > > > auc;
211
222 std::vector<std::vector<std::vector<std::pair<Index_, Stat_> > > > delta_mean;
223
234 std::vector<std::vector<std::vector<std::pair<Index_, Stat_> > > > delta_detected;
235};
236
240namespace internal {
241
242template<typename Stat_, typename Index_>
243using PairwiseTopQueues = std::vector<std::vector<topicks::TopQueue<Stat_, Index_> > >;
244
245template<typename Stat_, typename Index_>
246void allocate_best_top_queues(
247 PairwiseTopQueues<Stat_, Index_>& pqueues,
248 const std::size_t ngroups,
249 const Index_ top,
250 const bool larger,
251 const bool keep_ties,
252 const std::optional<Stat_>& bound
253) {
255 opt.check_nan = true;
256 opt.keep_ties = keep_ties;
257 if (bound.has_value()) {
258 opt.bound = *bound;
259 }
260
261 sanisizer::resize(pqueues, ngroups);
262 for (auto& x : pqueues) {
263 x.reserve(ngroups);
264 for (I<decltype(ngroups)> g = 0; g < ngroups; ++g) {
265 x.emplace_back(top, larger, opt);
266 }
267 }
268}
269
270template<typename Stat_, typename Index_>
271void add_best_top_queues(
272 PairwiseTopQueues<Stat_, Index_>& pqueues,
273 const Index_ gene,
274 std::size_t ngroups,
275 const std::vector<Stat_>& effects
276) {
277 for (I<decltype(ngroups)> g1 = 0; g1 < ngroups; ++g1) {
278 for (I<decltype(ngroups)> g2 = 0; g2 < ngroups; ++g2) {
279 const auto val = effects[sanisizer::nd_offset<std::size_t>(g2, ngroups, g1)];
280 if (g1 != g2) {
281 pqueues[g1][g2].emplace(val, gene);
282 }
283 }
284 }
285}
286
287template<typename Stat_, typename Index_>
288void report_best_top_queues(
289 std::vector<std::optional<PairwiseTopQueues<Stat_, Index_> > >& pqueues,
290 std::size_t ngroups,
291 std::vector<std::vector<std::vector<std::pair<Index_, Stat_> > > >& output
292) {
293 // We know it fits into an 'int' as this is what we got originally.
294 const int num_available = pqueues.size();
295
296 // If it's empty, we just create empty vectors and move on.
297 if (num_available == 0) {
298 sanisizer::resize(output, ngroups);
299 for (I<decltype(ngroups)> g1 = 0; g1 < ngroups; ++g1) {
300 sanisizer::resize(output[g1], ngroups);
301 }
302 return;
303 }
304
305 // Consolidating all of the thread-specific queues into a single queue.
306 auto& true_pqueue = *(pqueues.front());
307 for (int t = 1; t < num_available; ++t) {
308 auto& current_pqueue = *(pqueues[t]);
309 for (I<decltype(ngroups)> g1 = 0; g1 < ngroups; ++g1) {
310 for (I<decltype(ngroups)> g2 = 0; g2 < ngroups; ++g2) {
311 auto& current_in = current_pqueue[g1][g2];
312 auto& current_out = true_pqueue[g1][g2];
313 while (!current_in.empty()) {
314 current_out.push(current_in.top());
315 current_in.pop();
316 }
317 }
318 }
319 }
320
321 // Now spilling them out into a single vector.
322 sanisizer::resize(output, ngroups);
323 for (I<decltype(ngroups)> g1 = 0; g1 < ngroups; ++g1) {
324 sanisizer::resize(output[g1], ngroups);
325 for (I<decltype(ngroups)> g2 = 0; g2 < ngroups; ++g2) {
326 if (g1 == g2) {
327 continue;
328 }
329 auto& current_in = true_pqueue[g1][g2];
330 auto& current_out = output[g1][g2];
331 while (!current_in.empty()) {
332 const auto& best = current_in.top();
333 current_out.emplace_back(best.second, best.first);
334 current_in.pop();
335 }
336 std::reverse(current_out.begin(), current_out.end()); // earliest element should have the strongest effect sizes.
337 }
338 }
339}
340
341template<typename Index_, typename Stat_>
342void find_best_simple_best_effects(
343 const Index_ ngenes,
344 const std::size_t ngroups,
345 const std::size_t nblocks,
346 const std::size_t ncombos,
347 const std::vector<Stat_>& combo_means,
348 const std::vector<Stat_>& combo_vars,
349 const std::vector<Stat_>& combo_detected,
350 const BlockAverageInfo<Stat_>& average_info,
351 const Index_ top,
352 const ScoreMarkersBestOptions& options,
353 ScoreMarkersBestResults<Stat_, Index_>& output
354) {
355 std::optional<std::vector<Stat_> > total_weights_per_group;
356 const Stat_* total_weights_ptr = NULL;
357 if (average_info.use_mean()) {
358 if (options.compute_group_mean || options.compute_group_detected) {
359 if (nblocks > 1) {
360 total_weights_per_group = compute_total_weight_per_group(ngroups, nblocks, average_info.combo_weights().data());
361 total_weights_ptr = total_weights_per_group->data();
362 } else {
363 total_weights_ptr = average_info.combo_weights().data();
364 }
365 }
366 }
367
368 std::vector<Stat_*> mptrs;
369 if (options.compute_group_mean) {
370 mptrs.reserve(ngroups);
371 sanisizer::resize(output.mean, ngroups);
372 for (auto& x : output.mean) {
373 sanisizer::resize(x, ngenes);
374 mptrs.push_back(x.data());
375 }
376 }
377
378 std::vector<Stat_*> dptrs;
379 if (options.compute_group_detected) {
380 dptrs.reserve(ngroups);
381 sanisizer::resize(output.detected, ngroups);
382 for (auto& x : output.detected) {
383 sanisizer::resize(x, ngenes);
384 dptrs.push_back(x.data());
385 }
386 }
387
388 std::optional<PrecomputedPairwiseWeights<Stat_> > preweights;
389 if (average_info.use_mean()) {
390 if (options.compute_cohens_d || options.compute_delta_mean || options.compute_delta_detected) {
391 preweights.emplace(ngroups, nblocks, average_info.combo_weights().data());
392 }
393 }
394
395 // Setting up the output queues.
396 std::optional<std::vector<std::optional<PairwiseTopQueues<Stat_, Index_> > > > threaded_cohens_d_queues, threaded_delta_detected_queues, threaded_delta_mean_queues;
397 if (options.compute_cohens_d) {
398 threaded_cohens_d_queues.emplace(sanisizer::cast<I<decltype(threaded_cohens_d_queues->size())> >(options.num_threads));
399 }
400 if (options.compute_delta_mean) {
401 threaded_delta_mean_queues.emplace(sanisizer::cast<I<decltype(threaded_delta_mean_queues->size())> >(options.num_threads));
402 }
403 if (options.compute_delta_detected) {
404 threaded_delta_detected_queues.emplace(sanisizer::cast<I<decltype(threaded_delta_detected_queues->size())> >(options.num_threads));
405 }
406
407 const auto ngroups2 = sanisizer::product<typename std::vector<Stat_>::size_type>(ngroups, ngroups);
408
409 int num_used = tatami::parallelize([&](const int t, const Index_ start, const Index_ length) -> void {
410 std::optional<PairwiseTopQueues<Stat_, Index_> > local_cohens_d_queue, local_delta_mean_queue, local_delta_detected_queue;
411 if (options.compute_cohens_d) {
412 local_cohens_d_queue.emplace();
413 allocate_best_top_queues(*local_cohens_d_queue, ngroups, top, options.largest_cohens_d, options.keep_ties, options.threshold_cohens_d);
414 }
415 if (options.compute_delta_mean) {
416 local_delta_mean_queue.emplace();
417 allocate_best_top_queues(*local_delta_mean_queue, ngroups, top, options.largest_delta_mean, options.keep_ties, options.threshold_delta_mean);
418 }
419 if (options.compute_delta_detected) {
420 local_delta_detected_queue.emplace();
421 allocate_best_top_queues(*local_delta_detected_queue, ngroups, top, options.largest_delta_detected, options.keep_ties, options.threshold_delta_detected);
422 }
423
424 std::vector<Stat_> buffer;
425 if (options.compute_cohens_d || options.compute_delta_mean || options.compute_delta_detected) {
426 buffer.resize(ngroups2);
427 }
428
429 std::optional<std::vector<Stat_> > qbuffer, qrevbuffer;
430 std::optional<quickstats::SingleQuantileVariableNumber<Stat_, std::size_t> > qcalc;
431 if (!average_info.use_mean()) {
432 qbuffer.emplace();
433 qrevbuffer.emplace();
434 qcalc.emplace(nblocks, average_info.quantile());
435 }
436
437 for (Index_ gene = start, end = start + length; gene < end; ++gene) {
438 auto in_offset = sanisizer::product_unsafe<std::size_t>(gene, ncombos);
439
440 if (options.compute_group_mean) {
441 const auto tmp_means = combo_means.data() + in_offset;
442 if (average_info.use_mean()) {
443 average_group_stats_blockmean(gene, ngroups, nblocks, tmp_means, average_info.combo_weights().data(), total_weights_ptr, mptrs);
444 } else {
445 average_group_stats_blockquantile(gene, ngroups, nblocks, tmp_means, *qbuffer, *qcalc, mptrs);
446 }
447 }
448
449 if (options.compute_group_detected) {
450 const auto tmp_detected = combo_detected.data() + in_offset;
451 if (average_info.use_mean()) {
452 average_group_stats_blockmean(gene, ngroups, nblocks, tmp_detected, average_info.combo_weights().data(), total_weights_ptr, dptrs);
453 } else {
454 average_group_stats_blockquantile(gene, ngroups, nblocks, tmp_detected, *qbuffer, *qcalc, dptrs);
455 }
456 }
457
458 // Computing the effect sizes.
459 if (options.compute_cohens_d) {
460 const auto tmp_means = combo_means.data() + in_offset;
461 const auto tmp_variances = combo_vars.data() + in_offset;
462 if (average_info.use_mean()) {
463 compute_pairwise_cohens_d_blockmean(tmp_means, tmp_variances, ngroups, nblocks, options.threshold, *preweights, buffer.data());
464 } else {
465 compute_pairwise_cohens_d_blockquantile(tmp_means, tmp_variances, ngroups, nblocks, options.threshold, *qbuffer, *qrevbuffer, *qcalc, buffer.data());
466 }
467 add_best_top_queues(*local_cohens_d_queue, gene, ngroups, buffer);
468 }
469
470 if (options.compute_delta_mean) {
471 const auto tmp_means = combo_means.data() + in_offset;
472 if (average_info.use_mean()) {
473 compute_pairwise_simple_diff_blockmean(tmp_means, ngroups, nblocks, *preweights, buffer.data());
474 } else {
475 compute_pairwise_simple_diff_blockquantile(tmp_means, ngroups, nblocks, *qbuffer, *qcalc, buffer.data());
476 }
477 add_best_top_queues(*local_delta_mean_queue, gene, ngroups, buffer);
478 }
479
480 if (options.compute_delta_detected) {
481 const auto tmp_detected = combo_detected.data() + in_offset;
482 if (average_info.use_mean()) {
483 compute_pairwise_simple_diff_blockmean(tmp_detected, ngroups, nblocks, *preweights, buffer.data());
484 } else {
485 compute_pairwise_simple_diff_blockquantile(tmp_detected, ngroups, nblocks, *qbuffer, *qcalc, buffer.data());
486 }
487 add_best_top_queues(*local_delta_detected_queue, gene, ngroups, buffer);
488 }
489 }
490
491 // Only moving it into the shared buffer at the end to minimize false sharing.
492 if (options.compute_cohens_d) {
493 (*threaded_cohens_d_queues)[t] = std::move(local_cohens_d_queue);
494 }
495 if (options.compute_delta_mean) {
496 (*threaded_delta_mean_queues)[t] = std::move(local_delta_mean_queue);
497 }
498 if (options.compute_delta_detected) {
499 (*threaded_delta_detected_queues)[t] = std::move(local_delta_detected_queue);
500 }
501 }, ngenes, options.num_threads);
502
503 // Now figuring out which of these are the top dogs.
504 if (options.compute_cohens_d) {
505 threaded_cohens_d_queues->resize(num_used);
506 report_best_top_queues(*threaded_cohens_d_queues, ngroups, output.cohens_d);
507 }
508 if (options.compute_delta_mean) {
509 threaded_delta_mean_queues->resize(num_used);
510 report_best_top_queues(*threaded_delta_mean_queues, ngroups, output.delta_mean);
511 }
512 if (options.compute_delta_detected) {
513 threaded_delta_detected_queues->resize(num_used);
514 report_best_top_queues(*threaded_delta_detected_queues, ngroups, output.delta_detected);
515 }
516}
517
518template<
519 bool single_block_,
520 typename Stat_,
521 typename Value_,
522 typename Index_,
523 typename Group_,
524 typename Block_
525>
526ScoreMarkersBestResults<Stat_, Index_> score_markers_best(
527 const tatami::Matrix<Value_, Index_>& matrix,
528 const std::size_t ngroups,
529 const Group_* const group,
530 const std::size_t nblocks,
531 const Block_* const block,
532 const std::size_t ncombos,
533 const std::size_t* const combo,
534 const std::vector<Index_>& combo_sizes,
535 const Index_ top,
536 const ScoreMarkersBestOptions& options
537) {
538 const auto ngenes = matrix.nrow();
539 const auto payload_size = sanisizer::product<typename std::vector<Stat_>::size_type>(ngenes, ncombos);
540 std::vector<Stat_> combo_means, combo_vars, combo_detected;
541 if (options.compute_group_mean || options.compute_cohens_d || options.compute_delta_mean) {
542 combo_means.resize(payload_size);
543 }
544 if (options.compute_cohens_d) {
545 combo_vars.resize(payload_size);
546 }
547 if (options.compute_group_detected || options.compute_delta_detected) {
548 combo_detected.resize(payload_size);
549 }
550
551 // For a single block, this usually doesn't really matter, but we do it for consistency with the multi-block case,
552 // and to account for variable weighting where non-zero block sizes get zero weight.
553 BlockAverageInfo<Stat_> average_info;
554 if (options.block_average_policy == BlockAveragePolicy::MEAN) {
555 average_info = BlockAverageInfo<Stat_>(
557 combo_sizes,
558 options.block_weight_policy,
559 options.variable_block_weight_parameters
560 )
561 );
562 } else {
563 average_info = BlockAverageInfo<Stat_>(options.block_quantile);
564 }
565
566 ScoreMarkersBestResults<Stat_, Index_> output;
567
568 if (options.compute_auc) {
569 auto auc_queues = sanisizer::create<std::vector<std::optional<PairwiseTopQueues<Stat_, Index_> > > >(options.num_threads);
570
571 struct AucResultWorkspace {
572 AucResultWorkspace(const std::size_t ngroups) : pairwise_buffer(sanisizer::product<typename std::vector<Stat_>::size_type>(ngroups, ngroups)) {};
573 std::vector<Stat_> pairwise_buffer;
574 PairwiseTopQueues<Stat_, Index_> queue;
575 };
576
577 const auto num_used = scan_matrix_by_row_custom_auc<single_block_>(
578 matrix,
579 ngroups,
580 group,
581 nblocks,
582 block,
583 ncombos,
584 combo,
585 combo_sizes,
586 average_info,
587 combo_means,
588 combo_vars,
589 combo_detected,
590 /* do_auc = */ true,
591 /* auc_result_initialize = */ [&](const int) -> AucResultWorkspace {
592 AucResultWorkspace res_work(ngroups);
593 allocate_best_top_queues(res_work.queue, ngroups, top, options.largest_auc, options.keep_ties, options.threshold_auc);
594 return res_work;
595 },
596 /* auc_result_process = */ [&](const Index_ gene, AucScanWorkspace<Value_, Group_, Stat_, Index_>& auc_work, AucResultWorkspace& res_work) -> void {
597 process_auc_for_rows(auc_work, ngroups, nblocks, options.threshold, res_work.pairwise_buffer.data());
598 add_best_top_queues(res_work.queue, gene, ngroups, res_work.pairwise_buffer);
599 },
600 /* auc_result_finalize = */ [&](const int t, AucResultWorkspace& res_work) -> void {
601 auc_queues[t] = std::move(res_work.queue);
602 },
603 options.num_threads
604 );
605
606 auc_queues.resize(num_used);
607 report_best_top_queues(auc_queues, ngroups, output.auc);
608
609 } else if (matrix.prefer_rows()) {
610 scan_matrix_by_row_full_auc<single_block_>(
611 matrix,
612 ngroups,
613 group,
614 nblocks,
615 block,
616 ncombos,
617 combo,
618 combo_sizes,
619 average_info,
620 combo_means,
621 combo_vars,
622 combo_detected,
623 static_cast<Stat_*>(NULL),
624 options.threshold,
625 options.num_threads
626 );
627
628 } else {
629 scan_matrix_by_column(
630 matrix,
631 [&]{
632 if constexpr(single_block_) {
633 return ngroups;
634 } else {
635 return ncombos;
636 }
637 }(),
638 [&]{
639 if constexpr(single_block_) {
640 return group;
641 } else {
642 return combo;
643 }
644 }(),
645 combo_sizes,
646 combo_means,
647 combo_vars,
648 combo_detected,
649 options.num_threads
650 );
651 }
652
653 find_best_simple_best_effects(
654 matrix.nrow(),
655 ngroups,
656 nblocks,
657 ncombos,
658 combo_means,
659 combo_vars,
660 combo_detected,
661 average_info,
662 top,
663 options,
664 output
665 );
666
667 return output;
668}
669
670}
697template<typename Stat_, typename Value_, typename Index_, typename Group_>
699 const tatami::Matrix<Value_, Index_>& matrix,
700 const Group_* const group,
701 const Index_ top,
702 const ScoreMarkersBestOptions& options
703) {
704 const Index_ NC = matrix.ncol();
705 const auto group_sizes = tatami_stats::tabulate_groups(group, NC);
706 const auto ngroups = sanisizer::cast<std::size_t>(group_sizes.size());
707
708 return internal::score_markers_best<true, Stat_>(
709 matrix,
710 ngroups,
711 group,
712 1,
713 static_cast<int*>(NULL),
714 ngroups,
715 static_cast<std::size_t*>(NULL),
716 group_sizes,
717 top,
718 options
719 );
720}
721
748template<typename Stat_, typename Value_, typename Index_, typename Group_, typename Block_>
750 const tatami::Matrix<Value_, Index_>& matrix,
751 const Group_* const group,
752 const Block_* const block,
753 const Index_ top,
754 const ScoreMarkersBestOptions& options
755) {
756 const Index_ NC = matrix.ncol();
757 const auto ngroups = tatami_stats::total_groups(group, NC);
758 const auto nblocks = tatami_stats::total_groups(block, NC);
759
760 const auto combinations = internal::create_combinations(ngroups, group, nblocks, block, NC);
761 const auto combo_sizes = internal::tabulate_combinations<Index_>(ngroups, nblocks, combinations);
762 const auto ncombos = combo_sizes.size();
763
764 return internal::score_markers_best<false, Stat_>(
765 matrix,
766 sanisizer::cast<std::size_t>(ngroups),
767 group,
768 sanisizer::cast<std::size_t>(nblocks),
769 block,
770 sanisizer::cast<std::size_t>(ncombos),
771 combinations.data(),
772 combo_sizes,
773 top,
774 options
775 );
776}
777
778}
779
780#endif
Averaging statistics over blocks.
virtual Index_ ncol() const=0
virtual Index_ nrow() const=0
virtual bool prefer_rows() const=0
void compute_weights(const std::size_t num_blocks, const Size_ *const sizes, const WeightPolicy policy, const VariableWeightParameters &variable, Weight_ *const weights)
Marker detection for single-cell data.
Definition score_markers_pairwise.hpp:27
BlockAveragePolicy
Definition block_averages.hpp:27
ScoreMarkersBestResults< Stat_, Index_ > score_markers_best_blocked(const tatami::Matrix< Value_, Index_ > &matrix, const Group_ *const group, const Block_ *const block, const Index_ top, const ScoreMarkersBestOptions &options)
Definition score_markers_best.hpp:749
ScoreMarkersBestResults< Stat_, Index_ > score_markers_best(const tatami::Matrix< Value_, Index_ > &matrix, const Group_ *const group, const Index_ top, const ScoreMarkersBestOptions &options)
Definition score_markers_best.hpp:698
int parallelize(Function_ fun, const Index_ tasks, const int workers)
Options for score_markers_best() and friends.
Definition score_markers_best.hpp:33
bool compute_cohens_d
Definition score_markers_best.hpp:60
std::optional< double > threshold_delta_mean
Definition score_markers_best.hpp:120
double threshold
Definition score_markers_best.hpp:39
bool compute_auc
Definition score_markers_best.hpp:65
bool largest_cohens_d
Definition score_markers_best.hpp:81
BlockAveragePolicy block_average_policy
Definition score_markers_best.hpp:139
bool compute_delta_mean
Definition score_markers_best.hpp:70
bool largest_delta_mean
Definition score_markers_best.hpp:93
bool compute_group_detected
Definition score_markers_best.hpp:55
bool compute_group_mean
Definition score_markers_best.hpp:50
double block_quantile
Definition score_markers_best.hpp:166
std::optional< double > threshold_cohens_d
Definition score_markers_best.hpp:106
std::optional< double > threshold_delta_detected
Definition score_markers_best.hpp:127
bool largest_delta_detected
Definition score_markers_best.hpp:99
bool largest_auc
Definition score_markers_best.hpp:87
scran_blocks::WeightPolicy block_weight_policy
Definition score_markers_best.hpp:153
std::optional< double > threshold_auc
Definition score_markers_best.hpp:113
int num_threads
Definition score_markers_best.hpp:45
scran_blocks::VariableWeightParameters variable_block_weight_parameters
Definition score_markers_best.hpp:160
bool compute_delta_detected
Definition score_markers_best.hpp:75
bool keep_ties
Definition score_markers_best.hpp:132
Results for score_markers_best() and friends.
Definition score_markers_best.hpp:175
std::vector< std::vector< std::vector< std::pair< Index_, Stat_ > > > > auc
Definition score_markers_best.hpp:210
std::vector< std::vector< std::vector< std::pair< Index_, Stat_ > > > > delta_mean
Definition score_markers_best.hpp:222
std::vector< std::vector< std::vector< std::pair< Index_, Stat_ > > > > cohens_d
Definition score_markers_best.hpp:198
std::vector< std::vector< std::vector< std::pair< Index_, Stat_ > > > > delta_detected
Definition score_markers_best.hpp:234
std::vector< std::vector< Stat_ > > mean
Definition score_markers_best.hpp:180
std::vector< std::vector< Stat_ > > detected
Definition score_markers_best.hpp:186
std::optional< Stat_ > bound