SingleCellExperiment.js

import { SingleCellExperiment } from "bioconductor";
import { H5Group, H5DataSet } from "./h5.js";
import { readObject, readObjectFile, saveObject } from "./general.js";
import { readRangedSummarizedExperiment, saveRangedSummarizedExperiment } from "./RangedSummarizedExperiment.js"; 

/**
 * A single-cell experiment.
 * @external SingleCellExperiment 
 * @see {@link https://ltla.github.io/bioconductor.js/SingleCellExperiment.html}
 */

/**
 * @param {string} path - Path to the takane-formatted object directory containing the {@link external:SingleCellExperiment SingleCellExperiment}.
 * @param {object} metadata - Takane object metadata, typically generated by calling {@linkcode readObjectFile} on `path`.
 * @param {object} globals - Object containing `fs`, an object satisfying the {@link GlobalFsInterface}; and `h5`, an object satisfying the {@link GlobalH5Interface}.
 * @param {object} [options={}] - Further options.
 * @param {function|boolean} [options.SingleCellExperiment_readReducedDimension=true] - How to read each dimensionality reduction result.
 * If `true`, {@linkcode readObject} is used, while if `false`, the reduced dimensions will be skipped.
 * If a function is provided, it should accept `ncol` (the number of columns in the SingleCellExperiment) as well as `path`, `metadata`, `globals` and `options` (as described above);
 * and should return an object (possibly asynchronously) for which [`NUMBER_OF_ROWS`](https://ltla.github.io/bioconductor.js/global.html#NUMBER_OF_ROWS) is equal to `ncol`. 
 * @param {function|boolean} [options.SingleCellExperiment_readAlternativeExperiment=true] - How to read each alternative experiment.
 * If `true`, {@linkcode readObject} is used, while if `false`, the alternative experiments will be skipped.
 * If a function is provided, it should accept `ncol` (the number of columns in the SingleCellExperiment) as well as `path`, `metadata`, `globals` and `options` (as described above);
 * and should return a {@link external:SummarizedExperiment SummarizedExperiment} (possibly asynchronously)
 * for which [`NUMBER_OF_COLUMNS`](https://ltla.github.io/bioconductor.js/global.html#NUMBER_OF_COLUMNS) is equal to `ncol`. 
 *
 * @return {external:SingleCellExperiment} The single-cell experiment object.
 * @async
 */
export async function readSingleCellExperiment(path, metadata, globals, options = {}) {
    let rse = await readRangedSummarizedExperiment(path, metadata, globals, options);

    let sce = new SingleCellExperiment(
        rse.assays(),
        {
            assayOrder: rse.assayNames(),
            rowRanges: rse.rowRanges(),
            rowData: rse.rowData(),
            columnData: rse.columnData(),
            rowNames: rse.rowNames(),
            columnNames: rse.columnNames(),
            metadata: rse.metadata(),
        }
    );

    if ("main_experiment_name" in metadata.single_cell_experiment) {
        sce.setMainExperimentName(metadata.single_cell_experiment.main_experiment_name, { inPlace: true });
    }

    let read_rd = true;
    if ("SingleCellExperiment_readReducedDimension" in options) {
        read_rd = options.SingleCellExperiment_readReducedDimension;
    }
    if (read_rd !== false) {
        const rdpath = path + "/reduced_dimensions/names.json";
        if (await globals.fs.exists(rdpath)) {
            let names_contents = await globals.fs.get(rdpath, { asBuffer: true });
            const dec = new TextDecoder;
            const reddim_names = JSON.parse(dec.decode(names_contents));
            for (const [i, rname] of Object.entries(reddim_names)) {
                let rdpath = path + "/reduced_dimensions/" + String(i);
                let rdmeta = await readObjectFile(rdpath, globals);
                let currd;
                if (read_rd === true) {
                    currd = await readObject(rdpath, rdmeta, globals, options);
                } else {
                    currd = await read_rd(sce.numberOfColumns(), rdpath, rdmeta, globals, options);
                }
                sce.setReducedDimension(rname, currd, { inPlace: true });
            }
        }
    }

    let read_ae = true;
    if ("SingleCellExperiment_readAlternativeExperiment" in options) {
        read_ae = options.SingleCellExperiment_readAlternativeExperiment;
    }
    if (read_ae !== false) {
        const aepath = path + "/alternative_experiments/names.json";
        if (await globals.fs.exists(aepath)) {
            let names_contents = await globals.fs.get(aepath, { asBuffer: true });
            const dec = new TextDecoder;
            const altexp_names = JSON.parse(dec.decode(names_contents));
            for (const [i, aname] of Object.entries(altexp_names)) {
                let aepath = path + "/alternative_experiments/" + String(i);
                let aemeta = await readObjectFile(aepath, globals);
                let curae;
                if (read_ae === true) {
                    curae = await readObject(aepath, aemeta, globals, options);
                } else {
                    curae = await read_ae(sce.numberOfColumns(), aepath, aemeta, globals, options);
                }
                sce.setAlternativeExperiment(aname, curae, { inPlace: true });
            }
        }
    }

    return sce;
}

/**
 * @param {external:SingleCellExperiment} x - The single-cell experiment.
 * @param {string} path - Path to the directory in which to save `x`.
 * @param {object} globals - Object containing `fs`, an object satisfying the {@link GlobalFsInterface}; and `h5`, an object satisfying the {@link GlobalH5Interface}.
 * @param {object} [options={}] - Further options.
 *
 * @return `x` is stored at `path`.
 * @async
 */
export async function saveSingleCellExperiment(x, path, globals, options = {}) {
    await saveRangedSummarizedExperiment(x, path, globals, options);

    const existing = await readObjectFile(path, globals);
    existing.type = "single_cell_experiment";
    existing.single_cell_experiment = { "version": "1.0" };
    let mexp = x.mainExperimentName();
    if (mexp !== null) {
        existing.single_cell_experiment.main_experiment_name = mexp;
    }
    await globals.fs.write(path + "/OBJECT", JSON.stringify(existing));

    const reddim_names = x.reducedDimensionNames();
    if (reddim_names.length > 0) {
        await globals.fs.mkdir(path + "/reduced_dimensions");
        await globals.fs.write(path + "/reduced_dimensions/names.json", JSON.stringify(reddim_names));
        for (const [i, rname] of Object.entries(reddim_names)) {
            await saveObject(x.reducedDimension(rname), path + "/reduced_dimensions/" + String(i), globals, options);
        }
    }

    const altexp_names = x.alternativeExperimentNames();
    if (altexp_names.length > 0) {
        await globals.fs.mkdir(path + "/alternative_experiments");
        await globals.fs.write(path + "/alternative_experiments/names.json", JSON.stringify(altexp_names));
        for (const [i, aname] of Object.entries(altexp_names)) {
            await saveObject(x.alternativeExperiment(aname), path + "/alternative_experiments/" + String(i), globals, options);
        }
    }
}