Skip to content

Commit ded0400

Browse files
committed
add parallel AF assignment function to speed up get.af.spectra() in AutoSpectral
1 parent 2000e7c commit ded0400

6 files changed

Lines changed: 196 additions & 0 deletions

File tree

NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Generated by roxygen2: do not edit by hand
22

3+
export(assign.af.fluor.fast)
34
export(fast_kde2d_cpp)
45
export(find_local_maxima)
56
export(poisson_irls_rcpp_parallel)

R/RcppExports.R

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
# Generated by using Rcpp::compileAttributes() -> do not edit by hand
22
# Generator token: 10BE3573-1514-4C36-9D1C-5A225CD40393
33

4+
assign_af_fluor <- function(raw_data, spectra, af_spectra, n_threads = 4L) {
5+
.Call(`_AutoSpectralRcpp_assign_af_fluor`, raw_data, spectra, af_spectra, n_threads)
6+
}
7+
48
#' Fast Kernel Density Estimation in 2D
59
#'
610
#' @param x Numeric vector of x-values (typically FSC-A)

R/assign_af_fluor_fast.R

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# assign_af_fluor_fast.r
2+
3+
#' @title Assign AF Spectrum By Fluorophore Projection Fast
4+
#'
5+
#' @description
6+
#' Projects the autofluorescence spectral variants into fluorophore unmixed
7+
#' space to determine which best fits each cell (event). A fast approximation for
8+
#' brute force sequential unmixing method in early versions of AutoSpectral.
9+
#' Provides essentially identical results to minimization of fluorophore signal
10+
#' (dist0 method). Substantially faster. Use L1 (absolute value) minimization,
11+
#' which works better than the standard L2 (squared error). Wrapper for the C++
12+
#' version, which uses OpenMP for parallel processing.
13+
#'
14+
#' @param raw.data Expression data from raw fcs files. Cells in rows and
15+
#' detectors in columns. Columns should be fluorescent data only and must
16+
#' match the columns in spectra.
17+
#' @param spectra Spectral signatures of fluorophores, normalized between 0
18+
#' and 1, with fluorophores in rows and detectors in columns.
19+
#' @param af.spectra Spectral signatures of autofluorescences, normalized
20+
#' between 0 and 1, with fluorophores in rows and detectors in columns. Prepare
21+
#' using `get.af.spectra`.
22+
#' @param threads Numeric, default is `1`.
23+
#'
24+
#' @return The indices of the best-fitting AF spectrum per cell
25+
#'
26+
#' @export
27+
28+
assign.af.fluor.fast <- function(raw.data, spectra, af.spectra, threads = 1) {
29+
# remove AF if present
30+
if ("AF" %in% rownames(spectra))
31+
spectra <- spectra[rownames(spectra) != "AF", , drop = FALSE]
32+
33+
# Call C++ function
34+
# Ensure inputs are matrices
35+
af.idx <- assign_af_fluor(
36+
as.matrix(raw.data),
37+
as.matrix(spectra),
38+
as.matrix(af.spectra),
39+
n_threads = threads
40+
)
41+
42+
return(af.idx)
43+
}

man/assign.af.fluor.fast.Rd

Lines changed: 34 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/RcppExports.cpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,20 @@ Rcpp::Rostream<true>& Rcpp::Rcout = Rcpp::Rcpp_cout_get();
1111
Rcpp::Rostream<false>& Rcpp::Rcerr = Rcpp::Rcpp_cerr_get();
1212
#endif
1313

14+
// assign_af_fluor
15+
Rcpp::IntegerVector assign_af_fluor(const arma::mat& raw_data, const arma::mat& spectra, const arma::mat& af_spectra, int n_threads);
16+
RcppExport SEXP _AutoSpectralRcpp_assign_af_fluor(SEXP raw_dataSEXP, SEXP spectraSEXP, SEXP af_spectraSEXP, SEXP n_threadsSEXP) {
17+
BEGIN_RCPP
18+
Rcpp::RObject rcpp_result_gen;
19+
Rcpp::RNGScope rcpp_rngScope_gen;
20+
Rcpp::traits::input_parameter< const arma::mat& >::type raw_data(raw_dataSEXP);
21+
Rcpp::traits::input_parameter< const arma::mat& >::type spectra(spectraSEXP);
22+
Rcpp::traits::input_parameter< const arma::mat& >::type af_spectra(af_spectraSEXP);
23+
Rcpp::traits::input_parameter< int >::type n_threads(n_threadsSEXP);
24+
rcpp_result_gen = Rcpp::wrap(assign_af_fluor(raw_data, spectra, af_spectra, n_threads));
25+
return rcpp_result_gen;
26+
END_RCPP
27+
}
1428
// fast_kde2d_cpp
1529
List fast_kde2d_cpp(NumericVector x, NumericVector y, int n, NumericVector h, NumericVector x_limits, NumericVector y_limits);
1630
RcppExport SEXP _AutoSpectralRcpp_fast_kde2d_cpp(SEXP xSEXP, SEXP ySEXP, SEXP nSEXP, SEXP hSEXP, SEXP x_limitsSEXP, SEXP y_limitsSEXP) {
@@ -132,6 +146,7 @@ END_RCPP
132146
}
133147

