aggregateAcrossCells.js

import * as gc from "./gc.js";
import * as utils from "./utils.js";
import { ScranMatrix } from "./ScranMatrix.js";

/**
 * Wrapper for the cell aggregation results, produced by {@linkcode aggregateAcrossCells}.
 * @hideconstructor
 */
export class AggregateAcrossCellsResults {
    #id;
    #results;

    constructor(id, raw) {
        this.#id = id;
        this.#results = raw;
        return;
    }

    /**
     * @return {number} Number of groups.
     */
    numberOfGroups() {
        return this.#results.num_groups();
    }

    /**
     * @return {number} Number of genes.
     */
    numberOfGenes() {
        return this.#results.num_genes();
    }

    /**
     * @param {?number} group - Index of the group.
     * If a number, it should be non-negative and less than {@linkcode AggregateAcrossCellsResults#numberOfGroups numberOfGroups}.
     * This may also be `null` to obtain values for all groups.
     * @param {object} [options={}] - Optional parameters.
     * @param {(string|boolean)} [options.copy=true] - Copying mode to use when `asMatrix = false`, see {@linkcode possibleCopy} for details.
     *
     * @return {Float64Array|Float64WasmArray}
     * If `group` is a number, an array is returned where each entry corresponds to a gene and contains the summed value across all cells in the specified `group`.
     * If {@linkcode aggregateAcrossCells} was run with `average = true`, the array contains the mean value instead of the sum.
     *
     * If `group = null`, an array is returned containing the concatenation of the arrays for all groups.
     * If `copy = "view"`, the output can be used in {@linkcode ScranMatrix#createDenseMatrix ScranMatrix.createDenseMatrix} to create a {@linkcode ScranMatrix} for input into other functions.
     */
    sums(group, { copy = true } = {}) {
        let vec = (group !== null ? this.#results.group_sums(group) : this.#results.all_sums());
        return utils.possibleCopy(vec, copy);
    }

    /**
     * @param {number} group - Index of the group.
     * This should be non-negative and less than {@linkcode AggregateAcrossCellsResults#numberOfGroups numberOfGroups}.
     * This may also be `null` to obtain values for all groups.
     * @param {object} [options={}] - Optional parameters.
     * @param {(string|boolean)} [options.copy=true] - Copying mode to use when `asMatrix = false`, see {@linkcode possibleCopy} for details.
     *
     * @return {Float64Array|Float64WasmArray}
     * If `group` is a number, an array is returned where each entry corresponds to a gene and contains the number of detected cells in the specified `group`.
     * If {@linkcode aggregateAcrossCells} was run with `average = true`, each value is the proportion of cells with detected expression.
     * 
     * If `group = null`, an array is returned containing the concatenation of the arrays for all groups.
     * If `copy = "view"`, the output can be used in {@linkcode ScranMatrix#createDenseMatrix ScranMatrix.createDenseMatrix} to create a {@linkcode ScranMatrix} for input into other functions.
     */
    detected(group, { copy = true } = {}) {
        let vec = (group !== null ? this.#results.group_detected(group) : this.#results.all_detected());
        return utils.possibleCopy(vec, copy);
    }

    /**
     * @return Frees the memory allocated on the Wasm heap for this object.
     * This invalidates this object and all references to it.
     */
    free() {
        if (this.#results !== null) {
            gc.release(this.#id);
            this.#results = null;
        }
        return;
    }
}

/**
 * Aggregate per-cell expression profiles for each group of cells.
 * This is typically used to summarize data into per-cluster expression profiles for easier inspection.
 *
 * @param {ScranMatrix} x - Some expression matrix, typically containing normalized log-expression values.
 * @param {Int32Array|Int32WasmArray} groups - Array containing group IDs for each cell.
 * This should have length equal to the number of cells and contain all values from 0 to `n - 1` at least once, where `n` is the number of groups.
 * @param {object} [options={}] - Optional parameters.
 * @param {boolean} [options.average=false] - Whether to compute the average expression instead of the sum for each group.
 * Similarly, the proportion of detected expression is reported, rather than the number of detected cells in each group.
 * @param {?number} [options.numberOfThreads=null] - Number of threads to use.
 * If `null`, defaults to {@linkcode maximumThreads}.
 *
 * @return {AggregateAcrossCellsResults} Object containing the aggregation results.
 */
export function aggregateAcrossCells(x, groups, { average = false, numberOfThreads = null } = {}) {
    var group_data;
    var output;
    let nthreads = utils.chooseNumberOfThreads(numberOfThreads);

    try {
        group_data = utils.wasmifyArray(groups, "Int32WasmArray");
        if (group_data.length != x.numberOfColumns()) {
            throw new Error("length of 'groups' should be equal to number of columns in 'x'");
        }

        output = gc.call(
            module => module.aggregate_across_cells(x.matrix, group_data.offset, average, nthreads),
            AggregateAcrossCellsResults 
        );

    } catch (e) {
        utils.free(output);
        throw e;

    } finally {
        utils.free(group_data);
    }

    return output;
}