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.

266 lines (230 loc) 10.7 kB
import * as scran from "scran.js"; import * as utils from "./utils/general.js"; import * as inputs_module from "./inputs.js"; export const step_name = "adt_quality_control"; /** * Results of computing per-cell ADT-derived QC metrics, * see [here](https://kanaverse.github.io/scran.js/PerCellAdtQcMetricsResults.html) for details. * * @external PerCellAdtQcMetricsResults */ /** * Suggested filters for the ADT-derived QC metrics, * see [here](https://kanaverse.github.io/scran.js/SuggestAdtQcFiltersResults.html) for details. * * @external SuggestAdtQcFiltersResults */ /** * This step applies quality control on the ADT count matrix. * Specifically, it computes the QC metrics and filtering thresholds, * wrapping the [`perCellAdtQcMetrics`](https://kanaverse.github.io/scran.js/global.html#perCellAdtQcMetrics) * and [`suggestAdtQcFilters`](https://kanaverse.github.io/scran.js/global.html#suggestAdtQcFilters) functions * from [**scran.js**](https://github.com/kanaverse/scran.js). * Note that the actual filtering is done by {@linkplain CellFilteringState}. * * Methods not documented here are not part of the stable API and should not be used by applications. * @hideconstructor */ export class AdtQualityControlState { #inputs; #cache; #parameters; constructor(inputs, parameters = null, cache = null) { if (!(inputs instanceof inputs_module.InputsState)) { throw new Error("'inputs' should be a State object from './inputs.js'"); } this.#inputs = inputs; this.#parameters = (parameters === null ? {} : parameters); this.#cache = (cache === null ? {} : cache); this.changed = false; } free() { utils.freeCache(this.#cache.metrics); utils.freeCache(this.#cache.filters); utils.freeCache(this.#cache.metrics_buffer); utils.freeCache(this.#cache.keep_buffer); } /*************************** ******** Getters ********** ***************************/ valid() { let input = this.#inputs.fetchCountMatrix(); return input.has("ADT"); } /** * @return {object} Object containing the parameters. */ fetchParameters() { return { ...this.#parameters }; // avoid pass-by-reference links. } /** * @return {external:SuggestAdtQcFiltersResults} Result of filtering on the ADT-derived QC metrics. * This is available after running {@linkcode AdtQualityControlState#compute compute}. */ fetchFilters() { return this.#cache.filters; } /** * @return {Uint8WasmArray} Buffer containing a vector of length equal to the number of cells, * where each element is truthy if the corresponding cell is to be retained after filtering. * This is available after running {@linkcode AdtQualityControlState#compute compute}. */ fetchKeep() { return this.#cache.keep_buffer; } /** * @return {external:PerCellAdtQcMetricsResults} ADT-derived QC metrics, * available after running {@linkcode AdtQualityControlState#compute compute}. */ fetchMetrics() { return this.#cache.metrics; } /**************************** ******** Defaults ********** ****************************/ /** * @return {object} Object containing default parameters, * see the `parameters` argument in {@linkcode AdtQualityControlState#compute compute} for details. */ static defaults() { return { guess_ids: true, tag_id_column: null, igg_prefix: "IgG", filter_strategy: "automatic", nmads: 3, min_detected_drop: 0.1, detected_threshold: 0, igg_threshold: 1 }; } static #configureFeatureParameters(lower_igg, annotations) { let counter = val => { let n = 0; val.forEach(x => { if (x.toLowerCase().startsWith(lower_igg)) { n++; } }); return n; }; let best_key = null; let best = 0; let rn = annotations.rowNames(); if (rn !== null) { best = counter(rn); } for (const key of annotations.columnNames()) { let latest = counter(annotations.column(key)); if (latest > best) { best_key = key; best = latest; } } return best_key; } /*************************** ******** Compute ********** ***************************/ /** * This method should not be called directly by users, but is instead invoked by {@linkcode runAnalysis}. * * @param {object} parameters - Parameter object, equivalent to the `adt_quality_control` property of the `parameters` of {@linkcode runAnalysis}. * @param {boolean} [parameters.guess_ids] - Automatically choose feature-based parameters based on the feature annotations. * Specifically, `tag_id_column` is set to the column with the most matches to `igg_prefix`. * @param {?(string|number)} [parameters.tag_id_column] - Name or index of the column of the feature annotations that contains the tag identifiers. * If `null`, the row names are used. * Ignored if `guess_ids = true`. * @param {?string} [parameters.igg_prefix] - Prefix of the identifiers for isotype controls. * If `null`, no prefix-based identification is performed. * @param {string} [parameters.filter_strategy] - Strategy for defining a filter threshold for the QC metrics. * This can be `"automatic"` or `"manual"`. * @param {number} [parameters.nmads] - Number of MADs to use for automatically selecting the filter threshold for each metric. * Only used when `filter_strategy = "automatic"`. * @param {number} [parameters.min_detected_drop] - Minimum proportional drop in the number of detected features before a cell is to be considered low-quality. * Only used when `filter_strategy = "automatic"`. * @param {number} [parameters.detected_threshold] - Manual threshold on the detected number of features for each cell. * Cells are only retained if the detected number is equal to or greater than this threshold. * Only used when `filter_strategy = "manual"`. * @param {number} [parameters.igg_threshold] - Manual threshold on the isotype control totals for each cell. * Cells are only retained if their totals are less than or equal to this threshold. * Only used when `filter_strategy = "manual"`. * * @return The object is updated with the new results. */ compute(parameters) { parameters = utils.defaultizeParameters(parameters, AdtQualityControlState.defaults(), [ "automatic" ]); this.changed = false; // Some back-compatibility here. if (typeof parameters.guess_ids === "undefined") { if ("automatic" in parameters) { parameters.guess_ids = parameters.automatic; } else { parameters.guess_ids = true; } } if ( this.#inputs.changed || parameters.guess_ids !== this.#parameters.guess_ids || parameters.igg_prefix !== this.#parameters.igg_prefix || (!parameters.guess_ids && parameters.tag_id_column !== this.#parameters.tag_id_column) ) { utils.freeCache(this.#cache.metrics); if (this.valid()) { var tag_info = this.#inputs.fetchFeatureAnnotations()["ADT"]; var subsets = utils.allocateCachedArray(tag_info.numberOfRows(), "Uint8Array", this.#cache, "metrics_buffer"); subsets.fill(0); if (parameters.igg_prefix !== null) { var lower_igg = parameters.igg_prefix.toLowerCase(); let key = parameters.tag_id_column; if (parameters.guess_ids) { key = AdtQualityControlState.#configureFeatureParameters(lower_igg, tag_info); } let val = (key == null ? tag_info.rowNames() : tag_info.column(key)); if (val !== null) { var sub_arr = subsets.array(); val.forEach((x, i) => { if (x.toLowerCase().startsWith(lower_igg)) { sub_arr[i] = 1; } }); } } var mat = this.#inputs.fetchCountMatrix().get("ADT"); this.#cache.metrics = scran.perCellAdtQcMetrics(mat, [subsets]); this.changed = true; } else { delete this.#cache.metrics; } } if (this.changed || parameters.filter_strategy !== this.#parameters.filter_strategy || parameters.nmads !== this.#parameters.nmads || parameters.min_detected_drop !== this.#parameters.min_detected_drop || parameters.detected_threshold !== this.#parameters.detected_threshold || parameters.igg_threshold !== this.#parameters.igg_threshold ) { utils.freeCache(this.#cache.filters); if (this.valid()) { let block = this.#inputs.fetchBlock(); if (parameters.filter_strategy === "automatic") { this.#cache.filters = scran.suggestAdtQcFilters(this.#cache.metrics, { numberOfMADs: parameters.nmads, block: block }); } else if (parameters.filter_strategy === "manual") { let block_levels = this.#inputs.fetchBlockLevels(); this.#cache.filters = scran.emptySuggestAdtQcFiltersResults(1, block_levels === null ? 1 : block_levels.length); this.#cache.filters.detected({ copy: false }).fill(parameters.detected_threshold); this.#cache.filters.subsetSum(0, { copy: false }).fill(parameters.igg_threshold); } else { throw new Error("unknown ADT QC filtering strategy '" + filter_strategy + "'"); } var keep = utils.allocateCachedArray(this.#cache.metrics.numberOfCells(), "Uint8Array", this.#cache, "keep_buffer"); this.#cache.filters.filter(this.#cache.metrics, { block: block, buffer: keep }); this.changed = true; } else { delete this.#cache.filters; } } this.#parameters = parameters; return; } }