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
11#include "tatami/tatami.hpp"
12#include "irlba/irlba.hpp"
13#include "irlba/parallel.hpp"
14#include "irlba_tatami/irlba_tatami.hpp"
15#include "Eigen/Dense"
17#include "sanisizer/sanisizer.hpp"
18
19#include "utils.hpp"
20
26namespace scran_pca {
27
33template<typename EigenVector_ = Eigen::VectorXd>
39 // Avoid throwing an error if too many PCs are requested.
40 irlba_options.cap_number = true;
41 }
51 int number = 25;
52
59 bool scale = false;
60
65 bool transpose = true;
66
76 scran_blocks::WeightPolicy block_weight_policy = scran_blocks::WeightPolicy::VARIABLE;
77
83
91
96 bool realize_matrix = true;
97
104 int num_threads = 1;
105
110};
111
115/*****************************************************
116 ************* Blocking data structures **************
117 *****************************************************/
118
119template<typename Index_, class EigenVector_>
120struct BlockingDetails {
121 std::vector<Index_> block_size;
122
123 bool weighted = false;
124 typedef typename EigenVector_::Scalar Weight;
125
126 // The below should only be used if weighted = true.
127 std::vector<Weight> per_element_weight;
128 Weight total_block_weight = 0;
129 EigenVector_ expanded_weights;
130};
131
132template<class EigenVector_, typename Index_, typename Block_>
133BlockingDetails<Index_, EigenVector_> compute_blocking_details(
134 const Index_ ncells,
135 const Block_* block,
136 const scran_blocks::WeightPolicy block_weight_policy,
137 const scran_blocks::VariableWeightParameters& variable_block_weight_parameters)
138{
139 BlockingDetails<Index_, EigenVector_> output;
140 output.block_size = tatami_stats::tabulate_groups(block, ncells);
141 if (block_weight_policy == scran_blocks::WeightPolicy::NONE) {
142 return output;
143 }
144
145 const auto& block_size = output.block_size;
146 const auto nblocks = block_size.size();
147 output.weighted = true;
148 auto& total_weight = output.total_block_weight;
149 auto& element_weight = output.per_element_weight;
150 sanisizer::resize(element_weight, nblocks);
151
152 for (I<decltype(nblocks)> b = 0; b < nblocks; ++b) {
153 const auto bsize = block_size[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 sanisizer::resize(expanded, ncells);
184 for (Index_ c = 0; c < ncells; ++c) {
185 expanded.coeffRef(c) = sqrt_weights[block[c]];
186 }
187
188 return output;
189}
190
191/*****************************************************************
192 ************ Computing the blockwise mean and variance **********
193 *****************************************************************/
194
195template<typename Num_, typename Value_, typename Index_, typename Block_, typename EigenVector_, typename Float_>
196void compute_sparse_mean_and_variance_blocked(
197 const Num_ num_nonzero,
198 const Value_* values,
199 const Index_* indices,
200 const Block_* block,
201 const BlockingDetails<Index_, EigenVector_>& block_details,
202 Float_* centers,
203 Float_& variance,
204 std::vector<Index_>& block_copy,
205 const Num_ num_all)
206{
207 const auto& block_size = block_details.block_size;
208 const auto nblocks = block_size.size();
209
210 std::fill_n(centers, nblocks, 0);
211 for (Num_ i = 0; i < num_nonzero; ++i) {
212 centers[block[indices[i]]] += values[i];
213 }
214 for (I<decltype(nblocks)> b = 0; b < nblocks; ++b) {
215 auto bsize = block_size[b];
216 if (bsize) {
217 centers[b] /= bsize;
218 }
219 }
220
221 // Computing the variance from the sum of squared differences.
222 // This is technically not the correct variance estimate if we
223 // were to consider the loss of residual d.f. from estimating
224 // the block means, but it's what the PCA sees, so whatever.
225 variance = 0;
226 std::copy(block_size.begin(), block_size.end(), block_copy.begin());
227
228 if (block_details.weighted) {
229 for (Num_ i = 0; i < num_nonzero; ++i) {
230 const Block_ curb = block[indices[i]];
231 const auto diff = values[i] - centers[curb];
232 variance += diff * diff * block_details.per_element_weight[curb];
233 --block_copy[curb];
234 }
235 for (I<decltype(nblocks)> b = 0; b < nblocks; ++b) {
236 const auto val = centers[b];
237 variance += val * val * block_copy[b] * block_details.per_element_weight[b];
238 }
239 } else {
240 for (Num_ i = 0; i < num_nonzero; ++i) {
241 const Block_ curb = block[indices[i]];
242 const auto diff = values[i] - centers[curb];
243 variance += diff * diff;
244 --block_copy[curb];
245 }
246 for (I<decltype(nblocks)> b = 0; b < nblocks; ++b) {
247 const auto val = centers[b];
248 variance += val * val * block_copy[b];
249 }
250 }
251
252 // COMMENT ON DENOMINATOR:
253 // If we're not dealing with weights, we compute the actual sample
254 // variance for easy interpretation (and to match up with the
255 // per-PC calculations in clean_up).
256 //
257 // If we're dealing with weights, the concept of the sample variance
258 // becomes somewhat weird, but we just use the same denominator for
259 // consistency in clean_up_projected. Magnitude doesn't matter when
260 // scaling for process_scale_vector anyway.
261 variance /= num_all - 1;
262}
263
264template<class IrlbaSparseMatrix_, typename Block_, class Index_, class EigenVector_, class EigenMatrix_>
265void compute_blockwise_mean_and_variance_realized_sparse(
266 const IrlbaSparseMatrix_& emat, // this should be column-major with genes in the columns.
267 const Block_* block,
268 const BlockingDetails<Index_, EigenVector_>& block_details,
269 EigenMatrix_& centers,
270 EigenVector_& variances,
271 const int nthreads)
272{
273 const auto ngenes = emat.cols();
274 tatami::parallelize([&](const int, const I<decltype(ngenes)> start, const I<decltype(ngenes)> length) -> void {
275 const auto ncells = emat.rows();
276 const auto& values = emat.get_values();
277 const auto& indices = emat.get_indices();
278 const auto& pointers = emat.get_pointers();
279
280 const auto nblocks = block_details.block_size.size();
281 static_assert(!EigenMatrix_::IsRowMajor);
282 auto block_copy = sanisizer::create<std::vector<Index_> >(nblocks);
283
284 for (I<decltype(start)> g = start, end = start + length; g < end; ++g) {
285 const auto offset = pointers[g];
286 const auto next_offset = pointers[g + 1]; // increment won't overflow as 'g < end' and 'end' is of the same type.
287 compute_sparse_mean_and_variance_blocked(
288 static_cast<I<decltype(ncells)> >(next_offset - offset),
289 values.data() + offset,
290 indices.data() + offset,
291 block,
292 block_details,
293 centers.data() + sanisizer::product_unsafe<std::size_t>(g, nblocks),
294 variances[g],
295 block_copy,
296 ncells
297 );
298 }
299 }, ngenes, nthreads);
300}
301
302template<typename Num_, typename Value_, typename Block_, typename Index_, typename EigenVector_, typename Float_>
303void compute_dense_mean_and_variance_blocked(
304 const Num_ number,
305 const Value_* values,
306 const Block_* block,
307 const BlockingDetails<Index_, EigenVector_>& block_details,
308 Float_* centers,
309 Float_& variance)
310{
311 const auto& block_size = block_details.block_size;
312 const auto nblocks = block_size.size();
313 std::fill_n(centers, nblocks, 0);
314 for (Num_ i = 0; i < number; ++i) {
315 centers[block[i]] += values[i];
316 }
317 for (I<decltype(nblocks)> b = 0; b < nblocks; ++b) {
318 const auto& bsize = block_size[b];
319 if (bsize) {
320 centers[b] /= bsize;
321 }
322 }
323
324 variance = 0;
325
326 if (block_details.weighted) {
327 for (Num_ i = 0; i < number; ++i) {
328 const auto curb = block[i];
329 const auto delta = values[i] - centers[curb];
330 variance += delta * delta * block_details.per_element_weight[curb];
331 }
332 } else {
333 for (Num_ i = 0; i < number; ++i) {
334 const auto curb = block[i];
335 const auto delta = values[i] - centers[curb];
336 variance += delta * delta;
337 }
338 }
339
340 variance /= number - 1; // See COMMENT ON DENOMINATOR above.
341}
342
343template<class EigenMatrix_, typename Block_, class Index_, class EigenVector_>
344void compute_blockwise_mean_and_variance_realized_dense(
345 const EigenMatrix_& emat, // this should be column-major with genes in the columns.
346 const Block_* block,
347 const BlockingDetails<Index_, EigenVector_>& block_details,
348 EigenMatrix_& centers,
349 EigenVector_& variances,
350 const int nthreads)
351{
352 const auto ngenes = emat.cols();
353 tatami::parallelize([&](const int, const I<decltype(ngenes)> start, const I<decltype(ngenes)> length) -> void {
354 const auto ncells = emat.rows();
355 static_assert(!EigenMatrix_::IsRowMajor);
356 const auto nblocks = block_details.block_size.size();
357 for (I<decltype(start)> g = start, end = start + length; g < end; ++g) {
358 compute_dense_mean_and_variance_blocked(
359 ncells,
360 emat.data() + sanisizer::product_unsafe<std::size_t>(g, ncells),
361 block,
362 block_details,
363 centers.data() + sanisizer::product_unsafe<std::size_t>(g, nblocks),
364 variances[g]
365 );
366 }
367 }, ngenes, nthreads);
368}
369
370template<typename Value_, typename Index_, typename Block_, class EigenMatrix_, class EigenVector_>
371void compute_blockwise_mean_and_variance_tatami(
372 const tatami::Matrix<Value_, Index_>& mat, // this should have genes in the rows!
373 const Block_* block,
374 const BlockingDetails<Index_, EigenVector_>& block_details,
375 EigenMatrix_& centers,
376 EigenVector_& variances,
377 const int nthreads)
378{
379 const auto& block_size = block_details.block_size;
380 const auto nblocks = block_size.size();
381 const Index_ ngenes = mat.nrow();
382 const Index_ ncells = mat.ncol();
383
384 if (mat.prefer_rows()) {
385 tatami::parallelize([&](const int, const Index_ start, const Index_ length) -> void {
386 static_assert(!EigenMatrix_::IsRowMajor);
387 auto block_copy = sanisizer::create<std::vector<Index_> >(nblocks);
389
390 if (mat.is_sparse()) {
392 auto ext = tatami::consecutive_extractor<true>(mat, true, start, length);
393 for (Index_ g = start, end = start + length; g < end; ++g) {
394 auto range = ext->fetch(vbuffer.data(), ibuffer.data());
395 compute_sparse_mean_and_variance_blocked(
396 range.number,
397 range.value,
398 range.index,
399 block,
400 block_details,
401 centers.data() + sanisizer::product_unsafe<std::size_t>(g, nblocks),
402 variances[g],
403 block_copy,
404 ncells
405 );
406 }
407
408 } else {
409 auto ext = tatami::consecutive_extractor<false>(mat, true, start, length);
410 for (Index_ g = start, end = start + length; g < end; ++g) {
411 auto ptr = ext->fetch(vbuffer.data());
412 compute_dense_mean_and_variance_blocked(
413 ncells,
414 ptr,
415 block,
416 block_details,
417 centers.data() + sanisizer::product_unsafe<std::size_t>(g, nblocks),
418 variances[g]
419 );
420 }
421 }
422 }, ngenes, nthreads);
423
424 } else {
425 typedef typename EigenVector_::Scalar Scalar;
426 std::vector<std::pair<I<decltype(nblocks)>, Scalar> > block_multipliers;
427 block_multipliers.reserve(nblocks);
428
429 for (I<decltype(nblocks)> b = 0; b < nblocks; ++b) {
430 const auto bsize = block_size[b];
431 if (bsize > 1) { // skipping blocks with NaN variances.
432 Scalar mult = bsize - 1; // need to convert variances back into sum of squared differences.
433 if (block_details.weighted) {
434 mult *= block_details.per_element_weight[b];
435 }
436 block_multipliers.emplace_back(b, mult);
437 }
438 }
439
440 tatami::parallelize([&](const int, const Index_ start, const Index_ length) -> void {
441 std::vector<std::vector<Scalar> > re_centers, re_variances;
442 re_centers.reserve(nblocks);
443 re_variances.reserve(nblocks);
444 for (I<decltype(nblocks)> b = 0; b < nblocks; ++b) {
445 re_centers.emplace_back(length);
446 re_variances.emplace_back(length);
447 }
448
450
451 if (mat.is_sparse()) {
452 std::vector<tatami_stats::variances::RunningSparse<Scalar, Value_, Index_> > running;
453 running.reserve(nblocks);
454 for (I<decltype(nblocks)> b = 0; b < nblocks; ++b) {
455 running.emplace_back(length, re_centers[b].data(), re_variances[b].data(), /* skip_nan = */ false, /* subtract = */ start);
456 }
457
459 auto ext = tatami::consecutive_extractor<true>(mat, false, static_cast<Index_>(0), ncells, start, length);
460 for (Index_ c = 0; c < ncells; ++c) {
461 const auto range = ext->fetch(vbuffer.data(), ibuffer.data());
462 running[block[c]].add(range.value, range.index, range.number);
463 }
464
465 for (I<decltype(nblocks)> b = 0; b < nblocks; ++b) {
466 running[b].finish();
467 }
468
469 } else {
470 std::vector<tatami_stats::variances::RunningDense<Scalar, Value_, Index_> > running;
471 running.reserve(nblocks);
472 for (I<decltype(nblocks)> b = 0; b < nblocks; ++b) {
473 running.emplace_back(length, re_centers[b].data(), re_variances[b].data(), /* skip_nan = */ false);
474 }
475
476 auto ext = tatami::consecutive_extractor<false>(mat, false, static_cast<Index_>(0), ncells, start, length);
477 for (Index_ c = 0; c < ncells; ++c) {
478 auto ptr = ext->fetch(vbuffer.data());
479 running[block[c]].add(ptr);
480 }
481
482 for (I<decltype(nblocks)> b = 0; b < nblocks; ++b) {
483 running[b].finish();
484 }
485 }
486
487 static_assert(!EigenMatrix_::IsRowMajor);
488 for (Index_ i = 0; i < length; ++i) {
489 auto mptr = centers.data() + sanisizer::product_unsafe<std::size_t>(start + i, nblocks);
490 for (I<decltype(nblocks)> b = 0; b < nblocks; ++b) {
491 mptr[b] = re_centers[b][i];
492 }
493
494 auto& my_var = variances[start + i];
495 my_var = 0;
496 for (const auto& bm : block_multipliers) {
497 my_var += re_variances[bm.first][i] * bm.second;
498 }
499 my_var /= ncells - 1; // See COMMENT ON DENOMINATOR above.
500 }
501 }, ngenes, nthreads);
502 }
503}
504
505/******************************************************************
506 ************ Project matrices on their rotation vectors **********
507 ******************************************************************/
508
509template<class EigenMatrix_, class EigenVector_>
510const EigenMatrix_& scale_rotation_matrix(const EigenMatrix_& rotation, bool scale, const EigenVector_& scale_v, EigenMatrix_& tmp) {
511 if (scale) {
512 tmp = (rotation.array().colwise() / scale_v.array()).matrix();
513 return tmp;
514 } else {
515 return rotation;
516 }
517}
518
519template<class EigenVector_, class IrlbaSparseMatrix_, class EigenMatrix_>
520inline void project_matrix_realized_sparse(
521 const IrlbaSparseMatrix_& emat, // cell in rows, genes in the columns, CSC.
522 EigenMatrix_& components, // dims in rows, cells in columns
523 const EigenMatrix_& scaled_rotation, // genes in rows, dims in columns
524 int nthreads
525) {
526 const auto rank = scaled_rotation.cols();
527 const auto ncells = emat.rows();
528 const auto ngenes = emat.cols();
529
530 // Store as transposed for more cache efficiency.
531 components.resize(
532 sanisizer::cast<I<decltype(components.rows())> >(rank),
533 sanisizer::cast<I<decltype(components.cols())> >(ncells)
534 );
535 components.setZero();
536
537 const auto& values = emat.get_values();
538 const auto& indices = emat.get_indices();
539 const auto& pointers = emat.get_pointers();
540
541 if (nthreads == 1) {
542 auto multipliers = sanisizer::create<EigenVector_>(rank);
543 for (I<decltype(ngenes)> g = 0; g < ngenes; ++g) {
544 multipliers.noalias() = scaled_rotation.row(g);
545 const auto start = pointers[g], end = pointers[g + 1]; // increment is safe as 'g + 1 <= ngenes'.
546 for (auto i = start; i < end; ++i) {
547 components.col(indices[i]).noalias() += values[i] * multipliers;
548 }
549 }
550
551 } else {
552 // Here, the general strategy is to split the matrix by chunks into genes,
553 // perform the matrix multiplication for each chunk,
554 // and then sum the per-chunk products to obtain the final product.
555 // The exact result of the reduction depends on the number of threads,
556 // but this is an acceptable annoyance for greater speed.
557 const auto& primary_bounds = emat.get_primary_boundaries();
558 auto working = sanisizer::create<std::vector<EigenMatrix_> >(nthreads - 1);
559
560 irlba::parallelize(nthreads, [&](const int t) -> void {
561 EigenMatrix_* ptr;
562 if (t == 0) {
563 ptr = &components;
564 } else {
565 auto& mat = working[t - 1];
566 mat.resize(components.rows(), components.cols());
567 mat.setZero();
568 ptr = &mat;
569 }
570
571 const auto gstart = primary_bounds[t];
572 const auto gend = primary_bounds[t + 1]; // increment is safe as 't + 1 <= nthreads'.
573 auto multipliers = sanisizer::create<EigenVector_>(rank);
574 for (I<decltype(ngenes)> g = gstart; g < gend; ++g) {
575 multipliers.noalias() = scaled_rotation.row(g);
576 const auto start = pointers[g], end = pointers[g + 1]; // increment is safe as 'g + 1 <= ngenes'
577 for (auto i = start; i < end; ++i) {
578 ptr->col(indices[i]).noalias() += values[i] * multipliers;
579 }
580 }
581 });
582
583 for (auto& w : working) {
584 components += w;
585 }
586 }
587}
588
589template<typename Value_, typename Index_, class EigenMatrix_>
590void project_matrix_transposed_tatami(
591 const tatami::Matrix<Value_, Index_>& mat, // genes in rows, cells in columns
592 EigenMatrix_& components,
593 const EigenMatrix_& scaled_rotation, // genes in rows, dims in columns
594 const int nthreads)
595{
596 const auto rank = scaled_rotation.cols();
597 const auto ngenes = mat.nrow();
598 const auto ncells = mat.ncol();
599 typedef typename EigenMatrix_::Scalar Scalar;
600
601 // Store as transposed for more cache efficiency.
602 components.resize(
603 sanisizer::cast<I<decltype(components.rows())> >(rank),
604 sanisizer::cast<I<decltype(components.cols())> >(ncells)
605 );
606
607 if (mat.prefer_rows()) {
608 tatami::parallelize([&](const int, const Index_ start, const Index_ length) -> void {
609 static_assert(!EigenMatrix_::IsRowMajor);
610 const auto vptr = scaled_rotation.data();
612
613 std::vector<std::vector<Scalar> > local_buffers; // create separate buffers to avoid false sharing.
614 local_buffers.reserve(rank);
615 for (I<decltype(rank)> r = 0; r < rank; ++r) {
616 local_buffers.emplace_back(tatami::cast_Index_to_container_size<I<decltype(local_buffers.front())> >(length));
617 }
618
619 if (mat.is_sparse()) {
621 auto ext = tatami::consecutive_extractor<true>(mat, true, static_cast<Index_>(0), ngenes, start, length);
622 for (Index_ g = 0; g < ngenes; ++g) {
623 const auto range = ext->fetch(vbuffer.data(), ibuffer.data());
624 for (I<decltype(rank)> r = 0; r < rank; ++r) {
625 const auto mult = vptr[sanisizer::nd_offset<std::size_t>(g, ngenes, r)];
626 auto& local_buffer = local_buffers[r];
627 for (Index_ i = 0; i < range.number; ++i) {
628 local_buffer[range.index[i] - start] += range.value[i] * mult;
629 }
630 }
631 }
632
633 } else {
634 auto ext = tatami::consecutive_extractor<false>(mat, true, static_cast<Index_>(0), ngenes, start, length);
635 for (Index_ g = 0; g < ngenes; ++g) {
636 const auto ptr = ext->fetch(vbuffer.data());
637 for (I<decltype(rank)> r = 0; r < rank; ++r) {
638 const auto mult = vptr[sanisizer::nd_offset<std::size_t>(g, ngenes, r)];
639 auto& local_buffer = local_buffers[r];
640 for (Index_ i = 0; i < length; ++i) {
641 local_buffer[i] += ptr[i] * mult;
642 }
643 }
644 }
645 }
646
647 for (I<decltype(rank)> r = 0; r < rank; ++r) {
648 for (Index_ c = 0; c < length; ++c) {
649 components.coeffRef(r, c + start) = local_buffers[r][c];
650 }
651 }
652
653 }, ncells, nthreads);
654
655 } else {
656 tatami::parallelize([&](const int, const Index_ start, const Index_ length) -> void {
657 static_assert(!EigenMatrix_::IsRowMajor);
659
660 if (mat.is_sparse()) {
661 std::vector<Index_> ibuffer(ngenes);
662 auto ext = tatami::consecutive_extractor<true>(mat, false, start, length);
663
664 for (Index_ c = start, end = start + length; c < end; ++c) {
665 const auto range = ext->fetch(vbuffer.data(), ibuffer.data());
666 static_assert(!EigenMatrix_::IsRowMajor);
667 for (I<decltype(rank)> r = 0; r < rank; ++r) {
668 auto& output = components.coeffRef(r, c);
669 output = 0;
670 const auto rotptr = scaled_rotation.data() + sanisizer::product_unsafe<std::size_t>(r, ngenes);
671 for (Index_ i = 0; i < range.number; ++i) {
672 output += rotptr[range.index[i]] * range.value[i];
673 }
674 }
675 }
676
677 } else {
678 auto ext = tatami::consecutive_extractor<false>(mat, false, start, length);
679 for (Index_ c = start, end = start + length; c < end; ++c) {
680 const auto ptr = ext->fetch(vbuffer.data());
681 static_assert(!EigenMatrix_::IsRowMajor);
682 for (I<decltype(rank)> r = 0; r < rank; ++r) {
683 const auto rotptr = scaled_rotation.data() + sanisizer::product_unsafe<std::size_t>(r, ngenes);
684 components.coeffRef(r, c) = std::inner_product(rotptr, rotptr + ngenes, ptr, static_cast<Scalar>(0));
685 }
686 }
687 }
688 }, ncells, nthreads);
689 }
690}
691
692template<class EigenMatrix_, class EigenVector_>
693void clean_up_projected(EigenMatrix_& projected, EigenVector_& D) {
694 // Empirically centering to give nice centered PCs, because we can't
695 // guarantee that the projection is centered in this manner.
696 for (I<decltype(projected.rows())> i = 0, prows = projected.rows(); i < prows; ++i) {
697 projected.row(i).array() -= projected.row(i).sum() / projected.cols();
698 }
699
700 // Just dividing by the number of observations - 1 regardless of weighting.
701 const typename EigenMatrix_::Scalar denom = projected.cols() - 1;
702 for (auto& d : D) {
703 d = d * d / denom;
704 }
705}
706
707/*******************************
708 ***** Residual wrapper ********
709 *******************************/
710
711template<class EigenVector_, class IrlbaMatrix_, typename Block_, class CenterMatrix_>
712class ResidualWorkspace final : public irlba::Workspace<EigenVector_> {
713public:
714 ResidualWorkspace(const IrlbaMatrix_& matrix, const Block_* block, const CenterMatrix_& means) :
715 my_work(matrix.new_known_workspace()),
716 my_block(block),
717 my_means(means),
718 my_sub(sanisizer::cast<I<decltype(my_sub.size())> >(my_means.rows()))
719 {}
720
721private:
722 I<decltype(std::declval<IrlbaMatrix_>().new_known_workspace())> my_work;
723 const Block_* my_block;
724 const CenterMatrix_& my_means;
725 EigenVector_ my_sub;
726
727public:
728 void multiply(const EigenVector_& right, EigenVector_& output) {
729 my_work->multiply(right, output);
730
731 my_sub.noalias() = my_means * right;
732 for (I<decltype(output.size())> i = 0, end = output.size(); i < end; ++i) {
733 auto& val = output.coeffRef(i);
734 val -= my_sub.coeff(my_block[i]);
735 }
736 }
737};
738
739template<class EigenVector_, class IrlbaMatrix_, typename Block_, class CenterMatrix_>
740class ResidualAdjointWorkspace final : public irlba::AdjointWorkspace<EigenVector_> {
741public:
742 ResidualAdjointWorkspace(const IrlbaMatrix_& matrix, const Block_* block, const CenterMatrix_& means) :
743 my_work(matrix.new_known_adjoint_workspace()),
744 my_block(block),
745 my_means(means),
746 my_aggr(sanisizer::cast<I<decltype(my_aggr.size())> >(my_means.rows()))
747 {}
748
749private:
750 I<decltype(std::declval<IrlbaMatrix_>().new_known_adjoint_workspace())> my_work;
751 const Block_* my_block;
752 const CenterMatrix_& my_means;
753 EigenVector_ my_aggr;
754
755public:
756 void multiply(const EigenVector_& right, EigenVector_& output) {
757 my_work->multiply(right, output);
758
759 my_aggr.setZero();
760 for (I<decltype(right.size())> i = 0, end = right.size(); i < end; ++i) {
761 my_aggr.coeffRef(my_block[i]) += right.coeff(i);
762 }
763
764 output.noalias() -= my_means.adjoint() * my_aggr;
765 }
766};
767
768template<class EigenMatrix_, class IrlbaMatrix_, typename Block_, class CenterMatrix_>
769class ResidualRealizeWorkspace final : public irlba::RealizeWorkspace<EigenMatrix_> {
770public:
771 ResidualRealizeWorkspace(const IrlbaMatrix_& matrix, const Block_* block, const CenterMatrix_& means) :
772 my_work(matrix.new_known_realize_workspace()),
773 my_block(block),
774 my_means(means)
775 {}
776
777private:
778 I<decltype(std::declval<IrlbaMatrix_>().new_known_realize_workspace())> my_work;
779 const Block_* my_block;
780 const CenterMatrix_& my_means;
781
782public:
783 const EigenMatrix_& realize(EigenMatrix_& buffer) {
784 my_work->realize_copy(buffer);
785 for (I<decltype(buffer.rows())> i = 0, end = buffer.rows(); i < end; ++i) {
786 buffer.row(i) -= my_means.row(my_block[i]);
787 }
788 return buffer;
789 }
790};
791
792// This wrapper class mimics multiplication with the residuals,
793// i.e., after subtracting the per-block mean from each cell.
794template<class EigenVector_, class EigenMatrix_, class IrlbaMatrixPointer_, class Block_, class CenterMatrixPointer_>
795class ResidualMatrix final : public irlba::Matrix<EigenVector_, EigenMatrix_> {
796public:
797 ResidualMatrix(IrlbaMatrixPointer_ mat, const Block_* block, CenterMatrixPointer_ means) :
798 my_matrix(std::move(mat)),
799 my_block(block),
800 my_means(std::move(means))
801 {}
802
803public:
804 Eigen::Index rows() const {
805 return my_matrix->rows();
806 }
807
808 Eigen::Index cols() const {
809 return my_matrix->cols();
810 }
811
812private:
813 IrlbaMatrixPointer_ my_matrix;
814 const Block_* my_block;
815 CenterMatrixPointer_ my_means;
816
817public:
818 std::unique_ptr<irlba::Workspace<EigenVector_> > new_workspace() const {
819 return new_known_workspace();
820 }
821
822 std::unique_ptr<irlba::AdjointWorkspace<EigenVector_> > new_adjoint_workspace() const {
823 return new_known_adjoint_workspace();
824 }
825
826 std::unique_ptr<irlba::RealizeWorkspace<EigenMatrix_> > new_realize_workspace() const {
827 return new_known_realize_workspace();
828 }
829
830public:
831 std::unique_ptr<ResidualWorkspace<EigenVector_, decltype(*my_matrix), Block_, decltype(*my_means)> > new_known_workspace() const {
832 return std::make_unique<ResidualWorkspace<EigenVector_, decltype(*my_matrix), Block_, decltype(*my_means)> >(*my_matrix, my_block, *my_means);
833 }
834
835 std::unique_ptr<ResidualAdjointWorkspace<EigenVector_, decltype(*my_matrix), Block_, decltype(*my_means)> > new_known_adjoint_workspace() const {
836 return std::make_unique<ResidualAdjointWorkspace<EigenVector_, decltype(*my_matrix), Block_, decltype(*my_means)> >(*my_matrix, my_block, *my_means);
837 }
838
839 std::unique_ptr<ResidualRealizeWorkspace<EigenMatrix_, decltype(*my_matrix), Block_, decltype(*my_means)> > new_known_realize_workspace() const {
840 return std::make_unique<ResidualRealizeWorkspace<EigenMatrix_, decltype(*my_matrix), Block_, decltype(*my_means)> >(*my_matrix, my_block, *my_means);
841 }
842};
853template<typename EigenMatrix_, typename EigenVector_>
863 EigenMatrix_ components;
864
870 EigenVector_ variance_explained;
871
876 typename EigenVector_::Scalar total_variance = 0;
877
883 EigenMatrix_ rotation;
884
890 EigenMatrix_ center;
891
896 EigenVector_ scale;
897
902};
903
907template<typename Value_, typename Index_, typename Block_, typename EigenMatrix_, class EigenVector_, class SubsetFunction_>
908void blocked_pca_internal(
910 const Block_* block,
911 const BlockedPcaOptions<EigenVector_>& options,
913 SubsetFunction_ subset_fun
914) {
916 const auto block_details = compute_blocking_details<EigenVector_>(mat.ncol(), block, options.block_weight_policy, options.variable_block_weight_parameters);
917
918 const Index_ ngenes = mat.nrow(), ncells = mat.ncol();
919 const auto nblocks = block_details.block_size.size();
920 output.center.resize(
921 sanisizer::cast<I<decltype(output.center.rows())> >(nblocks),
922 sanisizer::cast<I<decltype(output.center.cols())> >(ngenes)
923 );
924 sanisizer::resize(output.scale, ngenes);
925
926 std::unique_ptr<irlba::Matrix<EigenVector_, EigenMatrix_> > ptr;
927 std::function<void(const EigenMatrix_&)> projector;
928
929 if (!options.realize_matrix) {
930 ptr.reset(new irlba_tatami::Transposed<EigenVector_, EigenMatrix_, Value_, Index_, decltype(&mat)>(&mat, options.num_threads));
931 compute_blockwise_mean_and_variance_tatami(mat, block, block_details, output.center, output.scale, options.num_threads);
932
933 projector = [&](const EigenMatrix_& scaled_rotation) -> void {
934 project_matrix_transposed_tatami(mat, output.components, scaled_rotation, options.num_threads);
935 };
936
937 } else if (mat.sparse()) {
938 // 'extracted' contains row-major contents... but we implicitly transpose it to CSC with genes in columns.
940 mat,
941 /* row = */ true,
942 [&]{
944 opt.two_pass = false;
945 opt.num_threads = options.num_threads;
946 return opt;
947 }()
948 );
949
950 // Storing sparse_ptr in the unique pointer should not invalidate the former,
951 // based on a reading of the C++ specification w.r.t. reset();
952 // so we can continue to use it for projection.
953 const auto sparse_ptr = new irlba::ParallelSparseMatrix<
954 EigenVector_,
955 EigenMatrix_,
956 I<decltype(extracted.value)>,
957 I<decltype(extracted.index)>,
958 I<decltype(extracted.pointers)>
959 >(
960 ncells,
961 ngenes,
962 std::move(extracted.value),
963 std::move(extracted.index),
964 std::move(extracted.pointers),
965 true,
966 options.num_threads
967 );
968 ptr.reset(sparse_ptr);
969
970 compute_blockwise_mean_and_variance_realized_sparse(*sparse_ptr, block, block_details, output.center, output.scale, options.num_threads);
971
972 // Make sure to copy sparse_ptr because it doesn't exist outside of this scope.
973 projector = [&,sparse_ptr](const EigenMatrix_& scaled_rotation) -> void {
974 project_matrix_realized_sparse<EigenVector_>(*sparse_ptr, output.components, scaled_rotation, options.num_threads);
975 };
976
977 } else {
978 // Perform an implicit transposition by performing a row-major extraction into a column-major transposed matrix.
979 auto tmp_ptr = std::make_unique<EigenMatrix_>(
980 sanisizer::cast<I<decltype(std::declval<EigenMatrix_>().rows())> >(ncells),
981 sanisizer::cast<I<decltype(std::declval<EigenMatrix_>().cols())> >(ngenes)
982 );
983 static_assert(!EigenMatrix_::IsRowMajor);
984
986 mat,
987 /* row_major = */ true,
988 tmp_ptr->data(),
989 [&]{
990 tatami::ConvertToDenseOptions opt;
991 opt.num_threads = options.num_threads;
992 return opt;
993 }()
994 );
995
996 compute_blockwise_mean_and_variance_realized_dense(*tmp_ptr, block, block_details, output.center, output.scale, options.num_threads);
997 const auto dense_ptr = tmp_ptr.get(); // do this before the move.
998 ptr.reset(new irlba::SimpleMatrix<EigenVector_, EigenMatrix_, decltype(tmp_ptr)>(std::move(tmp_ptr)));
999
1000 // Make sure to copy dense_ptr because it doesn't exist outside of this scope.
1001 projector = [&,dense_ptr](const EigenMatrix_& scaled_rotation) -> void {
1002 output.components.noalias() = (*dense_ptr * scaled_rotation).adjoint();
1003 };
1004 }
1005
1006 output.total_variance = process_scale_vector(options.scale, output.scale);
1007
1008 std::unique_ptr<irlba::Matrix<EigenVector_, EigenMatrix_> > alt;
1009 alt.reset(
1010 new ResidualMatrix<
1011 EigenVector_,
1012 EigenMatrix_,
1013 I<decltype(ptr)>,
1014 Block_,
1015 I<decltype(&(output.center))>
1016 >(
1017 std::move(ptr),
1018 block,
1019 &(output.center)
1020 )
1021 );
1022 ptr.swap(alt);
1023
1024 if (options.scale) {
1025 alt.reset(
1027 EigenVector_,
1028 EigenMatrix_,
1029 I<decltype(ptr)>,
1030 I<decltype(&(output.scale))>
1031 >(
1032 std::move(ptr),
1033 &(output.scale),
1034 /* column = */ true,
1035 /* divide = */ true
1036 )
1037 );
1038 ptr.swap(alt);
1039 }
1040
1041 if (block_details.weighted) {
1042 alt.reset(
1044 EigenVector_,
1045 EigenMatrix_,
1046 I<decltype(ptr)>,
1047 I<decltype(&(block_details.expanded_weights))>
1048 >(
1049 std::move(ptr),
1050 &(block_details.expanded_weights),
1051 /* column = */ false,
1052 /* divide = */ false
1053 )
1054 );
1055 ptr.swap(alt);
1056
1057 output.metrics = irlba::compute(*ptr, options.number, output.components, output.rotation, output.variance_explained, options.irlba_options);
1058
1059 subset_fun(block_details, output.components, output.variance_explained);
1060
1061 EigenMatrix_ tmp;
1062 const auto& scaled_rotation = scale_rotation_matrix(output.rotation, options.scale, output.scale, tmp);
1063 projector(scaled_rotation);
1064
1065 // Subtracting each block's mean from the PCs.
1066 if (options.center_scores_by_block) {
1067 EigenMatrix_ centering = (output.center * scaled_rotation).adjoint();
1068 for (I<decltype(ncells)> c =0 ; c < ncells; ++c) {
1069 output.components.col(c) -= centering.col(block[c]);
1070 }
1071 }
1072
1073 clean_up_projected(output.components, output.variance_explained);
1074 if (!options.transpose) {
1075 output.components.adjointInPlace();
1076 }
1077
1078 } else {
1079 output.metrics = irlba::compute(*ptr, options.number, output.components, output.rotation, output.variance_explained, options.irlba_options);
1080
1081 subset_fun(block_details, output.components, output.variance_explained);
1082
1083 if (options.center_scores_by_block) {
1084 clean_up(mat.ncol(), output.components, output.variance_explained);
1085 if (options.transpose) {
1086 output.components.adjointInPlace();
1087 }
1088
1089 } else {
1090 EigenMatrix_ tmp;
1091 const auto& scaled_rotation = scale_rotation_matrix(output.rotation, options.scale, output.scale, tmp);
1092 projector(scaled_rotation);
1093
1094 clean_up_projected(output.components, output.variance_explained);
1095 if (!options.transpose) {
1096 output.components.adjointInPlace();
1097 }
1098 }
1099 }
1100
1101 if (!options.scale) {
1102 output.scale = EigenVector_();
1103 }
1104}
1160template<typename Value_, typename Index_, typename Block_, typename EigenMatrix_, class EigenVector_>
1163 const Block_* block,
1164 const BlockedPcaOptions<EigenVector_>& options,
1166) {
1167 blocked_pca_internal<Value_, Index_, Block_, EigenMatrix_, EigenVector_>(
1168 mat,
1169 block,
1170 options,
1171 output,
1172 [&](const BlockingDetails<Index_, EigenVector_>&, const EigenMatrix_&, const EigenVector_&) -> void {}
1173 );
1174}
1175
1195template<typename EigenMatrix_ = Eigen::MatrixXd, class EigenVector_ = Eigen::VectorXd, typename Value_, typename Index_, typename Block_>
1198 blocked_pca(mat, block, options, output);
1199 return output;
1200}
1201
1202}
1203
1204#endif
virtual Index_ ncol() const=0
virtual Index_ nrow() const=0
virtual bool prefer_rows() const=0
virtual bool is_sparse() 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 BlockedPcaOptions< EigenVector_ > &options, BlockedPcaResults< EigenMatrix_, EigenVector_ > &output)
Definition blocked_pca.hpp:1161
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)
auto consecutive_extractor(const Matrix< Value_, Index_ > &matrix, const bool row, const Index_ iter_start, const Index_ iter_length, Args_ &&... args)
Options for blocked_pca().
Definition blocked_pca.hpp:34
int number
Definition blocked_pca.hpp:51
irlba::Options< EigenVector_ > irlba_options
Definition blocked_pca.hpp:109
bool transpose
Definition blocked_pca.hpp:65
scran_blocks::VariableWeightParameters variable_block_weight_parameters
Definition blocked_pca.hpp:82
scran_blocks::WeightPolicy block_weight_policy
Definition blocked_pca.hpp:76
bool scale
Definition blocked_pca.hpp:59
bool center_scores_by_block
Definition blocked_pca.hpp:90
bool realize_matrix
Definition blocked_pca.hpp:96
int num_threads
Definition blocked_pca.hpp:104
Results of blocked_pca().
Definition blocked_pca.hpp:854
EigenVector_::Scalar total_variance
Definition blocked_pca.hpp:876
EigenMatrix_ components
Definition blocked_pca.hpp:863
irlba::Metrics metrics
Definition blocked_pca.hpp:901
EigenMatrix_ rotation
Definition blocked_pca.hpp:883
EigenVector_ scale
Definition blocked_pca.hpp:896
EigenMatrix_ center
Definition blocked_pca.hpp:890
EigenVector_ variance_explained
Definition blocked_pca.hpp:870