defaults.js

import * as inputs from "./steps/inputs.js";
import * as qc from "./steps/rna_quality_control.js";
import * as qcadt from "./steps/adt_quality_control.js";
import * as qccrispr from "./steps/crispr_quality_control.js";
import * as filter from "./steps/cell_filtering.js";
import * as norm from "./steps/rna_normalization.js";
import * as normadt from "./steps/adt_normalization.js";
import * as normcrispr from "./steps/crispr_normalization.js";
import * as pca from "./steps/rna_pca.js";
import * as pcaadt from "./steps/adt_pca.js";
import * as pcacrispr from "./steps/crispr_pca.js";
import * as combine from "./steps/combine_embeddings.js";
import * as correct from "./steps/batch_correction.js";
import * as index from "./steps/neighbor_index.js";
import * as snngraph from "./steps/snn_graph_cluster.js";
import * as markers from "./steps/marker_detection.js";
import * as custom from "./steps/custom_selections.js";
import * as enrichment from "./steps/feature_set_enrichment.js";
import * as labelling from "./steps/cell_labelling.js";

/**
 * Generate an object containing all of the default analysis parameters.
 *
 * @return An object where each property corresponds to an analysis step and contains the default parameters for that step.
 * See the documentation for each step's `compute` method for more details:
 * 
 * - {@linkcode InputsState#compute inputs}
 * - {@linkcode RnaQualityControlState#compute rna_quality_control}
 * - {@linkcode AdtQualityControlState#compute adt_quality_control}
 * - {@linkcode CrisprQualityControlState#compute crispr_quality_control}
 * - {@linkcode CellFiltering#compute cell_filtering}
 * - {@linkcode RnaNormalizationState#compute rna_normalization}
 * - {@linkcode AdtNormalizationState#compute adt_normalization}
 * - {@linkcode CrisprNormalizationState#compute crispr_normalization}
 * - {@linkcode FeatureSelectionState#compute feature_selection}
 * - {@linkcode RnaPcaState#compute rna_pca}
 * - {@linkcode AdtPcaState#compute adt_pca}
 * - {@linkcode CrisprPcaState#compute crispr_pca}
 * - {@linkcode NeighborIndexState#compute neighbor_index}
 * - {@linkcode TsneState#compute tsne}
 * - {@linkcode UmapState#compute umap}
 * - {@linkcode KmeansClusterState#compute kmeans_cluster}
 * - {@linkcode SnnGraphClusterState#compute snn_graph_cluster}
 * - {@linkcode ChooseClusteringState#compute choose_clustering}
 * - {@linkcode CellLabellingState#compute cell_labelling}
 * - {@linkcode FeatureSetEnrichmentState#compute feature_set_enrichment}
 *
 * See also {@linkcode configureBatchCorrection} and {@linkcode configureApproximateNeighbors} to synchronize certain parameter settings across multiple steps.
 */
export function analysisDefaults() {
    var output = {
        feature_selection: {
            span: 0.3
        },
        combine_embeddings: {
            weights: null
        },
        batch_correction: {
            method: "none"
        },
        tsne: {
            perplexity: 30,
            iterations: 500,
            animate: false
        },
        umap: {
            num_neighbors: 15,
            num_epochs: 500,
            min_dist: 0.1,
            animate: false
        },
        kmeans_cluster: {
            k: 10
        },
        choose_clustering: {
            method: "snn_graph"
        }
    };

    output[inputs.step_name] = inputs.InputsState.defaults();

    output[qc.step_name] = qc.RnaQualityControlState.defaults();
    output[qcadt.step_name] = qcadt.AdtQualityControlState.defaults();
    output[qccrispr.step_name] = qccrispr.CrisprQualityControlState.defaults();
    output[filter.step_name] = filter.CellFilteringState.defaults();

    output[norm.step_name] = norm.RnaNormalizationState.defaults();
    output[normadt.step_name] = normadt.AdtNormalizationState.defaults();
    output[normcrispr.step_name] = normcrispr.CrisprNormalizationState.defaults();

    output[pca.step_name] = pca.RnaPcaState.defaults();
    output[pcaadt.step_name] = pcaadt.AdtPcaState.defaults();
    output[pcacrispr.step_name] = pcacrispr.CrisprPcaState.defaults();

    output[combine.step_name] = combine.CombineEmbeddingsState.defaults();
    output[correct.step_name] = correct.BatchCorrectionState.defaults();

    output[index.step_name] = index.NeighborIndexState.defaults();
    output[snngraph.step_name] = snngraph.SnnGraphClusterState.defaults();

    output[markers.step_name] = markers.MarkerDetectionState.defaults();
    output[custom.step_name] = custom.CustomSelectionsState.defaults();

    output[enrichment.step_name] = enrichment.FeatureSetEnrichmentState.defaults();
    output[labelling.step_name] = labelling.CellLabellingState.defaults();

    return output;
}

const correctible_pca_steps = [pca.step_name, pcaadt.step_name, pcacrispr.step_name];

