import * as scran from "scran.js";
import * as bioc from "bioconductor";
import * as afile from "./abstract/file.js";
import * as eutils from "./utils/extract.js";
import * as futils from "./utils/features.js";
/**************************
******* Internals ********
**************************/
function load_listData_names(lhandle) {
let ndx = lhandle.findAttribute("names");
if (ndx < 0) {
return null;
}
let nhandle;
let names;
try {
nhandle = lhandle.attribute(ndx);
names = nhandle.values();
} catch(e) {
throw new Error("failed to load listData names; " + e.message);
} finally {
scran.free(nhandle);
}
if (names.length != lhandle.length()) {
throw new Error("expected names to have same length as listData");
}
return names;
}
const acceptable_df_subclasses = { "DFrame": "S4Vectors" };
function load_data_frame(handle) {
check_class(handle, acceptable_df_subclasses, "DFrame");
let columns = {};
let colnames = [];
let lhandle;
try {
lhandle = handle.attribute("listData");
if (!(lhandle instanceof scran.RdsGenericVector)) {
throw new Error("listData slot should be a generic list");
}
colnames = load_listData_names(lhandle);
if (colnames == null) {
throw new Error("expected the listData list to be named");
}
for (var i = 0; i < lhandle.length(); i++) {
let curhandle;
try {
curhandle = lhandle.load(i);
if (curhandle instanceof scran.RdsVector && !(curhandle instanceof scran.RdsGenericVector)) {
let curcol = curhandle.values();
// Expand factors, if we detect them.
if (curhandle.findAttribute("class") >= 0) {
let clshandle;
let levhandle;
try {
clshandle = curhandle.attribute("class");
if (clshandle.values().indexOf("factor") >= 0 && curhandle.findAttribute("levels") >= 0) {
levhandle = curhandle.attribute("levels");
let copy = curcol.slice();
copy.forEach((x, i) => { copy[i] = x - 1 }); // get back to 0-based indices.
curcol = bioc.SLICE(levhandle.values(), copy);
}
} finally {
scran.free(clshandle);
scran.free(levhandle);
}
}
columns[colnames[i]] = curcol;
} else if (curhandle instanceof scran.RdsS4Object && check_acceptable_class(curhandle, acceptable_df_subclasses)) {
// Handle nested DataFrames.
columns[colnames[i]] = load_data_frame(curhandle);
}
} finally {
scran.free(curhandle);
}
}
} catch(e) {
throw new Error("failed to retrieve data from DataFrame's listData; " + e.message);
} finally {
scran.free(lhandle);
}
// Loading the row names.
let rnhandle;
let rownames = null;
try {
rnhandle = handle.attribute("rownames");
if (rnhandle instanceof scran.RdsStringVector) {
rownames = rnhandle.values();
}
} catch(e) {
throw new Error("failed to retrieve row names from DataFrame; " + e.message);
} finally {
scran.free(rnhandle);
}
// Loading the number of rows.
let nrows = null;
if (colnames.length == 0 && rownames == null) {
let nrhandle;
try {
nrhandle = handle.attribute("nrows");
if (!(nrhandle instanceof scran.RdsIntegerVector)) {
throw new Error("expected an integer vector as the 'nrows' slot");
}
let NR = nrhandle.values();
if (NR.length != 1) {
throw new Error("expected an integer vector of length 1 as the 'nrows' slot");
}
nrows = NR[0];
} catch (e) {
throw new Error("failed to retrieve nrows from DataFrame; " + e.message);
} finally {
scran.free(nrhandle);
}
}
return new bioc.DataFrame(columns, { columnOrder: colnames, rowNames: rownames, numberOfRows: nrows });
}
function check_acceptable_class(handle, accepted) {
for (const [k, v] of Object.entries(accepted)) {
if (handle.className() == k && handle.packageName() == v) {
return true;
}
}
return false;
}
function check_class(handle, accepted, base) {
if (!(handle instanceof scran.RdsS4Object)) {
throw new Error("expected an S4 object as the data frame");
}
if (!check_acceptable_class(handle, accepted)) {
throw new Error("object is not a " + base + " or one of its recognized subclasses");
}
}
function extract_NAMES(handle) {
let nidx = handle.findAttribute("NAMES");
if (nidx < 0) {
return null;
}
let nhandle;
let output = null;
try {
nhandle = handle.attribute(nidx);
if (nhandle instanceof scran.RdsStringVector) {
output = nhandle.values();
}
} catch(e) {
throw new Error("failed to extract NAMES; " + e.message);
} finally {
scran.free(nhandle);
}
return output;
}
function extract_features(handle) {
let rowdata;
let names = null;
let rrdx = handle.findAttribute("rowRanges");
if (rrdx < 0) {
// This is a base SummarizedExperiment.
let rhandle;
try {
rhandle = handle.attribute("elementMetadata");
rowdata = load_data_frame(rhandle);
} catch(e) {
throw new Error("failed to extract features from the rowData; " + e.message);
} finally {
scran.free(rhandle);
}
names = extract_NAMES(handle);
} else {
// Everything else is assumed to be an RSE.
let rrhandle;
let output;
try {
rrhandle = handle.attribute(rrdx);
let ehandle = rrhandle.attribute("elementMetadata");
try {
rowdata = load_data_frame(ehandle);
} catch(e) {
throw new Error("failed to extract mcols from the rowRanges; " + e.message);
} finally {
scran.free(ehandle);
}
let pidx = rrhandle.findAttribute("partitioning");
if (pidx < 0) { // if absent, we'll assume it's a GRanges.
let r2handle;
try {
r2handle = rrhandle.attribute("ranges");
names = extract_NAMES(r2handle);
} catch(e) {
throw new Error("failed to extract names from the rowRanges; " + e.message);
} finally {
scran.free(r2handle);
}
} else { // otherwise, it's a GRangesList.
let phandle;
try {
phandle = rrhandle.attribute(pidx);
names = extract_NAMES(phandle);
} catch(e) {
throw new Error("failed to extract names from the rowRanges; " + e.message);
} finally {
scran.free(phandle);
}
}
} catch(e) {
throw new Error("failed to extract features from the rowRanges; " + e.message);
} finally {
scran.free(rrhandle);
}
}
if (names !== null) {
rowdata.$setRowNames(names);
}
return rowdata;
}
function extract_assay_names(handle) {
let output;
let ahandle;
let dhandle;
let lhandle;
try {
ahandle = handle.attribute("assays");
dhandle = ahandle.attribute("data");
lhandle = dhandle.attribute("listData");
output = load_listData_names(lhandle);
if (output == null) {
output = new Array(lhandle.length());
output.fill(null);
}
} catch(e) {
throw new Error("failed to extract assay data; " + e.message);
} finally {
scran.free(ahandle);
scran.free(lhandle);
scran.free(dhandle);
}
return output;
}
function extract_assay(handle, assay, forceInteger) {
let output;
let ahandle;
let dhandle;
let lhandle;
try {
ahandle = handle.attribute("assays");
dhandle = ahandle.attribute("data");
lhandle = dhandle.attribute("listData");
// Choosing the assay index.
let chosen = null;
if (typeof assay == "string") {
let names = load_listData_names(lhandle);
if (assay !== null && names != null) {
for (var n = 0; n < names.length; n++) {
if (names[n] == assay) {
chosen = n;
break;
}
}
}
if (chosen == null) {
throw new Error("no assay named '" + assay + "'");
}
} else {
if (assay >= lhandle.length()) {
throw new Error("assay index " + String(assay) + " out of range");
}
chosen = assay;
}
let xhandle;
try {
xhandle = lhandle.load(chosen);
output = scran.initializeSparseMatrixFromRds(xhandle, { forceInteger });
} catch(e) {
throw new Error("failed to initialize sparse matrix from assay; " + e.message);
} finally {
scran.free(xhandle);
}
} catch(e) {
throw new Error("failed to extract assay data; " + e.message);
} finally {
scran.free(ahandle);
scran.free(lhandle);
scran.free(dhandle);
}
return output;
}
function extract_alt_exps(handle) {
let output = { handles: {}, order: [] };
let indx = handle.findAttribute("int_colData");
if (indx < 0) {
return output;
}
let in_handle;
let inld_handle;
let innn_handle;
let ae_handle;
let aeld_handle;
let aenn_handle;
try {
in_handle = handle.attribute(indx);
let inld_dx = in_handle.findAttribute("listData");
if (inld_dx < 0) {
return output;
}
inld_handle = in_handle.attribute(inld_dx);
let innn_dx = inld_handle.findAttribute("names");
if (innn_dx < 0) {
return output;
}
innn_handle = inld_handle.attribute(innn_dx);
let in_names = innn_handle.values();
let ae_dx = in_names.indexOf("altExps");
if (ae_dx < 0) {
return output;
}
ae_handle = inld_handle.load(ae_dx);
let aeld_dx = ae_handle.findAttribute("listData");
if (aeld_dx < 0) {
return output;
}
aeld_handle = ae_handle.attribute(aeld_dx);
let aenn_dx = aeld_handle.findAttribute("names");
if (aenn_dx < 0) {
return output;
}
aenn_handle = aeld_handle.attribute(aenn_dx);
let ae_names = aenn_handle.values();
for (var i = 0; i < ae_names.length; i++) {
let curhandle;
try {
curhandle = aeld_handle.load(i);
let asehandle = curhandle.attribute("se");
output.handles[ae_names[i]] = asehandle;
output.order.push(ae_names[i]);
check_for_se(asehandle);
} catch (e) {
throw new Error("failed to load alternative Experiment '" + ae_names[i] + "'; " + e.message);
} finally {
scran.free(curhandle);
}
}
} catch(e) {
for (const v of Object.values(output.handles)) {
scran.free(v);
}
throw e;
} finally {
scran.free(aenn_handle);
scran.free(aeld_handle);
scran.free(innn_handle);
scran.free(inld_handle);
scran.free(in_handle);
}
return output;
}
function extract_reduced_dims(handle) {
let output = { handles: {}, order: [] };
let indx = handle.findAttribute("int_colData");
if (indx < 0) {
return output;
}
let in_handle;
let inld_handle;
let innn_handle;
let rd_handle;
let rdld_handle;
let rdnn_handle;
try {
in_handle = handle.attribute(indx);
let inld_dx = in_handle.findAttribute("listData");
if (inld_dx < 0) {
return output;
}
inld_handle = in_handle.attribute(inld_dx);
let innn_dx = inld_handle.findAttribute("names");
if (innn_dx < 0) {
return output;
}
innn_handle = inld_handle.attribute(innn_dx);
let in_names = innn_handle.values();
let rd_dx = in_names.indexOf("reducedDims");
if (rd_dx < 0) {
return output;
}
rd_handle = inld_handle.load(rd_dx);
let rdld_dx = rd_handle.findAttribute("listData");
if (rdld_dx < 0) {
return output;
}
rdld_handle = rd_handle.attribute(rdld_dx);
let rdnn_dx = rdld_handle.findAttribute("names");
if (rdnn_dx < 0) {
return output;
}
rdnn_handle = rdld_handle.attribute(rdnn_dx);
let rd_names = rdnn_handle.values();
for (var i = 0; i < rd_names.length; i++) {
let curhandle;
try {
curhandle = rdld_handle.load(i);
let okay = false;
if (curhandle.type() == "double" && curhandle.findAttribute("dim") >= 0) { // only accepting double-precision matrics.
let dimhandle = curhandle.attribute("dim");
if (dimhandle.length() == 2) {
output.handles[rd_names[i]] = { handle: curhandle, dimensions: dimhandle.values() };
output.order.push(rd_names[i]);
okay = true;
}
}
if (!okay) {
scran.free(curhandle);
}
} catch (e) {
throw new Error("failed to load reduced dimension '" + rd_names[i] + "'; " + e.message);
}
}
} catch(e) {
for (const v of Object.values(output.handles)) {
scran.free(v.handle);
}
throw e;
} finally {
scran.free(rdnn_handle);
scran.free(rdld_handle);
scran.free(innn_handle);
scran.free(inld_handle);
scran.free(in_handle);
}
return output;
}
function check_for_se(handle) {
check_class(handle, {
"SummarizedExperiment": "SummarizedExperiment",
"RangedSummarizedExperiment": "SummarizedExperiment",
"SingleCellExperiment": "SingleCellExperiment",
"SpatialExperiment": "SpatialExperiment"
}, "SummarizedExperiment");
}
const main_experiment_name = "";
/************************
******* Dataset ********
************************/
/**
* Dataset stored as a SummarizedExperiment object (or one of its subclasses) inside an RDS file.
*/
export class SummarizedExperimentDataset {
#rds_file;
#rds_handle;
#se_handle;
#alt_handles;
#alt_handle_order;
#raw_features;
#raw_cells;
#options;
#dump_summary(fun) {
let files = [{ type: "rds", file: fun(this.#rds_file) }];
let options = this.options();
return { files, options };
}
/**
* @param {SimpleFile|string|Uint8Array|File} rdsFile - Contents of a RDS file.
* On browsers, this may be a File object.
* On Node.js, this may also be a string containing a file path.
*/
constructor(rdsFile) {
if (rdsFile instanceof afile.SimpleFile) {
this.#rds_file = rdsFile;
} else {
this.#rds_file = new afile.SimpleFile(rdsFile);
}
this.#options = SummarizedExperimentDataset.defaults();
this.clear();
}
/**
* @return {object} Default options, see {@linkcode SummarizedExperimentDataset#setOptions setOptions} for more details.
*/
static defaults() {
return {
rnaCountAssay: 0,
adtCountAssay: 0,
crisprCountAssay: 0,
rnaExperiment: "",
adtExperiment: "Antibody Capture",
crisprExperiment: "CRISPR Guide Capture",
primaryRnaFeatureIdColumn: null,
primaryAdtFeatureIdColumn: null,
primaryCrisprFeatureIdColumn: null
};
}
/**
* @return {object} Object containing all options used for loading.
*/
options() {
return { ...(this.#options) };
}
/**
* @param {object} options - Optional parameters that affect {@linkcode SummarizedExperimentDataset#load load} (but not {@linkcode SummarizedExperimentDataset#summary summary}).
* @param {string|number} [options.rnaCountAssay] - Name or index of the assay containing the RNA count matrix.
* @param {string|number} [options.adtCountAssay] - Name or index of the assay containing the ADT count matrix.
* @param {string|number} [options.crisprCountAssay] - Name or index of the assay containing the CRISPR count matrix.
* @param {?(string|number)} [options.rnaExperiment] - Name or index of the alternative experiment containing gene expression data.
*
* If `i` is `null` or invalid (e.g., out of range index, unavailable name), it is ignored and no RNA data is assumed to be present.
* If `i` is an empty string, the main experiment is assumed to contain the gene expression data.
* @param {?(string|number)} [options.adtExperiment] - Name or index of the alternative experiment containing ADT data.
*
* If `i` is `null` or invalid (e.g., out of range index, unavailable name), it is ignored and no ADTs are assumed to be present.
* If `i` is an empty string, the main experiment is assumed to contain the ADT data.
* @param {?(string|number)} [options.crisprExperiment] - Name or index of the alternative experiment containing CRISPR guide data.
*
* If `i` is `null` or invalid (e.g., out of range index, unavailable name), it is ignored and no CRISPR guides are assumed to be present.
* If `i` is an empty string, the main experiment is assumed to contain the guide data.
* @param {?(string|number)} [options.primaryRnaFeatureIdColumn] - Name or index of the column of the `features` {@linkplain external:DataFrame DataFrame} that contains the primary feature identifier for gene expression.
*
* If `i` is `null` or invalid (e.g., out of range index, unavailable name), it is ignored and the primary identifier is defined as the existing row names.
* However, if no row names are present in the SummarizedExperiment, no primary identifier is defined.
* @param {?(string|number)} [options.primaryAdtFeatureIdColumn] - Name or index of the column of the `features` {@linkplain external:DataFrame DataFrame} that contains the primary feature identifier for the ADTs.
*
* If `i` is `null` or invalid (e.g., out of range index, unavailable name), it is ignored and the primary identifier is defined as the existing row names.
* However, if no row names are present in the SummarizedExperiment, no primary identifier is defined.
* @param {?(string|number)} [options.primaryCrisprFeatureIdColumn] - Name or index of the column of the `features` {@linkplain external:DataFrame DataFrame} that contains the primary feature identifier for the CRISPR guides.
*
* If `i` is `null` or invalid (e.g., out of range index, unavailable name), it is ignored and the existing row names (if they exist) are used as the primary identifier.
* However, if no row names are present in the SummarizedExperiment, no primary identifier is defined.
*/
setOptions(options) {
for (const [k, v] of Object.entries(options)) {
this.#options[k] = v;
}
}
/**
* Destroy caches if present, releasing the associated memory.
* This may be called at any time but only has an effect if `cache = true` in {@linkcode SummarizedExperimentDataset#load load} or {@linkcodeSummarizedExperimentDataset#summary summary}.
*/
clear() {
scran.free(this.#se_handle);
if (typeof this.#alt_handles != 'undefined' && this.#alt_handles !== null) {
for (const v of Object.values(this.#alt_handles)) {
scran.free(v);
}
}
scran.free(this.#rds_handle);
this.#se_handle = null;
this.#alt_handles = null;
this.#rds_handle = null;
this.#raw_features = null;
this.#raw_cells = null;
}
/**
* @return {string} Format of this dataset class.
* @static
*/
static format() {
return "SummarizedExperiment";
}
/**
* @return {object} Object containing the abbreviated details of this dataset.
*/
abbreviate() {
return this.#dump_summary(f => { return { name: f.name(), size: f.size() }; });
}
#initialize() {
if (this.#rds_handle !== null) {
return;
}
this.#rds_handle = scran.readRds(this.#rds_file.content());
this.#se_handle = this.#rds_handle.value();
try {
check_for_se(this.#se_handle);
const { handles, order } = extract_alt_exps(this.#se_handle);
this.#alt_handles = handles;
this.#alt_handle_order = order;
} catch (e) {
this.#se_handle.free();
this.#rds_handle.free();
throw e;
}
}
#features() {
if (this.#raw_features !== null) {
return;
}
this.#initialize();
this.#raw_features = {};
this.#raw_features[main_experiment_name] = extract_features(this.#se_handle);
for (const [k, v] of Object.entries(this.#alt_handles)) {
try {
this.#raw_features[k] = extract_features(v);
} catch (e) {
console.warn("failed to extract features for alternative Experiment '" + k + "'; " + e.message);
}
}
return;
}
#cells() {
if (this.#raw_cells !== null) {
return;
}
this.#initialize();
let chandle = this.#se_handle.attribute("colData");
try {
this.#raw_cells = load_data_frame(chandle);
} catch(e) {
throw new Error("failed to extract colData from a SummarizedExperiment; " + e.message);
} finally {
scran.free(chandle);
}
return;
}
/**
* @param {object} [options={}] - Optional parameters.
* @param {boolean} [options.cache=false] - Whether to cache the intermediate results for re-use in subsequent calls to any methods with a `cache` option.
* If `true`, users should consider calling {@linkcode SummarizedExperimentDataset#clear clear} to release the memory once this dataset instance is no longer needed.
*
* @return {object} Object containing the per-feature and per-cell annotations.
* This has the following properties:
*
* - `modality_features`: an object where each key is a modality name and each value is a {@linkplain external:DataFrame DataFrame} of per-feature annotations for that modality.
* - `cells`: a {@linkplain external:DataFrame DataFrame} of per-cell annotations.
* - `modality_assay_names`: an object where each key is a modality name and each value is an Array containing the names of available assays for that modality.
* Unnamed assays are represented as `null` names.
*/
summary({ cache = false } = {}) {
this.#initialize();
this.#features();
this.#cells();
let assays = {};
assays[main_experiment_name] = extract_assay_names(this.#se_handle);
for (const [k, v] of Object.entries(this.#alt_handles)) {
try {
assays[k] = extract_assay_names(v);
} catch (e) {
console.warn("failed to extract features for alternative Experiment '" + k + "'; " + e.message);
}
}
let output = {
modality_features: this.#raw_features,
cells: this.#raw_cells,
modality_assay_names: assays
};
if (!cache) {
this.clear();
}
return output;
}
#primary_mapping() {
return {
RNA: this.#options.primaryRnaFeatureIdColumn,
ADT: this.#options.primaryAdtFeatureIdColumn,
CRISPR: this.#options.primaryCrisprFeatureIdColumn
};
}
/**
* @param {object} [options={}] - Optional parameters.
* @param {boolean} [options.cache=false] - Whether to cache the intermediate results for re-use in subsequent calls to any methods with a `cache` option.
* If `true`, users should consider calling {@linkcode SummarizedExperimentDataset#clear clear} to release the memory once this dataset instance is no longer needed.
*
* @return {object} An object where each key is a modality name and each value is an array (usually of strings) containing the primary feature identifiers for each row in that modality.
* The contents are the same as the `primary_ids` returned by {@linkcode SummarizedExperimentDataset#load load} but the order of values may be different.
*/
previewPrimaryIds({ cache = false } = {}) {
this.#features();
let fmapping = {
RNA: this.#options.rnaExperiment,
ADT: this.#options.adtExperiment,
CRISPR: this.#options.crisprExperiment
};
let preview = futils.extractRemappedPrimaryIds(this.#raw_features, fmapping, this.#primary_mapping());
if (!cache) {
this.clear();
}
return preview;
}
/**
* @param {object} [options={}] - Optional parameters.
* @param {boolean} [options.cache=false] - Whether to cache the intermediate results for re-use in subsequent calls to any methods with a `cache` option.
* If `true`, users should consider calling {@linkcode SummarizedExperimentDataset#clear clear} to release the memory once this dataset instance is no longer needed.
*
* @return {object} Object containing the per-feature and per-cell annotations.
* This has the following properties:
*
* - `features`: an object where each key is a modality name and each value is a {@linkplain external:DataFrame DataFrame} of per-feature annotations for that modality.
* - `cells`: a {@linkplain external:DataFrame DataFrame} containing per-cell annotations.
* - `matrix`: a {@linkplain external:MultiMatrix MultiMatrix} containing one {@linkplain external:ScranMatrix ScranMatrix} per modality.
* - `primary_ids`: an object where each key is a modality name and each value is an array (usually of strings) containing the primary feature identifiers for each row in that modality.
*
* Modality names are guaranteed to be one of `"RNA"`, `"ADT"` or `"CRISPR"`.
* We assume that the instance already contains an appropriate mapping from the observed feature types to each expected modality,
* either from the {@linkcode SummarizedExperimentDataset#defaults defaults} or with {@linkcode SummarizedExperimentDataset#setOptions setOptions}.
*/
load({ cache = false } = {}) {
this.#initialize();
this.#features();
this.#cells();
let output = {
matrix: new scran.MultiMatrix,
features: {},
cells: this.#raw_cells
};
let mapping = {
RNA: { exp: this.#options.rnaExperiment, assay: this.#options.rnaCountAssay },
ADT: { exp: this.#options.adtExperiment, assay: this.#options.adtCountAssay },
CRISPR: { exp: this.#options.crisprExperiment, assay: this.#options.crisprCountAssay }
};
try {
for (const [k, v] of Object.entries(mapping)) {
if (v.exp === null) {
continue;
}
let handle;
let name = v.exp;
if (typeof v.exp == "string") {
if (v.exp === "") {
handle = this.#se_handle;
} else {
if (!(v.exp in this.#alt_handles)) {
continue;
}
handle = this.#alt_handles[v.exp];
}
} else {
if (v.exp >= this.#alt_handle_order.length) {
continue;
}
name = this.#alt_handle_order[v.exp];
handle = this.#alt_handles[name];
}
let loaded = extract_assay(handle, v.assay, true);
output.matrix.add(k, loaded);
output.features[k] = this.#raw_features[name];
}
output.primary_ids = futils.extractPrimaryIds(output.features, this.#primary_mapping());
} catch (e) {
scran.free(output.matrix);
throw e;
}
if (!cache) {
this.clear();
}
return output;
}
/**
* @return {object} Object describing this dataset, containing:
*
* - `files`: Array of objects representing the files used in this dataset.
* Each object corresponds to a single file and contains:
* - `type`: a string denoting the type.
* - `file`: a {@linkplain SimpleFile} object representing the file contents.
* - `options`: An object containing additional options to saved.
*/
serialize() {
return this.#dump_summary(f => f);
}
/**
* @param {Array} files - Array of objects like that produced by {@linkcode SummarizedExperimentDataset#serialize serialize}.
* @param {object} options - Object containing additional options to be passed to the constructor.
* @return {SummarizedExperimentDataset} A new instance of this class.
* @static
*/
static async unserialize(files, options) {
if (files.length != 1 || files[0].type != "rds") {
throw new Error("expected exactly one file of type 'rds' for SummarizedExperiment unserialization");
}
let output = new SummarizedExperimentDataset(files[0].file);
output.setOptions(output);
return output;
}
}
/***********************
******* Result ********
***********************/
/**
* Pre-computed analysis results stored as a SummarizedExperiment object (or one of its subclasses) inside an RDS file.
*/
export class SummarizedExperimentResult {
#rds_file;
#rds_handle;
#se_handle;
#alt_handles;
#alt_handle_order;
#raw_features;
#raw_cells;
#rd_handles;
#rd_handle_order;
#options;
/**
* @param {SimpleFile|string|Uint8Array|File} rdsFile - Contents of a RDS file.
* On browsers, this may be a File object.
* On Node.js, this may also be a string containing a file path.
*/
constructor(rdsFile) {
if (rdsFile instanceof afile.SimpleFile) {
this.#rds_file = rdsFile;
} else {
this.#rds_file = new afile.SimpleFile(rdsFile);
}
// Cloning to avoid pass-by-reference links.
this.#options = SummarizedExperimentResult.defaults();
this.clear();
}
/**
* @return {object} Default options, see {@linkcode AbstractArtifactdbResults#setOptions setOptions} for more details.
*/
static defaults() {
return {
primaryAssay: 0,
isPrimaryNormalized: true,
reducedDimensionNames: null
};
}
/**
* @return {object} Object containing all options used for loading.
*/
options() {
return { ...(this.#options) };
}
/**
* @param {object} options - Optional parameters that affect {@linkcode AbstractArtifactdbResult#load load} (but not {@linkcode AbstractArtifactdbResult#summary summary}).
* @param {object|string|number} [options.primaryAssay] - Assay containing the relevant data for each modality.
*
* - If a string, this is used as the name of the assay across all modalities.
* - If a number, this is used as the index of the assay across all modalities.
* - If any object, the key should be the name of a modality and the value may be either a string or number specifying the assay to use for that modality.
* Modalities absent from this object will not be loaded.
* @param {object|boolean} [options.isPrimaryNormalized] - Whether or not the assay for a particular modality has already been normalized.
*
* - If a boolean, this is used to indicate normalization status of assays across all modalities.
* If `false`, that modality's assay is assumed to contain count data and is subjected to library size normalization.
* - If any object, the key should be the name of a modality and the value should be a boolean indicating whether that modality's assay has been normalized.
* Modalities absent from this object are assumed to have been normalized.
* @param {?Array} [options.reducedDimensionNames] - Array of names of the reduced dimensions to load.
* If `null`, all reduced dimensions found in the file are loaded.
*/
setOptions(options) {
// Cloning to avoid pass-by-reference links.
for (const [k, v] of Object.entries(options)) {
this.#options[k] = bioc.CLONE(v);
}
}
/**
* Destroy caches if present, releasing the associated memory.
* This may be called at any time but only has an effect if `cache = true` in {@linkcode SummarizedExperimentResult#load load} or {@linkcodeSummarizedExperimentResult#summary summary}.
*/
clear() {
scran.free(this.#se_handle);
if (typeof this.#alt_handles != 'undefined' && this.#alt_handles !== null) {
for (const v of Object.values(this.#alt_handles)) {
scran.free(v);
}
}
if (typeof this.#rd_handles != 'undefined' && this.#rd_handles !== null) {
for (const v of Object.values(this.#rd_handles)) {
scran.free(v.handle);
}
}
scran.free(this.#rds_handle);
this.#se_handle = null;
this.#alt_handles = null;
this.#rds_handle = null;
this.#raw_features = null;
this.#raw_cells = null;
}
#initialize() {
if (this.#rds_handle !== null) {
return;
}
this.#rds_handle = scran.readRds(this.#rds_file.content());
this.#se_handle = this.#rds_handle.value();
try {
check_for_se(this.#se_handle);
{
const { handles, order } = extract_alt_exps(this.#se_handle);
this.#alt_handles = handles;
this.#alt_handle_order = order;
}
{
const { handles, order } = extract_reduced_dims(this.#se_handle);
this.#rd_handles = handles;
this.#rd_handle_order = order;
}
} catch (e) {
this.#se_handle.free();
this.#rds_handle.free();
throw e;
}
}
#features() {
if (this.#raw_features !== null) {
return;
}
this.#initialize();
this.#raw_features = {};
this.#raw_features[main_experiment_name] = extract_features(this.#se_handle);
for (const [k, v] of Object.entries(this.#alt_handles)) {
try {
this.#raw_features[k] = extract_features(v);
} catch (e) {
console.warn("failed to extract features for alternative Experiment '" + k + "'; " + e.message);
}
}
return;
}
#cells() {
if (this.#raw_cells !== null) {
return;
}
this.#initialize();
let chandle = this.#se_handle.attribute("colData");
try {
this.#raw_cells = load_data_frame(chandle);
} catch(e) {
throw new Error("failed to extract colData from a SummarizedExperiment; " + e.message);
} finally {
scran.free(chandle);
}
return;
}
/**
* @param {object} [options={}] - Optional parameters.
* @param {boolean} [options.cache=false] - Whether to cache the results for re-use in subsequent calls to this method or {@linkcode SummarizedExperimentResult#load load}.
* If `true`, users should consider calling {@linkcode SummarizedExperimentResult#clear clear} to release the memory once this dataset instance is no longer needed.
*
* @return {object} Object containing the per-feature and per-cell annotations.
* This has the following properties:
*
* - `modality_features`: an object where each key is a modality name and each value is a {@linkplain external:DataFrame DataFrame} of per-feature annotations for that modality.
* - `cells`: a {@linkplain external:DataFrame DataFrame} of per-cell annotations.
* - `modality_assay_names`: an object where each key is a modality name and each value is an Array containing the names of available assays for that modality.
* Unnamed assays are represented as `null` names.
* - `reduced_dimension_names`: an Array of strings containing names of dimensionality reduction results.
*/
summary({ cache = false } = {}) {
this.#initialize();
this.#features();
this.#cells();
let assays = {};
assays[main_experiment_name] = extract_assay_names(this.#se_handle);
for (const [k, v] of Object.entries(this.#alt_handles)) {
try {
assays[k] = extract_assay_names(v);
} catch (e) {
console.warn("failed to extract features for alternative Experiment '" + k + "'; " + e.message);
}
}
let output = {
modality_features: this.#raw_features,
cells: this.#raw_cells,
modality_assay_names: assays,
reduced_dimension_names: this.#rd_handle_order
};
if (!cache) {
this.clear();
}
return output;
}
/**
* @param {object} [options={}] - Optional parameters.
* @param {boolean} [options.cache=false] - Whether to cache the results for re-use in subsequent calls to this method or {@linkcode SummarizedExperimentResult#summary summary}.
* If `true`, users should consider calling {@linkcode SummarizedExperimentResult#clear clear} to release the memory once this dataset instance is no longer needed.
*
* @return {object} Object containing the per-feature and per-cell annotations.
* This has the following properties:
*
* - `features`: an object where each key is a modality name and each value is a {@linkplain external:DataFrame DataFrame} of per-feature annotations for that modality.
* - `cells`: a {@linkplain external:DataFrame DataFrame} containing per-cell annotations.
* - `matrix`: a {@linkplain external:MultiMatrix MultiMatrix} containing one {@linkplain external:ScranMatrix ScranMatrix} per modality.
* - `reduced_dimensions`: an object containing the dimensionality reduction results.
* Each value is an array of arrays, where each inner array contains the coordinates for one dimension.
*/
load({ cache = false } = {}) {
this.#initialize();
this.#features();
this.#cells();
let output = {
matrix: new scran.MultiMatrix,
features: {},
cells: this.#raw_cells,
reduced_dimensions: {}
};
// Fetch the reduced dimensions first.
let reddims = this.#options.reducedDimensionNames;
if (reddims == null) {
reddims = this.#rd_handle_order;
}
for (const k of reddims) {
let v = this.#rd_handles[k];
let acquired = [];
let dims = v.dimensions;
let contents = v.handle.values();
for (var d = 0; d < dims[1]; d++) {
acquired.push(contents.slice(d * dims[0], (d + 1) * dims[0]));
}
output.reduced_dimensions[k] = acquired;
}
// Now fetching the assay matrix.
try {
for (const [k, v] of Object.entries(this.#raw_features)) {
let curassay = this.#options.primaryAssay;
if (typeof curassay == "object") {
if (k in curassay) {
curassay = curassay[k];
} else {
continue;
}
}
let curnormalized = this.#options.isPrimaryNormalized;
if (typeof curnormalized == "object") {
if (k in curnormalized) {
curnormalized = curnormalized[k];
} else {
curnormalized = true;
}
}
let handle;
if (k === "") {
handle = this.#se_handle;
} else {
handle = this.#alt_handles[k];
}
let loaded = extract_assay(handle, curassay, !curnormalized);
output.matrix.add(k, loaded);
if (!curnormalized) {
let normed = scran.logNormCounts(loaded, { allowZeros: true });
output.matrix.add(k, normed);
}
output.features[k] = this.#raw_features[k];
}
} catch (e) {
scran.free(output.matrix);
throw e;
}
if (!cache) {
this.clear();
}
return output;
}
}