labelCells.js

import * as gc from "./gc.js";
import * as wasm from "./wasm.js";
import * as utils from "./utils.js";
import { ScranMatrix } from "./ScranMatrix.js";
import * as wa from "wasmarrays.js";
import * as init from "./initializeSparseMatrixFromArrays.js";

/**************************************************
 **************************************************/

/**
 * Wrapper around a labelled reference dataset on the Wasm heap, typically produced by {@linkcode loadLabelCellsReferenceFromBuffers}.
 * @hideconstructor
 */
class LoadedLabelCellsReference {
    #id;
    #reference;

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

    // Internal use only, not documented.
    get reference() {
        return this.#reference;
    }

    /**
     * @return {number} Number of samples in this dataset.
     */
    numberOfSamples() {
        return this.#reference.num_samples();
    }

    /**
     * @return {number} Number of features in this dataset.
     */
    numberOfFeatures() {
        return this.#reference.num_features();
    }

    /**
     * @return {number} Number of labels in this dataset.
     */
    numberOfLabels() {
        return this.#reference.num_labels();
    }

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

/**
 * Load a reference dataset for annotation in {@linkecode labelCells}.
 * The reference should be represented by several files, the contents of which are described in the [**singlepp_loaders** documentation](https://github.com/SingleR-inc/singlepp_loaders).
 * 
 * @param {Uint8Array|Uint8WasmArray} ranks - Buffer containing the Gzipped CSV file containing a matrix of ranks.
 * Each line corresponds to a sample and contains a comma-separated vector of ranks across all features.
 * All lines should contain the same number of entries.
 * This is effectively a row-major matrix where rows are samples and columns are features.
 * (Advanced users may note that this is transposed in C++.) 
 * @param {Uint8Array|Uint8WasmArray} markers - Buffer containing the Gzipped GMT file containing the markers for each pairwise comparison between labels.
 * For `markers`, the GMT format is a tab-separated file with possibly variable numbers of fields for each line.
 * Each line corresponds to a pairwise comparison between labels, defined by the first two fields.
 * The remaining fields should contain indices of marker features (referring to columns of `matrix`) that are upregulated in the first label when compared to the second.
 * Markers should be sorted in order of decreasing strength.
 * @param {Uint8Array|Uint8WasmArray} labels - Buffer containing the Gzipped text file containing the label for each sample.
 * Each line should contain an integer representing a particular label, from `[0, N)` where `N` is the number of unique labels.
 * The number of lines should be equal to the number of rows in `matrix`.
 * The actual names of the labels are usually held elsewhere.
 * 
 * @return {LoadedLabelCellsReference} Object containing the reference dataset.
 */
export function loadLabelCellsReferenceFromBuffers(ranks, markers, labels) {
    var output;
    var matbuf;
    var markbuf;
    var labbuf;

    try {
        matbuf = utils.wasmifyArray(ranks, "Uint8WasmArray");
        markbuf = utils.wasmifyArray(markers, "Uint8WasmArray");
        labbuf = utils.wasmifyArray(labels, "Uint8WasmArray");
        output = gc.call(
            module => module.load_singlepp_reference(labbuf.offset, labbuf.length, markbuf.offset, markbuf.length, matbuf.offset, matbuf.length),
            LoadedLabelCellsReference
        );

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

    } finally {
        utils.free(matbuf);
        utils.free(markbuf);
        utils.free(labbuf);
    }

    return output;
}

/**************************************************
 **************************************************/

/**
 * Wrapper around a built labelled reference dataset on the Wasm heap, typically produced by {@linkcode trainLabelCellsReference}.
 * @hideconstructor
 */
class TrainedLabelCellsReference {
    #id;
    #reference;

    constructor(id, raw, expected_features) {
        this.#id = id;
        this.#reference = raw;
        this.expectedNumberOfFeatures = expected_features;
        return;
    }

    // internal use only.
    get reference() {
        return this.#reference;
    }

    /**
     * @return {number} Number of shared features between the test and reference datasets.
     */
    numberOfFeatures() {
        return this.#reference.num_features();
    }

