steps/crispr_quality_control.js

import * as scran from "scran.js"; 
import * as utils from "./utils/general.js";
import * as inputs_module from "./inputs.js";

export const step_name = "crispr_quality_control";

/**
 * Results of computing per-cell CRISPR-derived QC metrics,
 * see [here](https://kanaverse.github.io/scran.js/PerCellCrisprQcMetricsResults.html) for details.
 *
 * @external PerCellCrisprQcMetricsResults
 */

/**
 * Suggested filters for the CRISPR-derived QC metrics,
 * see [here](https://kanaverse.github.io/scran.js/SuggestCrisprQcFiltersResults.html) for details.
 *
 * @external SuggestCrisprQcFiltersResults
 */

/**
 * This step applies quality control on the CRISPR guide count matrix.
 * Specifically, it computes the QC metrics and filtering thresholds, 
 * wrapping the [`perCellCrisprQcMetrics`](https://kanaverse.github.io/scran.js/global.html#perCellCrisprQcMetrics)
 * and [`suggestCrisprQcFilters`](https://kanaverse.github.io/scran.js/global.html#suggestCrisprQcFilters) 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 CrisprQualityControlState {
    #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.discard_buffer);
    }

    /***************************
     ******** Getters **********
     ***************************/

    valid() {
        let input = this.#inputs.fetchCountMatrix();
        return input.has("CRISPR");
    }

    /**
     * @return {object} Object containing the parameters.
     */
    fetchParameters() {
        return { ...this.#parameters }; // avoid pass-by-reference links.
    }

    /**
     * @return {external:SuggestCrisprQcFiltersResults} Result of filtering on the CRISPR-derived QC metrics.
     * This is available after running {@linkcode CrisprQualityControlState#compute compute}.
     */
    fetchFilters() {
        return this.#cache.filters;
    }

    /**
     * @return {Uint8WasmArray} Buffer containing the discard vector of length equal to the number of cells,
     * where each element is truthy if the corresponding cell is to be discarded.
     * This is available after running {@linkcode CrisprQualityControlState#compute compute}.
     */
    fetchDiscards() {
        return this.#cache.discard_buffer;
    }

    /**
     * @return {external:PerCellCrisprQcMetricsResults} CRISPR-derived QC metrics,
     * available after running {@linkcode CrisprQualityControlState#compute compute}.
     */
    fetchMetrics() {
        return this.#cache.metrics;
    }

    /***************************
     ******** Compute **********
     ***************************/

    static defaults() {
        return {
            filter_strategy: "automatic",
            nmads: 3,

            max_threshold: 0
        };
    }

    /**
     * This method should not be called directly by users, but is instead invoked by {@linkcode runAnalysis}.
     *
     * @param {object} parameters - Parameter object, equivalent to the `crispr_quality_control` property of the `parameters` of {@linkcode runAnalysis}.
     * @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 on the maximum count. 
     * Only used when `filter_strategy = "automatic"`.
     * @param {number} parameters.max_threshold - Manual threshold on the maximum count for each cell.
     * Cells are only retained if their maximums are greater than or equal to this threshold.
     * Only used when `filter_strategy = "manual"`.
     *
     * @return The object is updated with the new results.
     */
    compute(parameters) {
        let { filter_strategy, nmads, max_threshold } = parameters;
        this.changed = false;

        if (this.#inputs.changed) {
            utils.freeCache(this.#cache.metrics);

            if (this.valid()) {
                var mat = this.#inputs.fetchCountMatrix().get("CRISPR");
                this.#cache.metrics = scran.perCellCrisprQcMetrics(mat);
                this.changed = true;
            } else {
                delete this.#cache.metrics;
            }
        }

        if (this.changed || 
            filter_strategy !== this.#parameters.filter_strategy ||
            nmads !== this.#parameters.nmads ||
            max_threshold !== this.#parameters.max_threshold
        ) {
            utils.freeCache(this.#cache.filters);

            if (this.valid()) {
                let block = this.#inputs.fetchBlock();

                if (filter_strategy === "automatic") {
                    this.#cache.filters = scran.suggestCrisprQcFilters(this.#cache.metrics, { numberOfMADs: nmads, block: block });
                } else if (filter_strategy === "manual") {
                    let block_levels = this.#inputs.fetchBlockLevels();
                    this.#cache.filters = scran.emptySuggestCrisprQcFiltersResults(block_levels === null ? 1 : block_levels.length);
                    this.#cache.filters.thresholdsMaxCount({ copy: false }).fill(max_threshold);
                } else {
                    throw new Error("unknown CRISPR QC filter strategy '" + filter_strategy + "'");
                }

                var discard = utils.allocateCachedArray(this.#cache.metrics.numberOfCells(), "Uint8Array", this.#cache, "discard_buffer");
                this.#cache.filters.filter(this.#cache.metrics, { block: block, buffer: discard });
                this.changed = true;
            } else {
                delete this.#cache.filters;
            }

            this.#parameters.filter_strategy = filter_strategy;
            this.#parameters.nmads = nmads;
            this.#parameters.max_threshold = max_threshold;
        }

        return;
    }
}