use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::Arc;
use pluot_core::cache::{use_memo_numeric_data, use_memo_vec_f32, use_memo_vec_string};
use pluot_core::log;
use pluot_core::numeric_data::NumericData;
use pluot_core::zarr::is_timed_out_zarrs_error;
use zarrs::array::ArrayError;
use zarrs::storage::AsyncReadableStorageTraits;
use crate::adata_io::{read_dataframe_index, read_encoding, read_matrix_column_f32, read_string_array};
use crate::adata_metadata::AnnDataEncoding;
use crate::zarr_numeric_data::load_arr_as_numeric_data;
pub type AdataStore = Arc<dyn AsyncReadableStorageTraits>;
#[derive(Clone, Debug, Default, PartialEq)]
pub struct GeneSummary {
pub var_colname: String,
pub var_name: String,
pub obs_colname: String,
pub obs_value: String,
pub sum: f32,
pub cell_count: u32,
pub num_expressing: u32,
}
impl GeneSummary {
pub fn mean_and_fraction_expressing(&self) -> (f32, f32) {
if self.cell_count > 0 {
(self.sum / self.cell_count as f32, self.num_expressing as f32 / self.cell_count as f32)
} else {
(0.0, 0.0)
}
}
}
thread_local! {
static USE_MEMO_CACHE_GENE_SUMMARY: RefCell<Option<HashMap<Vec<String>, Arc<GeneSummary>>>> = const { RefCell::new(None) };
}
async fn use_memo_gene_summary(initializer: impl AsyncFnOnce() -> GeneSummary, keys: &[String], cache_enabled: bool) -> Arc<GeneSummary> {
if !cache_enabled {
return Arc::new(initializer().await);
}
let cached = USE_MEMO_CACHE_GENE_SUMMARY.with(|cache| cache.borrow().as_ref().and_then(|map| map.get(keys).cloned()));
if let Some(summary) = cached {
return summary;
}
let summary = Arc::new(initializer().await);
USE_MEMO_CACHE_GENE_SUMMARY.with(|cache| {
cache.borrow_mut().get_or_insert_with(HashMap::new).insert(keys.to_vec(), summary.clone());
});
summary
}
pub async fn load_var_names(store: AdataStore, store_name: &str, var_column: Option<&str>, cache_enabled: bool) -> Result<Arc<Vec<String>>, ArrayError> {
let keys = vec!["dotplot_var_names".to_string(), store_name.to_string(), var_column.unwrap_or("index").to_string()];
use_memo_vec_string(
async || match var_column {
None => read_dataframe_index(store.clone(), "/var").await,
Some(column) => {
let column_path = format!("/var/{column}");
let values_path = match read_encoding(store.clone(), &column_path).await {
AnnDataEncoding::NullableStringArray { .. } => format!("{column_path}/values"),
AnnDataEncoding::StringArray { .. } => column_path,
other => panic!("Unsupported var column encoding at \"{column_path}\": {other:?}"),
};
read_string_array(store, &values_path).await
}
},
&keys,
cache_enabled,
)
.await
}
pub async fn load_obs_categorical(store: AdataStore, store_name: &str, groupby: &str, cache_enabled: bool) -> Result<(Arc<Vec<String>>, Arc<NumericData>), ArrayError> {
let column_path = format!("/obs/{groupby}");
let categories_keys = vec!["dotplot_obs_categories".to_string(), store_name.to_string(), groupby.to_string()];
let categories_future = use_memo_vec_string(
async || read_string_array(store.clone(), &format!("{column_path}/categories")).await,
&categories_keys,
cache_enabled,
);
let codes_keys = vec!["dotplot_obs_codes".to_string(), store_name.to_string(), groupby.to_string()];
let codes_future = use_memo_numeric_data(
async || load_arr_as_numeric_data(store.clone(), &format!("{column_path}/codes")).await,
&codes_keys,
cache_enabled,
);
futures::try_join!(categories_future, codes_future)
}
fn expr_array_path(layer: Option<&str>) -> String {
match layer {
None => "/X".to_string(),
Some(layer) => format!("/layers/{layer}"),
}
}
pub fn resolve_gene_columns(requested: &[String], var_index_values: &[String]) -> Vec<(String, u64)> {
let lookup: HashMap<&str, usize> = var_index_values.iter().enumerate().map(|(i, name)| (name.as_str(), i)).collect();
let mut resolved = Vec::new();
for gene in requested {
match lookup.get(gene.as_str()) {
Some(&index) => resolved.push((gene.clone(), index as u64)),
None => log(&format!("dotplot_data: gene \"{gene}\" not found in var names; skipping")),
}
}
resolved
}
pub fn bucket_rows_by_category(requested_categories: &[String], obs_categories: &[String], obs_codes: &NumericData) -> HashMap<String, Vec<u32>> {
let category_position: HashMap<&str, usize> = obs_categories.iter().enumerate().map(|(i, category)| (category.as_str(), i)).collect();
let mut code_to_requested: HashMap<usize, &str> = HashMap::new();
for category in requested_categories {
match category_position.get(category.as_str()) {
Some(&position) => {
code_to_requested.insert(position, category.as_str());
}
None => log(&format!("dotplot_data: obs category \"{category}\" not found; skipping")),
}
}
let mut rows_by_category: HashMap<String, Vec<u32>> = requested_categories.iter().map(|category| (category.clone(), Vec::new())).collect();
for (row, code) in obs_codes.as_f32().iter().enumerate() {
if *code < 0.0 {
continue;
}
if let Some(&category) = code_to_requested.get(&(*code as usize)) {
rows_by_category.get_mut(category).unwrap().push(row as u32);
}
}
rows_by_category
}
fn summarize_gene_category(
values: &[f32],
rows: &[u32],
expression_cutoff: f32,
var_colname: &str,
var_name: &str,
obs_colname: &str,
obs_value: &str,
) -> GeneSummary {
let mut sum = 0.0f32;
let mut num_expressing = 0u32;
for &row in rows {
let value = values[row as usize];
sum += value;
if value > expression_cutoff {
num_expressing += 1;
}
}
GeneSummary {
var_colname: var_colname.to_string(),
var_name: var_name.to_string(),
obs_colname: obs_colname.to_string(),
obs_value: obs_value.to_string(),
sum,
cell_count: rows.len() as u32,
num_expressing,
}
}
pub async fn load_gene_summaries_for_gene(
store: AdataStore,
store_name: &str,
var_colname: &str,
gene_name: &str,
col_index: u64,
layer: Option<&str>,
groupby: &str,
categories: &[String],
rows_by_category: &HashMap<String, Vec<u32>>,
expression_cutoff: f32,
cache_enabled: bool,
) -> Option<Vec<GeneSummary>> {
let array_path = expr_array_path(layer);
let expr_keys = vec![
"dotplot_expr".to_string(),
store_name.to_string(),
array_path.clone(),
var_colname.to_string(),
gene_name.to_string(),
];
let expr_values = match use_memo_vec_f32(async || read_matrix_column_f32(store.clone(), &array_path, col_index).await, &expr_keys, cache_enabled).await {
Ok(expr) => expr,
Err(error) => {
if is_timed_out_zarrs_error(&error) {
return None;
}
panic!("Zarrs error loading AnnData expression column for gene \"{gene_name}\" at \"{array_path}\": {error:?}");
}
};
let empty_rows: Vec<u32> = Vec::new();
let mut summaries = Vec::with_capacity(categories.len());
for category in categories {
let rows = rows_by_category.get(category).unwrap_or(&empty_rows);
let summary_keys = vec![
"dotplot_gene_summary".to_string(),
store_name.to_string(),
var_colname.to_string(),
gene_name.to_string(),
groupby.to_string(),
category.clone(),
expression_cutoff.to_string(),
];
let summary = use_memo_gene_summary(
async || summarize_gene_category(&expr_values, rows, expression_cutoff, var_colname, gene_name, groupby, category),
&summary_keys,
cache_enabled,
)
.await;
summaries.push((*summary).clone());
}
Some(summaries)
}
pub async fn load_gene_summaries(
store: AdataStore,
store_name: &str,
var_names: &[String],
var_column: Option<&str>,
layer: Option<&str>,
groupby: &str,
categories: &[String],
expression_cutoff: f32,
cache_enabled: bool,
) -> Result<Vec<GeneSummary>, ArrayError> {
let (var_index_values, (obs_categories, obs_codes)) = futures::try_join!(
load_var_names(store.clone(), store_name, var_column, cache_enabled),
load_obs_categorical(store.clone(), store_name, groupby, cache_enabled),
)?;
let resolved_genes = resolve_gene_columns(var_names, &var_index_values);
let rows_by_category = bucket_rows_by_category(categories, &obs_categories, &obs_codes);
let var_colname = var_column.unwrap_or("index").to_string();
let gene_futures = resolved_genes.iter().map(|(gene_name, col_index)| {
load_gene_summaries_for_gene(
store.clone(),
store_name,
&var_colname,
gene_name,
*col_index,
layer,
groupby,
categories,
&rows_by_category,
expression_cutoff,
cache_enabled,
)
});
let results = futures::future::join_all(gene_futures).await;
Ok(results.into_iter().flatten().flatten().collect())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_gene_columns_skips_genes_not_found() {
let var_index = vec!["GeneA".to_string(), "GeneB".to_string(), "GeneC".to_string()];
let requested = vec!["GeneC".to_string(), "GeneX".to_string(), "GeneA".to_string()];
let resolved = resolve_gene_columns(&requested, &var_index);
assert_eq!(resolved, vec![("GeneC".to_string(), 2), ("GeneA".to_string(), 0)]);
}
#[test]
fn bucket_rows_by_category_groups_by_requested_categories_only() {
let obs_categories = vec!["B".to_string(), "T".to_string(), "NK".to_string()];
let obs_codes = NumericData::Int32(Arc::new(vec![0, 0, 2, 1, 1, -1]));
let requested = vec!["T".to_string(), "B".to_string()];
let buckets = bucket_rows_by_category(&requested, &obs_categories, &obs_codes);
assert_eq!(buckets.get("B"), Some(&vec![0, 1]));
assert_eq!(buckets.get("T"), Some(&vec![3, 4]));
assert_eq!(buckets.len(), 2);
}
#[test]
fn bucket_rows_by_category_with_no_requested_categories_is_empty() {
let obs_categories = vec!["B".to_string(), "T".to_string()];
let obs_codes = NumericData::Int32(Arc::new(vec![0, 1]));
let buckets = bucket_rows_by_category(&[], &obs_categories, &obs_codes);
assert!(buckets.is_empty());
}
#[test]
fn bucket_rows_by_category_logs_and_skips_unknown_category() {
let obs_categories = vec!["B".to_string()];
let obs_codes = NumericData::Int32(Arc::new(vec![0, 0]));
let requested = vec!["B".to_string(), "Unknown".to_string()];
let buckets = bucket_rows_by_category(&requested, &obs_categories, &obs_codes);
assert_eq!(buckets.get("B"), Some(&vec![0, 1]));
assert_eq!(buckets.get("Unknown"), Some(&vec![]));
}
#[test]
fn summarize_gene_category_computes_sum_count_and_num_expressing() {
let values = [0.0, 1.0, 2.0, 9.0];
let summary = summarize_gene_category(&values, &[0, 1, 2], 0.0, "index", "GeneA", "celltype", "T");
assert_eq!(summary.cell_count, 3);
assert_eq!(summary.sum, 3.0);
assert_eq!(summary.num_expressing, 2);
assert_eq!(summary.var_name, "GeneA");
assert_eq!(summary.obs_value, "T");
let summary = summarize_gene_category(&values, &[3, 1], 1.5, "index", "GeneA", "celltype", "T");
assert_eq!(summary.sum, 10.0);
assert_eq!(summary.num_expressing, 1);
let summary = summarize_gene_category(&values, &[], 0.0, "index", "GeneA", "celltype", "T");
assert_eq!(summary.sum, 0.0);
assert_eq!(summary.cell_count, 0);
assert_eq!(summary.num_expressing, 0);
}
}