singlepp
A C++ library for cell type classification
Loading...
Searching...
No Matches
train_integrated.hpp
Go to the documentation of this file.
1#ifndef SINGLEPP_TRAIN_INTEGRATED_HPP
2#define SINGLEPP_TRAIN_INTEGRATED_HPP
3
4#include "defs.hpp"
5
6#include "tatami/tatami.hpp"
7
8#include "build_reference.hpp"
9#include "Markers.hpp"
10#include "Intersection.hpp"
11#include "utils.hpp"
12
13#include <vector>
14#include <algorithm>
15#include <cstdint>
16#include <memory>
17#include <optional>
18#include <cassert>
19
25namespace singlepp {
26
36template<typename Value_, typename Index_, typename Label_>
41 std::shared_ptr<const tatami::Matrix<Value_, Index_> > ref;
42 const Label_* labels;
44 Index_ test_nrow;
45 std::optional<Intersection<Index_> > intersection;
49};
50
72template<typename Value_, typename Index_, typename Label_>
74 std::shared_ptr<const tatami::Matrix<Value_, Index_> > ref,
75 const Label_* labels,
77) {
79 output.ref = std::move(ref);
80 output.labels = labels;
81 output.markers = std::move(markers);
82 output.test_nrow = output.ref->nrow(); // remember, test and ref are assumed to have the same features.
83 return output;
84}
85
112template<typename Index_, typename Value_, typename Label_>
114 Index_ test_nrow,
115 Intersection<Index_> intersection,
116 std::shared_ptr<const tatami::Matrix<Value_, Index_> > ref,
117 const Label_* labels,
119) {
121 output.ref = std::move(ref);
122 output.labels = labels;
123 output.markers = std::move(markers);
124 output.test_nrow = test_nrow;
125 output.intersection = std::move(intersection);
126 return output;
127}
128
157template<typename Index_, typename Id_, typename Value_, typename Label_>
159 Index_ test_nrow,
160 const Id_* test_id,
161 std::shared_ptr<const tatami::Matrix<Value_, Index_> > ref,
162 const Id_* ref_id,
163 const Label_* labels,
165) {
167 output.ref = std::move(ref);
168 output.labels = labels;
169 output.markers = std::move(markers);
170 output.test_nrow = test_nrow;
171 output.intersection = intersect_genes(test_nrow, test_id, output.ref->nrow(), ref_id);
172 return output;
173}
174
178template<typename Index_>
179struct IntegratedReference {
180 struct DensePerLabel {
181 Index_ num_samples;
182 std::vector<Index_> markers; // indices to 'universe'
183 RankedVector<Index_, Index_> all_ranked; // .second contains indices to 'universe'
184 };
185
186 struct SparsePerLabel {
187 Index_ num_samples;
188 std::vector<Index_> markers; // indices to 'universe'
189 RankedVector<Index_, Index_> negative_ranked, positive_ranked; // .second contains indices to 'universe'
190 std::vector<std::size_t> negative_indptrs, positive_indptrs;
191 };
192
193 std::optional<std::vector<DensePerLabel> > dense;
194 std::optional<std::vector<SparsePerLabel> > sparse;
195};
204template<typename Index_>
206public:
210 TrainedIntegrated(Index_ test_nrow, std::vector<Index_> universe, std::vector<IntegratedReference<Index_> > references) :
211 my_test_nrow(test_nrow),
212 my_universe(std::move(universe)),
213 my_references(std::move(references))
214 {
215 assert(is_sorted_unique(my_universe.size(), my_universe.data()));
216 }
217
218 const auto& references() const {
219 return my_references;
220 }
225private:
226 Index_ my_test_nrow;
227 std::vector<Index_> my_universe;
228 std::vector<IntegratedReference<Index_> > my_references;
229
230public:
234 std::size_t num_references() const {
235 return my_references.size();
236 }
237
241 Index_ test_nrow() const {
242 return my_test_nrow;
243 }
244
250 const std::vector<Index_>& subset() const {
251 return my_universe;
252 }
253
258 std::size_t num_labels(std::size_t r) const {
259 const auto& ref = my_references[r];
260 if (ref.dense.has_value()) {
261 return ref.dense->size();
262 } else {
263 return ref.sparse->size();
264 }
265 }
266
271 std::size_t num_profiles(std::size_t r) const {
272 std::size_t num_prof = 0;
273 const auto& ref = my_references[r];
274 if (ref.dense.has_value()) {
275 for (const auto& lab : *(ref.dense)) {
276 num_prof += sanisizer::sum<std::size_t>(num_prof, lab.num_samples);
277 }
278 } else {
279 for (const auto& lab : *(ref.sparse)) {
280 num_prof += sanisizer::sum<std::size_t>(num_prof, lab.num_samples);
281 }
282 }
283 return num_prof;
284 }
285};
286
297
301template<bool ref_sparse_, typename Value_, typename Index_, typename Label_>
302void train_integrated_per_reference_simple(
304 const std::vector<Index_>& universe,
305 const std::vector<Index_>& remap_test_to_universe,
306 const TrainIntegratedOptions& options,
307 const std::vector<Index_>& positions,
308 std::vector<std::vector<RankedVector<Index_, Index_> > >& out_ranked,
309 typename std::conditional<ref_sparse_, std::vector<std::vector<RankedVector<Index_, Index_> > >&, bool>::type other_ranked
310) {
311 const auto& ref = *(input.ref);
312 const auto NC = ref.ncol();
313 const auto num_universe = universe.size();
314
315 tatami::parallelize([&](int, Index_ start, Index_ len) {
316 auto vbuffer = sanisizer::create<std::vector<Value_> >(num_universe);
317 auto ibuffer = [&](){
318 if constexpr(ref_sparse_) {
319 return sanisizer::create<std::vector<Index_> >(num_universe);
320 } else {
321 return false;
322 }
323 }();
324
325 RankedVector<Value_, Index_> tmp_ranked;
326 tmp_ranked.reserve(num_universe);
327
328 // 'universe' technically refers to the row indices of the test matrix,
329 // but in simple mode, the rows of the test and reference are the same, so we can use it directly here.
330 tatami::VectorPtr<Index_> universe_ptr(tatami::VectorPtr<Index_>{}, &universe);
331 auto ext = tatami::consecutive_extractor<ref_sparse_>(ref, false, start, len, std::move(universe_ptr));
332
333 for (Index_ c = start, end = start + len; c < end; ++c) {
334 tmp_ranked.clear();
335
336 if constexpr(ref_sparse_) {
337 auto info = ext->fetch(vbuffer.data(), ibuffer.data());
338 for (I<decltype(info.number)> i = 0; i < info.number; ++i) {
339 const auto remapped = remap_test_to_universe[info.index[i]];
340 assert(sanisizer::is_less_than(remapped, num_universe));
341 tmp_ranked.emplace_back(info.value[i], remapped);
342 }
343 } else {
344 auto ptr = ext->fetch(vbuffer.data());
345 for (I<decltype(num_universe)> i = 0; i < num_universe; ++i) {
346 tmp_ranked.emplace_back(ptr[i], i); // a.k.a. remap_test_to_universe[universe[i]].
347 }
348 }
349
350 std::sort(tmp_ranked.begin(), tmp_ranked.end());
351
352 if constexpr(ref_sparse_) {
353 const auto tStart = tmp_ranked.begin(), tEnd = tmp_ranked.end();
354 auto zero_ranges = find_zero_ranges<Value_, Index_>(tStart, tEnd);
355 simplify_ranks<Value_, Index_>(tStart, zero_ranges.first, out_ranked[input.labels[c]][positions[c]]);
356 simplify_ranks<Value_, Index_>(zero_ranges.second, tEnd, other_ranked[input.labels[c]][positions[c]]);
357 } else {
358 simplify_ranks(tmp_ranked, out_ranked[input.labels[c]][positions[c]]);
359 }
360 }
361 }, NC, options.num_threads);
362}
363
364template<bool ref_sparse_, typename Value_, typename Index_, typename Label_>
365void train_integrated_per_reference_intersect(
366 const TrainIntegratedInput<Value_, Label_, Index_>& input,
367 const std::vector<Index_>& remap_test_to_universe,
368 const Index_ test_nrow,
369 const TrainIntegratedOptions& options,
370 const std::vector<Index_>& positions,
371 std::vector<std::vector<RankedVector<Index_, Index_> > >& out_ranked,
372 typename std::conditional<ref_sparse_, std::vector<std::vector<RankedVector<Index_, Index_> > >&, bool>::type other_ranked
373) {
374 const auto& ref = *(input.ref);
375 const auto NC = ref.ncol();
376
377 std::vector<Index_> ref_subset;
378 sanisizer::reserve(ref_subset, input.intersection->size());
379 auto remap_ref_subset_to_universe = sanisizer::create<std::vector<Index_> >(ref.nrow(), test_nrow); // all entries of remap_test_to_universe are less than test_nrow.
380 for (const auto& pair : *(input.intersection)) {
381 const auto rdex = remap_test_to_universe[pair.first];
382 if (rdex != test_nrow) {
383 ref_subset.push_back(pair.second);
384 remap_ref_subset_to_universe[pair.second] = rdex;
385 }
386 }
387 std::sort(ref_subset.begin(), ref_subset.end());
388
389 typename std::conditional<ref_sparse_, bool, std::vector<Index_> >::type remap_dense_to_universe;
390 if constexpr(!ref_sparse_) {
391 remap_dense_to_universe.reserve(ref_subset.size());
392 for (auto r : ref_subset) {
393 remap_dense_to_universe.push_back(remap_ref_subset_to_universe[r]);
394 }
395 }
396
397 tatami::parallelize([&](int, Index_ start, Index_ len) {
398 const auto ref_subset_size = ref_subset.size();
399 auto vbuffer = sanisizer::create<std::vector<Value_> >(ref_subset_size);
400 auto ibuffer = [&]() {
401 if constexpr(ref_sparse_) {
402 return sanisizer::create<std::vector<Index_> >(ref_subset_size);
403 } else {
404 return false;
405 }
406 }();
407
408 RankedVector<Value_, Index_> tmp_ranked;
409 tmp_ranked.reserve(ref_subset_size);
410 tatami::VectorPtr<Index_> to_extract_ptr(tatami::VectorPtr<Index_>{}, &ref_subset);
411 auto ext = tatami::consecutive_extractor<ref_sparse_>(ref, false, start, len, std::move(to_extract_ptr));
412
413 for (Index_ c = start, end = start + len; c < end; ++c) {
414 tmp_ranked.clear();
415
416 if constexpr(ref_sparse_) {
417 auto info = ext->fetch(vbuffer.data(), ibuffer.data());
418 for (I<decltype(info.number)> i = 0; i < info.number; ++i) {
419 tmp_ranked.emplace_back(info.value[i], remap_ref_subset_to_universe[info.index[i]]);
420 }
421 } else {
422 auto ptr = ext->fetch(vbuffer.data());
423 for (I<decltype(ref_subset_size)> i = 0; i < ref_subset_size; ++i) {
424 tmp_ranked.emplace_back(ptr[i], remap_dense_to_universe[i]);
425 }
426 }
427
428 std::sort(tmp_ranked.begin(), tmp_ranked.end());
429
430 if constexpr(ref_sparse_) {
431 const auto tStart = tmp_ranked.begin(), tEnd = tmp_ranked.end();
432 auto zero_ranges = find_zero_ranges<Value_, Index_>(tStart, tEnd);
433 simplify_ranks<Value_, Index_>(tStart, zero_ranges.first, out_ranked[input.labels[c]][positions[c]]);
434 simplify_ranks<Value_, Index_>(zero_ranges.second, tEnd, other_ranked[input.labels[c]][positions[c]]);
435 } else {
436 simplify_ranks(tmp_ranked, out_ranked[input.labels[c]][positions[c]]);
437 }
438 }
439 }, NC, options.num_threads);
440}
455template<typename Value_, typename Index_, typename Label_>
457
458 // Checking that the number of genes in the test dataset are consistent.
459 Index_ test_nrow = 0;
460 if (inputs.size()) {
461 test_nrow = inputs.front().test_nrow;
462 for (const auto& in : inputs) {
463 if (!sanisizer::is_equal(in.test_nrow, test_nrow)) {
464 throw std::runtime_error("inconsistent number of rows in the test dataset across entries of 'inputs'");
465 }
466 }
467 }
468
469 // For references with intersections, we need to map the marker indices to the test rows, for comparability across references.
470 const auto nrefs = inputs.size();
471 std::vector<std::vector<Index_> > remap_intersection_to_test_index;
472 for (I<decltype(nrefs)> r = 0; r < nrefs; ++r) {
473 if (inputs[r].intersection.has_value()) {
474 sanisizer::resize(remap_intersection_to_test_index, nrefs);
475 break;
476 }
477 }
478
479 // Identify the union of all marker genes as the universe, but excluding those markers that are not present in all intersections.
480 // Specifically, 'universe' contains sorted and unique row indices for the test matrix, where 'remap_test_to_universe[universe[i]] == i'.
481 // 'remap_test_to_universe[k]' defaults to the max number of rows in the test if 'k' is not present in all intersections.
482 std::vector<Index_> universe;
483 auto remap_test_to_universe = sanisizer::create<std::vector<Index_> >(test_nrow, test_nrow);
484 {
485 auto present = sanisizer::create<std::vector<char> >(test_nrow);
486 auto count_refs = sanisizer::create<std::vector<I<decltype(nrefs)> > >(test_nrow);
487 universe.reserve(test_nrow);
488
489 for (I<decltype(nrefs)> r = 0; r < nrefs; ++r) {
490 const auto& markers = inputs[r].markers;
491 const auto& inter = inputs[r].intersection;
492
493 if (inter.has_value()) {
494 auto& cur_test_remap = remap_intersection_to_test_index[r];
495 sanisizer::resize(cur_test_remap, inputs[r].ref->nrow(), test_nrow); // again, default to the max number of rows if not present in the current intersection.
496 for (const auto& pp : *inter) {
497 cur_test_remap[pp.second] = pp.first;
498 count_refs[pp.first] += 1;
499 }
500
501 for (const auto& labmrk : markers) {
502 for (const auto y : labmrk) {
503 const auto ty = cur_test_remap[y];
504 if (ty != test_nrow && !present[ty]) {
505 present[ty] = true;
506 universe.push_back(ty);
507 }
508 }
509 }
510
511 } else {
512 for (const auto& labmrk : markers) {
513 for (const auto y : labmrk) {
514 if (!present[y]) {
515 present[y] = true;
516 universe.push_back(y);
517 }
518 }
519 }
520
521 for (auto& x : count_refs) {
522 x += 1;
523 }
524 }
525 }
526
527 std::sort(universe.begin(), universe.end());
528 const auto num_universe = universe.size();
529 I<decltype(num_universe)> keep = 0;
530 for (I<decltype(num_universe)> u = 0; u < num_universe; ++u) {
531 const auto marker = universe[u];
532 if (count_refs[marker] == nrefs) {
533 universe[keep] = marker;
534 remap_test_to_universe[marker] = keep;
535 ++keep;
536 }
537 }
538 universe.resize(keep);
539 universe.shrink_to_fit();
540 }
541
542 // Remapping the per-label markers for this reference to refer to our universe.
543 auto references = sanisizer::create<std::vector<IntegratedReference<Index_> > >(nrefs);
544 for (I<decltype(nrefs)> r = 0; r < nrefs; ++r) {
545 const auto& curinput = inputs[r];
546 const auto& currefmarkers = curinput.markers;
547 const auto nlabels = currefmarkers.size();
548 auto& currefout = references[r];
549
550 const bool is_sparse = curinput.ref->is_sparse();
551 if (is_sparse) {
552 currefout.sparse.emplace(sanisizer::as_size_type<I<decltype(*(currefout.sparse))> >(nlabels));
553 } else {
554 currefout.dense.emplace(sanisizer::as_size_type<I<decltype(*(currefout.dense))> >(nlabels));
555 }
556
557 auto get_markers = [&](I<decltype(nlabels)> l) -> std::vector<Index_>& {
558 if (is_sparse) {
559 return (*(currefout.sparse))[l].markers;
560 } else {
561 return (*(currefout.dense))[l].markers;
562 }
563 };
564
565 if (curinput.intersection.has_value()) {
566 auto& cur_test_remap = remap_intersection_to_test_index[r];
567 for (I<decltype(nlabels)> l = 0; l < nlabels; ++l) {
568 const auto& curlabmarkers = currefmarkers[l];
569 auto& markers = get_markers(l);
570 markers.reserve(curlabmarkers.size());
571 for (const auto y : curlabmarkers) {
572 const auto ty = cur_test_remap[y];
573 if (ty != test_nrow) { // ignoring marker that can't be mapped via the current intersection.
574 const auto universe_index = remap_test_to_universe[ty];
575 if (universe_index != test_nrow) { // ignoring genes not present in all intersections.
576 markers.push_back(universe_index);
577 }
578 }
579 }
580 }
581
582 } else {
583 for (I<decltype(nlabels)> l = 0; l < nlabels; ++l) {
584 const auto& curlabmarkers = currefmarkers[l];
585 auto& markers = get_markers(l);
586 markers.reserve(curlabmarkers.size());
587 for (const auto y : curlabmarkers) {
588 const auto universe_index = remap_test_to_universe[y];
589 if (universe_index != test_nrow) { // ignoring genes not present in all intersections.
590 markers.push_back(universe_index);
591 }
592 }
593 }
594 }
595 }
596
597 // Not needed after this so we try to free its allocation for recycling in the next malloc call.
598 // Specifically, I want to give a chance for this memory to be re-used in train_integrated_per_reference_intersect.
599 remap_intersection_to_test_index.clear();
600
601 // Now, we create ranked vectors for each profile in the reference.
602 for (I<decltype(nrefs)> r = 0; r < nrefs; ++r) {
603 const auto& curinput = inputs[r];
604 auto& currefout = references[r];
605
606 // We enforce a non-zero number of samples as this implies that there is at least one label.
607 // Otherwise, if we had no labels, we would have to explicitly ignore this reference during classification.
608 // Specifically, the 'assigned' vector in classify_integrated() wouldn't refer to a valid label and have to be handled specially.
609 const Index_ NC = curinput.ref->ncol();
610 if (NC == 0) {
611 throw std::runtime_error("reference dataset must have at least one column");
612 }
613 std::vector<Index_> positions;
614 sanisizer::reserve(positions, NC);
615
616 const auto nlabels = curinput.markers.size();
617 auto samples_per_label = sanisizer::create<std::vector<Index_> >(nlabels);
618 for (Index_ c = 0; c < NC; ++c) {
619 auto& pos = samples_per_label[curinput.labels[c]];
620 positions.push_back(pos);
621 ++pos;
622 }
623
624 // We check that each label is non-empty to avoid having to deal with labels that emit invalid scores,
625 // in the unusual case that the 'assigned' vector is set to that label.
626 // This is also consistent with the behavior of train_single().
627 for (I<decltype(nlabels)> l = 0; l < nlabels; ++l) {
628 if (samples_per_label[l] == 0) {
629 throw std::runtime_error("no profiles available for label " + std::to_string(l) + " in reference " + std::to_string(r));
630 }
631 }
632
633 if (curinput.ref->is_sparse()) {
634 auto negative_ranked = sanisizer::create<std::vector<std::vector<RankedVector<Index_, Index_> > > >(nlabels);
635 auto positive_ranked = sanisizer::create<std::vector<std::vector<RankedVector<Index_, Index_> > > >(nlabels);
636 for (I<decltype(nlabels)> l = 0; l < nlabels; ++l) {
637 const auto num_samples = samples_per_label[l];
638 sanisizer::resize(negative_ranked[l], num_samples);
639 sanisizer::resize(positive_ranked[l], num_samples);
640 }
641
642 if (curinput.intersection) {
643 train_integrated_per_reference_intersect<true>(curinput, remap_test_to_universe, test_nrow, options, positions, negative_ranked, positive_ranked);
644 } else {
645 train_integrated_per_reference_simple<true, Value_>(curinput, universe, remap_test_to_universe, options, positions, negative_ranked, positive_ranked);
646 }
647
648 for (I<decltype(nlabels)> l = 0; l < nlabels; ++l) {
649 auto& curlabout = (*(currefout.sparse))[l];
650 const auto num_samples = samples_per_label[l];
651 curlabout.num_samples = num_samples;
652
653 I<decltype(curlabout.negative_ranked.size())> num_neg = 0;
654 for (const auto& x : negative_ranked[l]) {
655 num_neg = sanisizer::sum<I<decltype(num_neg)> >(num_neg, x.size());
656 }
657
658 I<decltype(curlabout.positive_ranked.size())> num_pos = 0;
659 for (const auto& x : positive_ranked[l]) {
660 num_pos = sanisizer::sum<I<decltype(num_pos)> >(num_pos, x.size());
661 }
662
663 curlabout.negative_ranked.reserve(num_neg);
664 curlabout.negative_indptrs.reserve(sanisizer::sum<I<decltype(curlabout.negative_indptrs.size())> >(num_samples, 1));
665 curlabout.negative_indptrs.push_back(0);
666 for (const auto& x : negative_ranked[l]) {
667 curlabout.negative_ranked.insert(curlabout.negative_ranked.end(), x.begin(), x.end());
668 curlabout.negative_indptrs.push_back(curlabout.negative_ranked.size());
669 }
670
671 curlabout.positive_ranked.reserve(num_pos);
672 curlabout.positive_indptrs.reserve(sanisizer::sum<I<decltype(curlabout.positive_indptrs.size())> >(num_samples, 1));
673 curlabout.positive_indptrs.push_back(0);
674 for (const auto& x : positive_ranked[l]) {
675 curlabout.positive_ranked.insert(curlabout.positive_ranked.end(), x.begin(), x.end());
676 curlabout.positive_indptrs.push_back(curlabout.positive_ranked.size());
677 }
678 }
679
680 } else {
681 auto out_ranked = sanisizer::create<std::vector<std::vector<RankedVector<Index_, Index_> > > >(nlabels);
682 for (I<decltype(nlabels)> l = 0; l < nlabels; ++l) {
683 const auto num_samples = samples_per_label[l];
684 sanisizer::resize(out_ranked[l], num_samples);
685 }
686
687 if (curinput.intersection) {
688 train_integrated_per_reference_intersect<false>(curinput, remap_test_to_universe, test_nrow, options, positions, out_ranked, true);
689 } else {
690 train_integrated_per_reference_simple<false, Value_>(curinput, universe, remap_test_to_universe, options, positions, out_ranked, true);
691 }
692
693 for (I<decltype(nlabels)> l = 0; l < nlabels; ++l) {
694 auto& curlabout = (*(currefout.dense))[l];
695 curlabout.num_samples = samples_per_label[l];
696 curlabout.all_ranked.reserve(sanisizer::product<I<decltype(curlabout.all_ranked.size())> >(universe.size(), curlabout.num_samples));
697 for (const auto& x : out_ranked[l]) {
698 curlabout.all_ranked.insert(curlabout.all_ranked.end(), x.begin(), x.end());
699 }
700 }
701 }
702 }
703
704 return TrainedIntegrated<Index_>(test_nrow, std::move(universe), std::move(references));
705}
706
707}
708
709#endif
Create an intersection of genes.
Define type aliases for marker lists.
Classifier that integrates multiple reference datasets.
Definition train_integrated.hpp:205
Index_ test_nrow() const
Definition train_integrated.hpp:241
std::size_t num_references() const
Definition train_integrated.hpp:234
const std::vector< Index_ > & subset() const
Definition train_integrated.hpp:250
std::size_t num_profiles(std::size_t r) const
Definition train_integrated.hpp:271
std::size_t num_labels(std::size_t r) const
Definition train_integrated.hpp:258
Common definitions for singlepp.
Cell type classification using the SingleR algorithm in C++.
Definition classify_single.hpp:20
Intersection< Index_ > intersect_genes(Index_ test_nrow, const Id_ *test_id, Index_ ref_nrow, const Id_ *ref_id)
Definition Intersection.hpp:54
std::vector< std::vector< Index_ > > PerLabelMarkers
Definition Markers.hpp:56
TrainIntegratedInput< Value_, Index_, Label_ > prepare_integrated_input(std::shared_ptr< const tatami::Matrix< Value_, Index_ > > ref, const Label_ *labels, PerLabelMarkers< Index_ > markers)
Definition train_integrated.hpp:73
std::vector< std::pair< Index_, Index_ > > Intersection
Definition Intersection.hpp:35
TrainedIntegrated< Index_ > train_integrated(const std::vector< TrainIntegratedInput< Value_, Index_, Label_ > > &inputs, const TrainIntegratedOptions &options)
Definition train_integrated.hpp:456
std::shared_ptr< const std::vector< Index_ > > VectorPtr
int parallelize(Function_ fun, const Index_ tasks, const int workers)
auto consecutive_extractor(const Matrix< Value_, Index_ > &matrix, const bool row, const Index_ iter_start, const Index_ iter_length, Args_ &&... args)
Input to train_integrated().
Definition train_integrated.hpp:37
Options for train_integrated().
Definition train_integrated.hpp:290
int num_threads
Definition train_integrated.hpp:295