/**
 * Set the batch correction parameters across multiple steps.
 * This is a convenient helper as the correction process is split across the PCA and batch correction steps.
 * For MNN, we project cells onto rotation vectors computed within each batch in the various PCA steps (e.g., {@linkplain PcaState}) before performing MNN correction in {@linkplain BatchCorrectionState};
 * for linear regression, we need to regress by block in the PCA without any additional correction in {@linkplain BatchCorrectionState};
 * and for no correction, we need to turn off any block handling in the PCA as well as removing any additional correction in {@linkplain BatchCorrectionState}.
 *
 * @param {object} parameters Object containing parameters for all steps, e.g., from {@linkcode analysisDefaults}.
 * @param {string} method Correction method to perform, one of `"mnn"`, "`regress"` or `"none"`.
 * 
 * @return `parameters` is modified with appropriate parameters in `batch_correction`, `pca` and `adt_pca`.
 */
export function configureBatchCorrection(parameters, method) {
    let correct_method;
    let pca_blocker;

    if (method == "mnn") {
        correct_method = method;
        pca_blocker = "project";
    } else if (method == "regress") {
        correct_method = "none";
        pca_blocker = method;
    } else if (method == "none") {
        correct_method = method;
        pca_blocker = method;
    } else {
        throw new Error("unknown correction method '" + method + "'");
    }

    parameters[correct.step_name].method = correct_method;
    for (const x of correctible_pca_steps) {
        parameters[x].block_method = pca_blocker;
    }

    return parameters;
}

/**
 * Guess the `method` value from {@linkcode configureBatchCorrection} based on the parameter object.
 * This effectively consolidates the various correction parameters into a single setting.
 *
 * @param {object} parameters - Object containing parameters for all steps, typically after {@linkcode configureBatchCorrection}.
 * @param {object} [options] - Optional parameters.
 * @param {boolean} [options.strict] - Whether to only report the `method` when the set of parameters modified by {@linkcode configureBatchCorrection} are consistent.
 *
 * @return {?string} One of `"mnn"`, `"regress"` or `"none"`, based on the expected set of modifications from {@linkcode configureBatchCorrection}.
 * If `strict = false` and there is no exact match to the expected set, the most appropriate method is returned;
 * otherwise, if `strict = true`, `null` is returned.
 */
export function guessBatchCorrectionConfig(parameters, { strict = false } = {}) {
    let pca_blockers = new Set(correctible_pca_steps.map(x => parameters[x].block_method));

    let resp;
    if (parameters[correct.step_name].method == "mnn") {
        resp = "mnn";
        if (strict) {
            if (pca_blockers.size > 1 || !pca_blockers.has("project")) {
                resp = null;
            }
        }
    } else {
        if (pca_blockers.has("regress")) {
            if (strict && pca_blockers.size > 1) {
                resp = null;
            } else {
                resp = "regress";
            }
        } else if (pca_blockers.has("none")) {
            if (strict && pca_blockers.size > 1) {
                resp = null;
            } else {
                resp = "none";
            }
        } else {
            // If pca block_methods are set to 'project',
            // this doesn't really correspond to anything,
            // but is closest to 'none'.
            if (strict) {
                resp = null;
            } else {
                resp = "none";
            }
        }
    }

    return resp;
}

const approximatable_steps = [correct.step_name, combine.step_name, index.step_name];

/**
 * Specify whether approximate neighbor searches should be performed across all affected steps.
 * This is a convenient helper as it is generally unnecessary to switch between exact and approximate searches in different steps.
 * Affected steps are {@linkplain BatchCorrectionState}, {@linkplain CombineEmbeddingsState} and {@linkplain NeighborIndexState}.
 *
 * @param {object} parameters Object containing parameters for all steps, e.g., from {@linkcode analysisDefaults}.
 * @param {boolean} approximate Whether to perform approximate nearest neighbor searces.
 * 
 * @return `parameters` is modified with appropriate parameters in relevant steps, e.g., `batch_correction`, `combine_embeddings` and `neighbor_index`.
 */
export function configureApproximateNeighbors(parameters, approximate) {
    for (const step of approximatable_steps) {
        parameters[step].approximate = approximate;
    }
    return parameters;
}

/**
 * Guess the value of `approximate` from {@linkcode configureApproximateNeighbors} based on the parameter object.
 * This effectively consolidates the various approximation parameters into a single setting.
 *
 * @param {object} parameters - Object containing parameters for all steps, typically after {@linkcode configureApproximateNeighbors}.
 * @param {object} [options] - Optional parameters.
 * @param {boolean} [options.strict] - Whether to only report `approximate` when the set of parameters modified by {@linkcode configureApproximateNeighbors} are consistent.
 *
 * @return {?boolean} Whether or not approximate neighbor search was used.
 * If `strict = false` and there is a mixture of approximate and exact matches, an approximate search is reported;
 * otherwise, if `strict = true`, `null` is returned.
 */
export function guessApproximateNeighborsConfig(parameters, { strict = false } = {}) {
    let approximates = new Set(approximatable_steps.map(x => parameters[x].approximate));
    if (strict && approximates.size > 1) {
        return null;
    } else {
        return approximates.has(true);
    }
}