    /**
     * @return {number} Number of labels in this dataset.
     */
    numberOfLabels() {
        return this.#reference.num_labels();
    }

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

/**
 * @ignore
 */
export function intersectFeatures(testFeatures, referenceFeatures) { // exported only for testing purposes.
    let registry = new Map;

    for (var i = 0; i < testFeatures.length; i++) {
        let id = testFeatures[i];
        if (id !== null && !registry.has(id)) { // first hit gets the preference.
            registry.set(id, i);
        }
    }

    let tkeep = [], rkeep = [];
    for (var i = 0; i < referenceFeatures.length; i++) {
        let id = referenceFeatures[i];
        if (id == null) {
            continue;
        }

        if (!Array.isArray(id)) {
            if (registry.has(id)) {
                tkeep.push(registry.get(id));
                registry.delete(id); // deleting to avoid a future match to the same ID, as the intersection must be unique in its first/second hits.
                rkeep.push(i);
            }

        } else { // otherwise, it's an array of multiple synonymous gene names.
            for (const xid of id) {
                if (registry.has(xid)) {
                    tkeep.push(registry.get(xid));
                    registry.delete(xid);
                    rkeep.push(i);
                    break;
                }
            }
        }
    }

    return { "test": tkeep, "reference": rkeep };
}

/**
 * Train a reference dataset for annotation in {@linkcode labelCells}.
 * The build process involves harmonizing the identities of the features available in the test dataset compared to the reference.
 * Specifically, a feature must be present in both datasets in order to be retained. 
 * Of those features in the intersection, only the `top` markers from each pairwise comparison are ultimately used for classification.
 *
 * Needless to say, `testFeatures` should match up to the rows of the {@linkplain ScranMatrix} that is actually used for annotation in {@linkcode labelCells}.
 *
 * @param {Array} testFeatures - An array of feature identifiers (usually strings) of length equal to the number of rows in the test matrix.
 * Each entry should contain the identifier for the corresponding row of the test matrix.
 * Any `null` entries are considered to be incomparable.
 * @param {LoadedLabelCellsReference} loadedReference - A reference dataset, typically loaded with {@linkcode loadLabelCellsReferenceFromBuffers}.
 * @param {Array} referenceFeatures - An array of feature identifiers (usually strings) of length equal to the number of features in `reference`.
 * Each entry may also be an array of synonymous identifiers, in which case the first identifier that matches to an entry of `features` is used.
 * Contents of `referenceFeatures` are expected to exhibit some overlap with identifiers in `testFeatures`.
 * Any `null` entries are considered to be incomparable.
 * If multiple entries of `referenceFeatures` match to the same feature in `features`, only the first matching entry is used and the rest are ignored.
 * @param {object} [options={}] - Optional parameters.
 * @param {number} [options.top=20] - Number of top marker features to use.
 * These features are taken from each pairwise comparison between labels.
 * @param {?number} [options.numberOfThreads=null] - Number of threads to use.
 * If `null`, defaults to {@linkcode maximumThreads}.
 *
 * @return {TrainedLabelCellsReference} Object containing the built reference dataset.
 */
export function trainLabelCellsReference(testFeatures, loadedReference, referenceFeatures, options = {}) {
    const { top = 20, numberOfThreads = null, ...others } = options;
    utils.checkOtherOptions(others);

    var test_id_buffer;
    var ref_id_buffer;
    var output;
    let nthreads = utils.chooseNumberOfThreads(numberOfThreads);

    if (referenceFeatures.length != loadedReference.numberOfFeatures()) {
        throw new Error("length of 'referenceFeatures' should be equal to the number of features in 'loadedReference'");
    }
    const intersection = intersectFeatures(testFeatures, referenceFeatures);

    try {
        test_id_buffer = utils.wasmifyArray(intersection.test, "Int32WasmArray");
        ref_id_buffer = utils.wasmifyArray(intersection.reference, "Int32WasmArray");
        output = gc.call(
            module => module.train_singlepp_reference(
                test_id_buffer.length,
                test_id_buffer.offset,
                ref_id_buffer.offset,
                loadedReference.reference,
                top,
                nthreads
            ),
            TrainedLabelCellsReference,
            testFeatures.length
        );

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

    } finally {
        utils.free(test_id_buffer);
        utils.free(ref_id_buffer);
    }

    return output;
}

/**************************************************
 **************************************************/

/**
 * Wrapper around the cell labelling results on the Wasm heap, typically produced by {@linkcode labelCells}.
 * @hideconstructor
 */
class LabelCellsResults {
    #id;
    #results;

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

    /**
     * @return {number} Number of labels used in {@linkcode labelCells}.
     */
    numberOfLabels() {
        return this.#results.num_labels();
    }

