RangedSummarizedExperiment.js

import * as generics from "./AllGenerics.js";
import * as gr from "./GRanges.js";
import * as ggr from "./GroupedGRanges.js";
import * as se from "./SummarizedExperiment.js";
import * as utils from "./utils.js";
import * as cutils from "./clone-utils.js";

/**
 * A RangedSummarizedExperiment is a {@linkplain SummarizedExperiment} subclass where each row represents a genomic interval.
 * As such, it stores an additional {@linkplain GRanges} or {@linkplain GroupedGRanges} of length equal to the number of rows,
 * where each element represents the genomic range(s) for the corresponding row of the SummarizedExperiment.
 *
 * The RangedSummarizedExperiment supports the same set of generics as the {@linkplain SummarizedExperiment}.
 * Each method will call the base method, with the following extensions:
 *
 * - {@linkcode SLICE_2D} will additionally slice the supplied genomic ranges by the desired `rows`.
 * - {@linkcode COMBINE_ROWS} will combine genomic ranges across objects.
 *   If some objects contain a GroupedGRanges and other objects contain GRanges, the latter will be coerced to a GroupedGRanges (where each group contains one range) before combining.
 *   If any object is a base SummarizedExperiment, a GroupedGRanges containing zero-length groups will be automatically constructed to attempt combining.
 * - {@linkcode COMBINE_COLUMNS} will use the genomic ranges from the first object.
 *
 * @extends SummarizedExperiment
 */
export class RangedSummarizedExperiment extends se.SummarizedExperiment {
    #check_rowRanges(x) {
        if (!(x instanceof gr.GRanges) && !(x instanceof ggr.GroupedGRanges)) {
            throw new Error("'rowRanges' should be a 'GRanges' or 'GroupedGRanges' instance");
        }
        if (generics.LENGTH(x) !== this._rowData.numberOfRows()) {
            throw utils.formatLengthError("'rowRanges'", "the number of rows");
        }
    }

    /**
     * @param {Object} assays - Object where keys are the assay names and values are multi-dimensional arrays of experimental data.
     * All arrays should have the same number of rows and columns.
     * @param {?(GRanges|GroupedGRanges)} rowRanges - Genomic ranges corresponding to each row.
     *
     * Alternatively, each row may correspond to a group of genomic ranges.
     *
     * If `null`, a {@linkplain GroupedGRanges} is constructed where each row corresponds to one group of ranges of zero length.
     * @param {Object} [options={}] - Optional parameters, including those used in the {@linkplain SummarizedExperiment} constructor.
     */
    constructor(assays, rowRanges, options = {}) {
        if (arguments.length == 0) {
            super();
            return;
        }

        super(assays, options);

        if (rowRanges === null) {
            rowRanges = ggr.GroupedGRanges.empty(this.numberOfRows());
        } else {
            this.#check_rowRanges(rowRanges);
        }
        this._rowRanges = rowRanges;

        return;
    }

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

    /**
     * @return {GRanges} Genomic ranges corresponding to each row.
     */
    rowRanges() {
        return this._rowRanges;
    }

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

    /**
     * @param {GRanges} value - Genomic ranges corresponding to each row.
     * This should have length equal to the number of rows in this RangedSummarizedExperiment.
     * @param {Object} [options={}] - Optional parameters.
     * @param {boolean} [options.inPlace=false] - Whether to mutate this Annotated instance in place.
     * If `false`, a new instance is returned.
     *
     * @return {RangedSummarizedExperiment} The RangedSummarizedExperiment after modifying its `rowRanges`.
     * If `inPlace = true`, this is a reference to the current instance, otherwise a new instance is created and returned.
     */
    setRowRanges(value, { inPlace = false } = {}) {
        this.#check_rowRanges(value);
        let target = cutils.setterTarget(this, inPlace);
        target._rowRanges = value;
        return target;
    }

    /**
     * @param {GRanges} value - Genomic ranges corresponding to each row.
     * This should have length equal to the number of rows in this RangedSummarizedExperiment.
     * @return {RangedSummarizedExperiment} A reference to this RangedSummarizedExperiment after modifying its `rowRanges`.
     */
    $setRowRanges(value) {
        return this.setRowRanges(value, { inPlace: true });
    }

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

    _bioconductor_SLICE_2D(output, rows, columns, { allowView = false }) {
        super._bioconductor_SLICE_2D(output, rows, columns, { allowView });
        if (rows !== null) {
            output._rowRanges = generics.SLICE(this._rowRanges, rows);
        } else {
            output._rowRanges = this._rowRanges;
        }
    }

    _bioconductor_COMBINE_ROWS(output, objects) {
        super._bioconductor_COMBINE_ROWS(output, objects);

        let collected = [];
        let has_empty = false;
        let has_ggr = false;

        for (var i = 0; i < objects.length; i++) {
            let x = objects[i];
            if (x instanceof RangedSummarizedExperiment) {
                let y = x._rowRanges;
                if (y instanceof ggr.GroupedGRanges) {
                    has_ggr = true;
                }
                collected.push(y);
            } else if (x instanceof se.SummarizedExperiment) {
                has_empty = true;
                collected.push(null);
            } else {
                throw new Error("objects to be combined must be SummarizedExperiments (failing for object " + String(i) + ")");
            }
        }

        // Promoting nulls and GRanges to GroupedGRanges, if necessary.
        if (has_empty || has_ggr) {
            for (var i = 0; i < collected.length; i++) {
                let current = collected[i];

                if (current instanceof gr.GRanges) {
                    let widths = new Int32Array(generics.LENGTH(current));
                    widths.fill(1);

                    let options = { 
                        rangeLengths: widths,
                        names: current.names(),
                        elementMetadata: current.elementMetadata(),
                        metadata: current.metadata()
                    };

                    if (options.names !== null) {
                        current = current.setNames(null);
                    } 

                    if (options.elementMetadata.metadata().size > 0 || options.elementMetadata.numberOfColumns() > 0) {
                        current = current.setElementMetadata(null);
                    }

                    if (options.metadata.size > 0) {
                        current = current.setMetadata(new Map);
                    }

                    collected[i] = new ggr.GroupedGRanges(current, options);

                } else if (current === null){
                    collected[i] = ggr.GroupedGRanges.empty(objects[i].numberOfRows());
                }
            }
        }

        output._rowRanges = generics.COMBINE(collected);

        return;
    }

    _bioconductor_COMBINE_COLUMNS(output, objects) {
        super._bioconductor_COMBINE_COLUMNS(output, objects);

        output._rowRanges = objects[0]._rowRanges;

        return;
    }

    _bioconductor_CLONE(output, { deepCopy }) {
        super._bioconductor_CLONE(output, { deepCopy });

        output._rowRanges = cutils.cloneField(this._rowRanges, deepCopy);

        return;
    }
}