UNPKG

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.

205 lines (175 loc) 7.35 kB
import * as scran from "scran.js"; import * as vizutils from "./utils/viz_parent.js"; import * as utils from "./utils/general.js"; import * as neighbor_module from "./neighbor_index.js"; import * as aworkers from "./abstract/worker_parent.js"; export const step_name = "umap"; /** * This creates a UMAP embedding based on the neighbor index constructed at {@linkplain NeighborIndexState}. * This wraps [`runUMAP`](https://kanaverse.github.io/scran.js/global.html#runUMAP) * and related functions from [**scran.js**](https://github.com/kanaverse/scran.js). * * Methods not documented here are not part of the stable API and should not be used by applications. * @hideconstructor */ export class UmapState { #index; #parameters; #reloaded; #worker_id; #ready; #run; constructor(index, parameters = null, reloaded = null) { if (!(index instanceof neighbor_module.NeighborIndexState)) { throw new Error("'index' should be a State object from './neighbor_index.js'"); } this.#index = index; this.#parameters = (parameters === null ? {} : parameters); this.#reloaded = reloaded; this.changed = false; let worker = aworkers.createUmapWorker(); let { worker_id, ready } = vizutils.initializeWorker(worker, vizutils.scranOptions); this.#worker_id = worker_id; this.#ready = ready; this.#run = null; } ready() { // It is assumed that the caller will await the ready() // status before calling any other methods of this instance. return this.#ready; } free() { return vizutils.killWorker(this.#worker_id); } /*************************** ******** Getters ********** ***************************/ /** * @param {object} [options={}] - Optional parameters. * @param {boolean} [options.copy=true] - Whether to create a copy of the coordinates, * if the caller might mutate them. * * @return {object} Object containing: * * - `x`: a Float64Array containing the x-coordinate for each cell. * - `y`: a Float64Array containing the y-coordinate for each cell. * - `iterations`: the number of iterations processed. * * @async */ async fetchResults({ copy = true } = {}) { if (this.#reloaded !== null) { let output = { x: this.#reloaded.x, y: this.#reloaded.y }; if (copy) { output.x = output.x.slice(); output.y = output.y.slice(); } output.iterations = this.#parameters.num_epochs; return output; } else { // Vectors that we get from the worker are inherently // copied, so no need to do anything extra here. await this.#run; return vizutils.sendTask(this.#worker_id, { "cmd": "FETCH" }); } } /** * @return {object} Object containing the parameters. */ fetchParameters() { return { ...this.#parameters }; // avoid pass-by-reference links. } /*************************** ******** Compute ********** ***************************/ /** * @return {object} Object containing default parameters, * see the `parameters` argument in {@linkcode UmapState#compute compute} for details. */ static defaults() { return { num_neighbors: 15, num_epochs: 500, min_dist: 0.1, animate: false }; } #core(num_neighbors, num_epochs, min_dist, animate, reneighbor) { var nn_out = null; if (reneighbor) { nn_out = vizutils.computeNeighbors(this.#index, num_neighbors); } let args = { "num_neighbors": num_neighbors, "num_epochs": num_epochs, "min_dist": min_dist, "animate": animate }; // This returns a promise but the message itself is sent synchronously, // which is important to ensure that the UMAP runs in its worker in // parallel with other analysis steps. Do NOT put the runWithNeighbors // call in a .then() as this may defer the message sending until // the current thread is completely done processing. this.#run = vizutils.runWithNeighbors(this.#worker_id, args, nn_out); return; } /** * This method should not be called directly by users, but is instead invoked by {@linkcode runAnalysis}. * * @param {object} parameters - Parameter object, equivalent to the `umap` property of the `parameters` of {@linkcode runAnalysis}. * @param {number} [parameters.num_neighbors] - Number of neighbors to use to construct the simplicial sets. * @param {number} [parameters.num_epochs] - Number of epochs to run the algorithm. * @param {number} [parameters.min_dist] - Number specifying the minimum distance between points. * @param {boolean} [parameters.animate] - Whether to process animation iterations, see {@linkcode setVisualizationAnimate} for details. * * @return UMAP coordinates are computed in parallel on a separate worker thread. * A promise that resolves when the calculations are complete. */ compute(parameters) { parameters = utils.defaultizeParameters(parameters, UmapState.defaults()); let same_neighbors = (!this.#index.changed && parameters.num_neighbors === this.#parameters.num_neighbors); if (same_neighbors && parameters.num_epochs === this.#parameters.num_epochs && parameters.min_dist === this.#parameters.min_dist) { this.changed = false; return new Promise(resolve => resolve(null)); } // In the reloaded state, we must send the neighbor // information, because it hasn't ever been sent before. if (this.#reloaded !== null) { same_neighbors = false; this.#reloaded = null; } this.#core(parameters.num_neighbors, parameters.num_epochs, parameters.min_dist, parameters.animate, !same_neighbors); this.#parameters = parameters; this.changed = true; return this.#run; } /*************************** ******** Getters ********** ***************************/ /** * Repeat the animation iterations. * It is assumed that {@linkcode setVisualizationAnimate} has been set appropriately to process each iteration. * * @return A promise that resolves on successful completion of all iterations. */ animate() { if (this.#reloaded !== null) { this.#reloaded = null; // We need to reneighbor because we haven't sent the neighbors across yet. this.#core(this.#parameters.num_neighbors, this.#parameters.num_epochs, this.#parameters.min_dist, true, true); // Mimicking the response from the re-run. return this.#run .then(contents => { return { "type": "umap_rerun", "data": { "status": "SUCCESS" } }; }); } else { return vizutils.sendTask(this.#worker_id, { "cmd": "RERUN" }); } } }