DenseMatrix.js

import * as cutils from "./clone-utils.js";

/**
 * Dense matrix of numbers.
 * Not really a Bioconductor-exclusive data structure, but we need this at a minimum for the {@linkplain SummarizedExperiment} to be useful.
 *
 * - {@linkcode NUMBER_OF_ROWS}
 * - {@linkcode NUMBER_OF_COLUMNS}
 * - {@linkcode SLICE_2D}
 * - {@linkcode COMBINE_ROWS}
 * - {@linkcode COMBINE_COLUMNS}
 * - {@linkcode CLONE}
 */
export class DenseMatrix {
    /**
     * @param {number} numberOfRows - Number of rows, duh.
     * @param {number} numberOfColumns - Number of columns.
     * @param {TypedArray} values - 1-dimensional array of the matrix contents.
     * This should have length equal to the product of `numberOfRows` and `numberOfColumns`.
     * @param {Object} [options={}] - Optional parameters.
     * @param {boolean} [options.columnMajor=true] - Whether `values` represents a column-major layout.
     */
    constructor(numberOfRows, numberOfColumns, values, { columnMajor = true } = {}) {
        if (arguments.length == 0) {
            return;
        }

        this._numberOfRows = numberOfRows;
        this._numberOfColumns = numberOfColumns;
        this._values = values;
        this._columnMajor = columnMajor;
        if (numberOfRows * numberOfColumns != values.length) {
            throw new Error("length of 'values' should be equal to the product of 'dimensions'");
        }
    }

    static name = "DenseMatrix";

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

    /**
     * @return {number} Number of rows.
     */
    numberOfRows() {
        return this._numberOfRows;
    }

    /**
     * @return {number} Number of columns.
     */
    numberOfColumns() {
        return this._numberOfColumns;
    }

    /**
     * @return {boolean} Whether the matrix is column-major.
     */
    isColumnMajor() {
        return this._columnMajor;
    }

    /**
     * @return {TypedArray} Matrix contents as a 1-dimensional array.
     */
    values() {
        return this._values;
    }

