Select highly variable genes based on dispersion scores and expression filters. Final step of HVG detection that applies thresholds to identify the most biologically relevant genes. Supports both top-N selection and threshold-based selection. ## Parameters `log_means` - Log-transformed mean expression values `dispersion_norm` - Normalized dispersion scores `n_top_genes` - Optional: select this m
(
log_means: &[f64], // These are already log-transformed
dispersion_norm: &[f64],
n_top_genes: Option<usize>,
min_mean: f64,
max_mean: f64,
min_dispersion: f64,
)
| 402 | /// - Otherwise: Apply all threshold filters (mean and dispersion) |
| 403 | /// - Genes with NaN dispersions are handled appropriately for each mode |
| 404 | fn subset_genes( |
| 405 | log_means: &[f64], // These are already log-transformed |
| 406 | dispersion_norm: &[f64], |
| 407 | n_top_genes: Option<usize>, |
| 408 | min_mean: f64, |
| 409 | max_mean: f64, |
| 410 | min_dispersion: f64, |
| 411 | ) -> anyhow::Result<Vec<bool>> { |
| 412 | let mut highly_variable = vec![false; log_means.len()]; |
| 413 | |
| 414 | if let Some(n_top) = n_top_genes { |
| 415 | // Python's approach for n_top_genes: |
| 416 | // 1. First, remove NaN values to compute threshold |
| 417 | let non_nan_dispersions: Vec<f64> = dispersion_norm |
| 418 | .iter() |
| 419 | .filter(|&&d| !d.is_nan()) |
| 420 | .copied() |
| 421 | .collect(); |
| 422 | |
| 423 | if non_nan_dispersions.is_empty() { |
| 424 | return Ok(highly_variable); |
| 425 | } |
| 426 | |
| 427 | // Find the nth highest value |
| 428 | let n_to_select = n_top.min(non_nan_dispersions.len()); |
| 429 | let mut sorted_dispersions = non_nan_dispersions.clone(); |
| 430 | sorted_dispersions.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)); |
| 431 | |
| 432 | let threshold = sorted_dispersions[n_to_select - 1]; |
| 433 | |
| 434 | // 2. Now apply threshold to nan_to_num version (NaN → -inf) |
| 435 | for i in 0..dispersion_norm.len() { |
| 436 | let disp_value = if dispersion_norm[i].is_nan() { |
| 437 | f64::NEG_INFINITY // Python uses -inf for NaN in final selection |
| 438 | } else { |
| 439 | dispersion_norm[i] |
| 440 | }; |
| 441 | |
| 442 | if disp_value >= threshold { |
| 443 | highly_variable[i] = true; |
| 444 | } |
| 445 | } |
| 446 | } else { |
| 447 | // Original cutoff-based selection |
| 448 | // Python applies nan_to_num (NaN → 0) before checking bounds |
| 449 | let clean_dispersions: Vec<f64> = dispersion_norm |
| 450 | .iter() |
| 451 | .map(|&d| if d.is_nan() { 0.0 } else { d }) |
| 452 | .collect(); |
| 453 | |
| 454 | // Apply mean filters |
| 455 | let valid_by_mean: Vec<bool> = log_means |
| 456 | .iter() |
| 457 | .map(|&log_mean| log_mean > min_mean && log_mean < max_mean) |
| 458 | .collect(); |
| 459 | |
| 460 | for i in 0..log_means.len() { |
| 461 | highly_variable[i] = valid_by_mean[i] && clean_dispersions[i] > min_dispersion; |
no test coverage detected