scran_pca
Principal component analysis for single-cell data
Loading...
Searching...
No Matches
blocked_pca.hpp
Go to the documentation of this file.
1#ifndef SCRAN_PCA_BLOCKED_PCA_HPP
2#define SCRAN_PCA_BLOCKED_PCA_HPP
3
4#include <vector>
5#include <cmath>
6#include <algorithm>
7#include <type_traits>
8#include <cstddef>
9#include <functional>
10#include <optional>
11#include <cassert>
12
13#include "tatami/tatami.hpp"
14#include "irlba/irlba.hpp"
15#include "irlba/parallel.hpp"
16#include "irlba_tatami/irlba_tatami.hpp"
17#include "Eigen/Dense"
19#include "sanisizer/sanisizer.hpp"
20
21#include "utils.hpp"
22
28namespace scran_pca {
29
35template<typename EigenVector_ = Eigen::VectorXd>
41 // Avoid throwing an error if too many PCs are requested.
42 irlba_options.cap_number = true;
43 }
53 int number = 25;
54
61 bool scale = false;
62
67 bool transpose = true;
68
78 scran_blocks::WeightPolicy block_weight_policy = scran_blocks::WeightPolicy::VARIABLE;
79
85
93
98 bool realize_matrix = true;
99
106 int num_threads = 1;
107
112};
113
117/*****************************************************
118 ************* Blocking data structures **************
119 *****************************************************/
120
121template<class EigenVector_>
122struct BlockingDetails {
123 template<typename Index_>
124 BlockingDetails(std::size_t num_blocks, Index_ num_cells) :
125 per_element_weight(sanisizer::cast<I<decltype(per_element_weight.size())> >(num_blocks)),
126 expanded_weights(tatami::cast_Index_to_container_size<EigenVector_>(num_cells))
127 {}
128
129 typedef typename EigenVector_::Scalar Weight;
130 std::vector<Weight> per_element_weight;
131 Weight total_block_weight = 0;
132 EigenVector_ expanded_weights;
133};
134
135template<class EigenVector_, typename Index_, typename Block_>
136std::optional<BlockingDetails<EigenVector_> > compute_blocking_details(
137 const Index_ ncells,
138 const Block_* block,
139 const std::size_t num_blocks,
140 const std::vector<Index_>& block_sizes,
141 const scran_blocks::WeightPolicy block_weight_policy,
142 const scran_blocks::VariableWeightParameters& variable_block_weight_parameters
143) {
144 if (block_weight_policy == scran_blocks::WeightPolicy::NONE) {
145 return std::optional<BlockingDetails<EigenVector_> >();
146 }
147
148 BlockingDetails<EigenVector_> output(num_blocks, ncells);
149 auto& total_weight = output.total_block_weight;
150 auto& element_weight = output.per_element_weight;
151
152 for (std::size_t b = 0; b < num_blocks; ++b) {
153 const auto bsize = block_sizes[b];
154
155 // Computing effective block weights that also incorporate division by the
156 // block size. This avoids having to do the division by block size in the
157 // 'compute_blockwise_mean_and_variance*()' functions.
158 if (bsize) {
159 typename EigenVector_::Scalar block_weight = 1;
160 if (block_weight_policy == scran_blocks::WeightPolicy::VARIABLE) {
161 block_weight = scran_blocks::compute_variable_weight(bsize, variable_block_weight_parameters);
162 }
163
164 element_weight[b] = block_weight / bsize;
165 total_weight += block_weight;
166 } else {
167 element_weight[b] = 0;
168 }
169 }
170
171 // Setting a placeholder value to avoid problems with division by zero.
172 if (total_weight == 0) {
173 total_weight = 1;
174 }
175
176 // Expanding them for multiplication in the IRLBA wrappers.
177 auto sqrt_weights = element_weight;
178 for (auto& s : sqrt_weights) {
179 s = std::sqrt(s);
180 }
181
182 auto& expanded = output.expanded_weights;
183 for (Index_ c = 0; c < ncells; ++c) {
184 expanded.coeffRef(c) = sqrt_weights[block[c]];
185 }
186
187 return output;
188}
189
190/*****************************************************************
191 ************ Computing the blockwise mean and variance **********
192 *****************************************************************/
193
194template<class IrlbaSparseMatrix_, typename Block_, class Index_, class EigenVector_, class EigenMatrix_>
195void compute_blockwise_mean_and_variance_realized_sparse(
196 const IrlbaSparseMatrix_& emat, // this should be column-major with genes in the columns.
197 const Block_* block,
198 const std::size_t num_blocks,
199 const std::vector<Index_>& block_sizes,
200 const std::optional<BlockingDetails<EigenVector_> >& block_details,
201 EigenMatrix_& centers,
202 EigenVector_& variances,
203 const int nthreads
204) {
205 const auto ngenes = emat.cols();
206 const auto ncells = emat.rows();
207 const auto& values = emat.get_values();
208 const auto& indices = emat.get_indices();
209 const auto& pointers = emat.get_pointers();
210 static_assert(!EigenMatrix_::IsRowMajor);
211
212 assert(sanisizer::is_equal(ngenes, variances.size()));
213 assert(sanisizer::is_equal(ngenes, centers.cols()));
214 assert(sanisizer::is_equal(num_blocks, centers.rows()));
215
216 tatami::parallelize([&](const int, const Index_ start, const Index_ length) -> void {
217 auto block_zeros = sanisizer::create<std::vector<Index_> >(num_blocks);
218 auto block_rss = sanisizer::create<std::vector<typename EigenVector_::Scalar> >(num_blocks);
219 auto block_centers = sanisizer::create<std::vector<typename EigenMatrix_::Scalar> >(num_blocks); // use a local copy to avoid false sharing.
220
221 for (I<decltype(start)> g = start, end = start + length; g < end; ++g) {
222 const auto offset = pointers[g];
223 const auto num_nonzero = pointers[g + 1] - offset; // increment won't overflow as 'g < end' and 'end' is of the same type.
224
225 const auto vptr = values.data() + offset;
226 const auto iptr = indices.data() + offset;
227
228 std::fill(block_centers.begin(), block_centers.end(), 0);
229 for (I<decltype(num_nonzero)> i = 0; i < num_nonzero; ++i) {
230 block_centers[block[iptr[i]]] += vptr[i];
231 }
232 for (std::size_t b = 0; b < num_blocks; ++b) {
233 const auto bsize = block_sizes[b];
234 if (bsize) {
235 block_centers[b] /= bsize;
236 }
237 }
238
239 // Computing the RSS instead of the sample variance.
240 // We don't consider the loss of residual d.f. from estimating the block means, as the PCA doesn't either.
241 std::copy(block_sizes.begin(), block_sizes.end(), block_zeros.begin());
242 std::fill(block_rss.begin(), block_rss.end(), 0);
243
244 for (I<decltype(num_nonzero)> i = 0; i < num_nonzero; ++i) {
245 const Block_ curb = block[iptr[i]];
246 const auto diff = vptr[i] - block_centers[curb];
247 block_rss[curb] += diff * diff;
248 --block_zeros[curb];
249 }
250
251 typename EigenVector_::Scalar rss = 0;
252 for (std::size_t b = 0; b < num_blocks; ++b) {
253 const auto bsize = block_sizes[b];
254 if (bsize) {
255 const auto val = block_centers[b];
256 const auto final_rss = block_rss[b] + val * val * block_zeros[b];
257 if (block_details.has_value()) {
258 rss += final_rss * block_details->per_element_weight[b];
259 } else {
260 rss += final_rss;
261 }
262 }
263 }
264
265 // COMMENT ON DENOMINATOR:
266 // If we're not dealing with weights, we compute the actual sample variance for easy interpretation
267 // (and to match up with the per-PC calculations in clean_up).
268 //
269 // If we're dealing with weights, the concept of the sample variance becomes somewhat weird.
270 // So, we just use the same denominator for consistency in clean_up_projected.
271 // Magnitude doesn't matter when scaling for process_scale_vector anyway.
272 //
273 // If there are not enough cells, we set the variance to zero so that no scaling is done in process_scale_vector().
274 // We don't set this to NaN to avoid problems with propagation.
275 if (ncells > 1) {
276 variances[g] = rss / (ncells - 1);
277 } else {
278 variances[g] = 0;
279 }
280
281 std::copy(block_centers.begin(), block_centers.end(), centers.data() + sanisizer::product_unsafe<std::size_t>(g, num_blocks));
282 }
283 }, ngenes, nthreads);
284}
285
286template<class EigenMatrix_, typename Block_, class Index_, class EigenVector_>
287void compute_blockwise_mean_and_variance_realized_dense(
288 const EigenMatrix_& emat, // this should be column-major with genes in the columns.
289 const Block_* block,
290 const std::size_t num_blocks,
291 const std::vector<Index_>& block_sizes,
292 const std::optional<BlockingDetails<EigenVector_> >& block_details,
293 EigenMatrix_& centers,
294 EigenVector_& variances,
295 const int nthreads
296) {
297 const auto ngenes = emat.cols();
298 const auto ncells = emat.rows();
299 static_assert(!EigenMatrix_::IsRowMajor);
300
301 assert(sanisizer::is_equal(ngenes, variances.size()));
302 assert(sanisizer::is_equal(ngenes, centers.cols()));
303 assert(sanisizer::is_equal(num_blocks, centers.rows()));
304
305 tatami::parallelize([&](const int, const Index_ start, const Index_ length) -> void {
306 auto block_rss = sanisizer::create<std::vector<typename EigenVector_::Scalar> >(num_blocks);
307 auto block_centers = sanisizer::create<std::vector<typename EigenMatrix_::Scalar> >(num_blocks); // use a local copy to avoid false sharing.
308
309 for (Index_ g = start, end = start + length; g < end; ++g) {
310 const auto values = emat.data() + sanisizer::product_unsafe<std::size_t>(g, ncells);
311
312 std::fill(block_centers.begin(), block_centers.end(), 0);
313 for (I<decltype(ncells)> i = 0; i < ncells; ++i) {
314 block_centers[block[i]] += values[i];
315 }
316 for (std::size_t b = 0; b < num_blocks; ++b) {
317 const auto bsize = block_sizes[b];
318 if (bsize) {
319 block_centers[b] /= bsize;
320 }
321 }
322
323 // See comments above on why we're computing RSS's.
324 std::fill(block_rss.begin(), block_rss.end(), 0);
325 for (I<decltype(ncells)> i = 0; i < ncells; ++i) {
326 const auto curb = block[i];
327 const auto delta = values[i] - block_centers[curb];
328 block_rss[curb] += delta * delta;
329 }
330
331 typename EigenVector_::Scalar rss = 0;
332 for (std::size_t b = 0; b < num_blocks; ++b) {
333 if (block_sizes[b]) {
334 if (block_details.has_value()) {
335 rss += block_rss[b] * block_details->per_element_weight[b];
336 } else {
337 rss += block_rss[b];
338 }
339 }
340 }
341
342 // See COMMENT ON DENOMINATOR above.
343 if (ncells > 1) {
344 variances[g] = rss / (ncells - 1);
345 } else {
346 variances[g] = 0;
347 }
348
349 std::copy(block_centers.begin(), block_centers.end(), centers.data() + sanisizer::product_unsafe<std::size_t>(g, num_blocks));
350 }
351 }, ngenes, nthreads);
352}
353
354template<typename Value_, typename Index_, typename Block_, class EigenMatrix_, class EigenVector_>
355void compute_blockwise_mean_and_variance_tatami(
356 const tatami::Matrix<Value_, Index_>& mat, // this should have genes in the rows!
357 const Block_* block,
358 const std::size_t num_blocks,
359 const std::vector<Index_>& block_sizes,
360 const std::optional<BlockingDetails<EigenVector_> >& block_details,
361 EigenMatrix_& centers,
362 EigenVector_& variances,
363 const int nthreads
364) {
365 static_assert(!EigenMatrix_::IsRowMajor); // need this for correct pointer calculations.
366 typedef typename EigenMatrix_::Scalar Float;
367
368 const auto ngenes = mat.nrow();
369 EigenMatrix_ tmp_mean(
370 sanisizer::cast<I<decltype(std::declval<EigenMatrix_>().rows())> >(ngenes),
371 sanisizer::cast<I<decltype(std::declval<EigenMatrix_>().cols())> >(num_blocks)
372 );
373
374 tatami_stats::GroupRssBuffers<Float> buffers;
375 buffers.mean.reserve(num_blocks);
376 buffers.rss.reserve(num_blocks);
377 auto tmp_rss = sanisizer::create<std::vector<std::vector<Float> > >(num_blocks);
378
379 for (std::size_t b = 0; b < num_blocks; ++b) {
380 buffers.mean.push_back(tmp_mean.data() + sanisizer::product_unsafe<std::size_t>(ngenes, b));
381 tatami::resize_container_to_Index_size(tmp_rss[b], ngenes);
382 buffers.rss.push_back(tmp_rss[b].data());
383 }
384
385 tatami_stats::GroupRssOptions<Float> opt;
386 opt.num_threads = nthreads;
387 opt.mean_placeholder = 0; // avoid NaN propagation in ResidualMatrix.
388 tatami_stats::group_rss(true, mat, block, num_blocks, block_sizes.data(), buffers, opt);
389
390 assert(sanisizer::is_equal(variances.size(), ngenes));
391 variances.setZero();
392 for (std::size_t b = 0; b < num_blocks; ++b) {
393 if (block_sizes[b]) {
394 const auto& currss = tmp_rss[b];
395 if (block_details.has_value()) {
396 for (Index_ g = 0; g < ngenes; ++g) {
397 variances.coeffRef(g) += currss[g] * block_details->per_element_weight[b];
398 }
399 } else {
400 for (Index_ g = 0; g < ngenes; ++g) {
401 variances.coeffRef(g) += currss[g];
402 }
403 }
404 }
405 }
406
407 centers = tmp_mean.adjoint();
408
409 // See COMMENT ON DENOMINATOR above.
410 const auto ncells = mat.ncol();
411 if (ncells > 1) {
412 for (Index_ g = 0; g < ngenes; ++g) {
413 variances.coeffRef(g) /= ncells - 1;
414 }
415 }
416}
417
418/******************************************************************
419 ************ Project matrices on their rotation vectors **********
420 ******************************************************************/
421
422template<class EigenMatrix_, class EigenVector_>
423const EigenMatrix_& scale_rotation_matrix(const EigenMatrix_& rotation, bool scale, const EigenVector_& scale_v, EigenMatrix_& tmp) {
424 if (scale) {
425 tmp = (rotation.array().colwise() / scale_v.array()).matrix();
426 return tmp;
427 } else {
428 return rotation;
429 }
430}
431
432template<class EigenVector_, class IrlbaSparseMatrix_, class EigenMatrix_>
433inline void project_matrix_realized_sparse(
434 const IrlbaSparseMatrix_& emat, // cell in rows, genes in the columns, CSC.
435 EigenMatrix_& components, // dims in rows, cells in columns
436 const EigenMatrix_& scaled_rotation, // genes in rows, dims in columns
437 int nthreads
438) {
439 const auto rank = scaled_rotation.cols();
440 const auto ncells = emat.rows();
441 const auto ngenes = emat.cols();
442
443 // Store as transposed for more cache efficiency.
444 components.resize(
445 sanisizer::cast<I<decltype(components.rows())> >(rank),
446 sanisizer::cast<I<decltype(components.cols())> >(ncells)
447 );
448 components.setZero();
449
450 const auto& values = emat.get_values();
451 const auto& indices = emat.get_indices();
452 const auto& pointers = emat.get_pointers();
453
454 if (nthreads == 1) {
455 auto multipliers = sanisizer::create<EigenVector_>(rank);
456 for (I<decltype(ngenes)> g = 0; g < ngenes; ++g) {
457 multipliers.noalias() = scaled_rotation.row(g);
458 const auto start = pointers[g], end = pointers[g + 1]; // increment is safe as 'g + 1 <= ngenes'.
459 for (auto i = start; i < end; ++i) {
460 components.col(indices[i]).noalias() += values[i] * multipliers;
461 }
462 }
463
464 } else {
465 // Here, the general strategy is to split the matrix by chunks into genes,
466 // perform the matrix multiplication for each chunk,
467 // and then sum the per-chunk products to obtain the final product.
468 // The exact result of the reduction depends on the number of threads,
469 // but this is an acceptable annoyance for greater speed.
470 const auto& primary_bounds = emat.get_primary_boundaries();
471 auto working = sanisizer::create<std::vector<EigenMatrix_> >(nthreads - 1);
472
473 irlba::parallelize(nthreads, [&](const int t) -> void {
474 EigenMatrix_* ptr;
475 if (t == 0) {
476 ptr = &components;
477 } else {
478 auto& mat = working[t - 1];
479 mat.resize(components.rows(), components.cols());
480 mat.setZero();
481 ptr = &mat;
482 }
483
484 const auto gstart = primary_bounds[t];
485 const auto gend = primary_bounds[t + 1]; // increment is safe as 't + 1 <= nthreads'.
486 auto multipliers = sanisizer::create<EigenVector_>(rank);
487 for (I<decltype(ngenes)> g = gstart; g < gend; ++g) {
488 multipliers.noalias() = scaled_rotation.row(g);
489 const auto start = pointers[g], end = pointers[g + 1]; // increment is safe as 'g + 1 <= ngenes'
490 for (auto i = start; i < end; ++i) {
491 ptr->col(indices[i]).noalias() += values[i] * multipliers;
492 }
493 }
494 });
495
496 for (auto& w : working) {
497 components += w;
498 }
499 }
500}
501
502template<typename Value_, typename Index_, class EigenMatrix_>
503void project_matrix_transposed_tatami(
504 const tatami::Matrix<Value_, Index_>& mat, // genes in rows, cells in columns
505 EigenMatrix_& components,
506 const EigenMatrix_& scaled_rotation, // genes in rows, dims in columns
507 const int nthreads)
508{
509 const auto rank = scaled_rotation.cols();
510 const auto ngenes = mat.nrow();
511 const auto ncells = mat.ncol();
512
513 // Store as transposed for more cache efficiency.
514 // This is a column-major rank x ncells matrix, which makes it a row-major ncells x rank matrix.
515 components.resize(
516 sanisizer::cast<I<decltype(components.rows())> >(rank),
517 sanisizer::cast<I<decltype(components.cols())> >(ncells)
518 );
519
521 static_assert(!EigenMatrix_::IsRowMajor);
522 auto get_right = [&](I<decltype(rank)> r) -> auto {
523 return scaled_rotation.data() + sanisizer::product_unsafe<std::size_t>(r, ngenes);
524 };
525
526 if (tmat.is_sparse()) {
527 if (tmat.prefer_rows()) {
528 tatami_mult::MultiplySparseRowWithDenseColumnMatrixToRowOutputOptions options;
529 options.num_threads = nthreads;
530 tatami_mult::multiply_sparse_row_with_dense_column_matrix_to_row_output(tmat, rank, get_right, components.data(), options);
531 } else {
532 tatami_mult::MultiplySparseColumnWithDenseColumnMatrixToRowOutputOptions options;
533 options.num_threads = nthreads;
534 tatami_mult::multiply_sparse_column_with_dense_column_matrix_to_row_output(tmat, rank, get_right, components.data(), options);
535 }
536 } else {
537 if (tmat.prefer_rows()) {
538 tatami_mult::MultiplyDenseRowWithDenseColumnMatrixToRowOutputOptions options;
539 options.num_threads = nthreads;
540 tatami_mult::multiply_dense_row_with_dense_column_matrix_to_row_output(tmat, rank, get_right, components.data(), options);
541 } else {
542 tatami_mult::MultiplyDenseColumnWithDenseColumnMatrixToRowOutputOptions options;
543 options.num_threads = nthreads;
544 tatami_mult::multiply_dense_column_with_dense_column_matrix_to_row_output(tmat, rank, get_right, components.data(), options);
545 }
546 }
547}
548
549template<class EigenMatrix_, class EigenVector_>
550void clean_up_projected(EigenMatrix_& projected, EigenVector_& D) {
551 // Empirically centering to give nice centered PCs, because we can't
552 // guarantee that the projection is centered in this manner.
553 for (I<decltype(projected.rows())> i = 0, prows = projected.rows(); i < prows; ++i) {
554 projected.row(i).array() -= projected.row(i).sum() / projected.cols();
555 }
556
557 // Just dividing by the number of observations - 1 regardless of weighting.
558 const typename EigenMatrix_::Scalar denom = projected.cols() - 1;
559 if (denom) {
560 for (auto& d : D) {
561 d = d * d / denom;
562 }
563 }
564}
565
566/*******************************
567 ***** Residual wrapper ********
568 *******************************/
569
570template<class EigenVector_, class IrlbaMatrix_, typename Block_, class CenterMatrix_>
571class ResidualWorkspace final : public irlba::Workspace<EigenVector_> {
572public:
573 ResidualWorkspace(const IrlbaMatrix_& matrix, const Block_* block, const CenterMatrix_& means) :
574 my_work(matrix.new_known_workspace()),
575 my_block(block),
576 my_means(means),
577 my_sub(sanisizer::cast<I<decltype(my_sub.size())> >(my_means.rows()))
578 {}
579
580private:
581 I<decltype(std::declval<IrlbaMatrix_>().new_known_workspace())> my_work;
582 const Block_* my_block;
583 const CenterMatrix_& my_means;
584 EigenVector_ my_sub;
585
586public:
587 void multiply(const EigenVector_& right, EigenVector_& output) {
588 my_work->multiply(right, output);
589
590 my_sub.noalias() = my_means * right;
591 for (I<decltype(output.size())> i = 0, end = output.size(); i < end; ++i) {
592 auto& val = output.coeffRef(i);
593 val -= my_sub.coeff(my_block[i]);
594 }
595 }
596};
597
598template<class EigenVector_, class IrlbaMatrix_, typename Block_, class CenterMatrix_>
599class ResidualAdjointWorkspace final : public irlba::AdjointWorkspace<EigenVector_> {
600public:
601 ResidualAdjointWorkspace(const IrlbaMatrix_& matrix, const Block_* block, const CenterMatrix_& means) :
602 my_work(matrix.new_known_adjoint_workspace()),
603 my_block(block),
604 my_means(means),
605 my_aggr(sanisizer::cast<I<decltype(my_aggr.size())> >(my_means.rows()))
606 {}
607
608private:
609 I<decltype(std::declval<IrlbaMatrix_>().new_known_adjoint_workspace())> my_work;
610 const Block_* my_block;
611 const CenterMatrix_& my_means;
612 EigenVector_ my_aggr;
613
614public:
615 void multiply(const EigenVector_& right, EigenVector_& output) {
616 my_work->multiply(right, output);
617
618 my_aggr.setZero();
619 for (I<decltype(right.size())> i = 0, end = right.size(); i < end; ++i) {
620 my_aggr.coeffRef(my_block[i]) += right.coeff(i);
621 }
622
623 output.noalias() -= my_means.adjoint() * my_aggr;
624 }
625};
626
627template<class EigenMatrix_, class IrlbaMatrix_, typename Block_, class CenterMatrix_>
628class ResidualRealizeWorkspace final : public irlba::RealizeWorkspace<EigenMatrix_> {
629public:
630 ResidualRealizeWorkspace(const IrlbaMatrix_& matrix, const Block_* block, const CenterMatrix_& means) :
631 my_work(matrix.new_known_realize_workspace()),
632 my_block(block),
633 my_means(means)
634 {}
635
636private:
637 I<decltype(std::declval<IrlbaMatrix_>().new_known_realize_workspace())> my_work;
638 const Block_* my_block;
639 const CenterMatrix_& my_means;
640
641public:
642 const EigenMatrix_& realize(EigenMatrix_& buffer) {
643 my_work->realize_copy(buffer);
644 for (I<decltype(buffer.rows())> i = 0, end = buffer.rows(); i < end; ++i) {
645 buffer.row(i) -= my_means.row(my_block[i]);
646 }
647 return buffer;
648 }
649};
650
651// This wrapper class mimics multiplication with the residuals,
652// i.e., after subtracting the per-block mean from each cell.
653template<class EigenVector_, class EigenMatrix_, class IrlbaMatrixPointer_, class Block_, class CenterMatrixPointer_>
654class ResidualMatrix final : public irlba::Matrix<EigenVector_, EigenMatrix_> {
655public:
656 ResidualMatrix(IrlbaMatrixPointer_ mat, const Block_* block, CenterMatrixPointer_ means) :
657 my_matrix(std::move(mat)),
658 my_block(block),
659 my_means(std::move(means))
660 {}
661
662public:
663 Eigen::Index rows() const {
664 return my_matrix->rows();
665 }
666
667 Eigen::Index cols() const {
668 return my_matrix->cols();
669 }
670
671private:
672 IrlbaMatrixPointer_ my_matrix;
673 const Block_* my_block;
674 CenterMatrixPointer_ my_means;
675
676public:
677 std::unique_ptr<irlba::Workspace<EigenVector_> > new_workspace() const {
678 return new_known_workspace();
679 }
680
681 std::unique_ptr<irlba::AdjointWorkspace<EigenVector_> > new_adjoint_workspace() const {
682 return new_known_adjoint_workspace();
683 }
684
685 std::unique_ptr<irlba::RealizeWorkspace<EigenMatrix_> > new_realize_workspace() const {
686 return new_known_realize_workspace();
687 }
688
689public:
690 std::unique_ptr<ResidualWorkspace<EigenVector_, decltype(*my_matrix), Block_, decltype(*my_means)> > new_known_workspace() const {
691 return std::make_unique<ResidualWorkspace<EigenVector_, decltype(*my_matrix), Block_, decltype(*my_means)> >(*my_matrix, my_block, *my_means);
692 }
693
694 std::unique_ptr<ResidualAdjointWorkspace<EigenVector_, decltype(*my_matrix), Block_, decltype(*my_means)> > new_known_adjoint_workspace() const {
695 return std::make_unique<ResidualAdjointWorkspace<EigenVector_, decltype(*my_matrix), Block_, decltype(*my_means)> >(*my_matrix, my_block, *my_means);
696 }
697
698 std::unique_ptr<ResidualRealizeWorkspace<EigenMatrix_, decltype(*my_matrix), Block_, decltype(*my_means)> > new_known_realize_workspace() const {
699 return std::make_unique<ResidualRealizeWorkspace<EigenMatrix_, decltype(*my_matrix), Block_, decltype(*my_means)> >(*my_matrix, my_block, *my_means);
700 }
701};
712template<typename EigenMatrix_, typename EigenVector_>
722 EigenMatrix_ components;
723
729 EigenVector_ variance_explained;
730
735 typename EigenVector_::Scalar total_variance = 0;
736
742 EigenMatrix_ rotation;
743
750 EigenMatrix_ center;
751
759 std::optional<EigenVector_> scale;
760
765};
766
770template<typename Value_, typename Index_, typename Block_, typename EigenMatrix_, class EigenVector_, class SubsetFunction_>
771void blocked_pca_internal(
773 const Block_* block,
774 const std::size_t num_blocks,
775 const BlockedPcaOptions<EigenVector_>& options,
777 SubsetFunction_ subset_fun
778) {
780 std::unique_ptr<irlba::Matrix<EigenVector_, EigenMatrix_> > ptr;
781 std::function<void(const EigenMatrix_&)> projector;
782
783 const Index_ ngenes = mat.nrow(), ncells = mat.ncol();
784 output.center.resize(
785 sanisizer::cast<I<decltype(output.center.rows())> >(num_blocks),
786 sanisizer::cast<I<decltype(output.center.cols())> >(ngenes)
787 );
789
790 auto block_sizes = sanisizer::create<std::vector<Index_> >(num_blocks);
791 for (Index_ c = 0; c < ncells; ++c) {
792 block_sizes[block[c]] += 1;
793 }
794 auto block_details = compute_blocking_details<EigenVector_>(
795 mat.ncol(),
796 block,
797 num_blocks,
798 block_sizes,
799 options.block_weight_policy,
801 );
802
803 if (!options.realize_matrix) {
804 compute_blockwise_mean_and_variance_tatami(
805 mat,
806 block,
807 num_blocks,
808 block_sizes,
809 block_details,
810 output.center,
811 scale,
812 options.num_threads
813 );
814 ptr.reset(new irlba_tatami::Transposed<EigenVector_, EigenMatrix_, Value_, Index_, decltype(&mat)>(&mat, options.num_threads));
815 projector = [&](const EigenMatrix_& scaled_rotation) -> void {
816 project_matrix_transposed_tatami(mat, output.components, scaled_rotation, options.num_threads);
817 };
818
819 } else if (mat.sparse()) {
820 // 'extracted' contains row-major contents... but we implicitly transpose it to CSC with genes in columns.
822 mat,
823 /* row = */ true,
824 [&]{
826 opt.two_pass = false;
827 opt.num_threads = options.num_threads;
828 return opt;
829 }()
830 );
831
832 // Storing sparse_ptr in the unique pointer should not invalidate the former,
833 // based on a reading of the C++ specification w.r.t. reset();
834 // so we can continue to use it for projection.
835 const auto sparse_ptr = new irlba::ParallelSparseMatrix<
836 EigenVector_,
837 EigenMatrix_,
838 I<decltype(extracted.value)>,
839 I<decltype(extracted.index)>,
840 I<decltype(extracted.pointers)>
841 >(
842 ncells,
843 ngenes,
844 std::move(extracted.value),
845 std::move(extracted.index),
846 std::move(extracted.pointers),
847 true,
848 options.num_threads
849 );
850 ptr.reset(sparse_ptr);
851
852 compute_blockwise_mean_and_variance_realized_sparse(
853 *sparse_ptr,
854 block,
855 num_blocks,
856 block_sizes,
857 block_details,
858 output.center,
859 scale,
860 options.num_threads
861 );
862
863 // Make sure to copy sparse_ptr because it doesn't exist outside of this scope.
864 projector = [&,sparse_ptr](const EigenMatrix_& scaled_rotation) -> void {
865 project_matrix_realized_sparse<EigenVector_>(*sparse_ptr, output.components, scaled_rotation, options.num_threads);
866 };
867
868 } else {
869 // Perform an implicit transposition by performing a row-major extraction into a column-major transposed matrix.
870 auto tmp_ptr = std::make_unique<EigenMatrix_>(
871 sanisizer::cast<I<decltype(std::declval<EigenMatrix_>().rows())> >(ncells),
872 sanisizer::cast<I<decltype(std::declval<EigenMatrix_>().cols())> >(ngenes)
873 );
874 static_assert(!EigenMatrix_::IsRowMajor);
875
877 mat,
878 /* row_major = */ true,
879 tmp_ptr->data(),
880 [&]{
881 tatami::ConvertToDenseOptions opt;
882 opt.num_threads = options.num_threads;
883 return opt;
884 }()
885 );
886
887 compute_blockwise_mean_and_variance_realized_dense(
888 *tmp_ptr,
889 block,
890 num_blocks,
891 block_sizes,
892 block_details,
893 output.center,
894 scale,
895 options.num_threads
896 );
897
898 const auto dense_ptr = tmp_ptr.get(); // do this before the move.
899 ptr.reset(new irlba::SimpleMatrix<EigenVector_, EigenMatrix_, decltype(tmp_ptr)>(std::move(tmp_ptr)));
900
901 // Make sure to copy dense_ptr because it doesn't exist outside of this scope.
902 projector = [&,dense_ptr](const EigenMatrix_& scaled_rotation) -> void {
903 output.components.noalias() = (*dense_ptr * scaled_rotation).adjoint();
904 };
905 }
906
907 output.total_variance = process_scale_vector(options.scale, scale);
908
909 std::unique_ptr<irlba::Matrix<EigenVector_, EigenMatrix_> > alt;
910 alt.reset(
911 new ResidualMatrix<
912 EigenVector_,
913 EigenMatrix_,
914 I<decltype(ptr)>,
915 Block_,
916 I<decltype(&(output.center))>
917 >(
918 std::move(ptr),
919 block,
920 &(output.center)
921 )
922 );
923 ptr.swap(alt);
924
925 if (options.scale) {
926 alt.reset(
928 EigenVector_,
929 EigenMatrix_,
930 I<decltype(ptr)>,
931 I<decltype(&(scale))>
932 >(
933 std::move(ptr),
934 &(scale),
935 /* column = */ true,
936 /* divide = */ true
937 )
938 );
939 ptr.swap(alt);
940 }
941
942 if (block_details.has_value()) {
943 alt.reset(
945 EigenVector_,
946 EigenMatrix_,
947 I<decltype(ptr)>,
948 I<decltype(&(block_details->expanded_weights))>
949 >(
950 std::move(ptr),
951 &(block_details->expanded_weights),
952 /* column = */ false,
953 /* divide = */ false
954 )
955 );
956 ptr.swap(alt);
957
958 output.metrics = irlba::compute(*ptr, options.number, output.components, output.rotation, output.variance_explained, options.irlba_options);
959 subset_fun(num_blocks, block_sizes, block_details, output.components, output.variance_explained);
960
961 EigenMatrix_ tmp;
962 const auto& scaled_rotation = scale_rotation_matrix(output.rotation, options.scale, scale, tmp);
963 projector(scaled_rotation);
964
965 // Subtracting each block's mean from the PCs.
966 if (options.center_scores_by_block) {
967 EigenMatrix_ centering = (output.center * scaled_rotation).adjoint();
968 for (I<decltype(ncells)> c =0 ; c < ncells; ++c) {
969 output.components.col(c) -= centering.col(block[c]);
970 }
971 }
972
973 clean_up_projected(output.components, output.variance_explained);
974 if (!options.transpose) {
975 output.components.adjointInPlace();
976 }
977
978 } else {
979 output.metrics = irlba::compute(*ptr, options.number, output.components, output.rotation, output.variance_explained, options.irlba_options);
980 subset_fun(num_blocks, block_sizes, block_details, output.components, output.variance_explained);
981
982 if (options.center_scores_by_block) {
983 clean_up(mat.ncol(), output.components, output.variance_explained);
984 if (options.transpose) {
985 output.components.adjointInPlace();
986 }
987
988 } else {
989 EigenMatrix_ tmp;
990 const auto& scaled_rotation = scale_rotation_matrix(output.rotation, options.scale, scale, tmp);
991 projector(scaled_rotation);
992
993 clean_up_projected(output.components, output.variance_explained);
994 if (!options.transpose) {
995 output.components.adjointInPlace();
996 }
997 }
998 }
999
1000 if (options.scale) {
1001 output.scale = std::move(scale);
1002 }
1003}
1059template<typename Value_, typename Index_, typename Block_, typename EigenMatrix_, class EigenVector_>
1062 const Block_* block,
1063 const std::size_t num_blocks,
1064 const BlockedPcaOptions<EigenVector_>& options,
1066) {
1067 blocked_pca_internal<Value_, Index_, Block_, EigenMatrix_, EigenVector_>(
1068 mat,
1069 block,
1070 num_blocks,
1071 options,
1072 output,
1073 [&](
1074 const std::size_t,
1075 const std::vector<Index_>&,
1076 const std::optional<BlockingDetails<EigenVector_> >&,
1077 const EigenMatrix_&,
1078 const EigenVector_&
1079 ) -> void {}
1080 );
1081}
1082
1102template<typename EigenMatrix_ = Eigen::MatrixXd, class EigenVector_ = Eigen::VectorXd, typename Value_, typename Index_, typename Block_>
1105 const Block_* block,
1106 const std::size_t num_blocks,
1107 const BlockedPcaOptions<EigenVector_>& options
1108) {
1110 blocked_pca(mat, block, num_blocks, options, output);
1111 return output;
1112}
1113
1114}
1115
1116#endif
virtual Index_ ncol() const=0
virtual Index_ nrow() const=0
virtual std::unique_ptr< MyopicSparseExtractor< Value_, Index_ > > sparse(bool row, const Options &opt) const=0
Metrics compute(const Matrix_ &matrix, const Eigen::Index number, EigenMatrix_ &outU, EigenMatrix_ &outV, EigenVector_ &outD, const Options< EigenVector_ > &options)
void parallelize(Task_ num_tasks, Run_ run_task)
double compute_variable_weight(const double s, const VariableWeightParameters &params)
Principal component analysis on single-cell data.
void blocked_pca(const tatami::Matrix< Value_, Index_ > &mat, const Block_ *block, const std::size_t num_blocks, const BlockedPcaOptions< EigenVector_ > &options, BlockedPcaResults< EigenMatrix_, EigenVector_ > &output)
Definition blocked_pca.hpp:1060
std::shared_ptr< const Matrix< Value_, Index_ > > wrap_shared_ptr(const Matrix< Value_, Index_ > *const ptr)
void resize_container_to_Index_size(Container_ &container, const Index_ x, Args_ &&... args)
CompressedSparseContents< StoredValue_, StoredIndex_, StoredPointer_ > retrieve_compressed_sparse_contents(const Matrix< InputValue_, InputIndex_ > &matrix, const bool row, const RetrieveCompressedSparseContentsOptions &options)
int parallelize(Function_ fun, const Index_ tasks, const int workers)
void convert_to_dense(const Matrix< InputValue_, InputIndex_ > &matrix, const bool row_major, StoredValue_ *const store, const ConvertToDenseOptions &options)
I< decltype(std::declval< Container_ >().size())> cast_Index_to_container_size(const Index_ x)
Container_ create_container_of_Index_size(const Index_ x, Args_ &&... args)
Options for blocked_pca().
Definition blocked_pca.hpp:36
int number
Definition blocked_pca.hpp:53
irlba::Options< EigenVector_ > irlba_options
Definition blocked_pca.hpp:111
bool transpose
Definition blocked_pca.hpp:67
scran_blocks::VariableWeightParameters variable_block_weight_parameters
Definition blocked_pca.hpp:84
scran_blocks::WeightPolicy block_weight_policy
Definition blocked_pca.hpp:78
bool scale
Definition blocked_pca.hpp:61
bool center_scores_by_block
Definition blocked_pca.hpp:92
bool realize_matrix
Definition blocked_pca.hpp:98
int num_threads
Definition blocked_pca.hpp:106
Results of blocked_pca().
Definition blocked_pca.hpp:713
EigenVector_::Scalar total_variance
Definition blocked_pca.hpp:735
EigenMatrix_ components
Definition blocked_pca.hpp:722
std::optional< EigenVector_ > scale
Definition blocked_pca.hpp:759
irlba::Metrics metrics
Definition blocked_pca.hpp:764
EigenMatrix_ rotation
Definition blocked_pca.hpp:742
EigenMatrix_ center
Definition blocked_pca.hpp:750
EigenVector_ variance_explained
Definition blocked_pca.hpp:729