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.
     * This should be non-negative and less than {@linkcode AggregateAcrossCellsResults#numberOfGroups numberOfGroups}.
     * @param {object} [options={}] - Optional parameters.
     * @param {(string|boolean)} [options.copy=true] - Copying mode to use, see {@linkcode possibleCopy} for details.
     *
     * @return {Float64Array|Float64WasmArray} Array of length equal to the number of genes, containing the per-gene sum of values across across all cells in the specified `group`.
     * If `average = true` in {@linkcode aggregateAcrossCells}, each element is the mean value instead.
     */
    groupSums(group, options = {}) {
        const { copy = true, ...others } = options;
        utils.checkOtherOptions(others);
        return utils.possibleCopy(this.#results.group_sums(group), copy);
    }

    /**
     * @param {object} [options={}] - Optional parameters.
     * @param {boolean} [options.asTypedArray=true] - Whether to return a Float64Array.
     * If `false`, a Float64WasmArray is returned instead.
     * @param {?Float64WasmArray} [options.buffer=null] - Buffer in which to store the output.
     * If not `null`, this should have the same length as the product of {@linkcode AggregateAcrossCellsResults#numberOfGenes numberOfGenes} 
     * and {@linkcode AggregateAcrossCellsResults#numberOfGroups numberOfGroups}.
     *
     * @return {Float64Array|Float64WasmArray} Array of length equal to the product of the number of genes and groups.
     * This can be treated as a column-major matrix where the rows are the genes and the columns are the groups,
     * and each element is the sum of values for the corresponding gene in the corresponding group.
     * If `average = true` in {@linkcode aggregateAcrossCells}, each element is the mean value instead.
     * If `buffer` is supplied, the function returns `buffer` if `asTypedArray = false`, or a view on `buffer` if `asTypedArray = true`.
     */
    allSums(options = {}) {
        let { asTypedArray = true, buffer = null, ...others } = options;
        utils.checkOtherOptions(others);
        let tmp = null;

        try {
            if (buffer == null) {
                tmp = utils.createFloat64WasmArray(this.numberOfGenes() * this.numberOfGroups());
                buffer = tmp;
            }
            this.#results.all_sums(buffer.offset);
        } catch (e) {
            utils.free(tmp);
            throw e;
        }

        return utils.toTypedArray(buffer, tmp == null, asTypedArray);
    }

    /**
     * @param {number} group - Index of the group.
     * This should be non-negative and less than {@linkcode AggregateAcrossCellsResults#numberOfGroups numberOfGroups}.
     * @param {object} [options={}] - Optional parameters.
     * @param {(string|boolean)} [options.copy=true] - Copying mode to use, see {@linkcode possibleCopy} for details.
     *
     * @return {Float64Array|Float64WasmArray} Array of length equal to the number of genes, containing the number of cells with detected expression for each gene in the specified `group`.
     * If `average = true` in {@linkcode aggregateAcrossCells}, each element is the proportion of detected cells instead.
     */
    groupDetected(group, options = {}) {
        const { copy = true, ...others } = options;
        utils.checkOtherOptions(others);
        return utils.possibleCopy(this.#results.group_detected(group), copy);
    }

    /**
     * @param {object} [options={}] - Optional parameters.
     * @param {boolean} [options.asTypedArray=true] - Whether to return a Float64Array.
     * If `false`, a Float64WasmArray is returned instead.
     * If not `null`, this should have the same length as the product of {@linkcode AggregateAcrossCellsResults#numberOfGenes numberOfGenes} 
     * and {@linkcode AggregateAcrossCellsResults#numberOfGroups numberOfGroups}.
     *
     * @return {Float64Array|Float64WasmArray} Array of length equal to the product of the number of genes and groups.
     * This can be treated as a column-major matrix where the rows are the genes and the columns are the groups,
     * and each element contains the number of detected cells for the corresponding gene in the corresponding group.
     * If `average = true` in {@linkcode aggregateAcrossCells}, each element is the proportion of detected cells instead.
     * If `buffer` is supplied, the function returns `buffer` if `asTypedArray = false`, or a view on `buffer` if `asTypedArray = true`.
     */
    allDetected(options = {}) {
        let { asTypedArray = true, buffer = null, ...others } = options;
        utils.checkOtherOptions(others);
        let tmp = null;

        try {
            if (buffer == null) {
                tmp = utils.createFloat64WasmArray(this.numberOfGenes() * this.numberOfGroups());
                buffer = tmp;
            }
            this.#results.all_detected(buffer.offset);
        } catch (e) {
            utils.free(tmp);
            throw e;
        }

        return utils.toTypedArray(buffer, tmp == null, asTypedArray);
    }

    /**
     * @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=null] - Whether to compute the average within each group for each statistic.
 * @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, options = {}) {
    const { average = false, numberOfThreads = null, ...others } = options;
    utils.checkOtherOptions(others);
    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;
}