    /**
     * @return {number} Number of cells that were labelled.
     */
    numberOfCells() {
        return this.#results.num_samples();
    }

    /**
     * @param {object} [options={}] - Optional parameters.
     * @param {boolean|string} [options.copy=true] - Copying mode, see {@linkcode possibleCopy} for details.
     * @return {Int32Array|Int32WasmArray} Array of length equal to the number of cells,
     * containing the index of the best label for each cell.
     */
    predicted(options = {}) {
        const { copy = true, ...others } = options;
        utils.checkOtherOptions(others);
        return utils.possibleCopy(this.#results.best(), copy);
    }

    /**
     * @param {number} i - Index of the cell of interest.
     * @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.
     * This should have the same length as the {@linkcode LabelCellsResults#numberOfLabels numberOfLabels}.
     *
     * @return {Float64Array|Float64WasmArray} Array containing the scores for this cell across all labels.
     * If `buffer` is supplied, the function returns `buffer` if `asTypedArray = false`, or a view on `buffer` if `asTypedArray = true`.
     */
    scoreForCell(i, options = {}) {
        let { asTypedArray = true, buffer = null, ...others } = options;
        utils.checkOtherOptions(others);

        let tmp = null;
        try {
            if (buffer == null) {
                tmp = utils.createFloat64WasmArray(this.#results.num_labels());
                buffer = tmp;
            }
            this.#results.score_for_sample(i, buffer.offset);
        } catch (e) {
            utils.free(tmp);
            throw e;
        }

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

    /**
     * @param {number} i - Index of the label of interest.
     * @param {object} [options={}] - Optional parameters.
     * @param {boolean|string} [options.copy=true] - Copying mode, see {@linkcode possibleCopy} for details.
     * Only used if `buffer` is not supplied.
     * @return {Float64Array|Float64WasmArray} Array containing the scores across all cells for this label.
     */
    scoreForLabel(i, options = {}) {
        const { copy = true, ...others } = options;
        utils.checkOtherOptions(others);
        return utils.possibleCopy(this.#results.score_for_label(i), copy);
    }

    /**
     * @param {object} [options={}] - Optional parameters.
     * @param {boolean|string} [options.copy=true] - Copying mode, see {@linkcode possibleCopy} for details.
     * @return {Float64Array|Float64WasmArray} Array of length equal to the number of cells,
     * containing the difference in scores between the best and second-best labels.
     */
    delta(options = {}) {
        const { copy = true, ...others } = options;
        utils.checkOtherOptions(others);
        return utils.possibleCopy(this.#results.delta(), 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;
        }
    }
}

/**
 * Label cells based on similarity in expression to a reference dataset.
 * This uses the [**SingleR** algorithm](https://github.com/SingleR-inc/singlepp) for cell type annotation.
 *
 * @param {ScranMatrix|Float64WasmArray} x - The count matrix, or log-normalized matrix, containing features in the rows and cells in the columns.
 * If a Float64WasmArray is supplied, it is assumed to contain a column-major dense matrix.
 * @param {BuildLabelledReferenceResults} reference - A built reference dataset, typically generated by {@linkcode buildLabelledReference}.
 * @param {object} [options={}] - Optional parameters.
 * @param {?number} [options.numberOfFeatures=null] - Number of features, used when `x` is a Float64WasmArray.
 * @param {?number} [options.numberOfCells=null] - Number of cells, used when `x` is a Float64WasmArray.
 * @param {number} [options.quantile=0.8] - Quantile on the correlations to use to compute the score for each label.
 * @param {?number} [options.numberOfThreads=null] - Number of threads to use.
 * If `null`, defaults to {@linkcode maximumThreads}.
 *
 * @return {LabelCellsResults} Labelling results for each cell in `x`.
 */
export function labelCells(x, reference, options = {}) {
    const { numberOfFeatures = null, numberOfCells = null, quantile = 0.8, numberOfThreads = null, ...others } = options;
    utils.checkOtherOptions(others);

    var output = null;
    var matbuf;
    var tempmat;
    let nthreads = utils.chooseNumberOfThreads(numberOfThreads);

    try {
        let target;
        if (x instanceof ScranMatrix) {
            target = x.matrix;
        } else if (x instanceof wa.Float64WasmArray) {
            tempmat = init.initializeDenseMatrixFromDenseArray(numberOfFeatures, numberOfCells, x, { forceInteger: false });
            target = tempmat.matrix;
        } else {
            throw new Error("unknown type for 'x'");
        }

        if (target.nrow() != reference.expectedNumberOfFeatures) {
            throw new Error("number of rows in 'x' should be equal to length of 'features' used to build 'reference'");
        }

        output = gc.call(
            module => module.run_singlepp(target, reference.reference, quantile, nthreads),
            LabelCellsResults
        );
    } finally {
        utils.free(matbuf);
        utils.free(tempmat);
    }

    return output;
}

/**************************************************
 **************************************************/

/**
 * Wrapper around integrated reference datasets on the Wasm heap, typically produced by {@linkcode integrateLabelledReferences}.
 * @hideconstructor
 */
class IntegratedLabelCellsReferences {
    #id;
    #integrated;

    constructor(id, raw, expected_features) {
        this.#id = id;
        this.#integrated = raw;
        this.expectedNumberOfFeatures = expected_features;
        return;
    }

    // Internal use only, not documented.
    get integrated() {
        return this.#integrated;
    }

    /**
     * @return {number} Number of reference datasets.
     */
    numberOfReferences() {
        return this.#integrated.num_references();
    }

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

/**
 * Prepare a classifier that integrates multiple reference datasets.
 * This allows users to choose the best label for a test cell based on its classifications in multiple references.
 *
 * @param {Array} testFeatures - An array of feature identifiers (usually strings) of length equal to the number of rows in the test matrix.
 * Each entry should contain a single identifier for the corresponding row of the test matrix.
 * Any `null` entries are considered to be incomparable.
 * If any entries are duplicated, only the first occurrence is used and the rest are ignored.
 * @param {Array} loadedReferences - Array of {@linkplain LoadedLabelCellsReference} objects, typically created with {@linkcode loadLabelCellsReferenceFromBuffers}.
 * @param {Array} referenceFeatures - Array of length equal to `loadedReferences`, 
 * containing arrays of feature identifiers (usually strings) of length equal to the number of features the corresponding entry of `loadedReferences`.
 * Each entry may also be an array of synonymous identifiers, in which case the first identifier that matches to an entry of `testFeatures` is used.
 * Contents of `referenceFeatures` are expected to exhibit some overlap with identifiers in `testFeatures`.
 * Any `null` entries are considered to be incomparable.
 * If multiple entries of `referenceFeatures` match to the same feature in `testFeatures`, only the first matching entry is used and the rest are ignored.
 * @param {Array} trainedReferences - Array of {@linkplain TrainedLabelCellsReference} objects, typically generated by calling {@linkcode trainLabelCellsReference} 
 * on the same `testFeatures` and the corresponding entries of `loadedReferences` and `referenceFeatures`.
 * This should have length equal to that of `loadedReferences`.
 * @param {object} [options={}] - Optional parameters.
 * @param {?number} [options.numberOfThreads=null] - Number of threads to use.
 * If `null`, defaults to {@linkcode maximumThreads}.
 *
 * @return {IntegratedLabelCellsReference} Object containing the integrated references.
 */
export function integrateLabelCellsReferences(testFeatures, loadedReferences, referenceFeatures, trainedReferences, options = {}) {
    const { numberOfThreads = null, ...others } = options;
    utils.checkOtherOptions(others);

    let interlen_arr;
    let test_id_arr = [];
    let test_id_ptr_arr;
    let ref_id_arr = [];
    let ref_id_ptr_arr;
    let loaded_arr;
    let trained_arr;
    let output;
    let nthreads = utils.chooseNumberOfThreads(numberOfThreads);

    // Checking the inputs.
    let nrefs = loadedReferences.length;
    if (referenceFeatures.length != nrefs) {
        throw new Error("'loadedReferences' and 'referenceFeatures' should be of the same length");
    }
    if (trainedReferences.length != nrefs) {
        throw new Error("'loadedReferences' and 'trainedReferences' should be of the same length");
    }
    for (var i = 0; i < nrefs; i++) {
        if (loadedReferences[i].numberOfFeatures() != referenceFeatures[i].length) {
            throw new Error("length of each 'referenceFeatures' should be equal to the number of features in the corresponding 'loadedReferences'");
        }
    }

    try {
        for (var i = 0; i < nrefs; i++) {
            const intersection = intersectFeatures(testFeatures, referenceFeatures[i]);
            test_id_arr.push(utils.wasmifyArray(intersection.test, "Int32WasmArray"));
            ref_id_arr.push(utils.wasmifyArray(intersection.reference, "Int32WasmArray"));
        }

        loaded_arr = utils.createBigUint64WasmArray(nrefs);
        trained_arr = utils.createBigUint64WasmArray(nrefs);
        test_id_ptr_arr = utils.createBigUint64WasmArray(nrefs);
        ref_id_ptr_arr = utils.createBigUint64WasmArray(nrefs);
        interlen_arr = utils.createInt32WasmArray(nrefs);
        {
            let la = loaded_arr.array();
            let ta = trained_arr.array(); 
            let tia = test_id_ptr_arr.array();
            let ria = ref_id_ptr_arr.array();
            let ia = interlen_arr.array();
            for (var i = 0; i < nrefs; i++) {
                la[i] = BigInt(loadedReferences[i].reference.$$.ptr);
                ta[i] = BigInt(trainedReferences[i].reference.$$.ptr);
                tia[i] = BigInt(test_id_arr[i].offset);
                ria[i] = BigInt(ref_id_arr[i].offset);
                ia[i] = test_id_arr[i].length;
            }
        }

        output = gc.call(
            module => module.integrate_singlepp_references(
                nrefs,
                interlen_arr.offset,
                test_id_ptr_arr.offset,
                ref_id_ptr_arr.offset,
                loaded_arr.offset,
                trained_arr.offset,
                nthreads
            ),
            IntegratedLabelCellsReferences,
            testFeatures.length
        );

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

    } finally {
        for (const x of test_id_arr) {
            utils.free(x);
        }
        for (const x of ref_id_arr) {
            utils.free(x);
        }
        utils.free(test_id_ptr_arr);
        utils.free(ref_id_ptr_arr);
        utils.free(loaded_arr);
        utils.free(trained_arr);
    }

    return output;
}

/**************************************************
 **************************************************/

/**
 * Wrapper around the integrated cell labelling results on the Wasm heap, typically produced by {@linkcode integrateLabelCells}.
 * @hideconstructor
 */
class IntegrateLabelCellsResults {
    #id
    #results;

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

    /**
     * @return {number} Number of labels used in {@linkcode integratedLabelCells}.
     */
    numberOfReferences() {
        return this.#results.num_references();
    }

    /**
     * @return {number} Number of cells that were labelled.
     */
    numberOfCells() {
        return this.#results.num_samples();
    }

    /**
     * @param {object} [options={}] - Optional parameters.
     * @param {boolean|string} [options.copy=true] - Copying mode, see {@linkcode possibleCopy} for details.
     *
     * @return {Int32Array|Int32WasmArray} Array of length equal to the number of cells,
     * containing the index of the best reference for each cell.
     */
    predicted(options = {}) {
        const { copy = true, ...others } = options;
        utils.checkOtherOptions(others);
        return utils.possibleCopy(this.#results.best(), copy);
    }

    /**
     * @param {number} i - Index of the cell of interest.
     * @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.
     * This should have the same length as the {@linkcode IntegrateLabelCellsResults#numberOfReferences numberOfReferences}.
     *
     * @return {Float64Array|Float64WasmArray} Array containing the scores for this cell across all labels.
     * If `buffer` is supplied, the function returns `buffer` if `asTypedArray = false`, or a view on `buffer` if `asTypedArray = true`.
     */
    scoreForCell(i, options = {}) {
        let { asTypedArray = true, buffer = null, ...others } = options;
        utils.checkOtherOptions(others);

        let tmp;
        try {
            if (buffer == null) {
                tmp = utils.createFloat64WasmArray(this.#results.num_references());
                buffer = tmp;
            }
            this.#results.score_for_sample(i, buffer.offset);
        } catch (e) {
            utils.free(tmp);
            throw e;
        }
        return utils.toTypedArray(buffer, tmp == null, asTypedArray);
    }

    /**
     * @param {number} i - Index of the reference of interest.
     * @param {object} [options={}] - Optional parameters.
     * @param {boolean|string} [options.copy=true] - Copying mode, see {@linkcode possibleCopy} for details.
     *
     * @return {Float64Array|Float64WasmArray} Array containing the scores across all cells for this label.
     */
    scoreForReference(i, options = {}) {
        const { copy = true, ...others } = options;
        utils.checkOtherOptions(others);
        return utils.possibleCopy(this.#results.score_for_reference(i), copy);
    }

    /**
     * @param {object} [options={}] - Optional parameters.
     * @param {boolean|string} [options.copy=true] - Copying mode, see {@linkcode possibleCopy} for details.
     *
     * @return {Float64Array|Float64WasmArray} Array of length equal to the number of cells,
     * containing the difference in scores between the best and second-best references.
     */
    delta(options = {}) {
        const { copy = true, ...others } = options;
        utils.checkOtherOptions(others);
        return utils.possibleCopy(this.#results.delta(), 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;
        }
    }
}

/**
 * Integrate cell labels across multiple reference datasets.
 *
 * @param {ScranMatrix|Float64WasmArray} x - The count matrix, or log-normalized matrix, containing features in the rows and cells in the columns.
 * If a Float64WasmArray is supplied, it is assumed to contain a column-major dense matrix.
 * @param {IntegratedLabelCellsReferences} integrated - An integrated set of reference datasets, typically generated by {@linkcode integrateLabelCellsReferences}.
 * @param {Array} assigned - An array of length equal to the number of references in `integrated`.
 * This should contain the result of classification of `x` with each individual reference via {@linkcode labelCells}.
 * Each element should be a {@linkplain LabelCellsResults} object; or an Array, TypedArray or Int32WasmArray of length equal to the number of cells in `x`.
 * @param {object} [options={}] - Optional parameters.
 * @param {?number} [options.numberOfFeatures=null] - Number of features, used when `x` is a Float64WasmArray.
 * @param {?number} [options.numberOfCells=null] - Number of cells, used when `x` is a Float64WasmArray.
 * @param {number} [options.quantile=0.8] - Quantile on the correlations to use to compute the score for each label.
 * @param {?number} [options.numberOfThreads=null] - Number of threads to use.
 * If `null`, defaults to {@linkcode maximumThreads}.
 *
 * @return {IntegrateLabelCellsResults} Integrated labelling results for each cell in `x`.
 */
export function integrateLabelCells(x, assigned, integrated, options = {}) {
    const { numberOfFeatures = null, numberOfCells = null, quantile = 0.8, numberOfThreads = null, ...others } = options;
    utils.checkOtherOptions(others);

    let nrefs = integrated.numberOfReferences();
    if (assigned.length != nrefs) {
        throw new Error("length of 'assigned' should be equal to the number of references in 'integrated'");
    }

    let output;
    var matbuf;
    var tempmat;
    let assigned_ptrs;
    let assigned_arrs = new Array(nrefs);
    let nthreads = utils.chooseNumberOfThreads(numberOfThreads);

    try {
        let target;
        if (x instanceof ScranMatrix) {
            target = x.matrix;
        } else if (x instanceof wa.Float64WasmArray) {
            tempmat = init.initializeScranMatrixFromDenseArray(numberOfFeatures, numberOfCells, x, { sparse: false, forceInteger: false });
            target = tempmat.matrix;
        } else {
            throw new Error("unknown type for 'x'");
        }
        if (target.nrow() != integrated.expectedNumberOfFeatures) {
            throw new Error("number of rows in 'x' should be equal to length of 'features' used to build 'reference'");
        }

        assigned_ptrs = utils.createBigUint64WasmArray(nrefs);
        let assigned_ptr_arr = assigned_ptrs.array();
        for (var i = 0; i < assigned.length; i++) {
            let current = assigned[i];
            if (current instanceof LabelCellsResults) {
                current = current.predicted({ copy: "view" });
            }
            if (current.length != x.numberOfColumns()) {
                throw new Error("length of each element in 'assigned' should be equal to number of columns in 'x'");
            }
            assigned_arrs[i] = utils.wasmifyArray(current, "Int32WasmArray");
            assigned_ptr_arr[i] = BigInt(assigned_arrs[i].offset);
        }
    
        output = gc.call(
            module => module.integrate_singlepp(
                target,
                assigned_ptrs.offset,
                integrated.integrated,
                quantile,
                nthreads
            ),
            IntegrateLabelCellsResults
        );

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

    } finally{
        utils.free(assigned_ptrs);
        for (const x of assigned_arrs) {
            utils.free(x);
        }
        utils.free(matbuf);
        utils.free(tempmat);
    }

    return output;
}