134148
static const R_CallMethodDef CallEntries[] = {
149+
{"_AutoSpectralRcpp_assign_af_fluor", (DL_FUNC) &_AutoSpectralRcpp_assign_af_fluor, 4},
135150
{"_AutoSpectralRcpp_fast_kde2d_cpp", (DL_FUNC) &_AutoSpectralRcpp_fast_kde2d_cpp, 6},
136151
{"_AutoSpectralRcpp_find_local_maxima", (DL_FUNC) &_AutoSpectralRcpp_find_local_maxima, 2},
137152
{"_AutoSpectralRcpp_optimize_unmix", (DL_FUNC) &_AutoSpectralRcpp_optimize_unmix, 12},

src/assign_af_fluor.cpp

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
#include <RcppArmadillo.h>
2+
#ifdef _OPENMP
3+
#include <omp.h>
4+
#endif
5+
6+
// [[Rcpp::depends(RcppArmadillo)]]
7+
// [[Rcpp::plugins(openmp)]]
8+
9+
using namespace arma;
10+
11+
// [[Rcpp::export]]
12+
Rcpp::IntegerVector assign_af_fluor(const arma::mat& raw_data,
13+
const arma::mat& spectra,
14+
const arma::mat& af_spectra,
15+
int n_threads = 4) {
16+
17+
// Setup dimensions
18+
int n_cells = raw_data.n_rows;
19+
int n_channels = raw_data.n_cols;
20+
int n_fluors = spectra.n_rows;
21+
int n_af = af_spectra.n_rows;
22+
23+
// Pseudoinverse: (S*S')^-1 * S
24+
mat P = solve(spectra * spectra.t(), spectra);
25+
mat S_t = spectra.t();
26+
mat AF_t = af_spectra.t(); // channels x af
27+
28+
// v.library: (n_fluors x n_af) - how much each AF looks like fluors
29+
mat v_library = P * AF_t;
30+
31+
// r.library: (n_channels x n_af) - the residual AF
32+
mat r_library = AF_t - (S_t * v_library);
33+
34+
// Denominator for k calculation: colSums(r.library^2)
35+
vec r_dots(n_af);
36+
for(int j = 0; j < n_af; ++j) {
37+
r_dots[j] = dot(r_library.col(j), r_library.col(j));
38+
}
39+
40+
// Transpose raw_data to make cell access contiguous (column-major memory)
41+
mat Y_t = raw_data.t();
42+
43+
// Output: Best AF index per cell
44+
Rcpp::IntegerVector best_indices(n_cells);
45+
46+
#ifdef _OPENMP
47+
omp_set_num_threads(n_threads);
48+
#endif
49+
50+
// Parallel Cell Loop
51+
#pragma omp parallel
52+
{
53+
// Thread-local buffers to avoid repeated allocations
54+
vec init_fluor(n_fluors);
55+
56+
#pragma omp for schedule(static)
57+
for (int i = 0; i < n_cells; ++i) {
58+
const double* y_ptr = Y_t.colptr(i);
59+
60+
// Step A: Initial unmix for this cell (P * y)
61+
for(int f = 0; f < n_fluors; ++f) {
62+
double sum = 0;
63+
for(int c = 0; c < n_channels; ++c) {
64+
sum += P(f, c) * y_ptr[c];
65+
}
66+
init_fluor[f] = sum;
67+
}
68+
69+
double min_l1_error = datum::inf;
70+
int best_af_val = 0;
71+
72+
// Step B: Iterate through AF variants
73+
for (int j = 0; j < n_af; ++j) {
74+
// Calculate k_i: (y . r_j) / dot(r_j, r_j)
75+
double numerator = 0;
76+
const double* r_ptr = r_library.colptr(j);
77+
for(int c = 0; c < n_channels; ++c) {
78+
numerator += y_ptr[c] * r_ptr[c];
79+
}
80+
double ki = numerator / r_dots[j];
81+
82+
// Calculate L1 error: sum(abs(init_fluor - ki * v_j))
83+
double current_l1 = 0;
84+
const double* v_ptr = v_library.colptr(j);
85+
for(int f = 0; f < n_fluors; ++f) {
86+
current_l1 += std::abs(init_fluor[f] - (ki * v_ptr[f]));
87+
}
88+
89+
if (current_l1 < min_l1_error) {
90+
min_l1_error = current_l1;
91+
best_af_val = j + 1; // R-style 1-indexing
92+
}
93+
}
94+
best_indices[i] = best_af_val;
95+
}
96+
}
97+
98+
return best_indices;
99+
}

0 commit comments

Comments
 (0)