bakana
Version:
Backend for kana's single-cell analyses. This supports single or multiple samples, execution in Node.js or the browser, in-memory caching of results for iterative analyses, and serialization to/from file for redistribution.
236 lines (204 loc) • 6.49 kB
JavaScript
import * as scran from "scran.js";
export function freeCache(object) {
// Just an alias for back-compatibility.
scran.free(object);
return;
}
export function defaultizeParameters(parameters, defaults, extra = []) {
let output = defaults;
for (const [name, val] of Object.entries(parameters)) {
if (name in defaults || extra.indexOf(name) >= 0) {
output[name] = val;
} else {
throw new Error("unknown parameter '" + name + "'");
}
}
return output;
}
function changedParametersIllegal(x, y, xskip, yskip) {
// Failing if this is a TypedArray or ArrayBuffer;
// we shouldn't be seeing these things here anyway.
if (!xskip) {
if (x instanceof ArrayBuffer || ArrayBuffer.isView(x)) {
throw new Error("parameters cannot contain ArrayBuffers or their views");
}
}
if (!yskip) {
if (y instanceof ArrayBuffer || ArrayBuffer.isView(y)) {
throw new Error("parameters cannot contain ArrayBuffers or their views");
}
}
}
export function changedParameters(x, y) {
if (typeof x != typeof y) {
changedParametersIllegal(x, y, false, false);
return true;
} else if (typeof x != "object") {
return x != y;
}
//Handling nulls (which are objects).
let xnull = x === null;
let ynull = y === null;
if (xnull !== ynull) {
changedParametersIllegal(x, y, xnull, ynull);
return true;
} else if (xnull) {
return false;
}
// Handling arrays (which are also objects).
let xarr = x instanceof Array;
let yarr = y instanceof Array;
if (xarr != yarr) {
changedParametersIllegal(x, y, xarr, yarr);
return true;
} else if (xarr) {
if (x.length != y.length) {
return true;
}
for (var i = 0; i < x.length; i++) {
if (changedParameters(x[i], y[i])) {
return true;
}
}
return false;
}
changedParametersIllegal(x, y, false, false);
// Now actually handling objects. We don't
// worry about the order of the keys here.
let xkeys = Object.keys(x);
let ykeys = Object.keys(y);
if (xkeys.length != ykeys.length) {
return true;
}
xkeys.sort();
ykeys.sort();
for (var i = 0; i < xkeys.length; i++) {
if (xkeys[i] != ykeys[i]) {
return true;
}
}
for (const k of xkeys) {
if (changedParameters(x[k], y[k])) {
return true;
}
}
return false;
}
export function allocateCachedArray(size, type, cache, name = "buffer") {
var reallocate = true;
if (name in cache) {
var candidate = cache[name];
// Views also trigger reallocation, because it is assumed that the
// caller of this function does not own the view, but downstream
// uses of the array will involve writing to it.
if (candidate.size != size || candidate.constructor.className != type || candidate.owner !== null) {
candidate.free();
} else {
reallocate = false;
}
}
if (reallocate) {
switch (type) {
case "Uint8Array":
cache[name] = scran.createUint8WasmArray(size);
break;
case "Int32Array":
cache[name] = scran.createInt32WasmArray(size);
break;
case "Float64Array":
cache[name] = scran.createFloat64WasmArray(size);
break;
default:
// We only ever use one of the three above types in our
// internal data stores, so no need to go all-out here.
throw "allocating '" + type + "' not yet supported";
}
}
return cache[name];
}
export function findValidUpstreamStates(states, msg) {
let to_use = [];
for (const [k, v] of Object.entries(states)) {
if (v.valid()) {
to_use.push(k);
}
}
if (to_use.length == 0) {
throw new Error("expected at least one valid upstream " + msg + " state");
}
return to_use;
}
export function checkIndices(indices, max) {
if (max !== null) {
for (const i of indices) {
if (i < 0 || i >= max) {
throw new Error("subset indices are out of range");
}
}
}
for (var i = 1; i < indices.length; i++) {
if (indices[i] <= indices[i-1]) {
throw new Error("subset indices must be sorted and unique");
}
}
}
export async function defaultDownload(url) {
let resp = await fetch(url);
if (!resp.ok) {
throw new Error("failed to fetch content at " + url + "(" + resp.status + ")");
}
return new Uint8Array(await resp.arrayBuffer());
}
export function guessFeatureTypes(genes) {
let output = { columns: {} };
let rn = genes.rowNames();
if (rn !== null) {
output.row_names = scran.guessFeatures(rn, { forceTaxonomy: true });
}
for (const key of genes.columnNames()) {
let curcol = genes.column(key);
if (curcol instanceof Array) {
output.columns[key] = scran.guessFeatures(genes.column(key), { forceTaxonomy: true });
}
}
return output;
}
export function subsetInvalidFactors(arrays) {
let N = arrays[0].length;
let output = { arrays: [], retain: null };
let invalid = new Uint8Array(N);
invalid.fill(0);
for (const x of arrays) {
let transformed = scran.convertToFactor(x, { action: "none", placeholder: -1 });
output.arrays.push(transformed);
transformed.ids.forEach((y, i) => {
if (y == -1) {
invalid[i] = 1;
}
});
}
let num_invalid = 0;
invalid.forEach(y => { num_invalid += y; });
if (num_invalid == 0) {
return output;
}
let retain = new Int32Array(N - num_invalid);
{
let counter = 0;
for (var i = 0; i < N; i++) {
if (invalid[i] == 0) {
retain[counter] = i;
counter++;
}
}
}
output.retain = retain;
for (var i = 0; i < output.arrays.length; i++) {
let x = output.arrays[i];
let new_x = scran.subsetFactor(x, retain);
scran.free(x.ids);
x.ids = new_x.ids;
x.levels = new_x.levels;
}
return output;
}