Skip to main content

bixverse_methods/methods/
nmf_bulk.rs

1//! Wrapper functions to run NMF on (likely) bulkRNAseq experiments of shape
2//! samples x features
3
4use faer::MatRef;
5
6use ann_search_rs::prelude::AnnSearchFloat;
7
8use crate::errors::MethodsErrors;
9use crate::methods::nmf_hals::consensus::*;
10use crate::methods::nmf_hals::dense::DenseInput;
11use crate::methods::nmf_hals::nmf_preprocessing::*;
12use crate::methods::nmf_hals::*;
13use crate::prelude::BixverseFloat;
14
15/////////////
16// Helpers //
17/////////////
18
19/// Parse the pre-processing string, warning and defaulting on unknown input.
20///
21/// ### Params
22///
23/// * `preprocessing` - String to parse, see [parse_nmf_processing].
24///
25/// ### Returns
26///
27/// The [NmfPreprocessing] to apply.
28fn resolve_preprocessing(preprocessing: &str) -> NmfPreprocessing {
29    parse_nmf_processing(preprocessing).unwrap_or_else(|| {
30        println!(
31            "Unknown string provided: {:?}. Using the default (No additional pre-processing)",
32            preprocessing
33        );
34        NmfPreprocessing::default()
35    })
36}
37
38////////////////
39// Single run //
40////////////////
41
42/// Run NMF (single run)
43///
44/// This version runs NMF on dense inputs (think bulk RNAseq experiments).
45/// Assumes samples x features.
46///
47/// ### Params
48///
49/// * `data` - The data on which to run a single NMF run. Assumes samples x
50///   features.
51/// * `k` - Number of k to return
52/// * `preprocessing` - String. Shall additional pre-processing be applied,
53///   see [parse_nmf_processing].
54/// * `nmf_hals_params` - Optional [HalsOpts] parameters.
55/// * `verbose` - If `0` -> silent or `1` for normal verbosity, `2` for detailed
56///   verbosity.
57///
58/// ### Returns
59///
60/// [NmfResult] or errors.
61pub fn nmf_single_run<T>(
62    data: MatRef<T>,
63    k: usize,
64    preprocessing: &str,
65    nmf_hals_params: Option<HalsOpts<T>>,
66    verbose: usize,
67) -> Result<NmfResult<T>, MethodsErrors>
68where
69    T: BixverseFloat,
70{
71    let hals_opt = nmf_hals_params.unwrap_or_default();
72
73    let data_processed = nmf_process_dense(data, &resolve_preprocessing(preprocessing));
74
75    let nmf_input = DenseInput::new(data_processed.as_ref())?;
76
77    let nmf_res = nmf_hals(&nmf_input, k, &hals_opt, verbose)?;
78
79    Ok(nmf_res)
80}
81
82///////////////////
83// Multiple runs //
84///////////////////
85
86/// Run NMF (multiple runs)
87///
88/// This version runs NMF on dense inputs (think bulk RNAseq experiments)
89/// across a set of random initialisations and returns the results across these
90/// runs. Assumes samples x features.
91///
92/// ### Params
93///
94/// * `data` - The data on which to run a single NMF run. Assumes samples x
95///   features.
96/// * `k` - Number of k to return
97/// * `preprocessing` - String. Shall additional pre-processing be applied,
98///   see [parse_nmf_processing].
99/// * `nmf_hals_params` - Optional [HalsOpts] parameters.
100/// * `n_runs` - Number of runs at this k
101/// * `base_seed` - The base seed.
102/// * `verbose` - If `0` -> silent or `1` for normal verbosity, `2` for detailed
103///   verbosity.
104///
105/// ### Returns
106///
107/// [StabilisedNmfResult] or errors.
108pub fn nmf_multiple_run<T>(
109    data: MatRef<T>,
110    k: usize,
111    preprocessing: &str,
112    nmf_hals_params: Option<HalsOpts<T>>,
113    n_runs: usize,
114    base_seed: usize,
115    verbose: usize,
116) -> Result<StabilisedNmfResult<T>, MethodsErrors>
117where
118    T: BixverseFloat,
119{
120    let hals_opt = nmf_hals_params.unwrap_or_default();
121
122    let data_processed = nmf_process_dense(data, &resolve_preprocessing(preprocessing));
123
124    let nmf_input = DenseInput::new(data_processed.as_ref())?;
125
126    let res = stabilised_nmf(&nmf_input, k, n_runs, base_seed as u64, &hals_opt, verbose)?;
127
128    Ok(res)
129}
130
131///////////////
132// Consensus //
133///////////////
134
135/// Run consensus NMF at a single k
136///
137/// Dense inputs (think bulk RNAseq experiments), assumes samples x features.
138/// Runs `n_runs` random restarts, clusters the pooled components and refits the
139/// partner factor against the consensus one. See [nmf_consensus] for the scale
140/// convention of the returned factors, which depends on the
141/// [ConsensusTarget].
142///
143/// ### Params
144///
145/// * `data` - The data to factorise. Assumes samples x features.
146/// * `k` - Number of components. Must be at least 2.
147/// * `preprocessing` - String. Shall additional pre-processing be applied,
148///   see [parse_nmf_processing].
149/// * `nmf_hals_params` - Optional [HalsOpts] parameters.
150/// * `consensus_params` - Optional [ConsensusParams] parameters.
151/// * `n_runs` - Number of restarts at this k. Must be at least 2.
152/// * `base_seed` - The base seed.
153/// * `verbose` - If `0` -> silent or `1` for normal verbosity, `2` for detailed
154///   verbosity.
155///
156/// ### Returns
157///
158/// [ConsensusNmfResult] or errors.
159#[allow(clippy::too_many_arguments)]
160pub fn nmf_consensus_run<T>(
161    data: MatRef<T>,
162    k: usize,
163    preprocessing: &str,
164    nmf_hals_params: Option<HalsOpts<T>>,
165    consensus_params: Option<ConsensusParams<T>>,
166    n_runs: usize,
167    base_seed: usize,
168    verbose: usize,
169) -> Result<ConsensusNmfResult<T>, MethodsErrors>
170where
171    T: BixverseFloat + AnnSearchFloat,
172{
173    let hals_opt = nmf_hals_params.unwrap_or_default();
174    let data_processed = nmf_process_dense(data, &resolve_preprocessing(preprocessing));
175    let nmf_input = DenseInput::new(data_processed.as_ref())?;
176
177    nmf_consensus(
178        &nmf_input,
179        k,
180        n_runs,
181        base_seed as u64,
182        &hals_opt,
183        consensus_params,
184        verbose,
185    )
186}
187
188/// Sweep k and report consensus stability against reconstruction error
189///
190/// Dense inputs (think bulk RNAseq experiments), assumes samples x features.
191/// Returns diagnostics only, so a wide `k_range` stays cheap. Pick the k where
192/// stability is high and the error curve has not yet flattened, then call
193/// [nmf_consensus_run] there.
194///
195/// ### Params
196///
197/// * `data` - The data to factorise. Assumes samples x features.
198/// * `k_range` - Ranks to evaluate. Must be non-empty, every entry at least 2.
199/// * `preprocessing` - String. Shall additional pre-processing be applied,
200///   see [parse_nmf_processing].
201/// * `nmf_hals_params` - Optional [HalsOpts] parameters.
202/// * `consensus_params` - Optional [ConsensusParams] parameters.
203/// * `n_runs` - Number of restarts per k. Must be at least 2.
204/// * `base_seed` - The base seed.
205/// * `verbose` - If `0` -> silent or `1` for normal verbosity, `2` for detailed
206///   verbosity.
207///
208/// ### Returns
209///
210/// One [KSweepEntry] per entry of `k_range`, or errors.
211#[allow(clippy::too_many_arguments)]
212pub fn nmf_k_sweep_run<T>(
213    data: MatRef<T>,
214    k_range: &[usize],
215    preprocessing: &str,
216    nmf_hals_params: Option<HalsOpts<T>>,
217    consensus_params: Option<ConsensusParams<T>>,
218    n_runs: usize,
219    base_seed: usize,
220    verbose: usize,
221) -> Result<Vec<KSweepEntry<T>>, MethodsErrors>
222where
223    T: BixverseFloat + AnnSearchFloat,
224{
225    let hals_opt = nmf_hals_params.unwrap_or_default();
226    let data_processed = nmf_process_dense(data, &resolve_preprocessing(preprocessing));
227    let nmf_input = DenseInput::new(data_processed.as_ref())?;
228
229    nmf_k_sweep(
230        &nmf_input,
231        k_range,
232        n_runs,
233        base_seed as u64,
234        &hals_opt,
235        consensus_params,
236        verbose,
237    )
238}