    #extractor(i, nprimary, nsecondary, allowView, primaryMajor) {
        if (!primaryMajor) {
            let output = new this._values.constructor(nsecondary);
            let offset = i;
            for (var s = 0; s < nsecondary; s++) {
                output[s] = this._values[offset];
                offset += nprimary;
            }
            return output;

        } else {
            let start = i * nsecondary;
            let end = start + nsecondary;
            if (allowView) {
                return this._values.subarray(start, end);
            } else {
                return this._values.slice(start, end);
            }
        }
    }

    /**
     * Retrieve the contents of a particular row.
     *
     * @param {number} i - Index of the row of interest.
     * @param {Object} [options={}] - Optional parameters.
     * @param {boolean} [options.allowView=false] - Whether to allow a view to be returned, if possible.
     *
     * @return {TypedArray} Contents of the row `i`.
     * This may be a view on the array returned by {@linkcode DenseMatrix#values values}, if permitted by the layout.
     */
    row(i, { allowView = false } = {}) {
        return this.#extractor(i, this._numberOfRows, this._numberOfColumns, allowView, !this._columnMajor);
    }

    /**
     * Retrieve the contents of a particular column.
     *
     * @param {number} i - Index of the column of interest.
     * @param {Object} [options={}] - Optional parameters.
     * @param {boolean} [options.allowView=false] - Whether to allow a view to be returned, if possible.
     *
     * @return {TypedArray} Contents of the column `i`.
     * This may be a view on the array returned by {@linkcode DenseMatrix#values values}, if permitted by the layout.
     */
    column(i, { allowView = false } = {}) {
        return this.#extractor(i, this._numberOfColumns, this._numberOfRows, allowView, this._columnMajor);
    }

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

    /**
     * @param {TypedArray} values - 1-dimensional array of matrix contents,
     * of the same length as the array returned by {@linkcode DenseMatrix#values values}.
     * @param {Object} [options={}] - Optional parameters.
     * @param {boolean} [options.inPlace=false] - Whether to mutate this DenseMatrix instance in place.
     * If `false`, a new instance is returned.
     *
     * @return {DenseMatrix} The DenseMatrix after modifying the matrix contents.
     * If `inPlace = true`, this is a reference to the current instance, otherwise a new instance is created and returned.
     */
    setValues(values, { inPlace = false } = {}) {
        if (values.length !== this._values.length) {
            throw new Error("replacement 'values' should have length equal to 'values()'");
        }

        let target = cutils.setterTarget(this, inPlace);
        target._values = values;
        return target;
    }

    /**
     * @param {TypedArray} values - 1-dimensional array of matrix contents,
     * of the same length as the array returned by {@linkcode DenseMatrix#values values}.
     * @return {DenseMatrix} A reference to this DenseMatrix after modifying the matrix contents.
     */
    $setValues(values) {
        return this.setValues(values, { inPlace: true });
    }

    #inserter(i, nprimary, nsecondary, primaryMajor, replacement) {
        if (!primaryMajor) {
            let output = new this._values.constructor(nsecondary);
            let offset = i;
            for (var s = 0; s < nsecondary; s++) {
                this._values[offset] = replacement[s];
                offset += nprimary;
            }
        } else {
            let start = i * nsecondary;
            this._values.set(replacement, start);
        }
    }

    /**
     * @param {number} i - Row index to set.
     * @param {TypedArray} values - Row contents, of length equal to the number of columns in this DenseMatrix.
     * @param {Object} [options={}] - Optional parameters.
     * @param {boolean} [options.inPlace=false] - Whether to mutate this DenseMatrix instance in place.
     * If `false`, a new instance is returned.
     *
     * @return {DenseMatrix} The DenseMatrix after modifying the matrix contents.
     * If `inPlace = true`, this is a reference to the current instance, otherwise a new instance is created and returned.
     */
    setRow(i, values, { inPlace = false } = {}) {
        if (values.length !== this._numberOfColumns) {
            throw new Error("replacement row should have length equal to 'numberOfColumns()'");
        }

        let target = cutils.setterTarget(this, inPlace);
        if (!inPlace) {
            target._values = target._values.slice();
        }

        target.#inserter(i, target._numberOfRows, target._numberOfColumns, !target._columnMajor, values);
        return target;
    }

    /**
     * @param {number} i - Row index to set.
     * @param {TypedArray} values - Row contents, of length equal to the number of columns in this DenseMatrix.
     * @return {DenseMatrix} A reference to this DenseMatrix after modifying the matrix contents.
     */
    $setRow(i, value) {
        return this.setRow(i, value, { inPlace: true });
    }

    /**
     * @param {number} i - Column index to set.
     * @param {TypedArray} values - Column contents, of length equal to the number of rows in this DenseMatrix.
     * @param {Object} [options={}] - Optional parameters.
     * @param {boolean} [options.inPlace=false] - Whether to mutate this DenseMatrix instance in place.
     * If `false`, a new instance is returned.
     *
     * @return {DenseMatrix} The DenseMatrix after modifying the matrix contents.
     * If `inPlace = true`, this is a reference to the current instance, otherwise a new instance is created and returned.
     */
    setColumn(i, values, { inPlace = false } = {}) {
        if (values.length !== this._numberOfRows) {
            throw new Error("replacement column should have length equal to 'numberOfRows()'");
        }

        let target = cutils.setterTarget(this, inPlace);
        if (!inPlace) {
            target._values = target._values.slice();
        }

        target.#inserter(i, target._numberOfColumns, target._numberOfRows, target._columnMajor, values);
        return target;
    }

    /**
     * @param {number} i - Column index to set.
     * @param {TypedArray} values - Column contents, of length equal to the number of columns in this DenseMatrix.
     * @return {DenseMatrix} A reference to this DenseMatrix after modifying the matrix contents.
     */
    $setColumn(i, value) {
        return this.setColumn(i, value, { inPlace: true });
    }

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

    _bioconductor_NUMBER_OF_ROWS() {
        return this.numberOfRows();
    }

    _bioconductor_NUMBER_OF_COLUMNS() {
        return this.numberOfColumns();
    }

    _bioconductor_SLICE_2D(output, rows, columns, {}) {
        let full_rows = (rows === null);
        let is_row_range = (!full_rows && rows.constructor == Object);
        let new_rows = full_rows ? this._numberOfRows : (is_row_range ? rows.end - rows.start : rows.length);
        output._numberOfRows = new_rows;

        let full_columns = (columns === null);
        let is_column_range = (!full_columns && columns.constructor == Object);
        let new_columns = full_columns ? this._numberOfColumns : (is_column_range ? columns.end - columns.start : columns.length);
        output._numberOfColumns = new_columns;

        let new_values = new this._values.constructor(new_rows * new_columns);
        output._values = new_values;

        if (this._columnMajor) {
            this.#primarySlicer(columns, full_columns, is_column_range, this._numberOfColumns, rows, full_rows, is_row_range, this._numberOfRows, new_rows, new_values);
        } else {
            this.#primarySlicer(rows, full_rows, is_row_range, this._numberOfRows, columns, full_columns, is_column_range, this._numberOfColumns, new_columns, new_values);
        }
        output._columnMajor = this._columnMajor;
        return;
    }

    #primarySlicer(primarySlice, fullPrimary, isPrimaryRange, primaryDim, secondarySlice, fullSecondary, isSecondaryRange, inSecondaryDim, outSecondaryDim, outputValues) {
        if (fullPrimary) {
            for (var p = 0; p < primaryDim; p++) {
                this.#secondarySlicer(secondarySlice, fullSecondary, isSecondaryRange, inSecondaryDim, outSecondaryDim, outputValues, p, p);
            }
        } else if (isPrimaryRange) {
            for (var p = primarySlice.start; p < primarySlice.end; p++) {
                this.#secondarySlicer(secondarySlice, fullSecondary, isSecondaryRange, inSecondaryDim, outSecondaryDim, outputValues, p, p - primarySlice.start);
            }
        } else {
            for (var pi = 0; pi < primarySlice.length; pi++) {
                this.#secondarySlicer(secondarySlice, fullSecondary, isSecondaryRange, inSecondaryDim, outSecondaryDim, outputValues, primarySlice[pi], pi);
            }
        }
    }

    #secondarySlicer(secondarySlice, fullSecondary, isSecondaryRange, inSecondaryDim, outSecondaryDim, outputValues, inPrimary, outPrimary) {
        let in_offset = inPrimary * inSecondaryDim;
        let out_offset = outPrimary * outSecondaryDim;

        if (fullSecondary) {
            let view = this._values.subarray(in_offset, in_offset + inSecondaryDim);
            outputValues.set(view, out_offset);
        } else if (isSecondaryRange) {
            for (var s = secondarySlice.start; s < secondarySlice.end; s++) {
                outputValues[out_offset + s - secondarySlice.start] = this._values[in_offset + s];
            }
        } else {
            for (var si = 0; si < secondarySlice.length; si++) {
                outputValues[out_offset + si] = this._values[in_offset + secondarySlice[si]];
            }
        }
    }

    _combiner(objects, primaryFun, secondaryFun, isPrimaryMajor, secondaryName) {
        let num_primary = primaryFun(objects[0]);
        let num_secondary = secondaryFun(objects[0]);
        for (var i = 1; i < objects.length; i++) {
            if (secondaryFun(objects[i]) !== num_secondary) {
                throw new Error("all objects must have the same number of " + secondaryName);
            }
            num_primary += primaryFun(objects[i]);
        }

        let primary_major = isPrimaryMajor(objects[0]);
        let values = new objects[0]._values.constructor(num_primary * num_secondary);

        if (primary_major) {
            let used_primary = 0;
            for (var i = 0; i < objects.length; i++) {
                let current = objects[i];
                let cur_primary = primaryFun(current);
                let out_offset = used_primary * num_secondary;

                if (isPrimaryMajor(current)) {
                    values.set(current._values, out_offset);
                } else {
                    for (var s = 0; s < num_secondary; s++) {
                        let in_offset = s * cur_primary;
                        let out_offset2 = out_offset + s;
                        for (var p = 0; p < cur_primary; p++) {
                            values[out_offset2 + p * num_secondary] = current._values[in_offset + p];
                        }
                    }
                }

                used_primary += cur_primary;
            }
        } else {
            let used_primary = 0;
            for (var i = 0; i < objects.length; i++) {
                let current = objects[i];
                let cur_primary = primaryFun(current);

                if (!isPrimaryMajor(current)) {
                    for (var s = 0; s < num_secondary; s++) {
                        let view_offset = s * cur_primary;
                        let view = current._values.subarray(view_offset, view_offset + cur_primary);
                        values.set(view, used_primary + s * num_primary);
                    }
                } else {
                    for (var p = 0; p < cur_primary; p++) {
                        let in_offset = p * num_secondary;
                        let out_offset = used_primary + p;
                        for (var s = 0; s < num_secondary; s++) {
                            values[out_offset + s * num_primary] = current._values[in_offset + s];
                        }
                    }
                }

                used_primary += cur_primary;
            }
        }

        return { num_primary, num_secondary, values, primary_major };
    }

    _bioconductor_COMBINE_ROWS(output, objects) {
        let combined = this._combiner(objects,
            x => x._numberOfRows,
            x => x._numberOfColumns,
            x => !(x._columnMajor),
            "columns"
        );

        output._numberOfRows = combined.num_primary;
        output._numberOfColumns = combined.num_secondary;
        output._values = combined.values;
        output._columnMajor = !(combined.primary_major);
        return;
    }

    _bioconductor_COMBINE_COLUMNS(output, objects) {
        let combined = this._combiner(objects,
            x => x._numberOfColumns,
            x => x._numberOfRows,
            x => x._columnMajor,
            "rows"
        );

        output._numberOfColumns = combined.num_primary;
        output._numberOfRows = combined.num_secondary;
        output._values = combined.values;
        output._columnMajor = combined.primary_major;
        return;
    }

    _bioconductor_CLONE(output, { deepCopy = true } = {}) {
        output._values = (deepCopy ? this._values.slice() : this._values);
        output._numberOfRows = this._numberOfRows;
        output._numberOfColumns = this._numberOfColumns;
        output._columnMajor = this._columnMajor;
        return;
    }
}