scran_aggregate
Aggregate expression values across cells
Loading...
Searching...
No Matches
aggregate_across_genes.hpp
Go to the documentation of this file.
1#ifndef SCRAN_AGGREGATE_AGGREGATE_ACROSS_GENES_HPP
2#define SCRAN_AGGREGATE_AGGREGATE_ACROSS_GENES_HPP
3
4#include <algorithm>
5#include <vector>
6#include <unordered_set>
7#include <stdexcept>
8#include <cstddef>
9
10#include "tatami/tatami.hpp"
11#include "quickstats/quickstats.hpp"
12#include "sanisizer/sanisizer.hpp"
13
14#include "utils.hpp"
15
21namespace scran_aggregate {
22
31 int num_threads = 1;
32
37 bool average = false;
38};
39
44template <typename Sum_>
51 std::vector<Sum_*> sum;
52};
53
59template<typename Gene_, typename Weight_>
65
75 AggregateAcrossGenesSet(std::size_t number, const Gene_* gene, const Weight_* weight) :
77 gene(gene),
79 {}
80
84 std::size_t number = 0;
85
89 const Gene_* gene = NULL;
90
95 const Weight_* weight = NULL;
96};
97
102template <typename Sum_>
109 std::vector<std::vector<Sum_> > sum;
110};
111
115template<typename Data_, typename Index_, typename Gene_, typename Weight_, typename Sum_>
116void aggregate_across_genes_by_column(
118 const std::vector<AggregateAcrossGenesSet<Gene_, Weight_> >& gene_sets,
120 const AggregateAcrossGenesOptions& options
121) {
122 const auto NR = p.nrow();
123 const auto num_sets = gene_sets.size();
124
125 // Identifying the subset of rows that actually need to be extracted.
126 std::vector<Index_> subset;
127 {
129 Index_ used = 0;
130 for (const auto& set : gene_sets) {
131 for (std::size_t i = 0; i < set.number; ++i) {
132 const auto g = set.gene[i];
133 if (g < 0 || sanisizer::is_greater_than_or_equal(g, NR)) {
134 throw std::runtime_error("set indices are out of range");
135 }
136 if (!occupancy[g]) {
137 ++used;
138 occupancy[g] = true;
139 }
140 }
141 }
142
143 subset.reserve(used);
144 for (Index_ r = 0; r < NR; ++r) {
145 if (occupancy[r]) {
146 subset.push_back(r);
147 }
148 }
149 }
150
151 // Remapping the row indices to the subset of genes across all sets.
152 // However, we only do this if the subset of genes does not consist of all genes.
153 // This choice requires some care to produce an alternative to 'gene_sets' with the remapped indices.
154 const std::vector<AggregateAcrossGenesSet<Gene_, Weight_> >* gene_sets_ptr = &gene_sets;
155 std::optional<std::vector<AggregateAcrossGenesSet<Gene_, Weight_> > > remapped_gene_sets;
156
157 class RemappedGeneSetLiberator {
158 public:
159 RemappedGeneSetLiberator(std::optional<std::vector<AggregateAcrossGenesSet<Gene_, Weight_> > >& host) : my_host(host) {}
160 ~RemappedGeneSetLiberator() {
161 // The only purpose of this class is to wipe out the dynamically allocated memory for the remapped indices.
162 if (my_host.has_value()) {
163 for (auto& rset : *my_host) {
164 if (rset.gene) {
165 delete [] rset.gene;
166 }
167 }
168 }
169 }
170 private:
171 std::optional<std::vector<AggregateAcrossGenesSet<Gene_, Weight_> > >& my_host;
172 };
173 RemappedGeneSetLiberator lib(remapped_gene_sets);
174
175 const auto nsubs = subset.size();
176 if (nsubs) {
177 const Index_ offset = subset.front();
178 const Index_ span = subset.back() - offset + 1;
179
180 if (offset || !sanisizer::is_equal(span, nsubs)) { // i.e., not a consecutive block starting at zero.
182 for (I<decltype(nsubs)> i = 0; i < nsubs; ++i) {
183 mapping[subset[i] - offset] = i;
184 }
185
186 remapped_gene_sets.emplace(num_sets); // type is already correct, no need for sanisizer protection.
187 gene_sets_ptr = &(*remapped_gene_sets);
188
189 for (I<decltype(num_sets)> s = 0; s < num_sets; ++s) {
190 const auto& set = gene_sets[s];
191 auto& remapped = (*remapped_gene_sets)[s];
192 remapped.number = set.number;
193 remapped.weight = set.weight;
194
195 const auto rgene = new Gene_ [set.number]; // set.number is already size_t, no need to cast.
196 remapped.gene = rgene; // set it here ASAP to avoid memory leak if there are any exceptions.
197 for (std::size_t g = 0; g < set.number; ++g) {
198 rgene[g] = mapping[set.gene[g] - offset];
199 }
200 }
201 }
202 }
203
204 tatami::parallelize([&](const int, const Index_ start, const Index_ length) -> void {
205 // We extract as dense even if it is sparse, as it's just easier to index from a dense vector.
206 auto ext = [&]{
207 if (nsubs) {
208 const Index_ offset = subset.front();
209 if (offset == 0 && sanisizer::is_equal(nsubs, NR)) {
210 return tatami::consecutive_extractor<false>(p, false, start, length);
211 }
212 const Index_ span = subset.back() - offset + 1;
213 if (sanisizer::is_equal(span, nsubs)) {
214 return tatami::consecutive_extractor<false>(p, false, start, length, offset, span);
215 }
216 }
218 }();
220
221 // Using a pairwise sum for a more-or-less free improvement to accuracy.
222 quickstats::PairwiseSumWorkspace<Sum_> pswrk;
223 quickstats::PairwiseSumOptions psopt;
224
225 for (Index_ x = start, end = start + length; x < end; ++x) {
226 const auto ptr = ext->fetch(vbuffer.data());
227 for (std::size_t s = 0; s < num_sets; ++s) {
228 const auto& set = (*gene_sets_ptr)[s];
229
230 if (set.weight) {
231 buffers.sum[s][x] = quickstats::pairwise_sum_abstract(
232 set.number,
233 [&](std::size_t i) -> Sum_ {
234 return ptr[set.gene[i]] * set.weight[i];
235 },
236 pswrk,
237 psopt
238 );
239 } else {
240 buffers.sum[s][x] = quickstats::pairwise_sum_abstract(
241 set.number,
242 [&](std::size_t i) -> Sum_ {
243 return ptr[set.gene[i]];
244 },
245 pswrk,
246 psopt
247 );
248 }
249 }
250 }
251
252 }, p.ncol(), options.num_threads);
253}
254
255template<typename Data_, typename Index_, typename Gene_, typename Weight_, typename Sum_>
256void aggregate_across_genes_by_row(
258 const std::vector<AggregateAcrossGenesSet<Gene_, Weight_> >& gene_sets,
259 const AggregateAcrossGenesBuffers<Sum_>& buffers,
260 const AggregateAcrossGenesOptions& options
261) {
262 const auto NR = p.nrow();
263 const auto NC = p.ncol();
264 const auto num_sets = gene_sets.size();
265 typedef I<decltype(num_sets)> SetIndex;
266
267 // Identifying the subset of rows that actually need to be extracted.
268 std::vector<Index_> subset;
269 std::vector<std::pair<std::vector<SetIndex>, std::vector<Weight_> > > revmapping;
270 {
272 Index_ used = 0;
273 for (const auto& set : gene_sets) {
274 for (std::size_t i = 0; i < set.number; ++i) {
275 const auto g = set.gene[i];
276 if (g < 0 || sanisizer::is_greater_than_or_equal(g, NR)) {
277 throw std::runtime_error("set indices are out of range");
278 }
279 used += (occupancy[g] == 0);
280 occupancy[g] += 1;
281 }
282 }
283
284 subset.reserve(used);
286 for (Index_ r = 0; r < NR; ++r) {
287 if (occupancy[r]) {
288 auto& revmap_dest = revmapping[subset.size()];
289 revmap_dest.first.reserve(occupancy[r]);
290 revmap_dest.second.reserve(occupancy[r]);
291 subset.push_back(r);
292 }
293 }
294 }
295
296 // Reverse the mapping to get genes->sets.
297 const Index_ nsubs = subset.size();
298 if (nsubs) {
299 const Index_ offset = subset.front();
300 const Index_ span = subset.back() - offset + 1;
301
302 if (!sanisizer::is_equal(span, nsubs)) { // i.e., not a consecutive block.
304 for (I<decltype(nsubs)> i = 0; i < nsubs; ++i) {
305 mapping[subset[i] - offset] = i;
306 }
307
308 for (I<decltype(num_sets)> s = 0; s < num_sets; ++s) {
309 const auto& set = gene_sets[s];
310 if (set.weight) {
311 for (std::size_t g = 0; g < set.number; ++g) {
312 auto& dest = revmapping[mapping[set.gene[g] - offset]];
313 dest.first.push_back(s);
314 dest.second.push_back(set.weight[g]);
315 }
316 } else {
317 for (std::size_t g = 0; g < set.number; ++g) {
318 auto& dest = revmapping[mapping[set.gene[g] - offset]];
319 dest.first.push_back(s);
320 dest.second.push_back(1);
321 }
322 }
323 }
324
325 } else {
326 for (I<decltype(num_sets)> s = 0; s < num_sets; ++s) {
327 const auto& set = gene_sets[s];
328 if (set.weight) {
329 for (std::size_t g = 0; g < set.number; ++g) {
330 auto& dest = revmapping[set.gene[g] - offset];
331 dest.first.push_back(s);
332 dest.second.push_back(set.weight[g]);
333 }
334 } else {
335 for (std::size_t g = 0; g < set.number; ++g) {
336 auto& dest = revmapping[set.gene[g] - offset];
337 dest.first.push_back(s);
338 dest.second.push_back(1);
339 }
340 }
341 }
342 }
343 }
344
345 for (I<decltype(num_sets)> s = 0; s < num_sets; ++s) {
346 std::fill_n(buffers.sum[s], NC, 0);
347 }
348
349 const bool do_parallel = options.num_threads > 1;
350 std::optional<std::vector<std::optional<std::vector<std::vector<Sum_> > > > > per_thread_sums;
351 if (do_parallel) {
352 per_thread_sums.emplace(sanisizer::cast<I<decltype(per_thread_sums->size())> >(options.num_threads - 1));
353 }
354
355 const bool is_sparse = p.is_sparse();
356 const auto nused = tatami::parallelize([&](const int t, const Index_ start, const Index_ length) -> void {
357 auto sub_oracle = std::make_shared<tatami::FixedViewOracle<Index_> >(subset.data() + start, length);
358 std::optional<std::vector<std::vector<Sum_> > > tmp_sums;
359 if (t > 0) {
360 tmp_sums.emplace(sanisizer::cast<I<decltype(tmp_sums->size())> >(num_sets));
361 }
362
363 auto get_output_ptr = [&](SetIndex curset) -> Sum_* {
364 if (t == 0) {
365 return buffers.sum[curset];
366 }
367 // Only allocate each set's memory if we actually need it in the current thread.
368 // Recall that we split the gene subset across multiple threads.
369 // A thread could get a sub-subset where certain gene sets are not represented.
370 // In such cases, there's no point allocating temporary storage for those gene sets in that thread.
371 if ((*tmp_sums)[curset].empty()) {
372 tatami::resize_container_to_Index_size((*tmp_sums)[curset], NC);
373 }
374 return (*tmp_sums)[curset].data();
375 };
376
377 if (is_sparse){
378 auto ext = tatami::new_extractor<true, true>(p, true, std::move(sub_oracle));
381
382 for (Index_ g = 0; g < length; ++g) {
383 const auto range = ext->fetch(vbuffer.data(), ibuffer.data());
384 const auto& cursets = revmapping[start + g];
385 const auto ncursets = cursets.first.size();
386
387 for (I<decltype(ncursets)> s = 0; s < ncursets; ++s) {
388 const auto curset = cursets.first[s];
389 const auto curw = cursets.second[s];
390 const auto outptr = get_output_ptr(curset);
391 if (curw != 1) {
392 for (Index_ i = 0; i < range.number; ++i) {
393 outptr[range.index[i]] += range.value[i] * curw;
394 }
395 } else {
396 for (Index_ i = 0; i < range.number; ++i) {
397 outptr[range.index[i]] += range.value[i];
398 }
399 }
400 }
401 }
402
403 } else {
404 auto ext = tatami::new_extractor<false, true>(p, true, std::move(sub_oracle));
406
407 for (Index_ g = 0; g < length; ++g) {
408 const auto ptr = ext->fetch(vbuffer.data());
409 const auto& cursets = revmapping[start + g];
410 const auto ncursets = cursets.first.size();
411
412 for (I<decltype(ncursets)> s = 0; s < ncursets; ++s) {
413 const auto curset = cursets.first[s];
414 const auto curw = cursets.second[s];
415 const auto outptr = get_output_ptr(curset);
416 if (curw != 1) {
417 for (Index_ c = 0; c < NC; ++c) {
418 outptr[c] += ptr[c] * curw;
419 }
420 } else {
421 for (Index_ c = 0; c < NC; ++c) {
422 outptr[c] += ptr[c];
423 }
424 }
425 }
426 }
427 }
428
429 if (t > 0) {
430 (*per_thread_sums)[t - 1] = std::move(tmp_sums);
431 }
432 }, static_cast<Index_>(nsubs), options.num_threads);
433
434 if (do_parallel) {
435 for (int u = 1; u < nused; ++u) {
436 const auto& thread_sums = *((*per_thread_sums)[u - 1]);
437 for (SetIndex s = 0; s < num_sets; ++s) {
438 const auto& thread_sum = thread_sums[s];
439 if (thread_sum.empty()) {
440 continue;
441 }
442 const auto outptr = buffers.sum[s];
443 for (Index_ c = 0; c < NC; ++c) {
444 outptr[c] += thread_sum[c];
445 }
446 }
447 }
448 }
449}
472template<typename Data_, typename Index_, typename Gene_, typename Weight_, typename Sum_>
475 const std::vector<AggregateAcrossGenesSet<Gene_, Weight_> >& gene_sets,
477 const AggregateAcrossGenesOptions& options
478) {
479 if (input.prefer_rows()) {
480 aggregate_across_genes_by_row(input, gene_sets, buffers, options);
481 } else {
482 aggregate_across_genes_by_column(input, gene_sets, buffers, options);
483 }
484
485 if (options.average) {
486 const auto nsets = gene_sets.size();
487 tatami::parallelize([&](const int, const Index_ start, const Index_ length) -> void {
488 const Index_ NC = input.ncol();
489 quickstats::PairwiseSumWorkspace<Sum_> pswrk;
490 quickstats::PairwiseSumOptions psopt;
491
492 for (Index_ s = start, end = start + length; s < end; ++s) {
493 const auto& set = gene_sets[s];
494 Sum_ denom = 0;
495 if (set.weight) {
496 denom = quickstats::pairwise_sum(set.number, set.weight, pswrk, psopt);
497 } else {
498 denom = set.number;
499 }
500
501 const auto current = buffers.sum[s];
502 for (Index_ c = 0; c < NC; ++c) {
503 current[c] /= denom;
504 }
505 }
506 }, nsets, options.num_threads);
507 }
508}
509
525template<typename Sum_ = double, typename Data_, typename Index_, typename Gene_, typename Weight_>
528 const std::vector<AggregateAcrossGenesSet<Gene_, Weight_> >& gene_sets,
529 const AggregateAcrossGenesOptions& options
530) {
533
534 const Index_ NC = input.ncol();
535 const auto nsets = gene_sets.size();
536 sanisizer::resize(output.sum, nsets);
537 sanisizer::resize(buffers.sum, nsets);
538
539 for (I<decltype(nsets)> s = 0; s < nsets; ++s) {
541 output.sum[s],
542 NC
543#ifdef SCRAN_AGGREGATE_TEST_INIT
544 , SCRAN_AGGREGATE_TEST_INIT
545#endif
546 );
547 buffers.sum[s] = output.sum[s].data();
548 }
549
550 aggregate_across_genes(input, gene_sets, buffers, options);
551 return output;
552}
553
554}
555
556#endif
virtual Index_ ncol() const=0
virtual Index_ nrow() const=0
virtual bool prefer_rows() const=0
virtual bool is_sparse() const=0
Aggregate single-cell expression values.
Definition aggregate_across_cells.hpp:22
void aggregate_across_genes(const tatami::Matrix< Data_, Index_ > &input, const std::vector< AggregateAcrossGenesSet< Gene_, Weight_ > > &gene_sets, const AggregateAcrossGenesBuffers< Sum_ > &buffers, const AggregateAcrossGenesOptions &options)
Definition aggregate_across_genes.hpp:473
auto new_extractor(const Matrix< Value_, Index_ > &matrix, const bool row, MaybeOracle< oracle_, Index_ > oracle, Args_ &&... args)
std::shared_ptr< const std::vector< Index_ > > VectorPtr
void resize_container_to_Index_size(Container_ &container, const Index_ x, Args_ &&... args)
int parallelize(Function_ fun, const Index_ tasks, const int workers)
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)
Buffers for aggregate_across_genes().
Definition aggregate_across_genes.hpp:45
std::vector< Sum_ * > sum
Definition aggregate_across_genes.hpp:51
Options for aggregate_across_genes().
Definition aggregate_across_genes.hpp:26
bool average
Definition aggregate_across_genes.hpp:37
int num_threads
Definition aggregate_across_genes.hpp:31
Results of aggregate_across_genes().
Definition aggregate_across_genes.hpp:103
std::vector< std::vector< Sum_ > > sum
Definition aggregate_across_genes.hpp:109
Gene set to use in aggregate_across_genes().
Definition aggregate_across_genes.hpp:60
const Gene_ * gene
Definition aggregate_across_genes.hpp:89
const Weight_ * weight
Definition aggregate_across_genes.hpp:95
std::size_t number
Definition aggregate_across_genes.hpp:84
AggregateAcrossGenesSet(std::size_t number, const Gene_ *gene, const Weight_ *weight)
Definition aggregate_across_genes.hpp:75